/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Yahoo! Inc., Tom Huybrechts * * 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.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.maven.MavenModuleSet; import hudson.maven.MavenEmbedder; import hudson.model.Node.Mode; import hudson.security.csrf.CrumbIssuer; import hudson.slaves.CommandLauncher; import hudson.slaves.DumbSlave; import hudson.slaves.RetentionStrategy; import hudson.stapler.WebAppController; import hudson.stapler.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.jna.GNUCLibrary; 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.acegisecurity.AuthenticationException; import org.acegisecurity.BadCredentialsException; import org.acegisecurity.GrantedAuthority; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.userdetails.UserDetails; import org.acegisecurity.userdetails.UsernameNotFoundException; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.io.FileUtils; import org.apache.commons.beanutils.PropertyUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException; 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 hudson.maven.MavenBuild; import hudson.maven.MavenModule; import hudson.maven.MavenModuleSetBuild; import hudson.slaves.ComputerListener; import java.util.concurrent.CountDownLatch; /** * 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","?"); WebAppMain.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(); // 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(); 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()) GNUCLibrary.LIBC.chmod(new File(mvnHome,mavenVersion+"/bin/mvn").getPath(),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()) GNUCLibrary.LIBC.chmod(new File(antHome,"apache-ant-1.8.1/bin/ant").getPath(),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); } /** * Creates a empty Maven project with an unique name. * * @see #configureDefaultMaven() */ protected MavenModuleSet createMavenProject() throws IOException { return createMavenProject(createUniqueProjectName()); } /** * Creates a empty Maven project with the given name. * * @see #configureDefaultMaven() */ protected MavenModuleSet createMavenProject(String name) throws IOException { return hudson.createProject(MavenModuleSet.class,name); } private 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.acegisecurity.userdetails.User(username,"",true,true,true,true,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 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)); } public MavenModuleSetBuild buildAndAssertSuccess(MavenModuleSet job) throws Exception { return assertBuildStatusSuccess(job.scheduleBuild2(0)); } public MavenBuild buildAndAssertSuccess(MavenModule 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 { return (HtmlPage)form.submit((HtmlButton)last(form.getHtmlElementsByTagName("button"))); } /** * 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); } } 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; } /** * 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++; if (streak>5) // the system is quiet for a while 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 final URL hpl = e.nextElement(); 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"); FileUtils.copyURLToFile(hpl,new File(home,"plugins/"+shortName+".hpl")); // 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) { MavenEmbedder embedder = new MavenEmbedder(getClass().getClassLoader(), null); 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.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("http://maven.glassfish.org/content/groups/public/","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"); } /** * 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; } }