com.cloudbees.hudson.plugins.folder.computed.ComputedFolderTest.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudbees.hudson.plugins.folder.computed.ComputedFolderTest.java

Source

/*
 * 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);
            }

        }

    }

}