Files
ghidra/gradle/support/testUtils.gradle
T

554 lines
22 KiB
Groovy

import java.util.regex.*;
import groovy.io.FileType;
import java.lang.reflect.Constructor;
import java.lang.*;
import java.io.*;
// This is a map of configuration type names (integration vs. non-integration vs. docking etc..)
// to tests (test name, duration)
ext.testReport = null;
/*
* Checks if html test report for an individual test class has a valid name.
*/
boolean hasValidTestReportClassName(String name) {
return name.endsWith("Test.html") && !name.contains("Suite")
}
/*
* Returns duration for a test class report.
*/
long getDurationFromTestReportClass(String fileContents, String fileName) {
/* The duration for the entire test class appears in the test report as (multiline):
* <div class="infoBox" id="duration">
* <div class="counter">0s</div>
* The duration value may appear in the format of: 1m2s, 1m2.3s, 3.4s
*/
Pattern p = Pattern.compile("(?<=id=\"duration\">[\r\n]<div\\sclass=\"counter\">)(.*?)(?=</div)",
Pattern.MULTILINE);
Matcher m = p.matcher(fileContents);
assert m.find() == true
String duration = m.group()
assert duration != null && duration.trim().length() > 0
long durationInMillis
// Parse out the duration
if (duration.contains("m") && duration.contains("s")) { // has minute and seconds
int minutes = Integer.parseInt(duration.substring(0, duration.indexOf("m")))
double seconds = Double.parseDouble(duration.substring(duration.indexOf("m") + 1
, duration.length()-1))
durationInMillis = (minutes * 60 * 1000) + (seconds * 1000)
} else if (!duration.contains("m") && duration.contains("s")) { // has only seconds
double seconds = Double.parseDouble(duration.substring(0, duration.length()-1))
durationInMillis = (seconds * 1000)
} else { // unknown format
assert false : "getDurationFromTestReportClass: Unknown duration format in $fileName. 'duration' value is $duration"
}
logger.debug("getDurationFromTestReportClass: durationInMillis = '"+ durationInMillis
+"' parsed from duration = '" + duration + "' in $fileName")
return durationInMillis
}
/*
* Creates <fully qualified classname, duration> from JUnit test report
*/
def Map<String, Map<String, Long>> getTestReport() {
// populate testReport only once per gradle configuration phase
if (project.testReport == null) {
logger.debug("getTestReport: Populating 'testReport' using '$testTimeParserInputDir'")
testReport = new HashMap<String,Map>();
Map dockingConfigurationBucket = new HashMap<String, Long>();
Map integrationConfigurationBucket = new HashMap<String, Long>();
Map appConfigurationBucket = new HashMap<String, Long>();
Map ghidraConfigurationBucket = new HashMap<String, Long>();
Map unknownConfigurationBucket = new HashMap<String, Long>();
File classesReportDir = new File(testTimeParserInputDir)
if(!classesReportDir.exists()) {
logger.info("getTestReport: The path '$testTimeParserInputDir' does not exist on the file system." +
" Returning empty testReport map.")
return Collections.emptyList();
}
int excludedHtmlFiles = 0 // counter
int totalHtmlFiles = 0
String excludedHtmlFileNames = "" // for log.info summary message
classesReportDir.eachFileRecurse (FileType.FILES) { file ->
totalHtmlFiles++
// Only read html file for a Test and not a test Suite
if(hasValidTestReportClassName(file.name)) {
String fileContents = file.text
/* The fully qualified class name appears in the test report as:
* <h1>Class ghidra.app.plugin.assembler.sleigh.BuilderTest</h1>
*/
String fqNameFromTestReport = fileContents.find("(?<=<h1>Class\\s).*?(?=</h1>)")
int nameIndex = fqNameFromTestReport.lastIndexOf('.')
String shortName = fqNameFromTestReport.substring(nameIndex+1);
long durationInMillis = getDurationFromTestReportClass(fileContents, file.name)
File rootDir = project.rootDir.getParentFile();
File foundFile;
fileTree(rootDir.getAbsolutePath()).visit { FileVisitDetails details ->
if (details.getName().contains(shortName + ".java")) {
foundFile = details.getFile();
}
}
if (!foundFile.exists()) {
// throw error
}
String javaFileContents = foundFile.text;
List<String> integrationConfigs = new ArrayList<>();
List<String> dockingConfigs = new ArrayList<>();
List<String> appConfigs = new ArrayList<>();
List<String> ghidraConfigs = new ArrayList<>();
// Creates HeadlessGhidraApplicationConfig
integrationConfigs.add("AbstractGhidraHeadlessIntegrationTest");
integrationConfigs.add("ProcessorEmulatorTestAdapter");
integrationConfigs.add("SaveAsOutboundTest");
// Extenders
integrationConfigs.add("AbstractDbgTest");
integrationConfigs.add("AbstractGProtocolTest");
integrationConfigs.add("GProtocolClientServerTestBase");
integrationConfigs.add("AbstractHeadlessAnalyzerTest");
integrationConfigs.add("SuperListingTest");
integrationConfigs.add("SuperProgramTest");
integrationConfigs.add("EmulatorTest");
//---------------------
// Creates DockingApplicationConfiguration
dockingConfigs.add("AbstractDockingTest");
// Extenders
dockingConfigs.add("AbstractCRIntegrationTest");
dockingConfigs.add("AbstractDropDownTextFieldTest<T>");
dockingConfigs.add("AbstractFcgTest");
dockingConfigs.add("AbstractTaskTest");
dockingConfigs.add("AbstractThreadedTableTest");
dockingConfigs.add("DefaultThreadedTableFilterTest");
dockingConfigs.add("ThreadedTableTest");
dockingConfigs.add("AbstractVisualGraphTest");
dockingConfigs.add("AbstractSimpleVisualGraphTest");
//---------------------
// Creates ApplicationConfiguration
appConfigs.add("AbstractGenericTest");
appConfigs.add("SolverTest");
appConfigs.add("TestClassFileCreator");
// Extenders
appConfigs.add("AbstractAssemblyTest");
appConfigs.add("AbstractChainedBufferTest");
appConfigs.add("AbstractCreateDataTypeModelTest");
appConfigs.add("AbstractGraphAlgorithmsTest");
appConfigs.add("AbstractHelpModuleLocationTest");
appConfigs.add("AbstractHelpTest");
appConfigs.add("AbstractLocalFileSystemTest");
appConfigs.add("AbstractOpBehaviorTest");
appConfigs.add("MDMangBaseTest");
appConfigs.add("StorageEditorModelTest");
appConfigs.add("VTBaseTestCase");
appConfigs.add("AbstractEHTest");
appConfigs.add("AbstractRttiTest");
//---------------------
// Creates GhidraAppConfig
ghidraConfigs.add("DataTypeSelectionTextFieldTest");
ghidraConfigs.add("AbstractGhidraHeadedIntegrationTest");
// Extenders
ghidraConfigs.add("AbstractAddressMapDBTestClass");
ghidraConfigs.add("AbstractCodeBrowserNavigationTest");
ghidraConfigs.add("AbstractCorrelatorTest");
ghidraConfigs.add("AbstractCreateArchiveTest");
ghidraConfigs.add("AbstractDataActionTest");
ghidraConfigs.add("AbstractEditorTest");
ghidraConfigs.add("AbstractFileFormatsTest");
ghidraConfigs.add("AbstractFollowFlowTest");
ghidraConfigs.add("AbstractFunctionGraphTest");
ghidraConfigs.add("AbstractFunctionSignatureMarkupTest");
ghidraConfigs.add("AbstractGFileSystemBaseTest");
ghidraConfigs.add("AbstractGhidraScriptMgrPluginTest");
ghidraConfigs.add("AbstractMergeTest");
ghidraConfigs.add("AbstractProgramBasedTest");
ghidraConfigs.add("AbstractProgramDiffTest");
ghidraConfigs.add("AbstractProgramTreePluginTest");
ghidraConfigs.add("AbstractScreenShotGenerator");
ghidraConfigs.add("AbstractSelectByFlowPluginTest");
ghidraConfigs.add("AbstractSymbolTreePluginExternalsTest");
ghidraConfigs.add("AbstractToolSavingTest");
ghidraConfigs.add("AbstractVersionControlActionTest");
ghidraConfigs.add("AbstractVTCorrelatorTest");
ghidraConfigs.add("AbstractVTMarkupItemTest");
ghidraConfigs.add("DiffTestAdapter");
ghidraConfigs.add("DWARFTestBase");
ghidraConfigs.add("AbstractSelfSimilarCorrelatorTest");
ghidraConfigs.add("AbstractStackEditorTest");
ghidraConfigs.add("AbstractStructureEditorTest");
ghidraConfigs.add("AbstractUnionEditorTest");
ghidraConfigs.add("AbstractStackEditorProviderTest");
ghidraConfigs.add("AbstractStructureEditorLockedActionsTest");
ghidraConfigs.add("AbstractStructureEditorUnlockedActionsTest");
ghidraConfigs.add("AbstractStructureEditorUnlockedCellEditTest");
ghidraConfigs.add("AbstractDataTypeMergeTest");
ghidraConfigs.add("AbstractProgramTreeMergeManagerTest");
ghidraConfigs.add("AbstractListingMergeManagerTest");
ghidraConfigs.add("AbstractExternalMergerTest");
ghidraConfigs.add("AbstractDecompilerTest");
ghidraConfigs.add("AbstractEquatePluginTest");
ghidraConfigs.add("AbstractLocationReferencesTest");
ghidraConfigs.add("AbstractMemSearchTest");
ghidraConfigs.add("AbstractDecompilerFindReferencesActionTest");
ghidraConfigs.add("GhidraScreenShotGenerator");
ghidraConfigs.add("TutorialScreenShotGenerator");
ghidraConfigs.add("AbstractSearchScreenShots");
ghidraConfigs.add("DiffApplyTestAdapter");
//-------------------------------------------------
if (javaFileContents.contains(shortName)) {
// Match the word right after "extends", if there is one
Pattern p = Pattern.compile("extends\\W+(\\w+)");
Matcher m = p.matcher(javaFileContents);
String extendsClass = "";
while (m.find()) {
extendsClass = m.group(1);
break;
}
if (extendsClass.isEmpty()) {
unknownConfigurationBucket.put(fqNameFromTestReport, durationInMillis);
}
else {
if (integrationConfigs.contains(extendsClass)) {
integrationConfigurationBucket.put(fqNameFromTestReport, durationInMillis);
}
else if (dockingConfigs.contains(extendsClass)) {
dockingConfigurationBucket.put(fqNameFromTestReport, durationInMillis);
}
else if (appConfigs.contains(extendsClass)) {
appConfigurationBucket.put(fqNameFromTestReport, durationInMillis);
}
else if (ghidraConfigs.contains(extendsClass)) {
ghidraConfigurationBucket.put(fqNameFromTestReport, durationInMillis);
}
else {
unknownConfigurationBucket.put(fqNameFromTestReport, durationInMillis);
}
}
}
testReport.put("integration", integrationConfigurationBucket);
testReport.put("docking", dockingConfigurationBucket);
testReport.put("app", appConfigurationBucket);
testReport.put("ghidra", ghidraConfigurationBucket);
testReport.put("unknown", unknownConfigurationBucket);
// END TEST
logger.debug("getTestReport: Added to testReport: class name = '"
+ fqNameFromTestReport + "' and durationInMillis = '"+ durationInMillis
+"' from " + file.name)
} else {
logger.debug("getTestReport: Excluding " + file.name + " from test report parsing.")
excludedHtmlFileNames += file.name + ", "
excludedHtmlFiles++
}
}
assert totalHtmlFiles != 0 : "getTestReport: Did not parse any valid html files in $testTimeParserInputDir. Directory might be empty"
assert totalHtmlFiles == (testReport.size() + excludedHtmlFiles) : "Not all html files processed."
logger.info("getTestReport:\n" +
"\tIncluded " + testReport.size() + " and excluded " + excludedHtmlFiles
+ " html files out of " + totalHtmlFiles + " in Junit test report.\n"
+ "\tExcluded html file names are: " + excludedHtmlFileNames + "\n"
+ "\tParsed test report located at " + testTimeParserInputDir)
}
return project.testReport
}
/*
* Checks if Java test class has a valid name.
*/
boolean hasValidTestClassName(String name) {
return name != null && name.endsWith("Test.java") &&
!(name.contains("Abstract") || name.contains("Suite"))
}
/*
* Checks if Java test class is excluded via 'org.junit.experimental.categories.Category'
*/
boolean hasCategoryExcludes(String fileContents) {
String annotation1 = "@Category\\(PortSensitiveCategory.class\\)" // evaluated as regex
String annotation2 = "@Category\\(NightlyCategory.class\\)"
return fileContents.find(annotation1) || fileContents.find(annotation2)
}
/*
* Returns a fully qualified class name from a java class.
*/
String constructFullyQualifiedClassName(String fileContents, String fileName) {
String packageName = fileContents.find("(?<=package\\s).*?(?=;)")
logger.debug("constructFullyQualifiedClassName: Found '" + packageName + "' in " + fileName)
assert packageName != null : "constructFullyQualifiedClassName: Null packageName found in $fileName"
assert !packageName.startsWith("package")
assert !packageName.endsWith(";")
return packageName + "." + fileName.replace(".java","")
}
/* Creates a list of test classes, sorted by duration, for a subproject.
* First parses JUnit test report located at 'testTimeParserInputDir' for <fully qualified class name, duration in milliseconds> .
* Then traverses a test sourceSet for a subproject for a test to include and assigns a duration value.
* Returns a sorted list of test classes for the sourceSet parameter.
*/
def Map<String, Map> getTestsForSubProject(SourceDirectorySet sourceDirectorySet) {
assert (getTestReport() != null) : "getTestsForSubProject: testReport should not be null"
def testsForSubProject = new HashMap<String,LinkedHashMap>();
int includedClassFilesNotInTestReport = 0 // class in sourceSet but not in test report, 'bumped' to first task
int includedClassFilesInTestReport = 0 // class in sourceSet and in test report
int excludedClassFilesBadName = 0 // excluded class in sourceSet with invalid name
int excludedClassFilesCategory = 0 // excluded class in sourceSet with @Category annotation
int excludedClassAllTestsIgnored = 0 // excluded class in sourceSet with test report duration of 0
logger.debug("getTestsForSubProject: Found " + sourceDirectorySet.files.size()
+ " file(s) in source set to process.")
Map<String,Map> testReports = getTestReport();
for (File file : sourceDirectorySet.getFiles()) {
logger.debug("getTestsForSubProject: Found file in sourceSet = " + file.name)
// Must have a valid class name
if(!hasValidTestClassName(file.name)) {
logger.debug("getTestsForSubProject: Excluding file '" + file.name + "' based on name.")
excludedClassFilesBadName++
continue
}
String fileContents = file.text
// Must not have a Category annotation
if (hasCategoryExcludes(fileContents)) {
logger.debug("getTestsForSubProject: Found category exclude for '"
+ file.name + "'. Excluding this class from running.")
excludedClassFilesCategory++
continue
}
String fqName = constructFullyQualifiedClassName( fileContents, file.name)
boolean foundTest = false;
for (Map.Entry<String,Map> entry : testReports.entrySet()) {
String configName = entry.getKey();
Map<String,Long> tests = entry.getValue();
if (tests.containsKey(fqName)) {
foundTest = true;
if (!testsForSubProject.containsKey(configName)) {
Map<String,Map> configToTestMap = new LinkedHashMap<>();
testsForSubProject.put(configName, configToTestMap);
}
Map<String,Long> subTests = testsForSubProject.get(configName);
long duration = tests.get(fqName);
if (duration > 0) {
subTests.put(fqName,duration);
logger.debug("getTestsForSubProject: Adding '" + fqName + "'")
includedClassFilesInTestReport++
}
else {
logger.debug("getTestsForSubProject: Excluding '" + fqName
+ "' because duration from test report is " + duration
+ "ms. Probably because all test methods are @Ignore'd." )
excludedClassAllTestsIgnored++
}
}
}
if (!foundTest) {
// Don't know what this test is so put it in the "unknown" bucket
if (!testsForSubProject.containsKey("unknown")) {
Map<String,Map> configToTestMap = new LinkedHashMap<>();
testsForSubProject.put("unknown", configToTestMap);
}
Map<String,Long> subTests = testsForSubProject.get("unknown");
logger.debug("getTestsForSubProject: Found test class not in test report."
+ " Bumping to front of tasks '" + fqName + "'")
subTests.put(fqName, 3600000) // cheap way to bump to front of (eventually) sorted list
includedClassFilesNotInTestReport++
}
}
// Sort by duration
for (Map.Entry<String,Map> entry : testsForSubProject.entrySet()) {
Map<String,Long> testMap = entry.getValue();
testMap.sort { a, b -> b.value <=> a.value }
}
logger.info ("getTestsForSubProject:\n"
+ "\tIncluding " + includedClassFilesInTestReport + " test classes for this sourceSet because they are in the test report.\n"
+ "\tIncluding/bumping " + includedClassFilesNotInTestReport + " not in test report.\n"
+ "\tExcluding "+ excludedClassFilesBadName +" based on name not ending in 'Test' or contains 'Abstract' or 'Suite', " + excludedClassFilesCategory
+ " based on '@Category, " + excludedClassAllTestsIgnored + " because duration = 0ms.\n"
+ "\tReturning sorted list of size "+ sorted.size() + " out of " + sourceDirectorySet.files.size()
+ " total files found in sourceSet.")
int filesProcessed = includedClassFilesNotInTestReport + includedClassFilesInTestReport +
excludedClassFilesBadName + excludedClassFilesCategory + excludedClassAllTestsIgnored
assert sourceDirectorySet.files.size() == filesProcessed : "getTestsForSubProject did not process every file in sourceSet"
return testsForSubProject;
}
/*********************************************************************************
* Determines if test task creation should be skipped for parallelCombinedTestReport task.
*********************************************************************************/
def boolean shouldSkipTestTaskCreation(Project subproject) {
if (!parallelMode) {
logger.debug("shouldSkipTestTaskCreation: Skip task creation for $subproject.name. Not in parallel mode.")
return true
}
if (!subproject.hasProperty("sourceSets")) {
logger.debug("shouldSkipTestTaskCreation: subproject $subproject.name has no sourceSet property.")
return true
}
if (subproject.sourceSets.findByName("test") == null ||
subproject.sourceSets.test.java.files.isEmpty()) {
logger.debug("shouldSkipTestTaskCreation: Skip task creation for $subproject.name. No test sources.")
return true
}
if (subproject.findProperty("excludeFromParallelTests") ?: false) {
logger.debug("shouldSkipTestTaskCreation: Skip task creation for $subproject.name."
+ " 'excludeFromParallelTests' found.")
return true
}
if (subproject.findProperty("repoToTest")) {
subproject.ext.repoToTest = subproject.getProperty('repoToTest')
}
else {
subproject.ext.repoToTest = "ALL_REPOS"
}
if (!subproject.ext.repoToTest.equals("ALL_REPOS") && !subproject.getProjectDir().toString().contains("/" + repoToTest + "/")) {
return true
}
return false
}
/*********************************************************************************
* Determines if integrationTest task creation should be skipped for parallelCombinedTestReport task.
*********************************************************************************/
def boolean shouldSkipIntegrationTestTaskCreation(Project subproject) {
if (!parallelMode) {
logger.debug("shouldSkipIntegrationTestTaskCreation: Skip task creation for $subproject.name."
+ " Not in parallel mode.")
return true
}
if (!subproject.hasProperty("sourceSets")) {
logger.debug("shouldSkipIntegrationTestTaskCreation: subproject $subproject.name has no sourceSet property.")
return true
}
if (subproject.sourceSets.findByName("integrationTest") == null ||
subproject.sourceSets.integrationTest.java.files.isEmpty()) {
logger.debug("shouldSkipIntegrationTestTaskCreation: Skip task creation for $subproject.name."
+ " No integrationTest sources.")
return true
}
if (subproject.findProperty("excludeFromParallelIntegrationTests") ?: false) {
logger.debug("shouldSkipIntegrationTestTaskCreation: Skip task creation for $subproject.name."
+ "'excludeFromParallelIntegrationTests' found.")
return true
}
if (subproject.findProperty("repoToTest")) {
subproject.ext.repoToTest = subproject.getProperty('repoToTest')
}
else {
subproject.ext.repoToTest = "ALL_REPOS"
}
if (!subproject.ext.repoToTest.equals("ALL_REPOS") && !subproject.getProjectDir().toString().contains("/" + repoToTest + "/")) {
return true
}
return false
}
/*********************************************************************************
* Gets the path to the last archived test report. This is used by the
* 'parallelCombinedTestReport' task when no -PtestTimeParserInputDir is supplied
* via cmd line.
*********************************************************************************/
def String getLastArchivedReport(String reportArchivesPath) {
// skip configuration for this property if not in parallelMode
if (!parallelMode) {
logger.debug("getLastArchivedReport: not in 'parallelMode'. Skipping.")
return ""
}
File reportArchiveDir = new File(reportArchivesPath);
logger.info("getLastArchivedReport: searching for test report to parse in " + reportArchivesPath)
if(!reportArchiveDir.exists()) {
logger.info("getLastArchivedReport: '$reportArchiveDir' does not exist.")
return ""
}
// filter for report archive directories.
File[] files = reportArchiveDir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.startsWith("reports_");
}
});
assert (files != null && files.size() > 0) :
"""Could not find test report archives in '$reportArchiveDir'.
because no -PtestTimeParserInputDir=<path/to/report> supplied via cmd line"""
logger.debug("getLastArchivedReport: found " + files.size() + " archived report directories in '"
+ reportArchiveDir.getPath() + "'.")
// Sort by lastModified date. The last modified directory will be first.
files = files.sort{-it.lastModified()}
logger.debug("getLastArchivedReport: selecting report archive to parse: " + files[0].getAbsolutePath())
return files[0].getAbsolutePath()
}
ext {
getTestsForSubProject = this.&getTestsForSubProject // export this utility method to project
shouldSkipTestTaskCreation = this.&shouldSkipTestTaskCreation
shouldSkipIntegrationTestTaskCreation = this.&shouldSkipIntegrationTestTaskCreation
getLastArchivedReport = this.&getLastArchivedReport
}