/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt,
 * Yahoo! Inc., Tom Huybrechts, Olivier Lamy
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.jvnet.hudson.test;

import com.gargoylesoftware.css.parser.CSSErrorHandler;
import com.gargoylesoftware.css.parser.CSSException;
import com.gargoylesoftware.css.parser.CSSParseException;
import com.gargoylesoftware.htmlunit.AjaxController;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.DefaultCssErrorHandler;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebClientOptions;
import com.gargoylesoftware.htmlunit.WebClientUtil;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.WebResponseData;
import com.gargoylesoftware.htmlunit.WebResponseListener;
import com.gargoylesoftware.htmlunit.html.DomNode;
import com.gargoylesoftware.htmlunit.html.HtmlButton;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlElementUtil;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlImage;
import com.gargoylesoftware.htmlunit.html.HtmlInput;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.javascript.AbstractJavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.HtmlUnitContextFactory;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.host.xml.XMLHttpRequest;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import com.gargoylesoftware.htmlunit.util.WebResponseWrapper;
import com.gargoylesoftware.htmlunit.xml.XmlPage;
import com.google.common.net.HttpHeaders;
import hudson.ClassicPluginStrategy;
import hudson.CloseProofOutputStream;
import hudson.DNSMultiCast;
import hudson.DescriptorExtensionList;
import hudson.EnvVars;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.Functions;
import hudson.Launcher;
import hudson.Main;
import hudson.PluginManager;
import hudson.Util;
import hudson.WebAppMain;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.DownloadService;
import hudson.model.Executor;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.JDK;
import hudson.model.Label;
import hudson.model.Node;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.model.RootAction;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.model.UpdateSite;
import hudson.model.User;
import hudson.model.View;
import hudson.remoting.Which;
import hudson.security.ACL;
import hudson.security.AbstractPasswordBasedSecurityRealm;
import hudson.security.GroupDetails;
import hudson.security.csrf.CrumbIssuer;
import hudson.slaves.ComputerConnector;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.DumbSlave;
import hudson.slaves.OfflineCause;
import hudson.slaves.RetentionStrategy;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrapperDescriptor;
import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.tools.ToolProperty;
import hudson.util.PersistedList;
import hudson.util.ReflectionUtils;
import hudson.util.StreamTaskListener;
import hudson.util.jna.GNUCLibrary;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.management.ThreadInfo;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.jar.Manifest;
import java.util.logging.Filter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;

import jenkins.model.Jenkins;
import jenkins.model.JenkinsAdaptor;
import jenkins.model.JenkinsLocationConfiguration;
import jenkins.security.ApiTokenProperty;
import net.sf.json.JSONObject;
import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.UserStore;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.security.Password;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebXmlConfiguration;
import org.hamcrest.Matchers;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.*;
import org.junit.internal.AssumptionViolatedException;
import static org.junit.matchers.JUnitMatchers.containsString;
import org.junit.rules.MethodRule;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import com.gargoylesoftware.htmlunit.html.DomNodeUtil;
import com.gargoylesoftware.htmlunit.html.HtmlFormUtil;
import hudson.console.AnnotatedLargeText;
import hudson.init.InitMilestone;
import hudson.model.Job;
import hudson.model.Slave;
import hudson.model.queue.QueueTaskFuture;

import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import jenkins.model.ParameterizedJobMixIn;
import jenkins.security.MasterToSlaveCallable;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.Timeout;
import org.junit.runners.model.TestTimedOutException;
import org.jvnet.hudson.test.recipes.Recipe;
import org.jvnet.hudson.test.recipes.WithTimeout;
import org.jvnet.hudson.test.rhino.JavaScriptDebugger;
import org.kohsuke.stapler.ClassDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.Dispatcher;
import org.kohsuke.stapler.MetaClass;
import org.kohsuke.stapler.MetaClassLoader;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.mozilla.javascript.tools.debugger.Dim;
import org.mozilla.javascript.tools.shell.Global;
import org.springframework.dao.DataAccessException;
import org.xml.sax.SAXException;

/**
 * JUnit rule to allow test cases to fire up a Jenkins instance.
 *
 * @see <a href="http://wiki.jenkins-ci.org/display/JENKINS/Unit+Test">Wiki article about unit testing in Jenkins</a>
 * @author Stephen Connolly
 * @since 1.436
 * @see RestartableJenkinsRule
 */
@SuppressWarnings({"deprecation","rawtypes"})
public class JenkinsRule implements TestRule, MethodRule, RootAction {

    protected TestEnvironment env;

    protected Description testDescription;

    /**
     * Points to the same object as {@link #jenkins} does.
     */
    @Deprecated
    public Hudson hudson;

    public Jenkins jenkins;

    protected HudsonHomeLoader homeLoader = HudsonHomeLoader.NEW;
    /**
     * TCP/IP port that the server is listening on.
     */
    protected int localPort;
    protected Server server;

    /**
     * Where in the {@link Server} is Jenkins deployed?
     * <p>
     * Just like {@link javax.servlet.ServletContext#getContextPath()}, starts with '/' but doesn't end with '/'.
     * Unlike {@link WebClient#getContextPath} this is not a complete URL.
     */
    public String contextPath = "/jenkins";

    /**
     * {@link Runnable}s to be invoked at {@link #after()} .
     */
    protected List<LenientRunnable> tearDowns = new ArrayList<LenientRunnable>();

    protected List<JenkinsRecipe.Runner> recipes = new ArrayList<JenkinsRecipe.Runner>();

    /**
     * Remember {@link WebClient}s that are created, to release them properly.
     */
    private List<WebClient> clients = new ArrayList<WebClient>();

    /**
     * JavaScript "debugger" that provides you information about the JavaScript call stack
     * and the current values of the local variables in those stack frame.
     *
     * <p>
     * Unlike Java debugger, which you as a human interfaces directly and interactively,
     * this JavaScript debugger is to be interfaced by your program (or through the
     * expression evaluation capability of your Java debugger.)
     */
    protected JavaScriptDebugger jsDebugger = new JavaScriptDebugger();

    /**
     * If this test case has additional {@link org.jvnet.hudson.test.recipes.WithPlugin} annotations, set to true.
     * This will cause a fresh {@link hudson.PluginManager} to be created for this test.
     * Leaving this to false enables the test harness to use a pre-loaded plugin manager,
     * which runs faster.
     *
     * @deprecated
     *      Use {@link #pluginManager}
     */
    public boolean useLocalPluginManager;

    /**
     * Number of seconds until the test times out.
     * 
     * The {@link WithTimeout} rule can be used to specify this value per test.
     * 
     * In case of debugging session, the default timeout behavior is removed. Otherwise it's set to 3 minutes. 
     */
    public int timeout = Integer.getInteger("jenkins.test.timeout", new DisableOnDebug(null).isDebugging() ? 0 : 180);

    /**
     * Set the plugin manager to be passed to {@link Jenkins} constructor.
     *
     * For historical reasons, {@link #useLocalPluginManager}==true will take the precedence.
     */
    private PluginManager pluginManager = TestPluginManager.INSTANCE;

    public JenkinsComputerConnectorTester computerConnectorTester = new JenkinsComputerConnectorTester(this);

    private boolean origDefaultUseCache = true;

    private static final Charset UTF8 = Charset.forName("UTF-8");

    public Jenkins getInstance() {
        return jenkins;
    }

    /**
     * Override to set up your specific external resource.
     * @throws Throwable if setup fails (which will disable {@code after}
     */
    public void before() throws Throwable {
        for (Handler h : Logger.getLogger("").getHandlers()) {
            if (h instanceof ConsoleHandler) {
                ((ConsoleHandler) h).setFormatter(new DeltaSupportLogFormatter());
            }
        }

        if (Thread.interrupted()) { // JENKINS-30395
            LOGGER.warning("was interrupted before start");
        }

        if(Functions.isWindows()) {
            // JENKINS-4409.
            // URLConnection caches handles to jar files by default,
            // and it prevents delete temporary directories on Windows.
            // Disables caching here.
            // Though defaultUseCache is a static field,
            // its setter and getter are provided as instance methods.
            URLConnection aConnection = new File(".").toURI().toURL().openConnection();
            origDefaultUseCache = aConnection.getDefaultUseCaches();
            aConnection.setDefaultUseCaches(false);
        }
        
        // Not ideal (https://github.com/junit-team/junit/issues/116) but basically works.
        if (Boolean.getBoolean("ignore.random.failures")) {
            RandomlyFails rf = testDescription.getAnnotation(RandomlyFails.class);
            if (rf != null) {
                throw new AssumptionViolatedException("Known to randomly fail: " + rf.value());
            }
        }

        env = new TestEnvironment(testDescription);
        env.pin();
        recipe();
        AbstractProject.WORKSPACE.toString();
        User.clear();

        try {
            Field theInstance = Jenkins.class.getDeclaredField("theInstance");
            theInstance.setAccessible(true);
            if (theInstance.get(null) != null) {
                LOGGER.warning("Jenkins.theInstance was not cleared by a previous test, doing that now");
                theInstance.set(null, null);
            }
        } catch (Exception x) {
            LOGGER.log(Level.WARNING, null, x);
        }

        try {
            jenkins = hudson = newHudson();
            // If the initialization graph is corrupted, we cannot expect that Jenkins is in the good shape.
            // Likely it is an issue in @Initializer() definitions (see JENKINS-37759).
            // So we just fail the test.
            if (jenkins.getInitLevel() != InitMilestone.COMPLETED) {
                throw new Exception("Jenkins initialization has not reached the COMPLETED initialization stage. Current state is " + jenkins.getInitLevel() +
                        ". Likely there is an issue with the Initialization task graph (e.g. usage of @Initializer(after = InitMilestone.COMPLETED)). See JENKINS-37759 for more info");
            }
        } catch (Exception e) {
            // if Hudson instance fails to initialize, it leaves the instance field non-empty and break all the rest of the tests, so clean that up.
            Field f = Jenkins.class.getDeclaredField("theInstance");
            f.setAccessible(true);
            f.set(null,null);
            throw e;
        }

        jenkins.setCrumbIssuer(new TestCrumbIssuer());  // TODO: Move to _configureJenkinsForTest after JENKINS-55240
        _configureJenkinsForTest(jenkins);
        configureUpdateCenter();

        // expose the test instance as a part of URL tree.
        // this allows tests to use a part of the URL space for itself.
        jenkins.getActions().add(this);

        JenkinsLocationConfiguration.get().setUrl(getURL().toString());
    }

    /**
     * Configures a Jenkins instance for test.
     *
     * @param jenkins jenkins instance which has to be configured
     * @throws Exception if unable to configure
     * @since 2.50
     */
    public static void _configureJenkinsForTest(Jenkins jenkins) throws Exception {
        jenkins.setNoUsageStatistics(true); // collecting usage stats from tests is pointless.
        jenkins.servletContext.setAttribute("app", jenkins);
        jenkins.servletContext.setAttribute("version", "?");
        WebAppMain.installExpressionFactory(new ServletContextEvent(jenkins.servletContext));

        // set a default JDK to be the one that the harness is using.
        jenkins.getJDKs().add(new JDK("default", System.getProperty("java.home")));
    }
    
    private static void dumpThreads() {
        ThreadInfo[] threadInfos = Functions.getThreadInfos();
        Functions.ThreadGroupMap m = Functions.sortThreadsAndGetGroupMap(threadInfos);
        for (ThreadInfo ti : threadInfos) {
            System.err.println(Functions.dumpThreadInfo(ti, m));
        }
    }

    /**
     * Configures the update center setting for the test.
     * By default, we load updates from local proxy to avoid network traffic as much as possible.
     */
    protected void configureUpdateCenter() throws Exception {
        _configureUpdateCenter(jenkins);
    }

