means of creating diff

This commit is contained in:
Greg DiCristofaro 2020-10-20 17:18:41 -04:00
parent 023f975455
commit 0673969304
6 changed files with 340 additions and 66 deletions

View File

@ -3,30 +3,30 @@
<configurations >
<!-- module dependencies -->
<conf name="core"/>
</configurations>
<dependencies >
<dependency conf="core->default" org="com.github.vlsi.mxgraph" name="jgraphx" rev="4.1.0" />
<dependency conf="core->default" org="org.apache.activemq" name="activemq-all" rev="5.11.1"/>
<dependency conf="core->default" org="org.apache.curator" name="curator-client" rev="2.8.0"/>
<dependency conf="core->default" org="org.apache.curator" name="curator-framework" rev="2.8.0"/>
<dependency conf="core->default" org="org.apache.curator" name="curator-recipes" rev="2.8.0"/>
<dependency conf="core->default" org="org.python" name="jython-standalone" rev="2.7.0" />
<dependency conf="core->default" org="com.adobe.xmp" name="xmpcore" rev="5.1.2"/>
<dependency conf="core->default" org="org.apache.zookeeper" name="zookeeper" rev="3.4.6"/>
<dependency conf="core->default" org="com.healthmarketscience.jackcess" name="jackcess" rev="2.2.0"/>
<dependency conf="core->default" org="com.healthmarketscience.jackcess" name="jackcess-encrypt" rev="2.1.4"/>
<dependency conf="core->default" org="org.apache.commons" name="commons-dbcp2" rev="2.1.1"/>
<dependency conf="core->default" org="org.apache.commons" name="commons-pool2" rev="2.4.2"/>
<dependency conf="core->default" org="commons-codec" name="commons-codec" rev="1.11"/>
<dependency conf="core->default" org="org.jsoup" name="jsoup" rev="1.10.3"/>
<dependency conf="core->default" org="com.fasterxml.jackson.core" name="jackson-databind" rev="2.9.7"/>
<dependency conf="core->default" org="com.drewnoakes" name="metadata-extractor" rev="2.11.0"/>
@ -34,7 +34,7 @@
<dependency conf="core->default" org="org.apache.opennlp" name="opennlp-tools" rev="1.9.1"/>
<dependency conf="core->default" org="com.ethteck.decodetect" name="decodetect-core" rev="0.3"/>
<dependency conf="core->default" org="org.sejda.webp-imageio" name="webp-imageio-sejda" rev="0.1.0"/>
<dependency conf="core->default" org="com.googlecode.libphonenumber" name="libphonenumber" rev="3.5" />
<dependency conf="core->default" org="commons-validator" name="commons-validator" rev="1.6"/>
@ -45,13 +45,17 @@
<!-- for yaml reading/writing -->
<dependency org="org.yaml" name="snakeyaml" rev="1.27"/>
<!-- map support for geolocation -->
<dependency conf="core->default" org="org.jxmapviewer" name="jxmapviewer2" rev="2.4"/>
<dependency conf="core->default" org="org.jxmapviewer" name="jxmapviewer2" rev="2.4"/>
<!-- For Discovery testing -->
<dependency conf="core->default" org="org.mockito" name="mockito-core" rev="3.5.7"/>
<!-- for handling diffs -->
<dependency org="io.github.java-diff-utils" name="java-diff-utils" rev="4.8"/>
<!-- https://mvnrepository.com/artifact/javax.ws.rs/javax.ws.rs-api -->
<dependency conf="core->default" org="javax.ws.rs" name="javax.ws.rs-api" rev="2.0"/>
<override org="jakarta.ws.rs" module="jakarta.ws.rs-api" rev="2.1.5"/>

View File

@ -21,6 +21,7 @@ file.reference.commons-dbcp2-2.1.1.jar=release\\modules\\ext\\commons-dbcp2-2.1.
file.reference.commons-digester-1.8.1.jar=release\\modules\\ext\\commons-digester-1.8.1.jar
file.reference.commons-logging-1.2.jar=release\\modules\\ext\\commons-logging-1.2.jar
file.reference.commons-pool2-2.4.2.jar=release\\modules\\ext\\commons-pool2-2.4.2.jar
file.reference.java-diff-utils-4.8.jar=release\\modules\\ext\\java-diff-utils-4.8.jar
file.reference.commons-validator-1.6.jar=release\\modules\\ext\\commons-validator-1.6.jar
file.reference.curator-client-2.8.0.jar=release\\modules\\ext\\curator-client-2.8.0.jar
file.reference.curator-framework-2.8.0.jar=release\\modules\\ext\\curator-framework-2.8.0.jar

