import java.util.regex.*; import groovy.io.FileType; ext.testReport = null; // contains from JUnit test report /* * 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): *
*
0s
* The duration value may appear in the format of: 1m2s, 1m2.3s, 3.4s */ Pattern p = Pattern.compile("(?<=id=\"duration\">[\r\n])(.*?)(?= 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 from JUnit test report */ def HashMap getTestReport() { // populate testReport only once per gradle configuration phase if (project.testReport == null) { logger.debug("getTestReport: Populating 'testReport' using '$testTimeParserInputDir'") assert (testTimeParserInputDir != null && testTimeParserInputDir.contains("classes")) : """getTestReport: The value of 'testTimeParserInputDir' does not exist. Specify this value via cmd line -PtestTimeParserInputDir=""" File classesReportDir = new File(testTimeParserInputDir) assert classesReportDir.exists() : "getTestReport: The path '$testTimeParserInputDir' does not exist on the file system" testReport = new HashMap(); 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: *

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

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

Class\\s).*?(?=

)") long durationInMillis = getDurationFromTestReportClass(fileContents, file.name) testReport.put(fqNameFromTestReport, durationInMillis) 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 . * 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) { assert (getTestReport() != null && getTestReport().size() > 0) : "getTestsForSubProject: Problem parsing test report located at: " + testTimeParserInputDir def testsForSubProject = new 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.") 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) // 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++ } } else { 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 includedClassFilesNotInTestReport++ } } // Sort by duration def sorted = testsForSubProject.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 new ArrayList(sorted.keySet()) } /********************************************************************************* * 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.sourceSets.test.java.files.isEmpty()) { logger.debug("shouldSkipTestTaskCreation: Skip task creation for $subproject.name. No test sources.") return true } if (!isTestable(subproject)) { logger.debug("shouldSkipTestTaskCreation: Skip task creation for $subproject.name. isTestable == false") return true } if (subproject.findProperty("excludeFromParallelTests") ?: false) { logger.debug("shouldSkipTestTaskCreation: Skip task creation for $subproject.name." + " 'excludeFromParallelTests' found.") 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.sourceSets.integrationTest.java.files.isEmpty()) { logger.debug("shouldSkipIntegrationTestTaskCreation: Skip task creation for $subproject.name." + " No integrationTest sources.") return true } if (!isTestable(subproject)) { logger.debug("shouldSkipIntegrationTestTaskCreation: Skip task creation for $subproject.name." + " isTestable == false") return true } if (subproject.findProperty("excludeFromParallelIntegrationTests") ?: false) { logger.debug("shouldSkipIntegrationTestTaskCreation: Skip task creation for $subproject.name." + "'excludeFromParallelIntegrationTests' found.") 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) assert (reportArchiveDir.exists()) : """Tried to parse test report durations from archive location ' $reportArchiveDir ' because no -PtestTimeParserInputDir= supplied via cmd line""" // 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= 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() } /********************************************************************************* * Returns true if subproject is not a support module. * These modules are commonly excluded in type:TestReport tasks. *********************************************************************************/ def isTestable(Project p) { return !(p.findProperty("isSupportProject") ?: false) } ext { getTestsForSubProject = this.&getTestsForSubProject // export this utility method to project shouldSkipTestTaskCreation = this.&shouldSkipTestTaskCreation shouldSkipIntegrationTestTaskCreation = this.&shouldSkipIntegrationTestTaskCreation getLastArchivedReport = this.&getLastArchivedReport isTestable = this.&isTestable }