    /**
     * Internal method used to configure update center to avoid network traffic.
     * @param jenkins the Jenkins to configure
     * @since 2.50
     */
    public static void _configureUpdateCenter(Jenkins jenkins) throws Exception {
        final String updateCenterUrl;
        jettyLevel(Level.WARNING);
        try {
            updateCenterUrl = "http://localhost:"+ JavaNetReverseProxy.getInstance().localPort+"/update-center.json";
        } finally {
            jettyLevel(Level.INFO);
        }

        // don't waste bandwidth talking to the update center
        DownloadService.neverUpdate = true;
        UpdateSite.neverUpdate = true;

        PersistedList<UpdateSite> sites = jenkins.getUpdateCenter().getSites();
        sites.clear();
        sites.add(new UpdateSite("default", updateCenterUrl));
    }

    /**
     * Override to tear down your specific external resource.
     */
    public void after() throws Exception {
        try {
            if (jenkins!=null) {
                for (EndOfTestListener tl : jenkins.getExtensionList(EndOfTestListener.class))
                    tl.onTearDown();
            }

            // cancel pending asynchronous operations, although this doesn't really seem to be working
            for (WebClient client : clients) {
                // unload the page to cancel asynchronous operations
                try {
                    client.getPage("about:blank");
                } catch (IOException e) {
                    // ignore
                }
                client.close();
            }
            clients.clear();

        } finally {
            _stopJenkins(server, tearDowns, jenkins);

            try {
                env.dispose();
            } finally {
                // Hudson creates ClassLoaders for plugins that hold on to file descriptors of its jar files,
                // but because there's no explicit dispose method on ClassLoader, they won't get GC-ed until
                // at some later point, leading to possible file descriptor overflow. So encourage GC now.
                // see http://bugs.sun.com/view_bug.do?bug_id=4950148
                // TODO use URLClassLoader.close() in Java 7
                System.gc();

                // restore defaultUseCache
                if(Functions.isWindows()) {
                    URLConnection aConnection = new File(".").toURI().toURL().openConnection();
                    aConnection.setDefaultUseCaches(origDefaultUseCache);
                }
            }
        }
    }

    /**
     * Internal method to stop Jenkins instance.
     *
     * @param server    server on which Jenkins is running.
     * @param tearDowns tear down methods for tests
     * @param jenkins   the jenkins instance
     * @since 2.50
     */
    public static void _stopJenkins(Server server, List<LenientRunnable> tearDowns, Jenkins jenkins) {
        final RuntimeException exception = new RuntimeException("One or more problems while shutting down Jenkins");

        jettyLevel(Level.WARNING);
        try {
            server.stop();
        } catch (Exception e) {
            exception.addSuppressed(e);
        } finally {
            jettyLevel(Level.INFO);
        }

        if (tearDowns != null) {
            for (LenientRunnable r : tearDowns) {
                try {
                    r.run();
                } catch (Exception e) {
                    exception.addSuppressed(e);
                }
            }
        }

        if (jenkins != null)
            jenkins.cleanUp();
        ExtensionList.clearLegacyInstances();
        DescriptorExtensionList.clearLegacyInstances();

        if (exception.getSuppressed().length > 0) {
            throw exception;
        }
    }

    private static void jettyLevel(Level level) {
        Logger.getLogger("org.eclipse.jetty").setLevel(level);
    }

    /**
     * Backward compatibility with JUnit 4.8.
     */
    public Statement apply(Statement base, FrameworkMethod method, Object target) {
        return apply(base,Description.createTestDescription(method.getMethod().getDeclaringClass(), method.getName(), method.getAnnotations()));
    }

    public Statement apply(final Statement base, final Description description) {
        if (description.getAnnotation(WithoutJenkins.class) != null) {
            // request has been made to not create the instance for this test method
            return base;
        }
        Statement wrapped = new Statement() {
            @Override
            public void evaluate() throws Throwable {
                testDescription = description;
                Thread t = Thread.currentThread();
                String o = t.getName();
                t.setName("Executing "+ testDescription.getDisplayName());
                System.out.println("=== Starting " + testDescription.getDisplayName());
                before();
                try {
                    // so that test code has all the access to the system
                    ACL.impersonate(ACL.SYSTEM);
                    try {
                        base.evaluate();
                    } catch (Throwable th) {
                        // allow the late attachment of a debugger in case of a failure. Useful
                        // for diagnosing a rare failure
                        try {
                            throw new BreakException();
                        } catch (BreakException e) {}

                        RandomlyFails rf = testDescription.getAnnotation(RandomlyFails.class);
                        if (rf != null) {
                            System.err.println("Note: known to randomly fail: " + rf.value());
                        }

                        throw th;
                    }
                } finally {
                    after();
                    testDescription = null;
                    t.setName(o);
                }
            }
        };
        final int testTimeout = getTestTimeoutOverride(description);
        if (testTimeout <= 0) {
            System.out.println("Test timeout disabled.");
            return wrapped;
        } else {
            final Statement timeoutStatement = Timeout.seconds(testTimeout).apply(wrapped, description);
            return new Statement() {
                @Override
                public void evaluate() throws Throwable {
                    try {
                        timeoutStatement.evaluate();
                    } catch (TestTimedOutException x) {
                        // withLookingForStuckThread does not work well; better to just have a full thread dump.
                        LOGGER.warning(String.format("Test timed out (after %d seconds).", testTimeout));
                        dumpThreads();
                        throw x;
                    }
                }
            };
        }
    }

    private int getTestTimeoutOverride(Description description) {
        WithTimeout withTimeout = description.getAnnotation(WithTimeout.class);
        return withTimeout != null ? withTimeout.value(): this.timeout;
    }

    @SuppressWarnings("serial")
    public static class BreakException extends Exception {}

    public String getIconFileName() {
        return null;
    }

    public String getDisplayName() {
        return null;
    }

    public String getUrlName() {
        return "self";
    }

    /**
     * Creates a new instance of {@link jenkins.model.Jenkins}. If the derived class wants to create it in a different way,
     * you can override it.
     */
    protected Hudson newHudson() throws Exception {
        jettyLevel(Level.WARNING);
        ServletContext webServer = createWebServer();
        File home = homeLoader.allocate();
        for (JenkinsRecipe.Runner r : recipes)
            r.decorateHome(this,home);
        try {
            return new Hudson(home, webServer, getPluginManager());
        } catch (InterruptedException x) {
            throw new AssumptionViolatedException("Jenkins startup interrupted", x);
        } finally {
            jettyLevel(Level.INFO);
        }
    }

    public PluginManager getPluginManager() {
        if (jenkins == null) {
            return useLocalPluginManager ? null : pluginManager;
        } else {
            return jenkins.getPluginManager();
        }
    }

    /**
     * Sets the {@link PluginManager} to be used when creating a new {@link Jenkins} instance.
     *
     * @param pluginManager
     *      null to let Jenkins create a new instance of default plugin manager, like it normally does when running as a webapp outside the test.
     */
    public void setPluginManager(PluginManager pluginManager) {
        this.useLocalPluginManager = false;
        this.pluginManager = pluginManager;
        if (jenkins!=null)
            throw new IllegalStateException("Too late to override the plugin manager");
    }

    public JenkinsRule with(PluginManager pluginManager) {
        setPluginManager(pluginManager);
        return this;
    }

    public File getWebAppRoot() throws Exception {
        return WarExploder.getExplodedDir();
    }

    /**
     * Prepares a webapp hosting environment to get {@link javax.servlet.ServletContext} implementation
     * that we need for testing.
     */
    protected ServletContext createWebServer() throws Exception {
        return createWebServer(null);
    }

    /**
     * Prepares a webapp hosting environment to get {@link javax.servlet.ServletContext} implementation
     * that we need for testing.
     * 
     * @param contextAndServerConsumer configures the {@link WebAppContext} and the {@link Server} for the instance, before they are started
     * @since 2.63
     */
    protected ServletContext createWebServer(@CheckForNull BiConsumer<WebAppContext, Server> contextAndServerConsumer) throws Exception {
        ImmutablePair<Server,  ServletContext> results = _createWebServer(contextPath,
                (x) -> localPort = x, getClass().getClassLoader(), localPort, this::configureUserRealm, contextAndServerConsumer);
        server = results.left;
        LOGGER.log(Level.INFO, "Running on {0}", getURL());
        return results.right;
    }

    /**
     * Creates a web server on which Jenkins can run
     *
     * @param contextPath          the context path at which to put Jenkins
     * @param portSetter           the port on which the server runs will be set using this function
     * @param classLoader          the class loader for the {@link WebAppContext}
     * @param localPort            port on which the server runs
     * @param loginServiceSupplier configures the {@link LoginService} for the instance
     * @return ImmutablePair consisting of the {@link Server} and the {@link ServletContext}
     * @since 2.50
     */
    public static ImmutablePair<Server, ServletContext> _createWebServer(String contextPath, Consumer<Integer> portSetter,
                                                                         ClassLoader classLoader, int localPort,
                                                                         Supplier<LoginService> loginServiceSupplier)
            throws Exception {
        return _createWebServer(contextPath, portSetter, classLoader, localPort, loginServiceSupplier, null);
    }
    /**
     * Creates a web server on which Jenkins can run
     *
     * @param contextPath              the context path at which to put Jenkins
     * @param portSetter               the port on which the server runs will be set using this function
     * @param classLoader              the class loader for the {@link WebAppContext}
     * @param localPort                port on which the server runs
     * @param loginServiceSupplier     configures the {@link LoginService} for the instance
     * @param contextAndServerConsumer configures the {@link WebAppContext} and the {@link Server} for the instance, before they are started
     * @return ImmutablePair consisting of the {@link Server} and the {@link ServletContext}
     * @since 2.50
     */
    public static ImmutablePair<Server, ServletContext> _createWebServer(String contextPath, Consumer<Integer> portSetter,
                                                                         ClassLoader classLoader, int localPort,
                                                                         Supplier<LoginService> loginServiceSupplier,
                                                                         @CheckForNull BiConsumer<WebAppContext, Server> contextAndServerConsumer)
            throws Exception {
        QueuedThreadPool qtp = new QueuedThreadPool();
        qtp.setName("Jetty (JenkinsRule)");
        Server server = new Server(qtp);

        WebAppContext context = new WebAppContext(WarExploder.getExplodedDir().getPath(), contextPath);
        context.setClassLoader(classLoader);
        context.setConfigurations(new Configuration[]{new WebXmlConfiguration()});
        context.addBean(new NoListenerConfiguration(context));
        server.setHandler(context);
        context.setMimeTypes(MIME_TYPES);
        context.getSecurityHandler().setLoginService(loginServiceSupplier.get());
        context.setResourceBase(WarExploder.getExplodedDir().getPath());

        ServerConnector connector = new ServerConnector(server);
        HttpConfiguration config = connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration();
        // use a bigger buffer as Stapler traces can get pretty large on deeply nested URL
        config.setRequestHeaderSize(12 * 1024);
        connector.setHost("localhost");
        if (System.getProperty("port") != null) {
            connector.setPort(Integer.parseInt(System.getProperty("port")));
        } else if (localPort != 0) {
            connector.setPort(localPort);
        }

        server.addConnector(connector);
        if (contextAndServerConsumer != null) {
            contextAndServerConsumer.accept(context, server);
        }
        server.start();

        portSetter.accept(connector.getLocalPort());

        ServletContext servletContext =  context.getServletContext();
        return new ImmutablePair<>(server, servletContext);
    }

    /**
     * Configures a security realm for a test.
     */
    protected LoginService configureUserRealm() {
        return _configureUserRealm();
    }