View File

@ -643,6 +643,10 @@
<runtime-relative-path>ext/commons-collections-3.2.2.jar</runtime-relative-path>
<binary-origin>release\modules\ext\commons-collections-3.2.2.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/java-diff-utils-4.8.jar</runtime-relative-path>
<binary-origin>release\modules\ext\java-diff-utils-4.8.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/SparseBitSet-1.1.jar</runtime-relative-path>
<binary-origin>release\modules\ext\SparseBitSet-1.1.jar</binary-origin>
@ -811,7 +815,7 @@
<runtime-relative-path>ext/grpc-netty-shaded-1.19.0.jar</runtime-relative-path>
<binary-origin>release\modules\ext\grpc-netty-shaded-1.19.0.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/snakeyaml-1.27.jar</runtime-relative-path>
<binary-origin>release\modules\ext\snakeyaml-1.27.jar</binary-origin>
</class-path-extension>

View File

@ -0,0 +1,196 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2020 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.integrationtesting;
import com.github.difflib.DiffUtils;
import com.github.difflib.patch.AbstractDelta;
import com.github.difflib.patch.Chunk;
import com.github.difflib.patch.Patch;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
/**
* Handles creating diffs with files.
*/
public class DiffService {
private static final Logger logger = Logger.getLogger(DiffUtils.class.getName());
private static final String ORIG_LINE_PREFIX = "< ";
private static final String CUR_LINE_PREFIX = "> ";
private static final String[] DIFF_BREAK = new String[]{"", "", ""};
private static final String[] FILE_DIFF_BREAK = new String[]{"", "", "", ""};
private static final String NEW_LINE = System.getProperty("line.separator");
/**
* Creates a diff of all the files found in the directories provided or
* between two files.
*
* @param prevResult The previous file or directory. Must be of same type as
* curResult (file/directory).
* @param curResult The current file or directory. Must be of same type as
* prevResult (file/directory).
* @return The string contents of the diff.
*/
String diffFilesOrDirs(File prevResult, File curResult) {
if (prevResult.isDirectory() && curResult.isDirectory()) {
final Map<String, File> prevFiles = FileUtils.listFiles(prevResult, null, true).stream()
.collect(Collectors.toMap(f -> getRelative(prevResult, f), f -> f, (f1, f2) -> f1));
final Map<String, File> curFiles = FileUtils.listFiles(curResult, null, true).stream()
.collect(Collectors.toMap(f -> getRelative(curResult, f), f -> f, (f1, f2) -> f1));
Map<String, Pair<File, File>> prevCurMapping = Stream.of(prevFiles, curFiles)
.flatMap((map) -> map.keySet().stream())
.collect(Collectors.toMap(k -> k, k -> Pair.of(prevFiles.get(k), curFiles.get(k)), (v1, v2) -> v1));
String fullDiff = prevCurMapping.entrySet().stream()
.map((entry) -> getFileDiffs(entry.getValue().getLeft(), entry.getValue().getRight(), entry.getKey()))
.filter((val) -> val != null)
.collect(Collectors.joining(String.join(NEW_LINE, FILE_DIFF_BREAK)));
return fullDiff;
} else if (prevResult.isFile() && curResult.isFile()) {
return getFileDiffs(prevResult, curResult, prevResult.toString() + " / " + curResult.toString());
} else {
logger.log(Level.WARNING, String.format("%s and %s must be of same type (directory/file).", prevResult.toString(), curResult.toString()));
return null;
}
}
/**
* Handles creating a diff between files noting if one of them is not
* present. If both are not present or both are the same, null is returned.
*
* @param orig The original file.
* @param cur The current file.
* @param identifier The identifier for the header.
* @return The String representing the differences.
*/
private String getFileDiffs(File orig, File cur, String identifier) {
boolean hasOrig = (orig != null && orig.exists());
boolean hasCur = (cur != null && cur.exists());
if (!hasOrig && !hasCur) {
return null;
} else if (!hasOrig && hasCur) {
return getHeaderWithDivider("MISSING FILE IN CURRENT: " + identifier);
} else if (hasOrig && !hasCur) {
return getHeaderWithDivider("ADDITIONAL FILE IN CURRENT: " + identifier);
} else {
try {
return diffLines(Files.readAllLines(orig.toPath()), Files.readAllLines(cur.toPath()), getHeaderWithDivider(identifier + ":"));
} catch (IOException ex) {
return getHeaderWithDivider(String.format("ERROR reading files at %s / %s %s%s",
orig.toString(), cur.toString(), NEW_LINE, ExceptionUtils.getStackTrace(ex)));
}
}
}
private String getChunkLineNumString(Chunk<?> chunk) {
return String.format("%d,%d", chunk.getPosition() + 1, chunk.getLines().size());
}
/**
* Gets a github-like line difference (i.e. -88,3 +90,3) of the form
* -orig_line_num,orig_lines, +new_line_num,new_lines.
*
* @param orig The previous chunk.
* @param cur The current chunk.
* @return The line number difference.
*/
private String getDiffLineNumString(Chunk<?> orig, Chunk<?> cur) {
return String.format("-%s +%s", getChunkLineNumString(orig), getChunkLineNumString(cur));
}
/**
* Creates a line by line difference similar to integration tests like:
* < original
* > new
*
* @param orig The original chunk.
* @param cur The new chunk.
* @return The lines representing the diff.
*/
private List<String> getLinesDiff(Chunk<String> orig, Chunk<String> cur) {
Stream<String> origPrefixed = orig.getLines().stream()
.map((line) -> ORIG_LINE_PREFIX + line);
Stream<String> curPrefixed = cur.getLines().stream()
.map((line) -> CUR_LINE_PREFIX + line);
return Stream.concat(origPrefixed, curPrefixed)
.collect(Collectors.toList());
}
private String getLinesDiffString(AbstractDelta<String> delta) {
String lineNums = getDiffLineNumString(delta.getSource(), delta.getTarget());
List<String> linesDiff = getLinesDiff(delta.getSource(), delta.getTarget());
return Stream.concat(Stream.of(lineNums), linesDiff.stream())
.collect(Collectors.joining(NEW_LINE)) + NEW_LINE;
}
/**
* Creates a line difference String with a header if non-null. Null is
* returned if there is no diff.
*
* @param orig The original lines.
* @param cur The current lines.
* @param header The header to be used if non-null diff. If header is null,
* no header included.
* @return The pretty-printed diff.
*/
private String diffLines(List<String> orig, List<String> cur, String header) {
//compute the patch: this is the diffutils part
Patch<String> patch = DiffUtils.diff(orig, cur);
String diff = patch.getDeltas().stream()
.map(delta -> getLinesDiffString(delta))
.collect(Collectors.joining(String.join(NEW_LINE, DIFF_BREAK)));
if (StringUtils.isBlank(diff)) {
return null;
}
return (header != null)
? header + NEW_LINE + diff
: diff;
}
private String getHeaderWithDivider(String remark) {
String divider = "-----------------------------------------------------------";
return String.join(NEW_LINE, divider, remark, divider);
}
private String getRelative(File rootDirectory, File file) {
return rootDirectory.toURI().relativize(file.toURI()).getPath();
}
}

