diff --git a/README.md b/README.md index 8c30bce..bb650b5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Maven Mojo for testing [Antlr4](http://www.antlr.org/) Grammars ```xml com.khubla.antlr antlr4test-maven-plugin -1.11 +1.14 jar ``` @@ -233,3 +233,47 @@ The following two configurations are completely equivalent. ``` + +## Checking parsed tree + +Sometimes you want to assure that a given input file generates an specific parsed tree. To do so, you can create sibling files to the parsed examples provided adding the .tree extension to the full name of the parsed file to ask the plugin to check if the parsed tree obtained from the file matchs an expected tree. + +For example, if you have an 'examples/fileToBeParsed.txt' file, you can create a 'examples/fileToBeParsed.txt.tree' file containing the LISP style tree expected to be parsed from you file. + +The checking is done by taking the parsed tree and transforming it to LISP style tree with toStringTree(parserRuleContext, parser) of class org.antlr.v4.runtime.tree.Trees. If they match, everything is fine. If don't, a Diff is generated to show where differences was found. + +Here is the code snippet that checks the parsed tree against the expected one: + +```java + final File treeFile = new File(grammarFile.getAbsolutePath() + GrammarTestMojo.TREE_SUFFIX); + if (treeFile.exists()) { + final String lispTree = Trees.toStringTree(parserRuleContext, parser); + if (null != lispTree) { + final String treeFileData = FileUtils.fileRead(treeFile, scenario.getFileEncoding()); + if (null != treeFileData) { + if (0 != treeFileData.compareTo(lispTree)) { + StringBuilder sb = new StringBuilder( + "Parse tree does not match '" + treeFile.getName() + "'. Differences: "); + for (DiffMatchPatch.Diff diff : new DiffMatchPatch().diffMain(treeFileData, lispTree)) { + sb.append(diff.toString()); + sb.append(", "); + } + throw new Exception(sb.toString()); + } else { + log.info("Parse tree for '" + grammarFile.getName() + "' matches '" + treeFile.getName() + "'"); + } + } + } + } +``` + +A tip to use this feature is to configure the plugin to shows the parsed tree using **true** option. Then you can check the log output manually to see if the generated tree is the expected one. If it is ok, you can copy/paste the parsed tree (only the parsed tree with no other message accessories) to the .tree file. + +## Checking expected errors + +Sometimes you want to check if specific input files generate parsing errors. This can be done with the plugin by adding a new file sibling to the parsed one with .errors extension. Each line is interpreted as an expected parsing error, and the message in the file is compared to the parsed results. If they match, everything is ok, otherwise a test error is raised. + +For example, if you have an 'examples/fileToBeParsed.txt' file, you can create a 'examples/fileToBeParsed.txt.errors' file containing the expected parse error messages. + +A tip to use this feature is to configure the plugin to shows the errors using **true** option. Then you can check the log output manually to see exact error message generated and copy/paste the error messages (only the parsed tree with no other message accessories) to the .errors file. + diff --git a/pom.xml b/pom.xml index 2168711..af7d78b 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,7 @@ 3.6.0 4.8-1 + 3.6 @@ -111,6 +112,12 @@ diff-match-patch 1.2 + + org.easymock + easymock + ${org.easymock.version} + test + diff --git a/src/main/java/com/khubla/antlr/antlr4test/AssertErrorsErrorListener.java b/src/main/java/com/khubla/antlr/antlr4test/AssertErrorsErrorListener.java index dd26fea..3d8f52b 100644 --- a/src/main/java/com/khubla/antlr/antlr4test/AssertErrorsErrorListener.java +++ b/src/main/java/com/khubla/antlr/antlr4test/AssertErrorsErrorListener.java @@ -36,6 +36,7 @@ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.RecognitionException; import org.antlr.v4.runtime.Recognizer; +import org.apache.maven.plugin.logging.Log; public class AssertErrorsErrorListener extends BaseErrorListener { protected static final String CHAR_CR_PLACEHOLDER_REGEXP = "\\\\r"; // to match \r @@ -48,10 +49,21 @@ public class AssertErrorsErrorListener extends BaseErrorListener { protected static final String LITERAL_BACKSLASH_N = "\\\\n"; protected List errorMessages = new ArrayList<>(); + private Scenario scenario = null; + private Log log = null; + + public AssertErrorsErrorListener(Scenario scenario, Log log) { + this.scenario = scenario; + this.log = log; + } + public void assertErrors(File errorMessagesFile, String encoding) throws AssertErrorsException { if (!errorMessages.isEmpty()) { List expectedErrorMessages = null; - String errorMessageFileName = errorMessagesFile.getName(); + String errorMessageFileName = null; + if (errorMessagesFile != null ) { + errorMessageFileName = errorMessagesFile.getName(); + } try { expectedErrorMessages = FileUtil.getNonEmptyLines(errorMessagesFile, encoding); } catch (final FileNotFoundException ex) { @@ -61,7 +73,7 @@ public void assertErrors(File errorMessagesFile, String encoding) throws AssertE } asserts(expectedErrorMessages, errorMessageFileName); } else { - if (errorMessagesFile.exists()) { + if (errorMessagesFile != null && errorMessagesFile.exists()) { throw new AssertErrorsException(String.format("no errors found, but errors file exists %s", errorMessagesFile.getAbsolutePath())); } } @@ -106,6 +118,9 @@ protected String replacePlaceholders(String pString) { @Override public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { final String errorMessage = String.format("line %d:%d %s", line, charPositionInLine, msg); + if (this.scenario.isVerbose()) { + log.warn(errorMessage); + } errorMessages.add(errorMessage); } } diff --git a/src/main/java/com/khubla/antlr/antlr4test/ScenarioExecutor.java b/src/main/java/com/khubla/antlr/antlr4test/ScenarioExecutor.java index 4af09cc..29f9d75 100644 --- a/src/main/java/com/khubla/antlr/antlr4test/ScenarioExecutor.java +++ b/src/main/java/com/khubla/antlr/antlr4test/ScenarioExecutor.java @@ -142,7 +142,7 @@ private void testGrammar(Scenario scenario, File grammarFile) throws Exception { /* * build lexer */ - final AssertErrorsErrorListener assertErrorsErrorListener = new AssertErrorsErrorListener(); + final AssertErrorsErrorListener assertErrorsErrorListener = new AssertErrorsErrorListener(this.scenario, this.log); Lexer lexer = (Lexer) lexerConstructor.newInstance(antlrFileStream); lexer.addErrorListener(assertErrorsErrorListener); /* diff --git a/src/test/java/com/khubla/antlr/antlr4test/AssertErrorsErrorListenerTest.java b/src/test/java/com/khubla/antlr/antlr4test/AssertErrorsErrorListenerTest.java index 9ea085b..fe3fe20 100644 --- a/src/test/java/com/khubla/antlr/antlr4test/AssertErrorsErrorListenerTest.java +++ b/src/test/java/com/khubla/antlr/antlr4test/AssertErrorsErrorListenerTest.java @@ -27,12 +27,18 @@ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT */ package com.khubla.antlr.antlr4test; +import java.io.File; import java.util.Collections; import java.util.List; + +import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; +import static org.easymock.EasyMock.*; + +import org.apache.maven.plugin.logging.Log; /** * @@ -40,11 +46,22 @@ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT */ public class AssertErrorsErrorListenerTest { - private AssertErrorsErrorListener classUnderTest; + private Scenario scenario = null; + private Log log = null; + private AssertErrorsErrorListener classUnderTest = null; + + @After + public void tearDown() { + this.scenario = null; + this.log = null; + this.classUnderTest = null; + } @Before public void setUp() { - classUnderTest = new AssertErrorsErrorListener(); + this.scenario = mock(Scenario.class); + this.log = mock(Log.class); + classUnderTest = new AssertErrorsErrorListener(scenario, log); } /** @@ -52,6 +69,8 @@ public void setUp() { */ @Test public void testReplacePlaceholders_Unchanged() { + replay(this.scenario); + replay(this.log); String line = "mismatched input '\\n' expecting 'foo'"; List lines = Collections.singletonList(line); classUnderTest.replacePlaceholders(lines); @@ -63,6 +82,9 @@ public void testReplacePlaceholders_Unchanged() { */ @Test public void testAssertErrors_Matching() throws AssertErrorsException { + expect(this.scenario.isVerbose()).andReturn(false).times(1); + replay(this.scenario); + replay(this.log); String line = "mismatched input '\\n' expecting 'foo'"; classUnderTest.syntaxError(null, null, 1, 7, line, null); @@ -72,4 +94,196 @@ public void testAssertErrors_Matching() throws AssertErrorsException { assertEquals(lines.get(0), expected); } + /** + * Test of assertErrors method with empty file and an error. + */ + @Test + public void testAssertErrors_EmptyFile_Errors() { + expect(this.scenario.isVerbose()).andReturn(false).times(1); + replay(this.scenario); + replay(this.log); + File errorMessagesFile = new File ( ClassLoader.getSystemResource( "./SampleErrorsEmptyFile.errors").getFile() ); + String line = "mismatched input '\\n' expecting 'foo'"; + classUnderTest.syntaxError(null, null, 1, 7, line, null); + try + { + classUnderTest.assertErrors( errorMessagesFile, "UTF-8" ); + fail("Expected exception AssertErrorsException does not thrown"); + } + catch ( AssertErrorsException e ) + { + assertEquals("SampleErrorsEmptyFile.errors : expected 0 errors, but was 1 errors", e.getMessage()); + } + } + + /** + * Test of assertErrors method with empty file and no errors. + */ + @Test + public void testAssertErrors_EmptyFile_NoErrors() { + replay(this.scenario); + replay(this.log); + File errorMessagesFile = new File ( ClassLoader.getSystemResource( "./SampleErrorsEmptyFile.errors").getFile() ); + try + { + classUnderTest.assertErrors( errorMessagesFile, "UTF-8" ); + fail("Expected exception AssertErrorsException does not thrown"); + } + catch ( AssertErrorsException e ) + { + assertTrue(e.getMessage().startsWith( "no errors found, but errors file exists" )); + assertTrue(e.getMessage().endsWith( "SampleErrorsEmptyFile.errors" )); + } + } + + /** + * Test of assertErrors method with non existing file and no errors. + */ + @Test + public void testAssertErrors_NonExistingFile_NoErrors() { + replay(this.scenario); + replay(this.log); + File errorMessagesFile = null; + try + { + classUnderTest.assertErrors( errorMessagesFile, "UTF-8" ); + } + catch ( Exception e ) + { + e.printStackTrace(); + fail("Unexpected exception thrown: " + e.getMessage()); + } + } + + /** + * Test of assertErrors method with non existing file and no errors. + */ + @Test + public void testAssertErrors_NullFile_Errors() { + expect(this.scenario.isVerbose()).andReturn(false).times(1); + replay(this.scenario); + replay(this.log); + File errorMessagesFile = null; + String line = "mismatched input '\\n' expecting 'foo'"; + classUnderTest.syntaxError(null, null, 1, 7, line, null); + try + { + classUnderTest.assertErrors( errorMessagesFile, "UTF-8" ); + fail("Expected exception AssertErrorsException does not thrown"); + } + catch ( NullPointerException e ) + { + assertNull(e.getMessage()); + } + catch ( Exception e ) + { + fail("Unexpected exception thrown: " + e.getMessage()); + } + } + + /** + * Test of assertErrors method with non existing file and no errors. + */ + @Test + public void testAssertErrors_NonExistingFile_Errors() { + expect(this.scenario.isVerbose()).andReturn(false).times(1); + replay(this.scenario); + replay(this.log); + File errorMessagesFile = new File("Foo.bar"); + String line = "mismatched input '\\n' expecting 'foo'"; + classUnderTest.syntaxError(null, null, 1, 7, line, null); + try + { + classUnderTest.assertErrors( errorMessagesFile, "UTF-8" ); + fail("Expected exception AssertErrorsException does not thrown"); + } + catch ( AssertErrorsException e ) + { + assertEquals("found 1 errors, but missing file Foo.bar", e.getMessage()); + } + catch ( Exception e ) + { + fail("Unexpected exception thrown: " + e.getMessage()); + } + } + + /** + * Test of assertErrors method with non existing file and no errors. + */ + @Test + public void testAssertErrors_Mismatching_Errors() { + expect(this.scenario.isVerbose()).andReturn(false).times(2); + replay(this.scenario); + replay(this.log); + File errorMessagesFile = new File ( ClassLoader.getSystemResource( "./SampleErrorsFileNonEmpty.errors").getFile() ); + String line = "mismatched input '\\n' expecting 'foo'"; + classUnderTest.syntaxError(null, null, 1, 7, line, null); + classUnderTest.syntaxError(null, null, 13, 5, line, null); + try + { + classUnderTest.assertErrors( errorMessagesFile, "UTF-8" ); + fail("Expected exception AssertErrorsException does not thrown"); + } + catch ( NullPointerException e ) + { + assertNull(e.getMessage()); + } + catch ( AssertErrorsException e ) + { + assertEquals("SampleErrorsFileNonEmpty.errors : expected (line 2:8 mismatched input '\n" + + "' expecting 'foo'), but was (line 13:5 mismatched input '\n" + + "' expecting 'foo')", e.getMessage()); + } + catch ( Exception e ) + { + fail("Unexpected exception thrown: " + e.getMessage()); + } + } + + /** + * Test of assertErrors method with non existing file and no errors. + */ + @Test + public void testAssertErrors_Matching_Errors() { + expect(this.scenario.isVerbose()).andReturn(false).times(2); + replay(this.scenario); + replay(this.log); + File errorMessagesFile = new File ( ClassLoader.getSystemResource( "./SampleErrorsFileNonEmpty.errors").getFile() ); + String line = "mismatched input '\\n' expecting 'foo'"; + classUnderTest.syntaxError(null, null, 1, 7, line, null); + classUnderTest.syntaxError(null, null, 2, 8, line, null); + try + { + classUnderTest.assertErrors( errorMessagesFile, "UTF-8" ); + } + catch ( Exception e ) + { + fail("Unexpected exception thrown: " + e.getMessage()); + } + } + /** + * Test of assertErrors method with non existing file and no errors. + */ + @Test + public void testAssertLog() { + File errorMessagesFile = new File ( ClassLoader.getSystemResource( "./SampleErrorsFileNonEmpty.errors").getFile() ); + String line = "mismatched input '\\n' expecting 'foo'"; + expect(this.scenario.isVerbose()).andReturn(true).times(2); + this.log.warn("line 1:7 mismatched input '\\n' expecting 'foo'"); + expectLastCall().times(1); + this.log.warn("line 2:8 mismatched input '\\n' expecting 'foo'"); + expectLastCall().times(1); + replay(this.scenario); + replay(this.log); + classUnderTest.syntaxError(null, null, 1, 7, line, null); + classUnderTest.syntaxError(null, null, 2, 8, line, null); + try + { + classUnderTest.assertErrors( errorMessagesFile, "UTF-8" ); + } + catch ( Exception e ) + { + fail("Unexpected exception thrown: " + e.getMessage()); + } + } } diff --git a/src/test/resources/SampleErrorsEmptyFile.errors b/src/test/resources/SampleErrorsEmptyFile.errors new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/SampleErrorsFileNonEmpty.errors b/src/test/resources/SampleErrorsFileNonEmpty.errors new file mode 100644 index 0000000..7ba04ac --- /dev/null +++ b/src/test/resources/SampleErrorsFileNonEmpty.errors @@ -0,0 +1,2 @@ +line 1:7 mismatched input '\n' expecting 'foo' +line 2:8 mismatched input '\n' expecting 'foo'