diff --git a/gradle/root/test.gradle b/gradle/root/test.gradle index 8545c4ba8f..837b8cb230 100644 --- a/gradle/root/test.gradle +++ b/gradle/root/test.gradle @@ -418,36 +418,52 @@ configure(subprojects.findAll {parallelMode == true}) { subproject -> if (!shouldSkipTestTaskCreation(subproject)) { logger.info("parallelCombinedTestReport: Creating 'test' tasks for " + subproject.name + " subproject.") - ArrayList classesList = getTestsForSubProject(subproject.sourceSets.test.java) - int classesListPosition = 0 // current position in classesList - int taskNameCounter = 1 // task suffix - int numMaxParallelForks = 40 // unit tests are fast; 40 seems to be a reasonable number + Map testMap = getTestsForSubProject(subproject.sourceSets.test.java) - while (classesListPosition < classesList.size()) { - createTestTask(subproject, "test", taskNameCounter, classesList, classesListPosition, numMaxParallelForks) - classesListPosition+=numMaxParallelForks - taskNameCounter+=1; // "test_1", "test_2, etc. + for (Map.Entry classMap : testMap.entrySet()) { + + int classesListPosition = 0 // current position in classesList + int taskNameCounter = 1 // task suffix + int numMaxParallelForks = 40 // unit tests are fast; 40 seems to be a reasonable number + + Map tests = classMap.getValue(); + def sorted = tests.sort { a, b -> b.value <=> a.value }; + List classesList = new ArrayList(sorted.keySet()); + + while (classesListPosition < classesList.size()) { + createTestTask(subproject, "test", taskNameCounter, classesList, classesListPosition, numMaxParallelForks) + classesListPosition+=numMaxParallelForks + taskNameCounter+=1; // "test_1", "test_2, etc. + } } } - + if (!shouldSkipIntegrationTestTaskCreation(subproject)) { logger.info("parallelCombinedTestReport: Creating 'integrationTest' tasks for " + subproject.name + " subproject.") - ArrayList classesList = getTestsForSubProject(subproject.sourceSets.integrationTest.java) - int classesListPosition = 0 // current position in classesList - int taskNameCounter = 1 // task suffix - - // Through trial-and-error we found that 40 is too many - // concurrent integration tests (ghidratest server has 40 CPUs). - // 20 seems like a good balance of throughput vs resource usage for ghidratest server. - int numMaxParallelForks = 20 - - while (classesListPosition < classesList.size()) { - createTestTask(subproject, "integrationTest", taskNameCounter, classesList, classesListPosition, numMaxParallelForks) - classesListPosition+=numMaxParallelForks - taskNameCounter+=1; // "integrationTest_1", "integrationTest_2, etc. + Map testMap = getTestsForSubProject(subproject.sourceSets.integrationTest.java) + + for (Map.Entry classMap : testMap.entrySet()) { + + int classesListPosition = 0 // current position in classesList + int taskNameCounter = 1 // task suffix + + // Through trial-and-error we found that 40 is too many + // concurrent integration tests (ghidratest server has 40 CPUs). + // 20 seems like a good balance of throughput vs resource usage for ghidratest server. + int numMaxParallelForks = 20 + + Map tests = classMap.getValue(); + def sorted = tests.sort { a, b -> b.value <=> a.value }; + List classesList = new ArrayList(sorted.keySet()); + + while (classesListPosition < classesList.size()) { + createTestTask(subproject, "integrationTest", taskNameCounter, classesList, classesListPosition, numMaxParallelForks) + classesListPosition+=numMaxParallelForks + taskNameCounter+=1; // "integrationTest_1", "integrationTest_2, etc. + } } - } + } } // end afterEvaluate }// end subprojects diff --git a/gradle/support/testUtils.gradle b/gradle/support/testUtils.gradle index 05acab2ed3..c6da11b762 100644 --- a/gradle/support/testUtils.gradle +++ b/gradle/support/testUtils.gradle @@ -1,7 +1,12 @@ import java.util.regex.*; import groovy.io.FileType; +import java.lang.reflect.Constructor; +import java.lang.*; +import java.io.*; -ext.testReport = null; // contains from JUnit test report +// 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. @@ -48,25 +53,31 @@ long getDurationFromTestReportClass(String fileContents, String fileName) { /* * Creates from JUnit test report */ -def HashMap getTestReport() { +def Map> getTestReport() { // populate testReport only once per gradle configuration phase if (project.testReport == null) { + logger.debug("getTestReport: Populating 'testReport' using '$testTimeParserInputDir'") - testReport = new HashMap(); + testReport = new HashMap(); + Map dockingConfigurationBucket = new HashMap(); + Map headlessConfigurationBucket = new HashMap(); + Map appConfigurationBucket = new HashMap(); + Map ghidraConfigurationBucket = new HashMap(); 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 project.testReport + 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)) { @@ -76,10 +87,42 @@ def HashMap getTestReport() { *

Class ghidra.app.plugin.assembler.sleigh.BuilderTest

*/ String fqNameFromTestReport = fileContents.find("(?<=

Class\\s).*?(?=

)") + 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; + + if (javaFileContents.contains(shortName) && javaFileContents.contains(" extends ")) { + if (javaFileContents.contains("AbstractGhidraHeadlessIntegrationTest")) { + headlessConfigurationBucket.put(fqNameFromTestReport, durationInMillis); + } + else if (javaFileContents.contains("AbstractDockingTest")) { + dockingConfigurationBucket.put(fqNameFromTestReport, durationInMillis); + } + } + + testReport.put("headless", headlessConfigurationBucket); + testReport.put("docking", dockingConfigurationBucket); + testReport.put("app", appConfigurationBucket); + testReport.put("ghidra", ghidraConfigurationBucket); + + // END TEST - testReport.put(fqNameFromTestReport, durationInMillis) logger.debug("getTestReport: Added to testReport: class name = '" + fqNameFromTestReport + "' and durationInMillis = '"+ durationInMillis +"' from " + file.name) @@ -100,6 +143,7 @@ def HashMap getTestReport() { + "\tParsed test report located at " + testTimeParserInputDir) } + return project.testReport } @@ -141,11 +185,11 @@ String constructFullyQualifiedClassName(String fileContents, String fileName) { * 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 ArrayList getTestsForSubProject(SourceDirectorySet sourceDirectorySet) { +def Map getTestsForSubProject(SourceDirectorySet sourceDirectorySet) { assert (getTestReport() != null) : "getTestsForSubProject: testReport should not be null" - def testsForSubProject = new LinkedHashMap<>(); + def testsForSubProject = new HashMap(); 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 @@ -155,6 +199,8 @@ def ArrayList getTestsForSubProject(SourceDirectorySet sourceDirectorySet) { logger.debug("getTestsForSubProject: Found " + sourceDirectorySet.files.size() + " file(s) in source set to process.") + + Map testReports = getTestReport(); for (File file : sourceDirectorySet.getFiles()) { logger.debug("getTestsForSubProject: Found file in sourceSet = " + file.name) @@ -177,31 +223,57 @@ def ArrayList getTestsForSubProject(SourceDirectorySet sourceDirectorySet) { } String fqName = constructFullyQualifiedClassName( fileContents, file.name) - // Lookup the test duration - if (getTestReport().containsKey(fqName)) { - long duration = getTestReport().get(fqName) - - // Some classes from test report have duration value of 0. Exclude these from running. - if (duration > 0) { - testsForSubProject.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++ + + boolean foundTest = false; + for (Map.Entry entry : testReports.entrySet()) { + String configName = entry.getKey(); + Map tests = entry.getValue(); + + if (tests.containsKey(fqName)) { + foundTest = true; + if (!testsForSubProject.containsKey(configName)) { + Map configToTestMap = new LinkedHashMap<>(); + testsForSubProject.put(configName, configToTestMap); + } + + Map 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++ + } } - } else { + } + if (!foundTest) { + // Don't know what this test is so put it in the "unknown" bucket + if (!testsForSubProject.containsKey("unknown")) { + Map configToTestMap = new LinkedHashMap<>(); + testsForSubProject.put("unknown", configToTestMap); + } + + Map subTests = testsForSubProject.get("unknown"); + logger.debug("getTestsForSubProject: Found test class not in test report." + " Bumping to front of tasks '" + fqName + "'") - testsForSubProject.put(fqName, 3600000) // cheap way to bump to front of (eventually) sorted list + subTests.put(fqName, 3600000) // cheap way to bump to front of (eventually) sorted list includedClassFilesNotInTestReport++ } } // Sort by duration - def sorted = testsForSubProject.sort { a, b -> b.value <=> a.value } + for (Map.Entry entry : testsForSubProject.entrySet()) { + Map 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" @@ -216,7 +288,7 @@ def ArrayList getTestsForSubProject(SourceDirectorySet sourceDirectorySet) { assert sourceDirectorySet.files.size() == filesProcessed : "getTestsForSubProject did not process every file in sourceSet" - return new ArrayList(sorted.keySet()) + return testsForSubProject; } /*********************************************************************************