import groovy.json.JsonSlurper   // For parsing HTTP get responses
import java.net.URLConnection    // For control of the KMTronic power switch
import hudson.plugins.git.GitSCM // So that we can make scmPlus
import hudson.plugins.git.UserRemoteConfig // So that we can add master to the scm

// If you change the following line you need to change EXCLUDE_PATTERNS in Doxyfile to match
ubxlib_dir = "ubxlib"
working_dir = "_jenkins_work"
automation_subdir = "port/platform/common/automation"
scripts_subdir = "${automation_subdir}/scripts"
py_tasks_subdir = automation_subdir // The "tasks" dir used for PyInvoke is located in automation dir

default_summary_file = "summary.txt"
default_test_report_file = "test_report.xml"
default_debug_file = "debug.log"
default_build_sub_dir = "build"
// timeout_overall_seconds is just a default timeout for the entire script, allowing
// it to wait for agents to become available that might be Raspberry Pi's
// running 40-minute-long sets of tests.  The individual stages have their own
// timeouts (below) which are the guards against more prevalent dangers, for instance
// tests running forever or a board getting into a boot-loop
timeout_overall_seconds = 60 * 60 * 12
timeout_checkout_seconds = 60 * 5
timeout_build_seconds = 60 * 30 // Long because on first run tools may have to be installed
timeout_power_up_seconds = 60
timeout_flash_seconds = 60 * 30 // Long because on first run tools may have to be installed
timeout_test_seconds = 85 * 60 // Really don't want more than 3600 but testing is just beginning to edge over that
check_instance_max = 9
dut_start_time_seconds = 10

// Check for required environment variables
def environment_check() {
    def is_good = true;

    if ("${env.UBXLIB_LOCAL_STATE_DIR}" == "") {
        println "Environment variable UBXLIB_LOCAL_STATE_DIR not set, cannot continue."
        is_good = false
    }
    if ("${env.UBXLIB_COUNTER_FILE}" == "") {
        println "Environment variable UBXLIB_COUNTER_FILE not set, cannot continue."
        is_good = false
    }
    if ("${env.UBXLIB_TOKEN}" == "") {
        println "Environment variable UBXLIB_TOKEN not set, cannot continue."
        is_good = false
    }

    return is_good
}

def isCodeChecker(json_entry) {
    return json_entry.platform.toLowerCase().startsWith("codechecker:")
}

def isFirmware(json_entry) {
    def m = json_entry.mcu.toLowerCase()
    return !isCodeChecker(json_entry) && m != "win32" && m != "linux32" && m != "linux64" && m != ""
}

def hasUhubctl(json_entry) {
    def has = !isWindows(json_entry) && !isCodeChecker(json_entry)
    if (has && (sh(script: "uhubctl", returnStatus: true) != 0)) {
        has = false
    }
    return has
}

def isWindows(json_entry) {
    def m = json_entry.mcu.toLowerCase()
    return m == "win32"
}

// If a Windows or Linux MCU is specified then we do the building on the
// test machine so that the architectures match
def buildOnTest(json_entry) {
    def m = json_entry.mcu.toLowerCase()
    return m == "win32" || m == "linux32" || m == "linux64"
}

// Read a string representing a single integer value from a file and return
// it as an integer, 0 if there is no file or it does not contain a string
// which can be converted to an integer
def readIntegerFromFile(file_path) {
    def value_int = 0
    if (fileExists("${file_path}")) {
        try {
            def value_str = readFile(file: "${file_path}")
            value_int =  value_str as int
        } catch(e) {
        }
    }
    return value_int
}

// Write a single integer value to a file as a string
def writeIntegerToFile(file_path, value_int) {
    def value_str = value_int as String
    writeFile(file: "${file_path}", text: value_str)
}

// Acquire any shared resources
def sharedResourceAcquire() {
    powered_up = false

    lock("shared_resource") {
        node("built-in") { 
            ws("${env.UBXLIB_LOCAL_STATE_DIR}") {
                def counter = readIntegerFromFile("${env.UBXLIB_COUNTER_FILE}")
                if (counter == 0) {
                    build job: "ubxlib_shared_resource_control", parameters: [booleanParam(name: "ON_NOT_OFF", value: true)]
                    powered_up = true
                }
                counter++
                writeIntegerToFile("${env.UBXLIB_COUNTER_FILE}", counter)
            }
        }
    }

    return powered_up
}

