From e5c1c20ff590dae1d9d9c36684f713391f2333a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=A2=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=85=D0=BE=D0=B2?= Date: Mon, 1 Jul 2024 10:46:02 +0400 Subject: [PATCH] feat(webUI): run tests from the web interface Closes: #30 --- README.md | 10 +- .../java/ru/ewc/checklogic/FileUtils.java | 34 +---- .../java/ru/ewc/checklogic/LogicChecker.java | 93 +----------- .../{Computation.java => ServerContext.java} | 21 +-- src/main/java/ru/ewc/checklogic/TestData.java | 48 +++++++ .../java/ru/ewc/checklogic/TestResult.java | 50 +++++++ .../ru/ewc/checklogic/server/CommandPage.java | 6 +- .../checklogic/server/ResultOfTestsPage.java | 134 ++++++++++++++++++ .../ru/ewc/checklogic/server/StatePage.java | 6 +- .../ru/ewc/checklogic/server/WebServer.java | 24 ++-- src/main/resources/templates/test.html | 41 ++++++ 11 files changed, 316 insertions(+), 151 deletions(-) rename src/main/java/ru/ewc/checklogic/{Computation.java => ServerContext.java} (76%) create mode 100644 src/main/java/ru/ewc/checklogic/TestData.java create mode 100644 src/main/java/ru/ewc/checklogic/TestResult.java create mode 100644 src/main/java/ru/ewc/checklogic/server/ResultOfTestsPage.java create mode 100644 src/main/resources/templates/test.html diff --git a/README.md b/README.md index e5fc62b..589be8f 100644 --- a/README.md +++ b/README.md @@ -109,8 +109,10 @@ If it is the assertion on a state, then it's the same format as in the initial s ## Running the tests -The tests are run by issuing the following command: +The tests are run by: -```bash -java -jar logic-checker.jar -``` +1. starting the web server and passing the absolute path to the project folder as an argument: + ```bash + java -jar logic-checker.jar + ``` +2. and then heading to the `http://localhost:8080/test` in the browser. diff --git a/src/main/java/ru/ewc/checklogic/FileUtils.java b/src/main/java/ru/ewc/checklogic/FileUtils.java index 0e7dfc6..a0abd38 100644 --- a/src/main/java/ru/ewc/checklogic/FileUtils.java +++ b/src/main/java/ru/ewc/checklogic/FileUtils.java @@ -40,6 +40,7 @@ * * @since 0.3.0 */ +@SuppressWarnings("PMD.ProhibitPublicStaticMethods") public final class FileUtils { /** * Primary hidden constructor. @@ -48,36 +49,13 @@ private FileUtils() { // Utility class } - static InputStream applicationConfig(final String root) throws IOException { + public static InputStream applicationConfig(final String root) throws IOException { return Files.newInputStream(Path.of(root, "application.yaml")); } - /** - * Gets the path to a folder with specified resources. - * - * @param resource Resource name to get its folder. - * @return Path to a resource folder as a String. - */ - static String getFinalPathTo(final String resource) { - final String states; - if (System.getProperties().containsKey("sources")) { - states = String.format("%s/%s", System.getProperty("sources"), resource); - } else if (System.getProperties().containsKey(resource)) { - states = System.getProperty(resource); - } else { - states = String.format( - "%s%s%s", - System.getProperty("user.dir"), - "/src/test/resources/", - resource - ); - } - return states; - } - @SneakyThrows - static Stream readFileNames() { - return Files.walk(Paths.get(Computation.uriFrom(getFinalPathTo("states")))) + public static Stream readFileNames(final String root) { + return Files.walk(Path.of(root, "states")) .filter(Files::isRegularFile) .map( path -> path.toFile().getAbsolutePath() @@ -94,7 +72,7 @@ static Stream readFileNames() { } @SuppressWarnings("unchecked") - private static LogicChecker.TestData createTestData( + private static TestData createTestData( final String path, final InputStream stream ) { @@ -110,7 +88,7 @@ private static LogicChecker.TestData createTestData( if (iterator.hasNext()) { expectations = (Map>) iterator.next(); } - return new LogicChecker.TestData(path, command, expectations); + return new TestData(path, command, expectations); } @SuppressWarnings("unchecked") diff --git a/src/main/java/ru/ewc/checklogic/LogicChecker.java b/src/main/java/ru/ewc/checklogic/LogicChecker.java index 4614a55..283a07a 100644 --- a/src/main/java/ru/ewc/checklogic/LogicChecker.java +++ b/src/main/java/ru/ewc/checklogic/LogicChecker.java @@ -24,23 +24,18 @@ package ru.ewc.checklogic; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.SneakyThrows; -import org.assertj.core.api.SoftAssertions; import org.yaml.snakeyaml.Yaml; import ru.ewc.checklogic.server.WebServer; import ru.ewc.decisions.api.ComputationContext; -import ru.ewc.decisions.api.Locator; import ru.ewc.state.State; /** @@ -50,63 +45,26 @@ */ @SuppressWarnings("PMD.ProhibitPublicStaticMethods") public final class LogicChecker { - /** - * The logger. - */ - private static final Logger LOGGER = Logger.getLogger(LogicChecker.class.getName()); private LogicChecker() { // Utility class } public static void main(final String[] args) throws IOException { - if (args.length == 0) { + if (args.length != 1) { throw new IllegalArgumentException("Please provide the path to the resources"); } final String root = args[0]; - if (args.length > 1 && "server".equals(args[1])) { - final ComputationContext context = new ComputationContext( - stateFromAppConfig(FileUtils.applicationConfig(root)), - Path.of(root, "tables").toUri(), - Path.of(root, "commands").toUri() - ); - new WebServer(new Computation(context)).start(); - } else { - System.setProperty("sources", root); - final SoftAssertions softly = new SoftAssertions(); - FileUtils.readFileNames().forEach(test -> performTest(test, softly, root)); - } + new WebServer(new ServerContext(getContext(root)), root).start(); } @SneakyThrows - static void performTest( - final TestData test, - final SoftAssertions softly, - final String root - ) { - final Computation target = new Computation( - new ComputationContext( - stateFromFile(Files.newInputStream(new File(test.file()).toPath()), root), - Path.of(FileUtils.getFinalPathTo("tables")).toUri(), - Path.of(FileUtils.getFinalPathTo("commands")).toUri() - ) + private static ComputationContext getContext(final String root) { + return new ComputationContext( + stateFromAppConfig(FileUtils.applicationConfig(root)), + Path.of(root, "tables").toUri(), + Path.of(root, "commands").toUri() ); - try { - if (!test.command.isEmpty()) { - target.perform(test.command); - } - for (final String locator : test.expectations.keySet()) { - softly - .assertThat(target.stateFor(locator, test.expectations.get(locator))) - .describedAs(String.format("State for entity '%s'", locator)) - .containsExactlyInAnyOrderEntriesOf(test.expectations.get(locator)); - } - softly.assertAll(); - LOGGER.info("Running test for %s... done".formatted(test.toString())); - } catch (final AssertionError error) { - LOGGER.severe("Running test for %s... failed".formatted(test.toString())); - LOGGER.severe(error.getMessage()); - } } @SneakyThrows @@ -123,41 +81,4 @@ private static State stateFromAppConfig(final InputStream file) { ) ); } - - @SuppressWarnings("unchecked") - private static State stateFromFile( - final InputStream stream, - final String root - ) throws IOException { - final Map config = new Yaml().load(FileUtils.applicationConfig(root)); - final List names = (List) config.get("locators"); - final Map locators = HashMap.newHashMap(names.size()); - names.forEach(name -> locators.put(name, new InMemoryStorage(new HashMap<>()))); - final Map> raw = - (Map>) new Yaml().loadAll(stream).iterator().next(); - raw.keySet().forEach(name -> locators.put(name, new InMemoryStorage(raw.get(name)))); - return new State(locators); - } - - /** - * I am the helper class containing the data for parameterized state tests. - * - * @param file The path to the file containing state and expectations. - * @param command The name of the command to execute before the decision. - * @param expectations The collection of expected decision table results. - * @since 0.2.3 - */ - public record TestData( - String file, - String command, - Map> expectations) { - - @Override - public String toString() { - final String test = this.file - .substring(Math.max(0, this.file.lastIndexOf('\\'))) - .substring(Math.max(0, this.file.lastIndexOf('/'))); - return String.format("file='%s'", test); - } - } } diff --git a/src/main/java/ru/ewc/checklogic/Computation.java b/src/main/java/ru/ewc/checklogic/ServerContext.java similarity index 76% rename from src/main/java/ru/ewc/checklogic/Computation.java rename to src/main/java/ru/ewc/checklogic/ServerContext.java index 651ad0a..ba20be0 100644 --- a/src/main/java/ru/ewc/checklogic/Computation.java +++ b/src/main/java/ru/ewc/checklogic/ServerContext.java @@ -24,7 +24,6 @@ package ru.ewc.checklogic; -import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,32 +35,16 @@ * @since 0.1.0 */ @SuppressWarnings("PMD.ProhibitPublicStaticMethods") -public final class Computation { +public final class ServerContext { /** * The context of the computation. */ private final ComputationContext context; - public Computation(final ComputationContext context) { + public ServerContext(final ComputationContext context) { this.context = context; } - /** - * Converts a string representation of the file system path to a correct URI. - * - * @param path File system path as a String. - * @return URI that corresponds to a given path. - */ - public static URI uriFrom(final String path) { - final StringBuilder result = new StringBuilder("file:/"); - if (path.charAt(0) == '/') { - result.append(path.replace('\\', '/').substring(1)); - } else { - result.append(path.replace('\\', '/')); - } - return URI.create(result.toString()); - } - public void perform(final String command) { this.context.perform(command); } diff --git a/src/main/java/ru/ewc/checklogic/TestData.java b/src/main/java/ru/ewc/checklogic/TestData.java new file mode 100644 index 0000000..8ef3656 --- /dev/null +++ b/src/main/java/ru/ewc/checklogic/TestData.java @@ -0,0 +1,48 @@ +/* + * MIT License + * + * Copyright (c) 2024 Decision-Driven Development + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package ru.ewc.checklogic; + +import java.util.Map; + +/** + * I am the helper class containing the data for parameterized state tests. + * + * @param file The path to the file containing state and expectations. + * @param command The name of the command to execute before the decision. + * @param expectations The collection of expected decision table results. + * @since 0.2.3 + */ +public record TestData( + String file, + String command, + Map> expectations) { + + @Override + public String toString() { + final String test = this.file + .substring(Math.max(0, this.file.lastIndexOf('\\'))) + .substring(Math.max(0, this.file.lastIndexOf('/'))); + return String.format("file='%s'", test); + } +} diff --git a/src/main/java/ru/ewc/checklogic/TestResult.java b/src/main/java/ru/ewc/checklogic/TestResult.java new file mode 100644 index 0000000..97fb43d --- /dev/null +++ b/src/main/java/ru/ewc/checklogic/TestResult.java @@ -0,0 +1,50 @@ +/* + * MIT License + * + * Copyright (c) 2024 Decision-Driven Development + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package ru.ewc.checklogic; + +public record TestResult( + String file, + boolean successful, + String error +) { + + public String result() { + final String result; + if (this.successful) { + result = "PASS"; + } else { + result = "FAIL"; + } + return result; + } + + public String asHtmlTableRow() { + return String.format( + "%s%s%s", + this.file, + this.result(), + this.error + ); + } +} diff --git a/src/main/java/ru/ewc/checklogic/server/CommandPage.java b/src/main/java/ru/ewc/checklogic/server/CommandPage.java index 65550a6..392d3ab 100644 --- a/src/main/java/ru/ewc/checklogic/server/CommandPage.java +++ b/src/main/java/ru/ewc/checklogic/server/CommandPage.java @@ -28,7 +28,7 @@ import com.renomad.minum.web.Response; import com.renomad.minum.web.WebFramework; import java.util.Map; -import ru.ewc.checklogic.Computation; +import ru.ewc.checklogic.ServerContext; import ru.ewc.decisions.api.DecitaException; /** @@ -40,7 +40,7 @@ public final class CommandPage implements Endpoints { /** * The computation to be used for the command processing. */ - private final Computation computation; + private final ServerContext computation; /** * The template processor for the Command page. @@ -57,7 +57,7 @@ public final class CommandPage implements Endpoints { * * @param computation The computation to be used for the command processing. */ - public CommandPage(final Computation computation) { + public CommandPage(final ServerContext computation) { this.computation = computation; this.description = TemplateProcessor.buildProcessor( WebResource.readFileFromResources("templates/command-info.html") diff --git a/src/main/java/ru/ewc/checklogic/server/ResultOfTestsPage.java b/src/main/java/ru/ewc/checklogic/server/ResultOfTestsPage.java new file mode 100644 index 0000000..ac97bc3 --- /dev/null +++ b/src/main/java/ru/ewc/checklogic/server/ResultOfTestsPage.java @@ -0,0 +1,134 @@ +/* + * MIT License + * + * Copyright (c) 2024 Decision-Driven Development + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package ru.ewc.checklogic.server; + +import com.renomad.minum.templating.TemplateProcessor; +import com.renomad.minum.web.Request; +import com.renomad.minum.web.Response; +import com.renomad.minum.web.WebFramework; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.SneakyThrows; +import org.assertj.core.api.SoftAssertions; +import org.yaml.snakeyaml.Yaml; +import ru.ewc.checklogic.FileUtils; +import ru.ewc.checklogic.InMemoryStorage; +import ru.ewc.checklogic.ServerContext; +import ru.ewc.checklogic.TestData; +import ru.ewc.checklogic.TestResult; +import ru.ewc.decisions.api.ComputationContext; +import ru.ewc.decisions.api.Locator; +import ru.ewc.state.State; + +/** + * I am the configuration and logic for the test results web page. + * + * @since 0.3.0 + */ +public final class ResultOfTestsPage implements Endpoints { + /** + * The root directory of the application. + */ + private final String root; + + /** + * The template processor for the TestResults page. + */ + private final TemplateProcessor template; + + public ResultOfTestsPage(final String root) { + this.root = root; + this.template = TemplateProcessor.buildProcessor( + WebResource.readFileFromResources("templates/test.html") + ); + } + + @SuppressWarnings("unchecked") + public static State stateFromFile( + final InputStream stream, + final String root + ) throws IOException { + final Map config = new Yaml().load(FileUtils.applicationConfig(root)); + final List names = (List) config.get("locators"); + final Map locators = HashMap.newHashMap(names.size()); + names.forEach(name -> locators.put(name, new InMemoryStorage(new HashMap<>()))); + final Map> raw = + (Map>) new Yaml().loadAll(stream).iterator().next(); + raw.keySet().forEach(name -> locators.put(name, new InMemoryStorage(raw.get(name)))); + return new State(locators); + } + + @Override + public void register(final WebFramework web) { + web.registerPath(GET, "test", this::testPage); + } + + private Response testPage(final Request request) { + Objects.requireNonNull(request); + final String results = FileUtils.readFileNames(this.root) + .map(this::performTest) + .map(TestResult::asHtmlTableRow) + .collect(Collectors.joining()); + return Response.htmlOk( + this.template.renderTemplate(Map.of("tests", "%s".formatted(results))) + ); + } + + @SneakyThrows + private TestResult performTest(final TestData test) { + final SoftAssertions softly = new SoftAssertions(); + final ServerContext target = new ServerContext( + new ComputationContext( + stateFromFile(Files.newInputStream(new File(test.file()).toPath()), this.root), + Path.of(this.root, "tables").toUri(), + Path.of(this.root, "commands").toUri() + ) + ); + TestResult result; + try { + if (!test.command().isEmpty()) { + target.perform(test.command()); + } + for (final String locator : test.expectations().keySet()) { + softly + .assertThat(target.stateFor(locator, test.expectations().get(locator))) + .describedAs(String.format("State for entity '%s'", locator)) + .containsExactlyInAnyOrderEntriesOf(test.expectations().get(locator)); + } + softly.assertAll(); + result = new TestResult(test.toString(), true, ""); + } catch (final AssertionError error) { + result = new TestResult(test.toString(), false, error.getMessage()); + } + return result; + } +} diff --git a/src/main/java/ru/ewc/checklogic/server/StatePage.java b/src/main/java/ru/ewc/checklogic/server/StatePage.java index 317a58e..e6a3fde 100644 --- a/src/main/java/ru/ewc/checklogic/server/StatePage.java +++ b/src/main/java/ru/ewc/checklogic/server/StatePage.java @@ -28,7 +28,7 @@ import com.renomad.minum.web.Response; import com.renomad.minum.web.WebFramework; import java.util.Map; -import ru.ewc.checklogic.Computation; +import ru.ewc.checklogic.ServerContext; /** * I am the configuration and logic for the state web page. @@ -44,9 +44,9 @@ public final class StatePage implements Endpoints { /** * An instance of an object tracking the application state. */ - private final Computation computation; + private final ServerContext computation; - public StatePage(final Computation computation) { + public StatePage(final ServerContext computation) { this.computation = computation; this.template = TemplateProcessor.buildProcessor( WebResource.readFileFromResources("templates/state.html") diff --git a/src/main/java/ru/ewc/checklogic/server/WebServer.java b/src/main/java/ru/ewc/checklogic/server/WebServer.java index edf397a..6e30199 100644 --- a/src/main/java/ru/ewc/checklogic/server/WebServer.java +++ b/src/main/java/ru/ewc/checklogic/server/WebServer.java @@ -25,7 +25,7 @@ import com.renomad.minum.web.FullSystem; import com.renomad.minum.web.WebFramework; -import ru.ewc.checklogic.Computation; +import ru.ewc.checklogic.ServerContext; /** * I am the web server for the CheckLogic application. My main responsibility is to configure the @@ -35,24 +35,32 @@ */ public final class WebServer { /** - * The computation to be used for the web server. + * The context (all the paths) to be used for the web server. */ - private final Computation computation; + private final ServerContext context; + + /** + * The root path for the external business logic resources. + */ + private final String root; /** * Ctor. * - * @param computation The computation to be used for the web server. + * @param context The computation to be used for the web server. + * @param root The root path for the external business logic resources. */ - public WebServer(final Computation computation) { - this.computation = computation; + public WebServer(final ServerContext context, final String root) { + this.context = context; + this.root = root; } public void start() { final FullSystem minum = FullSystem.initialize(); final WebFramework web = minum.getWebFramework(); - registerEndpoints(web, new StatePage(this.computation)); - registerEndpoints(web, new CommandPage(this.computation)); + registerEndpoints(web, new StatePage(this.context)); + registerEndpoints(web, new CommandPage(this.context)); + registerEndpoints(web, new ResultOfTestsPage(this.root)); registerEndpoints(web, new StaticResources()); minum.block(); } diff --git a/src/main/resources/templates/test.html b/src/main/resources/templates/test.html new file mode 100644 index 0000000..aef8e3a --- /dev/null +++ b/src/main/resources/templates/test.html @@ -0,0 +1,41 @@ + + + + + Simple HTML Page + + + + + +

Test results

+ + + {{ tests }} +
TestResultMessage
+ + \ No newline at end of file