Files
JIMRI/java/test/jmri/jmrit/signalsystemeditor/configurexml/LoadAndStoreAllSignalSystemsTest.java
T
2026-06-17 14:00:51 +02:00

398 lines
18 KiB
Java

package jmri.jmrit.signalsystemeditor.configurexml;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jmri.jmrit.signalsystemeditor.*;
import jmri.util.FileUtil;
import jmri.util.JUnitUtil;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Loads and stores all the signal systems and verifies that the stored data
* is equal to the loaded data.
*
* @author Daniel Bergqvist (C) 2022
*/
public class LoadAndStoreAllSignalSystemsTest {
private final boolean REMOVE_SPACES = false;
private static String lastSignalSystem = null;
private void checkImageLinks(SignalSystem signalSystem, SignalMastType smt, List<ImageLink> imageLinks) throws IOException {
for (ImageLink link : imageLinks) {
Assertions.assertFalse( link.getImageLink().startsWith("http:"),
String.format("Signal system: %s, Signal mast: %s, File %s is a http link%n",
signalSystem.getFolderName(), smt.getFileName(), link.getImageLink()));
File file = new File("xml/signals" + link.getImageLink());
Assertions.assertTrue( file.getCanonicalFile().exists(),
String.format("Signal system: %s, Signal mast: %s, File %s does not exists%n",
signalSystem.getFolderName(), smt.getFileName(), file.getCanonicalPath()));
}
}
private void checkImageLinks(SignalSystem signalSystem) throws IOException {
for (SignalMastType smt : signalSystem.getSignalMastTypes()) {
for (Appearance appearance : smt.getAppearances()) {
checkImageLinks(signalSystem, smt, appearance.getImageLinks());
}
checkImageLinks(signalSystem, smt, smt.getAppearanceDanger().getImageLinks());
checkImageLinks(signalSystem, smt, smt.getAppearancePermissive().getImageLinks());
checkImageLinks(signalSystem, smt, smt.getAppearanceHeld().getImageLinks());
checkImageLinks(signalSystem, smt, smt.getAppearanceDark().getImageLinks());
}
}
private boolean checkFileForSpaces(ImageLink link, List<String> filenamesWithSpaces) throws IOException {
boolean changed = false;
if (link.getImageLink().contains(" ")) {
if (REMOVE_SPACES) {
String filename = link.getImageLink();
link.setImageLink(link.getImageLink().replace(" ", "_"));
String newFilename = link.getImageLink();
File file = new File("xml/signals" + filename).getCanonicalFile();
File newFile = new File("xml/signals" + newFilename).getCanonicalFile();
Assertions.assertTrue( file.renameTo(newFile), String.format("Can rename file %s to file %s", file, newFile));
changed = true;
} else {
filenamesWithSpaces.add(link.getImageLink());
}
}
return changed;
}
private boolean checkImageLinksSpaces(List<ImageLink> imageLinks, List<String> filenamesWithSpaces) throws IOException {
boolean changed = false;
for (ImageLink link : imageLinks) {
changed |= checkFileForSpaces(link, filenamesWithSpaces);
}
return changed;
}
private boolean checkSpaces(SignalSystem signalSystem) throws IOException {
boolean spacesRemoved = false;
List<String> filenamesWithSpaces = new ArrayList<>();
Assertions.assertFalse( signalSystem.getFolderName().contains(" "), "The signal system folder name contains no spaces: " + signalSystem.getFolderName());
for (SignalMastType smt : signalSystem.getSignalMastTypes()) {
boolean changed = false;
for (Appearance appearance : smt.getAppearances()) {
changed |= checkImageLinksSpaces(appearance.getImageLinks(), filenamesWithSpaces);
}
changed |= checkImageLinksSpaces(smt.getAppearanceDanger().getImageLinks(), filenamesWithSpaces);
changed |= checkImageLinksSpaces(smt.getAppearancePermissive().getImageLinks(), filenamesWithSpaces);
changed |= checkImageLinksSpaces(smt.getAppearanceHeld().getImageLinks(), filenamesWithSpaces);
changed |= checkImageLinksSpaces(smt.getAppearanceDark().getImageLinks(), filenamesWithSpaces);
if (changed) {
SignalMastTypeXml signalMastXml = new SignalMastTypeXml();
signalMastXml.save(signalSystem, smt, "", false);
}
spacesRemoved |= changed;
}
if (!filenamesWithSpaces.isEmpty()) {
for (String filename : filenamesWithSpaces) {
log.error("File {} has spaces", filename);
}
log.error("To remove spaces in filenames, run");
log.error("jmri.jmrit.signalsystemeditor.configurexml.LoadAndStoreAllSignalSystemsTest");
log.error("with REMOVE_SPACES = true");
}
return spacesRemoved;
}
/**
* Get all XML files in a directory and validate them.
*
* @param directory the directory containing XML files
* @param recurse if true, will recurse into subdirectories
* @param pass if true, successful validation will pass; if false,
* successful validation will fail
* @return a stream of {@link Arguments}, where each Argument contains the
* {@link java.io.File} with a filename ending in {@literal .xml} to
* validate and a boolean matching the pass parameter
*/
private static Stream<Arguments> getFiles(File directory, boolean recurse, boolean pass) {
ArrayList<Arguments> files = new ArrayList<>();
if (directory.isDirectory()) {
var list = directory.listFiles();
Assertions.assertNotNull(list);
for (File file : list ) {
if (file.isDirectory()) {
if (recurse) {
files.addAll(getFiles(file, recurse, pass).collect(Collectors.toList()));
}
} else {
files.addAll(getFiles(file, recurse, pass).collect(Collectors.toList()));
}
}
} else if (directory.getName().endsWith(".xml") || directory.getName().endsWith(".html")) {
files.add(Arguments.of(directory, pass));
}
return files.stream();
}
public static Stream<Arguments> data() {
return getFiles(new File("xml/signals"), true, true);
}
private static boolean checkFile(File inFile1, File inFile2) throws IOException {
try ( // compare files, except for certain special lines
BufferedReader fileStream1 = new BufferedReader( new InputStreamReader(
new FileInputStream(inFile1), java.nio.charset.StandardCharsets.UTF_8));
BufferedReader fileStream2 = new BufferedReader( new InputStreamReader(
new FileInputStream(inFile2), java.nio.charset.StandardCharsets.UTF_8));
) {
String line1 = fileStream1.readLine();
String line2 = fileStream2.readLine();
int lineNumber1 = 0, lineNumber2 = 0;
String next1 = null;
String next2 = null;
// Remove BOM (Byte Order Mark)
// https://en.wikipedia.org/wiki/Byte_order_mark
Assertions.assertNotNull(line2);
if (line2.codePointAt(0) == 65279) {
line2 = line2.substring(1);
}
line2 = line2.replaceAll(" encoding=\"utf-8\"", " encoding=\"UTF-8\"");
while ((next1 = fileStream1.readLine()) != null && (next2 = fileStream2.readLine()) != null) {
lineNumber1++;
lineNumber2++;
while ((next1.isBlank()) && (next1 = fileStream2.readLine()) != null) {
lineNumber1++;
}
next2 = next2.replace("<!-- Start of Specific Appearances list -->", "");
next2 = next2.replace("<!-- End of Specific Appearances list -->", "");
next2 = next2.replace("<!-- Start of Aspect Mapping -->", "");
next2 = next2.replace("<!-- End of Aspect Mapping -->", "");
next2 = next2.replace("<!-- Start of Advanced Aspect Mapping -->", "");
next2 = next2.replace("<!-- Start of Advanced Aspect Mapping -->", "");
next2 = next2.replace("<!-- Start of Advanced Aspect mapping -->", "");
next2 = next2.replace("<!-- Start of Advanced-Aspect mapping -->", "");
next2 = next2.replace("<!-- End of Advanced Aspect Mapping -->", "");
next2 = next2.replace("<!-- NOTE 1: advancedAspect here means the signal ahead of \"our\", and aspect is same or more restrictive -->", "");
next2 = next2.replace("<!-- NOTE 2: Refer to related aspects.xml to consider and apply all possible aspects ahead to these \"our\" aspects -->", "");
next2 = next2.replace("<!-- The following references the \"Restricted Proceed\" aspect, which is undefined here-->", "");
next2 = next2.replace("<email></email>", "");
while (next2 != null && next2.isBlank() && (next2 = fileStream2.readLine()) != null) {
next2 = next2.replace("<!-- Start of Specific Appearances list -->", "");
next2 = next2.replace("<!-- End of Specific Appearances list -->", "");
next2 = next2.replace("<!-- Start of Aspect Mapping -->", "");
next2 = next2.replace("<!-- End of Aspect Mapping -->", "");
next2 = next2.replace("<!-- Start of Advanced Aspect Mapping -->", "");
next2 = next2.replace("<!-- Start of Advanced Aspect Mapping -->", "");
next2 = next2.replace("<!-- End of Advanced Aspect Mapping -->", "");
next2 = next2.replace("<!-- NOTE 1: advancedAspect here means the signal ahead of \"our\", and aspect is same or more restrictive -->", "");
next2 = next2.replace("<!-- NOTE 2: Refer to related aspects.xml to consider and apply all possible aspects ahead to these \"our\" aspects -->", "");
next2 = next2.replace("<!-- The following references the \"Restricted Proceed\" aspect, which is undefined here-->", "");
next2 = next2.replace("<email></email>", "");
lineNumber2++;
}
if (next1 == null || next2 == null) break;
next1 = next1.strip();
next2 = next2.strip();
if (next1.startsWith("<aspecttable ") && next1.startsWith(next2) && !next1.equals(next2)) {
while (next1.startsWith(next2) && !next1.equals(next2)) {
next2 += " " + fileStream2.readLine().strip();
// Remove space before and after = sign
next2 = next2.replaceAll("\\s*=\\s*", "=");
lineNumber2++;
}
}
if (next1.startsWith("<appearancetable ") && next1.startsWith(next2) && !next1.equals(next2)) {
while (next1.startsWith(next2) && !next1.equals(next2)) {
next2 += " " + fileStream2.readLine().strip();
// Remove space before and after = sign
next2 = next2.replaceAll("\\s*=\\s*", "=");
lineNumber2++;
}
}
while (next1.startsWith("<reference>")
&& next2.startsWith("<reference>")
&& (next1.startsWith(next2) || next2.startsWith(next1))
&& (!next1.endsWith("</reference>") || !next2.endsWith("</reference>"))) {
if (next1.startsWith(next2)) {
// \u00A0 is non breaking space
next2 += fileStream2.readLine().strip().replaceAll("\u00A0", " ");
lineNumber2++;
} else { // next2.startsWith(next1)
// \u00A0 is non breaking space
next1 += fileStream1.readLine().strip().replaceAll("\u00A0", " ");
lineNumber1++;
}
}
while (next1.startsWith("<description>")
&& next2.startsWith("<description>")
&& (next1.startsWith(next2) || next2.startsWith(next1))
&& (!next1.endsWith("</description>") || !next2.endsWith("</description>"))) {
if (next1.startsWith(next2)) {
// \u00A0 is non breaking space
next2 += fileStream2.readLine().strip().replaceAll("\u00A0", " ");
lineNumber2++;
} else { // next2.startsWith(next1)
// \u00A0 is non breaking space
next1 += fileStream1.readLine().strip().replaceAll("\u00A0", " ");
lineNumber1++;
}
}
// Remove space before and after = sign
next2 = next2.replaceAll("\\s*=\\s*", "=");
// Remove space between " and >
next2 = next2.replaceAll("\"\\s+\\>", "\">");
while (!next2.equals(next1) && next2.startsWith(next1)) {
next1 += fileStream1.readLine().strip();
lineNumber1++;
}
if (next2.endsWith("\"/>")) {
next2 = next2.substring(0, next2.length() - "\"/>".length()) + "\" />";
}
if (!line1.equals(line2)) {
log.error("match failed in LoadAndStoreTest:");
log.error(" file1:line {}: \"{}\"", lineNumber1, line1);
log.error(" file2:line {}: \"{}\"", lineNumber2, line2);
log.error(" comparing file1:\"{}\"", inFile1.getPath());
log.error(" to file2:\"{}\"", inFile2.getPath());
Assertions.assertEquals(line2, line1);
}
line1 = next1;
line2 = next2;
} // while readLine() != null
if (next1 != null) {
while ((next1 = fileStream1.readLine()) != null) {
lineNumber1++;
if (!next1.isBlank()) {
log.warn("The file {} has extra content: {}",
inFile1.getPath(), next1.strip());
}
}
}
if (next2 != null) {
while ((next2 = fileStream2.readLine()) != null) {
lineNumber2++;
if (!next2.isBlank()) {
log.warn("The file {} has extra content: {}",
inFile2.getPath(), next2.strip());
}
}
}
} catch (java.io.FileNotFoundException ex) {
// See this comment in PR #11736
// https://github.com/JMRI/JMRI/pull/11736#issuecomment-1379451919
// Once you create a signal mast, I think it will continue to
// reference the same appearance* file, even if that later
// disappears from the aspect file. It's possible that removing
// those three files will break some existing layout signal
// configurations because they're still pointing at those files.
log.warn("File not found: {}", ex.getMessage());
}
return true;
}
private void loadAndStoreFileCheck(File file) throws IOException {
log.debug("Start check file {}", file.getCanonicalPath());
File signalSystemFolder = file.getCanonicalFile().getParentFile();
Assertions.assertNotNull(signalSystemFolder);
String signalSystemName = signalSystemFolder.getName();
if (!signalSystemName.equals(lastSignalSystem)) {
lastSignalSystem = signalSystemName;
SignalSystemXml signalSystemXml = new SignalSystemXml();
SignalMastTypeXml signalMastXml = new SignalMastTypeXml();
SignalSystem signalSystem = signalSystemXml.load(new File(file.getParent()+"/aspects.xml"));
if (checkSpaces(signalSystem)) {
// If spaces have been removed from the file names, reload the signal system
signalSystem = signalSystemXml.load(new File(file.getParent()+"/aspects.xml"));
}
checkImageLinks(signalSystem);
signalSystemXml.save(signalSystem);
for (SignalMastType signalMastType : signalSystem.getSignalMastTypes()) {
signalMastXml.save(signalSystem, signalMastType, true);
}
File parentFile = file.getParentFile();
Assertions.assertNotNull(parentFile);
File compFile = new File(FileUtil.getProfilePath()
+ "xml/signals/" + "/" + parentFile.getName() + "/" + file.getName() );
checkFile(compFile, file);
}
}
@ParameterizedTest(name = "{index}: {0} (pass={1})")
@MethodSource("data")
public void loadAndStoreTest(File file, boolean pass) throws IOException {
String parentFile = file.getParent();
Assertions.assertNotNull(parentFile);
if (!parentFile.equals("xml/signals") && !parentFile.equals("xml\\signals")) {
loadAndStoreFileCheck(file);
}
}
@BeforeEach
public void setUp(@TempDir File tempDir) throws IOException {
JUnitUtil.setUp();
// tempDir = new File("temp/temp/SignalSystemEditor");
JUnitUtil.resetProfileManager( new jmri.profile.NullProfile( tempDir));
JUnitUtil.resetInstanceManager();
JUnitUtil.initConfigureManager();
}
@AfterEach
public void tearDown() {
JUnitUtil.tearDown();
}
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LoadAndStoreAllSignalSystemsTest.class);
}