All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xbib.gradle.task.elasticsearch.test.AntFixture.groovy Maven / Gradle / Ivy

Go to download

Gradle plugins for the developer kit for building and testing Elasticsearch and Elasticsearch plugins

The newest version!
package org.xbib.gradle.task.elasticsearch.test

import org.gradle.api.GradleException
import org.gradle.api.Task
import org.gradle.api.tasks.Input
import org.xbib.gradle.task.elasticsearch.qa.AntTask

/**
 * A fixture for integration tests which runs in a separate process launched by Ant.
 */
class AntFixture extends AntTask implements Fixture {

    /** The path to the executable that starts the fixture. */
    @Input
    String executable

    private final List arguments = new ArrayList<>()

    @Input
    void args(Object... args) {
        arguments.addAll(args)
    }

    /**
     * Environment variables for the fixture process. The value can be any object, which
     * will have toString() called at execution time.
     */
    private final Map environment = new HashMap<>()

    @Input
    void env(String key, Object value) {
        environment.put(key, value)
    }

    /** A flag to indicate whether the command should be executed from a shell. */
    @Input
    boolean useShell = false

    /**
     * A flag to indicate whether the fixture should be run in the foreground, or spawned.
     * It is protected so subclasses can override (eg RunTask).
     */
    protected boolean spawn = true

    /**
     * A closure to call before the fixture is considered ready. The closure is passed the fixture object,
     * as well as a groovy AntBuilder, to enable running ant condition checks. The default wait
     * condition is for http on the http port.
     */
    Closure waitCondition = { AntFixture fixture, AntBuilder ant ->
        File tmpFile = new File(fixture.cwd, 'wait.success')
        ant.get(src: "http://${fixture.addressAndPort}",
                dest: tmpFile.toString(),
                ignoreerrors: true, // do not fail on error, so logging information can be flushed
                retries: 10)
        return tmpFile.exists()
    }

    private final Task stopTask

    AntFixture() {
        stopTask = createStopTask()
        finalizedBy(stopTask)
    }

    @Override
    Task getStopTask() {
        return stopTask
    }

    @Override
    protected void runAnt(AntBuilder ant) {
        project.delete(baseDir) // reset everything
        cwd.mkdirs()
        final String realExecutable
        final List realArgs = new ArrayList<>()
        final Map realEnv = environment
        // We need to choose which executable we are using. In shell mode, or when we
        // are spawning and thus using the wrapper script, the executable is the shell.
        if (useShell || spawn) {
            realExecutable = 'sh'
        } else {
            realExecutable = executable
            realArgs.addAll(arguments)
        }
        if (spawn) {
            writeWrapperScript(executable)
            realArgs.add(wrapperScript)
            realArgs.addAll(arguments)
        }
        commandString.eachLine { line -> logger.info(line) }

        ant.exec(executable: realExecutable, spawn: spawn, dir: cwd, taskname: name) {
            realEnv.each { key, value -> env(key: key, value: value) }
            realArgs.each { arg(value: it) }
        }

        String failedProp = "failed${name}"
        // first wait for resources, or the failure marker from the wrapper script
        ant.waitfor(maxwait: '30', maxwaitunit: 'second', checkevery: '500', checkeveryunit: 'millisecond', timeoutproperty: failedProp) {
            or {
                resourceexists {
                    file(file: failureMarker.toString())
                }
                and {
                    resourceexists {
                        file(file: pidFile.toString())
                    }
                    resourceexists {
                        file(file: portsFile.toString())
                    }
                }
            }
        }

        if (ant.project.getProperty(failedProp) || failureMarker.exists()) {
            fail("Failed to start ${name}")
        }

        // the process is started (has a pid) and is bound to a network interface
        // so now wait undil the waitCondition has been met
        // TODO: change this to a loop?
        boolean success = false
        try {
            success = waitCondition(this, ant) == false
        } catch (Exception e) {
            String msg = "Wait condition caught exception for ${name}"
            logger.error(msg, e)
            fail(msg, e)
        }
        if (!success) {
            fail("Wait condition failed for ${name}")
        }
    }

