JAL-4167 Added colours to bash output
[jalview.git] / build.gradle
index 851c5f6..3b06476 100644 (file)
@@ -9,6 +9,7 @@ import org.gradle.util.ConfigureUtil
 import org.gradle.plugins.ide.eclipse.model.Output
 import org.gradle.plugins.ide.eclipse.model.Library
 import java.security.MessageDigest
+import java.util.regex.Matcher
 import groovy.transform.ExternalizeMethods
 import groovy.util.XmlParser
 import groovy.xml.XmlUtil
@@ -37,6 +38,7 @@ buildscript {
   dependencies {
     classpath "com.vladsch.flexmark:flexmark-all:0.62.0"
     classpath "org.jsoup:jsoup:1.14.3"
+    classpath "com.eowise:gradle-imagemagick:0.5.1"
   }
 }
 
@@ -48,7 +50,7 @@ plugins {
   id "com.diffplug.gradle.spotless" version "3.28.0"
   id 'com.github.johnrengelman.shadow' version '4.0.3'
   id 'com.install4j.gradle' version '10.0.3'
-  id 'com.dorongold.task-tree' version '2.1.0' // only needed to display task dependency tree with  gradle task1 [task2 ...] taskTree
+  id 'com.dorongold.task-tree' version '2.1.1' // only needed to display task dependency tree with  gradle task1 [task2 ...] taskTree
   id 'com.palantir.git-version' version '0.13.0' apply false
 }
 
