/*******************************************************************************
*
* Copyright (c) 2004-2009 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Erik Ramfelt, Yahoo! Inc., Tom Huybrechts
*
*
*******************************************************************************/
package org.jvnet.hudson.test;
import com.gargoylesoftware.htmlunit.DefaultCssErrorHandler;
import com.gargoylesoftware.htmlunit.javascript.HtmlUnitContextFactory;
import com.gargoylesoftware.htmlunit.javascript.host.xml.XMLHttpRequest;
import hudson.*;
import hudson.Util;
import hudson.model.*;
import hudson.model.Queue.Executable;
import hudson.security.ACL;
import hudson.security.AbstractPasswordBasedSecurityRealm;
import hudson.security.GroupDetails;
import hudson.security.SecurityRealm;
import hudson.slaves.ComputerConnector;
import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.tools.ToolProperty;
import hudson.remoting.Which;
import hudson.Launcher.LocalLauncher;
import hudson.matrix.MatrixProject;
import hudson.matrix.MatrixBuild;
import hudson.matrix.MatrixRun;
import hudson.security.csrf.CrumbIssuer;
import hudson.slaves.CommandLauncher;
import hudson.slaves.DumbSlave;
import hudson.slaves.RetentionStrategy;
import org.eclipse.hudson.WebAppController;
import org.eclipse.hudson.WebAppController.DefaultInstallStrategy;
import hudson.tasks.Mailer;
import hudson.tasks.Maven;
import hudson.tasks.Ant;
import hudson.tasks.Ant.AntInstallation;
import hudson.tasks.Maven.MavenInstallation;
import hudson.util.PersistedList;
import hudson.util.ReflectionUtils;
import hudson.util.StreamTaskListener;
import hudson.util.PluginServletFilter;
import hudson.model.Node.Mode;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
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 java.beans.PropertyDescriptor;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import junit.framework.TestCase;
import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.ContextFactory.Listener;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.io.FileUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.hudsonci.inject.Smoothie;
import org.hudsonci.inject.SmoothieUtil;
import org.hudsonci.inject.internal.SmoothieContainerBootstrap;
import org.jvnet.hudson.test.HudsonHomeLoader.CopyExisting;
import org.jvnet.hudson.test.recipes.Recipe;
import org.jvnet.hudson.test.recipes.Recipe.Runner;
import org.jvnet.hudson.test.recipes.WithPlugin;
import org.jvnet.hudson.test.rhino.JavaScriptDebugger;
import org.kohsuke.stapler.ClassDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.Dispatcher;
import org.kohsuke.stapler.MetaClass;
import org.kohsuke.stapler.MetaClassLoader;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.Stapler;
import org.mortbay.jetty.MimeTypes;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.bio.SocketConnector;
import org.mortbay.jetty.security.HashUserRealm;
import org.mortbay.jetty.security.UserRealm;
import org.mortbay.jetty.webapp.Configuration;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.jetty.webapp.WebXmlConfiguration;
import org.mozilla.javascript.tools.debugger.Dim;
import org.mozilla.javascript.tools.shell.Global;
import org.springframework.dao.DataAccessException;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.ErrorHandler;
import org.xml.sax.SAXException;
import com.gargoylesoftware.htmlunit.AjaxController;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebRequestSettings;
import com.gargoylesoftware.htmlunit.xml.XmlPage;
import com.gargoylesoftware.htmlunit.html.*;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import hudson.slaves.ComputerListener;
import java.util.concurrent.CountDownLatch;
import hudson.maven.MavenEmbedder;
import hudson.maven.MavenRequest;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.charset.Charset;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException;
import org.eclipse.hudson.HudsonServletContextListener;
import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder;
import org.eclipse.hudson.security.HudsonSecurityManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* Base class for all Hudson test cases.
*
* @see <a href="http://wiki.hudson-ci.org/display/HUDSON/Unit+Test">Wiki
* article about unit testing in Hudson</a>
* @author Kohsuke Kawaguchi
*/
public abstract class HudsonTestCase extends TestCase implements RootAction {
public Hudson hudson;
protected final TestEnvironment env = new TestEnvironment(this);
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 Hudson deployed? <p> Just like
* {@link ServletContext#getContextPath()}, starts with '/' but doesn't end
* with '/'.
*/
protected String contextPath = "";
/**
* {@link Runnable}s to be invoked at {@link #tearDown()}.
*/
protected List<LenientRunnable> tearDowns = new ArrayList<LenientRunnable>();
protected List<Runner> recipes = new ArrayList<Runner>();
/**
* Remember {@link WebClient}s that are created, to release them properly.
*/
private List<WeakReference<WebClient>> clients = new ArrayList<WeakReference<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 WithPlugin} annotations, set to
* true. This will cause a fresh {@link 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.
*/
public boolean useLocalPluginManager = true; // FIXME: At the new smoothie container needs the real plugin manager
public ComputerConnectorTester computerConnectorTester = new ComputerConnectorTester(this);
protected HudsonTestCase(String name) {
super(name);
}
protected HudsonTestCase() {
}
@Override
public void runBare() throws Throwable {
// override the thread name to make the thread dump more useful.
Thread t = Thread.currentThread();
String o = getClass().getName() + '.' + t.getName();
t.setName("Executing " + getName());
try {
super.runBare();
} finally {
t.setName(o);
}
}
@Override
protected void setUp() throws Exception {
//System.setProperty("hudson.PluginStrategy", "hudson.ClassicPluginStrategy");
env.pin();
recipe();
AbstractProject.WORKSPACE.toString();
User.clear();
// Bootstrap the container with details about our testing classes, so it can figure out what to scan/include
SmoothieUtil.reset(); // force-reset, some tests may not properly hit the tear-down to reset so do it again here
new SmoothieContainerBootstrap().bootstrap(getClass().getClassLoader(), Hudson.class, Smoothie.class, HudsonTestCase.class, getClass());
try {
hudson = newHudson();
} 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 = Hudson.class.getDeclaredField("theInstance");
f.setAccessible(true);
f.set(null, null);
throw e;
}
hudson.setNoUsageStatistics(true); // collecting usage stats from tests are pointless.
hudson.setCrumbIssuer(new TestCrumbIssuer());
final WebAppController controller = WebAppController.get();
try {
controller.setContext(hudson.servletContext);
} catch (IllegalStateException e) {
// handle tests which run several times inside the same JVM
Field f = WebAppController.class.getDeclaredField("context");
f.setAccessible(true);
f.set(controller, hudson.servletContext);
}
try {
controller.setInstallStrategy(new DefaultInstallStrategy());
} catch (IllegalStateException e) {
// strategy already set ignore
}
controller.install(hudson);
hudson.servletContext.setAttribute("version", "?");
HudsonServletContextListener.installExpressionFactory(new ServletContextEvent(hudson.servletContext));
// set a default JDK to be the one that the harness is using.
hudson.getJDKs().add(new JDK("default", System.getProperty("java.home")));
configureUpdateCenter();
// expose the test instance as a part of URL tree.
// this allows tests to use a part of the URL space for itself.
hudson.getActions().add(this);
// cause all the descriptors to reload.
// ideally we'd like to reset them to properly emulate the behavior, but that's not possible.
Mailer.descriptor().setHudsonUrl(null);
for (Descriptor d : hudson.getExtensionList(Descriptor.class)) {
d.load();
}
}
/**
* 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 {
final String updateCenterUrl = "http://localhost:" + JavaNetReverseProxy.getInstance().localPort + "/update-center.json";
// don't waste bandwidth talking to the update center
DownloadService.neverUpdate = true;
UpdateSite.neverUpdate = true;
PersistedList<UpdateSite> sites = hudson.getUpdateCenter().getSites();
sites.clear();
sites.add(new UpdateSite("default", updateCenterUrl));
}
@Override
protected void tearDown() throws Exception {
try {
// cancel pending asynchronous operations, although this doesn't really seem to be working
for (WeakReference<WebClient> client : clients) {
WebClient c = client.get();
if (c == null) {
continue;
}
// unload the page to cancel asynchronous operations
c.getPage("about:blank");
}
clients.clear();
} finally {
server.stop();
for (LenientRunnable r : tearDowns) {
r.run();
}
hudson.cleanUp();
ExtensionList.clearLegacyInstances();
DescriptorExtensionList.clearLegacyInstances();
// Force the container bits to reset
SmoothieUtil.reset();
// Clear hudson refrence
hudson = null;
server = null;
// Clear any filters stored in this singlton
PluginServletFilter.clearFilters();
// 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
System.gc();
env.dispose();
}
}
@Override
protected void runTest() throws Throwable {
String testName = getClass().getSimpleName() + "." + getName();
System.out.println(">>> Starting " + testName + " >>>");
// so that test code has all the access to the system
SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM);
try {
super.runTest();
} finally {
System.out.println("<<< Finished " + testName + " <<<");
}
}
public String getIconFileName() {
return null;
}
public String getDisplayName() {
return null;
}
public String getUrlName() {
return "self";
}
/**
* Creates a new instance of {@link Hudson}. If the derived class wants to
* create it in a different way, you can override it.
*/
protected Hudson newHudson() throws Exception {
File home = homeLoader.allocate();
// Create the Security Manager
HudsonSecurityEntitiesHolder.setHudsonSecurityManager(new HudsonSecurityManager(home));
for (Runner r : recipes) {
r.decorateHome(this, home);
}
return new Hudson(home, createWebServer(), useLocalPluginManager ? null : TestPluginManager.INSTANCE);
}
/**
* Prepares a webapp hosting environment to get {@link ServletContext}
* implementation that we need for testing.
*/
protected ServletContext createWebServer() throws Exception {
server = new Server();
WebAppContext context = new WebAppContext(WarExploder.getExplodedDir().getPath(), contextPath);
context.setClassLoader(getClass().getClassLoader());
context.setConfigurations(new Configuration[]{new WebXmlConfiguration(), new NoListenerConfiguration()});
server.setHandler(context);
context.setMimeTypes(MIME_TYPES);
SocketConnector connector = new SocketConnector();
server.addConnector(connector);
server.addUserRealm(configureUserRealm());
server.start();
localPort = connector.getLocalPort();
return context.getServletContext();
}
/**
* Configures a security realm for a test.
*/
protected UserRealm configureUserRealm() {
HashUserRealm realm = new HashUserRealm();
realm.setName("default"); // this is the magic realm name to make it effective on everywhere
realm.put("alice", "alice");
realm.put("bob", "bob");
realm.put("charlie", "charlie");
realm.addUserToRole("alice", "female");
realm.addUserToRole("bob", "male");
realm.addUserToRole("charlie", "male");
return realm;
}
// /**
// * Sets guest credentials to access java.net Subversion repo.
// */
// protected void setJavaNetCredential() throws SVNException, IOException {
// // set the credential to access svn.dev.java.net
// hudson.getDescriptorByType(SubversionSCM.DescriptorImpl.class).postCredential("https://svn.dev.java.net/svn/hudson/","guest","",null,new PrintWriter(new NullStream()));
// }
/**
* Returns the older default Maven, while still allowing specification of
* other bundled Mavens.
*/
protected MavenInstallation configureDefaultMaven() throws Exception {
return configureDefaultMaven("apache-maven-2.2.1", MavenInstallation.MAVEN_20);
}
protected MavenInstallation configureMaven3() throws Exception {
MavenInstallation mvn = configureDefaultMaven("apache-maven-3.0.1", MavenInstallation.MAVEN_30);
MavenInstallation m3 = new MavenInstallation("apache-maven-3.0.1", mvn.getHome(), NO_PROPERTIES);
hudson.getDescriptorByType(Maven.DescriptorImpl.class).setInstallations(m3);
return m3;
}
/**
* Locates Maven2 and configure that as the only Maven in the system.
*/
protected MavenInstallation configureDefaultMaven(String mavenVersion, int mavenReqVersion) throws Exception {
// first if we are running inside Maven, pick that Maven, if it meets the criteria we require..
// does it exists in the buildDirectory see maven-junit-plugin systemProperties
// buildDirectory -> ${project.build.directory} (so no reason to be null ;-) )
String buildDirectory = System.getProperty("buildDirectory", "./target/classes/");
File mavenAlreadyInstalled = new File(buildDirectory, mavenVersion);
if (mavenAlreadyInstalled.exists()) {
MavenInstallation mavenInstallation = new MavenInstallation("default", mavenAlreadyInstalled.getAbsolutePath(), NO_PROPERTIES);
hudson.getDescriptorByType(Maven.DescriptorImpl.class).setInstallations(mavenInstallation);
return mavenInstallation;
}
String home = System.getProperty("maven.home");
if (home != null) {
MavenInstallation mavenInstallation = new MavenInstallation("default", home, NO_PROPERTIES);
if (mavenInstallation.meetsMavenReqVersion(createLocalLauncher(), mavenReqVersion)) {
hudson.getDescriptorByType(Maven.DescriptorImpl.class).setInstallations(mavenInstallation);
return mavenInstallation;
}
}
// otherwise extract the copy we have.
// this happens when a test is invoked from an IDE, for example.
LOGGER.warning("Extracting a copy of Maven bundled in the test harness. "
+ "To avoid a performance hit, set the system property 'maven.home' to point to a Maven2 installation.");
FilePath mvn = hudson.getRootPath().createTempFile("maven", "zip");
mvn.copyFrom(HudsonTestCase.class.getClassLoader().getResource(mavenVersion + "-bin.zip"));
File mvnHome = new File(buildDirectory);//createTmpDir();
mvn.unzip(new FilePath(mvnHome));
// TODO: switch to tar that preserves file permissions more easily
if (!Functions.isWindows()) {
Util.chmod(new File(mvnHome, mavenVersion + "/bin/mvn"), 0755);
}
MavenInstallation mavenInstallation = new MavenInstallation("default",
new File(mvnHome, mavenVersion).getAbsolutePath(), NO_PROPERTIES);
hudson.getDescriptorByType(Maven.DescriptorImpl.class).setInstallations(mavenInstallation);
return mavenInstallation;
}
/**
* Extracts Ant and configures it.
*/
protected Ant.AntInstallation configureDefaultAnt() throws Exception {
Ant.AntInstallation antInstallation;
if (System.getenv("ANT_HOME") != null) {
antInstallation = new AntInstallation("default", System.getenv("ANT_HOME"), NO_PROPERTIES);
} else {
LOGGER.warning("Extracting a copy of Ant bundled in the test harness. "
+ "To avoid a performance hit, set the environment variable ANT_HOME to point to an Ant installation.");
FilePath ant = hudson.getRootPath().createTempFile("ant", "zip");
ant.copyFrom(HudsonTestCase.class.getClassLoader().getResource("apache-ant-1.8.1-bin.zip"));
File antHome = createTmpDir();
ant.unzip(new FilePath(antHome));
// TODO: switch to tar that preserves file permissions more easily
if (!Functions.isWindows()) {
Util.chmod(new File(antHome, "apache-ant-1.8.1/bin/ant"), 0755);
}
antInstallation = new AntInstallation("default", new File(antHome, "apache-ant-1.8.1").getAbsolutePath(), NO_PROPERTIES);
}
hudson.getDescriptorByType(Ant.DescriptorImpl.class).setInstallations(antInstallation);
return antInstallation;
}
//
// Convenience methods
//
protected FreeStyleProject createFreeStyleProject() throws IOException {
return createFreeStyleProject(createUniqueProjectName());
}
protected FreeStyleProject createFreeStyleProject(String name) throws IOException {
return hudson.createProject(FreeStyleProject.class, name);
}
protected MatrixProject createMatrixProject() throws IOException {
return createMatrixProject(createUniqueProjectName());
}
protected MatrixProject createMatrixProject(String name) throws IOException {
return hudson.createProject(MatrixProject.class, name);
}
protected String createUniqueProjectName() {
return "test" + hudson.getItems().size();
}
/**
* Creates {@link LocalLauncher}. Useful for launching processes.
*/
protected LocalLauncher createLocalLauncher() {
return new LocalLauncher(StreamTaskListener.fromStdout());
}
/**
* Allocates a new temporary directory for the duration of this test.
*/
public File createTmpDir() throws IOException {
return env.temporaryDirectoryAllocator.allocate();
}
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 SecurityRealm} that recognizes username==password
* as valid.
*/
public SecurityRealm createDummySecurityRealm() {
return new AbstractPasswordBasedSecurityRealm() {
@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 {
return new org.springframework.security.core.userdetails.User(username, "", true, true, true, true, Arrays.asList(new GrantedAuthority[]{AUTHENTICATED_AUTHORITY}));
}
@Override
public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException {
throw new UsernameNotFoundException(groupname);
}
};
}
/**
* 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 (hudson) {
// this synchronization block is so that we don't end up adding the same slave name more than once.
int sz = hudson.getNodes().size();
DumbSlave slave = new DumbSlave("slave" + sz, "dummy",
createTmpDir().getPath(), "1", Mode.NORMAL, labels == null ? "" : labels, createComputerLauncher(env), RetentionStrategy.NOOP, Collections.EMPTY_LIST);
hudson.addNode(slave);
return slave;
}
}
public DumbSlave createSlave(String nodeName, String labels, EnvVars env) throws Exception {
synchronized (hudson) {
DumbSlave slave = new DumbSlave(nodeName, "dummy",
createTmpDir().getPath(), "1", Node.Mode.NORMAL, labels==null?"":labels, createComputerLauncher(env), RetentionStrategy.NOOP, Collections.EMPTY_LIST);
hudson.addNode(slave);
return slave;
}
}
public PretendSlave createPretendSlave(FakeLauncher faker) throws Exception {
synchronized (hudson) {
int sz = hudson.getNodes().size();
PretendSlave slave = new PretendSlave("slave" + sz, createTmpDir().getPath(), "", createComputerLauncher(null), faker);
hudson.addNode(slave);
return slave;
}
}
/**
* Creates a {@link CommandLauncher} for launching a slave locally.
*
* @param env Environment variables to add to the slave process. Can be
* null.
*/
public CommandLauncher createComputerLauncher(EnvVars env) throws URISyntaxException, MalformedURLException {
int sz = hudson.getNodes().size();
return new CommandLauncher(
String.format("\"%s/bin/java\" %s -jar \"%s\"",
System.getProperty("java.home"),
SLAVE_DEBUG_PORT > 0 ? " -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=" + (SLAVE_DEBUG_PORT + sz) : "",
new File(hudson.getJnlpJars("slave.jar").getURL().toURI()).getAbsolutePath()),
env);
}
/**
* Create a new slave on the local host and wait for it to come onilne
* before returning.
*/
public DumbSlave createOnlineSlave() throws Exception {
return createOnlineSlave(null);
}
/**
* Create a new slave on the local host and wait for it to come onilne
* 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
*/
public DumbSlave createOnlineSlave(Label l, EnvVars env) throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
ComputerListener waiter = new ComputerListener() {
@Override
public void onOnline(Computer C, TaskListener t) {
latch.countDown();
unregister();
}
};
waiter.register();
DumbSlave s = createSlave(l, env);
latch.await();
return s;
}
/**
* 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("Hudson is running at http://localhost:" + localPort + "/");
new BufferedReader(new InputStreamReader(System.in)).readLine();
}
/**
* Returns the last item in the list.
*/
protected <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.
*/
protected void pause() throws IOException {
new BufferedReader(new InputStreamReader(System.in)).readLine();
}
/**
* Performs a search from the search box.
*/
protected Page search(String q) throws Exception {
return new WebClient().search(q);
}
/**
* Hits the Hudson system configuration and submits without any
* modification.
*/
protected 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.hudson-ci.org/display/HUDSON/Unit+Test#UnitTest-Configurationroundtriptesting
*/
protected <P extends Job> P configRoundtrip(P job) throws Exception {
submit(createWebClient().getPage(job, "configure").getFormByName("config"));
return job;
}
// protected <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.
*/
protected <B extends Builder> B configRoundtrip(B before) throws Exception {
FreeStyleProject p = createFreeStyleProject();
p.getBuildersList().add(before);
configRoundtrip(p);
return (B) p.getBuildersList().get(before.getClass());
}
/**
* Performs a configuration round-trip testing for a publisher.
*/
protected <P extends Publisher> P configRoundtrip(P before) throws Exception {
FreeStyleProject p = createFreeStyleProject();
p.getPublishersList().add(before);
configRoundtrip(p);
return (P) p.getPublishersList().get(before.getClass());
}
protected <C extends ComputerConnector> C configRoundtrip(C before) throws Exception {
computerConnectorTester.connector = before;
submit(createWebClient().goTo("self/computerConnectorTester/configure").getFormByName("config"));
return (C) computerConnectorTester.connector;
}
/**
* 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
String msg = "unexpected build status; build log was:\n------\n" + getLog(r) + "\n------\n";
if (r instanceof MatrixBuild) {
MatrixBuild mb = (MatrixBuild) r;
for (MatrixRun mr : mb.getRuns()) {
msg += "--- " + mr.getParent().getCombination() + " ---\n" + getLog(mr) + "\n------\n";
}
}
assertEquals(msg, status, r.getResult());
return r;
}
/**
* Determines whether the specifed 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 specifed 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) {
assertTrue(isGoodHttpStatus(page.getWebResponse().getStatusCode()));
}
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 {
assertNotNull("build was actually scheduled", r);
return assertBuildStatusSuccess(r.get());
}
public <J extends AbstractProject<J, R>, R extends AbstractBuild<J, R>> R buildAndAssertSuccess(J job) throws Exception {
return assertBuildStatusSuccess(job.scheduleBuild2(0));
}
/**
* Avoids need for cumbersome {@code this.<J,R>buildAndAssertSuccess(...)}
* type hints under JDK 7 javac (and supposedly also IntelliJ).
*/
public FreeStyleBuild buildAndAssertSuccess(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 Exception {
String log = getLog(run);
if (log.contains(substring)) {
return; // good!
}
System.out.println(log);
fail("Console output of " + run + " didn't contain " + substring);
}
/**
* Get entire log file (this method is deprecated in hudson.model.Run, but
* in tests it is OK to load entire log).
*/
protected static String getLog(Run run) throws IOException {
return Util.loadFile(run.getLogFile(), run.getCharset());
}
/**
* Asserts that the XPath matches.
*/
public void assertXPath(HtmlPage page, String xpath) {
assertNotNull("There should be an object that matches XPath:" + xpath,
page.getDocumentElement().selectSingleNode(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);
assertFalse("There should be an object that matches XPath:" + xpath, nodes.isEmpty());
}
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);
assertFalse("no nodes matching xpath found", nodes.isEmpty());
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;
}
}
}
assertTrue("needle found in haystack", found);
}
public void assertStringContains(String message, String haystack, String needle) {
if (haystack.contains(needle)) {
// good
return;
} else {
fail(message + " (seeking '" + needle + "')");
}
}
public void assertStringContains(String haystack, String needle) {
if (haystack.contains(needle)) {
// good
return;
} else {
fail("Could not find '" + 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 = hudson.getDescriptor(type);
WebClient wc = createWebClient();
for (String property : listProperties(properties)) {
String url = d.getHelpFile(property);
assertNotNull("Help file for the property " + property + " is missing on " + type, url);
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 ArrayList<String>(Arrays.asList(properties.split(",")));
for (String p : props.toArray(new String[props.size()])) {
if (p.startsWith("-")) {
props.remove(p);
props.remove(p.substring(1));
}
}
return props;
}
/**
* Submits the form.
*
* Plain {@link HtmlForm#submit()} doesn't work correctly due to the use of
* YUI in Hudson.
*/
public HtmlPage submit(HtmlForm form) throws Exception {
List<HtmlButton> buttons = form.getHtmlElementsByTagName("button");
if ((buttons != null) && (buttons.size() > 0)) {
HtmlButton button = (HtmlButton) last(form.getHtmlElementsByTagName("button"));
return (HtmlPage) form.submit(button);
} else {
return (HtmlPage) form.submit();
}
}
/**
* Submits the form by clikcing the submit button of the given name.
*
* @param name This corresponds to the
* @name of <f:submit />
*/
public HtmlPage submit(HtmlForm form, String name) throws Exception {
for (HtmlElement e : form.getHtmlElementsByTagName("button")) {
HtmlElement p = (HtmlElement) e.getParentNode().getParentNode();
if (p.getAttribute("name").equals(name)) {
// To make YUI event handling work, this combo seems to be necessary
// the click will trigger _onClick in buton-*.js, but it doesn't submit the form
// (a comment alluding to this behavior can be seen in submitForm method)
// so to complete it, submit the form later.
//
// Just doing form.submit() doesn't work either, because it doesn't do
// the preparation work needed to pass along the name of the button that
// triggered a submission (more concretely, m_oSubmitTrigger is not set.)
((HtmlButton) e).click();
return (HtmlPage) form.submit((HtmlButton) e);
}
}
for (HtmlElement e : form.getHtmlElementsByTagName("input")) {
if (e.getAttribute("name").equals(name)) {
((HtmlInput) e).click();
return (HtmlPage) form.submit((HtmlInput) e);
}
}
throw new AssertionError("No such submit button with the name " + name);
}
protected HtmlInput findPreviousInputElement(HtmlElement current, String name) {
return (HtmlInput) current.selectSingleNode("(preceding::input[@name='_." + name + "'])[last()]");
}
protected HtmlButton getButtonByCaption(HtmlForm f, String s) {
for (HtmlElement b : f.getHtmlElementsByTagName("button")) {
if (b.getTextContent().trim().equals(s)) {
return (HtmlButton) b;
}
}
return null;
}
protected HtmlInput getInputByName(HtmlForm f, String name) {
for (HtmlElement b : f.getHtmlElementsByTagName("button")) {
if (b.getAttribute("name").equals(name)) {
return (HtmlInput) 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 #assertEquals(Object, Object)})
*
* <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 {
assertNotNull("lhs is null", lhs);
assertNotNull("rhs is null", rhs);
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) {
assertNotNull("No such property " + p + " on " + lhs.getClass(), pd);
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);
assertEquals("Array length is different for property " + p, m, n);
for (int i = 0; i < m; i++) {
assertEquals(p + "[" + i + "] is different", Array.get(lp, i), Array.get(rp, i));
}
return;
}
assertEquals("Property " + p + " is different", lp, rp);
}
}
/**
* Works like {@link #assertEqualBeans(Object, Object, String)} but figure
* out the properties via {@link DataBoundConstructor}
*/
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=" + rhs);
}
Constructor<?> lc = findDataBoundConstructor(lhs.getClass());
Constructor<?> rc = findDataBoundConstructor(rhs.getClass());
assertEquals("Data bound constructor mismatch. Different type?", lc, rc);
List<String> primitiveProperties = new ArrayList<String>();
String[] names = ClassDescriptor.loadParameterNames(lc);
Class<?>[] types = lc.getParameterTypes();
assertEquals(names.length, types.length);
for (int i = 0; i < types.length; i++) {
Object lv = ReflectionUtils.getPublicProperty(lhs, names[i]);
Object rv = ReflectionUtils.getPublicProperty(rhs, names[i]);
if (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 {
assertEquals(litem, ritem);
}
}
assertFalse("collection size mismatch between " + lhs + " and " + rhs, ltr.hasNext() ^ rtr.hasNext());
} else if (findDataBoundConstructor(types[i]) != 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, ","));
}
}
private 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.
*/
protected <T extends Descriptor<?>> T get(Class<T> d) {
return hudson.getDescriptorByType(d);
}
/**
* Returns true if Hudson is building something or going to build something.
*/
protected boolean isSomethingHappening() {
if (!hudson.getQueue().isEmpty()) {
return true;
}
for (Computer n : hudson.getComputers()) {
if (!n.isIdle()) {
return true;
}
}
return false;
}
/**
* Waits until Hudson finishes building everything, including those in the
* queue. <p> This method uses a default time out to prevent infinite hang
* in the automated test execution environment.
*/
protected void waitUntilNoActivity() throws Exception {
waitUntilNoActivityUpTo(60 * 1000);
}
/**
* Waits until Hudson finishes building everything, including those in the
* queue, or fail the test if the specified timeout milliseconds is
*/
protected void waitUntilNoActivityUpTo(int timeout) throws Exception {
long startTime = System.currentTimeMillis();
int streak = 0;
while (true) {
Thread.sleep(10);
if (isSomethingHappening()) {
streak = 0;
} else {
streak++;
}
// the system is quiet for a while
if (streak > 5) {
return;
}
if (System.currentTimeMillis() - startTime > timeout) {
List<Executable> building = new ArrayList<Executable>();
for (Computer c : hudson.getComputers()) {
for (Executor e : c.getExecutors()) {
if (e.isBusy()) {
building.add(e.getCurrentExecutable());
}
}
}
throw new AssertionError(String.format("Hudson is still doing something after %dms: queue=%s building=%s",
timeout, Arrays.asList(hudson.getQueue().getItems()), building));
}
}
}
//
// recipe methods. Control the test environments.
//
/**
* Called during the {@link #setUp()} 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.
*/
protected void recipe() throws Exception {
recipeLoadCurrentPlugin();
// look for recipe meta-annotation
try {
Method runMethod = getClass().getMethod(getName());
for (final Annotation a : runMethod.getAnnotations()) {
Recipe r = a.annotationType().getAnnotation(Recipe.class);
if (r == null) {
continue;
}
final Runner runner = r.value().newInstance();
recipes.add(runner);
tearDowns.add(new LenientRunnable() {
public void run() throws Exception {
runner.tearDown(HudsonTestCase.this, a);
}
});
runner.setup(this, a);
}
} catch (NoSuchMethodException e) {
// not a plain JUnit test.
}
}
/**
* If this test harness is launched for a Hudson plugin, locate the
* <tt>target/test-classes/the.hpl</tt> and add a recipe to install that to
* the new Hudson.
*
* <p> This file is created by <tt>maven-hpi-plugin</tt> at the testCompile
* phase when the current packaging is <tt>hpi</tt>.
*/
protected void recipeLoadCurrentPlugin() throws Exception {
final Enumeration<URL> e = getClass().getClassLoader().getResources("the.hpl");
if (!e.hasMoreElements()) {
return; // nope
}
recipes.add(new Runner() {
@Override
public void decorateHome(HudsonTestCase testCase, File home) throws Exception {
while (e.hasMoreElements()) {
final URL hpl = e.nextElement();
// make the plugin itself available
Manifest m = new Manifest(hpl.openStream());
String shortName = m.getMainAttributes().getValue("Short-Name");
if (shortName == null) {
throw new Error(hpl + " doesn't have the Short-Name attribute");
}
final File targetLocation = new File(home, "plugins/" + shortName + ".hpl");
FileUtils.copyURLToFile(hpl, targetLocation);
// Clear libraries and resource-path to prevent duplicate classes
//
{
Manifest copied = null;
try (FileInputStream fis = new FileInputStream(targetLocation))
{
copied = new Manifest(fis);
copied.getMainAttributes().putValue("Libraries", "");
copied.getMainAttributes().putValue("Resource-Path", "");
}
try (FileOutputStream fos = new FileOutputStream(targetLocation))
{
copied.write(fos);
}
}
// 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. As a backup, we do a best-effort lookup in the Maven repository
// For transitive dependencies, we could evaluate Plugin-Dependencies transitively.
String dependencies = m.getMainAttributes().getValue("Plugin-Dependencies");
if (dependencies != null) {
MavenRequest mavenRequest = MavenUtil.createMavenRequest(new StreamTaskListener(System.out,Charset.defaultCharset()));
MavenEmbedder embedder = new MavenEmbedder(getClass().getClassLoader(), mavenRequest);
for (String dep : dependencies.split(",")) {
String[] tokens = dep.split(":");
String artifactId = tokens[0];
String version = tokens[1];
File dependencyJar = null;
// need to search multiple group IDs
// TODO: extend manifest to include groupID:artifactID:version
Exception resolutionError = null;
for (String groupId : new String[]{"org.hudsonci.plugins", "org.jvnet.hudson.plugins", "org.jvnet.hudson.main"}) {
// first try to find it on the classpath.
// this takes advantage of Maven POM located in POM
URL dependencyPomResource = getClass().getResource("/META-INF/maven/" + groupId + "/" + artifactId + "/pom.xml");
if (dependencyPomResource != null) {
// found it
dependencyJar = Which.jarFile(dependencyPomResource);
break;
} else {
Artifact a;
a = embedder.createArtifact(groupId, artifactId, version, "compile"/*doesn't matter*/, "hpi");
try {
embedder.resolve(a, Arrays.asList(embedder.createRepository("https://oss.sonatype.org/content/repositories/releases/", "repo")), embedder.getLocalRepository());
dependencyJar = a.getFile();
} catch (AbstractArtifactResolutionException x) {
// could be a wrong groupId
resolutionError = x;
}
}
}
if (dependencyJar == null) {
throw new Exception("Failed to resolve plugin: " + dep, resolutionError);
}
File dst = new File(home, "plugins/" + artifactId + ".hpi");
if (!dst.exists() || dst.lastModified() != dependencyJar.lastModified()) {
FileUtils.copyFile(dependencyJar, dst);
}
}
}
}
}
});
}
public HudsonTestCase withNewHome() {
return with(HudsonHomeLoader.NEW);
}
public HudsonTestCase withExistingHome(File source) throws Exception {
return with(new CopyExisting(source));
}
/**
* Declares that this test case expects to start with one of the preset data
* sets. See
* https://svn.dev.java.net/svn/hudson/trunk/hudson/main/test/src/main/preset-data/
* for available datasets and what they mean.
*/
public HudsonTestCase 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 CopyExisting(res));
}
public HudsonTestCase 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 HudsonTestCase}, 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 StaplerRequest} and {@link StaplerResponse},
* or getting the credential of the current user (via
* {@link Hudson#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 = hudson.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;
}
}
});
createWebClient().goTo("closures/?uuid=" + id);
if (t[0] != null) {
throw t[0];
}
return r.get(0);
}
/**
* Sometimes a part of a test case may ends up creeping into the
* serialization tree of {@link Saveable#save()}, so detect that and flag
* that as an error.
*/
private Object writeReplace() {
throw new AssertionError("HudsonTestCase " + getName() + " is not supposed to be serialized");
}
/**
* @return A resource that allows testing of REST services, at the root
* of the application
*/
public WebResource createJaxRSClient() {
Client client = Client.create(customizeJaxRSClient(new DefaultClientConfig()));
return client.resource("http://localhost:" + localPort + contextPath + "/");
}
/**
* All sub classes to customize the client configuration
* @param client
*/
protected ClientConfig customizeJaxRSClient(ClientConfig client) {
// To be overridden
return client;
}
/**
* 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 {
public WebClient() {
// default is IE6, but this causes 'n.doScroll('left')' to fail in event-debug.js:1907 as HtmlUnit doesn't implement such a method,
// so trying something else, until we discover another problem.
super(BrowserVersion.FIREFOX_3);
// setJavaScriptEnabled(false);
setPageCreator(HudsonPageCreator.INSTANCE);
clients.add(new WeakReference<WebClient>(this));
// make ajax calls run as post-action for predictable behaviors that simplify debugging
setAjaxController(new AjaxController() {
public boolean processSynchron(HtmlPage page, WebRequestSettings settings, boolean async) {
return false;
}
});
setCssErrorHandler(new ErrorHandler() {
final ErrorHandler defaultHandler = new DefaultCssErrorHandler();
public void warning(CSSParseException exception) throws CSSException {
if (!ignore(exception)) {
defaultHandler.warning(exception);
}
}
public void error(CSSParseException exception) throws CSSException {
if (!ignore(exception)) {
defaultHandler.error(exception);
}
}
public void fatalError(CSSParseException exception) throws CSSException {
if (!ignore(exception)) {
defaultHandler.fatalError(exception);
}
}
private boolean ignore(CSSParseException e) {
return e.getURI().contains("/yui/");
}
});
// if no other debugger is installed, install jsDebugger,
// so as not to interfere with the 'Dim' class.
getJavaScriptEngine().getContextFactory().addListener(new Listener() {
public void contextCreated(Context cx) {
if (cx.getDebugger() == null) {
cx.setDebugger(jsDebugger, null);
}
}
public void contextReleased(Context cx) {
}
});
}
/**
* Logs in to Hudson.
*/
public WebClient login(String username, String password) 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);
form.submit(null);
return this;
}
/**
* Logs in to Hudson, by using the user name as the password.
*
* <p> See {@link HudsonTestCase#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;
}
public HtmlPage search(String q) throws IOException, SAXException {
HtmlPage top = goTo("");
HtmlForm search = top.getFormByName("search");
search.getInputByName("q").setValueAttribute(q);
return (HtmlPage) search.submit(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.)
*/
@Override
public Page getPage(String url) throws IOException, FailingHttpStatusCodeException {
return super.getPage(url);
}
/**
* Requests a page within Hudson.
*
* @param relative Relative path within Hudson. 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());
}
}
public Page goTo(String relative, String expectedContentType) throws IOException, SAXException {
Page p = super.getPage(getContextPath() + relative);
assertEquals(expectedContentType, p.getWebResponse().getContentType());
return p;
}
/**
* Loads a page as XML. Useful for testing Hudson'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;
}
}
/**
* Returns the URL of the webapp top page. URL ends with '/'.
*/
public String getContextPath() throws IOException {
return getURL().toExternalForm();
}
/**
* Adds a security crumb to the quest
*/
public WebRequestSettings addCrumb(WebRequestSettings req) {
NameValuePair crumb[] = {new NameValuePair()};
crumb[0].setName(hudson.getCrumbIssuer().getDescriptor().getCrumbRequestField());
crumb[0].setValue(hudson.getCrumbIssuer().getCrumb(null));
req.setRequestParameters(Arrays.asList(crumb));
return req;
}
/**
* Creates a URL with crumb parameters relative to
* {{@link #getContextPath()}
*/
public URL createCrumbedUrl(String relativePath) throws IOException {
CrumbIssuer issuer = hudson.getCrumbIssuer();
String crumbName = issuer.getDescriptor().getCrumbRequestField();
String crumb = issuer.getCrumb(null);
return new URL(getContextPath() + relativePath + "?" + crumbName + "=" + crumb);
}
/**
* 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 = hudson.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 Dim.StackFrame#onLineChange(Context, int)}.
*/
public Dim interactiveJavaScriptDebugger() {
Global global = new Global();
HtmlUnitContextFactory cf = getJavaScriptEngine().getContextFactory();
global.init(cf);
Dim dim = org.mozilla.javascript.tools.debugger.Main.mainEmbedded(cf, global, "Rhino debugger: " + getName());
// 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());
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;
// 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 INFO output from Spring, which is verbose
Logger.getLogger("org.springframework").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.");
}
});
// 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());
protected static final List<ToolProperty<?>> NO_PROPERTIES = Collections.<ToolProperty<?>>emptyList();
/**
* Specify this to a TCP/IP port number to have slaves started with the
* debugger.
*/
public static int SLAVE_DEBUG_PORT = Integer.getInteger(HudsonTestCase.class.getName() + ".slaveDebugPort", -1);
public static final MimeTypes MIME_TYPES = new MimeTypes();
static {
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;
}
}