JAL-4059 rename files from jsmol j2s zip from jsmol to swingjs
[jalview.git] / build.gradle
index 2d2dad0..6137539 100644 (file)
@@ -10,6 +10,10 @@ 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 java.util.concurrent.Executors
+import java.util.concurrent.Future
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.TimeUnit
 import groovy.transform.ExternalizeMethods
 import groovy.util.XmlParser
 import groovy.xml.XmlUtil
@@ -48,7 +52,7 @@ plugins {
   id 'application'
   id 'eclipse'
   id "com.diffplug.gradle.spotless" version "3.28.0"
-  id 'com.github.johnrengelman.shadow' version '4.0.3'
+  id 'com.github.johnrengelman.shadow' version '6.0.0'
   id 'com.install4j.gradle' version '10.0.3'
   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
@@ -559,6 +563,7 @@ ext {
   }
   jalviewjsTransferSiteLibDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_lib")
   jalviewjsTransferSiteSwingJsDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_swingjs")
+  jalviewjsTransferSiteMergeDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_merge")
   jalviewjsTransferSiteCoreDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_core")
   jalviewjsJalviewCoreHtmlFile = string("")
   jalviewjsJalviewCoreName = string(jalviewjs_core_name)
@@ -568,11 +573,16 @@ ext {
   jalviewjsJ2sAltSettingsFileName = string("${jalviewDir}/${jalviewjs_j2s_alt_settings}")
   jalviewjsJ2sProps = null
   jalviewjsJ2sPlugin = jalviewjs_j2s_plugin
+  jalviewjsStderrLaunchFilename = "${jalviewjsSiteDir}/"+(file(jalviewjs_stderr_launch).getName())
 
   eclipseWorkspace = null
   eclipseBinary = string("")
   eclipseVersion = string("")
   eclipseDebug = false
+
+  jalviewjsChromiumUserDir = "${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}"
+  jalviewjsChromiumProfileDir = "${ext.jalviewjsChromiumUserDir}/${jalviewjs_chromium_profile_name}"
+
   // ENDEXT
 }
 
@@ -1735,9 +1745,24 @@ run.dependsOn compileJava
 compileTestJava.dependsOn compileJava
 
 
-ext.testsFailed = false
+
+test {
+  group = "Verification"
+  description = "Runs all testTaskN tasks)"
+
+  if (useClover) {
+    dependsOn cloverClasses
+  } else { //?
+    dependsOn testClasses
+  }
+
+  // not running tests in this task
+  exclude "**/*"
+}
 /* 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(",")
@@ -1749,15 +1774,51 @@ task testTask0(type: Test) {
 
 /* separated tests */
 task testTask1(type: Test) {
+  group = "Verification"
+  description = "Tests that need to be isolated from the main test run"
   useTestNG() {
     includeGroups name
     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
   }
 }
 
+task testTask2(type: Test) {
+  group = "Verification"
+  description = "Tests that need to be isolated from the main test run"
+  useTestNG() {
+    includeGroups name
+    excludeGroups testng_excluded_groups.split(",")
+    preserveOrder true
+    useDefaultListeners=true
+  }
+}
+task testTask3(type: Test) {
+  group = "Verification"
+  description = "Tests that need to be isolated from the main test run"
+  useTestNG() {
+    includeGroups name
+    excludeGroups testng_excluded_groups.split(",")
+    preserveOrder true
+    useDefaultListeners=true
+  }
+}
+
+/* insert more testTaskNs here -- change N to next digit or other string */
+/*
+task testTaskN(type: Test) {
+  group = "Verification"
+  description = "Tests that need to be isolated from the main test run"
+  useTestNG() {
+    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
@@ -1766,7 +1827,6 @@ task testTask1(type: Test) {
 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 ->
@@ -1775,7 +1835,7 @@ tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}.all { te
   if (useClover) {
     dependsOn cloverClasses
   } else { //?
-    dependsOn compileJava //?
+    dependsOn testClasses //?
   }
 
   // run main tests first
@@ -1783,44 +1843,36 @@ tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}.all { te
     testTask.mustRunAfter "testTask0"
 
   testTask.testLogging { logging ->
-    events TestLogEvent.FAILED,
-      TestLogEvent.SKIPPED,
-      TestLogEvent.STANDARD_OUT,
-      TestLogEvent.STANDARD_ERROR
+    events TestLogEvent.FAILED
+//      TestLogEvent.SKIPPED,
+//      TestLogEvent.STANDARD_OUT,
+//      TestLogEvent.STANDARD_ERROR
 
     exceptionFormat TestExceptionFormat.FULL
     showExceptions true
     showCauses true
     showStackTraces true
+    if (test_output) {
+      showStandardStreams true
+    }
+    info.events = [ TestLogEvent.FAILED ]
   }
 
+  if (OperatingSystem.current().isMacOsX()) {
+    testTask.systemProperty "apple.awt.UIElement", "true"
+    testTask.environment "JAVA_TOOL_OPTIONS", "-Dapple.awt.UIElement=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
 
-    if (desc.parent) return // Only summarize results for whole modules
-
-    String summary = "${testTask.project.name}:${testTask.name} results: ${result.resultType} " +
-      "(" +
-      "${result.testCount} tests, " +
-      "${result.successfulTestCount} successes, " +
-      "${result.failedTestCount} failures, " +
-      "${result.skippedTestCount} skipped" +
-      ") " +
-      "in ${TimeCategory.minus(new Date(result.endTime), new Date(result.startTime))}" +
-      "\n" +
-          "Report file: ${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 += summary
-    }
+    def resultsInfo = [testTask.project.name, testTask.name, result, TimeCategory.minus(new Date(result.endTime), new Date(result.startTime)), testTask.reports.html.entryPoint]
 
-    if (result.resultType == TestResult.ResultType.FAILURE) {
-      testsFailed = true
-    }
+    rootProject.ext.testsResults.add(resultsInfo)
   }
 
   // from original test task
@@ -1842,11 +1894,25 @@ tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}.all { te
   jvmArgs += additional_compiler_args
 
   doFirst {
+    // this is not perfect yet -- we should only add the commandLineIncludePatterns to the
+    // testTasks that include the tests, and exclude all from the others.
+    // get --test argument
+    filter.commandLineIncludePatterns = test.filter.commandLineIncludePatterns
+    // do something with testTask.getCandidateClassFiles() to see if the test should silently finish because of the
+    // commandLineIncludePatterns not matching anything.  Instead we are doing setFailOnNoMatchingTests(false) below
+
+
     if (useClover) {
       println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover")
     }
   }
 
+
+  /* don't fail on no matching tests (so --tests will run across all testTasks) */
+  testTask.filter.setFailOnNoMatchingTests(false)
+
+  /* ensure the "test" task dependsOn all the testTasks */
+  test.dependsOn testTask
 }
 
 gradle.buildFinished {
@@ -1854,47 +1920,117 @@ gradle.buildFinished {
 
     if (!allResults.isEmpty()) {
         printResults allResults
+        allResults.each {r ->
+          if (r[2].resultType == TestResult.ResultType.FAILURE)
+            throw new GradleException("Failed tests!")
+        }
     }
 }
 
-private static void printResults(allResults) {
-    def maxLength = allResults*.readLines().flatten().collect { it.length() }.max()
-
-    println "┌${"${"─" * maxLength}"}┐"
-
-    println allResults.collect {
-        it.readLines().collect {
-            "│" + it + " " * (maxLength - it.length()) + "│"
-        }.join("\n")
-    }.join("\n├${"${"─" * maxLength}"}┤\n")
-
-    println "└${"${"─" * maxLength}"}┘"
+private static String colString(styler, col, colour, text) {
+  return col?"${styler[colour](text)}":text
 }
-/* END of test tasks results summary */
 
-task verifyTestStatus {
-  doLast {
-    if (testsFailed) {
-      throw new GradleException("There were failing tests!")
+private static String getSummaryLine(s, pn, tn, rt, rc, rs, rf, rsk, t, col) {
+  def colour = 'black'
+  def text = rt
+  def nocol = false
+  if (rc == 0) {
+    text = "-----"
+    nocol = true
+  } else {
+    switch(rt) {
+      case TestResult.ResultType.SUCCESS:
+        colour = 'green'
+        break;
+      case TestResult.ResultType.FAILURE:
+        colour = 'red'
+        break;
+      default:
+        nocol = true
+        break;
     }
   }
+  StringBuilder sb = new StringBuilder()
+  sb.append("${pn}")
+  if (tn != null)
+    sb.append(":${tn}")
+  sb.append(" results: ")
+  sb.append(colString(s, col && !nocol, colour, text))
+  sb.append(" (")
+  sb.append("${rc} tests, ")
+  sb.append(colString(s, col && rs > 0, 'green', rs))
+  sb.append(" successes, ")
+  sb.append(colString(s, col && rf > 0, 'red', rf))
+  sb.append(" failures, ")
+  sb.append("${rsk} skipped) in ${t}")
+  return sb.toString()
 }
 
-test {
-  // from original test task
-  if (useClover) {
-    //dependsOn.clear()
-    dependsOn cloverClasses
-  } else { //?
-    dependsOn compileJava //?
-  }
-  dependsOn tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}
-  finalizedBy verifyTestStatus
+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 = []
+    def totalcount = 0
+    def totalsuccess = 0
+    def totalfail = 0
+    def totalskip = 0
+    def totaltime = TimeCategory.getSeconds(0)
+    // sort on project name then task name
+    allResults.sort {a, b -> a[0] == b[0]? a[1]<=>b[1]:a[0] <=> b[0]}.each {
+      def projectName = it[0]
+      def taskName = it[1]
+      def result = it[2]
+      def time = it[3]
+      def report = it[4]
+      def summaryCol = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, true)
+      def summaryPlain = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, false)
+      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)
+      failedTests |= result.resultType == TestResult.ResultType.FAILURE
+      totalcount += result.testCount
+      totalsuccess += result.successfulTestCount
+      totalfail += result.failedTestCount
+      totalskip += result.skippedTestCount
+      totaltime += time
+    }
+    def totalSummaryCol = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, true)
+    def totalSummaryPlain = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, false)
+    def tls = totalSummaryPlain.length()
+    if (tls > maxLength)
+      maxLength = tls
+    def info = [tls, totalSummaryCol, null]
+    summaryLines.add(info)
+
+    def allSummaries = []
+    for(sInfo : summaryLines) {
+      def ls = sInfo[0]
+      def summary = sInfo[1]
+      def report = sInfo[2]
+
+      StringBuilder sb = new StringBuilder()
+      sb.append("│" + summary + " " * (maxLength - ls) + "│")
+      if (report != null) {
+        sb.append("\n│" + report + " " * (maxLength - report.length()) + "│")
+      }
+      allSummaries += sb.toString()
+    }
 