// Release any shared resources
def sharedResourceRelease() {
    powered_down = false

    lock("shared_resource") {
        node("built-in") { 
            ws("${env.UBXLIB_LOCAL_STATE_DIR}") {
                def counter = readIntegerFromFile("${env.UBXLIB_COUNTER_FILE}")
                if (counter > 0) {
                    counter--
                }
                if (counter == 0) {
                    build job: "ubxlib_shared_resource_control", parameters: [booleanParam(name: "ON_NOT_OFF", value: false)]
                    powered_down = true
                }
                writeIntegerToFile("${env.UBXLIB_COUNTER_FILE}", counter)
            }
        }
    }

    return powered_down
}

// Helper function for converting an instance (e.g. [12,1]) to string (e.g. "12.1")
// Can't use join() on json structs so we need to do this manually
def instanceToStr(instance) {
    def str = ""
    for (entry in instance) {
        if (str.size() > 0) {
            str += "."
        }
        str += entry;
    }
    return str
}

// This is a helper function for executing commands inside the "ubxlib_builder" docker container.
// Jenkins does have its own support for running docker containers, however starting up a container
// using this method is very slow. So to speed up the build process we simply use "docker run"
// in this function instead.
def dockerCommand(cmd, relWorkspaceDir=".", extra_env=[]) {
    def volume_args = [
        "${env.HOME}:/home/ubxlib:rw",
        "${env.WORKSPACE}:${env.WORKSPACE}:rw"
    ].collect { "-v $it" }.join(' ')
    def env_args = ([
        "HOME=/home/ubxlib",
        "BUILD_DIR=${env.WORKSPACE}/_build",
        "TEST_DIR=${env.WORKSPACE}/_test",
        // Jenkins environment variables
        "BUILD_URL=${env.BUILD_URL}",
        "WORKSPACE=${env.WORKSPACE}"
    ] + extra_env).collect { "-e $it" }.join(' ')
    def id = sh(script: "id -u -n", returnStdout: true).trim()
    // Needs --privileged to access TTY devices (since there is no dialout group
    // in the docker image) and -v /dev:/dev to map the devices into the docker image
    // NET_ADMIN capability is added so that, for the case where we are running on
    // Linux inside the Docker image, the script can prevent Linux from using eth0
    // by mistake (rather than some PPP or whatever interface provided by the module
    // it is meant to be testing)
    sh "docker run -i -u ${id} --rm --privileged --cap-add NET_ADMIN -v /dev:/dev -w ${env.WORKSPACE}/$relWorkspaceDir $volume_args $env_args ubxlib_builder /bin/bash -c \"$cmd\""
}

// Perform a check-out stage
def stageCheckout() {
    stage("Checkout (${env.NODE_NAME})") {
        timeout(time: timeout_checkout_seconds, unit: 'SECONDS') {
            dir(ubxlib_dir) {
                checkout([
                    $class: 'GitSCM',
                    branches: scm.branches,
                    doGenerateSubmoduleConfigurations: false, 
                        extensions: [[
                          $class: 'SubmoduleOption', 
                          disableSubmodules: false, 
                          parentCredentials: true, 
                          recursiveSubmodules: true, 
                          reference: '', 
                          trackingSubmodules: false
                        ]], 
                    submoduleCfg: [], 
                    userRemoteConfigs: scm.userRemoteConfigs
                ])
            }
        }
    }
}