@@ -108,8 +110,12 @@ ext {
   getdownChannelName = CHANNEL.toLowerCase()
   // default to "default". Currently only has different cosmetics for "develop", "release", "default"
   propertiesChannelName = ["develop", "release", "test-release", "jalviewjs", "jalviewjs-release" ].contains(getdownChannelName) ? getdownChannelName : "default"
+  channelDirName = propertiesChannelName
   // Import channel_properties
-  channelDir = string("${jalviewDir}/${channel_properties_dir}/${propertiesChannelName}")
+  if (getdownChannelName.startsWith("develop-")) {
+    channelDirName = "develop-SUFFIX"
+  }
+  channelDir = string("${jalviewDir}/${channel_properties_dir}/${channelDirName}")
   channelGradleProperties = string("${channelDir}/channel_gradle.properties")
   channelPropsFile = string("${channelDir}/${resource_dir}/${channel_props}")
   overrideProperties(channelGradleProperties, false)
@@ -198,6 +204,8 @@ ext {
   testSourceDir = useClover ? cloverTestInstrDir : testDir
   testClassesDir = useClover ? cloverTestClassesDir : "${jalviewDir}/${test_output_dir}"
 
+  channelSuffix = ""
+  backgroundImageText = BACKGROUNDIMAGETEXT
   getdownChannelDir = string("${getdown_website_dir}/${propertiesChannelName}")
   getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
   getdownArchiveDir = string("${jalviewDir}/${getdown_archive_dir}")
@@ -215,12 +223,15 @@ ext {
   getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher}")
   getdownAppDistDir = getdown_app_dir_alt
   getdownImagesDir = string("${jalviewDir}/${getdown_images_dir}")
+  getdownImagesBuildDir = string("${buildDir}/imagemagick/getdown")
   getdownSetAppBaseProperty = false // whether to pass the appbase and appdistdir to the application
   reportRsyncCommand = false
   jvlChannelName = CHANNEL.toLowerCase()
   install4jSuffix = CHANNEL.substring(0, 1).toUpperCase() + CHANNEL.substring(1).toLowerCase(); // BUILD -> Build
   install4jDMGDSStore = "${install4j_images_dir}/${install4j_dmg_ds_store}"
-  install4jDMGBackgroundImage = "${install4j_images_dir}/${install4j_dmg_background}"
+  install4jDMGBackgroundImageDir = "${install4j_images_dir}"
+  install4jDMGBackgroundImageBuildDir = "build/imagemagick/install4j"
+  install4jDMGBackgroundImageFile = "${install4j_dmg_background}"
   install4jInstallerName = "${jalview_name} Non-Release Installer"
   install4jExecutableName = install4j_executable_name
   install4jExtraScheme = "jalviewx"
@@ -245,6 +256,7 @@ ext {
       testng_excluded_groups = "Not-bamboo"
     }
     install4jExtraScheme = "jalviewb"
+    backgroundImageText = true
     break
 
     case [ "RELEASE", "JALVIEWJS-RELEASE" ]:
@@ -275,7 +287,7 @@ ext {
     getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
     getdownAppBase = file(getdownAppBaseDir).toURI().toString()
     if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
-      throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution")
+      throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution [did not find '${ARCHIVEDIR}/${package_dir}']")
     } else {
       package_dir = string("${ARCHIVEDIR}/${package_dir}")
       buildProperties = string("${ARCHIVEDIR}/${classes_dir}/${build_properties_file}")
@@ -287,6 +299,23 @@ ext {
     install4jExtraScheme = "jalviewa"
     break
 
+    case ~/^DEVELOP-([\.\-\w]*)$/:
+    def suffix = Matcher.lastMatcher[0][1]
+    reportRsyncCommand = true
+    getdownSetAppBaseProperty = true
+    JALVIEW_VERSION=JALVIEW_VERSION+"-d${suffix}-${buildDate}"
+    install4jSuffix = "Develop ${suffix}"
+    install4jExtraScheme = "jalviewd"
+    install4jInstallerName = "${jalview_name} Develop ${suffix} Installer"
+    getdownChannelName = string("develop-${suffix}")
+    getdownChannelDir = string("${getdown_website_dir}/${getdownChannelName}")
+    getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
+    getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
+    getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
+    channelSuffix = string(suffix)
+    backgroundImageText = true
+    break
+
     case "DEVELOP":
     reportRsyncCommand = true
     getdownSetAppBaseProperty = true
@@ -296,6 +325,7 @@ ext {
     install4jSuffix = "Develop"
     install4jExtraScheme = "jalviewd"
     install4jInstallerName = "${jalview_name} Develop Installer"
+    backgroundImageText = true
     break
 
     case "TEST-RELEASE":
@@ -310,6 +340,7 @@ ext {
     install4jSuffix = "Test"
     install4jExtraScheme = "jalviewt"
     install4jInstallerName = "${jalview_name} Test Installer"
+    backgroundImageText = true
     break
 
     case ~/^SCRATCH(|-[-\w]*)$/:
@@ -333,6 +364,7 @@ ext {
     install4jSuffix = "Test-Local"
     install4jExtraScheme = "jalviewt"
     install4jInstallerName = "${jalview_name} Test Installer"
+    backgroundImageText = true
     break
 
     case [ "LOCAL", "JALVIEWJS" ]:
@@ -508,6 +540,14 @@ ext {
   helpSourceDir = string("${helpParentDir}/${help_dir}")
   helpFile = string("${helpBuildDir}/${help_dir}/help.jhm")
 
+  convertBinary = null
+  convertBinaryExpectedLocation = imagemagick_convert
+  if (convertBinaryExpectedLocation.startsWith("~/")) {
+    convertBinaryExpectedLocation = System.getProperty("user.home") + convertBinaryExpectedLocation.substring(1)
+  }
+  if (file(convertBinaryExpectedLocation).exists()) {
+    convertBinary = convertBinaryExpectedLocation
+  }
 
   relativeBuildDir = file(jalviewDirAbsolutePath).toPath().relativize(buildDir.toPath())
   jalviewjsBuildDir = string("${relativeBuildDir}/jalviewjs")
@@ -1024,7 +1064,7 @@ compileJava {
   // JBP->BS should the print statement in doFirst refer to compile_target_compatibility ?
   sourceCompatibility = compile_source_compatibility
   targetCompatibility = compile_target_compatibility
-  options.compilerArgs = additional_compiler_args
+  options.compilerArgs += additional_compiler_args
   options.encoding = "UTF-8"
   doFirst {
     print ("Setting target compatibility to "+compile_target_compatibility+"\n")
@@ -1036,7 +1076,7 @@ compileJava {
 compileTestJava {
   sourceCompatibility = compile_source_compatibility
   targetCompatibility = compile_target_compatibility
-  options.compilerArgs = additional_compiler_args
+  options.compilerArgs += additional_compiler_args
   doFirst {
     print ("Setting target compatibility to "+targetCompatibility+"\n")
   }
@@ -1613,7 +1653,19 @@ task copyChannelResources(type: Copy) {
 
   def inputDir = "${channelDir}/${resource_dir}"
   def outputDir = resourcesBuildDir
-  from inputDir
+  from(inputDir) {
+    include(channel_props)
+    filter(ReplaceTokens,
+      beginToken: '__',
+      endToken: '__',
+      tokens: [
+        'SUFFIX': channelSuffix
+      ]
+    )
+  }
+  from(inputDir) {
+    exclude(channel_props)
+  }
   into outputDir
 
   inputs.dir(inputDir)
@@ -1680,26 +1732,91 @@ task prepare {
 
 compileJava.dependsOn prepare
 run.dependsOn compileJava
-//run.dependsOn prepare
-
+compileTestJava.dependsOn compileJava
 
-//testReportDirName = "test-reports" // note that test workingDir will be $jalviewDir
-test {
-  dependsOn prepare
 
-  if (useClover) {
-    dependsOn cloverClasses
-   } else { //?
-    dependsOn compileJava //?
+ext.testsFailed = false
+/* testTask0 is the main test task */
+task testTask0(type: Test) {
+  group = "Verification"
+  description = "The main test task. Runs all non-testTaskN-labelled tests (unless excluded)"
+  useTestNG() {
+    includeGroups testng_groups.split(",")
+    excludeGroups testng_excluded_groups.split(",")
+    tasks.withType(Test).matching {it.name.startsWith("testTask") && it.name != name}.all {t -> excludeGroups t.name}
+    preserveOrder true
+    useDefaultListeners=true
   }
+}
 
+/* separated tests */
+task testTask1(type: Test) {
+  group = "Verification"
+  description = "Tests that need to be isolated from the main test run"
   useTestNG() {
-    includeGroups testng_groups
-    excludeGroups testng_excluded_groups
+    includeGroups name
+    excludeGroups testng_excluded_groups.split(",")
     preserveOrder true
     useDefaultListeners=true
   }
+}
+
+/*
+ * adapted from https://medium.com/@wasyl/pretty-tests-summary-in-gradle-744804dd676c
+ * to summarise test results from all Test tasks
+ */
+/* START of test tasks results summary */
+import groovy.time.TimeCategory
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+rootProject.ext.testsResults = [] // Container for tests summaries
+
+tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}.all { testTask ->
+
+  // from original test task
+  if (useClover) {
+    dependsOn cloverClasses
+  } else { //?
+    dependsOn testClasses //?
+  }
+
+  // run main tests first
+  if (!testTask.name.equals("testTask0"))
+    testTask.mustRunAfter "testTask0"
+
+  testTask.testLogging { logging ->
+    events TestLogEvent.FAILED,
+      TestLogEvent.SKIPPED,
+      TestLogEvent.STANDARD_OUT,
+      TestLogEvent.STANDARD_ERROR
+
+    exceptionFormat TestExceptionFormat.FULL
+    showExceptions true
+    showCauses true
+    showStackTraces true
+  }
+
+  ignoreFailures = true // Always try to run all tests for all modules
+
+  afterSuite { desc, result ->
+
+    if (desc.parent) return // Only summarize results for whole modules
+
+    def summary = [testTask.project.name, testTask.name, result, TimeCategory.minus(new Date(result.endTime), new Date(result.startTime)), testTask.reports.html.entryPoint]
+
+    // Add reports in `testsResults`, keep failed suites at the end
+    if (result.resultType == TestResult.ResultType.SUCCESS) {
+      rootProject.ext.testsResults.add(0, summary)
+    } else {
+      rootProject.ext.testsResults.add(summary)
+    }
+
+    if (result.resultType == TestResult.ResultType.FAILURE) {
+      testsFailed = true
+    }
+  }
 
+  // from original test task
   maxHeapSize = "1024m"
 
   workingDir = jalviewDir
@@ -1722,6 +1839,108 @@ test {
       println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover")
     }
   }
+
+}
+
+gradle.buildFinished {
+    def allResults = rootProject.ext.testsResults
+
+    if (!allResults.isEmpty()) {
+        printResults allResults
+        allResults.each {r ->
+          if (r[2].resultType == TestResult.ResultType.FAILURE)
+            throw GradleException("Failed tests (buildFinished)!")
+        }
+    }
+}
+
+private static void printResults(allResults) {
+    // styler from https://stackoverflow.com/a/56139852
+    def styler = 'black red green yellow blue magenta cyan white'.split().toList().withIndex(30).collectEntries { key, val -> [(key) : { "\033[${val}m${it}\033[0m" }] }
+
+    def maxLength = 0
+    def failedTests = false
+    def summaryLines = []
+    allResults.each {
+      def projectName = it[0]
+      def taskName = it[1]
+      def result = it[2]
+      def time = it[3]
+      def report = it[4]
+      def colour = 'black'
+      switch(result.resultType) {
+        case TestResult.ResultType.SUCCESS:
+          colour = 'green'
+          break;
+        case TestResult.ResultType.FAILURE:
+          colour = 'red'
+          failedTests = true
+          break;
+        default:
+          colour = 'yellow'
+          break;
+      }
+      def summaryCol = "${projectName}:${taskName} results: ${styler[colour](result.resultType)} (" +
+      "${result.testCount} tests, " +
+      (result.successfulTestCount > 0 ? "${styler['green'](result.successfulTestCount)} successes" : "${result.successfulTestCount} successes") + ", " +
+      (result.failedTestCount > 0 ? "${styler['red'](result.failedTestCount)} failures" : "${result.failedTestCount} failures") + ", " +
+      "${result.skippedTestCount} skipped" +
+      ") " + "in ${time}"
+      def summaryPlain = "${projectName}:${taskName} results: ${result.resultType} (" +
+      "${result.testCount} tests, " +
+      "${result.successfulTestCount} successes, " +
+      "${result.failedTestCount} failures, " +
+      "${result.skippedTestCount} skipped" +
+      ") " + "in ${time}"
+      def reportLine = "Report file: ${report}"
+      def ls = summaryPlain.length()
+      def lr = reportLine.length()
+      def m = [ls, lr].max()
+      if (m > maxLength)
+        maxLength = m
+      def info = [ls, summaryCol, reportLine]
+      summaryLines.add(info)
+    }
+
+    println "┌${"${"─" * maxLength}"}┐"
+
+    println summaryLines.collect {info ->
+      def ls = info[0]
+      def summary = info[1]
+      def report = info[2]
+
+      return "│" + summary + " " * (maxLength - ls) + "│" + "\n" +
+      "│" + report + " " * (maxLength - report.length()) + "│"
+
+      }.join("\n├${"${"─" * maxLength}"}┤\n")
+
+    println "└${"${"─" * maxLength}"}┘"
+}
+/* END of test tasks results summary */
+
+task verifyTestStatus {
+  group = "Verification"
+  description = "Task that FAILs the build if any tests failed"
+  doLast {
+    if (testsFailed) {
+      throw new GradleException("There were failing tests!")
+    }
+  }
+}
+
+test {
+  // from original test task
+  if (useClover) {
+    dependsOn cloverClasses
+  } else { //?
+    dependsOn testClasses
+  }
+  dependsOn tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}
+  finalizedBy verifyTestStatus
+
+
+  // not running tests in this task
+  exclude "**/*"
 }
 
 
@@ -1865,10 +2084,58 @@ shadowJar {
   minimize()
 }
 
+task getdownImagesCopy() {
+  inputs.dir getdownImagesDir
+  outputs.dir getdownImagesBuildDir
+
+  doFirst {
+    copy {
+      from(getdownImagesDir) {
+        include("*getdown*.png")
+      }
+      into getdownImagesBuildDir
+    }
+  }
+}
+
+task getdownImagesProcess() {
+  dependsOn getdownImagesCopy
+
+  doFirst {
+    if (backgroundImageText) {
+      if (convertBinary == null) {
+        throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
+      }
+      if (!project.hasProperty("getdown_background_image_text_suffix_cmd")) {
+        throw new StopExecutionException("No property 'getdown_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
+      }
+      fileTree(dir: getdownImagesBuildDir, include: "*background*.png").getFiles().each { file ->
+        exec {
+          executable convertBinary
+          args = [
+            file.getPath(),
+            '-font', getdown_background_image_text_font,
+            '-fill', getdown_background_image_text_colour,
+            '-draw', sprintf(getdown_background_image_text_suffix_cmd, channelSuffix),
+            '-draw', sprintf(getdown_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
+            '-draw', sprintf(getdown_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
+            file.getPath()
+          ]
+        }
+      }
+    }
+  }
+}
+
+task getdownImages() {
+  dependsOn getdownImagesProcess
+}
 
 task getdownWebsite() {
   group = "distribution"
   description = "Create the getdown minimal app folder, and website folder for this version of jalview. Website folder also used for offline app installer"
+
+  dependsOn getdownImages
   if (buildDist) {
     dependsOn makeDist
   }
@@ -1891,6 +2158,13 @@ task getdownWebsite() {
 
     copy {
       from channelPropsFile
+      filter(ReplaceTokens,
+        beginToken: '__',
+        endToken: '__',
+        tokens: [
+          'SUFFIX': channelSuffix
+        ]
+      )
       into getdownAppBaseDir
     }
     getdownWebsiteResourceFilenames += file(channelPropsFile).getName()
@@ -1906,13 +2180,13 @@ task getdownWebsite() {
     if (getdownAltMultiJavaLocation != null && getdownAltMultiJavaLocation.length() > 0) {
       props.put("getdown_txt_multi_java_location", getdownAltMultiJavaLocation)
     }
-    if (getdownImagesDir != null && file(getdownImagesDir).exists()) {
-      props.put("getdown_txt_ui.background_image", "${getdownImagesDir}/${getdown_background_image}")
-      props.put("getdown_txt_ui.instant_background_image", "${getdownImagesDir}/${getdown_instant_background_image}")
-      props.put("getdown_txt_ui.error_background", "${getdownImagesDir}/${getdown_error_background}")
-      props.put("getdown_txt_ui.progress_image", "${getdownImagesDir}/${getdown_progress_image}")
-      props.put("getdown_txt_ui.icon", "${getdownImagesDir}/${getdown_icon}")
-      props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesDir}/${getdown_mac_dock_icon}")
+    if (getdownImagesBuildDir != null && file(getdownImagesBuildDir).exists()) {
+      props.put("getdown_txt_ui.background_image", "${getdownImagesBuildDir}/${getdown_background_image}")
+      props.put("getdown_txt_ui.instant_background_image", "${getdownImagesBuildDir}/${getdown_instant_background_image}")
+      props.put("getdown_txt_ui.error_background", "${getdownImagesBuildDir}/${getdown_error_background}")
+      props.put("getdown_txt_ui.progress_image", "${getdownImagesBuildDir}/${getdown_progress_image}")
+      props.put("getdown_txt_ui.icon", "${getdownImagesBuildDir}/${getdown_icon}")
+      props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesBuildDir}/${getdown_mac_dock_icon}")
     }
 
     props.put("getdown_txt_title", jalview_name)
@@ -2368,12 +2642,59 @@ task cleanInstallersDataFiles {
   }
 }
 
+task install4jDMGBackgroundImageCopy {
+  inputs.file "${install4jDMGBackgroundImageDir}/${install4jDMGBackgroundImageFile}"
+  outputs.dir "${install4jDMGBackgroundImageBuildDir}"
+  doFirst {
+    copy {
+      from(install4jDMGBackgroundImageDir) {
+        include(install4jDMGBackgroundImageFile)
+      }
+      into install4jDMGBackgroundImageBuildDir
+    }
+  }
+}
+
+task install4jDMGBackgroundImageProcess {
+  dependsOn install4jDMGBackgroundImageCopy
+
+  doFirst {
+    if (backgroundImageText) {
+      if (convertBinary == null) {
+        throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
+      }
+      if (!project.hasProperty("install4j_background_image_text_suffix_cmd")) {
+        throw new StopExecutionException("No property 'install4j_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
+      }
+      fileTree(dir: install4jDMGBackgroundImageBuildDir, include: "*.png").getFiles().each { file ->
+        exec {
+          executable convertBinary
+          args = [
+            file.getPath(),
+            '-font', install4j_background_image_text_font,
+            '-fill', install4j_background_image_text_colour,
+            '-draw', sprintf(install4j_background_image_text_suffix_cmd, channelSuffix),
+            '-draw', sprintf(install4j_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
+            '-draw', sprintf(install4j_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
+            file.getPath()
+          ]
+        }
+      }
+    }
+  }
+}
+
+task install4jDMGBackgroundImage {
+  dependsOn install4jDMGBackgroundImageProcess
+}
+
 task installerFiles(type: com.install4j.gradle.Install4jTask) {
   group = "distribution"
   description = "Create the install4j installers"
   dependsOn getdown
   dependsOn copyInstall4jTemplate
   dependsOn cleanInstallersDataFiles
+  dependsOn install4jDMGBackgroundImage
 
   projectFile = install4jConfFile
 
@@ -2410,7 +2731,7 @@ task installerFiles(type: com.install4j.gradle.Install4jTask) {
     'INTERNAL_ID': install4jInternalId,
     'WINDOWS_APPLICATION_ID': install4jWinApplicationId,
     'MACOS_DMG_DS_STORE': install4jDMGDSStore,
-    'MACOS_DMG_BG_IMAGE': install4jDMGBackgroundImage,
+    'MACOS_DMG_BG_IMAGE': "${install4jDMGBackgroundImageBuildDir}/${install4jDMGBackgroundImageFile}",
     'WRAPPER_LINK': getdownWrapperLink,
     'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script,
     'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script,
@@ -3832,3 +4153,4 @@ task jalviewjs {
   description "Build the site"
   dependsOn jalviewjsBuildSite
 }
+