-  // not running tests in this task
-  exclude "**/*"
+    println "┌${"${"─" * maxLength}"}┐"
+    println allSummaries.join("\n├${"${"─" * maxLength}"}┤\n")
+    println "└${"${"─" * maxLength}"}┘"
 }
+/* END of test tasks results summary */
 
 
 task compileLinkCheck(type: JavaCompile) {
@@ -2021,12 +2157,33 @@ shadowJar {
   if (buildDist) {
     dependsOn makeDist
   }
-  from ("${jalviewDir}/${libDistDir}") {
-    include("*.jar")
-  }
-  manifest {
-    attributes "Implementation-Version": JALVIEW_VERSION,
-    "Application-Name": applicationName
+
+  def jarFiles = fileTree(dir: "${jalviewDir}/${libDistDir}", include: "*.jar", exclude: "regex.jar").getFiles()
+  def groovyJars = jarFiles.findAll {it1 -> file(it1).getName().startsWith("groovy-swing")}
+  def otherJars = jarFiles.findAll {it2 -> !file(it2).getName().startsWith("groovy-swing")}
+  from groovyJars
+  from otherJars
+
+  // we need to include the groovy-swing Include-Package for it to run in the shadowJar
+  doFirst {
+    def jarFileManifests = []
+    groovyJars.each { jarFile ->
+      def mf = zipTree(jarFile).getFiles().find { it.getName().equals("MANIFEST.MF") }
+      if (mf != null) {
+        jarFileManifests += mf
+      }
+    }
+
+    manifest {
+      attributes "Implementation-Version": JALVIEW_VERSION, "Application-Name": applicationName
+      from (jarFileManifests) {
+        eachEntry { details ->
+          if (!details.key.equals("Import-Package")) {
+            details.exclude()
+          }
+        }
+      }
+    }
   }
 
   duplicatesStrategy "INCLUDE"
@@ -2084,9 +2241,9 @@ task getdownImages() {
   dependsOn getdownImagesProcess
 }
 
-task getdownWebsite() {
+task getdownWebsiteBuild() {
   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"
+  description = "Create the getdown minimal app folder, and website folder for this version of jalview. Website folder also used for offline app installer. No digest is created."
 
   dependsOn getdownImages
   if (buildDist) {
@@ -2138,8 +2295,8 @@ task getdownWebsite() {
       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_ui.icon", "${getdownImagesDir}/${getdown_icon}")
+      props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesDir}/${getdown_mac_dock_icon}")
     }
 
     props.put("getdown_txt_title", jalview_name)
@@ -2198,7 +2355,7 @@ task getdownWebsite() {
           from s
           into "${getdownAppBaseDir}/${getdown_wrapper_script_dir}"
         }
-        getdownTextLines += "resource = ${getdown_wrapper_script_dir}/${script}"
+        getdownTextLines += "xresource = ${getdown_wrapper_script_dir}/${script}"
       }
     }
 
@@ -2340,7 +2497,9 @@ task getdownDigestDir(type: JavaExec) {
 task getdownDigest(type: JavaExec) {
   group = "distribution"
   description = "Digest the getdown website folder"
-  dependsOn getdownWebsite
+
+  dependsOn getdownWebsiteBuild
+
   doFirst {
     classpath = files(getdownLauncher)
   }
@@ -2373,12 +2532,19 @@ task getdown() {
   }
 }
 
+task getdownWebsite {
+  group = "distribution"
+  description = "A task to create the whole getdown channel website dir including digest file"
+
+  dependsOn getdownWebsiteBuild
+  dependsOn getdownDigest
+}
 
 task getdownArchiveBuild() {
   group = "distribution"
   description = "Put files in the archive dir to go on the website"
 
-  dependsOn getdownWebsite
+  dependsOn getdownWebsiteBuild
 
   def v = "v${JALVIEW_VERSION_UNDERSCORES}"
   def vDir = "${getdownArchiveDir}/${v}"
@@ -2688,6 +2854,7 @@ task installerFiles(type: com.install4j.gradle.Install4jTask) {
     'WRAPPER_LINK': getdownWrapperLink,
     'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script,
     'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script,
+    'BATCH_WRAPPER_SCRIPT': getdown_batch_wrapper_script,
     'WRAPPER_SCRIPT_BIN_DIR': getdown_wrapper_script_dir,
     'INSTALLER_NAME': install4jInstallerName,
     'INSTALL4J_UTILS_DIR': install4j_utils_dir,
@@ -2887,6 +3054,7 @@ task sourceDist(type: Tar) {
   into project.name
 
   def EXCLUDE_FILES=[
+    "dist/*",
     "build/*",
     "bin/*",
     "test-output/",
@@ -3251,13 +3419,40 @@ task jalviewjsTransferUnzipSwingJs {
 
 
 task jalviewjsTransferUnzipLib {
-  def zipFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_libjs_dir}", include: "*.zip")
+  def zipFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_libjs_dir}", include: "*.zip").sort()
 
   doLast {
     zipFiles.each { file_zip -> 
       copy {
         from zipTree(file_zip)
         into "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
+
+        // rename files in jsmol/j2s/... to swingjs/j2s/...
+        if (file_zip.getName().startsWith("Jmol-j2s-site")) {
+          eachFile { fcd ->
+            // jsmol/... -> swingjs/...
+            def relPathSegments = fcd.relativePath.getSegments()
+            if (relPathSegments[0] == "jsmol") {
+              def newRelPathSegments = relPathSegments
+              newRelPathSegments[0] = "swingjs"
+              fcd.relativePath = new RelativePath(true, newRelPathSegments)
+            }
+          }
+        }
+
+        // The following replace() is needed due to a mismatch in Jmol calls to
+        // colorPtToFFRGB$javajs_util_T3d when only colorPtToFFRGB$javajs_util_T3 is defined
+        // in the SwingJS.zip (github or the one distributed with JSmol)
+        if (file_zip.getName().startsWith("Jmol-SwingJS")) {
+          filter { line ->
+            def l = ""
+            while(!line.equals(l)) {
+              line = line.replace('colorPtToFFRGB$javajs_util_T3d', 'colorPtToFFRGB$javajs_util_T3')
+              l = line
+            }
+            return line
+          }
+        }
       }
     }
   }
@@ -3268,8 +3463,8 @@ task jalviewjsTransferUnzipLib {
 
 
 task jalviewjsTransferUnzipAllLibs {
-  dependsOn jalviewjsTransferUnzipSwingJs
   dependsOn jalviewjsTransferUnzipLib
+  dependsOn jalviewjsTransferUnzipSwingJs
 }
 
 
@@ -3312,7 +3507,8 @@ task jalviewjsEclipseSetup {
 
 task jalviewjsSyncAllLibs (type: Sync) {
   dependsOn jalviewjsTransferUnzipAllLibs
-  def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteLibDir}")
+  def inputFiles = []
+  inputFiles += fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteLibDir}")
   inputFiles += fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}")
   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
 
@@ -3327,7 +3523,7 @@ task jalviewjsSyncAllLibs (type: Sync) {
     include "**"
   }
 
-  // should this be exclude really ?
+  // should this be exclude really ? No, swingjs dir should be transferred last (and overwrite)
   duplicatesStrategy "INCLUDE"
 
   outputs.files outputFiles
@@ -3519,6 +3715,71 @@ DEBUG: ${eclipseDebug}
 }
 
 
+task jalviewjsTranserSiteMergeLibDirs (type: Sync) {
+  dependsOn jalviewjsTransferUnzipAllLibs
+  dependsOn jalviewjsTransferUnzipSwingJs
+  dependsOn jalviewjsTranspile
+
+  def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteLibDir}")
+  // merge swingjs lib last
+  inputFiles += fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}")
+
+  def outputDir = "${jalviewDir}/${jalviewjsTransferSiteMergeDir}"
+
+  from inputFiles
+  into outputDir
+  def outputFiles = []
+  rename { filename ->
+    outputFiles += "${outputDir}/${filename}"
+    null
+  }
+
+  exclude "**/*.html"
+  exclude "**/*.htm"
+
+  // should this be exclude really ? No, swingjs dir should be transferred last (and overwrite)
+  duplicatesStrategy "INCLUDE"
+
+  outputs.files outputFiles
+  inputs.files inputFiles
+}
+
+
+task jalviewjsTranserSiteMergeSwingDir (type: Sync) {
+  dependsOn jalviewjsTransferUnzipAllLibs
+  dependsOn jalviewjsTransferUnzipSwingJs
+  dependsOn jalviewjsTranspile
+
+  // merge jalview files very last
+  def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteJsDir}")
+
+  def outputDir = "${jalviewDir}/${jalviewjsTransferSiteMergeDir}"
+
+  from inputFiles
+  into outputDir
+  def outputFiles = []
+  rename { filename ->
+    outputFiles += "${outputDir}/${filename}"
+    null
+  }
+  preserve {
+    include "**"
+  }
+
+  // should this be exclude really ? No, jalview dir should be transferred last (and overwrite)
+  duplicatesStrategy "INCLUDE"
+
+  outputs.files outputFiles
+  inputs.files inputFiles
+}
+
+
+task jalviewjsTranserSiteMergeDirs {
+  dependsOn jalviewjsTranserSiteMergeLibDirs
+  dependsOn jalviewjsTranserSiteMergeSwingDir
+}
+
+
 def jalviewjsCallCore(String name, FileCollection list, String prefixFile, String suffixFile, String jsfile, String zjsfile, File logOutFile, Boolean logOutConsole) {
 
   def stdout = new ByteArrayOutputStream()
@@ -3533,6 +3794,7 @@ def jalviewjsCallCore(String name, FileCollection list, String prefixFile, Strin
 
   def coreTop = file(prefixFile)
   def coreBottom = file(suffixFile)
+  def missingFiles = []
   coreFile.getParentFile().mkdirs()
   coreFile.createNewFile()
   coreFile.write( coreTop.getText("UTF-8") )
@@ -3546,6 +3808,7 @@ def jalviewjsCallCore(String name, FileCollection list, String prefixFile, Strin
       msg = "...file '"+f.getPath()+"' does not exist, skipping"
       println(msg)
       logOutFile.append(msg+"\n")
+      missingFiles += f
     }
   }
   coreFile.append( coreBottom.getText("UTF-8") )
@@ -3560,7 +3823,7 @@ def jalviewjsCallCore(String name, FileCollection list, String prefixFile, Strin
     classpath = files(["${jalviewDir}/${jalviewjs_closure_compiler}"])
     main = "com.google.javascript.jscomp.CommandLineRunner"
     jvmArgs = [ "-Dfile.encoding=UTF-8" ]
-    args = [ "--compilation_level", "SIMPLE_OPTIMIZATIONS", "--warning_level", "QUIET", "--charset", "UTF-8", "--js", jsfile, "--js_output_file", zjsfile ]
+    args = [ "--compilation_level", jalviewjs_closure_compiler_optimization_level, "--warning_level", "QUIET", "--charset", "UTF-8", "--js", jsfile, "--js_output_file", zjsfile ]
     maxHeapSize = "2g"
 
     msg = "\nRunning '"+commandLine.join(' ')+"'\n"
@@ -3588,6 +3851,11 @@ def jalviewjsCallCore(String name, FileCollection list, String prefixFile, Strin
     }
   }
   msg = "--"
+  if (missingFiles.size() > 0) {
+    msg += "\n!!! These files were listed but missing:\n"
+    missingFiles.each { file -> msg += "!!!  " + file.getPath() + "\n" }
+    msg = "--"
+  }
   println(msg)
   logOutFile.append(msg+"\n")
 }
@@ -3596,13 +3864,12 @@ def jalviewjsCallCore(String name, FileCollection list, String prefixFile, Strin
 task jalviewjsBuildAllCores {
   group "JalviewJS"
   description "Build the core js lib closures listed in the classlists dir"
-  dependsOn jalviewjsTranspile
-  dependsOn jalviewjsTransferUnzipSwingJs
+  dependsOn jalviewjsTranserSiteMergeDirs
 
-  def j2sDir = "${jalviewDir}/${jalviewjsTransferSiteJsDir}/${jalviewjs_j2s_subdir}"
-  def swingJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_j2s_subdir}"
-  def libJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteLibDir}/${jalviewjs_j2s_subdir}"
-  def jsDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_js_subdir}"
+  def j2sDir = "${jalviewDir}/${jalviewjsTransferSiteMergeDir}/${jalviewjs_j2s_subdir}"
+  def swingJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteMergeDir}/${jalviewjs_j2s_subdir}"
+  def libJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteMergeDir}/${jalviewjs_j2s_subdir}"
+  def jsDir = "${jalviewDir}/${jalviewjsTransferSiteMergeDir}/${jalviewjs_js_subdir}"
   def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}/${jalviewjs_j2s_subdir}/core"
   def prefixFile = "${jsDir}/core/coretop2.js"
   def suffixFile = "${jsDir}/core/corebottom2.js"