// This is the check pipeline for one instance
// It will check-out the code for and run the
// PC-based check stage for "check" instances
def checkPipeline(instance_str, json_entry, filter) {
    return {
        def summary_file = default_summary_file
        def debug_file = default_debug_file
        def instance_index = "instance_${json_entry.id[0]}"

        // Run checks on the build nodes since they are big and beefy
        node('ubxlib && linux && docker && build') {
            lock(instance_index) {
                stageCheckout()
                stage('Check') {
                    echo "Starting checks on ${env.NODE_NAME}"
                    def instance_workdir = "${working_dir}/${instance_str}"
                    def workspace_path = env.WORKSPACE
                    def test_report_arg = ""
                    try {
                        // Start the check
                        def abs_ubxlib_dir = "${workspace_path}/${ubxlib_dir}"
                        def abs_instance_workdir = "${workspace_path}/${instance_workdir}"
                        // Make sure to clean first
                        sh "rm -rf ${abs_instance_workdir}"
                        dir("${abs_instance_workdir}") {
                            ansiColor('xterm') {
                                // Check if there is a test filter
                                def filter_arg = filter ? "--filter \"${filter}\"" : ""
                                dockerCommand("inv -r ${abs_ubxlib_dir}/${py_tasks_subdir} automation.test --build-dir=. ${filter_arg} ${instance_str}", instance_workdir)
                            }
                        }
                    } finally {
                        // Store summary- and debug- files as artifacts
                        archiveArtifacts artifacts: "${instance_workdir}/**/${summary_file}, ${instance_workdir}/**/${debug_file}, ${instance_workdir}/**/*.hex, ${instance_workdir}/**/*.bin, ${instance_workdir}/**/*.elf, ${instance_workdir}/**/*.exe, ${instance_workdir}/**/*.map, ${instance_workdir}/**/analyze_html/*", allowEmptyArchive: true
                    }
                }
            }
        }
    }
}

// Return parallel pipelines for all of the "check" instances
def parallelCheck(json) {
    def parallel_pipelines = [:]
    for (json_entry in json.instances) {
        if (json_entry.id[0] <= check_instance_max) {
            def instance_str = instanceToStr(json_entry.id)
            def desc = json_entry.description.toLowerCase()
            if (desc != "reserved") { // Skip "reserved" instances
                def stage_name = "${instance_str} ${json_entry.description}"
                parallel_pipelines[stage_name] = checkPipeline(instance_str, json_entry, json.filter)
            }
        }
    }

    return parallel_pipelines
}