    /**
     * Creates a {@link HashLoginService} with three users: alice, bob and charlie
     *
     * The password is same as the username
     * @return a new login service
     * @since 2.50
     */
    public static LoginService _configureUserRealm() {
        HashLoginService realm = new HashLoginService();
        realm.setName("default");   // this is the magic realm name to make it effective on everywhere
        UserStore userStore = new UserStore();
        realm.setUserStore( userStore );
        userStore.addUser("alice", new Password("alice"), new String[]{"user","female"});
        userStore.addUser("bob", new Password("bob"), new String[]{"user","male"});
        userStore.addUser("charlie", new Password("charlie"), new String[]{"user","male"});

        return realm;
    }

//
// Convenience methods
//

    /**
     * Creates a new job.
     *
     * @param type Top level item type.
     * @param name Item name.
     *
     * @throws IllegalArgumentException if the project of the given name already exists.
     */
    public <T extends TopLevelItem> T createProject(Class<T> type, String name) throws IOException {
        return jenkins.createProject(type, name);
    }

    /**
     * Creates a new job with an unique name.
     *
     * @param type Top level item type.
     */
    public <T extends TopLevelItem> T createProject(Class<T> type) throws IOException {
        return jenkins.createProject(type, createUniqueProjectName());
    }

    public FreeStyleProject createFreeStyleProject() throws IOException {
        return createFreeStyleProject(createUniqueProjectName());
    }

    public FreeStyleProject createFreeStyleProject(String name) throws IOException {
        return createProject(FreeStyleProject.class, name);
    }

    /**
     * Creates a simple folder that other jobs can be placed in.
     * @since 1.494
     */
    public MockFolder createFolder(String name) throws IOException {
        return createProject(MockFolder.class, name);
    }

    protected String createUniqueProjectName() {
        return "test"+jenkins.getItems().size();
    }

    /**
     * Creates {@link hudson.Launcher.LocalLauncher}. Useful for launching processes.
     */
    public Launcher.LocalLauncher createLocalLauncher() {
        return new Launcher.LocalLauncher(StreamTaskListener.fromStdout());
    }

    /**
     * Allocates a new temporary directory for the duration of this test.
     * @deprecated Use {@link TemporaryFolder} instead.
     */
    @Deprecated
    public File createTmpDir() throws IOException {
        return env.temporaryDirectoryAllocator.allocate();
    }

    public DumbSlave createSlave(boolean waitForChannelConnect) throws Exception {
        DumbSlave slave = createSlave();
        if (waitForChannelConnect) {
            long start = System.currentTimeMillis();
            while (slave.getChannel() == null) {
                if (System.currentTimeMillis() > (start + 10000)) {
                    throw new IllegalStateException("Timed out waiting on DumbSlave channel to connect.");
                }
                Thread.sleep(200);
            }
        }
        return slave;
    }

    public void disconnectSlave(DumbSlave slave) throws Exception {
        slave.getComputer().disconnect(new OfflineCause.ChannelTermination(new Exception("terminate")));
        long start = System.currentTimeMillis();
        while (slave.getChannel() != null) {
            if (System.currentTimeMillis() > (start + 10000)) {
                throw new IllegalStateException("Timed out waiting on DumbSlave channel to disconnect.");
            }
            Thread.sleep(200);
        }
    }

    public DumbSlave createSlave() throws Exception {
        return createSlave("",null);
    }

    /**
     * Creates and launches a new slave on the local host.
     */
    public DumbSlave createSlave(Label l) throws Exception {
    	return createSlave(l, null);
    }

    /**
     * Creates a test {@link hudson.security.SecurityRealm} that recognizes username==password as valid.
     * @see MockAuthorizationStrategy
     */
    public DummySecurityRealm createDummySecurityRealm() {
        return new DummySecurityRealm();
    }

    /** @see #createDummySecurityRealm */
    public static class DummySecurityRealm extends AbstractPasswordBasedSecurityRealm {

        private final Map<String,Set<String>> groupsByUser = new HashMap<String,Set<String>>();

        DummySecurityRealm() {}

        @Override
        protected UserDetails authenticate(String username, String password) throws AuthenticationException {
            if (username.equals(password))
                return loadUserByUsername(username);
            throw new BadCredentialsException(username);
        }

        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException,
                DataAccessException {
            List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
            auths.add(AUTHENTICATED_AUTHORITY);
            Set<String> groups = groupsByUser.get(username);
            if (groups != null) {
                for (String g : groups) {
                    auths.add(new GrantedAuthorityImpl(g));
                }
            }
            return new org.acegisecurity.userdetails.User(username,"",true,true,true,true, auths.toArray(new GrantedAuthority[0]));
        }

        @Override
        public GroupDetails loadGroupByGroupname(final String groupname) throws UsernameNotFoundException, DataAccessException {
            for (Set<String> groups : groupsByUser.values()) {
                if (groups.contains(groupname)) {
                    return new GroupDetails() {
                        @Override
                        public String getName() {
                            return groupname;
                        }
                    };
                }
            }
            throw new UsernameNotFoundException(groupname);
        }

        /** Associate some groups with a username. */
        public void addGroups(String username, String... groups) {
            Set<String> gs = groupsByUser.get(username);
            if (gs == null) {
                groupsByUser.put(username, gs = new TreeSet<String>());
            }
            gs.addAll(Arrays.asList(groups));
        }

    }

    /**
     * Returns the URL of the webapp top page.
     * URL ends with '/'.
     */
    public URL getURL() throws IOException {
        return new URL("http://localhost:"+localPort+contextPath+"/");
    }

    public DumbSlave createSlave(EnvVars env) throws Exception {
        return createSlave("",env);
    }

    public DumbSlave createSlave(Label l, EnvVars env) throws Exception {
        return createSlave(l==null ? null : l.getExpression(), env);
    }

    /**
     * Creates a slave with certain additional environment variables
     */
    public DumbSlave createSlave(String labels, EnvVars env) throws Exception {
        synchronized (jenkins) {
            int sz = jenkins.getNodes().size();
            return createSlave("slave" + sz,labels,env);
    	}
    }

    public DumbSlave createSlave(String nodeName, String labels, EnvVars env) throws Exception {
        synchronized (jenkins) {
            DumbSlave slave = new DumbSlave(nodeName, "dummy",
    				createTmpDir().getPath(), "1", Node.Mode.NORMAL, labels==null?"":labels, createComputerLauncher(env), RetentionStrategy.NOOP, Collections.EMPTY_LIST);                        
    		jenkins.addNode(slave);
    		return slave;
    	}
    }

    public PretendSlave createPretendSlave(FakeLauncher faker) throws Exception {
        synchronized (jenkins) {
            int sz = jenkins.getNodes().size();
            PretendSlave slave = new PretendSlave("slave" + sz, createTmpDir().getPath(), "", createComputerLauncher(null), faker);
    		jenkins.addNode(slave);
    		return slave;
        }
    }

    /**
     * Creates a launcher for starting a local agent.
     *
     * @param env
     *      Environment variables to add to the slave process. Can be null.
     */
    public ComputerLauncher createComputerLauncher(EnvVars env) throws URISyntaxException, IOException {
        int sz = jenkins.getNodes().size();
        return new SimpleCommandLauncher(
                String.format("\"%s/bin/java\" %s %s -jar \"%s\"",
                        System.getProperty("java.home"),
                        SLAVE_DEBUG_PORT>0 ? " -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address="+(SLAVE_DEBUG_PORT+sz): "",
                        "-Djava.awt.headless=true",
                        new File(jenkins.getJnlpJars("slave.jar").getURL().toURI()).getAbsolutePath()),
                env);
    }

    /**
     * Create a new slave on the local host and wait for it to come online
     * before returning.
     */
    public DumbSlave createOnlineSlave() throws Exception {
        return createOnlineSlave(null);
    }

    /**
     * Create a new slave on the local host and wait for it to come online
     * before returning.
     */
    public DumbSlave createOnlineSlave(Label l) throws Exception {
        return createOnlineSlave(l, null);
    }

    /**
     * Create a new slave on the local host and wait for it to come online
     * before returning
     * @see #waitOnline
     */
    @SuppressWarnings({"deprecation"})
    public DumbSlave createOnlineSlave(Label l, EnvVars env) throws Exception {
        DumbSlave s = createSlave(l, env);
        waitOnline(s);
        return s;
    }

    /**
     * Waits for a newly created slave to come online.
     * @see #createSlave()
     */
    public void waitOnline(Slave s) throws Exception {
        Computer computer = s.toComputer();
        AtomicBoolean run = new AtomicBoolean(true);
        AnnotatedLargeText<?> logText = computer.getLogText();
        Computer.threadPoolForRemoting.submit(() -> {
            long pos = 0;
            while (run.get() && !logText.isComplete()) {
                pos = logText.writeLogTo(pos, System.out);
                Thread.sleep(100);
            }
            return null;
        });
        try {
            if (s.getLauncher().isLaunchSupported()) {
                computer.connect(false).get();
            } else {
                while (!computer.isOnline()) {
                    Thread.sleep(100);
                }
            }
        } finally {
            run.set(false);
        }
    }

    /**
     * Same as {@link #showAgentLogs(Slave, Map)} but taking a preconfigured list of loggers as a convenience.
     */
    public void showAgentLogs(Slave s, LoggerRule loggerRule) throws Exception {
        showAgentLogs(s, loggerRule.getRecordedLevels());
    }

    /**
     * Forward agent logs to standard error of the test process.
     * Otherwise log messages would be sent only to {@link Computer#getLogText} etc.,
     * or discarded entirely (if below {@link Level#INFO}).
     * @param s an <em>online</em> agent
     * @param loggers {@link Logger#getName} tied to log level
     */
    public void showAgentLogs(Slave s, Map<String, Level> loggers) throws Exception {
        s.getChannel().call(new RemoteLogDumper(s.getNodeName(), loggers));
    }

    private static final class RemoteLogDumper extends MasterToSlaveCallable<Void, RuntimeException> {
        private final String name;
        private final Map<String, Level> loggers;
        private final TaskListener stderr = StreamTaskListener.fromStderr();
        private final long start = DeltaSupportLogFormatter.start;
        @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
        private static final List<Logger> loggerReferences = new LinkedList<>();
        RemoteLogDumper(String name, Map<String, Level> loggers) {
            this.name = name;
            this.loggers = loggers;
        }
        @Override public Void call() throws RuntimeException {
            Handler handler = new Handler() {
                final Formatter formatter = new DeltaSupportLogFormatter();
                @Override public void publish(LogRecord record) {
                    if (isLoggable(record)) {
                        stderr.getLogger().print(formatter.format(record).replaceAll("(?m)^([ 0-9.]*)", "$1[" + name + "] "));
                        stderr.getLogger().flush();
                    }
                }
                @Override public void flush() {}
                @Override public void close() throws SecurityException {}
            };
            handler.setLevel(Level.ALL);
            loggers.entrySet().forEach(e -> {
                Logger logger = Logger.getLogger(e.getKey());
                logger.setLevel(e.getValue());
                logger.addHandler(handler);
                loggerReferences.add(logger);
            });
            DeltaSupportLogFormatter.start = start; // match clock time on master
            stderr.getLogger().println("Set up log dumper on " + name + ": " + loggers);
            stderr.getLogger().flush();
            return null;
        }
    }

    /**
     * Blocks until the ENTER key is hit.
     * This is useful during debugging a test so that one can inspect the state of Hudson through the web browser.
     */
    public void interactiveBreak() throws Exception {
        System.out.println("Jenkins is running at " + getURL());
        new BufferedReader(new InputStreamReader(System.in)).readLine();
    }

    /**
     * Returns the last item in the list.
     */
    public <T> T last(List<T> items) {
        return items.get(items.size()-1);
    }

    /**
     * Pauses the execution until ENTER is hit in the console.
     * <p>
     * This is often very useful so that you can interact with Hudson
     * from an browser, while developing a test case.
     */
    public void pause() throws IOException {
        new BufferedReader(new InputStreamReader(System.in)).readLine();
    }

    /**
     * Performs a search from the search box.
     */
    public Page search(String q) throws Exception {
        return new WebClient().search(q);
    }

