? main/core/.settings ? main/core/patch Index: extras/tester/src/test/java/hudson/model/HudsonTestCase.java =================================================================== RCS file: /cvs/hudson/hudson/extras/tester/src/test/java/hudson/model/HudsonTestCase.java,v retrieving revision 1.4 diff -u -r1.4 HudsonTestCase.java --- extras/tester/src/test/java/hudson/model/HudsonTestCase.java 7 Sep 2007 17:00:42 -0000 1.4 +++ extras/tester/src/test/java/hudson/model/HudsonTestCase.java 10 Sep 2007 10:06:02 -0000 @@ -22,14 +22,20 @@ @Override protected void setUp() throws Exception { - if (hudson == null) { - hudson = newHudson(); - } + hudson = newHudson(); // Limit to 1 executor setNumExecutors(1); } + @Override + protected void tearDown() throws Exception { + // FIXME would be nice to be able to reset the Hudson instance programmatically + Field theInstance = Hudson.class.getDeclaredField("theInstance"); + theInstance.setAccessible(true); + theInstance.set(hudson, null); + } + protected void setNumExecutors(int i) { // FIXME would be nice to have a Hudson.setNumExecutors() method Field numExecutors; @@ -88,7 +94,7 @@ long slept = 0; while (project.getBuilds().size() != numbuilds || project.getBuildByNumber(numbuilds).isBuilding()) { Thread.sleep(100); - slept+=100; + slept += 100; if (slept >= 20000) fail("Timed out waiting 20 seconds for project " + project.getName() + " build #" + numbuilds); } @@ -100,6 +106,7 @@ throw new RuntimeException(e); } } + protected Result waitForNextBuild(Project project) { return waitForBuild(project.getBuilds().size() + 1, project); } Index: extras/tester/src/test/java/hudson/model/SubversionPollingTest.java =================================================================== RCS file: /cvs/hudson/hudson/extras/tester/src/test/java/hudson/model/SubversionPollingTest.java,v retrieving revision 1.1 diff -u -r1.1 SubversionPollingTest.java --- extras/tester/src/test/java/hudson/model/SubversionPollingTest.java 7 Sep 2007 17:00:42 -0000 1.1 +++ extras/tester/src/test/java/hudson/model/SubversionPollingTest.java 10 Sep 2007 10:06:02 -0000 @@ -1,5 +1,7 @@ package hudson.model; +import hudson.DependencyRunner; +import hudson.DependencyRunner.ProjectRunnable; import hudson.tasks.BuildTrigger; import hudson.triggers.SCMTrigger; import hudson.triggers.Trigger; @@ -7,8 +9,8 @@ import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; -import java.lang.reflect.Method; import java.util.Arrays; +import java.util.GregorianCalendar; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; @@ -21,25 +23,32 @@ private static final Logger LOGGER = Logger.getLogger(SubversionPollingTest.class.getName()); private static final String EVERY_SECOND = "* * * * *"; + FreeStyleProject projectA, projectB; + File wcProjectA, wcProjectB; @SuppressWarnings("unchecked") - public void testSubversionPolling() throws Exception { - FreeStyleProject projectA = (FreeStyleProject) hudson.createProject(FreeStyleProject.DESCRIPTOR, "projectFoo"); + protected void createProjects() throws Exception { + projectA = (FreeStyleProject) hudson.createProject(FreeStyleProject.DESCRIPTOR, "projectFoo"); Trigger t1 = new SCMTrigger(EVERY_SECOND); projectA.addTrigger(t1); t1.start(projectA, false); - File wcProjectA = createSubversionProject(projectA); - FreeStyleProject projectB = (FreeStyleProject) hudson.createProject(FreeStyleProject.DESCRIPTOR, "projectBar"); + wcProjectA = createSubversionProject(projectA); + projectB = (FreeStyleProject) hudson.createProject(FreeStyleProject.DESCRIPTOR, "projectBar"); // Setup dependency between projects A and B projectA.addPublisher(new BuildTrigger(Arrays.asList(new AbstractProject[]{projectB}), null)); - hudson.rebuildDependencyGraph(); + hudson.rebuildDependencyGraph(); Trigger t2 = new SCMTrigger(EVERY_SECOND); projectB.addTrigger(t2); t2.start(projectB, false); - File wcProjectB = createSubversionProject(projectB); + wcProjectB = createSubversionProject(projectB); setCommand(projectB, "sh -xe build.sh"); + } + + protected void buildProjects() throws Exception { + createProjects(); + File build = new File(wcProjectB, "build.sh"); OutputStream out = new FileOutputStream(build); IOUtils.write("exit 0", out); @@ -62,18 +71,31 @@ IOUtils.write("test -e ../../projectFoo/workspace/hello\n", out); out.close(); exec("svn", "commit", "-m", "build", build.getPath()); + } + + int count = 0; + public void testDeps() throws Exception { + createProjects(); + new DependencyRunner(new ProjectRunnable() { + public void run(AbstractProject p) { + if (count == 0) + assertEquals(projectA, p); + else if (count == 1) + assertEquals(projectB, p); + count++; + } + }); + } + + public void testSubversionPolling() throws Exception { + buildProjects(); - // poll for changes manually - Hudson inst = Hudson.getInstance(); - for (AbstractProject p : inst.getAllItems(AbstractProject.class)) { + // poll for changes manually, this is a copy/paste of Trigger.Cron.run() + for (AbstractProject p : hudson.getAllItems(AbstractProject.class)) { for (Trigger t : p.getTriggers().values()) { - LOGGER.fine("cron checking "+p.getName()); + t.run(); // Introduce a delay to make polling nearly-synchronous Thread.sleep(1000); - // FIXME could we make Trigger.run() public? - Method run = Trigger.class.getDeclaredMethod("run", new Class[0]); - run.setAccessible(true); - run.invoke(t, new Object[0]); } } @@ -81,10 +103,27 @@ waitForBuild(3, projectB); assertSuccess(projectB.getBuildByNumber(1).getResult()); + // projectBar has built before projectFoo assertFailure(projectB.getBuildByNumber(2).getResult()); assertSuccess(projectB.getBuildByNumber(3).getResult()); assertSuccess(projectA.getBuildByNumber(1).getResult()); assertSuccess(projectA.getBuildByNumber(2).getResult()); } + + public void testSynchronousSubversionPolling() throws Exception { + hudson.synchronousPolling = true; + buildProjects(); + + Trigger.checkTriggers(new GregorianCalendar()); + + waitForBuild(2, projectA); + waitForBuild(2, projectB); + + assertSuccess(projectB.getBuildByNumber(1).getResult()); + assertSuccess(projectB.getBuildByNumber(2).getResult()); + + assertSuccess(projectA.getBuildByNumber(1).getResult()); + assertSuccess(projectA.getBuildByNumber(2).getResult()); + } } Index: main/core/src/main/java/hudson/DependencyRunner.java =================================================================== RCS file: main/core/src/main/java/hudson/DependencyRunner.java diff -N main/core/src/main/java/hudson/DependencyRunner.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ main/core/src/main/java/hudson/DependencyRunner.java 10 Sep 2007 10:06:02 -0000 @@ -0,0 +1,29 @@ +package hudson; +import hudson.model.AbstractProject; +import hudson.model.Hudson; + +import java.util.List; + + +/** + * Runs a job on all projects in the order of dependencies + */ +public class DependencyRunner { + AbstractProject currentProject; + @SuppressWarnings("unchecked") + public DependencyRunner(ProjectRunnable runnable) { + for (AbstractProject p : Hudson.getInstance().getAllItems(AbstractProject.class)) { + // Get all top-level projects + if (p.getUpstreamProjects().size() == 0) { + runnable.run(p); + // Get all downstream dependencies + // FIXME why a cast is needed here?! + for (AbstractProject q : (List)p.getDownstreamProjects()) + runnable.run(q); + } + } + } + public interface ProjectRunnable { + void run(AbstractProject p); + } +} Index: main/core/src/main/java/hudson/model/Hudson.java =================================================================== RCS file: /cvs/hudson/hudson/main/core/src/main/java/hudson/model/Hudson.java,v retrieving revision 1.110 diff -u -r1.110 Hudson.java --- main/core/src/main/java/hudson/model/Hudson.java 4 Sep 2007 05:09:19 -0000 1.110 +++ main/core/src/main/java/hudson/model/Hudson.java 10 Sep 2007 10:06:03 -0000 @@ -113,6 +113,8 @@ */ private transient final Map computers = new CopyOnWriteMap.Hash(); + public boolean synchronousPolling = false; + /** * Number of executors of the master node. */ Index: main/core/src/main/java/hudson/triggers/SCMTrigger.java =================================================================== RCS file: /cvs/hudson/hudson/main/core/src/main/java/hudson/triggers/SCMTrigger.java,v retrieving revision 1.20 diff -u -r1.20 SCMTrigger.java --- main/core/src/main/java/hudson/triggers/SCMTrigger.java 10 Aug 2007 06:20:48 -0000 1.20 +++ main/core/src/main/java/hudson/triggers/SCMTrigger.java 10 Sep 2007 10:06:03 -0000 @@ -4,6 +4,7 @@ import hudson.Util; import hudson.model.AbstractProject; import hudson.model.Action; +import hudson.model.Hudson; import hudson.model.Item; import hudson.model.Project; import hudson.model.SCMedItem; @@ -63,15 +64,18 @@ return super.readResolve(); } - protected void run() { + public void run() { if(pollingScheduled) return; // noop pollingScheduled = true; - // schedule the polling. - // even if we end up submitting this too many times, that's OK. - // the real exclusion control happens inside Runner. - DESCRIPTOR.getExecutor().submit(new Runner()); + if (Hudson.getInstance().synchronousPolling) + new Runner().run(); + else + // schedule the polling. + // even if we end up submitting this too many times, that's OK. + // the real exclusion control happens inside Runner. + DESCRIPTOR.getExecutor().submit(new Runner()); } public Action getProjectAction() { Index: main/core/src/main/java/hudson/triggers/TimerTrigger.java =================================================================== RCS file: /cvs/hudson/hudson/main/core/src/main/java/hudson/triggers/TimerTrigger.java,v retrieving revision 1.4 diff -u -r1.4 TimerTrigger.java --- main/core/src/main/java/hudson/triggers/TimerTrigger.java 8 Feb 2007 14:17:17 -0000 1.4 +++ main/core/src/main/java/hudson/triggers/TimerTrigger.java 10 Sep 2007 10:06:03 -0000 @@ -1,17 +1,19 @@ package hudson.triggers; -import antlr.ANTLRException; import static hudson.Util.fixNull; -import hudson.model.Descriptor; import hudson.model.BuildableItem; import hudson.model.Item; import hudson.scheduler.CronTabList; import hudson.util.FormFieldValidator; + +import java.io.IOException; + +import javax.servlet.ServletException; + import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import javax.servlet.ServletException; -import java.io.IOException; +import antlr.ANTLRException; /** * {@link Trigger} that runs a job periodically. @@ -23,7 +25,7 @@ super(cronTabSpec); } - protected void run() { + public void run() { job.scheduleBuild(); } Index: main/core/src/main/java/hudson/triggers/Trigger.java =================================================================== RCS file: /cvs/hudson/hudson/main/core/src/main/java/hudson/triggers/Trigger.java,v retrieving revision 1.11 diff -u -r1.11 Trigger.java --- main/core/src/main/java/hudson/triggers/Trigger.java 1 Aug 2007 17:05:30 -0000 1.11 +++ main/core/src/main/java/hudson/triggers/Trigger.java 10 Sep 2007 10:06:03 -0000 @@ -1,7 +1,9 @@ package hudson.triggers; import antlr.ANTLRException; +import hudson.DependencyRunner; import hudson.ExtensionPoint; +import hudson.DependencyRunner.ProjectRunnable; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.Build; @@ -17,6 +19,7 @@ import java.io.InvalidObjectException; import java.io.ObjectStreamException; import java.util.Calendar; +import java.util.Collection; import java.util.Collections; import java.util.GregorianCalendar; import java.util.Timer; @@ -53,7 +56,7 @@ * This method is invoked when {@link #Trigger(String)} is used * to create an instance, and the crontab matches the current time. */ - protected void run() {} + public void run() {} /** * Called before a {@link Trigger} is removed. @@ -131,16 +134,7 @@ LOGGER.fine("cron checking "+cal.getTime().toLocaleString()); try { - Hudson inst = Hudson.getInstance(); - for (AbstractProject p : inst.getAllItems(AbstractProject.class)) { - for (Trigger t : p.getTriggers().values()) { - LOGGER.fine("cron checking "+p.getName()); - if(t.tabs.check(cal)) { - LOGGER.fine("cron triggered "+p.getName()); - t.run(); - } - } - } + checkTriggers(cal); } catch (Throwable e) { LOGGER.log(Level.WARNING,"Cron thread throw an exception",e); // bug in the code. Don't let the thread die. @@ -150,6 +144,38 @@ cal.add(Calendar.MINUTE,1); } } + + public static void checkTriggers(final Calendar cal) { + Hudson inst = Hudson.getInstance(); + + if (inst.synchronousPolling) { + // Process SCMTriggers in the order of dependencies, SCMTrigger's crontab spec is ignored + // FIXME allow to set a crontab spec + new Thread() { + @Override + public void run() { + new DependencyRunner(new ProjectRunnable() { + public void run(AbstractProject p) { + for (Trigger t : (Collection) p.getTriggers().values()) { + if (t instanceof SCMTrigger) + t.run(); + } + } + }); + } + }.run(); + } + + for (AbstractProject p : inst.getAllItems(AbstractProject.class)) { + for (Trigger t : p.getTriggers().values()) { + LOGGER.fine("cron checking "+p.getName()); + if(t.tabs.check(cal)) { + LOGGER.fine("cron triggered "+p.getName()); + t.run(); + } + } + } + } private static final Logger LOGGER = Logger.getLogger(Trigger.class.getName());