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

457 lines
20 KiB
Java

package jmri.configurexml;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.awt.GraphicsEnvironment;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.text.ParseException;
import java.util.stream.Stream;
import jmri.*;
import jmri.jmrit.logix.WarrantPreferences;
import jmri.util.FileUtil;
import jmri.util.JUnitAppender;
import jmri.util.JUnitUtil;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.provider.Arguments;
/**
* Base for testing load-and-store of configuration files.
* <p>
* Creating a parameterized test class that extends this class will test each
* file in a "load" directory by loading it, then storing it, then comparing
* (with certain lines skipped) against either a file by the same name in the
* "loadref" directory, or against the original file itself. A minimal test
* class is:
<pre>
public class LoadAndStoreTest extends LoadAndStoreTestBase {
public static Stream&amp;Arguments&amp; data() {
return getFiles(new File("java/test/jmri/configurexml"), false, true);
}
@ParameterizedTest
@MethodSource("data")
public void loadAndStoreTest(File file, boolean pass) { super.validate(file, pass); }
}
</pre>
*
* @author Bob Jacobsen Copyright 2009, 2014
* @since 2.5.5 (renamed & reworked in 3.9 series)
*/
public class LoadAndStoreTestBase {
public enum SaveType {
All, Config, Prefs, User, UserPrefs
}
private SaveType saveType = SaveType.Config;
private boolean guiOnly = false;
/**
* Get all XML files in a directory and validate the ability to load and
* store them.
*
* @param saveType the type (i.e. level) of ConfigureXml information being
* saved
* @param isGUI true for files containing GUI elements, i.e. panels.
* These can only be loaded once (others can be loaded
* twice, and that's tested when this is false), and can't
* be loaded when running headless.
*/
public LoadAndStoreTestBase(SaveType saveType, boolean isGUI) {
this.saveType = saveType;
this.guiOnly = isGUI;
}
/**
* Get all XML files in a directory and validate the ability to load and
* store them.
*
* @param directory the directory containing XML files; the subdirectory
* <code>load</code> under this directory will be used
* @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} to validate and a boolean matching the pass
* parameter
*/
public static Stream<Arguments> getFiles(File directory, boolean recurse, boolean pass) {
// since this method gets the files to test, but does not trigger any
// tests itself, we can use SchemaTestBase.getFiles() by adding "load"
// to the directory to test
return SchemaTestBase.getFiles(new File(directory, "load"), recurse, pass);
}
/**
* Get all XML files in the immediate subdirectories of a directory and
* validate them.
*
* @param directory the directory containing subdirectories 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 collection of Object arrays, where each array contains the
* {@link java.io.File} with a filename ending in {@literal .xml} to
* validate and a boolean matching the pass parameter
* @throws IllegalArgumentException if directory is a file
*/
public static Stream<Arguments> getDirectories(File directory, boolean recurse, boolean pass) throws IllegalArgumentException {
// since this method gets the files to test, but does not trigger any
// tests itself, we can use SchemaTestBase.getDirectories() by adding "load"
// to the directory to test
return SchemaTestBase.getDirectories(new File(directory, "load"), recurse, pass);
}
public static void checkFile(File inFile1, File inFile2) throws IOException, ParseException {
try ( // compare files, except for certain special lines
BufferedReader fileStream1 = new BufferedReader( new InputStreamReader(new FileInputStream(inFile1)));
BufferedReader fileStream2 = new BufferedReader( new InputStreamReader(new FileInputStream(inFile2)));
) {
String line1 = fileStream1.readLine();
String line2 = fileStream2.readLine();
int lineNumber1 = 0, lineNumber2 = 0;
String next1, next2;
while ((next1 = fileStream1.readLine()) != null && (next2 = fileStream2.readLine()) != null) {
lineNumber1++;
lineNumber2++;
// Do we have a multi line comment? Comments in the xml file is used by LogixNG.
// This only happens in the first file since store() will not store comments
if (next1.startsWith("<!--")) {
while ((next1 = fileStream1.readLine()) != null && !next1.endsWith("-->")) {
lineNumber1++;
}
// If here, we either have a line that ends with --> or we have reached end of file
String nullCheck = fileStream1.readLine();
if (nullCheck == null) {
break;
}
// If here, we have a line that ends with --> or we have reached end of file
continue;
}
// where the (empty) entryexitpairs line ends up seems to be non-deterministic
// so if we see it in either file we just skip it
String entryexitpairs = "<entryexitpairs class=\"jmri.jmrit.signalling.configurexml.EntryExitPairsXml\" />";
if (line1.contains(entryexitpairs)) {
line1 = next1;
if ((next1 = fileStream1.readLine()) == null) {
break;
}
lineNumber1++;
}
if (line2.contains(entryexitpairs)) {
line2 = next2;
if ((next2 = fileStream2.readLine()) == null) {
break;
}
lineNumber2++;
}
// if we get to the file history...
String filehistory = "filehistory";
if (line1.contains(filehistory) && line2.contains(filehistory)) {
break; // we're done!
}
boolean match = false; // assume failure (pessimist!)
String[] startsWithStrings = {
" <!--Written by JMRI version",
" <test>", // version changes over time
" <modifier", // version changes over time
" <major", // version changes over time
" <minor", // version changes over time
"<layout-config", // Linux seems to put attributes in different order
"<?xml-stylesheet", // Linux seems to put attributes in different order
" <memory systemName=\"IMCURRENTTIME\"", // time varies - old format
" <modifier>This line ignored</modifier>"
};
for (String startsWithString : startsWithStrings) {
if (line1.startsWith(startsWithString) && line2.startsWith(startsWithString)) {
match = true;
break;
}
}
// Check the <timebase> tag. When the time is stored in the xml file, it's
// stored in the current timezone, which differs from user to user.
// This check accept two times in different timezones, as long as the
// actual time is the same. For example, "Sun May 17 08:12:43 PDT 2020"
// is the same time as "Sun May 17 17:12:43 CEST 2020" but in different
// timezones.
if (line1.startsWith(" <timebase") && line2.startsWith(" <timebase")) {
SimpleDateFormat format = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);
int beginning = " <timebase ".length();
// Remove the beginning and the last four characters "/ >
String t1s = line1.substring(beginning, line1.length()-4);
String t2s = line2.substring(beginning, line2.length()-4);
// Split the attributes. There might be spaces in the values so
// split using one double quote and a space.
String[] t1sa = t1s.split("\" ");
String[] t2sa = t2s.split("\" ");
if (t1sa.length == t2sa.length) {
boolean success = true;
for (int i=0; i < t1sa.length; i++) {
if (t1sa[i].startsWith("time=")) {
// Check time independent of timezone
String d1s = t1sa[i].substring("time=\"".length());
String d2s = t2sa[i].substring("time=\"".length());
Date d1 = format.parse(d1s);
Date d2 = format.parse(d2s);
if (!d1.equals(d2)) {
success = false;
}
} else if (t1sa[i].startsWith("class=") && t2sa[i].startsWith("class=")) {
// Accept different classes. jmri.jmrit.simpleclock.configurexml.SimpleTimebaseXml and jmri.time.implementation.configurexml.DefaultTimebaseXml
} else if (t1sa[i].equals(t2sa[i])) {
// Other attributes in <timebase>
} else {
// Attributes are not equal
success = false;
}
}
if (success) {
match = true;
}
}
}
// Screen size will vary when written out
if (!match) {
if (line1.contains(" <LayoutEditor")) {
// if either line contains a windowheight attribute
String windowheight_regexe = "( windowheight=\"[^\"]*\")";
line1 = filterLineUsingRegEx(line1, windowheight_regexe);
line2 = filterLineUsingRegEx(line2, windowheight_regexe);
// if either line contains a windowheight attribute
String windowwidth_regexe = "( windowwidth=\"[^\"]*\")";
line1 = filterLineUsingRegEx(line1, windowwidth_regexe);
line2 = filterLineUsingRegEx(line2, windowwidth_regexe);
}
}
// window positions will sometimes differ based on window decorations.
if (!match) {
if (line1.contains(" <LayoutEditor") ||
line1.contains(" <switchboardeditor")) {
// if either line contains a y position attribute
String yposition_regexe = "( y=\"[^\"]*\")";
line1 = filterLineUsingRegEx(line1, yposition_regexe);
line2 = filterLineUsingRegEx(line2, yposition_regexe);
// if either line contains an x position attribute
String xposition_regexe = "( x=\"[^\"]*\")";
line1 = filterLineUsingRegEx(line1, xposition_regexe);
line2 = filterLineUsingRegEx(line2, xposition_regexe);
}
}
// Time will vary when written out
if (!match) {
String memory_value = "<memory value";
if (line1.contains(memory_value) && line2.contains(memory_value)) {
String imcurrenttime = "<systemName>IMCURRENTTIME</systemName>";
if (next1.contains(imcurrenttime) && next2.contains(imcurrenttime)) {
match = true;
}
}
}
// Dates can vary when written out
String date_string = "<date>";
if (!match && line1.contains(date_string) && line2.contains(date_string)) {
match = true;
}
if (!match) {
// remove fontname and fontFamily attributes
String fontname_regexe = "( fontname=\"[^\"]*\")";
line1 = filterLineUsingRegEx(line1, fontname_regexe);
line2 = filterLineUsingRegEx(line2, fontname_regexe);
String fontFamily_regexe = "( fontFamily=\"[^\"]*\")";
line1 = filterLineUsingRegEx(line1, fontFamily_regexe);
line2 = filterLineUsingRegEx(line2, fontFamily_regexe);
}
if (!match && !line1.equals(line2)) {
assertEquals(line1, line2, "match failed in LoadAndStoreTest:" +
System.lineSeparator() + " file1:line " + lineNumber1 + ": \"" + line1+ "\"" +
System.lineSeparator() + " file2:line " + lineNumber2 + ": \"" + line2+ "\"" +
System.lineSeparator() + " comparing file1: " + inFile1.getPath() + " line " + lineNumber1 +
System.lineSeparator() + " file2: " + inFile2.getPath() + " line " + lineNumber2);
}
line1 = next1;
line2 = next2;
} // while readLine() != null
}
}
private static String filterLineUsingRegEx(String line, String regexe) {
String[] splits = line.split(regexe);
if (splits.length == 2) { // (yes) remove it
line = splits[0] + splits[1];
}
return line;
}
// load file
public static void loadFile(File inFile) throws JmriException {
ConfigureManager cm = InstanceManager.getDefault(ConfigureManager.class);
WarrantPreferences.getDefault().setShutdown(WarrantPreferences.Shutdown.NO_MERGE);
boolean good = cm.load(inFile);
assertTrue(good, "loadFile(\"" + inFile.getPath() + "\")");
InstanceManager.getDefault(jmri.LogixManager.class).activateAllLogixs();
InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class).initializeLayoutBlockPaths();
new jmri.jmrit.catalog.configurexml.DefaultCatalogTreeManagerXml().readCatalogTrees();
}
// store file
public static File storeFile(File inFile, SaveType inSaveType) {
String name = inFile.getName();
FileUtil.createDirectory(FileUtil.getUserFilesPath() + "temp");
File outFile = new File(FileUtil.getUserFilesPath() + "temp/" + name);
ConfigureManager cm = InstanceManager.getDefault(ConfigureManager.class);
switch (inSaveType) {
case Config: {
assertTrue(cm.storeConfig(outFile));
break;
}
case Prefs: {
cm.storePrefs(outFile);
break;
}
case User: {
assertTrue(cm.storeUser(outFile));
break;
}
case UserPrefs: {
cm.storeUserPrefs(outFile);
break;
}
default: {
Assertions.fail("Unknown save type "+inSaveType);
break;
}
}
return outFile;
}
public void loadLoadStoreFileCheck(File file) throws IOException, JmriException, ParseException {
if (guiOnly) {
Assumptions.assumeFalse(GraphicsEnvironment.isHeadless(), "GUI Only test");
}
log.debug("Start check file {}", file.getCanonicalPath());
loadFile(file);
// Panel sub-classes (with GUI) will fail if you try to load them twice.
// (So don't!)
if (!guiOnly) {
loadFile(file);
}
// to ease comparison, dump the history information;
// if you need to turn that off you can override
dumpHistory();
// find comparison files
File tmpFile = file.getCanonicalFile().getParentFile();
if ( tmpFile == null ) {
log.warn("null file to check {}", file);
return;
}
File compFile = new File( tmpFile.getParent() + "/loadref/" + file.getName() );
if (!compFile.exists()) {
compFile = file;
}
log.debug(" Chose comparison file {}", compFile.getCanonicalPath());
postLoadProcessing();
File outFile = storeFile(file, this.saveType);
checkFile(compFile, outFile);
JUnitAppender.suppressErrorMessageStartsWith("systemName is already registered: ");
}
/**
* By default, drop the history information
* to simplify diffing the files.
* Override if that info is needed for a test.
*/
protected void dumpHistory(){
jmri.InstanceManager.getDefault(jmri.jmrit.revhistory.FileHistory.class).purge(0);
}
/**
* If anything, i.e. typically a delay,
* is needed after loading the file before storing or doing
* any final tests,
* it can be added by override here.
*/
protected void postLoadProcessing() {
// by default do nothing
}
@BeforeEach
public void setUp(@TempDir java.io.File tempDir) throws IOException {
JUnitUtil.setUp();
JUnitUtil.resetProfileManager( new jmri.profile.NullProfile( tempDir));
JUnitUtil.resetInstanceManager();
JUnitUtil.initConfigureManager();
JUnitUtil.initInternalTurnoutManager();
JUnitUtil.initInternalLightManager();
JUnitUtil.initInternalSensorManager();
JUnitUtil.initInternalSignalHeadManager();
JUnitUtil.initMemoryManager();
JUnitUtil.clearBlockBossLogic();
System.setProperty("jmri.test.no-dialogs", "true");
// kill the fast clock and set to a consistent time
jmri.Timebase clock = jmri.InstanceManager.getDefault(jmri.Timebase.class);
clock.setRun(false);
try {
clock.setTime(
new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S").parse("2021-12-02 00:00:00.0")
);
} catch (ParseException e) {
log.warn("Unexpected Exception in test clock setup", e);
}
}
@AfterEach
public void tearDown() {
JUnitUtil.closeAllPanels();
JUnitUtil.clearShutDownManager();
JUnitUtil.clearBlockBossLogic();
JUnitUtil.tearDown();
System.setProperty("jmri.test.no-dialogs", "false");
}
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LoadAndStoreTestBase.class);
}