    /**
     * Get JSON from A Jenkins endpoint.
     * @param path The endpoint URL.
     * @return The JSON.
     */
    public JSONWebResponse getJSON(@Nonnull String path) throws IOException {
        assert !path.startsWith("/");

        JenkinsRule.WebClient webClient = createWebClient();
        Page runsPage = null;
        try {
            runsPage = webClient.goTo(path, "application/json");
        } catch (SAXException e) {
            // goTo shouldn't be throwing a SAXException for JSON.
            throw new IllegalStateException("Unexpected SAXException.", e);
        }
        WebResponse webResponse = runsPage.getWebResponse();

        return new JSONWebResponse(webResponse);
    }

    /**
     * POST a JSON payload to a URL on the underlying Jenkins instance.
     * @param path The url path on Jenkins.
     * @param json An object that produces a JSON string from it's {@code toString} method.
     * @return A JSON response.
     * @throws IOException
     * @throws SAXException
     */
    public JSONWebResponse postJSON(@Nonnull String path, @Nonnull Object json) throws IOException, SAXException {
        assert !path.startsWith("/");

        URL postUrl = new URL(getURL().toExternalForm() + path);
        HttpURLConnection conn = (HttpURLConnection) postUrl.openConnection();

        conn.setDoOutput(true);
        long startTime = System.currentTimeMillis();

        try {
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");

            NameValuePair crumb = getCrumbHeaderNVP();
            conn.setRequestProperty(crumb.getName(), crumb.getValue());

            byte[] content = json.toString().getBytes(UTF8);
            conn.setRequestProperty("Content-Length", String.valueOf(content.length));
            try (OutputStream os = conn.getOutputStream()) {
                os.write(content);
                os.flush();
            }

            WebResponseData webResponseData;
            InputStream responseStream = conn.getInputStream();
            try {
                if (responseStream != null) {
                    byte[] bytes = IOUtils.toByteArray(responseStream);
                    webResponseData = new WebResponseData(bytes, conn.getResponseCode(), conn.getResponseMessage(), extractHeaders(conn));
                } else {
                    webResponseData = new WebResponseData(new byte[0], conn.getResponseCode(), conn.getResponseMessage(), extractHeaders(conn));
                }
            } finally {
                IOUtils.closeQuietly(responseStream);
            }

            WebResponse webResponse = new WebResponse(webResponseData, postUrl, HttpMethod.POST, (System.currentTimeMillis() - startTime));

            return new JSONWebResponse(webResponse);
        } finally {
            conn.disconnect();
        }
    }

    private List<NameValuePair> extractHeaders(HttpURLConnection conn) {
        List<NameValuePair> headers = new ArrayList<NameValuePair>();
        Set<Map.Entry<String,List<String>>> headerFields = conn.getHeaderFields().entrySet();
        for (Map.Entry<String,List<String>> headerField : headerFields) {
            String name = headerField.getKey();
            if (name != null) { // Yes, the header name can be null.
                List<String> values = headerField.getValue();
                for (String value : values) {
                    if (value != null) {
                        headers.add(new NameValuePair(name, value));
                    }
                }
            }
        }
        return headers;
    }

    /**
     * Convenience wrapper for JSON responses.
     */
    public static class JSONWebResponse extends WebResponseWrapper {

        public JSONWebResponse(WebResponse webResponse) throws IllegalArgumentException {
            super(webResponse);
        }

        public JSONObject getJSONObject() {
            String json = getContentAsString();
            return JSONObject.fromObject(json);
        }
    }

    /**
     * Hits the Hudson system configuration and submits without any modification.
     */
    public void configRoundtrip() throws Exception {
        submit(createWebClient().goTo("configure").getFormByName("config"));
    }

    /**
     * Loads a configuration page and submits it without any modifications, to
     * perform a round-trip configuration test.
     * <p>
     * See http://wiki.jenkins-ci.org/display/JENKINS/Unit+Test#UnitTest-Configurationroundtriptesting
     */
    public <P extends Item> P configRoundtrip(P job) throws Exception {
        submit(createWebClient().getPage(job, "configure").getFormByName("config"));
        return job;
    }

    /**
     * Performs a configuration round-trip testing for a builder.
     */
    public <B extends Builder> B configRoundtrip(B before) throws Exception {
        FreeStyleProject p = createFreeStyleProject();
        p.getBuildersList().add(before);
        configRoundtrip((Item)p);
        return (B)p.getBuildersList().get(before.getClass());
    }

    /**
     * Performs a configuration round-trip testing for a publisher.
     */
    public <P extends Publisher> P configRoundtrip(P before) throws Exception {
        FreeStyleProject p = createFreeStyleProject();
        p.getPublishersList().add(before);
        configRoundtrip((Item) p);
        return (P)p.getPublishersList().get(before.getClass());
    }

    public <C extends ComputerConnector> C configRoundtrip(C before) throws Exception {
        computerConnectorTester.connector = before;
        submit(createWebClient().goTo("self/computerConnectorTester/configure").getFormByName("config"));
        return (C)computerConnectorTester.connector;
    }

    public User configRoundtrip(User u) throws Exception {
        submit(createWebClient().goTo(u.getUrl()+"/configure").getFormByName("config"));
        return u;
    }

    public <N extends Node> N configRoundtrip(N node) throws Exception {
        submit(createWebClient().goTo("computer/" + node.getNodeName() + "/configure").getFormByName("config"));
        return (N)jenkins.getNode(node.getNodeName());
    }

    public <V extends View> V configRoundtrip(V view) throws Exception {
        submit(createWebClient().getPage(view, "configure").getFormByName("viewConfig"));
        return view;
    }


    /**
     * Asserts that the outcome of the build is a specific outcome.
     */
    public <R extends Run> R assertBuildStatus(Result status, R r) throws Exception {
        if(status==r.getResult())
            return r;

        // dump the build output in failure message (in case BuildWatcher is not being used)
        String msg = "unexpected build status; build log was:\n------\n" + getLog(r) + "\n------\n";
        assertThat(msg, r.getResult(), is(status));
        return r;
    }

    public <R extends Run> R assertBuildStatus(Result status, Future<? extends R> r) throws Exception {
        assertThat("build was actually scheduled", r, Matchers.notNullValue());
        return assertBuildStatus(status, r.get());
    }

    /** Determines whether the specified HTTP status code is generally "good" */
    public boolean isGoodHttpStatus(int status) {
        if ((400 <= status) && (status <= 417)) {
            return false;
        }
        if ((500 <= status) && (status <= 505)) {
            return false;
        }
        return true;
    }

    /** Assert that the specified page can be served with a "good" HTTP status,
     * eg, the page is not missing and can be served without a server error
     * @param page
     */
    public void assertGoodStatus(Page page) {
        assertThat(isGoodHttpStatus(page.getWebResponse().getStatusCode()), is(true));
    }


    public <R extends Run> R assertBuildStatusSuccess(R r) throws Exception {
        assertBuildStatus(Result.SUCCESS, r);
        return r;
    }

    public <R extends Run> R assertBuildStatusSuccess(Future<? extends R> r) throws Exception {
        return assertBuildStatus(Result.SUCCESS, r);
    }

    @Nonnull
    public <J extends Job<J,R> & ParameterizedJobMixIn.ParameterizedJob<J,R>,R extends Run<J,R> & Queue.Executable> R buildAndAssertSuccess(@Nonnull J job) throws Exception {
        return buildAndAssertStatus(Result.SUCCESS, job);
    }

    /**
     * Runs specified job and asserts that in finished with given build result.
     * @since TODO
     */
    @Nonnull
    public <J extends Job<J,R> & ParameterizedJobMixIn.ParameterizedJob<J,R>,R extends Run<J,R> & Queue.Executable> R buildAndAssertStatus(@Nonnull Result status, @Nonnull J job) throws Exception {
        final QueueTaskFuture<R> f = new ParameterizedJobMixIn<J, R>() {
            @Override protected J asJob() {
                return job;
            }
        }.scheduleBuild2(0);
        return assertBuildStatus(status, f);
    }

    /**
     * Avoids need for cumbersome {@code this.<J,R>buildAndAssertSuccess(...)} type hints under JDK 7 javac (and supposedly also IntelliJ).
     */
    @Nonnull
    public FreeStyleBuild buildAndAssertSuccess(@Nonnull FreeStyleProject job) throws Exception {
        return assertBuildStatusSuccess(job.scheduleBuild2(0));
    }

    /**
     * Asserts that the console output of the build contains the given substring.
     */
    public void assertLogContains(String substring, Run run) throws IOException {
        assertThat(getLog(run), containsString(substring));
    }

    /**
     * Asserts that the console output of the build does not contain the given substring.
     */
    public void assertLogNotContains(String substring, Run run) throws IOException {
        assertThat(getLog(run), not(containsString(substring)));
    }

