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.Arrays;
26  import java.util.Collection;
27  import java.util.Iterator;
28  import java.util.ListIterator;
29  
30  import org.apache.maven.doxia.AbstractModuleTest;
31  import org.apache.maven.doxia.sink.Sink;
32  import org.apache.maven.doxia.sink.SinkEventAttributes;
33  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
34  import org.apache.maven.doxia.sink.impl.SinkEventElement;
35  import org.apache.maven.doxia.sink.impl.SinkEventTestingSink;
36  import org.apache.maven.doxia.sink.impl.SinkWrapper;
37  import org.apache.maven.doxia.sink.impl.SinkWrapperFactory;
38  import org.apache.maven.doxia.sink.impl.TextSink;
39  import org.apache.maven.doxia.sink.impl.WellformednessCheckingSink;
40  import org.junit.jupiter.api.Test;
41  
42  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
43  import static org.junit.jupiter.api.Assertions.assertEquals;
44  import static org.junit.jupiter.api.Assertions.assertFalse;
45  import static org.junit.jupiter.api.Assertions.assertTrue;
46  import static org.junit.jupiter.api.Assertions.fail;
47  import static org.junit.jupiter.api.Assumptions.assumeTrue;
48  
49  /**
50   * Test the parsing of sample input files.
51   * <br>
52   * <b>Note</b>: you have to provide a sample "test." + outputExtension()
53   * file in the test resources directory if you extend this class.
54   * @since 1.0
55   */
56  public abstract class AbstractParserTest extends AbstractModuleTest {
57      /**
58       * Example text which usually requires escaping in different markup languages as they have special meaning there
59       */
60      public static final String TEXT_WITH_SPECIAL_CHARS = "<>{}=#*";
61  
62      /**
63       * Create a new instance of the parser to test.
64       *
65       * @return the parser to test.
66       */
67      protected abstract AbstractParser createParser();
68  
69      /**
70       * Returns the directory where all parser test output will go.
71       *
72       * @return The test output directory.
73       */
74      protected String getOutputDir() {
75          return "parser/";
76      }
77  
78      /**
79       * Parse a test document '"test." + outputExtension()'
80       * with parser from {@link #createParser()}, and output to a new
81       * {@link WellformednessCheckingSink}. Asserts that output is well-formed.
82       *
83       * @throws IOException if the test document cannot be read.
84       * @throws ParseException if the test document cannot be parsed.
85       */
86      @Test
87      public final void parser() throws Exception {
88          WellformednessCheckingSink sink = new WellformednessCheckingSink();
89  
90          try (Reader reader = getTestReader("test", outputExtension())) {
91              createParser().parse(reader, sink);
92  
93              assertTrue(
94                      sink.isWellformed(),
95                      "Parser output not well-formed, last offending element: " + sink.getOffender());
96          }
97      }
98  
99      /**
100      * Parse a test document '"test." + outputExtension()'
101      * with parser from {@link #createParser()}, and output to a text file,
102      * using the {@link org.apache.maven.doxia.sink.impl.TextSink TextSink}.
103      *
104      * @throws IOException if the test document cannot be read.
105      * @throws ParseException if the test document cannot be parsed.
106      */
107     @Test
108     public final void document() throws Exception {
109         try (Writer writer = getTestWriter("test", "txt");
110                 Reader reader = getTestReader("test", outputExtension())) {
111             Sink sink = new TextSink(writer);
112             createParser().parse(reader, sink);
113         }
114     }
115 
116     private static final class TestSinkWrapperFactory implements SinkWrapperFactory {
117 
118         private final int priority;
119 
120         TestSinkWrapperFactory(int priority) {
121             super();
122             this.priority = priority;
123         }
124 
125         @Override
126         public Sink createWrapper(Sink sink) {
127             return new SinkWrapper(sink) {
128 
129                 @Override
130                 public void text(String text, SinkEventAttributes attributes) {
131                     super.text("beforeWrapper" + priority + text + "afterWrapper" + priority, attributes);
132                 }
133             };
134         }
135 
136         @Override
137         public int getPriority() {
138             return priority;
139         }
140     }
141 
142     @Test
143     public final void sinkWrapper() throws Exception {
144         Parser parser = createParser();
145 
146         parser.addSinkWrapperFactory(new TestSinkWrapperFactory(1));
147         parser.addSinkWrapperFactory(new TestSinkWrapperFactory(0));
148         parser.addSinkWrapperFactory(new TestSinkWrapperFactory(2)); // this must be the first in the pipeline
149 
150         try (StringWriter writer = new StringWriter();
151                 Reader reader = getTestReader("test", outputExtension())) {
152             Sink sink = new TextSink(writer);
153             parser.parse(reader, sink);
154 
155             // assert order of sink wrappers: wrapper with lower prio is called with prefix from wrapper with higher
156             // prio, therefore lower prio prefix/suffix is emitted prior higher prio prefix/suffix
157             assertTrue(writer.toString().contains("beforeWrapper0beforeWrapper1beforeWrapper2"));
158             assertTrue(writer.toString().contains("afterWrapper2afterWrapper1afterWrapper0"));
159         }
160     }
161 
162     /**
163      * Override this method if the parser always emits some static prefix for incomplete documents
164      * and consume the prefix related events from the given {@link Iterator}.
165      * @param eventIterator the iterator
166      */
167     protected void assertEventPrefix(Iterator<SinkEventElement> eventIterator) {
168         // do nothing by default, i.e. assume no prefix
169     }
170 
171     /**
172      * Override this method if the parser always emits some static suffix for incomplete documents
173      * and consume the suffix related events from the given {@link Iterator}.
174      * @param eventIterator the iterator
175      */
176     protected void assertEventSuffix(Iterator<SinkEventElement> eventIterator) {
177         assertFalse(eventIterator.hasNext(), "didn't expect any further events but got at least one");
178     }
179 
180     /**
181      * @return markup representing the verbatim text {@value #TEXT_WITH_SPECIAL_CHARS} (needs to be properly escaped).
182      * {@code null} can be returned to skip the test for a particular parser.
183      */
184     protected abstract String getVerbatimSource();
185 
186     /**
187      * Test a verbatim block (no code) given through {@link #getVerbatimSource()}
188      * @throws ParseException
189      */
190     @Test
191     public void verbatim() throws Exception {
192         String source = getVerbatimSource();
193         assumeTrue(source != null, "parser does not support simple verbatim text");
194         AbstractParser parser = createParser();
195         SinkEventTestingSink sink = new SinkEventTestingSink();
196 
197         parser.parse(source, sink);
198         ListIterator<SinkEventElement> it = sink.getEventList().listIterator();
199         assertEventPrefix(it);
200         assertEquals("verbatim", it.next().getName());
201         assertConcatenatedTextEquals(it, TEXT_WITH_SPECIAL_CHARS, true);
202         assertEquals("verbatim_", it.next().getName());
203         assertEventSuffix(it);
204     }
205 
206     /**
207      * @return markup representing the verbatim code block {@value #TEXT_WITH_SPECIAL_CHARS} (needs to be properly escaped).
208      * {@code null} can be returned to skip the test for a particular parser.
209      */
210     protected abstract String getVerbatimCodeSource();
211 
212     /**
213      * Test a verbatim code block given through {@link #getVerbatimCodeSource()}
214      * @throws ParseException
215      */
216     @Test
217     public void verbatimCode() throws Exception {
218         String source = getVerbatimCodeSource();
219         assumeTrue(source != null, "parser does not support verbatim code");
220         AbstractParser parser = createParser();
221         SinkEventTestingSink sink = new SinkEventTestingSink();
222 
223         parser.parse(source, sink);
224         ListIterator<SinkEventElement> it = sink.getEventList().listIterator();
225         assertEventPrefix(it);
226         SinkEventElement verbatimEvt = it.next();
227         assertEquals("verbatim", verbatimEvt.getName());
228         SinkEventAttributeSet atts = (SinkEventAttributeSet) verbatimEvt.getArgs()[0];
229 
230         // either verbatim event has attribute "source" or additional "inline" event
231         boolean isInlineCode;
232         if (atts.isEmpty()) {
233             isInlineCode = true;
234             SinkEventAttributes attrs = new SinkEventAttributeSet();
235             attrs.addAttributes(SinkEventAttributeSet.Semantics.CODE);
236             if (outputExtension().equals("md")) {
237                 attrs.addAttribute(SinkEventAttributes.CLASS, "nohighlight nocode");
238             }
239             assertSinkAttributesEqual(it.next(), "inline", attrs);
240         } else {
241             isInlineCode = false;
242             assertEquals(SinkEventAttributeSet.SOURCE, atts);
243         }
244         assertConcatenatedTextEquals(it, TEXT_WITH_SPECIAL_CHARS, true);
245         if (isInlineCode) {
246             assertEquals("inline_", it.next().getName());
247         }
248         assertEquals("verbatim_", it.next().getName());
249         assertEventSuffix(it);
250     }
251 
252     /**
253      * Parse the file and return a {@link SinkEventTestingSink}.
254      *
255      * @param file the file to parse with {@link #parser}
256      * @return a sink to test parsing events
257      * @throws ParseException if the document parsing failed
258      * @throws IOException if an I/O error occurs while closing test reader
259      */
260     protected SinkEventTestingSink parseFileToEventTestingSink(String file) throws ParseException, IOException {
261         SinkEventTestingSink sink;
262         try (Reader reader = getTestReader(file)) {
263             sink = new SinkEventTestingSink();
264             createParser().parse(reader, sink);
265         }
266         return sink;
267     }
268 
269     /**
270      * Parse the text and return a {@link SinkEventTestingSink}.
271      *
272      * @param file the file to parse with {@link #parser}
273      * @return a sink to test parsing events
274      * @throws ParseException if the document parsing failed
275      */
276     protected SinkEventTestingSink parseSourceToEventTestingSink(String text) throws ParseException {
277         SinkEventTestingSink sink;
278         sink = new SinkEventTestingSink();
279         createParser().parse(text, sink);
280         return sink;
281     }
282 
283     public static void assertSinkEquals(SinkEventElement element, String name, Object... args) {
284         assertEquals(name, element.getName(), "Name of element doesn't match");
285         assertArrayEquals(args, element.getArgs(), "Arguments don't match");
286     }
287 
288     public static void assertSinkAttributeEquals(SinkEventElement element, String name, String attr, String value) {
289         assertEquals(name, element.getName());
290         SinkEventAttributeSet atts = (SinkEventAttributeSet) element.getArgs()[0];
291         assertEquals(value, atts.getAttribute(attr));
292     }
293 
294     public static void assertSinkAttributesEqual(
295             SinkEventElement element, String name, SinkEventAttributes expectedAttributes) {
296         assertEquals(name, element.getName());
297         SinkEventAttributeSet atts = (SinkEventAttributeSet) element.getArgs()[0];
298         assertEquals(expectedAttributes, atts);
299     }
300 
301     /**
302      * Consumes all consecutive text events from the given {@link ListIterator} and compares the concatenated text with the expected text
303      * @param it the iterator to traverse, is positioned to the last text event once this method finishes
304      * @param expectedText the expected text which is compared with the concatenated text of all text events
305      * @param trimText {@code true} to trim the actual text before comparing with the expected one, otherwise compare without trimming
306      */
307     protected static void assertConcatenatedTextEquals(
308             ListIterator<SinkEventElement> it, String expectedText, boolean trimText) {
309         StringBuilder builder = new StringBuilder();
310         while (it.hasNext()) {
311             SinkEventElement currentEvent = it.next();
312             if (!"text".equals(currentEvent.getName())) {
313                 it.previous();
314                 break;
315             }
316             builder.append(currentEvent.getArgs()[0]);
317         }
318         String actualValue = builder.toString();
319         if (trimText) {
320             actualValue = actualValue.trim();
321         }
322         assertEquals(expectedText, actualValue);
323     }
324 
325     public static void assertSinkEquals(Iterator<SinkEventElement> it, String... names) {
326         StringBuilder expected = new StringBuilder();
327         StringBuilder actual = new StringBuilder();
328 
329         for (String name : names) {
330             expected.append(name).append('\n');
331         }
332 
333         while (it.hasNext()) {
334             actual.append(it.next().getName()).append('\n');
335         }
336 
337         assertEquals(expected.toString(), actual.toString());
338     }
339 
340     public static void assertSinkDoesNotContain(Iterator<SinkEventElement> it, String... names) {
341         Collection<String> forbiddenNames = Arrays.asList(names);
342         while (it.hasNext()) {
343             String name = it.next().getName();
344             if (forbiddenNames.contains(name)) {
345                 fail("Found unexpected event: " + name);
346             }
347         }
348     }
349 
350     public static void assertSinkStartsWith(Iterator<SinkEventElement> it, String... names) {
351         StringBuilder expected = new StringBuilder();
352         StringBuilder actual = new StringBuilder();
353 
354         for (String name : names) {
355             expected.append(name).append('\n');
356             if (it.hasNext()) {
357                 actual.append(it.next().getName()).append('\n');
358             }
359         }
360         assertEquals(expected.toString(), actual.toString());
361     }
362 }