@@ -3663,19 +3930,6 @@ task jalviewjsBuildAllCores {
     outputs.file(zjsfile)
   }
   
-  // _stevesoft core. add any cores without a classlist here (and the inputs and outputs)
-  def stevesoftClasslistName = "_stevesoft"
-  def stevesoftClasslist = [
-    'jsfile': "${outputDir}/core${stevesoftClasslistName}.js",
-    'zjsfile': "${outputDir}/core${stevesoftClasslistName}.z.js",
-    'list': fileTree(dir: j2sDir, include: "com/stevesoft/pat/**/*.js"),
-    'name': stevesoftClasslistName
-  ]
-  jalviewjsCoreClasslists += stevesoftClasslist
-  inputs.files(stevesoftClasslist['list'])
-  outputs.file(stevesoftClasslist['jsfile'])
-  outputs.file(stevesoftClasslist['zjsfile'])
-
   // _all core
   def allClasslistName = "_all"
   def allJsFiles = fileTree(dir: j2sDir, include: "**/*.js")
@@ -3752,6 +4006,7 @@ def jalviewjsPublishCoreTemplate(String coreName, String templateName, File inpu
 
 task jalviewjsPublishCoreTemplates {
   dependsOn jalviewjsBuildAllCores
+
   def inputFileName = "${jalviewDir}/${j2s_coretemplate_html}"
   def inputFile = file(inputFileName)
   def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}"
@@ -3776,6 +4031,7 @@ task jalviewjsPublishCoreTemplates {
 task jalviewjsSyncCore (type: Sync) {
   dependsOn jalviewjsBuildAllCores
   dependsOn jalviewjsPublishCoreTemplates
+
   def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteCoreDir}")
   def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"
 
@@ -3795,8 +4051,18 @@ task jalviewjsSyncCore (type: Sync) {
 
 
 // this Copy version of TransferSiteJs will delete anything else in the target dir
+task jalviewjsCopyTransferSiteMergeDir(type: Copy) {
+  dependsOn jalviewjsTranserSiteMergeDirs
+
+  from "${jalviewDir}/${jalviewjsTransferSiteMergeDir}"
+  into "${jalviewDir}/${jalviewjsSiteDir}"
+}
+
+
+// this Copy version of TransferSiteJs will delete anything else in the target dir
 task jalviewjsCopyTransferSiteJs(type: Copy) {
   dependsOn jalviewjsTranspile
+
   from "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
   into "${jalviewDir}/${jalviewjsSiteDir}"
 }
@@ -3827,7 +4093,7 @@ jalviewjsSyncBuildProperties.mustRunAfter jalviewjsSyncTransferSiteJs
 task jalviewjsPrepareSite {
   group "JalviewJS"
   description "Prepares the website folder including unzipping files and copying resources"
-  dependsOn jalviewjsSyncAllLibs
+  //dependsOn jalviewjsSyncAllLibs // now using jalviewjsCopyTransferSiteMergeDir
   dependsOn jalviewjsSyncResources
   dependsOn jalviewjsSyncSiteResources
   dependsOn jalviewjsSyncBuildProperties
@@ -3838,7 +4104,7 @@ task jalviewjsPrepareSite {
 task jalviewjsBuildSite {
   group "JalviewJS"
   description "Builds the whole website including transpiled code"
-  dependsOn jalviewjsCopyTransferSiteJs
+  dependsOn jalviewjsCopyTransferSiteMergeDir
   dependsOn jalviewjsPrepareSite
 }
 
@@ -4101,9 +4367,159 @@ task eclipseAutoBuildTask {
 }
 
 
+task jalviewjsCopyStderrLaunchFile(type: Copy) {
+  from file(jalviewjs_stderr_launch)
+  into jalviewjsSiteDir
+
+  inputs.file jalviewjs_stderr_launch
+  outputs.file jalviewjsStderrLaunchFilename
+}
+
+task cleanJalviewjsChromiumUserDir {
+  doFirst {
+    delete jalviewjsChromiumUserDir
+  }
+  outputs.dir jalviewjsChromiumUserDir
+  // always run when depended on
+  outputs.upToDateWhen { !file(jalviewjsChromiumUserDir).exists() }
+}
+
+task jalviewjsChromiumProfile {
+  dependsOn cleanJalviewjsChromiumUserDir
+  mustRunAfter cleanJalviewjsChromiumUserDir
+
+  def firstRun = file("${jalviewjsChromiumUserDir}/First Run")
+
+  doFirst {
+    mkdir jalviewjsChromiumProfileDir
+    firstRun.text = ""
+  }
+  outputs.file firstRun
+}
+
+task jalviewjsLaunchTest {
+  group "Test"
+  description "Check JalviewJS opens in a browser"
+  dependsOn jalviewjsBuildSite
+  dependsOn jalviewjsCopyStderrLaunchFile
+  dependsOn jalviewjsChromiumProfile
+
+  def macOS = OperatingSystem.current().isMacOsX()
+  def chromiumBinary = macOS ? jalviewjs_macos_chromium_binary : jalviewjs_chromium_binary
+  if (chromiumBinary.startsWith("~/")) {
+    chromiumBinary = System.getProperty("user.home") + chromiumBinary.substring(1)
+  }
+  
+  def stdout
+  def stderr
+  doFirst {
+    def timeoutms = Integer.valueOf(jalviewjs_chromium_overall_timeout) * 1000
+    
+    def binary = file(chromiumBinary)
+    if (!binary.exists()) {
+      throw new StopExecutionException("Could not find chromium binary '${chromiumBinary}'. Cannot run task ${name}.")
+    }
+    stdout = new ByteArrayOutputStream()
+    stderr = new ByteArrayOutputStream()
+    def execStdout
+    def execStderr
+    if (jalviewjs_j2s_to_console.equals("true")) {
+      execStdout = new org.apache.tools.ant.util.TeeOutputStream(
+        stdout,
+        System.out)
+      execStderr = new org.apache.tools.ant.util.TeeOutputStream(
+        stderr,
+        System.err)
+    } else {
+      execStdout = stdout
+      execStderr = stderr
+    }
+    // macOS not running properly with timeout arguments
+    def execArgs = macOS ? [] : [
+      "--virtual-time-budget=${timeoutms}",
+    ]
+    execArgs += [
+      "--no-sandbox", // --no-sandbox IS USED BY THE THORIUM APPIMAGE ON THE BUILDSERVER
+      "--headless=new",
+      "--disable-gpu",
+      "--user-data-dir=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}",
+      "--profile-directory=${jalviewjs_chromium_profile_name}",
+      "--allow-file-access-from-files",
+      "--enable-logging=stderr",
+      "file://${jalviewDirAbsolutePath}/${jalviewjsStderrLaunchFilename}"
+    ]
+    
+    if (true || macOS) {
+      ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
+      Future f1 = executor.submit(
+        () -> {
+          exec {
+            standardOutput = execStdout
+            errorOutput = execStderr
+            executable(chromiumBinary)
+            args(execArgs)
+            println "COMMAND: '"+commandLine.join(" ")+"'"
+          }
+          executor.shutdownNow()
+        }
+      )
+
+      def noChangeBytes = 0
+      def noChangeIterations = 0
+      executor.scheduleAtFixedRate(
+        () -> {
+          String stderrString = stderr.toString()
+          // shutdown the task if we have a success string
+          if (stderrString.contains(jalviewjs_desktop_init_string)) {
+            f1.cancel()
+            Thread.sleep(1000)
+            executor.shutdownNow()
+          }
+          // if no change in stderr for 10s then also end
+          if (noChangeIterations >= jalviewjs_chromium_idle_timeout) {
+            executor.shutdownNow()
+          }
+          if (stderrString.length() == noChangeBytes) {
+            noChangeIterations++
+          } else {
+            noChangeBytes = stderrString.length()
+            noChangeIterations = 0
+          }
+        },
+        1, 1, TimeUnit.SECONDS)
+
+      executor.schedule(new Runnable(){
+        public void run(){
+          f1.cancel()
+          executor.shutdownNow()
+        }
+      }, timeoutms, TimeUnit.MILLISECONDS)
+
+      executor.awaitTermination(timeoutms+10000, TimeUnit.MILLISECONDS)
+      executor.shutdownNow()
+    }
+
+  }
+  
+  doLast {
+    def found = false
+    stderr.toString().eachLine { line ->
+      if (line.contains(jalviewjs_desktop_init_string)) {
+        println("Found line '"+line+"'")
+        found = true
+        return
+      }
+    }
+    if (!found) {
+      throw new GradleException("Could not find evidence of Desktop launch in JalviewJS.")
+    }
+  }
+}
+  
+
 task jalviewjs {
   group "JalviewJS"
-  description "Build the site"
+  description "Build the JalviewJS site and run the launch test"
   dependsOn jalviewjsBuildSite
+  dependsOn jalviewjsLaunchTest
 }
-