/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Erik Ramfelt, Koichi Fujikawa, Red Hat, Inc., Seiji Sogabe, * Stephen Connolly, Tom Huybrechts, Yahoo! Inc., Alan Harder, CloudBees, Inc. * * 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.jenkins.tools.test; import hudson.Functions; import hudson.maven.MavenEmbedderException; import hudson.model.UpdateSite; import hudson.model.UpdateSite.Plugin; import hudson.util.VersionNumber; import java.io.BufferedReader; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.maven.scm.ScmFileSet; import org.apache.maven.scm.ScmTag; import org.apache.maven.scm.command.checkout.CheckOutScmResult; import org.apache.maven.scm.manager.ScmManager; import org.apache.maven.scm.repository.ScmRepository; import org.codehaus.plexus.PlexusContainerException; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.io.RawInputStreamFacade; import org.jenkins.tools.test.exception.PluginSourcesUnavailableException; import org.jenkins.tools.test.exception.PomExecutionException; import org.jenkins.tools.test.exception.PomTransformationException; import org.jenkins.tools.test.model.MavenCoordinates; import org.jenkins.tools.test.model.MavenPom; import org.jenkins.tools.test.model.PluginCompatReport; import org.jenkins.tools.test.model.PluginCompatResult; import org.jenkins.tools.test.model.PluginCompatTesterConfig; import org.jenkins.tools.test.model.hook.PluginCompatTesterHooks; import org.jenkins.tools.test.model.PluginInfos; import org.jenkins.tools.test.model.PluginRemoting; import org.jenkins.tools.test.model.PomData; import org.jenkins.tools.test.model.TestExecutionResult; import org.jenkins.tools.test.model.TestStatus; import org.jenkins.tools.test.model.comparators.VersionComparator; import org.springframework.core.io.ClassPathResource; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.lang.reflect.Constructor; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.SortedSet; import java.util.TreeSet; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarInputStream; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jenkins.tools.test.maven.ExternalMavenRunner; import org.jenkins.tools.test.maven.InternalMavenRunner; import org.jenkins.tools.test.maven.MavenRunner; /** * Frontend for plugin compatibility tests * @author Frederic Camblor, Olivier Lamy */ public class PluginCompatTester { private static final String DEFAULT_SOURCE_ID = "default"; /** First version with new parent POM. */ public static final String JENKINS_CORE_FILE_REGEX = "WEB-INF/lib/jenkins-core-([0-9.]+(?:-[0-9.]+)?(?:-(?i)(alpha|beta|rc)(-)?([0-9.]+)?)?(?:-SNAPSHOT)?)[.]jar"; private PluginCompatTesterConfig config; private final MavenRunner runner; public PluginCompatTester(PluginCompatTesterConfig config){ this.config = config; runner = config.getExternalMaven() == null ? new InternalMavenRunner() : new ExternalMavenRunner(config.getExternalMaven()); } private SortedSet<MavenCoordinates> generateCoreCoordinatesToTest(UpdateSite.Data data, PluginCompatReport previousReport){ SortedSet<MavenCoordinates> coreCoordinatesToTest = null; // If parent GroupId/Artifact are not null, this will be fast : we will only test // against 1 core coordinate if(config.getParentGroupId() != null && config.getParentArtifactId() != null){ coreCoordinatesToTest = new TreeSet<MavenCoordinates>(); // If coreVersion is not provided in PluginCompatTesterConfig, let's use latest core // version used in update center String coreVersion = config.getParentVersion()==null?data.core.version:config.getParentVersion(); MavenCoordinates coreArtifact = new MavenCoordinates(config.getParentGroupId(), config.getParentArtifactId(), coreVersion); coreCoordinatesToTest.add(coreArtifact); // If parent groupId/artifactId are null, we'll test against every already recorded // cores } else if(config.getParentGroupId() == null && config.getParentArtifactId() == null){ coreCoordinatesToTest = previousReport.getTestedCoreCoordinates(); } else { throw new IllegalStateException("config.parentGroupId and config.parentArtifactId should either be both null or both filled\n" + "config.parentGroupId="+String.valueOf(config.getParentGroupId())+", config.parentArtifactId="+String.valueOf(config.getParentArtifactId())); } return coreCoordinatesToTest; } public PluginCompatReport testPlugins() throws PlexusContainerException, IOException, MavenEmbedderException { PluginCompatTesterHooks pcth = new PluginCompatTesterHooks(config.getHookPrefixes()); // Providing XSL Stylesheet along xml report file if(config.reportFile != null){ if(config.isProvideXslReport()){ File xslFilePath = PluginCompatReport.getXslFilepath(config.reportFile); FileUtils.copyStreamToFile(new RawInputStreamFacade(getXslTransformerResource().getInputStream()), xslFilePath); } } DataImporter dataImporter = null; if(config.getGaeBaseUrl() != null && config.getGaeSecurityToken() != null){ dataImporter = new DataImporter(config.getGaeBaseUrl(), config.getGaeSecurityToken()); } HashMap<String,String> pluginGroupIds = new HashMap<String, String>(); // Used to track real plugin groupIds from WARs UpdateSite.Data data = config.getWar() == null ? extractUpdateCenterData() : scanWAR(config.getWar(), pluginGroupIds); PluginCompatReport report = PluginCompatReport.fromXml(config.reportFile); SortedSet<MavenCoordinates> testedCores = config.getWar() == null ? generateCoreCoordinatesToTest(data, report) : coreVersionFromWAR(data); MavenRunner.Config mconfig = new MavenRunner.Config(); mconfig.userSettingsFile = config.getM2SettingsFile(); // TODO REMOVE mconfig.userProperties.put( "failIfNoTests", "false" ); mconfig.userProperties.put( "argLine", "-XX:MaxPermSize=128m" ); String mavenPropertiesFilePath = this.config.getMavenPropertiesFile(); if ( StringUtils.isNotBlank( mavenPropertiesFilePath )) { File file = new File (mavenPropertiesFilePath); if (file.exists()) { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream( file ); Properties properties = new Properties( ); properties.load( fileInputStream ); for (Map.Entry<Object,Object> entry : properties.entrySet()) { mconfig.userProperties.put((String) entry.getKey(), (String) entry.getValue()); } } finally { IOUtils.closeQuietly( fileInputStream ); } } else { System.out.println("File " + mavenPropertiesFilePath + " not exists" ); } } SCMManagerFactory.getInstance().start(); for(MavenCoordinates coreCoordinates : testedCores){ System.out.println("Starting plugin tests on core coordinates : "+coreCoordinates.toString()); for (Plugin plugin : data.plugins.values()) { if(config.getIncludePlugins()==null || config.getIncludePlugins().contains(plugin.name.toLowerCase())){ PluginInfos pluginInfos = new PluginInfos(plugin.name, plugin.version, plugin.url); if(config.getExcludePlugins()!=null && config.getExcludePlugins().contains(plugin.name.toLowerCase())){ System.out.println("Plugin "+plugin.name+" is in excluded plugins => test skipped !"); continue; } String errorMessage = null; TestStatus status = null; MavenCoordinates actualCoreCoordinates = coreCoordinates; PluginRemoting remote = new PluginRemoting(plugin.url); PomData pomData; try { pomData = remote.retrievePomData(); System.out.println("detected parent POM " + pomData.parent.toGAV()); if ((pomData.parent.groupId.equals(PluginCompatTesterConfig.DEFAULT_PARENT_GROUP) && pomData.parent.artifactId.equals(PluginCompatTesterConfig.DEFAULT_PARENT_ARTIFACT) || pomData.parent.groupId.equals("org.jvnet.hudson.plugins")) && coreCoordinates.version.matches("1[.][0-9]+[.][0-9]+") && new VersionNumber(coreCoordinates.version).compareTo(new VersionNumber("1.485")) < 0) { // TODO unless 1.480.3+ System.out.println("Cannot test against " + coreCoordinates.version + " due to lack of deployed POM for " + coreCoordinates.toGAV()); actualCoreCoordinates = new MavenCoordinates(coreCoordinates.groupId, coreCoordinates.artifactId, coreCoordinates.version.replaceFirst("[.][0-9]+$", "")); } } catch (Throwable t) { status = TestStatus.INTERNAL_ERROR; errorMessage = t.getMessage(); pomData = null; } if(!config.isSkipTestCache() && report.isCompatTestResultAlreadyInCache(pluginInfos, actualCoreCoordinates, config.getTestCacheTimeout(), config.getCacheThresholStatus())){ System.out.println("Cache activated for plugin "+pluginInfos.pluginName+" => test skipped !"); continue; // Don't do anything : we are in the cached interval ! :-) } List<String> warningMessages = new ArrayList<String>(); if (errorMessage == null) { try { TestExecutionResult result = testPluginAgainst(actualCoreCoordinates, plugin, mconfig, pomData, data.plugins, pluginGroupIds, pcth); // If no PomExecutionException, everything went well... status = TestStatus.SUCCESS; warningMessages.addAll(result.pomWarningMessages); } catch (PomExecutionException e) { if(!e.succeededPluginArtifactIds.contains("maven-compiler-plugin")){ status = TestStatus.COMPILATION_ERROR; } else if(!e.succeededPluginArtifactIds.contains("maven-surefire-plugin")){ status = TestStatus.TEST_FAILURES; } else { // Can this really happen ??? status = TestStatus.SUCCESS; } errorMessage = e.getErrorMessage(); warningMessages.addAll(e.getPomWarningMessages()); } catch (Error e){ // Rethrow the error ... something is wrong ! throw e; } catch (Throwable t){ status = TestStatus.INTERNAL_ERROR; errorMessage = t.getMessage(); } } File buildLogFile = createBuildLogFile(config.reportFile, plugin.name, plugin.version, actualCoreCoordinates); String buildLogFilePath = ""; if(buildLogFile.exists()){ buildLogFilePath = createBuildLogFilePathFor(pluginInfos.pluginName, pluginInfos.pluginVersion, actualCoreCoordinates); } PluginCompatResult result = new PluginCompatResult(actualCoreCoordinates, status, errorMessage, warningMessages, buildLogFilePath); report.add(pluginInfos, result); // Adding result to GAE if(dataImporter != null){ dataImporter.importPluginCompatResult(result, pluginInfos, config.reportFile.getParentFile()); // TODO: import log files } if(config.reportFile != null){ if(!config.reportFile.exists()){ FileUtils.fileWrite(config.reportFile.getAbsolutePath(), ""); } report.save(config.reportFile); } } else { System.out.println("Plugin "+plugin.name+" not in included plugins => test skipped !"); } } } // Generating HTML report if needed if(config.reportFile != null){ if(config.isGenerateHtmlReport()){ generateHtmlReportFile(); } } return report; } private void generateHtmlReportFile() throws IOException { Source xmlSource = new StreamSource(config.reportFile); Source xsltSource = new StreamSource(getXslTransformerResource().getInputStream()); Result result = new StreamResult(PluginCompatReport.getHtmlFilepath(config.reportFile)); TransformerFactory factory = TransformerFactory.newInstance(); Transformer transformer = null; try { transformer = factory.newTransformer(xsltSource); transformer.transform(xmlSource, result); } catch (TransformerException e) { throw new RuntimeException(e); } } private static ClassPathResource getXslTransformerResource(){ return new ClassPathResource("resultToReport.xsl"); } private static File createBuildLogFile(File reportFile, String pluginName, String pluginVersion, MavenCoordinates coreCoords){ return new File(reportFile.getParentFile().getAbsolutePath() +"/"+createBuildLogFilePathFor(pluginName, pluginVersion, coreCoords)); } private static String createBuildLogFilePathFor(String pluginName, String pluginVersion, MavenCoordinates coreCoords){ return String.format("logs/%s/v%s_against_%s_%s_%s.log", pluginName, pluginVersion, coreCoords.groupId, coreCoords.artifactId, coreCoords.version); } private TestExecutionResult testPluginAgainst(MavenCoordinates coreCoordinates, Plugin plugin, MavenRunner.Config mconfig, PomData pomData, Map<String,Plugin> otherPlugins, Map<String, String> pluginGroupIds, PluginCompatTesterHooks pcth) throws PluginSourcesUnavailableException, PomTransformationException, PomExecutionException, IOException { System.out.println(String.format("%n%n%n%n%n")); System.out.println(String.format("#############################################")); System.out.println(String.format("#############################################")); System.out.println(String.format("##%n## Starting to test plugin %s v%s%n## against %s%n##", plugin.name, plugin.version, coreCoordinates)); System.out.println(String.format("#############################################")); System.out.println(String.format("#############################################")); System.out.println(String.format("%n%n%n%n%n")); File pluginCheckoutDir = new File(config.workDirectory.getAbsolutePath()+"/"+plugin.name+"/"); try { // Run any precheckout hooks Map<String, Object> beforeCheckout = new HashMap<String, Object>(); beforeCheckout.put("pluginName", plugin.name); beforeCheckout.put("plugin", plugin); beforeCheckout.put("pomData", pomData); beforeCheckout.put("config", config); beforeCheckout.put("runCheckout", true); beforeCheckout = pcth.runBeforeCheckout(beforeCheckout); if(beforeCheckout.get("executionResult") != null) { // Check if the hook returned a result return (TestExecutionResult)beforeCheckout.get("executionResult"); } else if((boolean)beforeCheckout.get("runCheckout")) { if(beforeCheckout.get("checkoutDir") != null){ pluginCheckoutDir = (File)beforeCheckout.get("checkoutDir"); } if(pluginCheckoutDir.exists()){ System.out.println("Deleting working directory "+pluginCheckoutDir.getAbsolutePath()); FileUtils.deleteDirectory(pluginCheckoutDir); } pluginCheckoutDir.mkdir(); System.out.println("Created plugin checkout dir : "+pluginCheckoutDir.getAbsolutePath()); // These hooks could redirect the SCM, skip checkout (if multiple plugins use the same preloaded repo) System.out.println("Checking out from SCM connection URL : "+pomData.getConnectionUrl()+" ("+plugin.name+"-"+plugin.version+")"); ScmManager scmManager = SCMManagerFactory.getInstance().createScmManager(); ScmRepository repository = scmManager.makeScmRepository(pomData.getConnectionUrl()); CheckOutScmResult result = scmManager.checkOut(repository, new ScmFileSet(pluginCheckoutDir), new ScmTag(plugin.name+"-"+plugin.version)); if(!result.isSuccess()){ throw new RuntimeException(result.getProviderMessage() + " || " + result.getCommandOutput()); } } else { // If the plugin exists in a different directory (multimodule plugins) if(beforeCheckout.get("pluginDir") != null){ pluginCheckoutDir = (File)beforeCheckout.get("checkoutDir"); } System.out.println("The plugin has already been checked out, likely due to a multimodule situation. Continue."); } } catch (ComponentLookupException e) { System.err.println("Error : " + e.getMessage()); throw new PluginSourcesUnavailableException("Problem while creating ScmManager !", e); } catch (Exception e) { System.err.println("Error : " + e.getMessage()); throw new PluginSourcesUnavailableException("Problem while checking out plugin sources!", e); } File buildLogFile = createBuildLogFile(config.reportFile, plugin.name, plugin.version, coreCoordinates); FileUtils.forceMkdir(buildLogFile.getParentFile()); // Creating log directory FileUtils.fileWrite(buildLogFile.getAbsolutePath(), ""); // Creating log file boolean ranCompile = false; try { // First build against the original POM. // This defends against source incompatibilities (which we do not care about for this purpose); // and ensures that we are testing a plugin binary as close as possible to what was actually released. // We also skip potential javadoc execution to avoid general test failure. runner.run(mconfig, pluginCheckoutDir, buildLogFile, "clean", "process-test-classes", "-Dmaven.javadoc.skip"); ranCompile = true; // Then transform the POM and run tests against that. // You might think that it would suffice to run e.g. // -Dmaven-surefire-plugin.version=2.15 -Dmaven.test.dependency.excludes=org.jenkins-ci.main:jenkins-war -Dmaven.test.additionalClasspath=/…/org/jenkins-ci/main/jenkins-war/1.580.1/jenkins-war-1.580.1.war clean test // (2.15+ required for ${maven.test.dependency.excludes} and ${maven.test.additionalClasspath} to be honored from CLI) // but it does not work; there are lots of linkage errors as some things are expected to be in the test classpath which are not. // Much simpler to do use the parent POM to set up the test classpath. MavenPom pom = new MavenPom(pluginCheckoutDir); try { addSplitPluginDependencies(plugin.name, mconfig, pluginCheckoutDir, pom, otherPlugins, pluginGroupIds); } catch (Exception x) { x.printStackTrace(); pomData.getWarningMessages().add(Functions.printThrowable(x)); // but continue } List<String> args = new ArrayList<String>(); args.add("--define=maven.test.redirectTestOutputToFile=false"); args.add("--define=concurrency=1"); args.add("hpi:resolve-test-dependencies"); args.add("hpi:test-hpl"); args.add("surefire:test"); // Run preexecution hooks Map<String, Object> forExecutionHooks = new HashMap<String, Object>(); forExecutionHooks.put("pluginName", plugin.name); forExecutionHooks.put("args", args); forExecutionHooks.put("pomData", pomData); forExecutionHooks.put("pom", pom); forExecutionHooks.put("coreCoordinates", coreCoordinates); pcth.runBeforeExecution(forExecutionHooks); runner.run(mconfig, pluginCheckoutDir, buildLogFile, ((List<String>)forExecutionHooks.get("args")).toArray(new String[args.size()])); return new TestExecutionResult(((PomData)forExecutionHooks.get("pomData")).getWarningMessages()); }catch(PomExecutionException e){ PomExecutionException e2 = new PomExecutionException(e); e2.getPomWarningMessages().addAll(pomData.getWarningMessages()); if (ranCompile) { // So the status is considered to be TEST_FAILURES not COMPILATION_ERROR: e2.succeededPluginArtifactIds.add("maven-compiler-plugin"); } throw e2; } } private UpdateSite.Data extractUpdateCenterData(){ URL url = null; String jsonp = null; try { url = new URL(config.updateCenterUrl); jsonp = IOUtils.toString(url.openStream()); }catch(IOException e){ throw new RuntimeException("Invalid update center url : "+config.updateCenterUrl, e); } String json = jsonp.substring(jsonp.indexOf('(')+1,jsonp.lastIndexOf(')')); UpdateSite us = new UpdateSite(DEFAULT_SOURCE_ID, url.toExternalForm()); return newUpdateSiteData(us, JSONObject.fromObject(json)); } /** * Scans through a WAR file, accumulating plugin information * @param war WAR to scan * @param pluginGroupIds Map pluginName to groupId if set in the manifest, MUTATED IN THE EXECUTION * @return Update center data * @throws IOException */ private UpdateSite.Data scanWAR(File war, Map<String, String> pluginGroupIds) throws IOException { JSONObject top = new JSONObject(); top.put("id", DEFAULT_SOURCE_ID); JSONObject plugins = new JSONObject(); JarFile jf = new JarFile(war); if (pluginGroupIds == null) { pluginGroupIds = new HashMap<String, String>(); } try { Enumeration<JarEntry> entries = jf.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String name = entry.getName(); Matcher m = Pattern.compile(JENKINS_CORE_FILE_REGEX).matcher(name); if (m.matches()) { if (top.has("core")) { throw new IOException(">1 jenkins-core.jar in " + war); } top.put("core", new JSONObject().accumulate("name", "core").accumulate("version", m.group(1)).accumulate("url", "")); } m = Pattern.compile("WEB-INF/(?:optional-)?plugins/([^/.]+)[.][hj]pi").matcher(name); if (m.matches()) { JSONObject plugin = new JSONObject().accumulate("url", ""); InputStream is = jf.getInputStream(entry); try { JarInputStream jis = new JarInputStream(is); try { Manifest manifest = jis.getManifest(); String shortName = manifest.getMainAttributes().getValue("Short-Name"); if (shortName == null) { shortName = manifest.getMainAttributes().getValue("Extension-Name"); if (shortName == null) { shortName = m.group(1); } } plugin.put("name", shortName); pluginGroupIds.put(shortName, manifest.getMainAttributes().getValue("Group-Id")); plugin.put("version", manifest.getMainAttributes().getValue("Plugin-Version")); plugin.put("url", "jar:" + war.toURI() + "!/" + name); JSONArray dependenciesA = new JSONArray(); String dependencies = manifest.getMainAttributes().getValue("Plugin-Dependencies"); if (dependencies != null) { // e.g. matrix-auth:1.0.2;resolution:=optional,credentials:1.8.3;resolution:=optional for (String pair : dependencies.replace(";resolution:=optional", "").split(",")) { String[] nameVer = pair.split(":"); assert nameVer.length == 2; dependenciesA.add(new JSONObject().accumulate("name", nameVer[0]).accumulate("version", nameVer[1])./* we do care about even optional deps here */accumulate("optional", "false")); } } plugin.accumulate("dependencies", dependenciesA); plugins.put(shortName, plugin); } finally { jis.close(); } } finally { is.close(); } } } } finally { jf.close(); } top.put("plugins", plugins); if (!top.has("core")) { throw new IOException("no jenkins-core.jar in " + war); } System.out.println("Scanned contents of " + war + ": " + top); return newUpdateSiteData(new UpdateSite(DEFAULT_SOURCE_ID, null), top); } private SortedSet<MavenCoordinates> coreVersionFromWAR(UpdateSite.Data data) { SortedSet<MavenCoordinates> result = new TreeSet<MavenCoordinates>(); result.add(new MavenCoordinates(PluginCompatTesterConfig.DEFAULT_PARENT_GROUP, PluginCompatTesterConfig.DEFAULT_PARENT_ARTIFACT, data.core.version)); return result; } private UpdateSite.Data newUpdateSiteData(UpdateSite us, JSONObject jsonO) throws RuntimeException { try { Constructor<UpdateSite.Data> dataConstructor = UpdateSite.Data.class.getDeclaredConstructor(UpdateSite.class, JSONObject.class); dataConstructor.setAccessible(true); return dataConstructor.newInstance(us, jsonO); }catch(Exception e){ throw new RuntimeException("UpdateSite.Data instanciation problems", e); } } private void addSplitPluginDependencies(String thisPlugin, MavenRunner.Config mconfig, File pluginCheckoutDir, MavenPom pom, Map<String,Plugin> otherPlugins, Map<String, String> pluginGroupIds) throws PomExecutionException, IOException { File tmp = File.createTempFile("dependencies", ".log"); VersionNumber coreDep = null; Map<String,VersionNumber> pluginDeps = new HashMap<String,VersionNumber>(); Map<String,VersionNumber> pluginDepsTest = new HashMap<String,VersionNumber>(); try { runner.run(mconfig, pluginCheckoutDir, tmp, "dependency:resolve"); Reader r = new FileReader(tmp); try { BufferedReader br = new BufferedReader(r); Pattern p = Pattern.compile("\\[INFO\\] ([^:]+):([^:]+):([a-z-]+):(([^:]+):)?([^:]+):(provided|compile|runtime|system)"); Pattern p2 = Pattern.compile("\\[INFO\\] ([^:]+):([^:]+):([a-z-]+):(([^:]+):)?([^:]+):(test)"); String line; while ((line = br.readLine()) != null) { Matcher m = p.matcher(line); Matcher m2 = p2.matcher(line); String groupId; String artifactId; VersionNumber version; if (!m.matches() && !m2.matches()) { continue; } else if (m.matches()) { groupId = m.group(1); artifactId = m.group(2); try { version = new VersionNumber(m.group(6)); } catch (IllegalArgumentException x) { // OK, some other kind of dep, just ignore continue; } } else { //m2.matches() groupId = m2.group(1); artifactId = m2.group(2); try { version = new VersionNumber(m2.group(6)); } catch (IllegalArgumentException x) { // OK, some other kind of dep, just ignore continue; } } if (groupId.equals("org.jenkins-ci.main") && artifactId.equals("jenkins-core")) { coreDep = version; } else if (groupId.equals("org.jenkins-ci.plugins")) { if(m2.matches()) { pluginDepsTest.put(artifactId, version); } else { pluginDeps.put(artifactId, version); } } else if (groupId.equals("org.jenkins-ci.main") && artifactId.equals("maven-plugin")) { if(m2.matches()) { pluginDepsTest.put(artifactId, version); } else { pluginDeps.put(artifactId, version); } } else if (groupId.equals(pluginGroupIds.get(artifactId))) { if(m2.matches()) { pluginDepsTest.put(artifactId, version); } else { pluginDeps.put(artifactId, version); } } } } finally { r.close(); } } finally { tmp.delete(); } System.out.println("Analysis: coreDep=" + coreDep + " pluginDeps=" + pluginDeps + " pluginDepsTest=" + pluginDepsTest); if (coreDep != null) { // Synchronize with ClassicPluginStrategy.DETACHED_LIST: String[] splits = { "maven-plugin:1.296:1.296", "subversion:1.310:1.0", "cvs:1.340:0.1", "ant:1.430.*:1.0", "javadoc:1.430.*:1.0", "external-monitor-job:1.467.*:1.0", "ldap:1.467.*:1.0", "pam-auth:1.467.*:1.0", "mailer:1.493.*:1.2", "matrix-auth:1.535.*:1.0.2", "windows-slaves:1.547.*:1.0", "antisamy-markup-formatter:1.553.*:1.0", "matrix-project:1.561.*:1.0", "junit:1.577.*:1.0", "bouncycastle-api:2.16.*:2.16.0", }; // Synchronize with ClassicPluginStrategy.BREAK_CYCLES: String[] exceptions = { "script-security/matrix-auth", "script-security/windows-slaves", "script-security/antisamy-markup-formatter", "script-security/matrix-project", "credentials/matrix-auth", "credentials/windows-slaves" }; Map<String,VersionNumber> toAdd = new HashMap<String,VersionNumber>(); Map<String,VersionNumber> toReplace = new HashMap<String,VersionNumber>(); Map<String,VersionNumber> toAddTest = new HashMap<String,VersionNumber>(); Map<String,VersionNumber> toReplaceTest = new HashMap<String,VersionNumber>(); for (String split : splits) { String[] pieces = split.split(":"); String plugin = pieces[0]; if (Arrays.asList(exceptions).contains(thisPlugin + "/" + plugin)) { System.out.println("Skipping implicit dep " + thisPlugin + " → " + plugin); continue; } VersionNumber splitPoint = new VersionNumber(pieces[1]); VersionNumber declaredMinimum = new VersionNumber(pieces[2]); // TODO this should only happen if the tested core version is ≥ splitPoint if (coreDep.compareTo(splitPoint) <= 0 && !pluginDeps.containsKey(plugin)) { Plugin bundledP = otherPlugins.get(plugin); if (bundledP != null) { VersionNumber bundledV; try { bundledV = new VersionNumber(bundledP.version); } catch (NumberFormatException x) { // TODO apparently this does not handle `1.0-beta-1` and the like?! System.out.println("Skipping unparseable dep on " + bundledP.name + ": " + bundledP.version); continue; } if (bundledV.isNewerThan(declaredMinimum)) { toAdd.put(plugin, bundledV); continue; } } toAdd.put(plugin, declaredMinimum); } } List<String> convertFromTestDep = new ArrayList<String>(); checkDefinedDeps(pluginDeps, toAdd, toReplace, otherPlugins, new ArrayList<>(pluginDepsTest.keySet()), convertFromTestDep); pluginDepsTest.putAll(difference(pluginDepsTest, toAdd)); pluginDepsTest.putAll(difference(pluginDepsTest, toReplace)); checkDefinedDeps(pluginDepsTest, toAddTest, toReplaceTest, otherPlugins); // Could contain transitive dependencies which were part of the plugin's dependencies or to be added toAddTest = difference(pluginDeps, toAddTest); toAddTest = difference(toAdd, toAddTest); if (!toAdd.isEmpty() || !toReplace.isEmpty() || !toAddTest.isEmpty() || !toReplaceTest.isEmpty()) { System.out.println("Adding/replacing plugin dependencies for compatibility: " + toAdd + " " + toReplace + "\nFor test: " + toAddTest + " " + toReplaceTest); pom.addDependencies(toAdd, toReplace, toAddTest, toReplaceTest, coreDep, pluginGroupIds, convertFromTestDep); } } } private void checkDefinedDeps(Map<String,VersionNumber> pluginList, Map<String,VersionNumber> adding, Map<String,VersionNumber> replacing, Map<String,Plugin> otherPlugins) { checkDefinedDeps(pluginList, adding, replacing, otherPlugins, new ArrayList<String>(), null); } private void checkDefinedDeps(Map<String,VersionNumber> pluginList, Map<String,VersionNumber> adding, Map<String,VersionNumber> replacing, Map<String,Plugin> otherPlugins, List<String> inTest, List<String> toConvertFromTest) { for (Map.Entry<String,VersionNumber> pluginDep : pluginList.entrySet()) { String plugin = pluginDep.getKey(); Plugin bundledP = otherPlugins.get(plugin); if (bundledP != null) { VersionNumber bundledV = new VersionNumber(bundledP.version); if (bundledV.isNewerThan(pluginDep.getValue())) { assert !adding.containsKey(plugin); replacing.put(plugin, bundledV); } // Also check any dependencies, so if we are upgrading cloudbees-folder, we also add an explicit dep on a bundled credentials. for (Map.Entry<String,String> dependency : bundledP.dependencies.entrySet()) { String depPlugin = dependency.getKey(); if (pluginList.containsKey(depPlugin)) { continue; // already handled } // We ignore the declared dependency version and go with the bundled version: Plugin depBundledP = otherPlugins.get(depPlugin); if (depBundledP != null) { updateAllDependents(plugin, depBundledP, pluginList, adding, replacing, otherPlugins, inTest, toConvertFromTest); } } } } } /** * Search the dependents of a given plugin to determine if we need to use the bundled version. * This helps in cases where tests fail due to new insufficient versions as well as more * accurtely representing the totality of upgraded plugins for provided war files. */ private void updateAllDependents(String parent, Plugin dependent, Map<String,VersionNumber> pluginList, Map<String,VersionNumber> adding, Map<String,VersionNumber> replacing, Map<String,Plugin> otherPlugins, List<String> inTest, List<String> toConvertFromTest) { // Check if this exists with an undesired scope String pluginName = dependent.name; if (inTest.contains(pluginName)) { // This is now required in the compile scope. For example: copyartifact's dependency matrix-project requires junit System.out.println("Converting " + pluginName + " from the test scope since it was a dependency of " + parent); toConvertFromTest.add(pluginName); replacing.put(pluginName, new VersionNumber(dependent.version)); } else { System.out.println("Adding " + pluginName + " since it was a dependency of " + parent); adding.put(pluginName, new VersionNumber(dependent.version)); } // Also check any dependencies for (Map.Entry<String,String> dependency : dependent.dependencies.entrySet()) { String depPlugin = dependency.getKey(); if (pluginList.containsKey(depPlugin)) { continue; // already handled } // We ignore the declared dependency version and go with the bundled version: Plugin depBundledP = otherPlugins.get(depPlugin); if (depBundledP != null) { updateAllDependents(pluginName, depBundledP, pluginList, adding, replacing, otherPlugins, inTest, toConvertFromTest); } } } /** * Finds the difference of the given maps. * In set theory: base - toAdd * * @param base the left map; all returned items are not in this map * @param toAdd the right map; all returned items are found in this map */ private Map<String, VersionNumber> difference(Map<String, VersionNumber> base, Map<String, VersionNumber> toAdd) { Map<String, VersionNumber> diff = new HashMap<String, VersionNumber>(); for (Map.Entry<String,VersionNumber> adding : toAdd.entrySet()) { if (!base.containsKey(adding.getKey())) { diff.put(adding.getKey(), adding.getValue()); } } return diff; } }