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