View File

@ -35,6 +35,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import junit.framework.Test;
import junit.framework.TestCase;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.cxf.common.util.CollectionUtils;
import org.netbeans.junit.NbModuleSuite;
import org.openide.util.Lookup;
@ -65,6 +67,7 @@ public class MainTestRunner extends TestCase {
private static final Logger logger = Logger.getLogger(MainTestRunner.class.getName());
private static final String CONFIG_FILE_KEY = "integrationConfigFile";
private static final ConfigDeserializer configDeserializer = new ConfigDeserializer();
private static final DiffService diffService = new DiffService();
private static final ConfigurationModuleManager configurationModuleManager = new ConfigurationModuleManager();
/**
@ -107,63 +110,67 @@ public class MainTestRunner extends TestCase {
if (!CollectionUtils.isEmpty(config.getTestSuites())) {
for (TestSuiteConfig testSuiteConfig : config.getTestSuites()) {
String caseName = testSuiteConfig.getName();
for (CaseType caseType : IntegrationCaseType.getCaseTypes(testSuiteConfig.getCaseTypes())) {
// create an autopsy case for each case in the config and for each case type for the specified case.
// then run ingest for the case.
Case autopsyCase = createCaseWithDataSources(
envConfig.getWorkingDirectory(),
envConfig.getRootCaseOutputPath(),
caseName,
caseType,
testSuiteConfig.getDataSources());
if (autopsyCase == null || autopsyCase != Case.getCurrentCase()) {
logger.log(Level.WARNING,
String.format("Case was not properly ingested or setup correctly for environment. Case is %s and current case is %s.",
autopsyCase, Case.getCurrentCase()));
return;
}
// run configuration modules and get result
Pair<IngestJobSettings, List<ConfigurationModule<?>>> configurationResult
= configurationModuleManager.runConfigurationModules(caseName, testSuiteConfig.getConfigurationModules());
IngestJobSettings ingestSettings = configurationResult.first();
List<ConfigurationModule<?>> configModules = configurationResult.second();
// run ingest with ingest settings derived from configuration modules.
runIngest(autopsyCase, ingestSettings, caseName);
// once ingested, run integration tests to produce output.
OutputResults results = runIntegrationTests(testSuiteConfig.getIntegrationTests());
// revert any autopsy environment changes made by configuration modules.
configurationModuleManager.revertConfigurationModules(configModules);
String outputFolder = PathUtil.getAbsolutePath(envConfig.getWorkingDirectory(), envConfig.getRootTestOutputPath());
// write the results for the case to a file
results.serializeToFile(
envConfig.getUseRelativeOutput() == true ?
Paths.get(outputFolder, testSuiteConfig.getRelativeOutputPath()).toString() :
outputFolder,
testSuiteConfig.getName(),
caseType
);
try {
Case.closeCurrentCase();
} catch (CaseActionException ex) {
logger.log(Level.WARNING, "There was an error while trying to close current case: {0}", caseName);
return;
runIntegrationTestSuite(envConfig, caseType, testSuiteConfig);
} catch (CaseActionException | IllegalStateException ex) {
logger.log(Level.WARNING, "There was an error working with current case: " + testSuiteConfig.getName(), ex);
}
}
}
// write diff to file if requested
writeDiff(envConfig);
}
}
/**
* Runs a single test suite.
*
* @param envConfig The integrationt test environment config.
* @param caseType The type of case (single user, multi user).
* @param testSuiteConfig The configuration for the case.
*/
private void runIntegrationTestSuite(EnvConfig envConfig, CaseType caseType, TestSuiteConfig testSuiteConfig) throws CaseActionException, IllegalStateException {
String caseName = testSuiteConfig.getName();
// create an autopsy case for each case in the config and for each case type for the specified case.
// then run ingest for the case.
Case autopsyCase = createCaseWithDataSources(
envConfig.getWorkingDirectory(),
envConfig.getRootCaseOutputPath(),
caseName,
caseType,
testSuiteConfig.getDataSources());
if (autopsyCase == null || autopsyCase != Case.getCurrentCase()) {
throw new IllegalStateException(String.format("Case was not properly ingested or setup correctly for environment. Case is %s and current case is %s.",
autopsyCase, Case.getCurrentCase()));
}
// run configuration modules and get result
Pair<IngestJobSettings, List<ConfigurationModule<?>>> configurationResult
= configurationModuleManager.runConfigurationModules(caseName, testSuiteConfig.getConfigurationModules());
IngestJobSettings ingestSettings = configurationResult.first();
List<ConfigurationModule<?>> configModules = configurationResult.second();
// run ingest with ingest settings derived from configuration modules.
runIngest(autopsyCase, ingestSettings, caseName);
// once ingested, run integration tests to produce output.
OutputResults results = runIntegrationTests(testSuiteConfig.getIntegrationTests());
// revert any autopsy environment changes made by configuration modules.
configurationModuleManager.revertConfigurationModules(configModules);
String outputFolder = PathUtil.getAbsolutePath(envConfig.getWorkingDirectory(), envConfig.getRootTestOutputPath());
// write the results for the case to a file
results.serializeToFile(
envConfig.getUseRelativeOutput() == true
? Paths.get(outputFolder, testSuiteConfig.getRelativeOutputPath()).toString()
: outputFolder,
testSuiteConfig.getName(),
caseType
);
Case.closeCurrentCase();
}
/**
* Creates a case with the given data sources.
*
@ -241,6 +248,7 @@ public class MainTestRunner extends TestCase {
/**
* Runs ingest on the current case.
*
* @param openCase The currently open case.
* @param ingestJobSettings The ingest job settings to be used.
* @param caseName The name of the case to be used for error messages.
@ -291,7 +299,7 @@ public class MainTestRunner extends TestCase {
if (testMethod.getParameters().length > 1) {
throw new IllegalArgumentException(String.format("Could not call method %s in class %s. Multiple parameters cannot be handled.",
testMethod.getName(), testGroup.getClass().getCanonicalName()));
// if there is a parameter, deserialize parameters to the specified type.
// if there is a parameter, deserialize parameters to the specified type.
} else if (testMethod.getParameters().length > 0) {
parameters = new Object[]{configDeserializer.convertToObj(parametersMap, testMethod.getParameterTypes()[0])};
}
@ -312,12 +320,13 @@ public class MainTestRunner extends TestCase {
return results;
}
/**
* Runs a test method in a test suite on the current case.
*
* @param testGroup The test suite to which the method belongs.
* @param testMethod The java reflection method to run.
* @param parameters The parameters to use with this method or none/empty array.
* @param parameters The parameters to use with this method or none/empty
* array.
* @return The results of running the method.
*/
private Object runIntegrationTestMethod(IntegrationTestGroup testGroup, Method testMethod, Object[] parameters) {
@ -343,4 +352,39 @@ public class MainTestRunner extends TestCase {
return serializableResult;
}
/**
* Writes any differences found between gold and output to a diff file. Only
* works if a gold and diff location are specified in the EnvConfig.
*
* @param envConfig The env config.
*/
private void writeDiff(EnvConfig envConfig) {
if (StringUtils.isBlank(envConfig.getRootGoldPath()) || StringUtils.isBlank(envConfig.getDiffOutputPath())) {
logger.log(Level.INFO, "gold path or diff output path not specified. Not creating diff.");
}
String goldPath = PathUtil.getAbsolutePath(envConfig.getWorkingDirectory(), envConfig.getRootGoldPath());
File goldDir = new File(goldPath);
if (!goldDir.exists()) {
logger.log(Level.WARNING, String.format("Gold does not exist at location: %s. Not creating diff.", goldDir.toString()));
}
String outputPath = PathUtil.getAbsolutePath(envConfig.getWorkingDirectory(), envConfig.getRootCaseOutputPath());
File outputDir = new File(outputPath);
if (!outputDir.exists()) {
logger.log(Level.WARNING, String.format("Output path does not exist at location: %s. Not creating diff.", outputDir.toString()));
}
String diffPath = PathUtil.getAbsolutePath(envConfig.getWorkingDirectory(), envConfig.getDiffOutputPath());
String diff = diffService.diffFilesOrDirs(goldDir, outputDir);
if (StringUtils.isNotBlank(diff)) {
try {
FileUtils.writeStringToFile(new File(diffPath), diff, "UTF-8");
} catch (IOException ex) {
logger.log(Level.SEVERE, "Unable to write diff file to " + diffPath);
}
}
}
}

View File

@ -29,12 +29,15 @@ public class EnvConfig {
private final String rootCaseOutputPath;
private final String rootTestOutputPath;
private final String rootTestSuitesPath;
private final String rootGoldPath;
private final String diffOutputPath;
private final ConnectionConfig connectionInfo;
private String workingDirectory;
private Boolean useRelativeOutput;
/**
* Main constructor.
*
@ -50,6 +53,8 @@ public class EnvConfig {
* the same relative path structure as the file (i.e. if file was found at
* /rootTestSuitesPath/folderX/fileY.json then it will now be outputted in
* /rootTestOutputPath/folderX/fileY/)
* @param rootGoldPath The path to the gold data for diff comparison.
* @param diffOutputPath The file location for diff output.
*/
@JsonCreator
public EnvConfig(
@ -58,11 +63,15 @@ public class EnvConfig {
@JsonProperty("rootTestOutputPath") String rootTestOutputPath,
@JsonProperty("connectionInfo") ConnectionConfig connectionInfo,
@JsonProperty("workingDirectory") String workingDirectory,
@JsonProperty("useRelativeOutput") Boolean useRelativeOutput) {
@JsonProperty("useRelativeOutput") Boolean useRelativeOutput,
@JsonProperty("rootGoldPath") String rootGoldPath,
@JsonProperty("diffOutputPath") String diffOutputPath) {
this.rootCaseOutputPath = rootCaseOutputPath;
this.rootTestOutputPath = rootTestOutputPath;
this.rootTestSuitesPath = rootTestSuitesPath;
this.rootGoldPath = rootGoldPath;
this.diffOutputPath = diffOutputPath;
this.connectionInfo = connectionInfo;
this.workingDirectory = workingDirectory;
@ -139,4 +148,20 @@ public class EnvConfig {
public void setUseRelativeOutput(boolean useRelativeOutput) {
this.useRelativeOutput = useRelativeOutput;
}
/**
* @return The path to the gold data for diff comparison.
*/
public String getRootGoldPath() {
return rootGoldPath;
}
/**
* @return The file location for diff output.
*/
public String getDiffOutputPath() {
return diffOutputPath;
}
}