/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
* 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 hudson;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.PluginManager.UberClassLoader;
import hudson.model.Hudson;
import hudson.model.UpdateCenter;
import hudson.model.UpdateCenter.UpdateCenterJob;
import hudson.model.UpdateSite;
import hudson.scm.SubversionSCM;
import hudson.util.FormValidation;
import hudson.util.PersistedList;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import jenkins.RestartRequiredException;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;
import org.apache.tools.ant.filters.StringInputStream;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.Url;
import org.jvnet.hudson.test.recipes.WithPlugin;
import org.jvnet.hudson.test.recipes.WithPluginManager;
/**
* @author Kohsuke Kawaguchi
*/
public class PluginManagerTest {
@Rule public JenkinsRule r = PluginManagerUtil.newJenkinsRule();
@Rule public TemporaryFolder tmp = new TemporaryFolder();
/**
* Manual submission form.
*/
@Test public void uploadJpi() throws Exception {
HtmlPage page = r.createWebClient().goTo("pluginManager/advanced");
HtmlForm f = page.getFormByName("uploadPlugin");
File dir = tmp.newFolder();
File plugin = new File(dir, "tasks.jpi");
FileUtils.copyURLToFile(getClass().getClassLoader().getResource("plugins/tasks.jpi"),plugin);
f.getInputByName("name").setValueAttribute(plugin.getAbsolutePath());
r.submit(f);
assertTrue( new File(r.jenkins.getRootDir(),"plugins/tasks.jpi").exists() );
}
/**
* Manual submission form.
*/
@Test public void uploadHpi() throws Exception {
HtmlPage page = r.createWebClient().goTo("pluginManager/advanced");
HtmlForm f = page.getFormByName("uploadPlugin");
File dir = tmp.newFolder();
File plugin = new File(dir, "legacy.hpi");
FileUtils.copyURLToFile(getClass().getClassLoader().getResource("plugins/legacy.hpi"),plugin);
f.getInputByName("name").setValueAttribute(plugin.getAbsolutePath());
r.submit(f);
// uploaded legacy plugins get renamed to *.jpi
assertTrue( new File(r.jenkins.getRootDir(),"plugins/legacy.jpi").exists() );
}
/**
* Tests the effect of {@link WithPlugin}.
*/
@WithPlugin("tasks.jpi")
@Test public void withRecipeJpi() throws Exception {
assertNotNull(r.jenkins.getPlugin("tasks"));
}
/**
* Tests the effect of {@link WithPlugin}.
*/
@WithPlugin("legacy.hpi")
@Test public void withRecipeHpi() throws Exception {
assertNotNull(r.jenkins.getPlugin("legacy"));
}
/**
* Makes sure that plugins can see Maven2 plugin that's refactored out in 1.296.
*/
@WithPlugin("tasks.jpi")
@Test public void optionalMavenDependency() throws Exception {
PluginWrapper.Dependency m2=null;
PluginWrapper tasks = r.jenkins.getPluginManager().getPlugin("tasks");
for( PluginWrapper.Dependency d : tasks.getOptionalDependencies() ) {
if(d.shortName.equals("maven-plugin")) {
assertNull(m2);
m2 = d;
}
}
assertNotNull(m2);
// this actually doesn't really test what we need, though, because
// I thought test harness is loading the maven classes by itself.
// TODO: write a separate test that tests the optional dependency loading
tasks.classLoader.loadClass(hudson.maven.agent.AbortException.class.getName());
}
/**
* Verifies that by the time {@link Plugin#start()} is called, uber classloader is fully functioning.
* This is necessary as plugin start method can engage in XStream loading activities, and they should
* resolve all the classes in the system (for example, a plugin X can define an extension point
* other plugins implement, so when X loads its config it better sees all the implementations defined elsewhere)
*/
@WithPlugin("tasks.jpi")
@WithPluginManager(PluginManagerImpl_for_testUberClassLoaderIsAvailableDuringStart.class)
@Test public void uberClassLoaderIsAvailableDuringStart() {
assertTrue(((PluginManagerImpl_for_testUberClassLoaderIsAvailableDuringStart) r.jenkins.pluginManager).tested);
}
public static class PluginManagerImpl_for_testUberClassLoaderIsAvailableDuringStart extends LocalPluginManager {
boolean tested;
public PluginManagerImpl_for_testUberClassLoaderIsAvailableDuringStart(File rootDir) {
super(rootDir);
}
@Override
protected PluginStrategy createPluginStrategy() {
return new ClassicPluginStrategy(this) {
@Override
public void startPlugin(PluginWrapper plugin) throws Exception {
tested = true;
// plugins should be already visible in the UberClassLoader
assertTrue(!activePlugins.isEmpty());
uberClassLoader.loadClass(SubversionSCM.class.getName());
uberClassLoader.loadClass("hudson.plugins.tasks.Messages");
super.startPlugin(plugin);
}
};
}
}
/**
* Makes sure that thread context classloader isn't used by {@link UberClassLoader}, or else
* infinite cycle ensues.
*/
@Url("http://jenkins.361315.n4.nabble.com/channel-example-and-plugin-classes-gives-ClassNotFoundException-td3756092.html")
@Test public void uberClassLoaderDoesntUseContextClassLoader() throws Exception {
Thread t = Thread.currentThread();
URLClassLoader ucl = new URLClassLoader(new URL[0], r.jenkins.pluginManager.uberClassLoader);
ClassLoader old = t.getContextClassLoader();
t.setContextClassLoader(ucl);
try {
try {
ucl.loadClass("No such class");
fail();
} catch (ClassNotFoundException e) {
// as expected
}
ucl.loadClass(Hudson.class.getName());
} finally {
t.setContextClassLoader(old);
}
}
@Test public void installWithoutRestart() throws Exception {
URL res = getClass().getClassLoader().getResource("plugins/htmlpublisher.jpi");
File f = new File(r.jenkins.getRootDir(), "plugins/htmlpublisher.jpi");
FileUtils.copyURLToFile(res, f);
r.jenkins.pluginManager.dynamicLoad(f);
Class c = r.jenkins.getPluginManager().uberClassLoader.loadClass("htmlpublisher.HtmlPublisher$DescriptorImpl");
assertNotNull(r.jenkins.getDescriptorByType(c));
}
@Test public void prevalidateConfig() throws Exception {
PersistedList<UpdateSite> sites = r.jenkins.getUpdateCenter().getSites();
sites.clear();
URL url = PluginManagerTest.class.getResource("/plugins/tasks-update-center.json");
UpdateSite site = new UpdateSite(UpdateCenter.ID_DEFAULT, url.toString());
sites.add(site);
assertEquals(FormValidation.ok(), site.updateDirectly(false).get());
assertNotNull(site.getData());
assertEquals(Collections.emptyList(), r.jenkins.getPluginManager().prevalidateConfig(new StringInputStream("<whatever><runant plugin=\"ant@1.1\"/></whatever>")));
assertNull(r.jenkins.getPluginManager().getPlugin("tasks"));
List<Future<UpdateCenterJob>> jobs = r.jenkins.getPluginManager().prevalidateConfig(new StringInputStream("<whatever><tasks plugin=\"tasks@2.23\"/></whatever>"));
assertEquals(1, jobs.size());
UpdateCenterJob job = jobs.get(0).get(); // blocks for completion
assertEquals("InstallationJob", job.getType());
UpdateCenter.InstallationJob ijob = (UpdateCenter.InstallationJob) job;
assertEquals("tasks", ijob.plugin.name);
assertNotNull(r.jenkins.getPluginManager().getPlugin("tasks"));
// TODO restart scheduled (SuccessButRequiresRestart) after upgrade or Support-Dynamic-Loading: false
// TODO dependencies installed or upgraded too
// TODO required plugin installed but inactive
}
// plugin "depender" optionally depends on plugin "dependee".
// they are written like this:
// org.jenkinsci.plugins.dependencytest.dependee:
// public class Dependee {
// public static String getValue() {
// return "dependee";
// }
// }
//
// public abstract class DependeeExtensionPoint implements ExtensionPoint {
// }
//
// org.jenkinsci.plugins.dependencytest.depender:
// public class Depender {
// public static String getValue() {
// if (Jenkins.getInstance().getPlugin("dependee") != null) {
// return Dependee.getValue();
// }
// return "depender";
// }
// }
//
// @Extension(optional=true)
// public class DependerExtension extends DependeeExtensionPoint {
// }
/**
* call org.jenkinsci.plugins.dependencytest.depender.Depender.getValue().
*
* @return
* @throws Exception
*/
private String callDependerValue() throws Exception {
Class<?> c = r.jenkins.getPluginManager().uberClassLoader.loadClass("org.jenkinsci.plugins.dependencytest.depender.Depender");
Method m = c.getMethod("getValue");
return (String)m.invoke(null);
}
/**
* Load "dependee" and then load "depender".
* Asserts that "depender" can access to "dependee".
*
* @throws Exception
*/
@Test public void installDependingPluginWithoutRestart() throws Exception {
// Load dependee.
{
dynamicLoad("dependee.hpi");
}
// before load depender, of course failed to call Depender.getValue()
try {
callDependerValue();
fail();
} catch (ClassNotFoundException _) {
}
// No extensions exist.
assertTrue(r.jenkins.getExtensionList("org.jenkinsci.plugins.dependencytest.dependee.DependeeExtensionPoint").isEmpty());
// Load depender.
{
dynamicLoad("depender.hpi");
}
// depender successfully accesses to dependee.
assertEquals("dependee", callDependerValue());
// Extension in depender is loaded.
assertFalse(r.jenkins.getExtensionList("org.jenkinsci.plugins.dependencytest.dependee.DependeeExtensionPoint").isEmpty());
}
/**
* Load "depender" and then load "dependee".
* Asserts that "depender" can access to "dependee".
*
* @throws Exception
*/
@Issue("JENKINS-19976")
@Test public void installDependedPluginWithoutRestart() throws Exception {
// Load depender.
{
dynamicLoad("depender.hpi");
}
// before load dependee, depender does not access to dependee.
assertEquals("depender", callDependerValue());
// before load dependee, of course failed to list extensions for dependee.
try {
r.jenkins.getExtensionList("org.jenkinsci.plugins.dependencytest.dependee.DependeeExtensionPoint");
fail();
} catch( ClassNotFoundException _ ){
}
// Load dependee.
{
dynamicLoad("dependee.hpi");
}
// (MUST) Not throws an exception
// (SHOULD) depender successfully accesses to dependee.
assertEquals("dependee", callDependerValue());
// No extensions exist.
// extensions in depender is not loaded.
assertTrue(r.jenkins.getExtensionList("org.jenkinsci.plugins.dependencytest.dependee.DependeeExtensionPoint").isEmpty());
}
@Issue("JENKINS-21486")
@Test public void installPluginWithObsoleteDependencyFails() throws Exception {
// Load dependee 0.0.1.
{
dynamicLoad("dependee.hpi");
}
// Load mandatory-depender 0.0.2, depending on dependee 0.0.2
try {
dynamicLoad("mandatory-depender-0.0.2.hpi");
fail("Should not have worked");
} catch (IOException e) {
// Expected
}
}
@Issue("JENKINS-21486")
@Test public void installPluginWithDisabledOptionalDependencySucceeds() throws Exception {
// Load dependee 0.0.2.
{
dynamicLoadAndDisable("dependee-0.0.2.hpi");
}
// Load depender 0.0.2, depending optionally on dependee 0.0.2
{
dynamicLoad("depender-0.0.2.hpi");
}
// dependee is not loaded so we cannot list any extension for it.
try {
r.jenkins.getExtensionList("org.jenkinsci.plugins.dependencytest.dependee.DependeeExtensionPoint");
fail();
} catch( ClassNotFoundException _ ){
}
}
@Issue("JENKINS-21486")
@Test public void installPluginWithDisabledDependencyFails() throws Exception {
// Load dependee 0.0.2.
{
dynamicLoadAndDisable("dependee-0.0.2.hpi");
}
// Load mandatory-depender 0.0.2, depending on dependee 0.0.2
try {
dynamicLoad("mandatory-depender-0.0.2.hpi");
fail("Should not have worked");
} catch (IOException e) {
// Expected
}
}
@Issue("JENKINS-21486")
@Test public void installPluginWithObsoleteOptionalDependencyFails() throws Exception {
// Load dependee 0.0.1.
{
dynamicLoad("dependee.hpi");
}
// Load depender 0.0.2, depending optionally on dependee 0.0.2
try {
dynamicLoad("depender-0.0.2.hpi");
fail("Should not have worked");
} catch (IOException e) {
// Expected
}
}
@Issue("JENKINS-12753")
@WithPlugin("tasks.jpi")
@Test public void dynamicLoadRestartRequiredException() throws Exception {
File jpi = new File(r.jenkins.getRootDir(), "plugins/tasks.jpi");
assertTrue(jpi.isFile());
FileUtils.touch(jpi);
File timestamp = new File(r.jenkins.getRootDir(), "plugins/tasks/.timestamp2");
assertTrue(timestamp.isFile());
long lastMod = timestamp.lastModified();
try {
r.jenkins.getPluginManager().dynamicLoad(jpi);
fail("should not have worked");
} catch (RestartRequiredException x) {
// good
}
assertEquals("should not have tried to delete & unpack", lastMod, timestamp.lastModified());
}
@WithPlugin("tasks.jpi")
@Test public void pluginListJSONApi() throws IOException {
JSONObject response = r.getJSON("pluginManager/plugins").getJSONObject();
// Check that the basic API endpoint invocation works.
assertEquals("ok", response.getString("status"));
JSONArray data = response.getJSONArray("data");
assertTrue(data.size() > 0);
// Check that there was some data in the response and that the first entry
// at least had some of the expected fields.
JSONObject pluginInfo = data.getJSONObject(0);
assertTrue(pluginInfo.getString("name") != null);
assertTrue(pluginInfo.getString("title") != null);
assertTrue(pluginInfo.getString("dependencies") != null);
}
private void dynamicLoad(String plugin) throws IOException, InterruptedException, RestartRequiredException {
PluginManagerUtil.dynamicLoad(plugin, r.jenkins);
}
private void dynamicLoadAndDisable(String plugin) throws IOException, InterruptedException, RestartRequiredException {
PluginManagerUtil.dynamicLoad(plugin, r.jenkins, true);
}
@Test public void uploadDependencyResolution() throws Exception {
PersistedList<UpdateSite> sites = r.jenkins.getUpdateCenter().getSites();
sites.clear();
URL url = PluginManagerTest.class.getResource("/plugins/upload-test-update-center.json");
UpdateSite site = new UpdateSite(UpdateCenter.ID_DEFAULT, url.toString());
sites.add(site);
assertEquals(FormValidation.ok(), site.updateDirectly(false).get());
assertNotNull(site.getData());
// neither of the following plugins should be installed
assertNull(r.jenkins.getPluginManager().getPlugin("Parameterized-Remote-Trigger"));
assertNull(r.jenkins.getPluginManager().getPlugin("token-macro"));
HtmlPage page = r.createWebClient().goTo("pluginManager/advanced");
HtmlForm f = page.getFormByName("uploadPlugin");
File dir = tmp.newFolder();
File plugin = new File(dir, "Parameterized-Remote-Trigger.hpi");
FileUtils.copyURLToFile(getClass().getClassLoader().getResource("plugins/Parameterized-Remote-Trigger.hpi"),plugin);
f.getInputByName("name").setValueAttribute(plugin.getAbsolutePath());
r.submit(f);
assertTrue(r.jenkins.getUpdateCenter().getJobs().size() > 0);
// wait for all the download jobs to complete
boolean done = true;
boolean passed = true;
do {
Thread.sleep(100);
done = true;
for(UpdateCenterJob job : r.jenkins.getUpdateCenter().getJobs()) {
if(job instanceof UpdateCenter.DownloadJob) {
UpdateCenter.DownloadJob j = (UpdateCenter.DownloadJob)job;
assertFalse(j.status instanceof UpdateCenter.DownloadJob.Failure);
done &= !(((j.status instanceof UpdateCenter.DownloadJob.Pending) ||
(j.status instanceof UpdateCenter.DownloadJob.Installing)));
}
}
} while(!done);
// the files get renamed to .jpi
assertTrue( new File(r.jenkins.getRootDir(),"plugins/Parameterized-Remote-Trigger.jpi").exists() );
assertTrue( new File(r.jenkins.getRootDir(),"plugins/token-macro.jpi").exists() );
// now the other plugins should have been found as dependencies and downloaded
assertNotNull(r.jenkins.getPluginManager().getPlugin("Parameterized-Remote-Trigger"));
assertNotNull(r.jenkins.getPluginManager().getPlugin("token-macro"));
}
}