diff --git a/build.gradle b/build.gradle
index c9f5a8d654fdd6a213e364528ce70ebe1173e63c..03170b2193172398fcaf465fdf0d94037ae28a1d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -53,6 +53,7 @@ dependencies {
     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'
 }
 
 test {
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/java/org/jastadd/relast/compiler/Mustache.java b/src/main/java/org/jastadd/relast/compiler/Mustache.java
index 3b428abf1f60b87ff004b7cb92aaefcbf99b03df..24d368abf9d13cbca1882caef1a49084943499a9 100644
--- a/src/main/java/org/jastadd/relast/compiler/Mustache.java
+++ b/src/main/java/org/jastadd/relast/compiler/Mustache.java
@@ -6,16 +6,34 @@ import com.github.jknack.handlebars.io.ClassPathTemplateLoader;
 import com.github.jknack.handlebars.io.TemplateLoader;
 import org.yaml.snakeyaml.Yaml;
 
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.Writer;
+import java.io.*;
+import java.nio.file.Paths;
 
 public class Mustache {
 
-  public static void javaMustache(String templateFileName, String yamlFileName, String outputFileName) throws IOException {
+  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);
+  }
 
-    Object context = new Yaml().load(new FileReader(yamlFileName));
+  private static void applyTemplate(String templateFileName, String outputFileName, Object context) throws IOException {
     TemplateLoader loader = new ClassPathTemplateLoader();
     loader.setSuffix(".mustache"); // the default is ".hbs"
 
diff --git a/src/main/java/org/jastadd/relast/compiler/RelAstProcessor.java b/src/main/java/org/jastadd/relast/compiler/RelAstProcessor.java
index b5624f2aee6e22179800de5e09c471df8b3e2071..4e3c97d120b8a1906b77b3d334fea536bdfa40bc 100644
--- a/src/main/java/org/jastadd/relast/compiler/RelAstProcessor.java
+++ b/src/main/java/org/jastadd/relast/compiler/RelAstProcessor.java
@@ -21,7 +21,7 @@ public abstract class RelAstProcessor extends AbstractCompiler {
   protected ValueOption optionOutputBaseDir;
   protected ValueOption optionInputBaseDir;
 
-  public RelAstProcessor(String name, boolean jastAddCompliant) {
+  protected RelAstProcessor(String name, boolean jastAddCompliant) {
     super(name, jastAddCompliant);
   }
 
diff --git a/src/test/java/org/jastadd/relast/tests/PreprocessorTest.java b/src/test/java/org/jastadd/relast/tests/PreprocessorTest.java
index c6fde6f5331ebc9e94680dee87b54ce31e58bad6..b58349632e19b3caf3848ae3fff836673c441221 100644
--- a/src/test/java/org/jastadd/relast/tests/PreprocessorTest.java
+++ b/src/test/java/org/jastadd/relast/tests/PreprocessorTest.java
@@ -1,15 +1,13 @@
 package org.jastadd.relast.tests;
 
 import org.jastadd.relast.compiler.RelastSourceToSourceCompiler;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.nio.file.Paths;
+import org.junit.jupiter.api.BeforeAll;
 
 public class PreprocessorTest extends RelAstProcessorTestBase {
 
-  @Test
-  void testMinimalGrammar() throws IOException, InterruptedException {
-    directoryTest(RelastSourceToSourceCompiler.class, Paths.get("src/test/resources/MinimalGrammar"));
+  @BeforeAll
+  static void init() {
+    mainClass = RelastSourceToSourceCompiler.class;
   }
+
 }
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
index c22d8d533545dff1d7139fa62a5464506d703c13..135f6bd74b057a00cc5582b7ca78e83995167e48 100644
--- a/src/test/resources/MinimalGrammar/config.yaml
+++ b/src/test/resources/MinimalGrammar/config.yaml
@@ -1,10 +1,9 @@
-- name: "MinimalGrammar"
+- name: "Parse and reprint tests"
   args:
     - "--inputBaseDir=in"
     - "--outputBaseDir=out"
     - "Example.relast"
-- name: "Comments in front"
-  args:
-    - "--inputBaseDir=in"
-    - "--outputBaseDir=out"
     - "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
index 99b677ef4d8481b99e4d9f2f366764e1de6a568b..96c5eecaae626aa6903b7d0b9641916688c7a997 100644
--- a/src/test/resources/MinimalGrammar/in/CommentInFront.relast
+++ b/src/test/resources/MinimalGrammar/in/CommentInFront.relast
@@ -1,2 +1,2 @@
 // comment
-CommentInFront ;
+CommentInFront;
diff --git a/src/test/resources/MinimalGrammar/in/Example.relast b/src/test/resources/MinimalGrammar/in/Example.relast
index 0d9c9069419cd5b38fd1f861e9152cce7ffeb750..7c28b7bcd84c826aedd622460eb93da08ee80c3d 100644
--- a/src/test/resources/MinimalGrammar/in/Example.relast
+++ b/src/test/resources/MinimalGrammar/in/Example.relast
@@ -4,7 +4,7 @@ ZoneModel ::= <Size:IntPosition> SafetyZone:Zone*;
 
 Zone ::= Coordinate*;
 
-RobotArm ::= Joint* EndEffector <_AttributeTestSource:int> /<_AppropriateSpeed:double>/ ; // normally this would be: <AttributeTestSource:int> ;
+RobotArm ::= Joint* EndEffector <_AttributeTestSource:int> /<_AppropriateSpeed:double>/; // normally this would be: <AttributeTestSource:int> ;
 
 Joint ::= <Name> <CurrentPosition:IntPosition>;  // normally this would be: <CurrentPosition:IntPosition>
 
diff --git a/src/testFixtures/java/org/jastadd/relast/tests/RelAstProcessorTestBase.java b/src/testFixtures/java/org/jastadd/relast/tests/RelAstProcessorTestBase.java
index bdd620c19de7ae3d9891960c6191c51cfb6ef37d..e83746c04a4a85327eabf5db0a1de580e6a42e3c 100644
--- a/src/testFixtures/java/org/jastadd/relast/tests/RelAstProcessorTestBase.java
+++ b/src/testFixtures/java/org/jastadd/relast/tests/RelAstProcessorTestBase.java
@@ -3,25 +3,25 @@ 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.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
+import java.io.*;
+import java.nio.charset.Charset;
 import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Objects;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
+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();
@@ -72,7 +72,7 @@ public class RelAstProcessorTestBase {
       command.addAll(args);
     }
 
-    System.out.println("Running java -jar -cp [...] " + className + " " + args.stream().reduce((s1, s2) -> s1 + " " + s2).orElse(""));
+    System.out.println("Running java -jar -cp [...] " + className + " " + (args != null ? args.stream().reduce((s1, s2) -> s1 + " " + s2).orElse("") : ""));
 
     return runProcess(workingDirectory, command, outStringBuider, errStringBuilder);
   }
@@ -86,6 +86,9 @@ public class RelAstProcessorTestBase {
 
     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);
@@ -96,46 +99,78 @@ public class RelAstProcessorTestBase {
       System.err.println(err);
 
       if (config.shouldFail()) {
-        Assertions.assertNotEquals(0, returnValue, "Zero return value of preprocessor for negative test.");
+        Assertions.assertNotEquals(0, returnValue, config.getName() + ": Zero return value of preprocessor for negative test.");
       } else {
-        Assertions.assertEquals(0, returnValue, "Non-Zero return value of preprocessor for positive test.");
+        Assertions.assertEquals(0, returnValue, config.getName() + ": Non-Zero return value of preprocessor for positive test.");
       }
 
-      for (String errMatchString : config.getErrMatches()) {
-        if (!err.matches(errMatchString)) {
-          Assertions.fail("Error stream does not match '" + errMatchString + "'");
-        }
+      checkOutput(config, out, err);
+
+      if (config.shouldCompare()) {
+        Path outPath = dir.resolve(config.getOut());
+        Path expectedPath = dir.resolve(config.getExpected());
+        comparePaths(outPath, expectedPath);
       }
+    }
+  }
 
-      for (String errContainsString : config.getErrContains()) {
-        if (!err.contains(errContainsString)) {
-          Assertions.fail("Error stream does not contain '" + errContainsString + "'");
-        }
+  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 outMatchString : config.getOutMatches()) {
-        if (!out.matches(outMatchString)) {
-          Assertions.fail("Error stream does not match '" + outMatchString + "'");
-        }
+    for (String errContainsString : config.getErrContains()) {
+      if (!err.contains(errContainsString)) {
+        Assertions.fail("Error stream does not contain '" + errContainsString + "'");
       }
+    }
 
-      for (String outContainsString : config.getOutContains()) {
-        if (!out.contains(outContainsString)) {
-          Assertions.fail("Error stream does not contain '" + outContainsString + "'");
-        }
+    for (String outMatchString : config.getOutMatches()) {
+      if (!out.matches(outMatchString)) {
+        Assertions.fail("Output stream does not match '" + outMatchString + "'");
       }
     }
-  }
 
-  protected void ensureOutputDir(String outputDir) {
-    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!");
+    for (String outContainsString : config.getOutContains()) {
+      if (!out.contains(outContainsString)) {
+        Assertions.fail("Output stream does not contain '" + outContainsString + "'");
       }
-    } else {
-      assertTrue(outputDirFile.mkdirs());
     }
   }
+
+  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
index 7a1ec9419bc5bb39463941ff70ceb097ab33c5d5..faeb91a1065ecf7530e299189444ce4d094086c5 100644
--- a/src/testFixtures/java/org/jastadd/relast/tests/config/Configuration.java
+++ b/src/testFixtures/java/org/jastadd/relast/tests/config/Configuration.java
@@ -9,6 +9,35 @@ public class Configuration {
   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() {
@@ -46,7 +75,7 @@ public class Configuration {
   }
 
   @com.fasterxml.jackson.annotation.JsonSetter("err-contains")
-  public void setErrContains(String[] errMatches) {
+  public void setErrContains(String[] errContains) {
     this.errContains = errContains;
   }
 
@@ -75,4 +104,14 @@ public class Configuration {
   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;
+  }
 }