diff --git a/build.gradle b/build.gradle index 26b6967..87a224c 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ dependencies { testImplementation platform('org.junit:junit-bom:5.9.1') testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.xmlunit:xmlunit-core:2.9.1' + testImplementation "org.mockito:mockito-core:5.+" } application { diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/InterlisValidator.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/InterlisValidator.java index 71ddbe4..c65eb73 100644 --- a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/InterlisValidator.java +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/InterlisValidator.java @@ -24,33 +24,24 @@ public InterlisValidator(TestOptions options) { } @Override - public boolean validate(Path filePath) throws ValidatorException { - var relativePath = options.basePath().relativize(filePath.getParent()); - var logDirectory = options.outputPath().resolve(relativePath); - - var filenameWithoutExtension = StringUtils.getFilenameWithoutExtension(filePath.getFileName().toString()); - var logFile = logDirectory.resolve(filenameWithoutExtension + ".log"); - + public boolean validate(Path filePath, Path logFile) throws ValidatorException { + LOGGER.info("Validating " + filePath + " with log file " + logFile); try { - Files.createDirectories(logDirectory); + Files.createDirectories(logFile.getParent()); var processBuilder = new ProcessBuilder() .command( "java", "-jar", options.ilivalidatorPath().toString(), "--log", logFile.toString(), + "--modeldir", options.basePath() + ";%ITF_DIR;http://models.interlis.ch/;%JAR_DIR/ilimodels", filePath.toString()) + .redirectOutput(ProcessBuilder.Redirect.DISCARD) + .redirectError(ProcessBuilder.Redirect.DISCARD) .directory(options.basePath().toFile()); var process = processBuilder.start(); var exitCode = process.waitFor(); - - if (exitCode == 0) { - LOGGER.info("Validation of " + filePath + " completed successfully."); - return true; - } else { - LOGGER.error("Validation of " + filePath + " failed with exit code " + exitCode + ". See " + logFile + " for details."); - return false; - } + return exitCode == 0; } catch (IOException | InterruptedException e) { throw new ValidatorException(e); } diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Main.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Main.java index f205282..2271481 100644 --- a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Main.java +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Main.java @@ -1,5 +1,6 @@ package ch.geowerkstatt.interlis.testbed.runner; +import ch.geowerkstatt.interlis.testbed.runner.xtf.XtfFileMerger; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; @@ -28,7 +29,8 @@ public static void main(String[] args) { var testOptions = parseTestOptions(args); var validator = new InterlisValidator(testOptions); - var runner = new Runner(testOptions, validator); + var xtfMerger = new XtfFileMerger(); + var runner = new Runner(testOptions, validator, xtfMerger); if (!runner.run()) { System.exit(1); } diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Runner.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Runner.java index 29020c1..1c0e2f0 100644 --- a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Runner.java +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Runner.java @@ -1,31 +1,37 @@ package ch.geowerkstatt.interlis.testbed.runner; +import ch.geowerkstatt.interlis.testbed.runner.xtf.XtfMerger; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.IOException; import java.nio.file.Path; -import java.util.Optional; +import java.util.List; public final class Runner { private static final Logger LOGGER = LogManager.getLogger(); private final TestOptions options; private final Validator validator; + private final XtfMerger xtfMerger; + private Path baseFilePath; /** * Creates a new instance of the Runner class. * - * @param options the test options. + * @param options the test options. * @param validator the validator to use. + * @param xtfMerger the XTF merger to use. */ - public Runner(TestOptions options, Validator validator) { + public Runner(TestOptions options, Validator validator, XtfMerger xtfMerger) { this.options = options; this.validator = validator; + this.xtfMerger = xtfMerger; } /** * Runs the testbed validation. + * * @return {@code true} if the validation was successful, {@code false} otherwise. */ public boolean run() { @@ -33,32 +39,76 @@ public boolean run() { try { if (!validateBaseData()) { - LOGGER.error("Validation of base data failed."); return false; } + + if (!mergeAndValidateTransferFiles()) { + return false; + } + + LOGGER.info("Validation of testbed completed."); + return true; } catch (ValidatorException e) { LOGGER.error("Validation could not run, check the configuration.", e); return false; } - - LOGGER.info("Validation of testbed completed."); - return true; } private boolean validateBaseData() throws ValidatorException { - Optional filePath; try { - filePath = options.baseDataFilePath(); + var baseFilePath = options.baseDataFilePath(); + if (baseFilePath.isEmpty()) { + LOGGER.error("No base data file found."); + return false; + } + this.baseFilePath = baseFilePath.get(); + } catch (IOException e) { + throw new ValidatorException(e); + } + + LOGGER.info("Validating base data file " + baseFilePath); + var filenameWithoutExtension = StringUtils.getFilenameWithoutExtension(baseFilePath.getFileName().toString()); + var logFile = options.resolveOutputFilePath(baseFilePath, filenameWithoutExtension + ".log"); + + var valid = validator.validate(baseFilePath, logFile); + if (valid) { + LOGGER.info("Validation of " + baseFilePath + " completed successfully."); + } else { + LOGGER.error("Validation of " + baseFilePath + " failed. See " + logFile + " for details."); + } + return valid; + } + + private boolean mergeAndValidateTransferFiles() throws ValidatorException { + List patchFiles; + try { + patchFiles = options.patchDataFiles(); } catch (IOException e) { throw new ValidatorException(e); } - if (filePath.isEmpty()) { - LOGGER.error("No base data file found."); + if (patchFiles.isEmpty()) { + LOGGER.error("No patch files found."); return false; } - LOGGER.info("Validating base data file " + filePath.get()); - return validator.validate(filePath.get()); + var valid = true; + for (var patchFile : patchFiles) { + var patchFileNameWithoutExtension = StringUtils.getFilenameWithoutExtension(patchFile.getFileName().toString()); + var mergedFile = options.resolveOutputFilePath(patchFile, patchFileNameWithoutExtension + "_merged.xtf"); + if (!xtfMerger.merge(baseFilePath, patchFile, mergedFile)) { + valid = false; + continue; + } + + var logFile = mergedFile.getParent().resolve(patchFileNameWithoutExtension + ".log"); + var mergedFileValid = validator.validate(mergedFile, logFile); + if (mergedFileValid) { + LOGGER.error("Validation of " + mergedFile + " was expected to fail but completed successfully."); + valid = false; + } + } + + return valid; } } diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/TestOptions.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/TestOptions.java index 056e5d1..b798ac7 100644 --- a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/TestOptions.java +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/TestOptions.java @@ -3,6 +3,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -26,11 +28,37 @@ public record TestOptions(Path basePath, Path ilivalidatorPath) { * @return the path to the base data file. */ public Optional baseDataFilePath() throws IOException { - try (var dataFiles = findDataFiles(basePath)) { + try (var dataFiles = findDataFiles(basePath, 1)) { return dataFiles.findFirst(); } } + /** + * Gets the paths to the patch data files. + * + * @return the paths to the patch data files. + */ + public List patchDataFiles() throws IOException { + try (var dataFiles = findDataFiles(basePath, 2)) { + var outputPath = outputPath(); + return dataFiles + .filter(path -> !path.getParent().equals(basePath) && !path.startsWith(outputPath)) + .toList(); + } + } + + /** + * Resolves the path to the output file based on the relative path of the input file. + * + * @param filePath the path to the input file. + * @param newFileName the name of the output file. + * @return the path to the output file. + */ + public Path resolveOutputFilePath(Path filePath, String newFileName) { + var relativePath = basePath.relativize(filePath.getParent()); + return outputPath().resolve(relativePath).resolve(newFileName); + } + /** * Gets the path to the output directory. * @@ -40,7 +68,11 @@ public Path outputPath() { return basePath.resolve(OUTPUT_DIR_NAME); } - private static Stream findDataFiles(Path basePath) throws IOException { - return Files.find(basePath, 1, (path, attributes) -> path.getFileName().toString().toLowerCase().endsWith(DATA_FILE_EXTENSION)); + private static Stream findDataFiles(Path basePath, int maxDepth) throws IOException { + return Files.find(basePath, maxDepth, TestOptions::isDataFile); + } + + private static boolean isDataFile(Path path, BasicFileAttributes attributes) { + return attributes.isRegularFile() && path.getFileName().toString().toLowerCase().endsWith(DATA_FILE_EXTENSION); } } diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Validator.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Validator.java index 0603842..715342c 100644 --- a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Validator.java +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Validator.java @@ -7,8 +7,9 @@ public interface Validator { * Validates the given file. * * @param filePath the path to the file to validate. + * @param logFile the path to the log file. * @return true if the validation was successful, false otherwise. * @throws ValidatorException if the validation could not be performed. */ - boolean validate(Path filePath) throws ValidatorException; + boolean validate(Path filePath, Path logFile) throws ValidatorException; } diff --git a/src/test/data/testbed-with-patches/constraintA/testcase-1.xtf b/src/test/data/testbed-with-patches/constraintA/testcase-1.xtf new file mode 100644 index 0000000..beaef79 --- /dev/null +++ b/src/test/data/testbed-with-patches/constraintA/testcase-1.xtf @@ -0,0 +1,5 @@ + + + + + diff --git a/src/test/data/testbed-with-patches/data.xtf b/src/test/data/testbed-with-patches/data.xtf new file mode 100644 index 0000000..3146760 --- /dev/null +++ b/src/test/data/testbed-with-patches/data.xtf @@ -0,0 +1,10 @@ + + + + + + interlis-testbed-runner + + + + diff --git a/src/test/java/ch/geowerkstatt/interlis/testbed/runner/MockitoTestBase.java b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/MockitoTestBase.java new file mode 100644 index 0000000..f5d07f7 --- /dev/null +++ b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/MockitoTestBase.java @@ -0,0 +1,24 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +public abstract class MockitoTestBase { + private MockitoSession mockito; + + @BeforeEach + public final void startMockitoSession() { + mockito = Mockito.mockitoSession() + .initMocks(this) + .strictness(Strictness.STRICT_STUBS) + .startMocking(); + } + + @AfterEach + public final void finishMockitoSession() { + mockito.finishMocking(); + } +} diff --git a/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerBaseDataTest.java b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerBaseDataTest.java new file mode 100644 index 0000000..dcdcf1c --- /dev/null +++ b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerBaseDataTest.java @@ -0,0 +1,86 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +import ch.geowerkstatt.interlis.testbed.runner.xtf.XtfMerger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public final class RunnerBaseDataTest extends MockitoTestBase { + private static final Path BASE_PATH = Path.of("src/test/data/testbed").toAbsolutePath().normalize(); + private static final Path BASE_DATA_FILE = BASE_PATH.resolve("data.xtf"); + + private TestOptions options; + private TestLogAppender appender; + @Mock + private Validator validatorMock; + @Mock + private XtfMerger mergerMock; + + @BeforeEach + public void setup() { + options = new TestOptions(BASE_PATH, Path.of("ilivalidator.jar")); + appender = TestLogAppender.registerAppender(Runner.class); + } + + @AfterEach + public void teardown() { + appender.stop(); + appender.unregister(); + } + + @Test + public void runValidatesBaseData() throws IOException, ValidatorException { + when(validatorMock.validate(any(), any())).thenReturn(true); + + var runner = new Runner(options, validatorMock, mergerMock); + + var runResult = runner.run(); + + assertFalse(runResult, "Testbed run should have failed without patch files."); + + var errors = appender.getErrorMessages(); + assertIterableEquals(List.of("No patch files found."), errors); + + var baseDataFile = options.baseDataFilePath(); + assertFalse(baseDataFile.isEmpty(), "Base data file should have been found."); + assertEquals(BASE_DATA_FILE, baseDataFile.get()); + + verify(validatorMock).validate(eq(BASE_DATA_FILE), any()); + } + + @Test + public void runLogsValidationError() throws ValidatorException { + when(validatorMock.validate(any(), any())).thenReturn(false); + + var runner = new Runner(options, validatorMock, mergerMock); + + var runResult = runner.run(); + + assertFalse(runResult, "Testbed run should have failed."); + + var errors = appender.getErrorMessages(); + assertEquals(1, errors.size(), "One error should have been logged."); + var errorMessage = errors.getFirst(); + assertTrue( + Pattern.matches("Validation of .*? failed\\..*", errorMessage), + "Error message should start with 'Validation of failed.', actual value: '" + errorMessage + "'." + ); + + verify(validatorMock).validate(eq(BASE_DATA_FILE), any()); + } +} diff --git a/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerTest.java b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerTest.java deleted file mode 100644 index 8fae6dc..0000000 --- a/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package ch.geowerkstatt.interlis.testbed.runner; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertIterableEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public final class RunnerTest { - private static final String BASE_PATH = "src/test/data/testbed"; - - private TestLogAppender appender; - private TestOptions options; - - @BeforeEach - public void setup() { - appender = TestLogAppender.registerAppender(Runner.class); - options = new TestOptions(Path.of(BASE_PATH), Path.of("ilivalidator.jar")); - } - - @AfterEach - public void teardown() { - appender.stop(); - appender.unregister(); - } - - @Test - public void runValidatesBaseData() throws IOException { - var validatedFiles = new ArrayList(); - - var runner = new Runner(options, file -> { - validatedFiles.add(file.toAbsolutePath().normalize()); - return true; - }); - - var runResult = runner.run(); - - assertTrue(runResult, "Testbed run should have been successful."); - - var expectedBaseDataFile = Path.of(BASE_PATH, "data.xtf").toAbsolutePath().normalize(); - var baseDataFile = options.baseDataFilePath(); - assertFalse(baseDataFile.isEmpty(), "Base data file should have been found."); - assertEquals(expectedBaseDataFile, baseDataFile.get()); - - var expectedFiles = List.of(expectedBaseDataFile); - assertIterableEquals(expectedFiles, validatedFiles); - - var errors = appender.getErrorMessages(); - assertEquals(0, errors.size(), "No errors should have been logged."); - } - - @Test - public void runLogsValidationError() { - var runner = new Runner(options, file -> false); - - var runResult = runner.run(); - - assertFalse(runResult, "Testbed run should have failed."); - - var errors = appender.getErrorMessages(); - assertEquals(1, errors.size(), "One error should have been logged."); - assertEquals("Validation of base data failed.", errors.getFirst()); - } -} diff --git a/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerWithConstraintsTest.java b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerWithConstraintsTest.java new file mode 100644 index 0000000..4890d8a --- /dev/null +++ b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerWithConstraintsTest.java @@ -0,0 +1,110 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +import ch.geowerkstatt.interlis.testbed.runner.xtf.XtfMerger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.nio.file.Path; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public final class RunnerWithConstraintsTest extends MockitoTestBase { + private static final Path BASE_PATH = Path.of("src/test/data/testbed-with-patches").toAbsolutePath().normalize(); + private static final String CONSTRAINT_NAME = "constraintA"; + private static final Path BASE_DATA_FILE = BASE_PATH.resolve("data.xtf"); + private static final Path PATCH_FILE = BASE_PATH.resolve(CONSTRAINT_NAME).resolve("testcase-1.xtf"); + private static final Path MERGED_FILE = BASE_PATH.resolve("output").resolve(CONSTRAINT_NAME).resolve("testcase-1_merged.xtf"); + private static final Path MERGED_LOG_FILE = BASE_PATH.resolve("output").resolve(CONSTRAINT_NAME).resolve("testcase-1.log"); + + private TestOptions options; + private TestLogAppender appender; + @Mock + private Validator validatorMock; + @Mock + private XtfMerger mergerMock; + + @BeforeEach + public void setup() { + options = new TestOptions(BASE_PATH, Path.of("ilivalidator.jar")); + appender = TestLogAppender.registerAppender(Runner.class); + + when(mergerMock.merge(any(), any(), any())).thenReturn(true); + } + + @AfterEach + public void teardown() { + appender.stop(); + appender.unregister(); + } + + @Test + public void runMergesAndValidatesPatchFiles() throws ValidatorException { + when(validatorMock.validate(eq(BASE_DATA_FILE), any())).thenReturn(true); + when(validatorMock.validate(MERGED_FILE, MERGED_LOG_FILE)).thenReturn(false); + + var runner = new Runner(options, validatorMock, mergerMock); + + var runResult = runner.run(); + + assertTrue(runResult, "Testbed run should have succeeded."); + + var errors = appender.getErrorMessages(); + assertTrue(errors.isEmpty(), "No errors should have been logged."); + + verify(validatorMock).validate(eq(BASE_DATA_FILE), any()); + verify(validatorMock).validate(eq(MERGED_FILE), eq(MERGED_LOG_FILE)); + + verify(mergerMock).merge(eq(BASE_DATA_FILE), eq(PATCH_FILE), eq(MERGED_FILE)); + } + + @Test + public void runFailsIfMergeFails() throws ValidatorException { + when(validatorMock.validate(eq(BASE_DATA_FILE), any())).thenReturn(true); + when(mergerMock.merge(eq(BASE_DATA_FILE), eq(PATCH_FILE), any())).thenReturn(false); + + var runner = new Runner(options, validatorMock, mergerMock); + + var runResult = runner.run(); + + assertFalse(runResult, "Testbed run should have failed if file merging failed."); + + verify(validatorMock).validate(eq(BASE_DATA_FILE), any()); + verify(validatorMock, never()).validate(eq(MERGED_FILE), eq(MERGED_LOG_FILE)); + + verify(mergerMock).merge(eq(BASE_DATA_FILE), eq(PATCH_FILE), eq(MERGED_FILE)); + } + + @Test + public void runFailsIfMergedFileIsValid() throws ValidatorException { + when(validatorMock.validate(eq(BASE_DATA_FILE), any())).thenReturn(true); + when(validatorMock.validate(MERGED_FILE, MERGED_LOG_FILE)).thenReturn(true); + + var runner = new Runner(options, validatorMock, mergerMock); + + var runResult = runner.run(); + + assertFalse(runResult, "Testbed run should have failed if merged file is valid."); + + var errors = appender.getErrorMessages(); + assertEquals(1, errors.size(), "One error should have been logged."); + var errorMessage = errors.getFirst(); + assertTrue( + Pattern.matches("Validation of .*? was expected to fail but completed successfully\\.", errorMessage), + "Expected: Validation of was expected to fail but completed successfully. Actual: '" + errorMessage + "'."); + + verify(validatorMock).validate(eq(BASE_DATA_FILE), any()); + verify(validatorMock).validate(eq(MERGED_FILE), any()); + + verify(mergerMock).merge(eq(BASE_DATA_FILE), eq(PATCH_FILE), eq(MERGED_FILE)); + } +}