diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/datasourcesummary/UserActivitySummaryTests.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/datasourcesummary/UserActivitySummaryTests.java
index ac349c7b5aa889d9336480856aaecf9e889f5cea..1ed9dbccafbc07b83562ce5ac832a0db9356bf73 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/datasourcesummary/UserActivitySummaryTests.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/datasourcesummary/UserActivitySummaryTests.java
@@ -32,13 +32,13 @@
 import org.sleuthkit.datamodel.TskCoreException;
 import org.openide.util.lookup.ServiceProvider;
 import org.sleuthkit.autopsy.integrationtesting.IntegrationTest;
-import org.sleuthkit.autopsy.integrationtesting.IntegrationTests;
+import org.sleuthkit.autopsy.integrationtesting.IntegrationTestGroup;
 
 /**
  * Tests for the UserActivitySummary class.
  */
-@ServiceProvider(service = IntegrationTests.class)
-public class UserActivitySummaryTests implements IntegrationTests {
+@ServiceProvider(service = IntegrationTestGroup.class)
+public class UserActivitySummaryTests implements IntegrationTestGroup {
 
     /**
      * Runs UserActivitySummary.getRecentDomains for all data sources found in
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/ConfigurationModule.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/ConfigurationModule.java
index 9e45cd858e4772647eac93778a34b6ca0b6f7145..6df583b1dceaebcb438d9baa3cd732b23749a9a3 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/ConfigurationModule.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/ConfigurationModule.java
@@ -18,17 +18,33 @@
  */
 package org.sleuthkit.autopsy.integrationtesting;
 
-import java.util.Map;
 import org.sleuthkit.autopsy.ingest.IngestJobSettings;
 
 /**
- *
- * @author gregd
+ * Interface for a module that performs configuration. Implementers of this
+ * class must have a no-parameter constructor in order to be instantiated
+ * properly.
  */
 public interface ConfigurationModule<T> {
 
+    /**
+     * Configures the autopsy environment and updates the current ingest job
+     * settings to augment with any additional templates that may need to be
+     * added or any other ingest job setting changes.
+     *
+     * @param curSettings The current IngestJobSettings.
+     * @param parameters The parameters object for this configuration module.
+     * @return The new IngestJobSettings or 'curSettings' if no
+     * IngestJobSettings need to be made.
+     */
     IngestJobSettings configure(IngestJobSettings curSettings, T parameters);
 
+    /**
+     * In the event that settings outside the IngestJobSettings were altered,
+     * this provides a means of reverting those changes when the test completes.
+     * Configuration changes in the 'configure' method can be captured in this
+     * object for revert to utilize.
+     */
     default void revert() {
     }
 }
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/ConfigurationModuleManager.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/ConfigurationModuleManager.java
index 241d13f4f36cde0c6f82bff43e9be3ca52a9118a..08ec03aaa02882acedf1291f2dc1ed967eef05a1 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/ConfigurationModuleManager.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/ConfigurationModuleManager.java
@@ -1,7 +1,20 @@
 /*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
+ * 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;
 
@@ -23,20 +36,25 @@
 import org.sleuthkit.autopsy.ingest.IngestModuleTemplate;
 import org.sleuthkit.autopsy.integrationtesting.config.ConfigDeserializer;
 import org.sleuthkit.autopsy.integrationtesting.config.ParameterizedResourceConfig;
-import org.sleuthkit.autopsy.integrationtesting.config.TestSuiteConfig;
 
 /**
- *
- * @author gregd
+ * In charge of running configuration modules to set up the environment and then
+ * running revert when the test completes.
  */
 public class ConfigurationModuleManager {
+
     private static final IngestModuleFactoryService ingestModuleFactories = new IngestModuleFactoryService();
     private static final Logger logger = Logger.getLogger(ConfigurationModuleManager.class.getName());
-    
+
     private static final IngestJobSettings.IngestType DEFAULT_INGEST_FILTER_TYPE = IngestJobSettings.IngestType.ALL_MODULES;
     private static final Set<String> DEFAULT_EXCLUDED_MODULES = Stream.of("Plaso").collect(Collectors.toSet());
     private static final ConfigDeserializer configDeserializer = new ConfigDeserializer();
-    
+
+    /**
+     * Reverts the effects of the given configuration module objects.
+     *
+     * @param configModules The configuration modules.
+     */
     void revertConfigurationModules(List<ConfigurationModule<?>> configModules) {
         List<ConfigurationModule<?>> reversed = new ArrayList<>(configModules);
         Collections.reverse(reversed);
@@ -45,10 +63,25 @@ void revertConfigurationModules(List<ConfigurationModule<?>> configModules) {
         }
     }
 
+    /**
+     * Returns a profile name to be used with IngestJobSettings for a given
+     * caseName.
+     *
+     * @param caseName The case name.
+     * @return The name of the profile.
+     */
     static String getProfileName(String caseName) {
         return String.format("integrationTestProfile-%s", caseName);
     }
 
+    /**
+     * Returns a default IngestJobSettings object in the event that no
+     * configuration modules are specified.
+     *
+     * @param caseName The name of the case used for the profile name of the
+     * settings.
+     * @return The default ingest job settings.
+     */
     private IngestJobSettings getDefaultIngestConfig(String caseName) {
         return new IngestJobSettings(
                 getProfileName(caseName),
@@ -60,11 +93,23 @@ private IngestJobSettings getDefaultIngestConfig(String caseName) {
         );
     }
 
-    Pair<IngestJobSettings, List<ConfigurationModule<?>>> runConfigurationModules(String caseName, TestSuiteConfig config) {
-        if (CollectionUtils.isEmpty(config.getConfigurationModules())) {
+    /**
+     * Runs configuration modules in the TestSuiteConfig.
+     *
+     * @param caseName The name of the case.
+     * @param configModules The configuration modules to be run specified by the
+     * resource and their accompanying parameters.
+     * @return A tuple of the generated IngestJobSettings and a list of the
+     * generated configuration modules to later be used for reverting any
+     * environmental changes.
+     */
+    Pair<IngestJobSettings, List<ConfigurationModule<?>>> runConfigurationModules(String caseName, List<ParameterizedResourceConfig> configModules) {
+        // if no config modules, return default ingest settings
+        if (CollectionUtils.isEmpty(configModules)) {
             return Pair.of(getDefaultIngestConfig(caseName), Collections.emptyList());
         }
 
+        // create a base ingest job settings object with no templates.
         IngestJobSettings curConfig = new IngestJobSettings(
                 getProfileName(caseName),
                 DEFAULT_INGEST_FILTER_TYPE,
@@ -72,8 +117,10 @@ Pair<IngestJobSettings, List<ConfigurationModule<?>>> runConfigurationModules(St
 
         List<ConfigurationModule<?>> configurationModuleCache = new ArrayList<>();
 
-        for (ParameterizedResourceConfig configModule : config.getConfigurationModules()) {
+        // run through the configuration for each configuration module 
+        for (ParameterizedResourceConfig configModule : configModules) {
             Pair<IngestJobSettings, ConfigurationModule<?>> ingestResult = runConfigurationModule(curConfig, configModule);
+            // if there are results, update to the new ingest job settings and cache the config module.
             if (ingestResult != null) {
                 curConfig = ingestResult.first() == null ? curConfig : ingestResult.first();
                 if (ingestResult.second() != null) {
@@ -84,7 +131,19 @@ Pair<IngestJobSettings, List<ConfigurationModule<?>>> runConfigurationModules(St
         return Pair.of(curConfig, configurationModuleCache);
     }
 
+    /**
+     * Run a configuration module as specified by the paramerized resource
+     * config returning the acquired configuration module and the updated ingest
+     * job settings.
+     *
+     * @param curConfig The current ingest job settings.
+     * @param configModule The resource identifying the config module and any
+     * accompanying parameters.
+     * @return A tuple containing the ingest job settings and the instantiated
+     * configuration module that generated changes (used later for reverting).
+     */
     private Pair<IngestJobSettings, ConfigurationModule<?>> runConfigurationModule(IngestJobSettings curConfig, ParameterizedResourceConfig configModule) {
+        // acquire class described by resource
         Class<?> clazz = null;
         try {
             clazz = Class.forName(configModule.getResource());
@@ -93,11 +152,13 @@ private Pair<IngestJobSettings, ConfigurationModule<?>> runConfigurationModule(I
             return null;
         }
 
+        // assure that the class is a configuration module.
         if (clazz == null || !ConfigurationModule.class.isAssignableFrom(clazz)) {
             logger.log(Level.WARNING, String.format("%s does not seem to be an instance of a configuration module.", configModule.getResource()));
             return null;
         }
 
+        // determine generic parameter type
         Type configurationModuleType = Stream.of(clazz.getGenericInterfaces())
                 .filter(type -> type instanceof ParameterizedType && ((ParameterizedType) type).getRawType().equals(ConfigurationModule.class))
                 .map(type -> ((ParameterizedType) type).getActualTypeArguments()[0])
@@ -109,6 +170,7 @@ private Pair<IngestJobSettings, ConfigurationModule<?>> runConfigurationModule(I
             return null;
         }
 
+        // instantiate the object from the class and run the configure method.
         ConfigurationModule<?> configModuleObj = null;
         Object result = null;
         try {
@@ -119,10 +181,11 @@ private Pair<IngestJobSettings, ConfigurationModule<?>> runConfigurationModule(I
             logger.log(Level.SEVERE, String.format("There was an error calling configure method on Configuration Module %s", configModule.getResource()), ex);
         }
 
+        // return results or an error if no results returned.
         if (result instanceof IngestJobSettings) {
             return Pair.of((IngestJobSettings) result, configModuleObj);
         } else {
-            logger.log(Level.SEVERE, String.format("Could not retrieve IngestJobSettings from %s", configModule.getResource()));
+            logger.log(Level.SEVERE, String.format("Could not retrieve IngestJobSettings or null was returned from %s", configModule.getResource()));
             return null;
         }
     }
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IngestModuleFactoryService.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IngestModuleFactoryService.java
index 1e4b8aefec0240149a4b535768af623359b3445f..467bb5e65cdd3c1c286e2e1b199faa44977bdaa4 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IngestModuleFactoryService.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IngestModuleFactoryService.java
@@ -1,21 +1,59 @@
 /*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
+ * 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 java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.openide.util.Lookup;
 import org.sleuthkit.autopsy.ingest.IngestModuleFactory;
+import org.sleuthkit.autopsy.ingest.IngestModuleFactoryAdapter;
+import org.sleuthkit.autopsy.python.JythonModuleLoader;
 
 /**
- *
- * @author gregd
+ * Class for obtaining ingest module factories since IngestModuleFactoryLoader
+ * is not accessible.
  */
 public class IngestModuleFactoryService {
+
+    /**
+     * Returns any found ingest modules from implementers of
+     * IngestModuleFactory, IngestModuleFactoryAdapter, and Jython modules.
+     *
+     * @return The ingest module factories.
+     */
     public List<IngestModuleFactory> getFactories() {
-        return new ArrayList<>(Lookup.getDefault().lookupAll(IngestModuleFactory.class));
+        // factories using lookup for IngestModuleFactory, IngestModuleFactoryAdapter and jython modules
+        Stream<Collection<? extends IngestModuleFactory>> factoryCollections = Stream.of(
+                Lookup.getDefault().lookupAll(IngestModuleFactory.class),
+                Lookup.getDefault().lookupAll(IngestModuleFactoryAdapter.class),
+                JythonModuleLoader.getIngestModuleFactories());
+
+        // get unique by module display name
+        Map<String, IngestModuleFactory> factories = factoryCollections
+                .flatMap(coll -> coll.stream())
+                .collect(Collectors.toMap(f -> f.getModuleDisplayName(), f -> f, (f1, f2) -> f1));
+
+        // return list of values
+        return new ArrayList<>(factories.values());
     }
 }
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IngestModuleSetupManager.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IngestModuleSetupManager.java
index 47b9354bbc36083741d8d073207530f5a9bee781..e25c7a2edb37c659077c0ec573a9ed8139ae4fae 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IngestModuleSetupManager.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IngestModuleSetupManager.java
@@ -33,48 +33,68 @@
  */
 public class IngestModuleSetupManager implements ConfigurationModule<IngestModuleSetupManager.ConfigArgs> {
 
+    /**
+     * The parameters used when calling 'configure' in this class.
+     */
     public static class ConfigArgs {
+
         private final List<String> modules;
 
+        /**
+         * Main constructor.
+         *
+         * @param modules The ingest module factories to be loaded.
+         */
         public ConfigArgs(List<String> modules) {
             this.modules = modules;
         }
 
+        /**
+         * @return The ingest module factories to be loaded.
+         */
         public List<String> getModules() {
             return modules;
         }
     }
-    
+
     private static final IngestModuleFactoryService ingestModuleFactories = new IngestModuleFactoryService();
-    
-    
+
     @Override
     public IngestJobSettings configure(IngestJobSettings curSettings, IngestModuleSetupManager.ConfigArgs parameters) {
-        
+
+        // get the profile from the IngestJobSettings
         String context = curSettings.getExecutionContext();
         if (StringUtils.isNotBlank(context) && context.indexOf('.') > 0) {
             context = context.substring(0, context.indexOf('.'));
         }
-        
+
+        // get current templates
         Map<String, IngestModuleTemplate> curTemplates = curSettings.getEnabledIngestModuleTemplates().stream()
                 .collect(Collectors.toMap(t -> t.getModuleFactory().getClass().getCanonicalName(), t -> t, (t1, t2) -> t1));
-        
+
+        // get all the factories determined by canonical name
         Map<String, IngestModuleFactory> allFactories = ingestModuleFactories.getFactories().stream()
                 .collect(Collectors.toMap(f -> f.getClass().getCanonicalName(), f -> f, (f1, f2) -> f1));
-        
+
+        // add current templates to the list of templates to return.
         List<IngestModuleTemplate> newTemplates = new ArrayList<>(curTemplates.values());
-        
+
+        // if there are parameters, add any relevant ingest module factories
         if (parameters != null && !CollectionUtils.isEmpty(parameters.getModules())) {
             List<IngestModuleTemplate> templatesToAdd = parameters.getModules().stream()
-                    .filter((className) -> !curTemplates.containsKey(className))
-                    .map((className) -> allFactories.get(className))
+                    // ensure only one of each type of factory is added
+                    .distinct()
+                    // if the factory to be added is already contained in curTemplates or allFactories does not contain item, null is returned
+                    .map((className) -> curTemplates.containsKey(className) ? null : allFactories.get(className))
+                    // filter out any null items
                     .filter((factory) -> factory != null)
+                    // create a template for any remaining items
                     .map((factory) -> new IngestModuleTemplate(factory, factory.getDefaultIngestJobSettings()))
                     .collect(Collectors.toList());
-            
+
             newTemplates.addAll(templatesToAdd);
         }
-        
+
         return new IngestJobSettings(
                 context,
                 IngestJobSettings.IngestType.ALL_MODULES,
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IntegrationTests.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IntegrationTestGroup.java
similarity index 81%
rename from Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IntegrationTests.java
rename to Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IntegrationTestGroup.java
index 59aa5ae5cd4352bf0e252f45edfe40f680f98400..244f7b35f967eda765452d162a4998a093340a3e 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IntegrationTests.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/IntegrationTestGroup.java
@@ -19,27 +19,31 @@
 package org.sleuthkit.autopsy.integrationtesting;
 
 /**
- * Basic interface for integration test suite.
+ * Basic interface for integration test group.
  */
-public interface IntegrationTests {
+public interface IntegrationTestGroup {
+
     /**
      * Allows for a test to perform any necessary setup.
      */
-    default void setup() {}
- 
+    default void setup() {
+    }
+
     /**
      * Allows for a test to perform any necessary disposing of resources.
      */
-    default void tearDown() {}
-    
+    default void tearDown() {
+    }
+
     /**
      * Allows for this suite of tests to perform any necessary setup.
      */
-    default void setupClass() {}
-    
+    default void setupClass() {
+    }
+
     /**
      * Allows for this suite of tests to dispose of resources.
      */
-    default void tearDownClass() {}
+    default void tearDownClass() {
+    }
 }
- 
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/MainTestRunner.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/MainTestRunner.java
index a78f211bf5b7d62c830ecd6226a40ccc2e31336e..13d351c1b6390958188c06e2717d43b35f462b32 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/MainTestRunner.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/MainTestRunner.java
@@ -54,7 +54,6 @@
 import org.sleuthkit.autopsy.integrationtesting.config.TestingConfig;
 import org.sleuthkit.autopsy.testutils.IngestUtils;
 import org.sleuthkit.datamodel.TskCoreException;
-import static ucar.unidata.util.Format.i;
 
 /**
  * Main entry point for running integration tests. Handles processing
@@ -94,7 +93,7 @@ public void runIntegrationTests() {
         String configFile = System.getProperty(CONFIG_FILE_KEY);
         IntegrationTestConfig config;
         try {
-            config = configDeserializer.getConfigFromFile(configFile);
+            config = configDeserializer.getConfigFromFile(new File(configFile));
         } catch (IOException ex) {
             logger.log(Level.WARNING, "There was an error processing integration test config at " + configFile, ex);
             return;
@@ -113,7 +112,12 @@ public void runIntegrationTests() {
                 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, caseName, caseType, testSuiteConfig.getDataSources());
+                    Case autopsyCase = createCaseWithDataSources(
+                            envConfig.getWorkingDirectory(),
+                            envConfig.getRootCaseOutputPath(),
+                            caseName,
+                            caseType,
+                            testSuiteConfig.getDataSources());
 
                     if (autopsyCase == null || autopsyCase != Case.getCurrentCase()) {
                         logger.log(Level.WARNING,
@@ -122,24 +126,29 @@ public void runIntegrationTests() {
                         return;
                     }
 
+                    // run configuration modules and get result
                     Pair<IngestJobSettings, List<ConfigurationModule<?>>> configurationResult
-                            = configurationModuleManager.runConfigurationModules(caseName, testSuiteConfig);
+                            = 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(
-                            Paths.get(outputFolder, testSuiteConfig.getRelativeOutputPath()).toString(),
+                            envConfig.getUseRelativeOutput() == true ? 
+                                    Paths.get(outputFolder, testSuiteConfig.getRelativeOutputPath()).toString() : 
+                                    outputFolder,
                             testSuiteConfig.getName(),
                             caseType
                     );
@@ -155,10 +164,22 @@ public void runIntegrationTests() {
         }
     }
 
-    private Case createCaseWithDataSources(EnvConfig envConfig, String caseName, CaseType caseType, List<String> dataSourcePaths) {
+    /**
+     * Creates a case with the given data sources.
+     *
+     * @param workingDirectory The base working directory (if caseOutputPath or
+     * dataSourcePaths are relative, this is the working directory).
+     * @param caseOutputPath The case output path.
+     * @param caseName The name of the case (unique case name appends time
+     * stamp).
+     * @param caseType The type of case (single user / multi user).
+     * @param dataSourcePaths The path to the data sources.
+     * @return The generated case that is now the current case.
+     */
+    private Case createCaseWithDataSources(String workingDirectory, String caseOutputPath, String caseName, CaseType caseType, List<String> dataSourcePaths) {
         Case openCase = null;
         String uniqueCaseName = String.format("%s_%s", caseName, TimeStampUtils.createTimeStamp());
-        String outputFolder = PathUtil.getAbsolutePath(envConfig.getWorkingDirectory(), envConfig.getRootCaseOutputPath());
+        String outputFolder = PathUtil.getAbsolutePath(workingDirectory, caseOutputPath);
         String caseOutputFolder = Paths.get(outputFolder, uniqueCaseName).toString();
         File caseOutputFolderFile = new File(caseOutputFolder);
         if (!caseOutputFolderFile.exists()) {
@@ -178,7 +199,7 @@ private Case createCaseWithDataSources(EnvConfig envConfig, String caseName, Cas
                 }
             }
             break;
-            
+
             case MULTI_USER_CASE:
             // TODO
             default:
@@ -190,7 +211,7 @@ private Case createCaseWithDataSources(EnvConfig envConfig, String caseName, Cas
             return null;
         }
 
-        addDataSourcesToCase(PathUtil.getAbsolutePaths(envConfig.getWorkingDirectory(), dataSourcePaths), caseName);
+        addDataSourcesToCase(PathUtil.getAbsolutePaths(workingDirectory, dataSourcePaths), caseName);
         return openCase;
     }
 
@@ -198,7 +219,7 @@ private Case createCaseWithDataSources(EnvConfig envConfig, String caseName, Cas
      * Adds the data sources to the current case.
      *
      * @param pathStrings The list of paths for the data sources.
-     * @param caseName The name of the case.
+     * @param caseName The name of the case (used for error messages).
      */
     private void addDataSourcesToCase(List<String> pathStrings, String caseName) {
         for (String strPath : pathStrings) {
@@ -218,6 +239,13 @@ private void addDataSourcesToCase(List<String> pathStrings, String caseName) {
         }
     }
 
+    /**
+     * 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.
+     * @return The case that was ingested.
+     */
     private Case runIngest(Case openCase, IngestJobSettings ingestJobSettings, String caseName) {
         try {
             // IngestJobSettings ingestJobSettings = SETUP_UTIL.setupEnvironment(envConfig, testSuiteConfig);
@@ -232,16 +260,14 @@ private Case runIngest(Case openCase, IngestJobSettings ingestJobSettings, Strin
     /**
      * Runs the integration tests and serializes results to disk.
      *
-     * @param envConfig The overall config.
      * @param testSuiteConfig The configuration for a particular case.
-     * @param caseType The case type (single user / multi user) to create.
      */
     private OutputResults runIntegrationTests(TestingConfig testSuiteConfig) {
         // this will capture output results
         OutputResults results = new OutputResults();
 
-        // run through each ConsumerIntegrationTest
-        for (IntegrationTests testGroup : Lookup.getDefault().lookupAll(IntegrationTests.class)) {
+        // run through each IntegrationTestGroup
+        for (IntegrationTestGroup testGroup : Lookup.getDefault().lookupAll(IntegrationTestGroup.class)) {
 
             // if test should not be included in results, skip it.
             if (!testSuiteConfig.hasIncludedTest(testGroup.getClass().getCanonicalName())) {
@@ -261,11 +287,13 @@ private OutputResults runIntegrationTests(TestingConfig testSuiteConfig) {
 
             for (Method testMethod : testMethods) {
                 Object[] parameters = new Object[0];
+                // no more than 1 parameter in a test method.
                 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.
                 } else if (testMethod.getParameters().length > 0) {
-                    parameters = new Object[]{configDeserializer.convertToObj(parametersMap, testMethod.getParameters().getClass())};
+                    parameters = new Object[]{configDeserializer.convertToObj(parametersMap, testMethod.getParameterTypes()[0])};
                 }
 
                 Object serializableResult = runIntegrationTestMethod(testGroup, testMethod, parameters);
@@ -284,20 +312,22 @@ private OutputResults runIntegrationTests(TestingConfig testSuiteConfig) {
         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.
+     * @return The results of running the method.
      */
-    private Object runIntegrationTestMethod(IntegrationTests testGroup, Method testMethod, Object[] parameters) {
+    private Object runIntegrationTestMethod(IntegrationTestGroup testGroup, Method testMethod, Object[] parameters) {
         testGroup.setup();
 
         // run the test method and get the results
         Object serializableResult = null;
 
         try {
-            serializableResult = testMethod.invoke(testGroup, parameters);
+            serializableResult = testMethod.invoke(testGroup, parameters == null ? new Object[0] : parameters);
         } catch (IllegalAccessException | IllegalArgumentException ex) {
             logger.log(Level.WARNING,
                     String.format("test method %s in %s could not be properly invoked",
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/OutputResults.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/OutputResults.java
index fc65a1ef5a7c9e9aeed5591d0fc067103a8b8fc9..4f21dd6d24680c693bac0fe6afc8f8a6c3194bf5 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/OutputResults.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/OutputResults.java
@@ -185,7 +185,6 @@ protected MappingNode representJavaBean(Set<Property> properties, Object javaBea
     /**
      * Serializes results of a test to a yaml file.
      *
-     * @param results The results to serialize.
      * @param outputFolder The folder where the yaml should be written.
      * @param caseName The name of the case (used to determine filename).
      * @param caseType The type of case (used to determine filename).
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/PathUtil.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/PathUtil.java
index 9eec6839fdf7a3cb7b6d70b0ccd24a9af0609d91..16e6ec0094119da953d3d9e54dcb2acbd0d1d75c 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/PathUtil.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/PathUtil.java
@@ -28,6 +28,15 @@
  */
 public final class PathUtil {
 
+    /**
+     * If the relPath is an absolute path, that is returned. Otherwise, it is
+     * treated as a relative path using the working directory.
+     *
+     * @param workingDirectory The working directory.
+     * @param relPath The path.
+     * @return The relPath with the workingDirectory prepended if relPath is a
+     * relative path.
+     */
     public static String getAbsolutePath(String workingDirectory, String relPath) {
         if (StringUtils.isBlank(workingDirectory)) {
             return relPath;
@@ -35,16 +44,25 @@ public static String getAbsolutePath(String workingDirectory, String relPath) {
             if (Paths.get(relPath).isAbsolute()) {
                 return relPath;
             } else {
-                return Paths.get(workingDirectory, relPath).toString();   
+                return Paths.get(workingDirectory, relPath).toString();
             }
         }
     }
 
+    /**
+     * If one of the relPaths is an absolute path, that is returned. Otherwise,
+     * each is treated as a relative path using the working directory.
+     *
+     * @param workingDirectory The working directory.
+     * @param relPath The paths.
+     * @return The list of paths with the workingDirectory prepended if a path
+     * is a relative path.
+     */
     public static List<String> getAbsolutePaths(String workingDirectory, List<String> relPaths) {
         if (relPaths == null) {
             return null;
         }
-        
+
         return relPaths.stream()
                 .map((relPath) -> getAbsolutePath(workingDirectory, relPath))
                 .collect(Collectors.toList());
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ConfigDeserializer.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ConfigDeserializer.java
index a6030f66191d7d23ca4c0dce427af05ed5fb49b2..1deda4ff7628e62f21af7d617c1f47a5afb05dde 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ConfigDeserializer.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ConfigDeserializer.java
@@ -1,13 +1,25 @@
 /*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
+ * 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.config;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.fasterxml.jackson.databind.type.CollectionType;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
@@ -30,8 +42,7 @@
 import org.sleuthkit.autopsy.integrationtesting.PathUtil;
 
 /**
- *
- * @author gregd
+ * Handles deserializing configuration items.
  */
 public class ConfigDeserializer {
 
@@ -39,6 +50,14 @@ public class ConfigDeserializer {
     private static final Logger logger = Logger.getLogger(ConfigDeserializer.class.getName());
     private static final ObjectMapper mapper = new ObjectMapper();
 
+    /**
+     * Creates an object of type T by re-deserializing the map to the specified
+     * type.
+     *
+     * @param toConvert The map to convert.
+     * @param clazz The type of object to deserialize to.
+     * @return The object converted to the specified type.
+     */
     public <T> T convertToObj(Map<String, Object> toConvert, Type clazz) {
         GsonBuilder builder = new GsonBuilder();
         Gson gson = builder.create();
@@ -48,14 +67,16 @@ public <T> T convertToObj(Map<String, Object> toConvert, Type clazz) {
 
     /**
      * Deserializes the json config specified at the given path into the java
-     * equivalent IntegrationTestConfig object.
+     * equivalent IntegrationTestConfig object. This uses information in env
+     * config file to determine test suite locations.
      *
      * @param filePath The path to the config.
      * @return The java object.
      * @throws IOException If there is an error opening the file.
+     * @throws IllegalStateException If the file cannot be validated.
      */
-    public IntegrationTestConfig getConfigFromFile(String envConfigFile) throws IOException, IllegalStateException {
-        EnvConfig envConfig = getEnvConfig(new File(envConfigFile));
+    public IntegrationTestConfig getConfigFromFile(File envConfigFile) throws IOException, IllegalStateException {
+        EnvConfig envConfig = getEnvConfig(envConfigFile);
         String testSuiteConfigPath = PathUtil.getAbsolutePath(envConfig.getWorkingDirectory(), envConfig.getRootTestSuitesPath());
 
         return new IntegrationTestConfig(
@@ -64,11 +85,28 @@ public IntegrationTestConfig getConfigFromFile(String envConfigFile) throws IOEx
         );
     }
 
+    /**
+     * Deserializes the specified json file into an EnvConfig object.
+     *
+     * @param envConfigFile The file location for the environmental config.
+     * @return The deserialized file.
+     * @throws IOException
+     * @throws IllegalStateException If the file cannot be validated.
+     */
     public EnvConfig getEnvConfig(File envConfigFile) throws IOException, IllegalStateException {
         EnvConfig config = mapper.readValue(envConfigFile, EnvConfig.class);
         return validate(envConfigFile, config);
     }
 
+    /**
+     * Validates the environment configuration file.
+     *
+     * @param envConfigFile The file location of the config (used for setting
+     * working directory if not specified).
+     * @param config The environmental config that was deserialized.
+     * @return The updated config.
+     * @throws IllegalStateException If could not be validated.
+     */
     private EnvConfig validate(File envConfigFile, EnvConfig config) throws IllegalStateException {
         if (config == null || StringUtils.isBlank(config.getRootCaseOutputPath()) || StringUtils.isBlank(config.getRootTestOutputPath())) {
             throw new IllegalStateException("EnvConfig must have both the root case output path and the root test output path set.");
@@ -82,6 +120,16 @@ private EnvConfig validate(File envConfigFile, EnvConfig config) throws IllegalS
         return config;
     }
 
+    /**
+     * Derives a list of test suite config's specified in the configFile. The
+     * root directory is the same or an ancestor directory of this configFile
+     * used for the relative output path.
+     *
+     * @param rootDirectory The ancestor directory of configFile.
+     * @param configFile The configFile to read.
+     * @return The list of test suite configs found after invalid configs are
+     * filtered.
+     */
     public List<TestSuiteConfig> getTestSuiteConfig(File rootDirectory, File configFile) {
         try {
             JsonNode root = mapper.readTree(configFile);
@@ -97,13 +145,39 @@ public List<TestSuiteConfig> getTestSuiteConfig(File rootDirectory, File configF
         }
     }
 
-    public List<TestSuiteConfig> getTestSuiteConfigs(File rootDirectory) {
-        Collection<File> jsonFiles = FileUtils.listFiles(rootDirectory, new String[]{JSON_EXT}, true);
-        return jsonFiles.stream()
-                .flatMap((file) -> getTestSuiteConfig(rootDirectory, file).stream())
-                .collect(Collectors.toList());
+    /**
+     * Finds all test suite config json files within this directory and sub
+     * directories or if a file is specified, just that file is loaded.
+     *
+     * @param fileOrDirectory The parent directory of test suite configurations
+     * or the configuration file itself.
+     * @return The list of determined test suite configurations found.
+     */
+    public List<TestSuiteConfig> getTestSuiteConfigs(File fileOrDirectory) {
+        if (fileOrDirectory.isDirectory()) {
+            Collection<File> jsonFiles = FileUtils.listFiles(fileOrDirectory, new String[]{JSON_EXT}, true);
+            return jsonFiles.stream()
+                    .flatMap((file) -> getTestSuiteConfig(fileOrDirectory, file).stream())
+                    .collect(Collectors.toList());
+        } else if (fileOrDirectory.isFile()) {
+            return getTestSuiteConfig(fileOrDirectory, fileOrDirectory);
+        }
+
+        logger.log(Level.WARNING, "Unable to read file at: " + fileOrDirectory);
+        return Collections.emptyList();
     }
 
+    /**
+     * Validates the list of test suite configurations.
+     *
+     * @param rootDirectory The root directory for configurations
+     * (relativeOutputPath is set by determining relative path from root
+     * directory to file).
+     * @param file The file containing the configuration.
+     * @param testSuites The list of test suite objects discovered.
+     * @return The test suites with invalid items filtered and relative output
+     * path set.
+     */
     private List<TestSuiteConfig> validate(File rootDirectory, File file, List<TestSuiteConfig> testSuites) {
         return IntStream.range(0, testSuites.size())
                 .mapToObj(idx -> validate(rootDirectory, file, idx, testSuites.get(idx)))
@@ -111,6 +185,20 @@ private List<TestSuiteConfig> validate(File rootDirectory, File file, List<TestS
                 .collect(Collectors.toList());
     }
 
+    /**
+     * Validates a single test suite by returning it or null if invalid. The
+     * relative output path is also determined based on the relative path from
+     * rootDirectory to file.
+     *
+     * @param rootDirectory The root directory for configurations
+     * (relativeOutputPath is set by determining relative path from root
+     * directory to file).
+     * @param file The file containing the configuration.
+     * @param index The index within a list of test suites which this item
+     * exists.
+     * @param config The test suite confi.
+     * @return The test suite with relative output path set or null if invalid.
+     */
     private TestSuiteConfig validate(File rootDirectory, File file, int index, TestSuiteConfig config) {
         if (config == null
                 || StringUtils.isBlank(config.getName())
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ConnectionConfig.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ConnectionConfig.java
index 7c477711d88f9eea00e0e046fe00875768286a31..1d55fa4c7bf21cd1228a67239a7826b8a0669950 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ConnectionConfig.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ConnectionConfig.java
@@ -22,8 +22,7 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
- *
- * @author gregd
+ * Configuration information for a postgres connection.
  */
 public class ConnectionConfig {
     private final String hostName;
@@ -31,6 +30,13 @@ public class ConnectionConfig {
     private final String userName;
     private final String password;
 
+    /**
+     * Main constructor.
+     * @param hostName The host name.
+     * @param port The port to use.
+     * @param userName The user name to use.
+     * @param password The password to use.
+     */
     @JsonCreator
     public ConnectionConfig(
             @JsonProperty("hostName") String hostName, 
@@ -44,18 +50,30 @@ public ConnectionConfig(
         this.password = password;
     }
 
+    /**
+     * @return The host name.
+     */
     public String getHostName() {
         return hostName;
     }
 
+    /**
+     * @return The port.
+     */
     public Integer getPort() {
         return port;
     }
 
+    /**
+     * @return The user name.
+     */
     public String getUserName() {
         return userName;
     }
 
+    /**
+     * @return The password to use.
+     */
     public String getPassword() {
         return password;
     }
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/EnvConfig.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/EnvConfig.java
index 2432a09350e32444eb9416ef626320535c40ae61..ccfefd6ba5837aa503f6e381c203614b07ddc7f6 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/EnvConfig.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/EnvConfig.java
@@ -22,8 +22,7 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 /**
- *
- * @author gregd
+ * Defines integration testing environment settings.
  */
 public class EnvConfig {
 
@@ -34,18 +33,40 @@ public class EnvConfig {
     private final ConnectionConfig connectionInfo;
 
     private String workingDirectory;
+    private Boolean useRelativeOutput;
 
+    /**
+     * Main constructor.
+     *
+     * @param rootCaseOutputPath The location where cases will be created.
+     * @param rootTestSuitesPath The location of test suite configuration
+     * file(s).
+     * @param rootTestOutputPath The location where output results should be
+     * created.
+     * @param connectionInfo The connection info for postgres.
+     * @param workingDirectory The working directory (if not specified, the
+     * parent directory of the EnvConfig file is used.
+     * @param useRelativeOutput If true, results will be outputted maintaining
+     * 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/)
+     */
     @JsonCreator
     public EnvConfig(
             @JsonProperty("rootCaseOutputPath") String rootCaseOutputPath,
             @JsonProperty("rootTestSuitesPath") String rootTestSuitesPath,
             @JsonProperty("rootTestOutputPath") String rootTestOutputPath,
-            @JsonProperty("connectionInfo") ConnectionConfig connectionInfo) {
-        
+            @JsonProperty("connectionInfo") ConnectionConfig connectionInfo,
+            @JsonProperty("workingDirectory") String workingDirectory,
+            @JsonProperty("useRelativeOutput") Boolean useRelativeOutput) {
+
         this.rootCaseOutputPath = rootCaseOutputPath;
         this.rootTestOutputPath = rootTestOutputPath;
         this.rootTestSuitesPath = rootTestSuitesPath;
         this.connectionInfo = connectionInfo;
+
+        this.workingDirectory = workingDirectory;
+        this.useRelativeOutput = useRelativeOutput;
     }
 
     /**
@@ -82,11 +103,40 @@ public void setWorkingDirectory(String workingDirectory) {
         this.workingDirectory = workingDirectory;
     }
 
+    /**
+     * @return The postgres connection information.
+     */
     public ConnectionConfig getConnectionInfo() {
         return connectionInfo;
     }
 
+    /**
+     * @return The root test suites path that will be searched or the path to a
+     * single file.
+     */
     public String getRootTestSuitesPath() {
         return rootTestSuitesPath;
     }
+
+    /**
+     * @return If true, results will be outputted maintaining 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/)
+     */
+    public boolean getUseRelativeOutput() {
+        return Boolean.TRUE.equals(useRelativeOutput);
+    }
+
+    /**
+     * Sets whether or not to use the relative output path.
+     *
+     * @param useRelativeOutput If true, results will be outputted maintaining
+     * 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/)
+     */
+    public void setUseRelativeOutput(boolean useRelativeOutput) {
+        this.useRelativeOutput = useRelativeOutput;
+    }
 }
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/IntegrationTestConfig.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/IntegrationTestConfig.java
index c8bd0f6532538910649677ce6bca0fa30e612596..b481428cddff811a4463e0462cd42354c874d9e1 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/IntegrationTestConfig.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/IntegrationTestConfig.java
@@ -31,6 +31,11 @@ public class IntegrationTestConfig {
     private final List<TestSuiteConfig> testSuites;
     private final EnvConfig envConfig;
 
+    /**
+     * Main constructor.
+     * @param testSuites The test suites to be run.
+     * @param envConfig The environment configuration.
+     */
     @JsonCreator
     public IntegrationTestConfig(
             @JsonProperty("testSuites") List<TestSuiteConfig> testSuites,
@@ -41,12 +46,15 @@ public IntegrationTestConfig(
     }
 
     /**
-     * @return The per-case configuration.
+     * @return A list of test suite configurations.
      */
     public List<TestSuiteConfig> getTestSuites() {
         return testSuites;
     }
 
+    /**
+     * @return The integration test environment configuration.
+     */
     public EnvConfig getEnvConfig() {
         return envConfig;
     }
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ParameterizedResourceConfig.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ParameterizedResourceConfig.java
index 520b0771dd63ed0dd4b31f0bcf01fe3913b28417..cbbfad96f5b777d764e8461806b563fc3e1f8de4 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ParameterizedResourceConfig.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/ParameterizedResourceConfig.java
@@ -37,24 +37,37 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import org.sleuthkit.autopsy.integrationtesting.config.ParameterizedResourceConfig.ParameterizedResourceConfigDeserializer;
 
 /**
- *
- * @author gregd
+ * A resource that potentially has parameters as well.
  */
-@JsonDeserialize(using = ParameterizedResourceConfigDeserializer.class)
+@JsonDeserialize(using = ParameterizedResourceConfig.ParameterizedResourceConfigDeserializer.class)
 public class ParameterizedResourceConfig {
 
+    /**
+     * Deserializes from json. If a string is specified, that will be the
+     * resource, otherwise, an object of { resource: string, parameters, {...} }
+     * should be specified. The parameters can be expected to be a
+     * Map<String, Object>, containing nested Maps, List<Object>, or json
+     * primitives of type String, Integer, Long, Boolean, or Double.
+     */
     public static class ParameterizedResourceConfigDeserializer extends StdDeserializer<ParameterizedResourceConfig> {
 
         private static TypeReference<HashMap<String, Object>> typeRef = new TypeReference<HashMap<String, Object>>() {
         };
 
+        /**
+         * Main constructor.
+         */
         public ParameterizedResourceConfigDeserializer() {
             this(null);
         }
 
+        /**
+         * Main constructor specifying type.
+         *
+         * @param vc The type.
+         */
         public ParameterizedResourceConfigDeserializer(Class<?> vc) {
             super(vc);
         }
@@ -63,11 +76,14 @@ public ParameterizedResourceConfigDeserializer(Class<?> vc) {
         public ParameterizedResourceConfig deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
             JsonNode node = jp.getCodec().readTree(jp);
 
+            // if no node, return null.
             if (node == null) {
                 return null;
             } else if (node instanceof TextNode) {
+                // if just a string, return a ParameterizedResourceConfig where the resource is the string.
                 return new ParameterizedResourceConfig(((TextNode) node).textValue());
             } else {
+                // otherwise, determine the resource and create an object
                 JsonNode resourceNode = node.get("resource");
                 String resource = (resourceNode != null) ? resourceNode.asText() : null;
 
@@ -81,6 +97,12 @@ public ParameterizedResourceConfig deserialize(JsonParser jp, DeserializationCon
             }
         }
 
+        /**
+         * Reads an Object node into a Map<String, Object>.
+         *
+         * @param node The json node.
+         * @return The Map<String, Object>.
+         */
         Map<String, Object> readMap(ObjectNode node) {
             Map<String, Object> jsonObject = new LinkedHashMap<>();
             Iterator<Map.Entry<String, JsonNode>> keyValIter = node.fields();
@@ -91,6 +113,12 @@ Map<String, Object> readMap(ObjectNode node) {
             return jsonObject;
         }
 
+        /**
+         * Reads an Array node into a List<Object>.
+         *
+         * @param node The json array node.
+         * @return The list of objects.
+         */
         List<Object> readList(ArrayNode node) {
             List<Object> objArr = new ArrayList<>();
             for (JsonNode childNode : node) {
@@ -99,6 +127,13 @@ List<Object> readList(ArrayNode node) {
             return objArr;
         }
 
+        /**
+         * Reads a Json value node into an Object (text, boolean, long, int,
+         * double).
+         *
+         * @param vNode The value node.
+         * @return The created object.
+         */
         Object readJsonPrimitive(ValueNode vNode) {
             if (vNode.isTextual()) {
                 return vNode.asText();
@@ -115,6 +150,12 @@ Object readJsonPrimitive(ValueNode vNode) {
             return null;
         }
 
+        /**
+         * Reads a json node of unknown type into a java object.
+         *
+         * @param node The json node.
+         * @return The object.
+         */
         Object readItem(JsonNode node) {
             if (node == null) {
                 return null;
@@ -135,19 +176,38 @@ Object readItem(JsonNode node) {
     private final String resource;
     private final Map<String, Object> parameters;
 
+    /**
+     * Main constructor.
+     *
+     * @param resource The resource name.
+     * @param parameters The parameters to be specified.
+     */
     public ParameterizedResourceConfig(String resource, Map<String, Object> parameters) {
         this.resource = resource;
         this.parameters = (parameters == null) ? Collections.emptyMap() : parameters;
     }
 
+    /**
+     * Main constructor where parameters are null.
+     *
+     * @param resource The resource.
+     */
     public ParameterizedResourceConfig(String resource) {
         this(resource, null);
     }
 
+    /**
+     * @return The resource identifier.
+     */
     public String getResource() {
         return resource;
     }
 
+    /**
+     * @return Parameters provided for the resource. Nested objects will be
+     * Map<String, Object>, List<Object> or a json primitive like boolean, int,
+     * long, double, string.
+     */
     public Map<String, Object> getParameters() {
         return parameters;
     }
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/TestSuiteConfig.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/TestSuiteConfig.java
index 22ab1e68ebc599d9fbed857692db4066e6ea51c8..51132c9feb0c6c776119fd87760798b37f9e149c 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/TestSuiteConfig.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/TestSuiteConfig.java
@@ -23,7 +23,7 @@
 import java.util.List;
 
 /**
- * Configuration in IntegrationTests per case.
+ * Configuration per test suite.
  */
 public class TestSuiteConfig {
 
@@ -35,6 +35,17 @@ public class TestSuiteConfig {
     private final IntegrationCaseType caseTypes;
     private String relativeOutputPath;
 
+    /**
+     * Main constructor.
+     *
+     * @param name Name of the test suite.
+     * @param description The description for the test suite.
+     * @param dataSources The data sources to use.
+     * @param configurationModules The modules in order to configure the autopsy
+     * environment.
+     * @param integrationTests The integration tests to run.
+     * @param caseTypes The case types (single user, multi user, both).
+     */
     @JsonCreator
     public TestSuiteConfig(
             @JsonProperty("name") String name,
@@ -52,34 +63,68 @@ public TestSuiteConfig(
         this.caseTypes = caseTypes;
     }
 
+    /**
+     * @return The name of the test suite.
+     */
     public String getName() {
         return name;
     }
 
+    /**
+     * @return The description of the test suite.
+     */
     public String getDescription() {
         return description;
     }
 
+    /**
+     * @return The data sources to be ingested.
+     */
     public List<String> getDataSources() {
         return dataSources;
     }
 
+    /**
+     * @return The configuration modules to be run to set up the autopsy
+     * environment.
+     */
     public List<ParameterizedResourceConfig> getConfigurationModules() {
         return configurationModules;
     }
 
+    /**
+     * @return The configuration for integration tests to run for output.
+     */
     public TestingConfig getIntegrationTests() {
         return integrationTests;
     }
 
+    /**
+     * @return The case type (single user, multi user, both).
+     */
     public IntegrationCaseType getCaseTypes() {
         return caseTypes;
     }
 
+    /**
+     * @return The relative output path used if EnvConfig.useRelativeOutputPath
+     * is true. If file is found at /testSuitePath/relPathX/fileY.json, and
+     * EnvConfig.useRelativeOutputPath is true, then output results will be
+     * located in /outputPath/relPathX/fileY/.
+     */
     public String getRelativeOutputPath() {
         return relativeOutputPath;
     }
 
+    /**
+     * Sets the relative output path.
+     *
+     * @param relativeOutputPath The relative output path used if
+     * EnvConfig.useRelativeOutputPath is true. If file is found at
+     * /testSuitePath/relPathX/fileY.json, and EnvConfig.useRelativeOutputPath
+     * is true, then output results will be located in
+     * /outputPath/relPathX/fileY/.
+     */
     public void setRelativeOutputPath(String relativeOutputPath) {
         this.relativeOutputPath = relativeOutputPath;
     }
diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/TestingConfig.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/TestingConfig.java
index a2a9b165adc7cf73521321e39ab2e86ff69618b1..b42416bf382f8a66fa2bed7a9634d4486ae37483 100644
--- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/TestingConfig.java
+++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/integrationtesting/config/TestingConfig.java
@@ -38,16 +38,23 @@ public class TestingConfig {
     private final Map<String, ParameterizedResourceConfig> excludeAllExcept;
     private final Set<String> includeAllExcept;
 
+    /**
+     * Main constructor for Integration tests to be run.
+     *
+     * @param excludeAllExcept Items that should be run to the exclusion of all
+     * others.
+     * @param includeAllExcept Items that should only be run.
+     */
     @JsonCreator
     public TestingConfig(
-            @JsonProperty("excludeAllExcept") List<ParameterizedResourceConfig> excludeAllExcept, 
+            @JsonProperty("excludeAllExcept") List<ParameterizedResourceConfig> excludeAllExcept,
             @JsonProperty("includeAllExcept") List<String> includeAllExcept) {
 
         List<ParameterizedResourceConfig> safeExcludeAllExcept = ((excludeAllExcept == null) ? Collections.emptyList() : excludeAllExcept);
         this.excludeAllExcept = safeExcludeAllExcept
                 .stream()
                 .collect(Collectors.toMap(
-                        (res) ->  res.getResource() == null ? "" : res.getResource().toUpperCase(),
+                        (res) -> res.getResource() == null ? "" : res.getResource().toUpperCase(),
                         (res) -> res,
                         (res1, res2) -> {
                             Map<String, Object> mergedArgs = new HashMap<>();
@@ -55,7 +62,7 @@ public TestingConfig(
                             mergedArgs.putAll(res2.getParameters());
                             return new ParameterizedResourceConfig(res1.getResource(), mergedArgs);
                         })
-        );
+                );
 
         List<String> safeIncludeAllExcept = ((includeAllExcept == null) ? Collections.emptyList() : includeAllExcept);
         this.includeAllExcept = safeIncludeAllExcept
@@ -80,7 +87,7 @@ public Set<ParameterizedResourceConfig> getExcludeAllExcept() {
     public Set<String> getIncludeAllExcept() {
         return includeAllExcept;
     }
-    
+
     public Map<String, Object> getParameters(String itemType) {
         ParameterizedResourceConfig resource = (itemType == null) ? null : excludeAllExcept.get(itemType.toUpperCase());
         return resource == null ? Collections.emptyMap() : new HashMap<String, Object>(resource.getParameters());