    /** Returns a debug string used to log information about how the fixture was run. */
    protected String getCommandString() {
        String commandString = "\n${name} configuration:\n"
        commandString += "-----------------------------------------\n"
        commandString += "  cwd: ${cwd}\n"
        commandString += "  command: ${executable} ${arguments.join(' ')}\n"
        commandString += '  environment:\n'
        environment.each { k, v -> commandString += "    ${k}: ${v}\n" }
        if (spawn) {
            commandString += "\n  [${wrapperScript.name}]\n"
            wrapperScript.eachLine('UTF-8', { line -> commandString += "    ${line}\n"})
        }
        return commandString
    }

    /**
     * Writes a script to run the real executable, so that stdout/stderr can be captured.
     * TODO: this could be removed if we do use our own ProcessBuilder and pump output from the process
     */
    private void writeWrapperScript(String executable) {
        wrapperScript.parentFile.mkdirs()
        String argsPasser = '"$@"'
        String exitMarker = "; if [ \$? != 0 ]; then touch run.failed; fi"
        wrapperScript.setText("\"${executable}\" ${argsPasser} > run.log 2>&1 ${exitMarker}", 'UTF-8')
    }

    /** Fail the build with the given message, and logging relevant info*/
    private void fail(String msg, Exception... suppressed) {
        if (!logger.isInfoEnabled()) {
            // We already log the command at info level. No need to do it twice.
            commandString.eachLine { line -> logger.error(line) }
        }
        logger.error("${name} output:")
        logger.error("-----------------------------------------")
        logger.error("  failure marker exists: ${failureMarker.exists()}")
        logger.error("  pid file exists: ${pidFile.exists()}")
        logger.error("  ports file exists: ${portsFile.exists()}")
        // also dump the log file for the startup script (which will include ES logging output to stdout)
        if (runLog.exists()) {
            logger.error("\n  [log]")
            runLog.eachLine { line -> logger.error("    ${line}") }
        }
        logger.error("-----------------------------------------")
        GradleException toThrow = new GradleException(msg)
        for (Exception e : suppressed) {
            toThrow.addSuppressed(e)
        }
        throw toThrow
    }

    /** Adds a task to kill an elasticsearch node with the given pidfile */
    private Task createStopTask() {
        final AntFixture fixture = this
        final Object pid = "${ -> fixture.pid }"
        Task stop = project.tasks.create(name: "${name}#stop", type: LoggedExec)
        stop.onlyIf { fixture.pidFile.exists() }
        stop.doFirst {
            logger.info("Shutting down ${fixture.name} with pid ${pid}")
        }
        stop.executable = 'kill'
        stop.args('-9', pid)
        stop.doLast {
            project.delete(fixture.pidFile)
        }
        return stop
    }

    /**
     * A path relative to the build dir that all configuration and runtime files
     * will live in for this fixture
     */
    protected File getBaseDir() {
        return new File(project.buildDir, "fixtures/${name}")
    }

    /** Returns the working directory for the process. Defaults to "cwd" inside baseDir. */
    protected File getCwd() {
        return new File(baseDir, 'cwd')
    }

    /** Returns the file the process writes its pid to. Defaults to "pid" inside baseDir. */
    protected File getPidFile() {
        return new File(baseDir, 'pid')
    }

    /** Reads the pid file and returns the process' pid */
    int getPid() {
        return Integer.parseInt(pidFile.getText('UTF-8').trim())
    }

    /** Returns the file the process writes its bound ports to. Defaults to "ports" inside baseDir. */
    protected File getPortsFile() {
        return new File(baseDir, 'ports')
    }

    /** Returns an address and port suitable for a uri to connect to this node over http */
    String getAddressAndPort() {
        return portsFile.readLines("UTF-8").get(0)
    }

    /** Returns a file that wraps around the actual command when {@code spawn == true}. */
    protected File getWrapperScript() {
        return new File(cwd, 'run')
    }

    /** Returns a file that the wrapper script writes when the command failed. */
    protected File getFailureMarker() {
        return new File(cwd, 'run.failed')
    }

    /** Returns a file that the wrapper script writes when the command failed. */
    protected File getRunLog() {
        return new File(cwd, 'run.log')
    }
}