    /**
     * Get entire log file as plain text.
     * {@link Run#getLog()} is deprecated for reasons that are irrelevant in tests,
     * and also does not strip console annotations which are a distraction in test output.
     */
    public static String getLog(Run run) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            run.getLogText().writeLogTo(0, baos);
        } catch (FileNotFoundException x) {
            return ""; // log file not yet created, OK
        }
        return baos.toString(run.getCharset().name());
    }

    /**
     * Waits for a build to complete.
     * Useful in conjunction with {@link BuildWatcher}.
     * @return the same build, once done
     * @since 1.607
     */
    public <R extends Run<?,?>> R waitForCompletion(R r) throws InterruptedException {
        // Could be using com.jayway.awaitility:awaitility but it seems like overkill here.
        while (r.isLogUpdated()) {
            Thread.sleep(100);
        }
        return r;
    }

    /**
     * Waits for a build log to contain a specified string.
     * Useful in conjunction with {@link BuildWatcher}.
     * @return the same build, once it does
     * @since 1.607
     */
    public <R extends Run<?,?>> R waitForMessage(String message, R r) throws IOException, InterruptedException {
        while (!getLog(r).contains(message)) {
            if (!r.isLogUpdated()) {
                assertLogContains(message, r); // should now fail
            }
            Thread.sleep(100);
        }
        return r;
    }

    /**
     * Asserts that the XPath matches.
     */
    public void assertXPath(HtmlPage page, String xpath) {
        HtmlElement documentElement = page.getDocumentElement();
        assertNotNull("There should be an object that matches XPath:" + xpath,
                DomNodeUtil.selectSingleNode(documentElement, xpath));
    }

    /** Asserts that the XPath matches the contents of a DomNode page. This
     * variant of assertXPath(HtmlPage page, String xpath) allows us to
     * examine XmlPages.
     * @param page
     * @param xpath
     */
    public void assertXPath(DomNode page, String xpath) {
        List<? extends Object> nodes = page.getByXPath(xpath);
        assertThat("There should be an object that matches XPath:" + xpath, nodes.isEmpty(), is(false));
    }

    public void assertXPathValue(DomNode page, String xpath, String expectedValue) {
        Object node = page.getFirstByXPath(xpath);
        assertNotNull("no node found", node);
        assertTrue("the found object was not a Node " + xpath, node instanceof org.w3c.dom.Node);

        org.w3c.dom.Node n = (org.w3c.dom.Node) node;
        String textString = n.getTextContent();
        assertEquals("xpath value should match for " + xpath, expectedValue, textString);
    }

    public void assertXPathValueContains(DomNode page, String xpath, String needle) {
        Object node = page.getFirstByXPath(xpath);
        assertNotNull("no node found", node);
        assertTrue("the found object was not a Node " + xpath, node instanceof org.w3c.dom.Node);

        org.w3c.dom.Node n = (org.w3c.dom.Node) node;
        String textString = n.getTextContent();
        assertTrue("needle found in haystack", textString.contains(needle));
    }

    public void assertXPathResultsContainText(DomNode page, String xpath, String needle) {
        List<? extends Object> nodes = page.getByXPath(xpath);
        assertThat("no nodes matching xpath found", nodes.isEmpty(), is(false));
        boolean found = false;
        for (Object o : nodes) {
            if (o instanceof org.w3c.dom.Node) {
                org.w3c.dom.Node n = (org.w3c.dom.Node) o;
                String textString = n.getTextContent();
                if ((textString != null) && textString.contains(needle)) {
                    found = true;
                    break;
                }
            }
        }
        assertThat("needle found in haystack", found, is(true));
    }


    /**
     * Makes sure that all the images in the page loads successfully.
     * (By default, HtmlUnit doesn't load images.)
     */
    public void assertAllImageLoadSuccessfully(HtmlPage p) {
        for (HtmlImage img : DomNodeUtil.<HtmlImage>selectNodes(p, "//IMG")) {
            try {
                assertEquals("Failed to load " + img.getSrcAttribute(),
                        200,
                        img.getWebResponse(true).getStatusCode());
            } catch (IOException e) {
                throw new AssertionError("Failed to load " + img.getSrcAttribute());
            }
        }
    }


    public void assertStringContains(String message, String haystack, String needle) {
        assertThat(message, haystack, Matchers.containsString(needle));
    }

    public void assertStringContains(String haystack, String needle) {
        assertThat(haystack, Matchers.containsString(needle));
    }

    /**
     * Asserts that help files exist for the specified properties of the given instance.
     *
     * @param type
     *      The describable class type that should have the associated help files.
     * @param properties
     *      ','-separated list of properties whose help files should exist.
     */
    public void assertHelpExists(final Class<? extends Describable> type, final String properties) throws Exception {
        executeOnServer(new Callable<Object>() {
            public Object call() throws Exception {
                Descriptor d = jenkins.getDescriptor(type);
                WebClient wc = createWebClient();
                for (String property : listProperties(properties)) {
                    String url = d.getHelpFile(property);
                    assertThat("Help file for the property " + property + " is missing on " + type, url,
                            Matchers.notNullValue());
                    wc.goTo(url); // make sure it successfully loads
                }
                return null;
            }
        });
    }

    /**
     * Tokenizes "foo,bar,zot,-bar" and returns "foo,zot" (the token that starts with '-' is handled as
     * a cancellation.
     */
    private List<String> listProperties(String properties) {
        List<String> props = new CopyOnWriteArrayList<>(properties.split(","));
        for (String p : props) {
            if (p.startsWith("-")) {
                props.remove(p);
                props.remove(p.substring(1));
            }
        }
        return props;
    }

    /**
     * Submits the form.
     *
     * Plain {@link HtmlForm#submit(com.gargoylesoftware.htmlunit.html.SubmittableElement)} doesn't work correctly due to the use of YUI in Hudson.
     */
    public HtmlPage submit(HtmlForm form) throws Exception {
        return (HtmlPage) HtmlFormUtil.submit(form);
    }

    /**
     * Submits the form by clicking the submit button of the given name.
     *
     * @param name
     *      This corresponds to the @name of {@code <f:submit />}
     */
    public HtmlPage submit(HtmlForm form, String name) throws Exception {
        for( HtmlElement e : form.getElementsByTagName("button")) {
            HtmlElement p = (HtmlElement)e.getParentNode().getParentNode();                        
            if (p.getAttribute("name").equals(name) && HtmlElementUtil.hasClassName(p, "yui-submit-button")) {
                // For YUI handled submit buttons, just do a click.
                return (HtmlPage) HtmlElementUtil.click(e);
            } else if (e.getAttribute("name").equals(name)) {
                return (HtmlPage) HtmlFormUtil.submit(form, e);
            }
        }
        throw new AssertionError("No such submit button with the name "+name);
    }

    public HtmlInput findPreviousInputElement(HtmlElement current, String name) {
        return DomNodeUtil.selectSingleNode(current, "(preceding::input[@name='_."+name+"'])[last()]");
    }

    public HtmlButton getButtonByCaption(HtmlForm f, String s) {
        for (HtmlElement b : f.getElementsByTagName("button")) {
            if(b.getTextContent().trim().equals(s))
                return (HtmlButton)b;
        }
        return null;
    }

    /**
     * Creates a {@link TaskListener} connected to stdout.
     */
    public TaskListener createTaskListener() {
        return new StreamTaskListener(new CloseProofOutputStream(System.out));
    }

    /**
     * Asserts that two JavaBeans are equal as far as the given list of properties are concerned.
     *
     * <p>
     * This method takes two objects that have properties (getXyz, isXyz, or just the public xyz field),
     * and makes sure that the property values for each given property are equals (by using {@link org.junit.Assert#assertThat(Object, org.hamcrest.Matcher)})
     *
     * <p>
     * Property values can be null on both objects, and that is OK, but passing in a property that doesn't
     * exist will fail an assertion.
     *
     * <p>
     * This method is very convenient for comparing a large number of properties on two objects,
     * for example to verify that the configuration is identical after a config screen roundtrip.
     *
     * @param lhs
     *      One of the two objects to be compared.
     * @param rhs
     *      The other object to be compared
     * @param properties
     *      ','-separated list of property names that are compared.
     * @since 1.297
     */
    public void assertEqualBeans(Object lhs, Object rhs, String properties) throws Exception {
        assertThat("LHS", lhs, notNullValue());
        assertThat("RHS", rhs, notNullValue());
        for (String p : properties.split(",")) {
            PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(lhs, p);
            Object lp,rp;
            if(pd==null) {
                // field?
                try {
                    Field f = lhs.getClass().getField(p);
                    lp = f.get(lhs);
                    rp = f.get(rhs);
                } catch (NoSuchFieldException e) {
                    assertThat("No such property " + p + " on " + lhs.getClass(), pd, notNullValue());
                    return;
                }
            } else {
                lp = PropertyUtils.getProperty(lhs, p);
                rp = PropertyUtils.getProperty(rhs, p);
            }

            if (lp!=null && rp!=null && lp.getClass().isArray() && rp.getClass().isArray()) {
                // deep array equality comparison
                int m = Array.getLength(lp);
                int n = Array.getLength(rp);
                assertThat("Array length is different for property " + p, n, is(m));
                for (int i=0; i<m; i++)
                    assertThat(p + "[" + i + "] is different", Array.get(rp, i), is(Array.get(lp,i)));
                return;
            }

            assertThat("Property " + p + " is different", rp, is(lp));
        }
    }

    public void setQuietPeriod(int qp) {
        JenkinsAdaptor.setQuietPeriod(jenkins, qp);
    }

    /**
     * Works like {@link #assertEqualBeans(Object, Object, String)} but figure out the properties
     * via {@link org.kohsuke.stapler.DataBoundConstructor} and {@link org.kohsuke.stapler.DataBoundSetter}
     */
    public void assertEqualDataBoundBeans(Object lhs, Object rhs) throws Exception {
        if (lhs==null && rhs==null)     return;
        if (lhs==null)      fail("lhs is null while rhs="+rhs);
        if (rhs==null)      fail("rhs is null while lhs="+lhs);

        Constructor<?> lc = findDataBoundConstructor(lhs.getClass());
        Constructor<?> rc = findDataBoundConstructor(rhs.getClass());
        assertThat("Data bound constructor mismatch. Different type?", (Constructor)rc, is((Constructor)lc));

        String[] names = ClassDescriptor.loadParameterNames(lc);
        Class<?>[] types = lc.getParameterTypes();
        assertThat(types.length, is(names.length));
        assertEqualProperties(lhs, rhs, names, types);

        Map<String, Class<?>> lprops = extractDataBoundSetterProperties(lhs.getClass());
        Map<String, Class<?>> rprops = extractDataBoundSetterProperties(rhs.getClass());
        assertThat("Data bound setters mismatch. Different type?", lprops, is(rprops));
        List<String> setterNames = new ArrayList<String>();
        List<Class<?>> setterTypes = new ArrayList<Class<?>>();
        for (Map.Entry<String, Class<?>> e : lprops.entrySet()) {
            setterNames.add(e.getKey());
            setterTypes.add(e.getValue());
        }
        assertEqualProperties(lhs, rhs, setterNames.toArray(new String[0]), setterTypes.toArray(new Class<?>[0]));
    }

    private void assertEqualProperties(@Nonnull Object lhs, @Nonnull Object rhs, @Nonnull String[] names, @Nonnull Class<?>[] types) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, Exception {
        List<String> primitiveProperties = new ArrayList<String>();

        for (int i=0; i<types.length; i++) {
            Object lv = ReflectionUtils.getPublicProperty(lhs, names[i]);
            Object rv = ReflectionUtils.getPublicProperty(rhs, names[i]);

            if (lv != null && rv != null && Iterable.class.isAssignableFrom(types[i])) {
                Iterable lcol = (Iterable) lv;
                Iterable rcol = (Iterable) rv;
                Iterator ltr,rtr;
                for (ltr=lcol.iterator(), rtr=rcol.iterator(); ltr.hasNext() && rtr.hasNext();) {
                    Object litem = ltr.next();
                    Object ritem = rtr.next();

                    if (findDataBoundConstructor(litem.getClass())!=null) {
                        assertEqualDataBoundBeans(litem,ritem);
                    } else {
                        assertThat(ritem, is(litem));
                    }
                }
                assertThat("collection size mismatch between " + lhs + " and " + rhs, ltr.hasNext() ^ rtr.hasNext(),
                        is(false));
            } else
            if (findDataBoundConstructor(types[i])!=null || (lv!=null && findDataBoundConstructor(lv.getClass())!=null) || (rv!=null && findDataBoundConstructor(rv.getClass())!=null)) {
                // recurse into nested databound objects
                assertEqualDataBoundBeans(lv,rv);
            } else {
                primitiveProperties.add(names[i]);
            }
        }

        // compare shallow primitive properties
        if (!primitiveProperties.isEmpty())
            assertEqualBeans(lhs,rhs,Util.join(primitiveProperties,","));
    }

    @Nonnull
    private Map<String, Class<?>> extractDataBoundSetterProperties(@Nonnull Class<?> c) {
        Map<String, Class<?>> ret = new HashMap<String, Class<?>>();
        for ( ;c != null; c = c.getSuperclass()) {

            for (Field f: c.getDeclaredFields()) {
                if (f.getAnnotation(DataBoundSetter.class) == null) {
                    continue;
                }
                f.setAccessible(true);
                ret.put(f.getName(), f.getType());
           }

            for (Method m: c.getDeclaredMethods()) {
                AbstractMap.SimpleEntry<String, Class<?>> nameAndType = extractDataBoundSetter(m);
                if (nameAndType == null) {
                    continue;
                }
                if (ret.containsKey(nameAndType.getKey())) {
                    continue;
                }
                ret.put(nameAndType.getKey(),  nameAndType.getValue());
            }
        }
        return ret;
    }

    @CheckForNull
    private AbstractMap.SimpleEntry<String, Class<?>> extractDataBoundSetter(@Nonnull Method m) {
        // See org.kohsuke.stapler.RequestImpl::findDataBoundSetter
        if (!Modifier.isPublic(m.getModifiers())) {
            return null;
        }
        if (!m.getName().startsWith("set")) {
            return null;
        }
        if (m.getParameterTypes().length != 1) {
            return null;
        }
        if (!m.isAnnotationPresent(DataBoundSetter.class)) {
            return null;
        }
        
        // setXyz -> xyz
        return new AbstractMap.SimpleEntry<String, Class<?>>(
                Introspector.decapitalize(m.getName().substring(3)),
                m.getParameterTypes()[0]
        );
    }

    /**
     * Makes sure that two collections are identical via {@link #assertEqualDataBoundBeans(Object, Object)}
     */
    public void assertEqualDataBoundBeans(List<?> lhs, List<?> rhs) throws Exception {
        assertThat(rhs.size(), is(lhs.size()));
        for (int i=0; i<lhs.size(); i++)
            assertEqualDataBoundBeans(lhs.get(i),rhs.get(i));
    }

    public Constructor<?> findDataBoundConstructor(Class<?> c) {
        for (Constructor<?> m : c.getConstructors()) {
            if (m.getAnnotation(DataBoundConstructor.class)!=null)
                return m;
        }
        return null;
    }

    /**
     * Gets the descriptor instance of the current Hudson by its type.
     */
    public <T extends Descriptor<?>> T get(Class<T> d) {
        return jenkins.getDescriptorByType(d);
    }


    /**
     * Returns true if Hudson is building something or going to build something.
     */
    public boolean isSomethingHappening() {
        if (!jenkins.getQueue().isEmpty())
            return true;
        for (Computer n : jenkins.getComputers())
            if (!n.isIdle())
                return true;
        return false;
    }

    /**
     * Waits until Hudson finishes building everything, including those in the queue.
     */
    public void waitUntilNoActivity() throws Exception {
        waitUntilNoActivityUpTo(Integer.MAX_VALUE);
    }

    /**
     * Waits until Hudson finishes building everything, including those in the queue, or fail the test
     * if the specified timeout milliseconds is
     */
    public void waitUntilNoActivityUpTo(int timeout) throws Exception {
        long startTime = System.currentTimeMillis();
        int streak = 0;

        while (true) {
            Thread.sleep(10);
            if (isSomethingHappening())
                streak=0;
            else
                streak++;

            if (streak>5)   // the system is quiet for a while
                return;

            if (System.currentTimeMillis()-startTime > timeout) {
                List<Queue.Executable> building = new ArrayList<Queue.Executable>();
                for (Computer c : jenkins.getComputers()) {
                    for (Executor e : c.getExecutors()) {
                        if (e.isBusy())
                            building.add(e.getCurrentExecutable());
                    }
                    for (Executor e : c.getOneOffExecutors()) {
                        if (e.isBusy())
                            building.add(e.getCurrentExecutable());
                    }
                }
                dumpThreads();
                throw new AssertionError(String.format("Jenkins is still doing something after %dms: queue=%s building=%s",
                        timeout, Arrays.asList(jenkins.getQueue().getItems()), building));
            }
        }
    }


