View Javadoc
1   package emissary.test.core.junit5;
2   
3   import emissary.core.BaseDataObject;
4   import emissary.core.IBaseDataObject;
5   import emissary.core.IBaseDataObjectHelper;
6   import emissary.core.IBaseDataObjectXmlCodecs;
7   import emissary.core.IBaseDataObjectXmlCodecs.ElementDecoders;
8   import emissary.core.IBaseDataObjectXmlCodecs.ElementEncoders;
9   import emissary.place.IServiceProviderPlace;
10  import emissary.test.core.junit5.LogbackTester.SimplifiedLogEvent;
11  import emissary.util.ByteUtil;
12  
13  import com.google.errorprone.annotations.ForOverride;
14  import org.apache.commons.lang3.ArrayUtils;
15  import org.jdom2.Document;
16  import org.junit.jupiter.params.ParameterizedTest;
17  import org.junit.jupiter.params.provider.MethodSource;
18  
19  import java.nio.charset.StandardCharsets;
20  import java.util.ArrayList;
21  import java.util.List;
22  import java.util.Map.Entry;
23  import java.util.Optional;
24  import java.util.TreeMap;
25  
26  import static org.junit.jupiter.api.Assertions.fail;
27  
28  /**
29   * <p>
30   * This test acts similarly to ExtractionTest; however, it compares the entire BDO instead of just what is defined in
31   * the XML. In other words, the XML must define exactly the output of the Place, no more and no less. There are methods
32   * provided to generate the XML required.
33   * </p>
34   * 
35   * <p>
36   * To implement this for a test, you should:
37   * </p>
38   * <ol>
39   * <li>Extend this class</li>
40   * <li>Either Override {@link #generateAnswers()} to return true or set the generateAnswers system property to true,
41   * which will generate the XML answer files</li>
42   * <li>Optionally, to generate answers file without changing code run {@code mvn clean test -DgenerateAnswers=true}</li>
43   * <li>Optionally, override the various provided methods if you want to customise the behaviour of providing the IBDO
44   * before/after processing</li>
45   * <li>Run the tests, which should pass - if they don't, you either have incorrect processing which needs fixing, or you
46   * need to further customise the initial/final IBDOs.</li>
47   * <li>Once the tests pass, you can remove the overridden method(s) added above.</li>
48   * </ol>
49   */
50  public abstract class RegressionTest extends ExtractionTest {
51      /**
52       * The list of actual logEvents generated by executing the place.
53       */
54      protected List<SimplifiedLogEvent> actualSimplifiedLogEvents;
55  
56      /**
57       * Override this or set the generateAnswers system property to true to generate XML for data files.
58       * 
59       * @return defaults to false if no XML should be generated (i.e. normal case of executing tests) or true to generate
60       *         automatically
61       */
62      @ForOverride
63      protected boolean generateAnswers() {
64          return Boolean.getBoolean("generateAnswers");
65      }
66  
67      /**
68       * Allow the initial IBDO to be overridden - for example, adding additional previous forms
69       * 
70       * This is used in the simple case to generate an IBDO from the file on disk and override the filename
71       * 
72       * @param resource path to the dat file
73       * @return the initial IBDO
74       */
75      @ForOverride
76      protected IBaseDataObject getInitialIbdo(final String resource) {
77          return RegressionTestUtil.getInitialIbdoWithFormInFilename(new ClearDataBaseDataObject(), resource, kff);
78      }
79  
80      /**
81       * Allow the initial IBDO to be overridden before serialising to XML.
82       * 
83       * In the default case, we null out the data in the BDO which will force the data to be loaded from the .dat file
84       * instead.
85       * 
86       * @param resource path to the dat file
87       * @param initialIbdo to tweak
88       */
89      @ForOverride
90      protected void tweakInitialIbdoBeforeSerialisation(final String resource, final IBaseDataObject initialIbdo) {
91          if (initialIbdo instanceof ClearDataBaseDataObject) {
92              ((ClearDataBaseDataObject) initialIbdo).clearData();
93          } else {
94              fail("Didn't get an expected type of IBaseDataObject");
95          }
96      }
97  
98      /**
99       * Allow the generated IBDO to be overridden - for example, adding certain field values. Will modify the provided IBDO.
100      * 
101      * This is used in the simple case to set the current form for the final object to be taken from the file name. If the
102      * test worked correctly no change will be made, but if there is a discrepancy this will be highlighted afterwards when
103      * the diff takes place.
104      * 
105      * @param resource path to the dat file
106      * @param finalIbdo the existing final BDO after it's been processed by a place
107      */
108     @ForOverride
109     protected void tweakFinalIbdoBeforeSerialisation(final String resource, final IBaseDataObject finalIbdo) {
110         RegressionTestUtil.tweakFinalIbdoWithFormInFilename(resource, finalIbdo);
111     }
112 
113     /**
114      * Allow the children generated by the place to be overridden before serialising to XML.
115      * 
116      * In the default case, do nothing.
117      * 
118      * @param resource path to the dat file
119      * @param children to tweak
120      */
121     @ForOverride
122     protected void tweakFinalResultsBeforeSerialisation(final String resource, final List<IBaseDataObject> children) {
123         // No-op unless overridden
124     }
125 
126     /**
127      * Allows the log events generated by the place to be modified before serialising to XML.
128      * 
129      * In the default case, do nothing.
130      * 
131      * @param resource path to the dat file
132      * @param simplifiedLogEvents to tweak
133      */
134     @ForOverride
135     protected void tweakFinalLogEventsBeforeSerialisation(final String resource, final List<SimplifiedLogEvent> simplifiedLogEvents) {
136         // No-op unless overridden
137     }
138 
139     @Override
140     @ForOverride
141     protected String getInitialForm(final String resource) {
142         return RegressionTestUtil.getInitialFormFromFilename(resource);
143     }
144 
145     /**
146      * This method returns the XML element decoders.
147      * 
148      * @return the XML element decoders.
149      */
150     protected ElementDecoders getDecoders() {
151         return IBaseDataObjectXmlCodecs.DEFAULT_ELEMENT_DECODERS;
152     }
153 
154     /**
155      * This method returns the XML element encoders.
156      * 
157      * @return the XML element encoders.
158      */
159     protected ElementEncoders getEncoders() {
160         return IBaseDataObjectXmlCodecs.SHA256_ELEMENT_ENCODERS;
161     }
162 
163     /**
164      * When the data is able to be retrieved from the XML (e.g. when getEncoders() returns the default encoders), then this
165      * method should be empty. However, in this case getEncoders() is returning the sha256 encoders which means the original
166      * data cannot be retrieved from the XML. Therefore, in order to test equivalence, all of the non-printable data in the
167      * IBaseDataObjects needs to be converted to a sha256 hash. The full encoders can be used by overriding the
168      * checkAnswersPreHook(...) to be empty and overriding getEncoders() to return the DEFAULT_ELEMENT_ENCODERS.
169      */
170     @Override
171     protected void checkAnswersPreHook(final Document answers, final IBaseDataObject payload, final List<IBaseDataObject> attachments,
172             final String tname) {
173 
174         if (getLogbackLoggerName() != null) {
175             checkAnswersPreHookLogEvents(actualSimplifiedLogEvents);
176         }
177 
178         if (!IBaseDataObjectXmlCodecs.SHA256_ELEMENT_ENCODERS.equals(getEncoders())) {
179             return;
180         }
181 
182         // touch up alternate views to match how their bytes would have encoded into the answer file
183         for (Entry<String, byte[]> entry : new TreeMap<>(payload.getAlternateViews()).entrySet()) {
184             Optional<String> viewSha256 = hashBytesIfNonPrintable(entry.getValue());
185             viewSha256.ifPresent(s -> payload.addAlternateView(entry.getKey(), s.getBytes(StandardCharsets.UTF_8)));
186         }
187 
188         // touch up primary view if necessary
189         Optional<String> payloadSha256 = hashBytesIfNonPrintable(payload.data());
190         payloadSha256.ifPresent(s -> payload.setData(s.getBytes(StandardCharsets.UTF_8)));
191 
192         if (payload.getExtractedRecords() != null) {
193             for (final IBaseDataObject extractedRecord : payload.getExtractedRecords()) {
194                 Optional<String> recordSha256 = hashBytesIfNonPrintable(extractedRecord.data());
195                 recordSha256.ifPresent(s -> extractedRecord.setData(s.getBytes(StandardCharsets.UTF_8)));
196             }
197         }
198 
199         if (attachments != null) {
200             for (final IBaseDataObject attachment : attachments) {
201                 if (ByteUtil.hasNonPrintableValues(attachment.data())) {
202                     Optional<String> attachmentSha256 = hashBytesIfNonPrintable(attachment.data());
203                     attachmentSha256.ifPresent(s -> attachment.setData(s.getBytes(StandardCharsets.UTF_8)));
204                 }
205             }
206         }
207     }
208 
209     /**
210      * This method returns the logger name to capture log events from or null if log events are not to be checked.
211      * 
212      * @return the logger name to capture log events from or null (the default) if log events are not to be checked.
213      */
214     protected String getLogbackLoggerName() {
215         return null;
216     }
217 
218     /**
219      * This method allows log events to be modified prior to checkAnswers being called.
220      * 
221      * In the default case, do nothing.
222      * 
223      * @param simplifiedLogEvents the log events to be tweaked.
224      */
225     protected void checkAnswersPreHookLogEvents(List<SimplifiedLogEvent> simplifiedLogEvents) {
226         // No-op unless overridden
227     }
228 
229     /**
230      * Generates a SHA 256 hash of the provided bytes if they contain any non-printable characters
231      * 
232      * @param bytes the bytes to evaluate
233      * @return a value optionally containing the generated hash
234      */
235     protected Optional<String> hashBytesIfNonPrintable(byte[] bytes) {
236         if (ArrayUtils.isNotEmpty(bytes) && ByteUtil.containsNonIndexableBytes(bytes)) {
237             return Optional.ofNullable(ByteUtil.sha256Bytes(bytes));
238         }
239 
240         return Optional.empty();
241     }
242 
243     protected static class ClearDataBaseDataObject extends BaseDataObject {
244         private static final long serialVersionUID = -8728006876784881020L;
245 
246         protected void clearData() {
247             theData = null;
248             seekableByteChannelFactory = null;
249         }
250     }
251 
252     @ParameterizedTest
253     @MethodSource("data")
254     @Override
255     public void testExtractionPlace(final String resource) {
256         logger.debug("Running {} test on resource {}", place.getClass().getName(), resource);
257 
258         if (generateAnswers()) {
259             try {
260                 generateAnswerFiles(resource);
261             } catch (final Exception e) {
262                 logger.error("Error running test {}", resource, e);
263                 fail("Unable to generate answer file", e);
264             }
265         }
266 
267         // Run the normal extraction/regression tests
268         super.testExtractionPlace(resource);
269     }
270 
271     /**
272      * Actually generate the answer file for a given resource
273      * 
274      * Takes initial form and final forms from the filename
275      * 
276      * @param resource to generate against
277      * @throws Exception if an error occurs during processing
278      */
279     protected void generateAnswerFiles(final String resource) throws Exception {
280         // Get the data and create a channel factory to it
281         final IBaseDataObject initialIbdo = getInitialIbdo(resource);
282         // Clone the BDO to create an 'after' copy
283         final IBaseDataObject finalIbdo = IBaseDataObjectHelper.clone(initialIbdo, true);
284         // Actually process the BDO and keep the children
285         final List<IBaseDataObject> finalResults;
286         final List<SimplifiedLogEvent> finalLogEvents;
287         if (getLogbackLoggerName() == null) {
288             finalResults = place.agentProcessHeavyDuty(finalIbdo);
289             finalLogEvents = new ArrayList<>();
290         } else {
291             try (LogbackTester logbackTester = new LogbackTester(getLogbackLoggerName())) {
292                 finalResults = place.agentProcessHeavyDuty(finalIbdo);
293                 finalLogEvents = logbackTester.getSimplifiedLogEvents();
294             }
295         }
296 
297         // Allow overriding things before serialising to XML
298         tweakInitialIbdoBeforeSerialisation(resource, initialIbdo);
299         tweakFinalIbdoBeforeSerialisation(resource, finalIbdo);
300         tweakFinalResultsBeforeSerialisation(resource, finalResults);
301         tweakFinalLogEventsBeforeSerialisation(resource, finalLogEvents);
302 
303         // Generate the full XML (setup & answers from before & after)
304         RegressionTestUtil.writeAnswerXml(resource, initialIbdo, finalIbdo, finalResults, finalLogEvents, getEncoders(),
305                 super.answerFileClassRef);
306     }
307 
308     @Override
309     protected List<IBaseDataObject> processHeavyDutyHook(IServiceProviderPlace place, IBaseDataObject payload)
310             throws Exception {
311         if (getLogbackLoggerName() == null) {
312             actualSimplifiedLogEvents = new ArrayList<>();
313 
314             return super.processHeavyDutyHook(place, payload);
315         } else {
316             try (LogbackTester logbackTester = new LogbackTester(getLogbackLoggerName())) {
317                 final List<IBaseDataObject> attachments = super.processHeavyDutyHook(place, payload);
318 
319                 actualSimplifiedLogEvents = logbackTester.getSimplifiedLogEvents();
320 
321                 return attachments;
322             }
323         }
324     }
325 
326     @Override
327     protected Document getAnswerDocumentFor(final String resource) {
328         // If generating answers, get the src version, otherwise get the normal XML file
329         return generateAnswers() ? RegressionTestUtil.getAnswerDocumentFor(resource, super.answerFileClassRef) : super.getAnswerDocumentFor(resource);
330     }
331 
332     @Override
333     protected void setupPayload(final IBaseDataObject payload, final Document answers) {
334         RegressionTestUtil.setupPayload(payload, answers, getDecoders());
335     }
336 
337     @Override
338     protected void checkAnswers(final Document answers, final IBaseDataObject payload,
339             final List<IBaseDataObject> attachments, final String tname) {
340         RegressionTestUtil.checkAnswers(answers, payload, actualSimplifiedLogEvents, attachments, place.getClass().getName(), getDecoders(),
341                 generateAnswers());
342     }
343 }