diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..88907420eabc71f41a10b13cd76de0738cf2d5df --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,84 @@ +variables: + GIT_SUBMODULE_STRATEGY: recursive + +stages: + - build + - test + - ragdoc_build + - ragdoc_view + - publish + +before_script: + - export GRADLE_USER_HOME=`pwd`/.gradle + +cache: + paths: + - .gradle/wrapper + - .gradle/caches + +build: + image: openjdk:8 + stage: build + script: + - ./gradlew --console=plain --no-daemon assemble + artifacts: + paths: + - "src/gen" + expire_in: 1 week + +test: + image: openjdk:8 + stage: test + script: + - ./gradlew --console=plain --no-daemon test + artifacts: + reports: + junit: build/test-results/test/**/TEST-*.xml + +ragdoc_build: + image: + name: "git-st.inf.tu-dresden.de:4567/jastadd/ragdoc-builder" + entrypoint: [""] + stage: ragdoc_build + needs: + - build + script: + - JAVA_FILES=$(find src/ -name '*.java') + - /ragdoc-builder/start-builder.sh -excludeGenerated -d data/ $JAVA_FILES + artifacts: + paths: + - "data/" + +ragdoc_view: + image: + name: "git-st.inf.tu-dresden.de:4567/jastadd/ragdoc-view:relations" + entrypoint: [""] + stage: ragdoc_view + needs: + - ragdoc_build + script: + - DATA_DIR=$(pwd -P)/data + - mkdir -p pages/docs/ragdoc + - OUTPUT_DIR=$(pwd -P)/pages/docs/ragdoc + - cd /ragdoc-view/src/ && rm -rf data && ln -s $DATA_DIR + - /ragdoc-view/build-view.sh --output-path=$OUTPUT_DIR + artifacts: + paths: + - "pages/docs/ragdoc" + +pages: + image: python:3.8-buster + stage: publish + needs: + - ragdoc_view + - test + before_script: + - pip install -U mkdocs mkdocs-macros-plugin mkdocs-git-revision-date-localized-plugin + script: + - cd pages && mkdocs build +# only: +# - develop +# - master + artifacts: + paths: + - public diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..7f348667c77aac1df614eddfa1721935c68dddcf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/main/jastadd/mustache"] + path = src/main/jastadd/mustache + url = ../mustache.git diff --git a/build-template.gradle b/build-template.gradle new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/build.gradle b/build.gradle index aa3f23414c768afc27fada826a68fb8d66c2afaf..cc16a72d97cc4efff8a2a44fcba801746c912d07 100644 --- a/build.gradle +++ b/build.gradle @@ -1,101 +1,137 @@ +plugins { + id 'java-library' + id 'application' + id 'org.jastadd' + id 'java' + id 'idea' + id 'java-test-fixtures' +} + +ext { + mainClassName = 'org.jastadd.relast.compiler.RelastSourceToSourceCompiler' +} -apply plugin: 'java' -apply plugin: 'jastadd' -apply plugin: 'application' -apply plugin: "idea" +// set the main class name for `gradle run` +application.mainClassName = "${mainClassName}" sourceCompatibility = 1.8 - -mainClassName = 'org.jastadd.relast.compiler.RelastSourceToSourceCompiler' +targetCompatibility = 1.8 repositories { - jcenter() + mavenCentral() } -buildscript { - repositories.jcenter() - dependencies { - classpath 'org.jastadd:jastaddgradle:1.13.3' +sourceSets { + model { + java { + srcDir "src/gen/java" + } } } -dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.0' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.0' - testCompile 'org.assertj:assertj-core:3.12.1' - compile 'org.jastadd:jastadd:2.3.4' - runtime 'org.jastadd:jastadd:2.3.4' - compile group: 'net.sf.beaver', name: 'beaver-rt', version: '0.9.11' +task modelJar(type: Jar) { + group = "build" + archiveBaseName = 'model' + archiveVersion = '' + from sourceSets.model.output } -sourceSets { - main { - java.srcDir "src/gen/java" - java.srcDir "buildSrc/gen/java" - } +artifacts { + archives modelJar } -test { - useJUnitPlatform() +dependencies { - maxHeapSize = '1G' + modelImplementation group: 'org.jastadd', name: 'jastadd', version: '2.3.4' + modelImplementation group: 'net.sf.beaver', name: 'beaver-rt', version: '0.9.11' + + implementation files(modelJar.archiveFile.get()) + api group: 'org.jastadd', name: 'jastadd', version: '2.3.4' + api group: 'net.sf.beaver', name: 'beaver-rt', version: '0.9.11' + implementation group: 'com.github.jknack', name: 'handlebars', version: '4.2.0' + implementation group: 'org.yaml', name: 'snakeyaml', version: '1.27' + + // test + testRuntimeClasspath files(modelJar.archiveFile.get()) + + // test fixtures + testFixturesApi group: 'org.slf4j', name: 'slf4j-jdk14', version: '1.7.30' + testFixturesApi group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.0' + testFixturesApi group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.0' + testFixturesApi group: 'org.assertj', name: 'assertj-core', version: '3.18.0' + testFixturesApi group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.12.0-rc1' + testFixturesApi group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.12.0-rc1' + testFixturesApi group: 'commons-io', name: 'commons-io', version: '2.8.0' +} + +def versionFile = 'src/main/resources/preprocessor.properties' +def versionProps = new Properties() + +try { + file(versionFile).withInputStream { stream -> versionProps.load(stream) } + version = versionProps['version'] +} catch (e) { + // this happens, if either the properties file is not present, or cannot be read from + throw new GradleException("File ${versionFile} not found or unreadable. Aborting.", e) } jar { manifest { - attributes "Main-Class": 'org.jastadd.relast.compiler.RelastSourceToSourceCompiler' + attributes "Main-Class": "${mainClassName}" } from { - configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } } +test { + useJUnitPlatform() + + maxHeapSize = '1G' +} + +// Input and output files for relast +def relastInputFiles = [ + "src/main/jastadd/RelAst.relast", + "src/main/jastadd/mustache/Mustache.relast" +] +def relastOutputFiles = [ + "src/gen/jastadd/RelAst.ast", + "src/gen/jastadd/RelAst.jadd", + "src/gen/jastadd/RelAstRefResolver.jadd", + "src/gen/jastadd/RelAstResolverStubs.jrag" +] + task relast(type: JavaExec) { + classpath = files("libs/relast.jar") group = 'Build' - main = "-jar" doFirst { - delete "src/gen/jastadd/*.ast" - delete "src/gen/jastadd/RelAst.jadd" - delete "src/gen/jastadd/RelAstRefResolver.jadd" - delete "src/gen/jastadd/RelAstResolverStubs.jrag" - mkdir "src/gen/jastadd/" + delete relastOutputFiles + mkdir "src/gen/jastadd/" } args = [ - "libs/relast.jar", - "./src/main/jastadd/RelAst.relast", "--listClass=java.util.ArrayList", "--jastAddList=JastAddList", "--useJastAddNames", "--file", "--resolverHelper", "--grammarName=./src/gen/jastadd/RelAst" - ] - - inputs.files file("../libs/relast.jar"), - file("src/main/jastadd/RelAst.relast") - outputs.files file("./src/gen/jastadd/RelAst.ast"), - file("src/gen/jastadd/RelAst.jadd"), - file("src/gen/jastadd/RelAstRefResolver.jadd"), - file('src/gen/jastadd/RelAstResolverStubs.jrag') + ] + relastInputFiles + + inputs.files relastInputFiles + outputs.files relastOutputFiles } jastadd { configureModuleBuild() modules { //noinspection GroovyAssignabilityCheck - module("RelAst") { - - java { - basedir "." - include "src/main/**/*.java" - include "src/gen/**/*.java" - } + module("Preprocessor") { jastadd { - basedir "." include "src/main/jastadd/**/*.ast" include "src/main/jastadd/**/*.jadd" include "src/main/jastadd/**/*.jrag" @@ -105,10 +141,10 @@ jastadd { } scanner { - include "src/main/jastadd/scanner/Header.flex", [-4] - include "src/main/jastadd/scanner/Preamble.flex", [-3] - include "src/main/jastadd/scanner/Macros.flex", [-2] - include "src/main/jastadd/scanner/RulesPreamble.flex", [-1] + include "src/main/jastadd/scanner/Header.flex", [-4] + include "src/main/jastadd/scanner/Preamble.flex", [-3] + include "src/main/jastadd/scanner/Macros.flex", [-2] + include "src/main/jastadd/scanner/RulesPreamble.flex", [-1] include "src/main/jastadd/scanner/Keywords.flex", [0] include "src/main/jastadd/scanner/Symbols.flex", [1] include "src/main/jastadd/scanner/RulesPostamble.flex", [2] @@ -122,8 +158,8 @@ jastadd { } cleanGen.doFirst { - delete "src/gen/java/org" - delete "src/gen-res/BuildInfo.properties" + delete "src/gen" + delete "src/gen-res" } preprocessParser.doFirst { @@ -132,7 +168,7 @@ jastadd { } - module = "RelAst" + module = "Preprocessor" astPackage = 'org.jastadd.relast.ast' @@ -149,3 +185,11 @@ jastadd { } generateAst.dependsOn relast + +clean.dependsOn(cleanGen) + +modelJar.dependsOn(generateAst, modelClasses) +modelClasses.dependsOn(generateAst) +compileJava.dependsOn(modelJar) + +jar.dependsOn(modelJar) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1b16c34a71cf212ed0cfb883d14d1b8511903eb2..be52383ef49cdf484098989f96738b3d82d7810d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/libs/relast.jar b/libs/relast.jar index df0f6ce751cc1351525ff1d46cf09e83185adfe5..9f1d60c7c99a1e35d9cf5558d5f329c5aa7ba66e 100644 Binary files a/libs/relast.jar and b/libs/relast.jar differ diff --git a/pages/.gitignore b/pages/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a95071c4b0fd05bccf528339a15b2e93458bc694 --- /dev/null +++ b/pages/.gitignore @@ -0,0 +1 @@ +/docs/ragdoc/ diff --git a/pages/custom_theme/footer.html b/pages/custom_theme/footer.html new file mode 100644 index 0000000000000000000000000000000000000000..104412a1203422ecedc0acda6149a88fb2d72eaa --- /dev/null +++ b/pages/custom_theme/footer.html @@ -0,0 +1,12 @@ +{% block footer %} +<p>{% if config.copyright %} +<small>{{ config.copyright }}<br></small> +{% endif %} +<hr> +Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>. +{% if page and page.meta and page.meta.git_revision_date_localized %} +<small><br><i>Last updated {{ page.meta.git_revision_date_localized }}</i></small> +{% endif %} +</p> +{% endblock %} + diff --git a/pages/docs/index.md b/pages/docs/index.md new file mode 100644 index 0000000000000000000000000000000000000000..ad4a5ac5810d184f65cb4e9a29e134235c89f9ca --- /dev/null +++ b/pages/docs/index.md @@ -0,0 +1,4 @@ +# Relational RAGs Preprocessor Documentation + +The [Relational RAGs Preprocessor](https://git-st.inf.tu-dresden.de/jastadd/relast-preprocessor) is a framework to create custom preprocessors for [JastAdd](http://jastadd.org) and JastAdd-based [Relational RAGs](http://www.relational-rags.eu). + diff --git a/pages/docs/using.md b/pages/docs/using.md new file mode 100644 index 0000000000000000000000000000000000000000..a42891831f635201bd179762e0fa9fc01ed91774 --- /dev/null +++ b/pages/docs/using.md @@ -0,0 +1,200 @@ +# Creating a Preprocessor + +## Project Structure + +The *gradle* build system is required to use the preprocessor. +It is recommended to use the same version of gradle as the preprocessor does internally, which currently is 6.7. + +To create a new preprocessor, follow the following steps: + +1. Create a new gradle project +2. Include this repository as a submodule in the directory `relast.preprocessor` + - copy the contents of this repository into this directory; this can be done using a git submodule + - in your `settings.gradle` add a dependency to it: + ```groovy + include 'relast.preprocessor' + ``` +3. Adapt the `build.gradle` file of your project to use the subproject; this step is described in the next section + +## Constructing a Gradle Build File + +The preprocessor and its test infrastructure are included as project dependencies. + +```groovy +dependencies { + implementation project(':relast.preprocessor') + testImplementation testFixtures(project(":relast.preprocessor")) +} +``` + +Next, the RelAST preprocessor must be called using files provided by the framework and potentially other extensions of the RelAST gramamr DSL. + +```groovy +// Input and output files for relast +def relastInputFiles = [ + "relast.preprocessor/src/main/jastadd/RelAst.relast", + "relast.preprocessor/src/main/jastadd/mustache/Mustache.relast" + // add more files here +] +def relastOutputFiles = [ + "src/gen/jastadd/RelAst.ast", + "src/gen/jastadd/RelAst.jadd", + "src/gen/jastadd/RelAstRefResolver.jadd", + "src/gen/jastadd/RelAstResolverStubs.jrag" +] + +task relast(type: JavaExec) { + classpath = files("relast.preprocessor/libs/relast.jar") + group = 'Build' + + doFirst { + delete relastOutputFiles + mkdir "src/gen/jastadd/" + } + + args = [ + "--listClass=java.util.ArrayList", + "--jastAddList=JastAddList", + "--useJastAddNames", + "--file", + "--resolverHelper", + "--grammarName=./src/gen/jastadd/RelAst" + ] + relastInputFiles + + inputs.files relastInputFiles + outputs.files relastOutputFiles +} +``` + +Finally, JastAdd must be configured to use the correct files. +Note that the scanner includes are split into several files to allow for an extensible lexical analysis. + +```groovy +jastadd { + configureModuleBuild() + modules { + //noinspection GroovyAssignabilityCheck + module("Preprocessor") { + + jastadd { + basedir "." + include "relast.preprocessor/src/main/jastadd/**/*.ast" + include "relast.preprocessor/src/main/jastadd/**/*.jadd" + include "relast.preprocessor/src/main/jastadd/**/*.jrag" + include "src/main/jastadd/**/*.ast" + include "src/main/jastadd/**/*.jadd" + include "src/main/jastadd/**/*.jrag" + include "src/gen/jastadd/**/*.ast" + include "src/gen/jastadd/**/*.jadd" + include "src/gen/jastadd/**/*.jrag" + } + + scanner { + include "relast.preprocessor/src/main/jastadd/scanner/Header.flex", [-4] + include "relast.preprocessor/src/main/jastadd/scanner/Preamble.flex", [-3] + include "relast.preprocessor/src/main/jastadd/scanner/Macros.flex", [-2] + include "relast.preprocessor/src/main/jastadd/scanner/RulesPreamble.flex", [-1] + include "relast.preprocessor/src/main/jastadd/scanner/Keywords.flex", [ 0] + include "relast.preprocessor/src/main/jastadd/scanner/Symbols.flex", [ 1] + include "relast.preprocessor/src/main/jastadd/scanner/RulesPostamble.flex", [ 2] + } + + parser { + include "relast.preprocessor/src/main/jastadd/parser/Preamble.parser" + include "relast.preprocessor/src/main/jastadd/parser/RelAst.parser" + } + } + } + + cleanGen.doFirst { + delete "src/gen" + delete "src/gen-res" + } + + preprocessParser.doFirst { + args += ["--no-beaver-symbol"] + } + + module = "Preprocessor" + astPackage = 'org.jastadd.relast.ast' + parser.name = 'RelAstParser' + genDir = 'src/gen/java' + buildInfoDir = 'src/gen-res' + scanner.genDir = "src/gen/java/org/jastadd/relast/scanner" + parser.genDir = "src/gen/java/org/jastadd/relast/parser" + jastaddOptions = ["--lineColumnNumbers", "--List=JastAddList", "--safeLazy", "--visitCheck=true", "--rewrite=cnta", "--cache=all"] +} +``` + +## Writing Tests + +The preprocessor provides a test framework based on on JUnit 5. +This framework can be used to write acceptance tests, comparing input and output of runs of the constructed preprocessor. +Hereby, no java code has to be written - only a simple configuration file and some input and expected data have to be provided. + +### Test Class + +Create a new class inheriting from `RelAstProcessorTestBase` and set the `mainClass` field to the correct class. + +```java +import org.jastadd.relast.tests.RelAstProcessorTestBase; +import org.junit.jupiter.api.BeforeAll; + +public class MyPreprocessorTest extends RelAstProcessorTestBase { + @BeforeAll + static void init() { + mainClass = Main.class; + } +} +``` + +### Test DSL + +Tests are described by a config file. Each directory in `src/main/resources` containing a `config.yaml` is assumed to contain test data. + +A test configuration must contain a list of objects containing the following properties: + +| Name | Type | Required | Default Value | Description | +| -------------- | --------------------- | -------- | ------------- | ---------------------------------------------------- | +| | | | | | +| `name` | string | yes | -- | name of the test case | +| `args` | list of strings | yes | -- | arguments passed to the preprocessor | +| `fail` | boolean | no | `false` | is the test a negative test | +| | | | | | +| `in` | string | no | `"in"` | directory containing input data | +| `out` | string | no | `"out"` | directory the output is written to | +| `expected` | string | no | `"expected"` | directory containing expected data | +| | | | | | +| `compare` | boolean | no | `false` | should `out` and `expected` directories be compared? | +| | | | | | +| `out-contains` | list of strings | no | empty list | string that must be contained in the `stdout` stream | +| `err-contains` | list of strings | no | empty list | string that must be contained in the `stderr` stream | +| `out-matches` | list of regex strings | no | empty list | regular expression the `stdout` stream must match | +| `err-matches` | list of regex strings | no | empty list | regular expression the `stderr` stream must match | +| | | | | | + +Example: + +```yaml +- name: "SimpleInheritance (null)" + compare: true + out: "null/out" + expected: "null/expected" + args: + - "--inputBaseDir=in" + - "--outputBaseDir=null/out" + - "--printYaml" + - "--errorHandling=null" + - "Example.relast" +- name: "SimpleInheritance (wrong exception)" + fail: true + err-matches: + - "(?s).*Invalid.*" + err-contains: + - "Invalid argument 'not.a.Class' for parameter 'errorHandling': Class not found (name must be qualified)." + args: + - "--inputBaseDir=in" + - "--outputBaseDir=out/does_not_matter_will_fail" + - "--errorHandling=not.a.Class" + - "Example.relast" +``` diff --git a/pages/mkdocs.yml b/pages/mkdocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..d31d6f91a1d8d278541c5e299ad0c06a5040194e --- /dev/null +++ b/pages/mkdocs.yml @@ -0,0 +1,18 @@ +site_name: Relational RAGs Preprocessor Documentation +nav: + - using.md + - API documentation: ragdoc/index.html +theme: + name: readthedocs + custom_dir: custom_theme/ +plugins: + - search + - git-revision-date-localized: + type: datetime + timezone: Europe/Berlin + locale: en + fallback_to_build_date: True + - macros +repo_url: https://git-st.inf.tu-dresden.de/jastadd/relast-preprocessor +site_dir: ../public + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000000000000000000000000000000000..5f99c5a83ca1b73205acb1de3960ccc501af1cc4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +pluginManagement { + plugins { + id 'org.jastadd' version '1.13.3' + } +} diff --git a/src/main/jastadd/backend/AbstractGrammar.jadd b/src/main/jastadd/backend/AbstractGrammar.jadd index da803c5448c6129d4123bc55ad80cc1abedcc6e9..3683fe27c2fba45459a5e7eb325f62ab1ad8e102 100644 --- a/src/main/jastadd/backend/AbstractGrammar.jadd +++ b/src/main/jastadd/backend/AbstractGrammar.jadd @@ -43,14 +43,16 @@ aspect BackendAbstractGrammar { if (getAbstract()) { b.append("abstract "); } - b.append(getName()).append(" "); + b.append(getName()); if (hasSuperType()) { - b.append(": ").append(getSuperType().getName()).append(" "); + b.append(" : ").append(getSuperType().getName()); } - b.append("::="); - for (Component component : getComponentList()) { - b.append(" "); - component.generateAbstractGrammar(b); + if (getNumComponent() > 0) { + b.append(" ::="); + for (Component component : getComponentList()) { + b.append(" "); + component.generateAbstractGrammar(b); + } } b.append(";"); super.generateAbstractGrammar(b); @@ -62,8 +64,7 @@ aspect BackendAbstractGrammar { if (getNTA()) { b.append("/"); } - - if (!getName().equals("")) { + if (!getName().equals("") && !getName().equals(getTypeDecl().getName())) { b.append(getName()).append(":"); } b.append(getTypeDecl().getName()); @@ -76,8 +77,7 @@ aspect BackendAbstractGrammar { if (getNTA()) { b.append("/"); } - - if (!getName().equals("")) { + if (!getName().equals("") && !getName().equals(getTypeDecl().getName())) { b.append(getName()).append(":"); } b.append(getTypeDecl().getName()).append("*"); @@ -91,7 +91,7 @@ aspect BackendAbstractGrammar { b.append("/"); } b.append("["); - if (!getName().equals("")) { + if (!getName().equals("") && !getName().equals(getTypeDecl().getName())) { b.append(getName()).append(":"); } b.append(getTypeDecl().getName()).append("]"); @@ -187,7 +187,7 @@ aspect BackendAbstractGrammar { } public void SingleLineComment.generateAbstractGrammar(StringBuilder b) { - b.append("//").append(getText()).append("\n"); + b.append("//").append(getText()); } public void MultiLineComment.generateAbstractGrammar(StringBuilder b) { diff --git a/src/main/jastadd/mustache b/src/main/jastadd/mustache new file mode 160000 index 0000000000000000000000000000000000000000..c10bed0d03e3fa18b8133ce1de48de7646899615 --- /dev/null +++ b/src/main/jastadd/mustache @@ -0,0 +1 @@ +Subproject commit c10bed0d03e3fa18b8133ce1de48de7646899615 diff --git a/src/main/java/org/jastadd/JastAddConfiguration.java b/src/main/java/org/jastadd/PreprocessorConfiguration.java similarity index 74% rename from src/main/java/org/jastadd/JastAddConfiguration.java rename to src/main/java/org/jastadd/PreprocessorConfiguration.java index 30774005356c0eb30354b7fb76e1bafe67d00873..39fd43963ecdc1cece0649cf3e1123ed228e4505 100644 --- a/src/main/java/org/jastadd/JastAddConfiguration.java +++ b/src/main/java/org/jastadd/PreprocessorConfiguration.java @@ -28,60 +28,90 @@ package org.jastadd; import org.jastadd.option.ArgumentParser; +import org.jastadd.option.FlagOption; import org.jastadd.option.Option; import java.io.PrintStream; import java.util.*; -import java.util.stream.Collectors; /** * Tracks JastAdd configuration options. * * @author Jesper Öqvist <jesper.oqvist@cs.lth.se> */ -public class JastAddConfiguration extends org.jastadd.Configuration { +public class PreprocessorConfiguration extends org.jastadd.Configuration { /** * Indicates if there were unknown command-line options */ final boolean unknownOptions; - - private boolean isJastAddCompliant; - - public boolean isJastAddCompliant() { - return isJastAddCompliant; - } - + private final Map<String, Option<?>> options = new HashMap<>(); + private final boolean isJastAddCompliant; + private final ArgumentParser argParser; /** * Parse options from an argument list. * * @param args Command-line arguments to build configuration from * @param err output stream to print configuration warnings to */ - public JastAddConfiguration(String[] args, PrintStream err, boolean isJastAddCompliant, Collection<Option<?>> extraOptions) { - ArgumentParser argParser = new ArgumentParser(); + public PreprocessorConfiguration(String[] args, PrintStream err, boolean isJastAddCompliant, Collection<Option<?>> extraOptions) { + argParser = new ArgumentParser(); this.isJastAddCompliant = isJastAddCompliant; + if (isJastAddCompliant) { Collection<Option<?>> jastAddOptions = allJastAddOptions(); + for (Option<?> o : jastAddOptions) { + options.put(o.name(), o); + } argParser.addOptions(jastAddOptions); + } - // if the JastAdd options are supported, we have to check for duplicates! - Set<String> jastAddOptionNames = jastAddOptions.stream().map(o -> o.name()).collect(Collectors.toSet()); - for (Option option : extraOptions) { - if (jastAddOptionNames.contains(option.name())) { - System.err.println("Unable to add option '" + option.name() + "', because there is a JastAdd option with the same name."); - } else { - argParser.addOption(option); + // if the JastAdd options are supported, we have to check for duplicates! + for (Option option : extraOptions) { + if (options.containsKey(option.name())) { + System.err.println("Unable to add option '" + option.name() + "', because there is a JastAdd option with the same name."); + } else { + if (option.name().equals("help") && option instanceof FlagOption) { + this.helpOption = (FlagOption) option; + } else if (option.name().equals("version") && option instanceof FlagOption) { + this.versionOption = (FlagOption) option; } + argParser.addOption(option); + options.put(option.name(), option); } - } else { - argParser.addOptions(extraOptions); } unknownOptions = !argParser.parseArgs(args, err); filenames = argParser.getFilenames(); } + public ArgumentParser getArgParser() { + return argParser; + } + + public Optional<Option> getOption(String name) { + return options.containsKey(name) ? Optional.of(options.get(name)) : Optional.empty(); + } + + public boolean isJastAddCompliant() { + return isJastAddCompliant; + } + + /** + * Print help + * + * @param out Output stream to print help to. + */ + @Override + public void printHelp(PrintStream out) { + out.println("This program reads a number of .jrag, .jadd, and .ast files"); + out.println("Options:"); + argParser.printHelp(out); + out.println(); + out.println("Arguments:"); + out.println(" Names of abstract grammr (.ast) and aspect (.jrag and .jadd) files."); + } + /** * @return all files */ diff --git a/src/main/java/org/jastadd/relast/compiler/AbstractCompiler.java b/src/main/java/org/jastadd/relast/compiler/AbstractCompiler.java index adc9bde23565b83df6fe6885d6a5d92d3bbad8b3..e9d55b5665735b74f05abe433c3bf1edc980ab57 100644 --- a/src/main/java/org/jastadd/relast/compiler/AbstractCompiler.java +++ b/src/main/java/org/jastadd/relast/compiler/AbstractCompiler.java @@ -1,27 +1,26 @@ package org.jastadd.relast.compiler; -import org.jastadd.JastAddConfiguration; +import org.jastadd.PreprocessorConfiguration; +import org.jastadd.option.FlagOption; import org.jastadd.option.Option; import java.util.ArrayList; +import java.util.MissingResourceException; +import java.util.ResourceBundle; public abstract class AbstractCompiler { private final boolean jastAddCompliant; - protected ArrayList<Option<?>> options; private final String name; + protected ArrayList<Option<?>> options; + private PreprocessorConfiguration configuration; - private JastAddConfiguration configuration; - - public AbstractCompiler(String name, boolean jastaddCompliant) { + protected AbstractCompiler(String name, boolean jastaddCompliant) { this.name = name; this.jastAddCompliant = jastaddCompliant; } - public JastAddConfiguration getConfiguration() throws CompilerException { - if (configuration == null) { - throw new CompilerException("Configuration only supported for JastAdd-compliant compilers!"); - } + public PreprocessorConfiguration getConfiguration() { return configuration; } @@ -29,7 +28,22 @@ public abstract class AbstractCompiler { options = new ArrayList<>(); initOptions(); - configuration = new JastAddConfiguration(args, System.err, jastAddCompliant, options); + configuration = new PreprocessorConfiguration(args, System.err, jastAddCompliant, options); + + if (configuration.shouldPrintHelp()) { + configuration.printHelp(System.out); + return 0; + } + + if (configuration.shouldPrintVersion()) { + try { + ResourceBundle resources = ResourceBundle.getBundle("preprocessor"); + System.out.println(getName() + ", version " + resources.getString("version")); + } catch (MissingResourceException e) { + System.out.println(getName() + ", unknown version"); + } + return 0; + } return compile(); } @@ -37,10 +51,13 @@ public abstract class AbstractCompiler { protected abstract int compile() throws CompilerException; protected void initOptions() { - // there are no options by default + if (!jastAddCompliant) { + addOption(new FlagOption("version", "print version info")); + addOption(new FlagOption("help", "print command-line usage info")); + } } - protected <OptionType extends Option<?>> OptionType addOption(OptionType option) { + protected <O extends Option<?>> O addOption(O option) { options.add(option); return option; } diff --git a/src/main/java/org/jastadd/relast/compiler/Mustache.java b/src/main/java/org/jastadd/relast/compiler/Mustache.java new file mode 100644 index 0000000000000000000000000000000000000000..24d368abf9d13cbca1882caef1a49084943499a9 --- /dev/null +++ b/src/main/java/org/jastadd/relast/compiler/Mustache.java @@ -0,0 +1,50 @@ +package org.jastadd.relast.compiler; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Template; +import com.github.jknack.handlebars.io.ClassPathTemplateLoader; +import com.github.jknack.handlebars.io.TemplateLoader; +import org.yaml.snakeyaml.Yaml; + +import java.io.*; +import java.nio.file.Paths; + +public class Mustache { + + private Mustache() { + // hide public constructor + } + + public static void javaMustache(String templateFileName, File yamlFile, String outputFileName) throws IOException { + + //noinspection ResultOfMethodCallIgnored + Paths.get(outputFileName).getParent().toFile().mkdirs(); // create directory structure if necessary + + Object context = new Yaml().load(new FileReader(yamlFile)); + applyTemplate(templateFileName, outputFileName, context); + } + + public static void javaMustache(String templateFileName, String yaml, String outputFileName) throws IOException { + + //noinspection ResultOfMethodCallIgnored + Paths.get(outputFileName).getParent().toFile().mkdirs(); // create directory structure if necessary + + Object context = new Yaml().load(new StringReader(yaml)); + applyTemplate(templateFileName, outputFileName, context); + } + + private static void applyTemplate(String templateFileName, String outputFileName, Object context) throws IOException { + TemplateLoader loader = new ClassPathTemplateLoader(); + loader.setSuffix(".mustache"); // the default is ".hbs" + + Handlebars handlebars = new Handlebars(loader); + handlebars.prettyPrint(true); // set handlebars to mustache mode (skip some whitespace) + Template template = handlebars.compile(templateFileName); + + try (Writer w = new FileWriter(outputFileName)) { + template.apply(context, w); + w.flush(); + } + } + +} diff --git a/src/main/java/org/jastadd/relast/compiler/RelAstProcessor.java b/src/main/java/org/jastadd/relast/compiler/RelAstProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..943583517f406e9b557e3bb34262b15185b38438 --- /dev/null +++ b/src/main/java/org/jastadd/relast/compiler/RelAstProcessor.java @@ -0,0 +1,133 @@ +package org.jastadd.relast.compiler; + +import org.jastadd.option.ValueOption; +import org.jastadd.relast.ast.GrammarFile; +import org.jastadd.relast.ast.Program; +import org.jastadd.relast.parser.RelAstParser; +import org.jastadd.relast.scanner.RelAstScanner; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; + +public abstract class RelAstProcessor extends AbstractCompiler { + + protected ValueOption optionOutputBaseDir; + protected ValueOption optionInputBaseDir; + + protected RelAstProcessor(String name, boolean jastAddCompliant) { + super(name, jastAddCompliant); + } + + protected boolean isGrammarFile(String fileName) { + String extension = fileName.subSequence(fileName.lastIndexOf('.'), fileName.length()).toString(); + return extension.equals(".relast") || extension.equals(".ast"); + } + + @Override + protected void initOptions() { + optionOutputBaseDir = addOption(new ValueOption("outputBaseDir", "base directory for generated files")); + optionInputBaseDir = addOption(new ValueOption("inputBaseDir", "base directory for input files")); + super.initOptions(); + } + + @Override + protected int compile() throws CompilerException { + final Path inputBasePath; + if (optionInputBaseDir.isMatched()) { + inputBasePath = Paths.get(optionInputBaseDir.value()).toAbsolutePath(); + } else { + inputBasePath = Paths.get(".").toAbsolutePath(); + printMessage("No input base dir is set. Assuming current directory '" + inputBasePath.toAbsolutePath().toString() + "'."); + } + + if (!inputBasePath.toFile().exists()) { + printMessage("Input path '" + inputBasePath.toAbsolutePath().toString() + "' does not exist. Exiting..."); + System.exit(-1); + } else if (!inputBasePath.toFile().isDirectory()) { + printMessage("Input path '" + inputBasePath.toAbsolutePath().toString() + "' is not a directory. Exiting..."); + System.exit(-1); + } + + final Path outputBasePath; + if (optionOutputBaseDir.isMatched()) { + outputBasePath = Paths.get(optionOutputBaseDir.value()).toAbsolutePath(); + } else { + throw new CompilerException("No output base dir is set."); + } + + if (outputBasePath.toFile().exists() && !outputBasePath.toFile().isDirectory()) { + printMessage("Output path '" + inputBasePath.toAbsolutePath().toString() + "' exists, but is not a directory. Exiting..."); + } + + printMessage("Running " + getName()); + + // gather all files + Collection<Path> inputFiles = new ArrayList<>(); + getConfiguration().getFiles().forEach(name -> relativizeFileName(inputBasePath, Paths.get(name)).ifPresent(inputFiles::add)); + + + Program program = parseProgram(inputFiles); + + return processGrammar(program, inputBasePath, outputBasePath); + } + + protected abstract int processGrammar(Program program, Path inputBasePath, Path outputBasePath) throws CompilerException; + + private Optional<Path> relativizeFileName(Path inputBasePath, Path filePath) { + if (filePath.isAbsolute()) { + if (filePath.startsWith(inputBasePath)) { + return Optional.of(filePath.relativize(inputBasePath)); + } else { + printMessage("Path '" + filePath + "' is not contained in the base path '" + inputBasePath + "'."); + return Optional.empty(); + } + } else { + return Optional.of(inputBasePath.resolve(filePath)); + } + } + + protected void printMessage(String message) { + System.out.println(message); + } + + protected void writeToFile(Path path, String str) throws CompilerException { + //noinspection ResultOfMethodCallIgnored + path.getParent().toFile().mkdirs(); // create directory structure if necessary + try (PrintWriter writer = new PrintWriter(path.toFile())) { + writer.print(str); + } catch (Exception e) { + throw new CompilerException("Could not write to file " + path, e); + } + } + + private Program parseProgram(Collection<Path> inputFiles) { + Program program = new Program(); + + RelAstParser parser = new RelAstParser(); + + inputFiles.stream().filter(path -> isGrammarFile(path.toString())).forEach( + path -> { + try (BufferedReader reader = Files.newBufferedReader(path)) { + RelAstScanner scanner = new RelAstScanner(reader); + GrammarFile inputGrammar = (GrammarFile) parser.parse(scanner); + inputGrammar.setFileName(path.toString()); + program.addGrammarFile(inputGrammar); + inputGrammar.treeResolveAll(); + } catch (IOException | beaver.Parser.Exception e) { + printMessage("Could not parse grammar file " + path); + e.printStackTrace(); + } + } + ); + + return program; + } +} + diff --git a/src/main/java/org/jastadd/relast/compiler/RelastSourceToSourceCompiler.java b/src/main/java/org/jastadd/relast/compiler/RelastSourceToSourceCompiler.java index 7d6949f5ff7ec74d2e1963e564bff9d633d4fbec..696c3c17ef59872ffaf174d4b8e4b4b7e8e9bdd1 100644 --- a/src/main/java/org/jastadd/relast/compiler/RelastSourceToSourceCompiler.java +++ b/src/main/java/org/jastadd/relast/compiler/RelastSourceToSourceCompiler.java @@ -1,144 +1,44 @@ package org.jastadd.relast.compiler; -import beaver.Parser; -import org.jastadd.option.ValueOption; import org.jastadd.relast.ast.GrammarFile; import org.jastadd.relast.ast.Program; -import org.jastadd.relast.parser.RelAstParser; -import org.jastadd.relast.scanner.RelAstScanner; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Optional; -public class RelastSourceToSourceCompiler extends AbstractCompiler { +public class RelastSourceToSourceCompiler extends RelAstProcessor { - protected ValueOption optionOutputBaseDir; - protected ValueOption optionInputBaseDir; - public RelastSourceToSourceCompiler(String name, boolean jastAddCompliant) { super(name, jastAddCompliant); } public static void main(String[] args) { try { - new RelastSourceToSourceCompiler("relast-preprocessor", true).run(args); + new RelastSourceToSourceCompiler("Relational RAGs Source-To-Source Compiler", false).run(args); } catch (CompilerException e) { System.err.println(e.getMessage()); System.exit(-1); } } - protected static boolean isGrammarFile(String fileName) { + @Override + protected boolean isGrammarFile(String fileName) { String extension = fileName.subSequence(fileName.lastIndexOf("."), fileName.length()).toString(); return extension.equals(".relast") || extension.equals(".ast"); } @Override - protected void initOptions() { - optionOutputBaseDir = addOption(new ValueOption("outputBaseDir", "base directory for generated files")); - optionInputBaseDir = addOption(new ValueOption("inputBaseDir", "base directory for input files")); - } - - @Override - protected int compile() throws CompilerException { - final Path inputBasePath; - if (optionInputBaseDir.isMatched()) { - inputBasePath = Paths.get(optionInputBaseDir.value()).toAbsolutePath(); - } else { - inputBasePath = Paths.get(".").toAbsolutePath(); - printMessage("No input base dir is set. Assuming current directory '" + inputBasePath.toAbsolutePath().toString() + "'."); - } - - if (!inputBasePath.toFile().exists()) { - printMessage("Input path '" + inputBasePath.toAbsolutePath().toString() + "' does not exist. Exiting..."); - System.exit(-1); - } else if (!inputBasePath.toFile().isDirectory()) { - printMessage("Input path '" + inputBasePath.toAbsolutePath().toString() + "' is not a directory. Exiting..."); - System.exit(-1); - } - - final Path outputBasePath; - if (optionOutputBaseDir.isMatched()) { - outputBasePath = Paths.get(optionOutputBaseDir.value()).toAbsolutePath(); - } else { - throw new CompilerException("No output base dir is set."); - } - - if (outputBasePath.toFile().exists() && !outputBasePath.toFile().isDirectory()) { - printMessage("Output path '" + inputBasePath.toAbsolutePath().toString() + "' exists, but is not a directory. Exiting..."); - } - - printMessage("Running RelAST Preprocessor"); - - // gather all files - Collection<Path> inputFiles = new ArrayList<>(); - getConfiguration().getFiles().forEach(name -> relativizeFileName(inputBasePath, Paths.get(name)).ifPresent(path -> inputFiles.add(path))); - - - Program program = parseProgram(inputFiles); + protected int processGrammar(Program program, Path inputBasePath, Path outputBasePath) throws CompilerException { printMessage("Writing output files"); for (GrammarFile grammarFile : program.getGrammarFileList()) { + printMessage("Writing output file " + grammarFile.getFileName()); // TODO decide and document what the file name should be, the full path or a simple name? writeToFile(outputBasePath.resolve(inputBasePath.relativize(Paths.get(grammarFile.getFileName()))), grammarFile.generateAbstractGrammar()); } return 0; } - - private Optional<Path> relativizeFileName(Path inputBasePath, Path filePath) { - if (filePath.isAbsolute()) { - if (filePath.startsWith(inputBasePath)) { - return Optional.of(filePath.relativize(inputBasePath)); - } else { - printMessage("Path '" + filePath + "' is not contained in the base path '" + inputBasePath + "'."); - return Optional.empty(); - } - } else { - return Optional.of(inputBasePath.resolve(filePath)); - } - } - - private void printMessage(String message) { - System.out.println(message); - } - - private void writeToFile(Path path, String str) throws CompilerException { - try (PrintWriter writer = new PrintWriter(path.toFile())) { - writer.print(str); - } catch (Exception e) { - throw new CompilerException("Could not write to file " + path, e); - } - } - - private Program parseProgram(Collection<Path> inputFiles) throws CompilerException { - Program program = new Program(); - - RelAstParser parser = new RelAstParser(); - inputFiles.stream().filter(path -> isGrammarFile(path.toString())).forEach( - path -> { - try (BufferedReader reader = Files.newBufferedReader(path)) { - RelAstScanner scanner = new RelAstScanner(reader); - GrammarFile inputGrammar = (GrammarFile) parser.parse(scanner); - inputGrammar.setFileName(path.toString()); - program.addGrammarFile(inputGrammar); - inputGrammar.treeResolveAll(); - } catch (IOException | Parser.Exception e) { - printMessage("Could not parse grammar file " + path); - e.printStackTrace(); - } - } - ); - - return program; -} } diff --git a/src/main/java/org/jastadd/relast/compiler/Utils.java b/src/main/java/org/jastadd/relast/compiler/Utils.java deleted file mode 100644 index 8e2ef0c8bcb8266fe5789b2608f4a6f78c47548a..0000000000000000000000000000000000000000 --- a/src/main/java/org/jastadd/relast/compiler/Utils.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.jastadd.relast.compiler; - -import java.util.*; -import java.util.function.Predicate; - -import static java.util.stream.Collectors.toList; - -public class Utils { - public static <T> List<T> filterToList(Collection<T> collection, Predicate<T> predicate) { - return collection.stream().filter(predicate).collect(toList()); - } - - public static <T> Set<T> asSet(T... t) { - return new HashSet<T>(Arrays.asList(t)); - } -} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml deleted file mode 100644 index 98cfd73c75df58d8598521bc10b043e214ec4ad8..0000000000000000000000000000000000000000 --- a/src/main/resources/log4j2.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<Configuration status="INFO"> - <Appenders> - <Console name="Console" target="SYSTEM_OUT"> - <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> - </Console> - </Appenders> - <Loggers> - <Root level="info"> - <AppenderRef ref="Console"/> - </Root> - </Loggers> -</Configuration> \ No newline at end of file diff --git a/src/main/resources/preprocessor.properties b/src/main/resources/preprocessor.properties new file mode 100644 index 0000000000000000000000000000000000000000..fb55bf09edd40713c458341e14b57714f11924db --- /dev/null +++ b/src/main/resources/preprocessor.properties @@ -0,0 +1 @@ +version=1.0.0-pre-release diff --git a/src/test/java/org/jastadd/relast/tests/PreprocessorTest.java b/src/test/java/org/jastadd/relast/tests/PreprocessorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b58349632e19b3caf3848ae3fff836673c441221 --- /dev/null +++ b/src/test/java/org/jastadd/relast/tests/PreprocessorTest.java @@ -0,0 +1,13 @@ +package org.jastadd.relast.tests; + +import org.jastadd.relast.compiler.RelastSourceToSourceCompiler; +import org.junit.jupiter.api.BeforeAll; + +public class PreprocessorTest extends RelAstProcessorTestBase { + + @BeforeAll + static void init() { + mainClass = RelastSourceToSourceCompiler.class; + } + +} diff --git a/src/test/java/org/jastadd/ros2rag/tests/RelAstTest.java b/src/test/java/org/jastadd/ros2rag/tests/RelAstTest.java deleted file mode 100644 index c87e51f7dbc70644b621998548d4f942160e0ea9..0000000000000000000000000000000000000000 --- a/src/test/java/org/jastadd/ros2rag/tests/RelAstTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.jastadd.ros2rag.tests; - -import org.jastadd.relast.compiler.CompilerException; -import org.jastadd.relast.compiler.RelastSourceToSourceCompiler; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.nio.file.Paths; -import java.util.Objects; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class RelAstTest { - - void transform(boolean jastAddCompliant, String inputDir, String outputDir) throws CompilerException { - - System.out.println("Running test in directory '" + Paths.get(".").toAbsolutePath() + "'."); - assertTrue(Paths.get(inputDir).toFile().exists(), "input directory does not exist"); - assertTrue(Paths.get(inputDir).toFile().isDirectory(), "input directory is not a directory"); - - File outputDirFile = Paths.get(outputDir).toFile(); - if (outputDirFile.exists()) { - assertTrue(outputDirFile.isDirectory()); - if (Objects.requireNonNull(outputDirFile.list(), "Could not read output directory").length != 0) { - System.out.println("output directory is not empty!"); - } - } else { - assertTrue(outputDirFile.mkdir()); - } - - String[] args = { - "--outputBaseDir=" + outputDir, - "--inputBaseDir=" + inputDir, - "Example.relast" - }; - - new RelastSourceToSourceCompiler("testCompiler", jastAddCompliant).run(args); - } - - @Test - void transformMinimalExample() throws CompilerException { - transform(false,"src/test/resources/in", "src/test/resources/out-simple"); - transform(true,"src/test/resources/in", "src/test/resources/out-compliant"); - } -} diff --git a/src/test/resources/Comments/config.yaml b/src/test/resources/Comments/config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6095c8da742b0eaa2f636aeee218d77d37221859 --- /dev/null +++ b/src/test/resources/Comments/config.yaml @@ -0,0 +1,8 @@ +- name: "Parse and reprint tests" + args: + - "--inputBaseDir=in" + - "--outputBaseDir=out" + - "CommentsA.relast" + out: "out" + expected: "in" + compare: true diff --git a/src/test/resources/Comments/in/CommentsA.relast b/src/test/resources/Comments/in/CommentsA.relast new file mode 100644 index 0000000000000000000000000000000000000000..37e963fdf8a7ca9b1613deca9bc46923f29f00d2 --- /dev/null +++ b/src/test/resources/Comments/in/CommentsA.relast @@ -0,0 +1,15 @@ +// this file only contains comments +// + +// + +// like this one +/* or this one */ +/* or this + one */ +/**/ +//test +/*test*/ + +// + diff --git a/src/test/resources/MinimalGrammar/config.yaml b/src/test/resources/MinimalGrammar/config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..135f6bd74b057a00cc5582b7ca78e83995167e48 --- /dev/null +++ b/src/test/resources/MinimalGrammar/config.yaml @@ -0,0 +1,9 @@ +- name: "Parse and reprint tests" + args: + - "--inputBaseDir=in" + - "--outputBaseDir=out" + - "Example.relast" + - "CommentInFront.relast" + out: "out" + expected: "in" + compare: true diff --git a/src/test/resources/MinimalGrammar/in/CommentInFront.relast b/src/test/resources/MinimalGrammar/in/CommentInFront.relast new file mode 100644 index 0000000000000000000000000000000000000000..96c5eecaae626aa6903b7d0b9641916688c7a997 --- /dev/null +++ b/src/test/resources/MinimalGrammar/in/CommentInFront.relast @@ -0,0 +1,2 @@ +// comment +CommentInFront; diff --git a/src/test/resources/MinimalGrammar/in/Example.relast b/src/test/resources/MinimalGrammar/in/Example.relast new file mode 100644 index 0000000000000000000000000000000000000000..7c28b7bcd84c826aedd622460eb93da08ee80c3d --- /dev/null +++ b/src/test/resources/MinimalGrammar/in/Example.relast @@ -0,0 +1,13 @@ +Model ::= RobotArm ZoneModel; + +ZoneModel ::= <Size:IntPosition> SafetyZone:Zone*; + +Zone ::= Coordinate*; + +RobotArm ::= Joint* EndEffector <_AttributeTestSource:int> /<_AppropriateSpeed:double>/; // normally this would be: <AttributeTestSource:int> ; + +Joint ::= <Name> <CurrentPosition:IntPosition>; // normally this would be: <CurrentPosition:IntPosition> + +EndEffector : Joint; + +Coordinate ::= <Position:IntPosition>; diff --git a/src/test/resources/in/Example.relast b/src/test/resources/in/Example.relast deleted file mode 100644 index aa17ad814128cf155d844e0f36f1114b1b1afa33..0000000000000000000000000000000000000000 --- a/src/test/resources/in/Example.relast +++ /dev/null @@ -1,13 +0,0 @@ -Model ::= RobotArm ZoneModel ; - -ZoneModel ::= <Size:IntPosition> SafetyZone:Zone* ; - -Zone ::= Coordinate* ; - -RobotArm ::= Joint* EndEffector <_AttributeTestSource:int> /<_AppropriateSpeed:double>/ ; // normally this would be: <AttributeTestSource:int> ; - -Joint ::= <Name> <CurrentPosition:IntPosition> ; // normally this would be: <CurrentPosition:IntPosition> - -EndEffector : Joint; - -Coordinate ::= <Position:IntPosition> ; diff --git a/src/testFixtures/java/org/jastadd/relast/tests/RelAstProcessorTestBase.java b/src/testFixtures/java/org/jastadd/relast/tests/RelAstProcessorTestBase.java new file mode 100644 index 0000000000000000000000000000000000000000..e83746c04a4a85327eabf5db0a1de580e6a42e3c --- /dev/null +++ b/src/testFixtures/java/org/jastadd/relast/tests/RelAstProcessorTestBase.java @@ -0,0 +1,176 @@ +package org.jastadd.relast.tests; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.FileFilterUtils; +import org.apache.commons.io.filefilter.TrueFileFilter; +import org.assertj.core.util.Files; +import org.jastadd.relast.tests.config.Configuration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Stream; + +public class RelAstProcessorTestBase { + + protected static Class<?> mainClass; + + protected static int runProcess(File workingDirectory, List<String> command, StringBuilder outStringBuider, StringBuilder errStringBuilder) throws IOException, InterruptedException { + + File outFile = Files.newTemporaryFile(); + File errFile = Files.newTemporaryFile(); + + ProcessBuilder pb = new ProcessBuilder(command). + directory(workingDirectory) + .redirectOutput(outFile) + .redirectError(errFile); + + Process p = pb.start(); + try { + p.waitFor(); + } catch (InterruptedException e) { + if (Thread.interrupted()) // Clears interrupted status! + throw e; + } + + try (BufferedReader outReader = new BufferedReader(new FileReader(outFile))) { + String outLine; + while ((outLine = outReader.readLine()) != null) { + outStringBuider.append(outLine).append("\n"); + } + } + + try (BufferedReader errReader = new BufferedReader(new FileReader(errFile))) { + String errLine; + while ((errLine = errReader.readLine()) != null) { + errStringBuilder.append(errLine).append("\n"); + } + } + + return p.exitValue(); + } + + protected static int runJavaProcess(Class<?> klass, File workingDirectory, List<String> args, StringBuilder outStringBuider, StringBuilder errStringBuilder) throws IOException, InterruptedException { + String javaHome = System.getProperty("java.home"); + String javaBin = javaHome + File.separator + "bin" + File.separator + "java"; + String classpath = System.getProperty("java.class.path"); + String className = klass.getName(); + + List<String> command = new LinkedList<>(); + command.add(javaBin); + command.add("-cp"); + command.add(classpath); + command.add(className); + if (args != null) { + command.addAll(args); + } + + System.out.println("Running java -jar -cp [...] " + className + " " + (args != null ? args.stream().reduce((s1, s2) -> s1 + " " + s2).orElse("") : "")); + + return runProcess(workingDirectory, command, outStringBuider, errStringBuilder); + } + + protected void directoryTest(Class<?> mainClass, Path dir) throws IOException, InterruptedException { + dir = dir.toAbsolutePath(); + Path configFile = dir.resolve("config.yaml"); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + List<Configuration> configs = mapper.readValue(configFile.toFile(), new TypeReference<List<Configuration>>() { + }); + + for (Configuration config : configs) { + + FileUtils.forceMkdir(dir.resolve(config.getOut()).toFile()); + FileUtils.cleanDirectory(dir.resolve(config.getOut()).toFile()); + + StringBuilder outBuilder = new StringBuilder(); + StringBuilder errBuilder = new StringBuilder(); + int returnValue = runJavaProcess(mainClass, dir.toFile(), Arrays.asList(config.getArgs()), outBuilder, errBuilder); + String out = outBuilder.toString(); + String err = errBuilder.toString(); + + System.out.println(out); + System.err.println(err); + + if (config.shouldFail()) { + Assertions.assertNotEquals(0, returnValue, config.getName() + ": Zero return value of preprocessor for negative test."); + } else { + Assertions.assertEquals(0, returnValue, config.getName() + ": Non-Zero return value of preprocessor for positive test."); + } + + checkOutput(config, out, err); + + if (config.shouldCompare()) { + Path outPath = dir.resolve(config.getOut()); + Path expectedPath = dir.resolve(config.getExpected()); + comparePaths(outPath, expectedPath); + } + } + } + + private void checkOutput(Configuration config, String out, String err) { + for (String errMatchString : config.getErrMatches()) { + if (!err.matches(errMatchString)) { + Assertions.fail("Error stream does not match '" + errMatchString + "'"); + } + } + + for (String errContainsString : config.getErrContains()) { + if (!err.contains(errContainsString)) { + Assertions.fail("Error stream does not contain '" + errContainsString + "'"); + } + } + + for (String outMatchString : config.getOutMatches()) { + if (!out.matches(outMatchString)) { + Assertions.fail("Output stream does not match '" + outMatchString + "'"); + } + } + + for (String outContainsString : config.getOutContains()) { + if (!out.contains(outContainsString)) { + Assertions.fail("Output stream does not contain '" + outContainsString + "'"); + } + } + } + + private void comparePaths(Path outPath, Path expectedPath) { + final Collection<File> files = FileUtils.listFiles(expectedPath.toFile(), TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE); + files.forEach(f -> { + final Path relative = expectedPath.relativize(f.toPath()); + final String relativePath = relative.toFile().getPath(); + final File bfile = new File(outPath.toFile(), relativePath); + if (bfile.exists()) { + try { + final Charset charset = Charset.defaultCharset(); + final String expected = FileUtils.readFileToString(f, charset); + final String result = FileUtils.readFileToString(bfile, charset); + Assertions.assertEquals(expected, result); + } catch (IOException e) { + Assertions.fail("Unable to compare input files '" + f + "' and '" + bfile + "'", e); + } + } else { + Assertions.fail(relativePath + " expected to exist"); + } + }); + } + + @TestFactory + Stream<DynamicTest> testAll() { + File baseDir = new File("src/test/resources/"); + + Assertions.assertTrue(baseDir.exists()); + Assertions.assertTrue(baseDir.isDirectory()); + File[] files = baseDir.listFiles((FileFilter) FileFilterUtils.directoryFileFilter()); + Assertions.assertNotNull(files); + return Arrays.stream(files).map(File::toPath).map(f -> DynamicTest.dynamicTest(f.getFileName().toString(), + () -> directoryTest(mainClass, f))); + } + +} diff --git a/src/testFixtures/java/org/jastadd/relast/tests/config/Configuration.java b/src/testFixtures/java/org/jastadd/relast/tests/config/Configuration.java new file mode 100644 index 0000000000000000000000000000000000000000..faeb91a1065ecf7530e299189444ce4d094086c5 --- /dev/null +++ b/src/testFixtures/java/org/jastadd/relast/tests/config/Configuration.java @@ -0,0 +1,117 @@ +package org.jastadd.relast.tests.config; + +public class Configuration { + + private String name; + private String[] args = new String[]{}; + private boolean fail = false; + private String[] errMatches = new String[]{}; + private String[] errContains = new String[]{}; + private String[] outMatches = new String[]{}; + private String[] outContains = new String[]{}; + private String in = "in"; + private String out = "out"; + private String expected = "expected"; + + private boolean compare = false; + + public String getIn() { + return in; + } + + public void setIn(String in) { + this.in = in; + } + + public String getOut() { + return out; + } + + public void setOut(String out) { + this.out = out; + } + + public String getExpected() { + return expected; + } + + public void setExpected(String expected) { + this.expected = expected; + } + + @com.fasterxml.jackson.annotation.JsonGetter("out-matches") + public String[] getOutMatches() { + return outMatches; + } + + @com.fasterxml.jackson.annotation.JsonSetter("out-matches") + public void setOutMatches(String[] outMatches) { + this.outMatches = outMatches; + } + + @com.fasterxml.jackson.annotation.JsonGetter("out-contains") + public String[] getOutContains() { + return outContains; + } + + @com.fasterxml.jackson.annotation.JsonSetter("out-contains") + public void setOutContains(String[] outContains) { + this.outContains = outContains; + } + + @com.fasterxml.jackson.annotation.JsonGetter("err-matches") + public String[] getErrMatches() { + return errMatches; + } + + @com.fasterxml.jackson.annotation.JsonSetter("err-matches") + public void setErrMatches(String[] errMatches) { + this.errMatches = errMatches; + } + + @com.fasterxml.jackson.annotation.JsonGetter("err-contains") + public String[] getErrContains() { + return errContains; + } + + @com.fasterxml.jackson.annotation.JsonSetter("err-contains") + public void setErrContains(String[] errContains) { + this.errContains = errContains; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String[] getArgs() { + return args; + } + + public void setArgs(String[] args) { + this.args = args; + } + + @com.fasterxml.jackson.annotation.JsonGetter("fail") + public boolean shouldFail() { + return fail; + } + + @com.fasterxml.jackson.annotation.JsonSetter("fail") + public void shouldFail(boolean fail) { + this.fail = fail; + } + + @com.fasterxml.jackson.annotation.JsonGetter("compare") + public boolean shouldCompare() { + return compare; + } + + @com.fasterxml.jackson.annotation.JsonSetter("compare") + public void shouldCompare(boolean compare) { + this.compare = compare; + } +}