View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.parser;
20  
21  import java.io.IOException;
22  import java.io.Reader;
23  import java.io.StringWriter;
24  import java.io.Writer;
25  import java.util.Iterator;
26  import java.util.ListIterator;
27  
28  import org.apache.maven.doxia.AbstractModuleTest;
29  import org.apache.maven.doxia.sink.Sink;
30  import org.apache.maven.doxia.sink.SinkEventAttributes;
31  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
32  import org.apache.maven.doxia.sink.impl.SinkEventElement;
33  import org.apache.maven.doxia.sink.impl.SinkEventTestingSink;
34  import org.apache.maven.doxia.sink.impl.SinkWrapper;
35  import org.apache.maven.doxia.sink.impl.SinkWrapperFactory;
36  import org.apache.maven.doxia.sink.impl.TextSink;
37  import org.apache.maven.doxia.sink.impl.WellformednessCheckingSink;
38  import org.junit.jupiter.api.Assertions;
39  import org.junit.jupiter.api.Test;
40  
41  import static org.junit.jupiter.api.Assertions.assertEquals;
42  import static org.junit.jupiter.api.Assertions.assertFalse;
43  import static org.junit.jupiter.api.Assertions.assertTrue;
44  import static org.junit.jupiter.api.Assumptions.assumeTrue;
45  
46  /**
47   * Test the parsing of sample input files.
48   * <br>
49   * <b>Note</b>: you have to provide a sample "test." + outputExtension()
50   * file in the test resources directory if you extend this class.
51   * @since 1.0
52   */
53  public abstract class AbstractParserTest extends AbstractModuleTest {
54      /**
55       * Example text which usually requires escaping in different markup languages as they have special meaning there
56       */
57      public static final String TEXT_WITH_SPECIAL_CHARS = "<>{}=#*";
58  
59      /**
60       * Create a new instance of the parser to test.
61       *
62       * @return the parser to test.
63       */
64      protected abstract AbstractParser createParser();
65  
66      /**
67       * Returns the directory where all parser test output will go.
68       *
69       * @return The test output directory.
70       */
71      protected String getOutputDir() {
72          return "parser/";
73      }
74  
75      /**
76       * Parse a test document '"test." + outputExtension()'
77       * with parser from {@link #createParser()}, and output to a new
78       * {@link WellformednessCheckingSink}. Asserts that output is well-formed.
79       *
80       * @throws IOException if the test document cannot be read.
81       * @throws ParseException if the test document cannot be parsed.
82       */
83      @Test
84      public final void testParser() throws IOException, ParseException {
85          WellformednessCheckingSink sink = new WellformednessCheckingSink();
86  
87          try (Reader reader = getTestReader("test", outputExtension())) {
88              createParser().parse(reader, sink);
89  
90              assertTrue(
91                      sink.isWellformed(),
92                      "Parser output not well-formed, last offending element: " + sink.getOffender());
93          }
94      }
95  
96      /**
97       * Parse a test document '"test." + outputExtension()'
98       * with parser from {@link #createParser()}, and output to a text file,
99       * using the {@link org.apache.maven.doxia.sink.impl.TextSink TextSink}.
100      *
101      * @throws IOException if the test document cannot be read.
102      * @throws ParseException if the test document cannot be parsed.
103      */
104     @Test
105     public final void testDocument() throws IOException, ParseException {
106         try (Writer writer = getTestWriter("test", "txt");
107                 Reader reader = getTestReader("test", outputExtension())) {
108             Sink sink = new TextSink(writer);
109             createParser().parse(reader, sink);
110         }
111     }
112 
113     private static final class TestSinkWrapperFactory implements SinkWrapperFactory {
114 
115         private final int priority;
116 
117         public TestSinkWrapperFactory(int priority) {
118             super();
119             this.priority = priority;
120         }
121 
122         @Override
123         public Sink createWrapper(Sink sink) {
124             return new SinkWrapper(sink) {
125 
126                 @Override
127                 public void text(String text, SinkEventAttributes attributes) {
128                     super.text("beforeWrapper" + priority + text + "afterWrapper" + priority, attributes);
129                 }
130             };
131         }
132 
133         @Override
134         public int getPriority() {
135             return priority;
136         }
137     }
138 
139     @Test
140     public final void testSinkWrapper() throws ParseException, IOException {
141         Parser parser = createParser();
142 
143         parser.addSinkWrapperFactory(new TestSinkWrapperFactory(1));
144         parser.addSinkWrapperFactory(new TestSinkWrapperFactory(0));
145         parser.addSinkWrapperFactory(new TestSinkWrapperFactory(2)); // this must be the first in the pipeline
146 
147         try (StringWriter writer = new StringWriter();
148                 Reader reader = getTestReader("test", outputExtension())) {
149             Sink sink = new TextSink(writer);
150             parser.parse(reader, sink);
151 
152             // assert order of sink wrappers: wrapper with lower prio is called with prefix from wrapper with higher
153             // prio, therefore lower prio prefix/suffix is emitted prior higher prio prefix/suffix
154             assertTrue(writer.toString().contains("beforeWrapper0beforeWrapper1beforeWrapper2"));
155             assertTrue(writer.toString().contains("afterWrapper2afterWrapper1afterWrapper0"));
156         }
157     }
158 
159     /**
160      * Override this method if the parser always emits some static prefix for incomplete documents
161      * and consume the prefix related events from the given {@link Iterator}.
162      * @param eventIterator the iterator
163      */
164     protected void assertEventPrefix(Iterator<SinkEventElement> eventIterator) {
165         // do nothing by default, i.e. assume no prefix
166     }
167 
168     /**
169      * Override this method if the parser always emits some static suffix for incomplete documents
170      * and consume the suffix related events from the given {@link Iterator}.
171      * @param eventIterator the iterator
172      */
173     protected void assertEventSuffix(Iterator<SinkEventElement> eventIterator) {
174         assertFalse(eventIterator.hasNext(), "didn't expect any further events but got at least one");
175     }
176 
177     /**
178      * @return markup representing the verbatim text {@value #TEXT_WITH_SPECIAL_CHARS} (needs to be properly escaped).
179      * {@code null} can be returned to skip the test for a particular parser.
180      */
181     protected abstract String getVerbatimSource();
182 
183     /**
184      * Test a verbatim block (no code) given through {@link #getVerbatimSource()}
185      * @throws ParseException
186      */
187     @Test
188     public void testVerbatim() throws ParseException {
189         String source = getVerbatimSource();
190         assumeTrue(source != null, "parser does not support simple verbatim text");
191         AbstractParser parser = createParser();
192         SinkEventTestingSink sink = new SinkEventTestingSink();
193 
194         parser.parse(source, sink);
195         ListIterator<SinkEventElement> it = sink.getEventList().listIterator();
196         assertEventPrefix(it);
197         assertEquals("verbatim", it.next().getName());
198         assertConcatenatedTextEquals(it, TEXT_WITH_SPECIAL_CHARS, true);
199         assertEquals("verbatim_", it.next().getName());
200         assertEventSuffix(it);
201     }
202 
203     /**
204      * @return markup representing the verbatim code block {@value #TEXT_WITH_SPECIAL_CHARS} (needs to be properly escaped).
205      * {@code null} can be returned to skip the test for a particular parser.
206      */
207     protected abstract String getVerbatimCodeSource();
208 
209     /**
210      * Test a verbatim code block given through {@link #getVerbatimCodeSource()}
211      * @throws ParseException
212      */
213     @Test
214     public void testVerbatimCode() throws ParseException {
215         String source = getVerbatimCodeSource();
216         assumeTrue(source != null, "parser does not support verbatim code");
217         AbstractParser parser = createParser();
218         SinkEventTestingSink sink = new SinkEventTestingSink();
219 
220         parser.parse(source, sink);
221         ListIterator<SinkEventElement> it = sink.getEventList().listIterator();
222         assertEventPrefix(it);
223         SinkEventElement verbatimEvt = it.next();
224         assertEquals("verbatim", verbatimEvt.getName());
225         SinkEventAttributeSet atts = (SinkEventAttributeSet) verbatimEvt.getArgs()[0];
226 
227         // either verbatim event has attribute "source" or additional "inline" event
228         boolean isInlineCode;
229         if (atts.isEmpty()) {
230             isInlineCode = true;
231             assertSinkAttributesEqual(it.next(), "inline", SinkEventAttributeSet.Semantics.CODE);
232         } else {
233             isInlineCode = false;
234             assertEquals(SinkEventAttributeSet.SOURCE, atts);
235         }
236         assertConcatenatedTextEquals(it, TEXT_WITH_SPECIAL_CHARS, true);
237         if (isInlineCode) {
238             assertEquals("inline_", it.next().getName());
239         }
240         assertEquals("verbatim_", it.next().getName());
241         assertEventSuffix(it);
242     }
243 
244     public static void assertSinkEquals(SinkEventElement element, String name, Object... args) {
245         Assertions.assertEquals(name, element.getName(), "Name of element doesn't match");
246         Assertions.assertArrayEquals(args, element.getArgs(), "Arguments don't match");
247     }
248 
249     public static void assertSinkAttributeEquals(SinkEventElement element, String name, String attr, String value) {
250         Assertions.assertEquals(name, element.getName());
251         SinkEventAttributeSet atts = (SinkEventAttributeSet) element.getArgs()[0];
252         Assertions.assertEquals(value, atts.getAttribute(attr));
253     }
254 
255     public static void assertSinkAttributesEqual(
256             SinkEventElement element, String name, SinkEventAttributes expectedAttributes) {
257         Assertions.assertEquals(name, element.getName());
258         SinkEventAttributeSet atts = (SinkEventAttributeSet) element.getArgs()[0];
259         Assertions.assertEquals(expectedAttributes, atts);
260     }
261 
262     /**
263      * Consumes all consecutive text events from the given {@link ListIterator} and compares the concatenated text with the expected text
264      * @param it the iterator to traverse, is positioned to the last text event once this method finishes
265      * @param expectedText the expected text which is compared with the concatenated text of all text events
266      * @param trimText {@code true} to trim the actual text before comparing with the expected one, otherwise compare without trimming
267      */
268     void assertConcatenatedTextEquals(ListIterator<SinkEventElement> it, String expectedText, boolean trimText) {
269         StringBuilder builder = new StringBuilder();
270         while (it.hasNext()) {
271             SinkEventElement currentEvent = it.next();
272             if (currentEvent.getName() != "text") {
273                 it.previous();
274                 break;
275             }
276             builder.append(currentEvent.getArgs()[0]);
277         }
278         String actualValue = builder.toString();
279         if (trimText) {
280             actualValue = actualValue.trim();
281         }
282         assertEquals(expectedText, actualValue);
283     }
284 
285     public static void assertSinkEquals(Iterator<SinkEventElement> it, String... names) {
286         StringBuilder expected = new StringBuilder();
287         StringBuilder actual = new StringBuilder();
288 
289         for (String name : names) {
290             expected.append(name).append('\n');
291         }
292 
293         while (it.hasNext()) {
294             actual.append(it.next().getName()).append('\n');
295         }
296 
297         Assertions.assertEquals(expected.toString(), actual.toString());
298     }
299 
300     public static void assertSinkStartsWith(Iterator<SinkEventElement> it, String... names) {
301         StringBuilder expected = new StringBuilder();
302         StringBuilder actual = new StringBuilder();
303 
304         for (String name : names) {
305             expected.append(name).append('\n');
306             if (it.hasNext()) {
307                 actual.append(it.next().getName()).append('\n');
308             }
309         }
310         Assertions.assertEquals(expected.toString(), actual.toString());
311     }
312 }