// This is the build and test pipeline for one instance.
// Note: I did try to find a way to define the build and test pipelines
// separately and then combine them programmatically but unfortunately my
// game of "guess the syntax" failed.
// The build part will be run on a node with "docker" label and use the
// "ubxlib_builder" docker image for building; the test part will be run
// on a node that reflects the required test instance.
// The firmware artifacts for each build will be stashed as "<instance>_fw"
// between the two parts, since they may be run on different machines.
def buildAndTestPipeline(instance_str, json_entry, filter) {
    return {
        def build_dir = "${working_dir}/${instance_str}/" + default_build_sub_dir
        def platform = json_entry.platform
        def summary_file = default_summary_file
        def test_report_file = default_test_report_file
        def debug_file = default_debug_file
        def instance_string = "instance_${json_entry.id[0]}"
        def extra_env = []

        // Create U_UBXLIB_DEFINES from the Jenkins master secrets and env variable
        // and add it to extra_env for dockerCommand() and directly to the environment
        // for the Windows bat command
        withCredentials([string(credentialsId: 'ubxlib_wifi_passkey', variable: 'WIFI_PASSKEY'),
                         string(credentialsId: 'ubxlib_cloud_locate_mqtt_password', variable: 'CLOUD_LOCATE_MQTT_PASSWORD'),
                         string(credentialsId: 'ubxlib_cell_locate_authentication_token', variable: 'CELL_LOCATE_AUTHENTICATION_TOKEN'),
                         string(credentialsId: 'ubxlib_assist_now_authentication_token', variable: 'ASSIST_NOW_AUTHENTICATION_TOKEN'),
                         string(credentialsId: 'ubxlib_google_maps_api_key', variable: 'GOOGLE_MAPS_API_KEY'),
                         string(credentialsId: 'ubxlib_skyhook_api_key', variable: 'SKYHOOK_API_KEY'),
                         string(credentialsId: 'ubxlib_here_api_key', variable: 'HERE_API_KEY')]) {
            def value = "${env.UBXLIB_EXTRA_DEFINES};U_WIFI_TEST_CFG_WPA2_PASSPHRASE=${WIFI_PASSKEY};U_CFG_APP_CLOUD_LOCATE_MQTT_PASSWORD=${CLOUD_LOCATE_MQTT_PASSWORD};U_CFG_APP_CELL_LOC_AUTHENTICATION_TOKEN=${CELL_LOCATE_AUTHENTICATION_TOKEN};U_CFG_APP_GNSS_ASSIST_NOW_AUTHENTICATION_TOKEN=${ASSIST_NOW_AUTHENTICATION_TOKEN};U_CFG_APP_GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY};U_CFG_APP_SKYHOOK_API_KEY=${SKYHOOK_API_KEY};U_CFG_APP_HERE_API_KEY=${HERE_API_KEY}"
            extra_env.add("U_UBXLIB_DEFINES=\"${value}\"")
            env.U_UBXLIB_DEFINES = value
        }

        if (!buildOnTest(json_entry)) {
            // The build part
            node("ubxlib && build && linux && docker") {
                stageCheckout()
                stage("Build ${platform}") {
                    timeout(time: timeout_build_seconds, unit: 'SECONDS') {
                        echo "Build dir: ${build_dir}"
                        def includes = [
                            "**/*.hex","**/*.bin","**/*.elf","**/*.map","**/*.out"
                        ].collect { "${build_dir}/$it" }.join(', ') // Add "${build_dir}/" prefix to all entries
                        def stash_includes = [
                            // Needed for Zephyr flashing using west command:
                            "**/CMakeCache.txt", "**/.config", "**/*.yaml",
                            // Needed for ESP-IDF flashing using esptool.py:
                            "**/flasher_args.json",
                            // Needed for PlatformIO:
                            "**/platformio.ini"
                        ].collect { "${build_dir}/$it" }.join(', ')
                        stash_includes += ", ${includes}"
                        def excludes = [
                            "**/*prebuilt*", "**/*libfibonacci*", "**/*hci_rpmsg*",
                            "**/isrList.bin", "**/*Determine*", "**/*partition-table*"
                        ].join(', ')
                        def stash_excludes = [
                            "**/*prebuilt*", "**/*libfibonacci*", "**/*hci_rpmsg*",
                            "**/isrList.bin", "**/*Determine*"
                        ].join(', ')

                        // Make sure to clean before build
                        dir("${build_dir}") {
                            // Make sure nothing is hanging over from a previous run
                            deleteDir()
                        }

                        // Check if there is a test filter
                        def filter_arg = filter ? "--filter \"${filter}\"" : ""

                        // Start building
                        dockerCommand("inv -r ${ubxlib_dir}/${py_tasks_subdir} -e automation.build --build-dir ${build_dir} ${filter_arg} ${instance_str}", ".", extra_env)
                        // Stash binaries so that they can be used on another machine in the Test stage
                        stash name: "${instance_str}_fw", includes: stash_includes, excludes: stash_excludes
                        archiveArtifacts artifacts: includes, excludes: excludes
                    }
                }
            }
        }

        // The test part
        node("ubxlib && ${instance_string}") {
            // Take one token: this is so that we can have a clean-up script
            // which takes the lot to prevent anything from starting while it
            // performs its work
            lock(label: "${env.UBXLIB_TOKEN}", quantity: 1) {
                // Lock the entire node for this bit as there will be one set of physical
                // HW attached and we don't want collisions
                lock("${env.NODE_NAME}_" + instance_string) {
                    def usb_on = false
                    def shared_resources_acquired = false
                    def workspace_path = env.WORKSPACE
                    def instance_workdir = "${working_dir}/${instance_str}"
                    def abs_ubxlib_dir = "${workspace_path}/${ubxlib_dir}"
                    def abs_instance_workdir = "${workspace_path}/${instance_workdir}"
                    try {
                        stageCheckout()
                        stage("Power-up") {
                            timeout(time: timeout_power_up_seconds, unit: 'SECONDS') {
                                if (sharedResourceAcquire()) {
                                    println "Powering-up shared resources."
                                } else {
                                    println "Shared resources already powered up."
                                }
                                shared_resources_acquired = true
                                if (hasUhubctl(json_entry)) {
                                    // This will be running on a Pi; need to remove any existing instance
                                    // of the Docker container first: it may be a zombie from a CTRL-C'ed
                                    // Jenkins run that has ownership of things we need
                                    def dockerContainers = sh(script: "docker container ps -aq -f ancestor=ubxlib_builder", returnStdout: true).replaceAll("\n", " ")
                                    if (dockerContainers) {
                                        sh "docker kill ${dockerContainers}"
                                    }
                                    println "Powering-up DUT on instance_${instance_str}."
                                    // Switch on USB
                                    sh(script: "uhubctl -a 1 -l 1-1")
                                    usb_on = true
                                    println "Waiting for DUT to connect (for which we allow ${dut_start_time_seconds} second(s))."
                                    sleep(time: dut_start_time_seconds, unit: 'SECONDS')
                                }
                            }
                        }
                        if (isFirmware(json_entry)) {
                            stage("Flash ${json_entry.mcu}") {
                                timeout(time: timeout_flash_seconds, unit: 'SECONDS') {
                                    dir("${abs_instance_workdir}") {
                                        // Make sure nothing is hanging over from a previous run
                                        deleteDir()
                                    }
                                    // Unstash files
                                    unstash name: "${instance_str}_fw"
                                    dir("${abs_instance_workdir}") {
                                        def flash_command = "inv -r ${abs_ubxlib_dir}/${py_tasks_subdir} -e automation.flash --build-dir=${default_build_sub_dir} ${instance_str}"
                                        retry(3) {
                                            if (isWindows(json_entry)) {
                                                bat flash_command
                                            } else {
                                                dockerCommand(flash_command, instance_workdir, extra_env)
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        stage('Test') {
                            timeout(time: timeout_test_seconds, unit: 'SECONDS') {
                                echo "Starting tests on ${instance_str} (${env.NODE_NAME})."
                                def test_report_arg = "--test-report ${test_report_file}"
                                try {
                                    // Start the test
                                    dir("${abs_instance_workdir}") {
                                        ansiColor('xterm') {
                                            // Check if there is a test filter
                                            def filter_arg = filter ? "--filter \"${filter}\"" : ""
                                            def test_command = "inv -r ${abs_ubxlib_dir}/${py_tasks_subdir} automation.test --build-dir=. ${test_report_arg} ${filter_arg} ${instance_str}"
                                            if (isWindows(json_entry)) {
                                                bat test_command
                                            } else {
                                                dockerCommand(test_command, instance_workdir, extra_env)
                                            }
                                        }
                                    }
                                } finally {
                                    // Store summary-, debug- and test report file as artifacts
                                    archiveArtifacts artifacts: "${instance_workdir}/**/${summary_file}, ${instance_workdir}/**/${test_report_file}, ${instance_workdir}/**/${debug_file}, ${instance_workdir}/**/*.hex, ${instance_workdir}/**/*.bin, ${instance_workdir}/**/*.elf, ${instance_workdir}/**/*.exe, ${instance_workdir}/**/*.map, ${instance_workdir}/*.kml,  ${instance_workdir}/*.log, ${instance_workdir}/**/analyze_html/*", allowEmptyArchive: true
                                    if (test_report_arg != "") {
                                        // Record the test results
                                        junit(testResults: "${working_dir}/${instance_str}/${test_report_file}", allowEmptyResults: true, skipOldReports: true, skipPublishingChecks: true)
                                        // The following print prevents any failure indication in the output being concatenated
                                        // with the junit operation (which it is almost always nothing to do with)
                                        println("Test stage completed.")
                                    }
                                }
                            }
                        }
                    } finally {
                        // Clean up
                        if (usb_on) {
                            // Make sure to switch off USB
                            // Following https://github.com/mvp/uhubctl#power-comes-back-on-after-few-seconds-on-linux,
                            // also do another few tricks to make some boxes which won't stay off, stay off
                            sh(script: "echo 0 > sudo tee /sys/bus/usb/devices/1-1.1/authorized")
                            sh(script: "echo 0 > sudo tee /sys/bus/usb/devices/1-1.2/authorized")
                            sh(script: "echo 0 > sudo tee /sys/bus/usb/devices/1-1.3/authorized")
                            sh(script: "echo 0 > sudo tee /sys/bus/usb/devices/1-1.4/authorized")
                            // Set return status to true so that it doesn't cause an exception if this instance doesn't
                            // happen to have a C030 board or an STM32F407 Discovery board or a Segger J-Link attached
                            sh(script: "sudo udisksctl power-off --block-device  /dev/disk/by-label/UBLOX-R412M", returnStatus : true)
                            sh(script: "sudo udisksctl power-off --block-device  /dev/disk/by-label/DIS_F407VG", returnStatus : true)
                            sh(script: "sudo udisksctl power-off --block-device  /dev/disk/by-label/JLINK", returnStatus : true)
                            sh(script: "uhubctl -a 0 -l 1-1 -r 100")
                        }
                        println "DUT attached to instance_${instance_str} (${env.NODE_NAME}) is powered down."
                        if (shared_resources_acquired) {
                            if (sharedResourceRelease()) {
                                println "Shared resources powered down."
                            } else {
                                println "Shared resources left on for other test instances."
                            }
                        }
                        // The following print prevents any failure indication in the output being concatenated
                        // with the clean-up operation, which it is nothing to do with
                        println("Test node completed.")
                    }
                }
            }
        }
    }
}

// Return parallel build and test pipelines for all instances
def parallelBuildAndTest(json) {
    def parallel_pipelines = [:]
    for (json_entry in json.instances) {
        if (json_entry.id[0] > check_instance_max) {
            def instance_str = instanceToStr(json_entry.id)
            // Deliberately add a space at the start of stage_name so that
            // these are sorted to the top; we're generally more interested
            // in them
            def stage_name = " ${instance_str} ${json_entry.mcu} (${json_entry.platform})"
            parallel_pipelines[stage_name] = buildAndTestPipeline(instance_str, json_entry, json.filter)
        }
    }

    return parallel_pipelines
}

// The main thing: distribute all of the above over all nodes
node('ubxlib && linux && distributor')
{
    def known_branches = [ 'master', 'development' ]
    def git_commit_text
    def changed_files

    // All the stages go here
    timeout(time: timeout_overall_seconds, unit: 'SECONDS') {
        stage("Info") {
            def environment = sh(script: "printenv", returnStdout: true).trim()
            def environment_good = true

            println "Environment variables are: \n" + environment

            // Do some checking
            if (!environment_check()) {
                currentBuild.result = 'ABORTED'
                error("Missing environment variable(s), aborting.")
            }

            // Uncomment the line below to print out the entries in the scm map
            // passed to us from the CloudBees magic
            // [this will trigger you to ask for additional script approvals in Jenkins]
            //println "scm contains: " + scm.properties.each{entry -> "$entry.key = $entry.value"} + "\n"
        }

        stage("Fetch") {
            timeout(time: timeout_checkout_seconds, unit: 'SECONDS') {
                dir(ubxlib_dir) {
                    // Create a modified version of the remote configs in scm to
                    // add known branches. With these fetched as well as the
                    // branch-under-test we can get the file difference between the two
                    println "Creating scmPlus to add fetch of known branches...\n"
                    refspec = scm.userRemoteConfigs[0].refspec
                    known_branches.each {
                        println "Add fetch of ${it}...\n"
                        if (!scm.userRemoteConfigs[0].refspec.contains("/${it}:")) {
                            refspec += " refs/heads/${it}:refs/remotes/origin/${it}"
                        }
                    }

                    userRemoteConfigPlus = new UserRemoteConfig(scm.userRemoteConfigs[0].url,
                                                                scm.userRemoteConfigs[0].name,
                                                                refspec,
                                                                scm.userRemoteConfigs[0].credentialsId)

                    scmPlus = new GitSCM([userRemoteConfigPlus],
                                         scm.branches,
                                         scm.doGenerateSubmoduleConfigurations,
                                         scm.submoduleCfg,
                                         scm.browser,
                                         scm.gitTool,
                                         scm.extensions)

                    // Uncomment the line below to print out scmPlus
                    // [this will trigger you to ask for additional script approvals in Jenkins]
                    // println "scmPlus contains: " + scmPlus.properties.each{entry -> "$entry.key = $entry.value"} + "\n"

                    // Get the code
                    println "checkout() branch \"" + scmPlus.branches[0] + "\" and also fetch known branches...\n"
                    scmMap = checkout scmPlus

                    // Recurse submodules
                    println "Recursing submodules..."
                    sh "git submodule update --init"

                    // For debug purposes, print out the entries in the scm
                    // map returned by checkout
                    println "checkout() returned: " + scmMap.each{entry -> "$entry.key = $entry.value"}

                    // Use git to get the last commit message
                    git_commit_text = sh(script: "git log -1 --pretty=%B", returnStdout: true)

                    if (git_commit_text) {
                        // Convert newlines to "/n" and remove marks to prevent them
                        // screwing up the command-line they get passed to lower down
                        git_commit_text = git_commit_text.replaceAll("\\n", "\\\\n").replaceAll("\"", "")
                        println "Last commit message was: \"" + git_commit_text + "\""
                    } else {
                        println "Unable to get last commit message."
                    }
                }
            }
        }

        dir(ubxlib_dir) {
            // Prefix each branch name with "origin/" and separate with " "
            branch_line = known_branches.collect { "origin/$it" }.join(' ')
            likely_branch = sh(script: "python3 ${scripts_subdir}/u_get_likely_base_branch.py --rev origin/${env.JOB_BASE_NAME} ${branch_line}", returnStdout: true).trim()
        }
        println "CHANGE_TARGET: ${env.CHANGE_TARGET}, CHANGE_ID: ${env.CHANGE_ID}"
        println "JOB_BASE_NAME: ${env.JOB_BASE_NAME}, likely_branch: ${likely_branch}"
        if ((env.CHANGE_TARGET && env.CHANGE_ID) || !env.JOB_BASE_NAME || (env.JOB_BASE_NAME == "master") || !likely_branch) {
            println "Either this is a pull request or we're on master or the branch name is not specified: all tests will be run."
        } else {
            // This is not master and not yet a pull request,
            // so we can save time by only running selected tests
            println "This is not master, checking which tests to run..."
            try {
                stage("Check Branch Differences") {
                    dir(ubxlib_dir) {
                        // No user direction so use git to get the list
                        // of file differences from main, @echo off to avoid
                        // capturing the command output as well as the  returned text
                        println "Checking for changed files between ${likely_branch} and ${env.JOB_BASE_NAME}..."
                        changed_files = sh(script: "git diff --name-only ${likely_branch}", returnStdout: true).trim()
                        changed_files = changed_files.replaceAll("\n", " ");
                        println "Changed file(s) were: " + changed_files
                    }
                }
            } catch (e) {
                println "Git was unable to determine the files that are different to master."
            }
        }

        stage("Test Selection") {
            // Fetch the test selection as JSON data
            // The JSON object also contains description, platform etc for each instance fetched from DATABASE.md
            def output = ''
            dir("${ubxlib_dir}/${py_tasks_subdir}") {
                // If changed_files is null we run everything
                def changed_files_arg = changed_files ? "--files \"${changed_files}\"" : ""
                def run_everything_arg = changed_files ? "" : "--run-everything"
                output = sh(script: "inv automation.get-test-selection ${changed_files_arg} ${run_everything_arg} --message \"${git_commit_text}\"", returnStdout: true).trim()
            }
            println output
            def jsonStr = output.split('JSON_DATA: ')[1]
            jsonObj = readJSON text: jsonStr
            println "Test filter string will be: " + jsonObj.filter
            def pretty_json = groovy.json.JsonOutput.prettyPrint(jsonObj.toString())
            println "Test selection JSON data: " + pretty_json
            if (jsonObj.filter instanceof net.sf.json.JSONNull) {
                // For some idiotic reason comparing a JSONNull with null will return false
                // For this reason we re-write JSONNull as null
                jsonObj.filter = null
            }
        }

        stage("Check & Build/Test") {
            // Now do everything at once, in parallel, all the time
            parallel parallelCheck(jsonObj) + parallelBuildAndTest(jsonObj)
        }
    }
}
