ghidra/gradleScripts/test.gradle
2019-03-26 13:46:51 -04:00

834 lines
29 KiB
Groovy

/*********************************************************************************
* test.gradle
*
* Contains tasks for running unit/integration tests and generating associated
* reports.
*
* Testing tasks: unitTestReport
* integrationTestReport
*
*
*********************************************************************************/
/*********************************************************************************
* Define some vars. Note that the first few vars must be defined. If they
* aren't set by a calling script, we default them.
*
*********************************************************************************/
apply from: "gradleScripts/testUtils.gradle"
configurations {
jmockitAgent
}
dependencies {
jmockitAgent('org.jmockit:jmockit:1.44') {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
}
if (!project.hasProperty('rootTestDir')) {
project.ext.rootTestDir = "build"
}
if (!project.hasProperty('machineName')) {
try {
project.ext.machineName = InetAddress.localHost.hostName
}
catch (UnknownHostException e) {
project.ext.machineName = "STANDALONE"
}
}
if (!project.hasProperty('xms')) {
project.ext.xms = "512M"
}
if (!project.hasProperty('xmx')) {
project.ext.xmx = "2048M"
}
loadApplicationProperties()
loadMachineSpecificProperties()
if (!project.hasProperty('srcTreeName')) {
project.ext.srcTreeName = System.properties.get("application.version")
}
project.ext.testRootDirName = "JunitTest_" + srcTreeName
project.ext.cunitTestRootDirName = "CunitTest_" + srcTreeName
project.ext.shareDir = System.properties.get('share.dir')
if (project.ext.shareDir != null) {
// add src tree name to machine specified share path
project.ext.testShareDir = shareDir + "/" + testRootDirName
project.ext.cunitTestShareDir = shareDir + "/" + cunitTestRootDirName
}
else {
project.ext.testShareDir = "$rootTestDir" + "/JUnit"
project.ext.cunitTestShareDir = "$rootTestDir" + "/CUnit"
}
project.ext.upgradeProgramErrorMessage = "WARNING! Attempting data upgrade for "
project.ext.upgradeTimeErrorMessage = "Upgrade Time (ms): "
project.ext.logPropertiesUrl = getLogFileUrl()
// Specify final report directory via cmd line using "-PreportDir=<location>". This is useful for
// the 'parallelCombinedTestReport' task.
project.ext.reportDir = project.hasProperty('reportDir') ? project.getProperty('reportDir') + "/reports"
: testShareDir + "/reports"
project.ext.reportArchivesDir = testShareDir + "/reportsArchive"
project.ext.archivedReportDir = reportArchivesDir + "/reports"
project.ext.testOutputDir = file(rootTestDir).getAbsolutePath() + "/output"
project.ext.userHome = System.properties.get("user.home")
// 'parallelMode' will change configs to allow concurrent test execution.
// Enable this mode if 'parallelCombinedTestReport' invoked in the command line.
project.ext.parallelMode = project.gradle.startParameter.taskNames.contains('parallelCombinedTestReport')
// 'parallelCombinedTestReport' task will parse a JUnit test report for test duration.
// Specify the location of the report via cmd line using "-PtestTimeParserInputDir=<location>".
// Otherwise, the default is to look for the latest test report in <reportDir>/../reportsArchive/reports_<date>
project.ext.testTimeParserInputDir = project.hasProperty('testTimeParserInputDir') ?
project.getProperty('testTimeParserInputDir') + "/reports/classes" : getLastArchivedReport("$reportArchivesDir") + "/classes"
// A port of 0 will allow the kernel to give a free port number in the set of "User"
// or "Registered" Ports (usually 1024 - 49151). This will prevent port collisions among
// concurrent JUnit tests.
project.ext.debugPort = parallelMode ? 0 : generateRandomDebugPort()
/*********************************************************************************
* Create specified absolute directory if it does not exist
*********************************************************************************/
def createDir(dirPath) {
println "Creating directory: " + dirPath
File dir = new File(dirPath);
dir.mkdirs();
if (!dir.exists()) {
throw new RuntimeException("Failed to create required directory: " + dirPath);
}
if (!dir.canWrite()) {
throw new RuntimeException("Process does not have write permission for directory: " + dirPath);
}
}
/*********************************************************************************
* Finds the log4j properties file and returns its path.
*********************************************************************************/
def getLogFileUrl() {
String rootDir = System.properties.get("user.dir")
String foundFile
new File(rootDir).eachFileRecurse(groovy.io.FileType.FILES) {
if (it.path.endsWith('src/main/resources/generic.log4jtest.xml')) {
foundFile = "file:" + it.path.toString()
return "file:" + it.path.toString()
}
}
return foundFile
}
/*********************************************************************************
* Creates a new debug port randomly.
*********************************************************************************/
def generateRandomDebugPort() {
// keep port within narrow range (NOTE: must be greater than 1024 and less than 65535)
// Generate random byte value
Random random = new Random()
def ran = random.nextInt();
for (int i = 1; i < 4; i++) {
ran ^= (ran >> 8);
}
ran &= 0xff;
// Generate random port between 18300 and 18555
def port = 18300 + ran
return port
}
/*********************************************************************************
* Loads application specific property file that contains info we need.
* Properties will be imediately added to the global System.properties file so we
* can readily access them from just one place.
*********************************************************************************/
def loadApplicationProperties() {
Properties props = new Properties()
File appProperties = new File("Ghidra/application.properties");
if (!appProperties.exists()) {
return;
}
props.load(new FileInputStream(appProperties));
props.each {k, v ->
System.setProperty(k, v)
}
}
/*********************************************************************************
* Record and Print test task start time
*********************************************************************************/
def startTestTimer(Task task) {
project.ext.testStartTime = new Date()
println ":" + task.project.name + ":" + task.name + " started: " + testStartTime;
}
/*********************************************************************************
* Print test task end time and elapsed time
*********************************************************************************/
def endTestTimer(Task task) {
Date endTime = new Date();
println ":" + task.project.name + ":" + task.name + " ended: " + endTime;
long elapsedMS = endTime.getTime() - testStartTime.getTime();
long msPerMin = 60 * 1000;
long msPerHour = 60 * msPerMin;
long hrs = elapsedMS / msPerHour;
long mins = (elapsedMS - (hrs * msPerHour)) / msPerMin;
long secs = (elapsedMS - (hrs * msPerHour) - (mins * msPerMin)) / 1000;
println ":" + task.project.name + ":" + task.name + " elapsed time: " +
String.format("%d:%02d:%02d", hrs, mins, secs);
}
/*********************************************************************************
* Loads any machine specific property file that contains info we need.
* Only those properties with our machine name prefix will be immediately
* added to the global System.properties file so we can readily access them from
* just one place (machine name prefix will be omitted).
*********************************************************************************/
def loadMachineSpecificProperties() {
if (project.hasProperty('testPropertiesPath')) {
Properties props = new Properties()
def testPropertiesPath = project.getProperty('testPropertiesPath')
File testProperties = new File(testPropertiesPath);
if (!testProperties.exists()) {
return;
}
println "loadMachineSpecificProperties: Using machine specific properties file '$testProperties'"
props.load(new FileInputStream(testProperties));
// Note: Only load those properties that contain our machine name (set above). This means
// that local test runs will not use these values. Also, if the test machine name
// changes, then so too will have to change the properties file.
props.each { k, v ->
if (k.startsWith(machineName)) {
// strip off <machine-name>_ prefix from property key
def key = k.substring(machineName.length()+1)
println "loadMachineSpecificProperties: Setting system property $key:$v"
System.setProperty(key, v)
}
}
}
}
/*********************************************************************************
* Archive previously generated test report
*********************************************************************************/
task archiveTestReport {
File reports = file(reportDir)
File archivedReports = file(archivedReportDir)
File reportArchives = file(reportArchivesDir)
onlyIf {
containsIndexFile(reports)
}
doLast {
reportArchives.mkdirs()
delete archivedReports
reports.renameTo(archivedReports)
renameReportArchive(archivedReports) // renames archived reports directory
generateArchiveIndex(reportArchives)
}
}
/*********************************************************************************
* Remove remnants of previous tests.
*********************************************************************************/
task deleteTestTempAndReportDirs() {
doFirst {
delete file(rootTestDir).getAbsolutePath()
delete file(reportDir).getAbsolutePath()
}
}
/*********************************************************************************
* Initialize test task
*********************************************************************************/
def initTestJVM(Task task, String rootDirName) {
def testTempDir = file(rootTestDir).getAbsolutePath()
def testReportDir = file(reportDir).getAbsolutePath()
task.doFirst {
println "Test Machine Name: " + machineName
println "Root Test Dir: " + rootTestDir
println "Test Output Dir: " + testOutputDir
println "Test Temp Dir: " + testTempDir
println "Test Report Dir: " + testReportDir
println "Java Debug Port: " + debugPort
createDir(testTempDir)
createDir(testOutputDir)
}
// If false, testing will halt when an error is found.
task.ignoreFailures true
// If false, then tests are re-run every time, even if no code has changed.
task.outputs.upToDateWhen {false}
// Must set this to see System.out print statements.
task.testLogging.showStandardStreams = true
// Min/Max heap size. These are passed in.
task.minHeapSize xms
task.maxHeapSize xmx
// for jmockit; needs the javaagent option
// -javaagent:/path/to/jmockit.jar
task.doFirst {
def jmockitPath = configurations.jmockitAgent.singleFile
task.jvmArgs '-DupgradeProgramErrorMessage=' + upgradeProgramErrorMessage,
'-DupgradeTimeErrorMessage=' + upgradeTimeErrorMessage,
'-Dlog4j.configuration=' + logPropertiesUrl,
'-Dghidra.test.property.batch.mode=true',
'-Dghidra.test.property.parallel.mode=' + parallelMode,
'-Dghidra.test.property.output.dir=' + testOutputDir,
'-Dghidra.test.property.report.dir=' + testReportDir,
'-DSystemUtilities.isTesting=true',
'-Dmachine.name=' + machineName,
'-Djava.io.tmpdir=' + testTempDir,
'-Duser.data.dir=' + userHome + '/.ghidra/.ghidra-' + srcTreeName + '-Test',
'-Dcpu.core.override=8',
'-XX:ParallelGCThreads=8',
'-XX:+UseParallelGC',
'-Djava.awt.headless=false',
// Dont' run this long winded analysis when testing (see DecompilerFunctionAnalyzer)
'-DDecompilerFunctionAnalyzer.enabled=false',
'-Djava.util.Arrays.useLegacyMergeSort=true',
'-Djdk.attach.allowAttachSelf',
'-javaagent:' + jmockitPath,
'-DLock.DEBUG=true',
'-Xdebug',
'-Xnoagent',
'-Djava.compiler=NONE',
'-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=' + debugPort
}
}
/*********************************************************************************
* Create this TestReport task so that individual test tasks may later assign themselves
* (using 'reportOn'). These tasks run tests concurrently.
*
* Invoking this task via command line enables 'parallelMode'.
*
* Example:
* ./gradlew parallelCombinedTestReport -PtestTimeParserInputDir=<path/to/report/classes> -PreportDir=<path/to/reports>
*
*********************************************************************************/
task parallelCombinedTestReport(type: TestReport) { t ->
group "test"
destinationDir = file("$reportDir")
logger.debug("parallelCombinedTestReport: Using destinationDir = $reportDir")
dependsOn ":deleteTestTempAndReportDirs"
mustRunAfter ":deleteTestTempAndReportDirs"
doFirst {
startTestTimer(t)
}
doLast {
endTestTimer(t)
}
}
/*********************************************************************************
* GRADLE TEST PROPERTIES
*
* This is where we add the test task to each subproject, and set generic test
* properties. These will apply to both unit and integration tests.
*********************************************************************************/
subprojects {
test { t ->
forkEvery 1
initTestJVM(t, testRootDirName)
// WARNING! WATCH OUT !!
// WARNING! Since a single shared JVM instance is used, the first
// test and its ApplicationConfiguration will be used to initialize
// the class searching environment. This can have a negative impact
// on test results due to the inconsistent Application environment
// which may exist when all tests are run versus a single test.
// Based on this limitation we should only use the Integration base
// test classes within integrationTest regions (e.g., test.slow).
doFirst {
startTestTimer(t)
}
doLast {
endTestTimer(t)
}
}
}
/*********************************************************************************
* UNIT TEST REPORT
*
* Summary: Runs all unit tests and generates a single report.
*
*********************************************************************************/
task unitTestReport(type: TestReport) { t ->
group "test"
destinationDir = file("$reportDir/unitTests")
reportOn subprojects.test
outputs.upToDateWhen {false}
}
/*********************************************************************************
* PREPARE FOR TESTS
*
* Summary: Setup for testing.
*********************************************************************************/
task prepareForTests {
dependsOn { project(":FunctionID").unpackFidDatabases }
}
/*********************************************************************************
* INTEGRATION TEST
*
* Summary: Applies a task to each subproject that will run all integration
* tests for that project.
*
*********************************************************************************/
subprojects { sub ->
task integrationTest (type: Test) { t ->
group "test"
dependsOn ':prepareForTests'
testClassesDirs = files sub.sourceSets.integrationTest.output.classesDirs
classpath = sub.sourceSets.integrationTest.runtimeClasspath
// Do not include suite classes; they trigger the tests in the suite to get run twice
// (once by normal jUnit and again when the suite runs).
excludes = ['**/*Suite*']
// Enable if you want to force Gradle to launch a new JVM for each test.
forkEvery 1
initTestJVM(t, testRootDirName)
doFirst {
startTestTimer(t)
}
doLast {
endTestTimer(t)
}
}
}
/*********************************************************************************
* Create a task of type: Test for a subproject.
* Each task will run a maximum of 'numMaxParallelForks' tests at a time.
* The task will include tests from classesList[classesListPosition, classesListPosition+numMaxParallelForks]
* and inherit excludes from its non-parallel counterpart.
*
* @param subproject
* @param testType - Either 'test' or 'integrationTest'. The sourceSets defined in setupJava.gradle
* @param taskNameCounter - Number to append to task name.
* @param classesList - List of classes for this subproject.testType. Sorted by duration.
* @param classesListPosition - Start position in 'classesList'
* @param numMaxParallelForks - Number of concurrent tests to run.
*********************************************************************************/
def createTestTask(Project subproject, String testType, int taskNameCounter, ArrayList classesList,
int classesListPosition, int numMaxParallelForks) {
subproject.task(testType+"_$taskNameCounter", type: Test) { t ->
group "test"
testClassesDirs = files subproject.sourceSets["$testType"].output.classesDirs
classpath = subproject.sourceSets["$testType"].runtimeClasspath
maxParallelForks = numMaxParallelForks
initTestJVM(t, testRootDirName)
rootProject.tasks['parallelCombinedTestReport'].reportOn t
// include classes in task up to the maxParallelForks value
for (int i = 0; (i < maxParallelForks && classesListPosition < classesList.size); i++) {
// Convert my.package.MyClass to my/package/MyClass.class
include classesList[classesListPosition].replace(".","/") + ".class"
classesListPosition++
}
// Inherit excludes from subproject build.gradle.
// These excludes will override any includes.
excludes = subproject.tasks["$testType"].excludes
// Display granularity of 0 will log method-level events
// as "Task > Worker > org.SomeClass > org.someMethod".
testLogging {
displayGranularity 0
events "started", "passed", "skipped", "failed"
}
doFirst {
startTestTimer(t)
}
doLast {
endTestTimer(t)
}
// Current task mustRunAfter previous task (test_1 before test_2,etc.)
if (taskNameCounter > 1) {
def prevTaskName = "$testType" + "_" + (taskNameCounter -1)
mustRunAfter prevTaskName
}
logger.info("createTestTask: Created $subproject.name" + ":$testType" + "_$taskNameCounter"
+ "\n\tincludes = \n" + t.getIncludes() + "\n\tinheriting excludes (overrides any 'includes') = \n"
+ t.excludes)
} // end task
}
/*********************************************************************************
* Split each subproject's integrationTest ("src/test.slow") and test ("src/test")
* task into multiple tasks, based on duration parsed from a previous JUnit report.
* Each task will run tests concurrently.
*
* Each task will run a maximum of the 'maxParallelForks' value at a time.
*********************************************************************************/
configure(subprojects.findAll {parallelMode == true}) { subproject ->
afterEvaluate {
// "subprojects { afterEvaluate { evaluate()"
// forces evaluation of subproject configuration. Needed for inheriting excludes
// from non-parallel counterpart.
subproject.evaluate()
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
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.
}
}
} // end afterEvaluate
}// end subprojects
/*********************************************************************************
* NON-BASE INTEGRATION TEST REPORT
*
* Summary: Runs integration tests for all modules except "Base"
* and generates a single report.
*
*********************************************************************************/
task integrationTestReportNoBase(type: TestReport) { t ->
group "test"
destinationDir = file("$reportDir/integrationTests")
subprojects.each {
if (!it.name.equals("Base")) { // excludes Base module tests
reportOn it.integrationTest
}
}
}
/*********************************************************************************
* INTEGRATION TEST REPORT BASE ONLY
*
* Summary: Runs the integration tests for just the "Base" module and generates a
* report.
*
*********************************************************************************/
task integrationTestReportBaseOnly(type: TestReport) { t ->
group "test"
destinationDir = file("$reportDir/integrationTests")
subprojects.each {
if (it.name.equals("Base")) { // excludes Base module tests
reportOn it.integrationTest
}
}
}
/*********************************************************************************
* COMBINED TEST REPORT
*
* Summary: Runs all integration and unit tests, and creates a single report.
*
*********************************************************************************/
task combinedTestReport(type: TestReport) { t ->
group "test"
destinationDir = file("$reportDir")
dependsOn ":deleteTestTempAndReportDirs"
mustRunAfter ":deleteTestTempAndReportDirs"
subprojects { project ->
afterEvaluate{
if (isTestable(project)) {
reportOn project.test
reportOn project.integrationTest
}
}
}
}
/*********************************************************************************
* ALL TESTS
*
* Summary: Applies a task to each subproject that will run all unit tests and all
* integration tests for that project.
*
*********************************************************************************/
subprojects { sub ->
task allTests {
dependsOn 'integrationTest'
dependsOn 'test'
}
}
/*********************************************************************************
* CUNIT TEST
*
* Summary: Applies a task to each "Processor Test" subproject that will run all
* CUNIT tests for that project.
*
*********************************************************************************/
subprojects { sub ->
task cunitTest (type: Test) { t ->
group "cunit"
dependsOn ':prepareForTests'
testClassesDirs = files sub.sourceSets.cunitTest.output.classesDirs
classpath = sub.sourceSets.cunitTest.runtimeClasspath
// Enable if you want to force Gradle to launch a new JVM for each test.
forkEvery 1
initTestJVM(t, cunitTestRootDirName)
doFirst {
startTestTimer(t)
}
doLast {
endTestTimer(t)
}
}
}
/*********************************************************************************
* CUNIT TEST REPORT
*
* Summary: Runs all CUNIT tests
*
*********************************************************************************/
task cunitTestReport(type: TestReport) { t ->
group "cunit"
destinationDir = file("$cunitTestShareDir" + "/reports")
reportOn subprojects.cunitTest
}
/*********************************************************************************
* CUNIT TEST REPORT w/ CUNIT MATRIX REPORT
*
* Summary: Runs all CUNIT tests and consolidate JUnit and Matrix report in
* results directory.
*
*********************************************************************************/
task cunitConsolidatedTestReport(type: Copy) {
group "cunit"
dependsOn ':cunitTestReport'
into (cunitTestShareDir + "/reports")
from (testOutputDir + "/test-output") {
exclude '**/*.xml', 'cache/**'
}
}
/*********************************************************************************
* Rename archived report directory to reflect lastModified date
*********************************************************************************/
def renameReportArchive(dir) {
long modifiedTime = dir.lastModified()
Calendar calendar = new GregorianCalendar()
calendar.setTimeInMillis( modifiedTime )
java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat("_MMM_d_yyyy")
String newFilename = dir.getName() + dateFormat.format(calendar.getTime())
File newFile = new File( dir.getParent(), newFilename )
newFile = getUniqueName( newFile )
dir.renameTo( newFile )
println "Archived reports: " + newFile.getCanonicalPath()
}
/*********************************************************************************
* Attempt to generate unique directory/file name to avoid duplication
*********************************************************************************/
def getUniqueName(file) {
if (!file.exists()) {
return file
}
for (int i = 2; i < 22; i++ ) {
String newName = file.getName() + "_" + i
File newFile = new File( file.getParentFile(), newName )
if (!newFile.exists()) {
return newFile
}
}
return file
}
/*********************************************************************************
* Generate report archive index.html file within the archivesDir directory
*********************************************************************************/
def generateArchiveIndex(archivesDir) {
File file = new File( archivesDir, "index.html" );
println "Generating Archived Reports Index: " + file.getCanonicalPath()
PrintWriter p = new PrintWriter(file);
try {
p.println("<HTML>");
p.println("<HEAD>");
printStyleSheet(p);
p.println("</HEAD>");
p.println("<BODY>");
p.println("<CENTER>");
p.println("<H2>Past Test Reports</H2>");
p.println(" <TABLE BORDER=1 ALIGN=CENTER WIDTH=\"90%\">");
// get a dir listing of all the files from the report dir and
// create a hyperlink for the filename
File[] reportDirFiles = archivesDir.listFiles();
Arrays.sort(reportDirFiles, new Comparator<File>() {
public int compare(File file1, File file2) {
if ( file1 == file2 ) {
return 0;
}
long timestamp1 = file1.lastModified();
long timestamp2 = file2.lastModified();
if (timestamp1 == timestamp2) {
return file1.getName().compareTo(file2.getName());
}
return (timestamp1 < timestamp2) ? 1 : -1;
}
});
// archived reports older than one month will be removed
long now = System.currentTimeMillis();
println "Current time: " + now
long oldestTime = now - (1000L * 60 * 60 * 24 * 30);
println "Delete date: " + oldestTime
for (File f : reportDirFiles) {
if (!f.isDirectory()) {
continue;
}
println "File: " + f.toString()
println "\tlast modified: " + f.lastModified()
if (f.lastModified() < oldestTime) {
println "\t\tFile older than the oldest time--deleting!..."
f.deleteDir(); // delete old archive results
}
else {
println "\t\tFile is a spring chicken--creating link..."
createLinkForDir(p, f);
}
}
p.println(" </TABLE>");
p.println("</CENTER>");
p.println("</BODY>");
p.println("</HTML>");
}
finally {
p.close();
}
}
/*********************************************************************************
* Add HTML code to PrintWriter p which provides link to specified file which
* is assumed to reside within the same directory as the index.html file being written
*********************************************************************************/
def createLinkForDir(p, file) {
// skip files - dirs only
if ( !file.isDirectory() ) {
return;
}
String columnPadding = " "
p.println( columnPadding + "<TR><TD>" )
String dirName = file.getName()
if (containsIndexFile(file)) {
String linkText = "<a href=\"./" + dirName + "/index.html\">" + dirName + "</a>"
p.println( columnPadding + linkText);
}
else {
p.println( columnPadding + dirName);
}
p.println( columnPadding + "</TD></TR>" )
}
/*********************************************************************************
* Returns true if the specified directory File contains an index.html file
*********************************************************************************/
def containsIndexFile(dir) {
File indexFile = new File(dir, "index.html");
return indexFile.exists();
}
/*********************************************************************************
* Add HTML code to PrintWriter p which provides the STYLE tag
*********************************************************************************/
def printStyleSheet(p) {
p.println( "<style>" );
p.println( "<!--" );
p.println( "" );
p.println( "-->" );
p.println( "</style>" );
}