Java tutorial
/* * The MIT License * * Copyright 2016 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 com.cloudbees.hudson.plugins.folder.computed; import com.cloudbees.hudson.plugins.folder.AbstractFolderDescriptor; import com.cloudbees.hudson.plugins.folder.views.AbstractFolderViewHolder; import com.gargoylesoftware.htmlunit.html.DomElement; import com.gargoylesoftware.htmlunit.html.HtmlPage; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.AbortException; import hudson.Functions; import hudson.model.AllView; import hudson.model.Computer; import hudson.model.Descriptor; import hudson.model.Executor; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.model.ItemGroup; import hudson.model.ListView; import hudson.model.MyView; import hudson.model.OneOffExecutor; import hudson.model.Queue; import hudson.model.Result; import hudson.model.TaskListener; import hudson.model.TopLevelItem; import hudson.model.View; import hudson.model.ViewGroup; import hudson.model.queue.QueueTaskFuture; import hudson.triggers.TimerTrigger; import hudson.util.StreamTaskListener; import hudson.triggers.Trigger; import hudson.views.DefaultViewsTabBar; import hudson.views.ViewsTabBar; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.management.ThreadInfo; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javax.servlet.ServletException; import org.apache.commons.lang.StringUtils; import org.junit.Test; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.*; import org.junit.Rule; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.SleepBuilder; import org.jvnet.hudson.test.TestExtension; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; public class ComputedFolderTest { @Rule public JenkinsRule r = new JenkinsRule(); @Issue("JENKINS-32179") @Test public void duplicateEntries() throws Exception { SampleComputedFolder d = r.jenkins.createProject(SampleComputedFolder.class, "d"); d.recompute(Result.SUCCESS); d.assertItemNames(1); d.kids.addAll(Arrays.asList("A", "B", "A", "C")); d.recompute(Result.SUCCESS); d.assertItemNames(2, "A", "B", "C"); assertEquals("[A, B, C]", d.created.toString()); d.recompute(Result.SUCCESS); d.assertItemNames(3, "A", "B", "C"); assertEquals("[A, B, C]", d.created.toString()); d.kids.remove("B"); d.recompute(Result.SUCCESS); d.assertItemNames(4, "A", "C"); assertEquals("[A, B, C]", d.created.toString()); assertEquals("[B]", d.deleted.toString()); d.kids.addAll(Arrays.asList("D", "B")); d.recompute(Result.SUCCESS); d.assertItemNames(5, "A", "B", "C", "D"); assertEquals("[A, B, C, D, B]", d.created.toString()); assertEquals("[B]", d.deleted.toString()); Map<String, String> descriptions = new TreeMap<String, String>(); for (FreeStyleProject p : d.getItems()) { descriptions.put(p.getName(), p.getDescription()); } assertEquals("{A=updated in round #5, B=created in round #5, C=updated in round #5, D=created in round #5}", descriptions.toString()); } @Test public void abortException() throws Exception { SampleComputedFolder d = r.jenkins.createProject(SampleComputedFolder.class, "d"); d.setDisplayName("My Folder"); d.kids.addAll(Arrays.asList("A", "B")); d.recompute(Result.SUCCESS); d.assertItemNames(1, "A", "B"); d.kids.add("Z"); d.kids.remove("A"); // Despite its name, AbortException is intended to be FAILURE, not ABORTED which would be InterruptedException. String log = d.recompute(Result.FAILURE); d.assertItemNames(2, "A", "B"); assertTrue(log, log.contains("not adding Z")); assertFalse(log, log.contains(SampleComputedFolder.class.getName())); } @Issue("JENKINS-25240") @Test public void runningBuild() throws Exception { SampleComputedFolder d = r.jenkins.createProject(SampleComputedFolder.class, "d"); d.kids.addAll(Arrays.asList("A", "B")); d.recompute(Result.SUCCESS); d.assertItemNames(1, "A", "B"); d.getItem("B").getBuildersList().add(new SleepBuilder(Long.MAX_VALUE)); d.getItem("B").setConcurrentBuild(true); FreeStyleBuild b1 = d.getItem("B").scheduleBuild2(0).waitForStart(); FreeStyleBuild b2 = d.getItem("B").scheduleBuild2(0).waitForStart(); d.kids.remove("B"); d.recompute(Result.SUCCESS); d.assertItemNames(2, "A", "B"); for (FreeStyleBuild b : new FreeStyleBuild[] { b1, b2 }) { assertTrue(b.isBuilding()); b.doStop(); r.assertBuildStatus(Result.ABORTED, r.waitForCompletion(b)); } d.recompute(Result.SUCCESS); d.assertItemNames(3, "A"); FreeStyleBuild a1 = d.getItem("A").scheduleBuild2(0).get(); FreeStyleBuild a2 = d.getItem("A").scheduleBuild2(0).get(); a1.keepLog(true); d.kids.remove("A"); d.recompute(Result.SUCCESS); d.assertItemNames(4, "A"); a1.keepLog(false); d.recompute(Result.SUCCESS); d.assertItemNames(5); } @Test public void notAddChildren() throws Exception { JenkinsRule.WebClient client = r.createWebClient(); SampleComputedFolder s = r.jenkins.createProject(SampleComputedFolder.class, "s"); assertEquals(client.getPage(s).getByXPath("//a[contains(text(), \"New Item\")]").size(), 0); s.kids.add("A"); s.recompute(Result.SUCCESS); assertEquals(client.getPage(s).getByXPath("//a[contains(text(), \"New Item\")]").size(), 0); } @Test public void runByTrigger() throws Exception { SampleComputedFolder s = r.jenkins.createProject(SampleComputedFolder.class, "s"); s.assertItemNames(0); PeriodicFolderTrigger t = new PeriodicFolderTrigger("1m"); s.addTrigger(t); t.run(); r.waitUntilNoActivity(); s.assertItemNames(1); } /** Verify that running branch projects are not deleted even after an organization folder reindex. */ @Issue("JENKINS-25240") @Test public void runningBuildMeta() throws Exception { SecondOrderComputedFolder org = r.jenkins.createProject(SecondOrderComputedFolder.class, "org"); org.metakids.add(Arrays.asList("A", "B")); org.metakids.add(Arrays.asList("C", "D")); org.assertItemNames("A+B", "C+D"); FreeStyleProject b = r.jenkins.getItemByFullName("org/A+B/B", FreeStyleProject.class); b.getBuildersList().add(new SleepBuilder(Long.MAX_VALUE)); FreeStyleBuild b1 = b.scheduleBuild2(0).waitForStart(); org.metakids.remove(0); org.assertItemNames("A+B", "C+D"); assertTrue(b1.isBuilding()); b1.doStop(); r.assertBuildStatus(Result.ABORTED, r.waitForCompletion(b1)); org.assertItemNames("C+D"); } @Test public void viewHolderRestrictions() throws Exception { LockedDownSampleComputedFolder org = r.jenkins.createProject(LockedDownSampleComputedFolder.class, "org"); // initial setup is correct assertThat(org.getViews().size(), is(2)); assertThat(org.getPrimaryView().getViewName(), is("Empty")); assertThat(org.getView("All"), notNullValue()); // permissions are as expected assertThat(org.getFolderViews().isPrimaryModifiable(), is(false)); assertThat(org.getFolderViews().isViewsModifiable(), is(false)); assertThat(org.getFolderViews().isTabBarModifiable(), is(false)); // trying to set the primary view is a no-op org.setPrimaryView(org.getView("All")); assertThat(org.getPrimaryView().getViewName(), is("Empty")); // adding a view is a no-op org.addView(new MyView("mine", org)); assertThat(org.getViews().size(), is(2)); // primary not modifiable, no ability to configure primary view JenkinsRule.WebClient client = r.createWebClient(); HtmlPage configure = client.getPage(org, "configure"); assertThat(configure.getElementsByName("primaryView"), is(Collections.<DomElement>emptyList())); SampleComputedFolder org2 = r.jenkins.createProject(SampleComputedFolder.class, "org2"); // only one view, no ability to configure primary configure = client.getPage(org, "configure"); assertThat(configure.getElementsByName("primaryView"), is(Collections.<DomElement>emptyList())); // more than one view and primary is modifiable, so we can configure it org2.addView(new MyView("mine", org2)); configure = client.getPage(org2, "configure"); assertThat(configure.getElementsByName("primaryView"), not(is(Collections.<DomElement>emptyList()))); } @Test public void recomputationSuppression() throws Exception { final VariableRecomputationComputedFolder org = r.jenkins .createProject(VariableRecomputationComputedFolder.class, "org"); // no recalculateAfterSubmitted calls means we recalculate r.waitUntilNoActivity(); int round = org.round; r.configRoundtrip(org); r.waitUntilNoActivity(); assertThat(org.round, is(round + 1)); // recalculateAfterSubmitted(true) calls means we recalculate org.submit = new Runnable() { @Override public void run() { org.recalculateAfterSubmitted(true); } }; round = org.round; r.configRoundtrip(org); r.waitUntilNoActivity(); assertThat(org.round, is(round + 1)); // recalculateAfterSubmitted(false) calls means we suppress org.submit = new Runnable() { @Override public void run() { org.recalculateAfterSubmitted(false); } }; round = org.round; r.configRoundtrip(org); r.waitUntilNoActivity(); assertThat(org.round, is(round)); } @Test public void recomputationSuppressionMulti() throws Exception { final VariableRecomputationComputedFolder org = r.jenkins .createProject(VariableRecomputationComputedFolder.class, "org"); // at least one recalculateAfterSubmitted(true) calls means we recalculate org.submit = new Runnable() { @Override public void run() { org.recalculateAfterSubmitted(true); org.recalculateAfterSubmitted(true); } }; int round = org.round; r.configRoundtrip(org); r.waitUntilNoActivity(); assertThat(org.round, is(round + 1)); // at least one recalculateAfterSubmitted(true) calls means we recalculate org.submit = new Runnable() { @Override public void run() { org.recalculateAfterSubmitted(true); org.recalculateAfterSubmitted(false); } }; round = org.round; r.configRoundtrip(org); r.waitUntilNoActivity(); assertThat(org.round, is(round + 1)); // at least one recalculateAfterSubmitted(true) calls means we recalculate org.submit = new Runnable() { @Override public void run() { org.recalculateAfterSubmitted(false); org.recalculateAfterSubmitted(true); } }; round = org.round; r.configRoundtrip(org); r.waitUntilNoActivity(); assertThat(org.round, is(round + 1)); // all recalculateAfterSubmitted(false) calls means we suppress org.submit = new Runnable() { @Override public void run() { org.recalculateAfterSubmitted(false); org.recalculateAfterSubmitted(false); } }; round = org.round; r.configRoundtrip(org); r.waitUntilNoActivity(); assertThat(org.round, is(round)); } @Test public void triggersRoundtrip() throws Exception { SampleComputedFolder s = r.jenkins.createProject(SampleComputedFolder.class, "s"); s.addTrigger(new PeriodicFolderTrigger("30m")); SampleComputedFolder s2 = r.configRoundtrip(s); Trigger<?> trigger = s2.getTriggers() .get(r.jenkins.getDescriptorByType(PeriodicFolderTrigger.DescriptorImpl.class)); assertThat(trigger, notNullValue()); assertThat(trigger, instanceOf(PeriodicFolderTrigger.class)); assertThat(((PeriodicFolderTrigger) trigger).getInterval(), is("30m")); } @Test @Issue("JENKINS-42511") public void concurrentEvents() throws Exception { CoordinatedComputedFolder d = r.jenkins.createProject(CoordinatedComputedFolder.class, "d"); d.kids.addAll(Arrays.asList("A", "B")); QueueTaskFuture<Queue.Executable> future = d.scheduleBuild2(0).getFuture(); d.onKid("B"); future.get(); waitUntilNoActivityIgnoringThreadDeathUpTo(10000); List<Throwable> deaths = new ArrayList<Throwable>(); for (Computer comp : r.jenkins.getComputers()) { for (Executor e : comp.getExecutors()) { if (e.getCauseOfDeath() != null) { deaths.add(e.getCauseOfDeath()); } } for (Executor e : comp.getOneOffExecutors()) { if (e.getCauseOfDeath() != null) { deaths.add(e.getCauseOfDeath()); } } } assertThat("None of the executors have died abnormally", deaths, containsInAnyOrder()); } @Test @Issue("JENKINS-35112") public void deleteWhileComputing() throws Exception { CoordinatedComputedFolder d = r.jenkins.createProject(CoordinatedComputedFolder.class, "d"); d.kids.addAll(Arrays.asList("A", "B")); QueueTaskFuture<Queue.Executable> future = d.scheduleBuild2(0).getFuture(); FolderComputation<FreeStyleProject> computation; while (Executor.of((computation = d.getComputation())) == null) { Thread.sleep(50); } d.delete(); assertThat(computation.getResult(), is(Result.ABORTED)); } /** * Waits until Hudson finishes building everything, including those in the queue, or fail the test * if the specified timeout milliseconds is */ public void waitUntilNoActivityIgnoringThreadDeathUpTo(int timeout) throws Exception { long startTime = System.currentTimeMillis(); int streak = 0; while (true) { Thread.sleep(10); if (isSomethingHappeningIgnoringThreadDeath()) streak = 0; else streak++; if (streak > 5) // the system is quiet for a while return; if (System.currentTimeMillis() - startTime > timeout) { List<Queue.Executable> building = new ArrayList<Queue.Executable>(); List<Throwable> deaths = new ArrayList<Throwable>(); for (Computer c : r.jenkins.getComputers()) { for (Executor e : c.getExecutors()) { if (e.isBusy()) { if (e.getCauseOfDeath() == null) { building.add(e.getCurrentExecutable()); } else { deaths.add(e.getCauseOfDeath()); } } } for (Executor e : c.getOneOffExecutors()) { if (e.isBusy()) { if (e.getCauseOfDeath() == null) { building.add(e.getCurrentExecutable()); } else { deaths.add(e.getCauseOfDeath()); } } } } ThreadInfo[] threadInfos = Functions.getThreadInfos(); Functions.ThreadGroupMap m = Functions.sortThreadsAndGetGroupMap(threadInfos); for (ThreadInfo ti : threadInfos) { System.err.println(Functions.dumpThreadInfo(ti, m)); } throw new AssertionError( String.format("Jenkins is still doing something after %dms: queue=%s building=%s deaths=%s", timeout, Arrays.asList(r.jenkins.getQueue().getItems()), building, deaths)); } } } /** * Returns true if Hudson is building something or going to build something. */ public boolean isSomethingHappeningIgnoringThreadDeath() { if (!r.jenkins.getQueue().isEmpty()) { return true; } for (Computer n : r.jenkins.getComputers()) { for (OneOffExecutor e : n.getOneOffExecutors()) { if (e.getCauseOfDeath() == null && e.isBusy()) { return true; } } for (Executor e : n.getExecutors()) { if (e.getCauseOfDeath() == null && e.isBusy()) { return true; } } } return false; } @SuppressWarnings({ "unchecked", "rawtypes" }) public static class SampleComputedFolder extends ComputedFolder<FreeStyleProject> { List<String> kids = new ArrayList<String>(); int round; List<String> created = new ArrayList<String>(); List<String> deleted = new ArrayList<String>(); private SampleComputedFolder(ItemGroup parent, String name) { super(parent, name); } @Override protected void computeChildren(ChildObserver<FreeStyleProject> observer, TaskListener listener) throws IOException, InterruptedException { round++; listener.getLogger().println("=== Round #" + round + " ==="); for (String kid : kids) { if (kid.equals("Z")) { throw new AbortException("not adding Z"); } listener.getLogger().println("considering " + kid); FreeStyleProject p = observer.shouldUpdate(kid); try { if (p == null) { if (observer.mayCreate(kid)) { listener.getLogger().println("creating a child"); p = new FreeStyleProject(this, kid); p.setDescription("created in round #" + round); observer.created(p); created.add(kid); } else { listener.getLogger().println("not allowed to create a child"); } } else { listener.getLogger() .println("updated existing child with description " + p.getDescription()); p.setDescription("updated in round #" + round); } } finally { observer.completed(kid); } } } @Override protected Collection<FreeStyleProject> orphanedItems(Collection<FreeStyleProject> orphaned, TaskListener listener) throws IOException, InterruptedException { Collection<FreeStyleProject> deleting = super.orphanedItems(orphaned, listener); for (FreeStyleProject p : deleting) { String kid = p.getName(); listener.getLogger().println("deleting " + kid + " in round #" + round); deleted.add(kid); } return deleting; } String recompute(Result result) throws Exception { return doRecompute(this, result); } void assertItemNames(int round, String... names) { assertEquals(round, this.round); Set<String> actual = new TreeSet<String>(); for (FreeStyleProject p : getItems()) { actual.add(p.getName()); } assertEquals(new TreeSet<String>(Arrays.asList(names)).toString(), actual.toString()); } @TestExtension public static class DescriptorImpl extends AbstractFolderDescriptor { @Override public TopLevelItem newInstance(ItemGroup parent, String name) { return new SampleComputedFolder(parent, name); } } } public static class LockedDownSampleComputedFolder extends SampleComputedFolder { private LockedDownSampleComputedFolder(ItemGroup parent, String name) { super(parent, name); } @Override protected AbstractFolderViewHolder newFolderViewHolder() { return new FixedViewHolder(this); } @TestExtension public static class DescriptorImpl extends AbstractFolderDescriptor { @Override public TopLevelItem newInstance(ItemGroup parent, String name) { return new LockedDownSampleComputedFolder(parent, name); } } private static class FixedViewHolder extends AbstractFolderViewHolder { final List<View> views; final ViewsTabBar tabBar = new DefaultViewsTabBar(); public FixedViewHolder(ViewGroup owner) { views = new ArrayList<View>(Arrays.asList(new AllView("All", owner), new ListView("Empty", owner))); } @NonNull @Override public List<View> getViews() { return Collections.unmodifiableList(views); } @Override public void setViews(@NonNull List<? extends View> views) { throw new UnsupportedOperationException("Blow up if called"); } @Override public boolean isViewsModifiable() { return false; } @Override public boolean isPrimaryModifiable() { return false; } @Override public boolean isTabBarModifiable() { return false; } @Override public String getPrimaryView() { return "Empty"; } @Override public void setPrimaryView(@CheckForNull String name) { throw new UnsupportedOperationException("Blow up if called"); } @NonNull @Override public ViewsTabBar getTabBar() { return tabBar; } @Override public void setTabBar(@NonNull ViewsTabBar tabBar) { throw new UnsupportedOperationException("Blow up if called"); } } } public static class VariableRecomputationComputedFolder extends SampleComputedFolder { private transient Runnable submit; private VariableRecomputationComputedFolder(ItemGroup parent, String name) { super(parent, name); } @Override protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, Descriptor.FormException { super.submit(req, rsp); if (submit != null) { submit.run(); } } @TestExtension public static class DescriptorImpl extends AbstractFolderDescriptor { @Override public TopLevelItem newInstance(ItemGroup parent, String name) { return new VariableRecomputationComputedFolder(parent, name); } } } static String doRecompute(ComputedFolder<?> d, Result result) throws Exception { d.scheduleBuild2(0).getFuture().get(); FolderComputation<?> computation = d.getComputation(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); computation.writeWholeLogTo(baos); String log = baos.toString(); assertEquals(log, result, computation.getResult()); return log; } @SuppressWarnings({ "unchecked", "rawtypes" }) public static class SecondOrderComputedFolder extends ComputedFolder<SampleComputedFolder> { List<List<String>> metakids = new ArrayList<List<String>>(); private SecondOrderComputedFolder(ItemGroup parent, String name) { super(parent, name); } @Override protected void computeChildren(ChildObserver<SampleComputedFolder> observer, TaskListener listener) throws IOException, InterruptedException { for (List<String> kids : metakids) { String childName = StringUtils.join(kids, '+'); listener.getLogger().println("considering " + childName); SampleComputedFolder d = observer.shouldUpdate(childName); try { if (d == null) { if (observer.mayCreate(childName)) { listener.getLogger().println("creating a child"); d = new SampleComputedFolder(this, childName); d.kids = kids; observer.created(d); } else { listener.getLogger().println("not allowed to create a child"); } } else { listener.getLogger().println("left existing child"); } } finally { observer.completed(childName); } } } String assertItemNames(String... names) throws Exception { String log = doRecompute(this, Result.SUCCESS); Set<String> actual = new TreeSet<String>(); for (SampleComputedFolder d : getItems()) { d.recompute(Result.SUCCESS); d.assertItemNames(d.round, d.kids.toArray(new String[0])); actual.add(d.getName()); } assertEquals(new TreeSet<String>(Arrays.asList(names)).toString(), actual.toString()); return log; } @TestExtension public static class DescriptorImpl extends AbstractFolderDescriptor { @Override public TopLevelItem newInstance(ItemGroup parent, String name) { return new SecondOrderComputedFolder(parent, name); } } } public static class CoordinatedComputedFolder extends ComputedFolder<FreeStyleProject> { List<String> kids = new ArrayList<String>(); int round; List<String> created = new ArrayList<String>(); List<String> deleted = new ArrayList<String>(); CountDownLatch compute = new CountDownLatch(2); private CoordinatedComputedFolder(ItemGroup parent, String name) { super(parent, name); } @Override protected void computeChildren(ChildObserver<FreeStyleProject> observer, TaskListener listener) throws IOException, InterruptedException { round++; listener.getLogger().println("=== Round #" + round + " ==="); List<String> kids = new ArrayList<>(this.kids); compute.countDown(); compute.await(); for (String kid : kids) { Thread.sleep(25); if (kid.equals("Z")) { throw new AbortException("not adding Z"); } listener.getLogger().println("considering " + kid); FreeStyleProject p = observer.shouldUpdate(kid); try { if (p == null) { if (observer.mayCreate(kid)) { listener.getLogger().println("creating a child"); p = new FreeStyleProject(this, kid); p.setDescription("created in round #" + round); p.getBuildersList().add(new SleepBuilder(500)); observer.created(p); created.add(kid); p.scheduleBuild(0, new TimerTrigger.TimerTriggerCause()); } else { listener.getLogger().println("not allowed to create a child"); } } else { listener.getLogger() .println("updated existing child with description " + p.getDescription()); p.setDescription("updated in round #" + round); } } finally { observer.completed(kid); } } } public void onKid(String kid) throws InterruptedException { compute.countDown(); compute.await(); Thread.sleep(25); try (StreamTaskListener listener = getComputation().createEventsListener(); ChildObserver<FreeStyleProject> observer = openEventsChildObserver()) { listener.getLogger().println("considering " + kid); FreeStyleProject p = observer.shouldUpdate(kid); try { if (p == null) { if (observer.mayCreate(kid)) { listener.getLogger().println("creating a child"); p = new FreeStyleProject(this, kid); p.setDescription("created in event #" + round); p.getBuildersList().add(new SleepBuilder(500)); observer.created(p); created.add(kid); p.scheduleBuild(0, new TimerTrigger.TimerTriggerCause()); } else { listener.getLogger().println("not allowed to create a child"); } } else { listener.getLogger() .println("updated existing child with description " + p.getDescription()); p.setDescription("updated in event #" + round); } } finally { observer.completed(kid); } } catch (IOException | InterruptedException e) { e.printStackTrace(); } } @Override protected Collection<FreeStyleProject> orphanedItems(Collection<FreeStyleProject> orphaned, TaskListener listener) throws IOException, InterruptedException { Collection<FreeStyleProject> deleting = super.orphanedItems(orphaned, listener); for (FreeStyleProject p : deleting) { String kid = p.getName(); listener.getLogger().println("deleting " + kid + " in round #" + round); deleted.add(kid); } return deleting; } String recompute(Result result) throws Exception { return doRecompute(this, result); } void assertItemNames(int round, String... names) { assertEquals(round, this.round); Set<String> actual = new TreeSet<String>(); for (FreeStyleProject p : getItems()) { actual.add(p.getName()); } assertEquals(new TreeSet<String>(Arrays.asList(names)).toString(), actual.toString()); } @TestExtension public static class DescriptorImpl extends AbstractFolderDescriptor { @Override public TopLevelItem newInstance(ItemGroup parent, String name) { return new CoordinatedComputedFolder(parent, name); } } } }