//
// recipe methods. Control the test environments.
//

    /**
     * Called during the {@link #before()} to give a test case an opportunity to
     * control the test environment in which Hudson is run.
     *
     * <p>
     * One could override this method and call a series of {@code withXXX} methods,
     * or you can use the annotations with {@link Recipe} meta-annotation.
     */
    public void recipe() throws Exception {
        recipeLoadCurrentPlugin();
        // look for recipe meta-annotation
        try {
            for (final Annotation a : testDescription.getAnnotations()) {
                JenkinsRecipe r = a.annotationType().getAnnotation(JenkinsRecipe.class);
                if(r==null)     continue;
                final JenkinsRecipe.Runner runner = r.value().newInstance();
                recipes.add(runner);
                tearDowns.add(new LenientRunnable() {
                    public void run() throws Exception {
                        runner.tearDown(JenkinsRule.this,a);
                    }
                });
                runner.setup(this,a);
            }
        } catch (NoSuchMethodException e) {
            // not a plain JUnit test.
        }
    }

    /**
     * If this test harness is launched for a Jenkins plugin, locate the <tt>target/test-classes/the.jpl</tt>
     * and add a recipe to install that to the new Jenkins.
     *
     * <p>
     * This file is created by <tt>maven-hpi-plugin</tt> at the testCompile phase when the current
     * packaging is <tt>jpi</tt>.
     */
    public void recipeLoadCurrentPlugin() throws Exception {
    	final Enumeration<URL> jpls = getClass().getClassLoader().getResources("the.jpl");
        final Enumeration<URL> hpls = getClass().getClassLoader().getResources("the.hpl");

        final List<URL> all = Collections.list(jpls);
        all.addAll(Collections.list(hpls));
        
        if(all.isEmpty())    return; // nope
        
        recipes.add(new JenkinsRecipe.Runner() {
            @Override
            public void decorateHome(JenkinsRule testCase, File home) throws Exception {
                decorateHomeFor(home, all);
            }
        });
    }

    static void decorateHomeFor(File home, List<URL> all) throws Exception {
        List<Jpl> jpls = new ArrayList<Jpl>();
        for (URL hpl : all) {
            Jpl jpl = new Jpl(home, hpl);
            jpl.loadManifest();
            jpls.add(jpl);
        }
        for (Jpl jpl : jpls) {
            jpl.resolveDependencies(jpls);
        }
    }

    private static final class Jpl {
        private final File home;
                final URL jpl;
                Manifest m;
                private String shortName;

                Jpl(File home, URL jpl) {
                    this.home = home;
                    this.jpl = jpl;
                }

                void loadManifest() throws IOException {
                    m = new Manifest(jpl.openStream());
                    shortName = m.getMainAttributes().getValue("Short-Name");
                    if(shortName ==null)
                        throw new Error(jpl +" doesn't have the Short-Name attribute");
                    FileUtils.copyURLToFile(jpl, new File(home, "plugins/" + shortName + ".jpl"));
                }

                void resolveDependencies(List<Jpl> jpls) throws Exception {
                    // make dependency plugins available
                    // TODO: probably better to read POM, but where to read from?
                    // TODO: this doesn't handle transitive dependencies

                    // Tom: plugins are now searched on the classpath first. They should be available on
                    // the compile or test classpath.
                    // For transitive dependencies, we could evaluate Plugin-Dependencies transitively.
                    String dependencies = m.getMainAttributes().getValue("Plugin-Dependencies");
                    if(dependencies!=null) {
                        DEPENDENCY:
                        for( String dep : dependencies.split(",")) {
                            String suffix = ";resolution:=optional";
                            boolean optional = dep.endsWith(suffix);
                            if (optional) {
                                dep = dep.substring(0, dep.length() - suffix.length());
                            }
                            String[] tokens = dep.split(":");
                            String artifactId = tokens[0];
                            String version = tokens[1];

                            for (Jpl other : jpls) {
                                if (other.shortName.equals(artifactId))
                                    continue DEPENDENCY;    // resolved from another JPL file
                            }

                            File dependencyJar=resolveDependencyJar(artifactId,version);
                            if (dependencyJar == null) {
                                if (optional) {
                                    LOGGER.log(Level.INFO, "cannot resolve optional dependency {0} of {1}; skipping", new Object[] {dep, shortName});
                                    continue;
                                }
                                throw new IOException("Could not resolve " + dep + " in " + System.getProperty("java.class.path"));
                            }

                            File dst = new File(home, "plugins/" + artifactId + ".jpi");
                            if(!dst.exists() || dst.lastModified()!=dependencyJar.lastModified()) {
                                try {
                                    FileUtils.copyFile(dependencyJar, dst);
                                } catch (ClosedByInterruptException x) {
                                    throw new AssumptionViolatedException("copying dependencies was interrupted", x);
                                }
                            }
                        }
                    }
                }

            private @CheckForNull File resolveDependencyJar(String artifactId, String version) throws Exception {
                // try to locate it from manifest
                Enumeration<URL> manifests = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF");
                while (manifests.hasMoreElements()) {
                    URL manifest = manifests.nextElement();
                    InputStream is = manifest.openStream();
                    Manifest m = new Manifest(is);
                    is.close();

                    if (artifactId.equals(m.getMainAttributes().getValue("Short-Name")))
                        return Which.jarFile(manifest);
                }

                // For snapshot plugin dependencies, an IDE may have replaced ~/.m2/repository/…/${artifactId}.hpi with …/${artifactId}-plugin/target/classes/
                // which unfortunately lacks META-INF/MANIFEST.MF so try to find index.jelly (which every plugin should include) and thus the ${artifactId}.hpi:
                Enumeration<URL> jellies = getClass().getClassLoader().getResources("index.jelly");
                while (jellies.hasMoreElements()) {
                    URL jellyU = jellies.nextElement();
                    if (jellyU.getProtocol().equals("file")) {
                        File jellyF = new File(jellyU.toURI());
                        File classes = jellyF.getParentFile();
                        if (classes.getName().equals("classes")) {
                            File target = classes.getParentFile();
                            if (target.getName().equals("target")) {
                                File hpi = new File(target, artifactId + ".hpi");
                                if (hpi.isFile()) {
                                    return hpi;
                                }
                            }
                        }
                    }
                }

                return null;
            }
            
    }

    public JenkinsRule withNewHome() {
        return with(HudsonHomeLoader.NEW);
    }

    public JenkinsRule withExistingHome(File source) throws Exception {
        return with(new HudsonHomeLoader.CopyExisting(source));
    }

    /**
     * Declares that this test case expects to start with one of the preset data sets.
     * See {@code test/src/main/preset-data/}
     * for available datasets and what they mean.
     */
    public JenkinsRule withPresetData(String name) {
        name = "/" + name + ".zip";
        URL res = getClass().getResource(name);
        if(res==null)   throw new IllegalArgumentException("No such data set found: "+name);

        return with(new HudsonHomeLoader.CopyExisting(res));
    }

    public JenkinsRule with(HudsonHomeLoader homeLoader) {
        this.homeLoader = homeLoader;
        return this;
    }


    /**
     * Executes the given closure on the server, by the servlet request handling thread,
     * in the context of an HTTP request.
     *
     * <p>
     * In {@link JenkinsRule}, a thread that's executing the test code is different from the thread
     * that carries out HTTP requests made through {@link WebClient}. But sometimes you want to
     * make assertions and other calls with side-effect from within the request handling thread.
     *
     * <p>
     * This method allows you to do just that. It is useful for testing some methods that
     * require {@link org.kohsuke.stapler.StaplerRequest} and {@link org.kohsuke.stapler.StaplerResponse}, or getting the credential
     * of the current user (via {@link jenkins.model.Jenkins#getAuthentication()}, and so on.
     *
     * @param c
     *      The closure to be executed on the server.
     * @return
     *      The return value from the closure.
     * @throws Exception
     *      If a closure throws any exception, that exception will be carried forward.
     */
    public <V> V executeOnServer(Callable<V> c) throws Exception {
        return createWebClient().executeOnServer(c);
    }

    /**
     * Sometimes a part of a test case may ends up creeping into the serialization tree of {@link hudson.model.Saveable#save()},
     * so detect that and flag that as an error.
     */
    private Object writeReplace() {
        throw new AssertionError("JenkinsRule " + testDescription.getDisplayName() + " is not supposed to be serialized");
    }

    /**
     * This is to assist Groovy test clients who are incapable of instantiating the inner classes properly.
     */
    public WebClient createWebClient() {
        return new WebClient();
    }

    /**
     * Extends {@link com.gargoylesoftware.htmlunit.WebClient} and provide convenience methods
     * for accessing Hudson.
     */
    public class WebClient extends com.gargoylesoftware.htmlunit.WebClient {
        private static final long serialVersionUID = -7944895389154288881L;

        private List<WebResponseListener> webResponseListeners = new ArrayList<WebResponseListener>();

        public WebClient() {
            super(BrowserVersion.BEST_SUPPORTED);

//            setJavaScriptEnabled(false);
            setPageCreator(HudsonPageCreator.INSTANCE);
            clients.add(this);
            // make ajax calls run as post-action for predictable behaviors that simplify debugging
            setAjaxController(new AjaxController() {
                private static final long serialVersionUID = -76034615893907856L;
                public boolean processSynchron(HtmlPage page, WebRequest settings, boolean async) {
                    return false;
                }
            });

            setCssErrorHandler(new CSSErrorHandler() {
                final CSSErrorHandler defaultHandler = new DefaultCssErrorHandler();

                @Override
                public void warning(final CSSParseException exception) throws com.gargoylesoftware.css.parser.CSSException {
                    if (!ignore(exception))
                        defaultHandler.warning(exception);
                }

                @Override
                public void error(final CSSParseException exception) throws CSSException {
                    if (!ignore(exception))
                        defaultHandler.error(exception);
                }

                @Override
                public void fatalError(final CSSParseException exception) throws CSSException {
                    if (!ignore(exception))
                        defaultHandler.fatalError(exception);
                }

                private boolean ignore(final CSSParseException exception) {
                    String uri = exception.getURI();
                    return uri.contains("/yui/")
                        // TODO JENKINS-14749: these are a mess today, and we know that
                        || uri.contains("/css/style.css") || uri.contains("/css/responsive-grid.css");
                }
            });

            // if no other debugger is installed, install jsDebugger,
            // so as not to interfere with the 'Dim' class.
            AbstractJavaScriptEngine<?> javaScriptEngine = getJavaScriptEngine();
            if (javaScriptEngine instanceof JavaScriptEngine) {
                ((JavaScriptEngine) javaScriptEngine).getContextFactory()
                        .addListener(new ContextFactory.Listener() {
                            public void contextCreated(Context cx) {
                                if (cx.getDebugger() == null)
                                    cx.setDebugger(jsDebugger, null);
                            }

                            public void contextReleased(Context cx) {
                            }
                        });
            }

            // avoid a hang by setting a time out. It should be long enough to prevent
            // false-positive timeout on slow systems
            //setTimeout(60*1000);
        }


        public void addWebResponseListener(WebResponseListener listener) {
            webResponseListeners.add(listener);
        }

        @Override
        public WebResponse loadWebResponse(final WebRequest webRequest) throws IOException {
            WebResponse webResponse = super.loadWebResponse(webRequest);
            if (!webResponseListeners.isEmpty()) {
                for (WebResponseListener listener : webResponseListeners) {
                    listener.onLoadWebResponse(webRequest, webResponse);
                }
            }
            return webResponse;
        }

        /**
         * Logs in to Jenkins.
         */
        public WebClient login(String username, String password) throws Exception {
            return login(username,password,false);
        }

        /**
         * Returns {@code true} if JavaScript is enabled and the script engine was loaded successfully.
         * Short-hand method to ease discovery of feature + improve readability
         *
         * @return {@code true} if JavaScript is enabled
         * @see WebClientOptions#isJavaScriptEnabled()
         * @since 2.0
         */
        public boolean isJavaScriptEnabled() {
            return getOptions().isJavaScriptEnabled();
        }

        /**
         * Enables/disables JavaScript support.
         * Short-hand method to ease discovery of feature + improve readability
         *
         * @param enabled {@code true} to enable JavaScript support
         * @see WebClientOptions#setJavaScriptEnabled(boolean)
         * @since 2.0
         */
        public void setJavaScriptEnabled(boolean enabled) {
            getOptions().setJavaScriptEnabled(enabled);
        }

        /**
         * Enables/disables JavaScript support.
         * Fluent method to ease discovery of feature + improve readability
         *
         * @param enabled {@code true} to enable JavaScript support
         * @return self for fluent method chaining
         * @see WebClientOptions#setJavaScriptEnabled(boolean)
         * @since 2.42
         */
        public WebClient withJavaScriptEnabled(boolean enabled) {
            setJavaScriptEnabled(enabled);
            return this;
        }
    
        /**
         * Returns true if an exception will be thrown in the event of a failing response code.
         * Short-hand method to ease discovery of feature + improve readability
         * 
         * @return {@code true} if an exception will be thrown in the event of a failing response code
         * @see WebClientOptions#isThrowExceptionOnFailingStatusCode()
         * @since 2.42
         */
        public boolean isThrowExceptionOnFailingStatusCode() {
            return getOptions().isThrowExceptionOnFailingStatusCode();
        }

        /**
         * Changes the behavior of this webclient when a script error occurs.
         * Short-hand method to ease discovery of feature + improve readability
         *
         * @param enabled {@code true} to enable this feature
         * @see WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)
         * @since 2.42
         */
        public void setThrowExceptionOnFailingStatusCode(boolean enabled) {
            getOptions().setThrowExceptionOnFailingStatusCode(enabled);
        }

        /**
         * Changes the behavior of this webclient when a script error occurs.
         * Fluent method to ease discovery of feature + improve readability
         * 
         * @param enabled {@code true} to enable this feature
         * @return self for fluent method chaining
         * @see WebClientOptions#setThrowExceptionOnFailingStatusCode(boolean)
         * @since 2.42
         */
        public WebClient withThrowExceptionOnFailingStatusCode(boolean enabled) {
            setThrowExceptionOnFailingStatusCode(enabled);
            return this;
        }
        
        /**
         * Returns whether or not redirections will be followed automatically on receipt of a redirect status code from the server.
         * Short-hand method to ease discovery of feature + improve readability
         * 
         * @return {@code true} if automatic redirection is enabled
         * @see WebClientOptions#isRedirectEnabled()
         * @since 2.42
         */
        public boolean isRedirectEnabled() {
            return getOptions().isRedirectEnabled();
        }

        /**
         * Sets whether or not redirections will be followed automatically on receipt of a redirect status code from the server.
         * Short-hand method to ease discovery of feature + improve readability
         * 
         * @param enabled {@code true} to enable automatic redirection
         * @see com.gargoylesoftware.htmlunit.WebClientOptions#setRedirectEnabled(boolean)
         * @since 2.42
         */
        public void setRedirectEnabled(boolean enabled) {
            getOptions().setRedirectEnabled(enabled);
        }

        /**
         * Sets whether or not redirections will be followed automatically on receipt of a redirect status code from the server.
         * Fluent method to ease discovery of feature + improve readability
         *
         * @param enabled {@code true} to enable automatic redirection
         * @return self for fluent method chaining
         * @see com.gargoylesoftware.htmlunit.WebClientOptions#setRedirectEnabled(boolean)
         * @since 2.42
         */
        public WebClient withRedirectEnabled(boolean enabled) {
            setRedirectEnabled(enabled);
            return this;
        }

        /**
         * Logs in to Jenkins.
         */
        public WebClient login(String username, String password, boolean rememberMe) throws Exception {
            HtmlPage page = goTo("login");
//            page = (HtmlPage) page.getFirstAnchorByText("Login").click();

            HtmlForm form = page.getFormByName("login");
            form.getInputByName("j_username").setValueAttribute(username);
            form.getInputByName("j_password").setValueAttribute(password);
            try {
                form.getInputByName("remember_me").setChecked(rememberMe);
            } catch (ElementNotFoundException e) {
                // remember me not available is OK so long as the caller didn't ask for it
                assert !rememberMe;
            }
            HtmlFormUtil.submit(form, null);
            return this;
        }

        /**
         * Logs in to Hudson, by using the user name as the password.
         *
         * <p>
         * See {@link #configureUserRealm} for how the container is set up with the user names
         * and passwords. All the test accounts have the same user name and password.
         */
        public WebClient login(String username) throws Exception {
            login(username, username);
            return this;
        }

        /**
         * Executes the given closure on the server, by the servlet request handling thread,
         * in the context of an HTTP request.
         *
         * <p>
         * In {@link JenkinsRule}, a thread that's executing the test code is different from the thread
         * that carries out HTTP requests made through {@link WebClient}. But sometimes you want to
         * make assertions and other calls with side-effect from within the request handling thread.
         *
         * <p>
         * This method allows you to do just that. It is useful for testing some methods that
         * require {@link org.kohsuke.stapler.StaplerRequest} and {@link org.kohsuke.stapler.StaplerResponse}, or getting the credential
         * of the current user (via {@link jenkins.model.Jenkins#getAuthentication()}, and so on.
         *
         * @param c
         *      The closure to be executed on the server.
         * @return
         *      The return value from the closure.
         * @throws Exception
         *      If a closure throws any exception, that exception will be carried forward.
         */
        public <V> V executeOnServer(final Callable<V> c) throws Exception {
            final Exception[] t = new Exception[1];
            final List<V> r = new ArrayList<V>(1);  // size 1 list

            ClosureExecuterAction cea = jenkins.getExtensionList(RootAction.class).get(ClosureExecuterAction.class);
            UUID id = UUID.randomUUID();
            cea.add(id,new Runnable() {
                public void run() {
                    try {
                        StaplerResponse rsp = Stapler.getCurrentResponse();
                        rsp.setStatus(200);
                        rsp.setContentType("text/html");
                        r.add(c.call());
                    } catch (Exception e) {
                        t[0] = e;
                    }
                }
            });
            goTo("closures/?uuid="+id);

            if (t[0]!=null)
                throw t[0];
            return r.get(0);
        }

        public HtmlPage search(String q) throws IOException, SAXException {
            HtmlPage top = goTo("");
            HtmlForm search = top.getFormByName("search");
            search.getInputByName("q").setValueAttribute(q);
            return (HtmlPage)HtmlFormUtil.submit(search, null);
        }

        /**
         * Short for {@code getPage(r,"")}, to access the top page of a build.
         */
        public HtmlPage getPage(Run r) throws IOException, SAXException {
            return getPage(r,"");
        }

        /**
         * Accesses a page inside {@link Run}.
         *
         * @param relative
         *      Relative URL within the build URL, like "changes". Doesn't start with '/'. Can be empty.
         */
        public HtmlPage getPage(Run r, String relative) throws IOException, SAXException {
            return goTo(r.getUrl()+relative);
        }

        public HtmlPage getPage(Item item) throws IOException, SAXException {
            return getPage(item,"");
        }

        public HtmlPage getPage(Item item, String relative) throws IOException, SAXException {
            return goTo(item.getUrl()+relative);
        }

        public HtmlPage getPage(Node item) throws IOException, SAXException {
            return getPage(item, "");
        }

        public HtmlPage getPage(Node item, String relative) throws IOException, SAXException {
            return goTo(item.toComputer().getUrl()+relative);
        }

        public HtmlPage getPage(View view) throws IOException, SAXException {
            return goTo(view.getUrl());
        }

        public HtmlPage getPage(View view, String relative) throws IOException, SAXException {
            return goTo(view.getUrl()+relative);
        }

        /**
         * @deprecated
         *      This method expects a full URL. This method is marked as deprecated to warn you
         *      that you probably should be using {@link #goTo(String)} method, which accepts
         *      a relative path within the Hudson being tested. (IOW, if you really need to hit
         *      a website on the internet, there's nothing wrong with using this method.)
         */
        @SuppressWarnings("unchecked")
        @Override
        public Page getPage(String url) throws IOException, FailingHttpStatusCodeException {
            try {
                return super.getPage(url);
            } finally {
                WebClientUtil.waitForJSExec(this);
            }
        }

        /**
         * Requests an HTML page within Jenkins.
         *
         * @param relative
         *      Relative path within Jenkins. Starts without '/'.
         *      For example, "job/test/" to go to a job top page.
         */
        public HtmlPage goTo(String relative) throws IOException, SAXException {
            Page p = goTo(relative, "text/html");
            if (p instanceof HtmlPage) {
                return (HtmlPage) p;
            } else {
                throw new AssertionError("Expected text/html but instead the content type was "+p.getWebResponse().getContentType());
            }
        }

        /**
         * Requests a page within Jenkins.
         *
         * @param relative
         *      Relative path within Jenkins. Starts without '/'.
         *      For example, "job/test/" to go to a job top page.
         * @param expectedContentType the expected {@link WebResponse#getContentType}, or null to do no such check
         */
        public Page goTo(String relative, @CheckForNull String expectedContentType) throws IOException, SAXException {
            assert !relative.startsWith("/");
            Page p;
            try {
                p = super.getPage(getContextPath() + relative);
                WebClientUtil.waitForJSExec(this);
            } catch (IOException x) {
                Throwable cause = x.getCause();
                if (cause instanceof SocketTimeoutException) {
                    throw new AssumptionViolatedException("failed to get " + relative + " due to read timeout", cause);
                } else if (cause != null) {
                    cause.printStackTrace(); // SUREFIRE-1067 workaround
                }
                throw x;
            }
            if (expectedContentType != null) {
                assertThat(p.getWebResponse().getContentType(), is(expectedContentType));
            }
            return p;
        }

        /** Loads a page as XML. Useful for testing Jenkins's XML API, in concert with
         * assertXPath(DomNode page, String xpath)
         * @param path   the path part of the url to visit
         * @return  the XmlPage found at that url
         * @throws IOException
         * @throws SAXException
         */
        public XmlPage goToXml(String path) throws IOException, SAXException {
            Page page = goTo(path, "application/xml");
            if (page instanceof XmlPage)
                return (XmlPage) page;
            else
                return null;
        }

        /**
         * Verify that the server rejects an attempt to load the given page.
         * @param url a URL path (relative to Jenkins root)
         * @param statusCode the expected failure code (such as {@link HttpURLConnection#HTTP_FORBIDDEN})
         * @since 1.504
         */
        public void assertFails(String url, int statusCode) throws Exception {
            assert !url.startsWith("/");
            boolean currentConfiguration = isThrowExceptionOnFailingStatusCode();
            // enforce the throwing of exception for the catch scope only
            setThrowExceptionOnFailingStatusCode(true);
            try {
                fail(url + " should have been rejected but produced: " + super.getPage(getContextPath() + url).getWebResponse().getContentAsString());
            } catch (FailingHttpStatusCodeException x) {
                assertEquals(statusCode, x.getStatusCode());
            } finally {
                setThrowExceptionOnFailingStatusCode(currentConfiguration);
            }
        }

        /**
         * Returns the URL of the webapp top page.
         * URL ends with '/'.
         * <p>This is actually the same as {@link #getURL} and should not be confused with {@link #contextPath}.
         */
        public String getContextPath() throws IOException {
            return getURL().toExternalForm();
        }

        /**
         * Adds a security crumb to the request.
         * Use {@link #createCrumbedUrl} instead if you intend to call {@link WebRequest#setRequestBody}, typical of a POST request.
         */
        public WebRequest addCrumb(WebRequest req) {
            ArrayList<NameValuePair> params = new ArrayList<>();
            params.add(getCrumbHeaderNVP());
            List<NameValuePair> oldParams = req.getRequestParameters();
            if (oldParams != null) {
                params.addAll(oldParams);
            }
            req.setRequestParameters(params);
            return req;
        }

        /**
         * Creates a URL with crumb parameters relative to {{@link #getContextPath()}
         */
        public URL createCrumbedUrl(String relativePath) throws IOException {
            CrumbIssuer issuer = jenkins.getCrumbIssuer();
            String crumbName = issuer.getDescriptor().getCrumbRequestField();
            String crumb = issuer.getCrumb(null);
            if (relativePath.indexOf('?') == -1) {
                return new URL(getContextPath()+relativePath+"?"+crumbName+"="+crumb);
            }
            return new URL(getContextPath()+relativePath+"&"+crumbName+"="+crumb);
        }

        /**
         * Add the "Authorization" header with Basic credentials derived from login and password using Base64
         * @since 2.32
         */
        public @Nonnull WebClient withBasicCredentials(@Nonnull String login, @Nonnull String passwordOrToken) {
            String authCode = new String(Base64.getEncoder().encode((login + ":" + passwordOrToken).getBytes(StandardCharsets.UTF_8)));

            addRequestHeader(HttpHeaders.AUTHORIZATION, "Basic " + authCode);
            return this;
        }

        /**
         * Use {@code loginAndPassword} as login AND password, especially useful for {@link DummySecurityRealm}
         * Add the "Authorization" header with Basic credentials derived from login using Base64
         * @since 2.32
         */
        public @Nonnull WebClient withBasicCredentials(@Nonnull String loginAndPassword){
            return withBasicCredentials(loginAndPassword, loginAndPassword);
        }

        /**
         * Retrieve the {@link ApiTokenProperty} from the user, derive credentials from it and place it in Basic authorization header
         * @see #withBasicCredentials(String, String)
         * @since 2.32
         */
        public @Nonnull WebClient withBasicApiToken(@Nonnull User user){
            return withBasicCredentials(user.getId(), user.getProperty(ApiTokenProperty.class).getApiToken());
        }

        /**
         * Retrieve the {@link ApiTokenProperty} from the associated user, derive credentials from it and place it in Basic authorization header
         * @see #withBasicApiToken(User)
         * @since 2.32
         */
        public @Nonnull WebClient withBasicApiToken(@Nonnull String userId){
            User user = User.getById(userId, false);
            assertNotNull("The userId must correspond to an already created User", user);
            return withBasicApiToken(user);
        }

        /**
         * Makes an HTTP request, process it with the given request handler, and returns the response.
         */
        public HtmlPage eval(final Runnable requestHandler) throws IOException, SAXException {
            ClosureExecuterAction cea = jenkins.getExtensionList(RootAction.class).get(ClosureExecuterAction.class);
            UUID id = UUID.randomUUID();
            cea.add(id,requestHandler);
            return goTo("closures/?uuid="+id);
        }

        /**
         * Starts an interactive JavaScript debugger, and break at the next JavaScript execution.
         *
         * <p>
         * This is useful during debugging a test so that you can step execute and inspect state of JavaScript.
         * This will launch a Swing GUI, and the method returns immediately.
         *
         * <p>
         * Note that installing a debugger appears to make an execution of JavaScript substantially slower.
         *
         * <p>
         * TODO: because each script block evaluation in HtmlUnit is done in a separate Rhino context,
         * if you step over from one script block, the debugger fails to kick in on the beginning of the next script
         * block.
         * This makes it difficult to set a break point on arbitrary script block in the HTML page. We need to fix this
         * by tweaking {@link org.mozilla.javascript.tools.debugger.Dim.StackFrame#onLineChange(Context, int)}.
         */
        public Dim interactiveJavaScriptDebugger() {
            Global global = new Global();
            HtmlUnitContextFactory cf = ((JavaScriptEngine)getJavaScriptEngine()).getContextFactory();
            global.init(cf);

            Dim dim = org.mozilla.javascript.tools.debugger.Main.mainEmbedded(cf, global, "Rhino debugger: " + testDescription.getDisplayName());

            // break on exceptions. this catch most of the errors
            dim.setBreakOnExceptions(true);

            return dim;
        }
    }

    // needs to keep reference, or it gets GC-ed.
    private static final Logger XML_HTTP_REQUEST_LOGGER = Logger.getLogger(XMLHttpRequest.class.getName());
    private static final Logger SPRING_LOGGER = Logger.getLogger("org.springframework");
    private static final Logger JETTY_LOGGER = Logger.getLogger("org.mortbay.log");
    private static final Logger HTMLUNIT_DOCUMENT_LOGGER = Logger.getLogger("com.gargoylesoftware.htmlunit.javascript.host.Document");
    private static final Logger HTMLUNIT_JS_LOGGER = Logger.getLogger("com.gargoylesoftware.htmlunit.javascript.StrictErrorReporter");

    static {
        // screen scraping relies on locale being fixed.
        Locale.setDefault(Locale.ENGLISH);

        {// enable debug assistance, since tests are often run from IDE
            Dispatcher.TRACE = true;
            MetaClass.NO_CACHE=true;
            System.setProperty("jenkins.model.Jenkins.SHOW_STACK_TRACE", "true");
            // load resources from the source dir.
            File dir = new File("src/main/resources");
            if(dir.exists() && MetaClassLoader.debugLoader==null)
                try {
                    MetaClassLoader.debugLoader = new MetaClassLoader(
                        new URLClassLoader(new URL[]{dir.toURI().toURL()}));
                } catch (MalformedURLException e) {
                    throw new AssertionError(e);
                }
        }

        // suppress some logging which we do not much care about here
        SPRING_LOGGER.setLevel(Level.WARNING);
        JETTY_LOGGER.setLevel(Level.WARNING);

        // hudson-behavior.js relies on this to decide whether it's running unit tests.
        Main.isUnitTest = true;

        // prototype.js calls this method all the time, so ignore this warning.
        XML_HTTP_REQUEST_LOGGER.setFilter(new Filter() {
            public boolean isLoggable(LogRecord record) {
                return !record.getMessage().contains("XMLHttpRequest.getResponseHeader() was called before the response was available.");
            }
        });
        // JENKINS-14749: prototype.js intentionally swallows this exception (thrown on Firefox which we simulate), but HtmlUnit still tries to log it.
        HTMLUNIT_DOCUMENT_LOGGER.setFilter(new Filter() {
            @Override public boolean isLoggable(LogRecord record) {
                return !record.getMessage().equals("Unexpected exception occurred while parsing HTML snippet");
            }
        });
        HTMLUNIT_JS_LOGGER.setFilter(new Filter() {
            @Override public boolean isLoggable(LogRecord record) {
                return !record.getMessage().contains("Unexpected exception occurred while parsing HTML snippet: input name=\"x\"");
            }
        });

        // remove the upper bound of the POST data size in Jetty.
        System.setProperty("org.mortbay.jetty.Request.maxFormContentSize","-1");
    }

    private static final Logger LOGGER = Logger.getLogger(HudsonTestCase.class.getName());

    public static final List<ToolProperty<?>> NO_PROPERTIES = Collections.emptyList();

    /**
     * Specify this to a TCP/IP port number to have slaves started with the debugger.
     */
    public static final int SLAVE_DEBUG_PORT = Integer.getInteger(HudsonTestCase.class.getName()+".slaveDebugPort",-1);

    public static final MimeTypes MIME_TYPES;
    static {
        jettyLevel(Level.WARNING); // suppress Log.initialize message
        try {
            MIME_TYPES = new MimeTypes();
        } finally {
            jettyLevel(Level.INFO);
        }
        MIME_TYPES.addMimeMapping("js","application/javascript");
        Functions.DEBUG_YUI = true;

        // during the unit test, predictably releasing classloader is important to avoid
        // file descriptor leak.
        ClassicPluginStrategy.useAntClassLoader = true;

        // DNS multicast support takes up a lot of time during tests, so just disable it altogether
        // this also prevents tests from falsely advertising Hudson
        DNSMultiCast.disabled = true;

        try {
            GNUCLibrary.LIBC.unsetenv("MAVEN_OPTS");
            GNUCLibrary.LIBC.unsetenv("MAVEN_DEBUG_OPTS");
        } catch (LinkageError x) {
            // skip; TODO 1.630+ can use Functions.isGlibcSupported
        } catch (Exception e) {
            LOGGER.log(Level.WARNING,"Failed to cancel out MAVEN_OPTS",e);
        }
    }

    public static class TestBuildWrapper extends BuildWrapper {
        public Result buildResultInTearDown;

        @Override
        public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
            return new BuildWrapper.Environment() {
                @Override
                public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException {
                    buildResultInTearDown = build.getResult();
                    return true;
                }
            };
        }

        @Extension
        public static class TestBuildWrapperDescriptor extends BuildWrapperDescriptor {
            @Override
            public boolean isApplicable(AbstractProject<?, ?> project) {
                return true;
            }

            @Override
            public BuildWrapper newInstance(StaplerRequest req, JSONObject formData) {
                throw new UnsupportedOperationException();
            }

            @Override
            public String getDisplayName() {
                return "TestBuildWrapper";
            }
        }
    }

    public Description getTestDescription() {
        return testDescription;
    }

    private NameValuePair getCrumbHeaderNVP() {
        return new NameValuePair(jenkins.getCrumbIssuer().getDescriptor().getCrumbRequestField(),
                        jenkins.getCrumbIssuer().getCrumb( null ));
    }
}