From 0dfe3d7f02313f17adbe2df01c1a84687e5bdd6c Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Wed, 19 Jun 2013 15:02:48 +0200 Subject: [PATCH 01/11] check-out/update with up to 4 parallel threads --- src/main/java/hudson/scm/SubversionSCM.java | 87 +++++++++++++++++++-- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index 4c7e8ec20..ba47884c5 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -96,6 +96,7 @@ import hudson.scm.subversion.WorkspaceUpdaterDescriptor; import hudson.util.EditDistance; import hudson.util.FormValidation; +import hudson.util.IOException2; import hudson.util.LogTaskListener; import hudson.util.MultipartFormDataParser; import hudson.util.Scrambler; @@ -128,6 +129,12 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.UUID; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; @@ -176,6 +183,7 @@ import org.tmatesoft.svn.core.wc.SVNWCClient; import org.tmatesoft.svn.core.wc.SVNWCUtil; +import com.google.common.collect.Lists; import com.trilead.ssh2.DebugLogger; import com.trilead.ssh2.SCPClient; import com.trilead.ssh2.crypto.Base64; @@ -872,7 +880,7 @@ public boolean requiresWorkspaceForPolling() { * if the operation failed. Otherwise the set of local workspace paths * (relative to the workspace root) that has loaded due to svn:external. */ - private List checkout(Run build, FilePath workspace, TaskListener listener, EnvVars env) throws IOException, InterruptedException { + private List checkout(final Run build, final FilePath workspace, final TaskListener listener, final EnvVars env) throws IOException, InterruptedException { if (repositoryLocationsNoLongerExist(build, listener, env)) { Run lsb = build.getParent().getLastSuccessfulBuild(); if (build instanceof AbstractBuild && lsb != null && build.getNumber()-lsb.getNumber()>10 @@ -890,13 +898,44 @@ private List checkout(Run build, FilePath workspace, TaskListener list } List externals = new ArrayList(); - Set unauthenticatedRealms = new LinkedHashSet(); - for (ModuleLocation location : getLocations(env, build)) { - CheckOutTask checkOutTask = - new CheckOutTask(build, this, location, build.getTimestamp().getTime(), listener, env); - externals.addAll(workspace.act(checkOutTask)); - unauthenticatedRealms.addAll(checkOutTask.getUnauthenticatedRealms()); - // olamy: remove null check at it cause test failure + final Set unauthenticatedRealms = new LinkedHashSet(); + ModuleLocation[] expandedLocations = getLocations(env, build); + + int numberOfExecutors = Math.min(4, expandedLocations.length); + + final ExecutorService service; + + if (numberOfExecutors > 1) { + service = Executors.newFixedThreadPool(numberOfExecutors); + listener.getLogger().println("checking out/updating with " + numberOfExecutors + " parallel threads"); + } else { + service = new CurrentThreadExecutorService(); + } + + List>> callables = Lists.newArrayListWithExpectedSize(expandedLocations.length); + for (final ModuleLocation location : expandedLocations) { + callables.add(new java.util.concurrent.Callable>() { + + public List call() throws Exception { + CheckOutTask checkOutTask = + new CheckOutTask(build, SubversionSCM.this, location, build.getTimestamp().getTime(), listener, env); + List externals = workspace.act(checkOutTask); + unauthenticatedRealms.addAll(checkOutTask.getUnauthenticatedRealms()); + return externals; + } + }); + } + + List>> futures = service.invokeAll(callables); + + for (Future> future : futures) { + + try { + externals.addAll(future.get()); + } catch (ExecutionException e) { + throw new IOException2(e); + } + // olamy: remove null check as it causes test failure // see https://github.com/jenkinsci/subversion-plugin/commit/de23a2b781b7b86f41319977ce4c11faee75179b#commitcomment-1551273 /*if ( externalsFound != null ){ externals.addAll(externalsFound); @@ -904,6 +943,7 @@ private List checkout(Run build, FilePath workspace, TaskListener list externals.addAll( new ArrayList( 0 ) ); }*/ } + if (additionalCredentials != null) { for (AdditionalCredentials c : additionalCredentials) { unauthenticatedRealms.remove(c.getRealm()); @@ -933,6 +973,37 @@ private List checkout(Run build, FilePath workspace, TaskListener list return externals; } + + private static class CurrentThreadExecutorService extends AbstractExecutorService { + + private boolean terminated = false; + + public void shutdown() { + terminated = true; + } + + public List shutdownNow() { + terminated = true; + return Collections.emptyList(); + } + + public boolean isShutdown() { + return terminated; + } + + public boolean isTerminated() { + return terminated; + } + + public boolean awaitTermination(long timeout, TimeUnit unit) + throws InterruptedException { + return true; + } + + public void execute(Runnable command) { + command.run(); + } + } private synchronized Map> getProjectExternalsCache() { if (projectExternalsCache == null) { From f3480a2755055fa9bc0a28f9124d3fcbbe8309e2 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Wed, 19 Jun 2013 16:52:40 +0200 Subject: [PATCH 02/11] Shutdown executorservice after checkout and clean-up --- src/main/java/hudson/scm/SubversionSCM.java | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index ba47884c5..4a778a2d5 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -183,6 +183,7 @@ import org.tmatesoft.svn.core.wc.SVNWCClient; import org.tmatesoft.svn.core.wc.SVNWCUtil; +import com.google.common.collect.Lists; import com.google.common.collect.Lists; import com.trilead.ssh2.DebugLogger; import com.trilead.ssh2.SCPClient; @@ -912,6 +913,30 @@ private List checkout(final Run build, final FilePath workspace, final service = new CurrentThreadExecutorService(); } + ModuleLocation[] expandedLocations = getLocations(env, build); + + int numberOfExecutors = Math.min(4, expandedLocations.length); + + final ExecutorService service; + if (numberOfExecutors > 1) { + service = Executors.newFixedThreadPool(numberOfExecutors); + listener.getLogger().println("checking out/updating with " + numberOfExecutors + " parallel threads"); + } else { + service = new CurrentThreadExecutorService(); + } + + List>> callables = Lists.newArrayListWithExpectedSize(expandedLocations.length); + for (final ModuleLocation location : expandedLocations) { + callables.add(new java.util.concurrent.Callable>() { + + public List call() throws Exception { + return workspace.act(new CheckOutTask(build,SubversionSCM.this, location, build.getTimestamp().getTime(), listener, env)); + } + }); + } + + List>> futures = service.invokeAll(callables); + List>> callables = Lists.newArrayListWithExpectedSize(expandedLocations.length); for (final ModuleLocation location : expandedLocations) { callables.add(new java.util.concurrent.Callable>() { @@ -943,6 +968,7 @@ public List call() throws Exception { externals.addAll( new ArrayList( 0 ) ); }*/ } + service.shutdownNow(); if (additionalCredentials != null) { for (AdditionalCredentials c : additionalCredentials) { From 35c2bddacacb2a45f273d2c66666941ba3d1bb48 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Fri, 21 Jun 2013 10:56:47 +0200 Subject: [PATCH 03/11] synchronize output from threads on line level --- src/main/java/hudson/scm/SubversionSCM.java | 89 ++++++++++++++----- .../scm/subversion/CheckoutUpdater.java | 42 ++++++++- 2 files changed, 106 insertions(+), 25 deletions(-) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index 4a778a2d5..3393865d3 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -94,6 +94,7 @@ import hudson.scm.subversion.WorkspaceUpdater; import hudson.scm.subversion.WorkspaceUpdater.UpdateTask; import hudson.scm.subversion.WorkspaceUpdaterDescriptor; +import hudson.util.AbstractTaskListener; import hudson.util.EditDistance; import hudson.util.FormValidation; import hudson.util.IOException2; @@ -101,13 +102,16 @@ import hudson.util.MultipartFormDataParser; import hudson.util.Scrambler; import hudson.util.Secret; +import hudson.util.StreamTaskListener; import hudson.util.TimeUnit2; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; +import java.io.FilterOutputStream; import java.io.IOException; import java.io.InterruptedIOException; import java.io.OutputStream; @@ -183,6 +187,7 @@ import org.tmatesoft.svn.core.wc.SVNWCClient; import org.tmatesoft.svn.core.wc.SVNWCUtil; +import com.google.common.collect.Lists; import com.google.common.collect.Lists; import com.google.common.collect.Lists; import com.trilead.ssh2.DebugLogger; @@ -913,29 +918,8 @@ private List checkout(final Run build, final FilePath workspace, final service = new CurrentThreadExecutorService(); } - ModuleLocation[] expandedLocations = getLocations(env, build); - - int numberOfExecutors = Math.min(4, expandedLocations.length); - - final ExecutorService service; - if (numberOfExecutors > 1) { - service = Executors.newFixedThreadPool(numberOfExecutors); - listener.getLogger().println("checking out/updating with " + numberOfExecutors + " parallel threads"); - } else { - service = new CurrentThreadExecutorService(); - } - - List>> callables = Lists.newArrayListWithExpectedSize(expandedLocations.length); - for (final ModuleLocation location : expandedLocations) { - callables.add(new java.util.concurrent.Callable>() { - - public List call() throws Exception { - return workspace.act(new CheckOutTask(build,SubversionSCM.this, location, build.getTimestamp().getTime(), listener, env)); - } - }); - } - - List>> futures = service.invokeAll(callables); + @SuppressWarnings("deprecation") + final TaskListener syncedListener = new StreamTaskListener(new SynchronizedPrintStream(listener.getLogger())); List>> callables = Lists.newArrayListWithExpectedSize(expandedLocations.length); for (final ModuleLocation location : expandedLocations) { @@ -943,7 +927,7 @@ public List call() throws Exception { public List call() throws Exception { CheckOutTask checkOutTask = - new CheckOutTask(build, SubversionSCM.this, location, build.getTimestamp().getTime(), listener, env); + new CheckOutTask(build, SubversionSCM.this, location, build.getTimestamp().getTime(), syncedListener, env); List externals = workspace.act(checkOutTask); unauthenticatedRealms.addAll(checkOutTask.getUnauthenticatedRealms()); return externals; @@ -1000,6 +984,63 @@ public List call() throws Exception { return externals; } + /** + * {@link PrintStream} which is synchronized on line level. + * + * @author ckutz + */ + private static class SynchronizedPrintStream extends PrintStream { + + public SynchronizedPrintStream(OutputStream out) { + super(out); + } + + @Override + public synchronized void println(boolean x) { + super.println(x); + } + + @Override + public synchronized void println(char x) { + super.println(x); + } + + @Override + public synchronized void println(int x) { + super.println(x); + } + + @Override + public synchronized void println(long x) { + super.println(x); + } + + @Override + public synchronized void println(float x) { + super.println(x); + } + + @Override + public synchronized void println(double x) { + super.println(x); + } + + @Override + public synchronized void println(char[] x) { + super.println(x); + } + + @Override + public synchronized void println(String x) { + super.println(x); + } + + @Override + public synchronized void println(Object x) { + super.println(x); + } + } + private static class CurrentThreadExecutorService extends AbstractExecutorService { private boolean terminated = false; diff --git a/src/main/java/hudson/scm/subversion/CheckoutUpdater.java b/src/main/java/hudson/scm/subversion/CheckoutUpdater.java index 847291b6c..835e998da 100755 --- a/src/main/java/hudson/scm/subversion/CheckoutUpdater.java +++ b/src/main/java/hudson/scm/subversion/CheckoutUpdater.java @@ -31,7 +31,6 @@ import hudson.scm.SubversionSCM.External; import hudson.scm.SubversionWorkspaceSelector; import hudson.util.IOException2; -import hudson.util.StreamCopyThread; import org.apache.commons.lang.time.FastDateFormat; import org.kohsuke.stapler.DataBoundConstructor; @@ -45,8 +44,11 @@ import org.tmatesoft.svn.core.wc2.SvnCheckout; import org.tmatesoft.svn.core.wc2.SvnTarget; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.io.PrintStream; @@ -82,6 +84,7 @@ public List perform() throws IOException, InterruptedException { // buffer the output by a separate thread so that the update operation // won't be blocked by the remoting of the data PipedOutputStream pos = new PipedOutputStream(); + StreamCopyThread sct = new StreamCopyThread("svn log copier", new PipedInputStream(pos), listener.getLogger()); sct.start(); @@ -140,6 +143,43 @@ public List perform() throws IOException, InterruptedException { } }; } + + /** + * {@link Thread} that copies an {@link InputStream} line wise to a {@link PrintStream}. + * + * @author Kohsuke Kawaguchi + * @author Christoph Kutzinski + */ + private static class StreamCopyThread extends Thread { + private final BufferedReader in; + private final PrintStream out; + + public StreamCopyThread(String threadName, InputStream in, PrintStream out) { + super(threadName); + this.in = new BufferedReader(new InputStreamReader(in)); + if (out == null) { + throw new NullPointerException("out is null"); + } + this.out = out; + } + + @Override + public void run() { + try { + try { + String line; + while ((line = in.readLine()) != null) + out.println(line); + } finally { + // it doesn't make sense not to close InputStream that's already EOF-ed, + // so there's no 'closeIn' flag. + in.close(); + } + } catch (IOException e) { + e.printStackTrace(out); + } + } + } @Extension public static class DescriptorImpl extends WorkspaceUpdaterDescriptor { From 64d72d9b9693649f0005fc1829f3de9efdcb24b1 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Tue, 25 Jun 2013 15:29:22 +0200 Subject: [PATCH 04/11] with parallel checkout we should mention to which basedir 'at revision' applies --- src/main/java/hudson/scm/SubversionEventHandlerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/hudson/scm/SubversionEventHandlerImpl.java b/src/main/java/hudson/scm/SubversionEventHandlerImpl.java index a7f6d4fb6..12e89c666 100644 --- a/src/main/java/hudson/scm/SubversionEventHandlerImpl.java +++ b/src/main/java/hudson/scm/SubversionEventHandlerImpl.java @@ -117,7 +117,7 @@ public void handleEvent(SVNEvent event, double progress) throws SVNException { } } else if (action == SVNEventAction.UPDATE_COMPLETED) { // finished updating - out.println("At revision " + event.getRevision()); + out.println(baseDir+": At revision " + event.getRevision()); return; } else if (action == SVNEventAction.ADD){ out.println("A " + path); From 67313fe61c22caecd1ffef98cb2086b37f8b4e66 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Mon, 8 Jul 2013 17:39:19 +0200 Subject: [PATCH 05/11] Added TODO about BUSY error --- src/main/java/hudson/scm/SubversionSCM.java | 80 +++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index 3393865d3..cdd4afae2 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -918,6 +918,86 @@ private List checkout(final Run build, final FilePath workspace, final service = new CurrentThreadExecutorService(); } + + /* TODO: when previously there were 2 repository locations which were 'equal' i.e. checking out the same SVN module to the same local + * directory, this would succeed previously, but can now fail under unlucky timing with: + * (should at least detect this situation and give a better error message?) + * + * 15:10:44 ERROR: Failed to update http://xyz +15:10:44 org.tmatesoft.svn.core.SVNException: svn: E200030: BUSY +15:10:44 at org.tmatesoft.svn.core.internal.wc.SVNErrorManager.error(SVNErrorManager.java:64) +15:10:44 at org.tmatesoft.svn.core.internal.wc.SVNErrorManager.error(SVNErrorManager.java:51) +15:10:44 at org.tmatesoft.svn.core.internal.db.SVNSqlJetDb.createSqlJetError(SVNSqlJetDb.java:176) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbInsertWorkItem.exec(SVNWCDbInsertWorkItem.java:48) +15:10:44 at org.tmatesoft.svn.core.internal.db.SVNSqlJetStatement.done(SVNSqlJetStatement.java:367) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.addSingleWorkItem(SVNWCDb.java:267) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.addWorkItems(SVNWCDb.java:256) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.addWorkQueue(SVNWCDb.java:961) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.restoreFile(SVNReporter17.java:399) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.restoreNode(SVNReporter17.java:224) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:309) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.report(SVNReporter17.java:188) +15:10:44 at org.tmatesoft.svn.core.internal.io.dav.handlers.DAVEditorHandler.generateEditorRequest(DAVEditorHandler.java:106) +15:10:44 at org.tmatesoft.svn.core.internal.io.dav.DAVRepository.runReport(DAVRepository.java:1275) +15:10:44 at org.tmatesoft.svn.core.internal.io.dav.DAVRepository.update(DAVRepository.java:837) +15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgAbstractUpdate.updateInternal(SvnNgAbstractUpdate.java:192) +15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgAbstractUpdate.update(SvnNgAbstractUpdate.java:76) +15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgUpdate.run(SvnNgUpdate.java:38) +15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgUpdate.run(SvnNgUpdate.java:18) +15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgOperationRunner.run(SvnNgOperationRunner.java:20) +15:10:44 at org.tmatesoft.svn.core.internal.wc2.SvnOperationRunner.run(SvnOperationRunner.java:20) +15:10:44 at org.tmatesoft.svn.core.wc2.SvnOperationFactory.run(SvnOperationFactory.java:1235) +15:10:44 at org.tmatesoft.svn.core.wc2.SvnOperation.run(SvnOperation.java:291) +15:10:44 at org.tmatesoft.svn.core.wc.SVNUpdateClient.doUpdate(SVNUpdateClient.java:311) +15:10:44 at org.tmatesoft.svn.core.wc.SVNUpdateClient.doUpdate(SVNUpdateClient.java:291) +15:10:44 at org.tmatesoft.svn.core.wc.SVNUpdateClient.doUpdate(SVNUpdateClient.java:387) +15:10:44 at hudson.scm.subversion.UpdateUpdater$TaskImpl.perform(UpdateUpdater.java:157) +15:10:44 at hudson.scm.subversion.WorkspaceUpdater$UpdateTask.delegateTo(WorkspaceUpdater.java:153) +15:10:44 at hudson.scm.SubversionSCM$CheckOutTask.perform(SubversionSCM.java:1041) +15:10:44 at hudson.scm.SubversionSCM$CheckOutTask.invoke(SubversionSCM.java:1022) +15:10:44 at hudson.scm.SubversionSCM$CheckOutTask.invoke(SubversionSCM.java:1005) +15:10:44 at hudson.FilePath$FileCallableWrapper.call(FilePath.java:2246) +15:10:44 at hudson.remoting.UserRequest.perform(UserRequest.java:118) +15:10:44 at hudson.remoting.UserRequest.perform(UserRequest.java:48) +15:10:44 at hudson.remoting.Request$2.run(Request.java:326) +15:10:44 at hudson.remoting.InterceptingExecutorService$1.call(InterceptingExecutorService.java:72) +15:10:44 at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303) +15:10:44 at java.util.concurrent.FutureTask.run(FutureTask.java:138) +15:10:44 at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895) +15:10:44 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918) +15:10:44 at java.lang.Thread.run(Thread.java:662) +15:10:44 Caused by: svn: E200030: BUSY +15:10:44 at org.tmatesoft.svn.core.SVNErrorMessage.create(SVNErrorMessage.java:109) +15:10:44 at org.tmatesoft.svn.core.internal.db.SVNSqlJetDb.createSqlJetError(SVNSqlJetDb.java:175) +15:10:44 ... 47 more +15:10:44 Caused by: org.tmatesoft.sqljet.core.SqlJetException: BUSY: error code is BUSY +15:10:44 at org.tmatesoft.sqljet.core.internal.pager.SqlJetPager.begin(SqlJetPager.java:2785) +15:10:44 at org.tmatesoft.sqljet.core.internal.btree.SqlJetBtree.beginTrans(SqlJetBtree.java:929) +15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine.doBeginTransaction(SqlJetEngine.java:561) +15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine.access$100(SqlJetEngine.java:55) +15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine$12.runSynchronized(SqlJetEngine.java:535) +15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine.runSynchronized(SqlJetEngine.java:217) +15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine.runEngineTransaction(SqlJetEngine.java:529) +15:10:44 at org.tmatesoft.sqljet.core.table.SqlJetDb.runTransaction(SqlJetDb.java:238) +15:10:44 at org.tmatesoft.sqljet.core.table.SqlJetDb.runWriteTransaction(SqlJetDb.java:211) +15:10:44 at org.tmatesoft.sqljet.core.internal.table.SqlJetTable.runWriteTransaction(SqlJetTable.java:156) +15:10:44 at org.tmatesoft.sqljet.core.internal.table.SqlJetTable.insertByFieldNamesOr(SqlJetTable.java:190) +15:10:44 at org.tmatesoft.sqljet.core.internal.table.SqlJetTable.insertByFieldNames(SqlJetTable.java:173) +15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbInsertWorkItem.exec(SVNWCDbInsertWorkItem.java:46) +15:10:44 ... 46 more +15:10:44 ERROR: Subversion update failed + * + */ + @SuppressWarnings("deprecation") final TaskListener syncedListener = new StreamTaskListener(new SynchronizedPrintStream(listener.getLogger())); From 0023fe5b54c44fdf63c531e37941797e22b67092 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Thu, 18 Jul 2013 18:11:03 +0200 Subject: [PATCH 06/11] Log warning if 2 module locations have the same local path --- src/main/java/hudson/scm/SubversionSCM.java | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index cdd4afae2..040517389 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -907,6 +907,8 @@ private List checkout(final Run build, final FilePath workspace, final final Set unauthenticatedRealms = new LinkedHashSet(); ModuleLocation[] expandedLocations = getLocations(env, build); + checkForLocationDuplicates(expandedLocations,listener); + int numberOfExecutors = Math.min(4, expandedLocations.length); final ExecutorService service; @@ -1064,6 +1066,25 @@ public List call() throws Exception { return externals; } + /** + * Checks that there a no 2 locations which try to checkout to the same local location, + * as that may cause E200030: BUSY errors. + */ + private void checkForLocationDuplicates(ModuleLocation[] expandedLocations, + TaskListener listener) { + if (locations.length < 2) { + return; + } + + Set localPaths = new HashSet(); + for (ModuleLocation loc : expandedLocations) { + if (!localPaths.add(loc.getLocalDir())) { + listener.getLogger().println("WARN: you have 2 repository locations pointing to the same local path: " + loc.getLocalDir() + +".\n This may cause subsequent errors - e.g. E200030: BUSY"); + } + } + } + /** * {@link PrintStream} which is synchronized on line level. * From 5338fa68c67038599993ab9679b08ca4502d8c19 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Fri, 9 Jan 2015 13:22:10 +0000 Subject: [PATCH 07/11] TODO was done already with checkForLocationDuplicates() --- src/main/java/hudson/scm/SubversionSCM.java | 80 --------------------- 1 file changed, 80 deletions(-) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index 040517389..0d72f49cb 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -920,86 +920,6 @@ private List checkout(final Run build, final FilePath workspace, final service = new CurrentThreadExecutorService(); } - - /* TODO: when previously there were 2 repository locations which were 'equal' i.e. checking out the same SVN module to the same local - * directory, this would succeed previously, but can now fail under unlucky timing with: - * (should at least detect this situation and give a better error message?) - * - * 15:10:44 ERROR: Failed to update http://xyz -15:10:44 org.tmatesoft.svn.core.SVNException: svn: E200030: BUSY -15:10:44 at org.tmatesoft.svn.core.internal.wc.SVNErrorManager.error(SVNErrorManager.java:64) -15:10:44 at org.tmatesoft.svn.core.internal.wc.SVNErrorManager.error(SVNErrorManager.java:51) -15:10:44 at org.tmatesoft.svn.core.internal.db.SVNSqlJetDb.createSqlJetError(SVNSqlJetDb.java:176) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbInsertWorkItem.exec(SVNWCDbInsertWorkItem.java:48) -15:10:44 at org.tmatesoft.svn.core.internal.db.SVNSqlJetStatement.done(SVNSqlJetStatement.java:367) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.addSingleWorkItem(SVNWCDb.java:267) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.addWorkItems(SVNWCDb.java:256) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.addWorkQueue(SVNWCDb.java:961) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.restoreFile(SVNReporter17.java:399) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.restoreNode(SVNReporter17.java:224) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:309) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.reportRevisionsAndDepths(SVNReporter17.java:387) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.SVNReporter17.report(SVNReporter17.java:188) -15:10:44 at org.tmatesoft.svn.core.internal.io.dav.handlers.DAVEditorHandler.generateEditorRequest(DAVEditorHandler.java:106) -15:10:44 at org.tmatesoft.svn.core.internal.io.dav.DAVRepository.runReport(DAVRepository.java:1275) -15:10:44 at org.tmatesoft.svn.core.internal.io.dav.DAVRepository.update(DAVRepository.java:837) -15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgAbstractUpdate.updateInternal(SvnNgAbstractUpdate.java:192) -15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgAbstractUpdate.update(SvnNgAbstractUpdate.java:76) -15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgUpdate.run(SvnNgUpdate.java:38) -15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgUpdate.run(SvnNgUpdate.java:18) -15:10:44 at org.tmatesoft.svn.core.internal.wc2.ng.SvnNgOperationRunner.run(SvnNgOperationRunner.java:20) -15:10:44 at org.tmatesoft.svn.core.internal.wc2.SvnOperationRunner.run(SvnOperationRunner.java:20) -15:10:44 at org.tmatesoft.svn.core.wc2.SvnOperationFactory.run(SvnOperationFactory.java:1235) -15:10:44 at org.tmatesoft.svn.core.wc2.SvnOperation.run(SvnOperation.java:291) -15:10:44 at org.tmatesoft.svn.core.wc.SVNUpdateClient.doUpdate(SVNUpdateClient.java:311) -15:10:44 at org.tmatesoft.svn.core.wc.SVNUpdateClient.doUpdate(SVNUpdateClient.java:291) -15:10:44 at org.tmatesoft.svn.core.wc.SVNUpdateClient.doUpdate(SVNUpdateClient.java:387) -15:10:44 at hudson.scm.subversion.UpdateUpdater$TaskImpl.perform(UpdateUpdater.java:157) -15:10:44 at hudson.scm.subversion.WorkspaceUpdater$UpdateTask.delegateTo(WorkspaceUpdater.java:153) -15:10:44 at hudson.scm.SubversionSCM$CheckOutTask.perform(SubversionSCM.java:1041) -15:10:44 at hudson.scm.SubversionSCM$CheckOutTask.invoke(SubversionSCM.java:1022) -15:10:44 at hudson.scm.SubversionSCM$CheckOutTask.invoke(SubversionSCM.java:1005) -15:10:44 at hudson.FilePath$FileCallableWrapper.call(FilePath.java:2246) -15:10:44 at hudson.remoting.UserRequest.perform(UserRequest.java:118) -15:10:44 at hudson.remoting.UserRequest.perform(UserRequest.java:48) -15:10:44 at hudson.remoting.Request$2.run(Request.java:326) -15:10:44 at hudson.remoting.InterceptingExecutorService$1.call(InterceptingExecutorService.java:72) -15:10:44 at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303) -15:10:44 at java.util.concurrent.FutureTask.run(FutureTask.java:138) -15:10:44 at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895) -15:10:44 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918) -15:10:44 at java.lang.Thread.run(Thread.java:662) -15:10:44 Caused by: svn: E200030: BUSY -15:10:44 at org.tmatesoft.svn.core.SVNErrorMessage.create(SVNErrorMessage.java:109) -15:10:44 at org.tmatesoft.svn.core.internal.db.SVNSqlJetDb.createSqlJetError(SVNSqlJetDb.java:175) -15:10:44 ... 47 more -15:10:44 Caused by: org.tmatesoft.sqljet.core.SqlJetException: BUSY: error code is BUSY -15:10:44 at org.tmatesoft.sqljet.core.internal.pager.SqlJetPager.begin(SqlJetPager.java:2785) -15:10:44 at org.tmatesoft.sqljet.core.internal.btree.SqlJetBtree.beginTrans(SqlJetBtree.java:929) -15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine.doBeginTransaction(SqlJetEngine.java:561) -15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine.access$100(SqlJetEngine.java:55) -15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine$12.runSynchronized(SqlJetEngine.java:535) -15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine.runSynchronized(SqlJetEngine.java:217) -15:10:44 at org.tmatesoft.sqljet.core.table.engine.SqlJetEngine.runEngineTransaction(SqlJetEngine.java:529) -15:10:44 at org.tmatesoft.sqljet.core.table.SqlJetDb.runTransaction(SqlJetDb.java:238) -15:10:44 at org.tmatesoft.sqljet.core.table.SqlJetDb.runWriteTransaction(SqlJetDb.java:211) -15:10:44 at org.tmatesoft.sqljet.core.internal.table.SqlJetTable.runWriteTransaction(SqlJetTable.java:156) -15:10:44 at org.tmatesoft.sqljet.core.internal.table.SqlJetTable.insertByFieldNamesOr(SqlJetTable.java:190) -15:10:44 at org.tmatesoft.sqljet.core.internal.table.SqlJetTable.insertByFieldNames(SqlJetTable.java:173) -15:10:44 at org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbInsertWorkItem.exec(SVNWCDbInsertWorkItem.java:46) -15:10:44 ... 46 more -15:10:44 ERROR: Subversion update failed - * - */ - @SuppressWarnings("deprecation") final TaskListener syncedListener = new StreamTaskListener(new SynchronizedPrintStream(listener.getLogger())); From 925c55f48489cb7179416e191180428297e0c758 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Fri, 9 Jan 2015 13:28:13 +0000 Subject: [PATCH 08/11] Synchronize access to unauthenticatedRealms set. --- src/main/java/hudson/scm/SubversionSCM.java | 37 +++++++-------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index 0d72f49cb..3df17472c 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -903,8 +903,9 @@ private List checkout(final Run build, final FilePath workspace, final } } - List externals = new ArrayList(); - final Set unauthenticatedRealms = new LinkedHashSet(); + final List externals = Collections.synchronizedList(new ArrayList()); + Set unauthenticatedRealms = new LinkedHashSet(); + ModuleLocation[] expandedLocations = getLocations(env, build); checkForLocationDuplicates(expandedLocations,listener); @@ -923,37 +924,23 @@ private List checkout(final Run build, final FilePath workspace, final @SuppressWarnings("deprecation") final TaskListener syncedListener = new StreamTaskListener(new SynchronizedPrintStream(listener.getLogger())); - List>> callables = Lists.newArrayListWithExpectedSize(expandedLocations.length); + final Set synchronizedUnauthenticatedRealms = Collections.synchronizedSet(unauthenticatedRealms); + + List> callables = Lists.newArrayListWithExpectedSize(expandedLocations.length); for (final ModuleLocation location : expandedLocations) { - callables.add(new java.util.concurrent.Callable>() { + callables.add(new java.util.concurrent.Callable() { - public List call() throws Exception { + public Void call() throws Exception { CheckOutTask checkOutTask = new CheckOutTask(build, SubversionSCM.this, location, build.getTimestamp().getTime(), syncedListener, env); - List externals = workspace.act(checkOutTask); - unauthenticatedRealms.addAll(checkOutTask.getUnauthenticatedRealms()); - return externals; + externals.addAll(workspace.act(checkOutTask)); + synchronizedUnauthenticatedRealms.addAll(checkOutTask.getUnauthenticatedRealms()); + return null; } }); } - List>> futures = service.invokeAll(callables); - - for (Future> future : futures) { - - try { - externals.addAll(future.get()); - } catch (ExecutionException e) { - throw new IOException2(e); - } - // olamy: remove null check as it causes test failure - // see https://github.com/jenkinsci/subversion-plugin/commit/de23a2b781b7b86f41319977ce4c11faee75179b#commitcomment-1551273 - /*if ( externalsFound != null ){ - externals.addAll(externalsFound); - } else { - externals.addAll( new ArrayList( 0 ) ); - }*/ - } + service.invokeAll(callables); service.shutdownNow(); if (additionalCredentials != null) { From 1293366d138df16170ee1f8cdf6ad5ad40059dea Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Fri, 9 Jan 2015 14:09:44 +0000 Subject: [PATCH 09/11] We need to propagate execution exceptions of the callables --- src/main/java/hudson/scm/SubversionSCM.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index 3df17472c..dae98dc86 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -940,7 +940,15 @@ public Void call() throws Exception { }); } - service.invokeAll(callables); + List> futures = service.invokeAll(callables); + for (Future future : futures) { + + try { + future.get(); + } catch (ExecutionException e) { + throw new IOException(e); + } + } service.shutdownNow(); if (additionalCredentials != null) { From 8d0a195f64ffecdcc94d1602a12d7743a33f7ff7 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Fri, 13 Feb 2015 16:44:38 +0000 Subject: [PATCH 10/11] make maximum number of checkout threads configurable with system property --- src/main/java/hudson/scm/SubversionSCM.java | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index dae98dc86..4651c9d39 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -54,6 +54,7 @@ import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; + import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.BulkChange; @@ -193,6 +194,7 @@ import com.trilead.ssh2.DebugLogger; import com.trilead.ssh2.SCPClient; import com.trilead.ssh2.crypto.Base64; + import javax.annotation.Nonnull; /** @@ -215,7 +217,21 @@ */ @SuppressWarnings("rawtypes") public class SubversionSCM extends SCM implements Serializable { - /** + + private static final int MAX_CHECKOUT_THREADS; + static { + String prop = System.getProperty("jenkins.subversion-plugin.maxCheckoutThreads", "4"); + int nr; + try { + nr = Integer.parseInt(prop); + } catch (NumberFormatException e) { + nr = 4; + } + + MAX_CHECKOUT_THREADS = nr > 0 ? nr : 1; + } + + /** * the locations field is used to store all configured SVN locations (with * their local and remote part). Direct access to this field should be * avoided and the getLocations() method should be used instead. This is @@ -910,7 +926,7 @@ private List checkout(final Run build, final FilePath workspace, final checkForLocationDuplicates(expandedLocations,listener); - int numberOfExecutors = Math.min(4, expandedLocations.length); + int numberOfExecutors = Math.min(MAX_CHECKOUT_THREADS, expandedLocations.length); final ExecutorService service; From 81398de108ad9e82c64c80f973243e3288e0da65 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Fri, 13 Feb 2015 17:07:45 +0000 Subject: [PATCH 11/11] do check not only for exact local checkout duplicates, but also for overlaps --- src/main/java/hudson/scm/SubversionSCM.java | 30 ++++++++++++++----- .../hudson/scm/SubversionSCMUnitTest.java | 29 ++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/main/java/hudson/scm/SubversionSCM.java b/src/main/java/hudson/scm/SubversionSCM.java index 4651c9d39..bd7b232d0 100755 --- a/src/main/java/hudson/scm/SubversionSCM.java +++ b/src/main/java/hudson/scm/SubversionSCM.java @@ -924,7 +924,7 @@ private List checkout(final Run build, final FilePath workspace, final ModuleLocation[] expandedLocations = getLocations(env, build); - checkForLocationDuplicates(expandedLocations,listener); + checkForLocationDuplicatesOrOverlaps(expandedLocations,listener); int numberOfExecutors = Math.min(MAX_CHECKOUT_THREADS, expandedLocations.length); @@ -1001,19 +1001,33 @@ public Void call() throws Exception { * Checks that there a no 2 locations which try to checkout to the same local location, * as that may cause E200030: BUSY errors. */ - private void checkForLocationDuplicates(ModuleLocation[] expandedLocations, + static boolean checkForLocationDuplicatesOrOverlaps(ModuleLocation[] expandedLocations, TaskListener listener) { - if (locations.length < 2) { - return; + if (expandedLocations.length < 2) { + return false; } Set localPaths = new HashSet(); for (ModuleLocation loc : expandedLocations) { - if (!localPaths.add(loc.getLocalDir())) { - listener.getLogger().println("WARN: you have 2 repository locations pointing to the same local path: " + loc.getLocalDir() - +".\n This may cause subsequent errors - e.g. E200030: BUSY"); - } + + String locationPath = loc.getLocalDir(); + if (!locationPath.endsWith("/")) { + locationPath += "/"; + } + + for (String path : localPaths) { + if (path.startsWith(locationPath) || locationPath.startsWith(path)) { + listener.getLogger().println("WARN: you have 2 repository locations with overlapping local checkout paths: " + +path+", "+locationPath + +".\n This may cause subsequent errors - e.g. E200030: BUSY"); + return true; + } + } + + localPaths.add(locationPath); } + + return false; } /** diff --git a/src/test/java/hudson/scm/SubversionSCMUnitTest.java b/src/test/java/hudson/scm/SubversionSCMUnitTest.java index f745a12f0..829eef36e 100644 --- a/src/test/java/hudson/scm/SubversionSCMUnitTest.java +++ b/src/test/java/hudson/scm/SubversionSCMUnitTest.java @@ -12,14 +12,17 @@ import hudson.model.AbstractBuild; import hudson.remoting.VirtualChannel; import hudson.scm.SubversionSCM.ModuleLocation; +import hudson.util.StreamTaskListener; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; import org.jvnet.hudson.test.Bug; +import org.jvnet.hudson.test.Url; /** * Unit tests for {@link SubversionSCM}. @@ -105,6 +108,32 @@ public void shouldSetEnvironmentVariablesWithMultipleSvnModules() throws IOExcep assertThat(envVars.get("SVN_REVISION_2"), is("42")); } + @Test + @Url("https://github.com/jenkinsci/subversion-plugin/pull/110#issuecomment-73030817") + public void shouldDetectOverlappingModules() { + ModuleLocation[] locations = new ModuleLocation[2]; + locations[0] = new ModuleLocation("", null, "/trunk", null, false); + + locations[1] = new ModuleLocation("", null, "/trunk/someSubPath/", null, false); + + boolean foundDuplicates = SubversionSCM.checkForLocationDuplicatesOrOverlaps(locations, new StreamTaskListener(System.out)); + + assertThat(foundDuplicates, is(true)); + } + + @Test + @Url("https://github.com/jenkinsci/subversion-plugin/pull/110#issuecomment-73030817") + public void shouldNotBeConfusedByPartialDirectoryNameMatch() { + ModuleLocation[] locations = new ModuleLocation[2]; + locations[0] = new ModuleLocation("", null, "/trunk/someSubPath", null, false); + + locations[1] = new ModuleLocation("", null, "/trunk/someSubPath_and_more/", null, false); + + boolean foundDuplicates = SubversionSCM.checkForLocationDuplicatesOrOverlaps(locations, new StreamTaskListener(System.out)); + + assertThat(foundDuplicates, is(false)); + } + private SubversionSCM mockSCMForBuildEnvVars() { SubversionSCM scm = mock(SubversionSCM.class); doCallRealMethod().when(scm).buildEnvVars(any(AbstractBuild.class), anyMapOf(String.class, String.class));