diff --git a/README.md b/README.md index 9b99942..e7502ca 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ Der Testbed-Runner ermöglicht das Testen von Constraints bzw. der dazugehörigen Methoden basierend auf Testdaten in einer definierten Ordnerstruktur. ## Anwendung -Der Runner funktioniert generisch auf einer entsprechenden Verzeichnisstruktur und globalen ilivalidator-Installation. - -Diese Struktur ist folgendermassen aufgebaut: +Der Runner funktioniert generisch auf einer entsprechenden Verzeichnisstruktur mit diesem Aufbau: ``` TestSuiteA @@ -23,3 +21,8 @@ TestSuiteA FailCase-1_Merged.xtf FailCase-1.log ``` + +Der Runner kann mit folgendem Befehl ausgeführt werden: +```bash +java -jar interlis-testbed-runner.jar --validator +``` diff --git a/build.gradle b/build.gradle index d0670c3..6f4e828 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,13 @@ repositories { } dependencies { + implementation platform('org.apache.logging.log4j:log4j-bom:2.22.0') + implementation 'org.apache.logging.log4j:log4j-api' + testImplementation 'org.apache.logging.log4j:log4j-core' + runtimeOnly 'org.apache.logging.log4j:log4j-core' + + implementation 'commons-cli:commons-cli:1.6.0' + testImplementation platform('org.junit:junit-bom:5.9.1') testImplementation 'org.junit.jupiter:junit-jupiter' } @@ -27,6 +34,14 @@ java { withJavadocJar() } +jar { + manifest { + attributes( + 'Main-Class': application.mainClass + ) + } +} + test { useJUnitPlatform() } diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/InterlisValidator.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/InterlisValidator.java new file mode 100644 index 0000000..71ddbe4 --- /dev/null +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/InterlisValidator.java @@ -0,0 +1,58 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class InterlisValidator implements Validator { + private static final Logger LOGGER = LogManager.getLogger(); + + private final TestOptions options; + + /** + * Creates a new instance of the InterlisValidator class. + * + * @param options the test options. + */ + public InterlisValidator(TestOptions options) { + this.options = options; + + LOGGER.info("Using ilivalidator at " + options.ilivalidatorPath()); + } + + @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"); + + try { + Files.createDirectories(logDirectory); + + var processBuilder = new ProcessBuilder() + .command( + "java", "-jar", options.ilivalidatorPath().toString(), + "--log", logFile.toString(), + filePath.toString()) + .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; + } + } 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 84f3f62..f205282 100644 --- a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Main.java +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Main.java @@ -1,6 +1,17 @@ package ch.geowerkstatt.interlis.testbed.runner; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import java.nio.file.Path; + public final class Main { + private static final String VALIDATOR_PATH_OPTION = "validator"; + private Main() { } @@ -10,6 +21,57 @@ private Main() { * @param args the command line arguments. */ public static void main(String[] args) { - System.out.println("Hello world!"); + if (args.length == 0) { + printUsage(createCliOptions()); + return; + } + + var testOptions = parseTestOptions(args); + var validator = new InterlisValidator(testOptions); + var runner = new Runner(testOptions, validator); + if (!runner.run()) { + System.exit(1); + } + } + + private static TestOptions parseTestOptions(String[] args) { + var options = createCliOptions(); + try { + var parser = new DefaultParser(); + var commandLine = parser.parse(options, args); + return getTestOptions(commandLine); + } catch (ParseException e) { + System.err.println("Error parsing command line arguments: " + e.getMessage()); + printUsage(options); + System.exit(1); + return null; + } + } + + private static void printUsage(Options options) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("java -jar interlis-testbed-runner.jar [options] [testbed directory (default: current directory)]", options); + } + + private static TestOptions getTestOptions(CommandLine commandLine) throws ParseException { + var remainingArgs = commandLine.getArgList(); + var basePath = remainingArgs.isEmpty() ? Path.of(".") : Path.of(remainingArgs.get(0)); + var validatorPath = Path.of(commandLine.getOptionValue(VALIDATOR_PATH_OPTION)); + return new TestOptions(basePath, validatorPath); + } + + private static Options createCliOptions() { + var options = new Options(); + + var validatorPathOption = Option.builder("v") + .argName("path") + .longOpt(VALIDATOR_PATH_OPTION) + .hasArg() + .required() + .desc("path to ilivalidator.jar") + .build(); + options.addOption(validatorPathOption); + + return options; } } diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Runner.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Runner.java new file mode 100644 index 0000000..29020c1 --- /dev/null +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Runner.java @@ -0,0 +1,64 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +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; + +public final class Runner { + private static final Logger LOGGER = LogManager.getLogger(); + + private final TestOptions options; + private final Validator validator; + + /** + * Creates a new instance of the Runner class. + * + * @param options the test options. + * @param validator the validator to use. + */ + public Runner(TestOptions options, Validator validator) { + this.options = options; + this.validator = validator; + } + + /** + * Runs the testbed validation. + * @return {@code true} if the validation was successful, {@code false} otherwise. + */ + public boolean run() { + LOGGER.info("Starting validation of testbed at " + options.basePath()); + + try { + if (!validateBaseData()) { + LOGGER.error("Validation of base data failed."); + return false; + } + } 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(); + } catch (IOException e) { + throw new ValidatorException(e); + } + + if (filePath.isEmpty()) { + LOGGER.error("No base data file found."); + return false; + } + + LOGGER.info("Validating base data file " + filePath.get()); + return validator.validate(filePath.get()); + } +} diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/StringUtils.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/StringUtils.java new file mode 100644 index 0000000..534822e --- /dev/null +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/StringUtils.java @@ -0,0 +1,21 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +public final class StringUtils { + private StringUtils() { + } + + /** + * Gets the filename without the extension. + * + * @param filename the filename to get the name from. + * @return the filename without the extension. + */ + public static String getFilenameWithoutExtension(String filename) { + var lastDotIndex = filename.lastIndexOf('.'); + if (lastDotIndex == -1) { + return filename; + } + + return filename.substring(0, lastDotIndex); + } +} diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/TestOptions.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/TestOptions.java new file mode 100644 index 0000000..056e5d1 --- /dev/null +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/TestOptions.java @@ -0,0 +1,46 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.stream.Stream; + +public record TestOptions(Path basePath, Path ilivalidatorPath) { + private static final String DATA_FILE_EXTENSION = ".xtf"; + private static final String OUTPUT_DIR_NAME = "output"; + + /** + * Creates a new instance of the RunnerOptions class. + * + * @param basePath the base path of the testbed. + */ + public TestOptions { + basePath = basePath.toAbsolutePath().normalize(); + ilivalidatorPath = ilivalidatorPath.toAbsolutePath().normalize(); + } + + /** + * Gets the path to the data file that is used as the base for all validations. + * + * @return the path to the base data file. + */ + public Optional baseDataFilePath() throws IOException { + try (var dataFiles = findDataFiles(basePath)) { + return dataFiles.findFirst(); + } + } + + /** + * Gets the path to the output directory. + * + * @return the path to the output directory. + */ + 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)); + } +} diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Validator.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Validator.java new file mode 100644 index 0000000..0603842 --- /dev/null +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/Validator.java @@ -0,0 +1,14 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +import java.nio.file.Path; + +public interface Validator { + /** + * Validates the given file. + * + * @param filePath the path to the file to validate. + * @return true if the validation was successful, false otherwise. + * @throws ValidatorException if the validation could not be performed. + */ + boolean validate(Path filePath) throws ValidatorException; +} diff --git a/src/main/java/ch/geowerkstatt/interlis/testbed/runner/ValidatorException.java b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/ValidatorException.java new file mode 100644 index 0000000..9cddb72 --- /dev/null +++ b/src/main/java/ch/geowerkstatt/interlis/testbed/runner/ValidatorException.java @@ -0,0 +1,34 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +/** + * Exception that is thrown when the validation failed to run. + */ +public final class ValidatorException extends Exception { + /** + * Creates a new instance of the ValidatorException class. + * + * @param message the message of the exception. + */ + public ValidatorException(String message) { + super(message); + } + + /** + * Creates a new instance of the ValidatorException class. + * + * @param message the message of the exception. + * @param cause the cause of the exception. + */ + public ValidatorException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new instance of the ValidatorException class. + * + * @param cause the cause of the exception. + */ + public ValidatorException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..eb9b6c4 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/test/data/testbed/data.xtf b/src/test/data/testbed/data.xtf new file mode 100644 index 0000000..3146760 --- /dev/null +++ b/src/test/data/testbed/data.xtf @@ -0,0 +1,10 @@ + + + + + + interlis-testbed-runner + + + + diff --git a/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerTest.java b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerTest.java new file mode 100644 index 0000000..579cca3 --- /dev/null +++ b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/RunnerTest.java @@ -0,0 +1,78 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +import org.apache.logging.log4j.Level; +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.getMessages() + .stream() + .filter(e -> e.level().equals(Level.ERROR)); + assertEquals(0, errors.count(), "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.getMessages() + .stream() + .filter(e -> e.level().equals(Level.ERROR)) + .toList(); + assertEquals(1, errors.size(), "One error should have been logged."); + assertEquals("Validation of base data failed.", errors.get(0).message()); + } +} diff --git a/src/test/java/ch/geowerkstatt/interlis/testbed/runner/TestLogAppender.java b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/TestLogAppender.java new file mode 100644 index 0000000..8e0e0bd --- /dev/null +++ b/src/test/java/ch/geowerkstatt/interlis/testbed/runner/TestLogAppender.java @@ -0,0 +1,52 @@ +package ch.geowerkstatt.interlis.testbed.runner; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; + +import java.util.ArrayList; +import java.util.List; + +/** + * Appender that can be used to capture log messages from the logger registered to a specific class. + */ +public final class TestLogAppender extends AbstractAppender { + private final Class loggerClass; + private final List messages = new ArrayList<>(); + + public record LogEntry(Level level, String message) { + } + + private TestLogAppender(Class loggerClass) { + super("TestLogAppender", null, null, true, null); + + this.loggerClass = loggerClass; + } + + public static TestLogAppender registerAppender(Class loggerClass) { + var mockedAppender = new TestLogAppender(loggerClass); + + var logger = (Logger) LogManager.getLogger(loggerClass); + logger.addAppender(mockedAppender); + logger.setLevel(Level.ALL); + + mockedAppender.start(); + return mockedAppender; + } + + public void unregister() { + var logger = (Logger) LogManager.getLogger(loggerClass); + logger.removeAppender(this); + } + + public List getMessages() { + return messages; + } + + @Override + public void append(LogEvent event) { + messages.add(new LogEntry(event.getLevel(), event.getMessage().getFormattedMessage())); + } +}