/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.surefire.junitcore.pc;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;

import net.jcip.annotations.NotThreadSafe;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.junit.runners.Suite;
import org.junit.runners.model.InitializationError;

import static org.apache.maven.surefire.junitcore.pc.RangeMatcher.between;
import static org.hamcrest.core.AnyOf.anyOf;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;

/**
 * @author Tibor Digana (tibor17)
 * @since 2.16
 */
@SuppressWarnings("checkstyle:magicnumber")
public class ParallelComputerBuilderTest {
    private static final int DELAY_MULTIPLIER = 7;

    private static final Object CLASS1BLOCK = new Object();

    private static volatile boolean beforeShutdown;

    private static volatile Runnable shutdownTask;

    private static final ConsoleLogger LOGGER = mock(ConsoleLogger.class);

    private static void testKeepBeforeAfter(ParallelComputerBuilder builder, Class<?>... classes) {
        JUnitCore core = new JUnitCore();
        for (int round = 0; round < 5; round++) {
            NothingDoingTest1.METHODS.clear();
            Result result = core.run(builder.buildComputer(), classes);
            assertTrue(result.wasSuccessful());
            Iterator<String> methods = NothingDoingTest1.METHODS.iterator();
            for (Class<?> clazz : classes) {
                String a = clazz.getName() + "#a()";
                String b = clazz.getName() + "#b()";
                assertThat(methods.next(), is("init"));
                assertThat(methods.next(), anyOf(is(a), is(b)));
                assertThat(methods.next(), anyOf(is(a), is(b)));
                assertThat(methods.next(), is("deinit"));
            }
        }
    }

    @BeforeClass
    public static void cleanup() throws InterruptedException {
        System.gc();
        Thread.sleep(500L);
    }

    @Before
    public void beforeTest() {
        Class1.maxConcurrentMethods = 0;
        Class1.concurrentMethods = 0;
        shutdownTask = null;
        NotThreadSafeTest1.t = null;
        NotThreadSafeTest2.t = null;
        NotThreadSafeTest3.t = null;
        NormalTest1.t = null;
        NormalTest2.t = null;
    }

    @Test
    public void testsWithoutChildrenShouldAlsoBeRun() {
        ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER);
        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) parallelComputerBuilder.buildComputer();
        Result result = new JUnitCore().run(computer, TestWithoutPrecalculatedChildren.class);
        assertThat(result.getRunCount(), is(1));
    }

    @Test
    public void parallelMethodsReuseOneOrTwoThreads() {
        ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER);
        parallelComputerBuilder.useOnePool(4);

        // One thread because one suite: TestSuite, however the capacity is 5.
        parallelComputerBuilder.parallelSuites(5);

        // Two threads because TestSuite has two classes, however the capacity is 5.
        parallelComputerBuilder.parallelClasses(5);

        // One or two threads because one threads comes from '#useOnePool(4)'
        // and next thread may be reused from finished class, however the capacity is 3.
        parallelComputerBuilder.parallelMethods(3);

        assertFalse(parallelComputerBuilder.isOptimized());

        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) parallelComputerBuilder.buildComputer();
        final JUnitCore core = new JUnitCore();
        final long t1 = systemMillis();
        final Result result = core.run(computer, TestSuite.class);
        final long t2 = systemMillis();
        final long timeSpent = t2 - t1;

        assertThat(computer.getSuites().size(), is(1));
        assertThat(computer.getClasses().size(), is(0));
        assertThat(computer.getNestedClasses().size(), is(2));
        assertThat(computer.getNestedSuites().size(), is(0));
        assertFalse(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(4));
        assertTrue(result.wasSuccessful());
        if (Class1.maxConcurrentMethods == 1) {
            assertThat(timeSpent, between(2000 * DELAY_MULTIPLIER - 50, 2250 * DELAY_MULTIPLIER));
        } else if (Class1.maxConcurrentMethods == 2) {
            assertThat(timeSpent, between(1500 * DELAY_MULTIPLIER - 50, 1750 * DELAY_MULTIPLIER));
        } else {
            fail();
        }
    }

    @Test
    public void suiteAndClassInOnePool() {
        ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER);
        parallelComputerBuilder.useOnePool(5);
        parallelComputerBuilder.parallelSuites(5);
        parallelComputerBuilder.parallelClasses(5);
        parallelComputerBuilder.parallelMethods(3);
        assertFalse(parallelComputerBuilder.isOptimized());

        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) parallelComputerBuilder.buildComputer();
        final JUnitCore core = new JUnitCore();
        final long t1 = systemMillis();
        final Result result = core.run(computer, TestSuite.class, Class1.class);
        final long t2 = systemMillis();
        final long timeSpent = t2 - t1;

        assertThat(computer.getSuites().size(), is(1));
        assertThat(computer.getClasses().size(), is(1));
        assertThat(computer.getNestedClasses().size(), is(2));
        assertThat(computer.getNestedSuites().size(), is(0));
        assertFalse(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(5));
        assertTrue(result.wasSuccessful());
        assertThat(Class1.maxConcurrentMethods, is(2));
        assertThat(
                timeSpent,
                anyOf(
                        between(1500 * DELAY_MULTIPLIER - 50, 1750 * DELAY_MULTIPLIER),
                        between(2000 * DELAY_MULTIPLIER - 50, 2250 * DELAY_MULTIPLIER),
                        between(2500 * DELAY_MULTIPLIER - 50, 2750 * DELAY_MULTIPLIER)));
    }

    @Test
    public void onePoolWithUnlimitedParallelMethods() {
        // see ParallelComputerBuilder Javadoc
        ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER);
        parallelComputerBuilder.useOnePool(8);
        parallelComputerBuilder.parallelSuites(2);
        parallelComputerBuilder.parallelClasses(4);
        parallelComputerBuilder.parallelMethods();
        assertFalse(parallelComputerBuilder.isOptimized());

        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) parallelComputerBuilder.buildComputer();
        final JUnitCore core = new JUnitCore();
        final long t1 = systemMillis();
        final Result result = core.run(computer, TestSuite.class, Class1.class);
        final long t2 = systemMillis();
        final long timeSpent = t2 - t1;

        assertThat(computer.getSuites().size(), is(1));
        assertThat(computer.getClasses().size(), is(1));
        assertThat(computer.getNestedClasses().size(), is(2));
        assertThat(computer.getNestedSuites().size(), is(0));
        assertFalse(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(8));
        assertTrue(result.wasSuccessful());
        assertThat(Class1.maxConcurrentMethods, is(4));
        assertThat(timeSpent, between(1000 * DELAY_MULTIPLIER - 50, 1250 * DELAY_MULTIPLIER));
    }

    @Test
    public void underflowParallelism() {
        ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER);
        parallelComputerBuilder.useOnePool(3);

        // One thread because one suite: TestSuite.
        parallelComputerBuilder.parallelSuites(5);

        // One thread because of the limitation which is bottleneck.
        parallelComputerBuilder.parallelClasses(1);

        // One thread remains from '#useOnePool(3)'.
        parallelComputerBuilder.parallelMethods(3);

        assertFalse(parallelComputerBuilder.isOptimized());

        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) parallelComputerBuilder.buildComputer();
        final JUnitCore core = new JUnitCore();
        final long t1 = systemMillis();
        final Result result = core.run(computer, TestSuite.class);
        final long t2 = systemMillis();
        final long timeSpent = t2 - t1;

        assertThat(computer.getSuites().size(), is(1));
        assertThat(computer.getClasses().size(), is(0));
        assertThat(computer.getNestedClasses().size(), is(2));
        assertThat(computer.getNestedSuites().size(), is(0));
        assertFalse(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(3));
        assertTrue(result.wasSuccessful());
        assertThat(Class1.maxConcurrentMethods, is(1));
        assertThat(timeSpent, between(2000 * DELAY_MULTIPLIER - 50, 2250 * DELAY_MULTIPLIER));
    }

    @Test
    public void separatePoolsWithSuite() {
        ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER);
        parallelComputerBuilder.parallelSuites(5);
        parallelComputerBuilder.parallelClasses(5);
        parallelComputerBuilder.parallelMethods(3);
        assertFalse(parallelComputerBuilder.isOptimized());

        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) parallelComputerBuilder.buildComputer();
        final JUnitCore core = new JUnitCore();
        final long t1 = systemMillis();
        final Result result = core.run(computer, TestSuite.class);
        final long t2 = systemMillis();
        final long timeSpent = t2 - t1;

        assertThat(computer.getSuites().size(), is(1));
        assertThat(computer.getClasses().size(), is(0));
        assertThat(computer.getNestedClasses().size(), is(2));
        assertThat(computer.getNestedSuites().size(), is(0));
        assertTrue(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(ParallelComputerBuilder.TOTAL_POOL_SIZE_UNDEFINED));
        assertTrue(result.wasSuccessful());
        assertThat(Class1.maxConcurrentMethods, is(3));
        assertThat(timeSpent, between(1000 * DELAY_MULTIPLIER - 50, 1250 * DELAY_MULTIPLIER));
    }

    @Test
    public void separatePoolsWithSuiteAndClass() {
        ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER);
        parallelComputerBuilder.parallelSuites(5);
        parallelComputerBuilder.parallelClasses(5);
        parallelComputerBuilder.parallelMethods(3);
        assertFalse(parallelComputerBuilder.isOptimized());

        // 6 methods altogether.
        // 2 groups with 3 threads.
        // Each group takes 0.5s.
        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) parallelComputerBuilder.buildComputer();
        final JUnitCore core = new JUnitCore();
        final long t1 = systemMillis();
        final Result result = core.run(computer, TestSuite.class, Class1.class);
        final long t2 = systemMillis();
        final long timeSpent = t2 - t1;

        assertThat(computer.getSuites().size(), is(1));
        assertThat(computer.getClasses().size(), is(1));
        assertThat(computer.getNestedClasses().size(), is(2));
        assertThat(computer.getNestedSuites().size(), is(0));
        assertTrue(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(ParallelComputerBuilder.TOTAL_POOL_SIZE_UNDEFINED));
        assertTrue(result.wasSuccessful());
        assertThat(Class1.maxConcurrentMethods, is(3));
        assertThat(timeSpent, between(1000 * DELAY_MULTIPLIER - 50, 1250 * DELAY_MULTIPLIER));
    }

    @Test
    public void separatePoolsWithSuiteAndSequentialClasses() {
        ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER);
        parallelComputerBuilder.parallelSuites(5);
        parallelComputerBuilder.parallelClasses(1);
        parallelComputerBuilder.parallelMethods(3);
        assertFalse(parallelComputerBuilder.isOptimized());

        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) parallelComputerBuilder.buildComputer();
        final JUnitCore core = new JUnitCore();
        final long t1 = systemMillis();
        final Result result = core.run(computer, TestSuite.class, Class1.class);
        final long t2 = systemMillis();
        final long timeSpent = t2 - t1;

        assertThat(computer.getSuites().size(), is(1));
        assertThat(computer.getClasses().size(), is(1));
        assertThat(computer.getNestedClasses().size(), is(2));
        assertThat(computer.getNestedSuites().size(), is(0));
        assertTrue(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(ParallelComputerBuilder.TOTAL_POOL_SIZE_UNDEFINED));
        assertTrue(result.wasSuccessful());
        assertThat(Class1.maxConcurrentMethods, is(2));
        assertThat(timeSpent, between(1500 * DELAY_MULTIPLIER - 50, 1750 * DELAY_MULTIPLIER));
    }

    @Test(timeout = 2000 * DELAY_MULTIPLIER)
    public void shutdown() {
        final long t1 = systemMillis();
        final Result result = new ShutdownTest().run(false);
        final long t2 = systemMillis();
        final long timeSpent = t2 - t1;
        assertTrue(result.wasSuccessful());
        assertTrue(beforeShutdown);
        assertThat(timeSpent, between(500 * DELAY_MULTIPLIER - 50, 1250 * DELAY_MULTIPLIER));
    }

    @Test(timeout = 2000 * DELAY_MULTIPLIER)
    public void shutdownWithInterrupt() {
        final long t1 = systemMillis();
        new ShutdownTest().run(true);
        final long t2 = systemMillis();
        final long timeSpent = t2 - t1;
        assertTrue(beforeShutdown);
        assertThat(timeSpent, between(500 * DELAY_MULTIPLIER - 50, 1250 * DELAY_MULTIPLIER));
    }

    @Test
    public void nothingParallel() {
        JUnitCore core = new JUnitCore();
        ParallelComputerBuilder builder = new ParallelComputerBuilder(LOGGER);
        assertFalse(builder.isOptimized());

        Result result = core.run(builder.buildComputer(), NothingDoingTest1.class, NothingDoingTest2.class);
        assertTrue(result.wasSuccessful());

        result = core.run(builder.buildComputer(), NothingDoingTest1.class, NothingDoingSuite.class);
        assertTrue(result.wasSuccessful());

        builder.useOnePool(1);
        assertFalse(builder.isOptimized());
        result = core.run(builder.buildComputer(), NothingDoingTest1.class, NothingDoingTest2.class);
        assertTrue(result.wasSuccessful());

        builder.useOnePool(1);
        assertFalse(builder.isOptimized());
        result = core.run(builder.buildComputer(), NothingDoingTest1.class, NothingDoingSuite.class);
        assertTrue(result.wasSuccessful());

        builder.useOnePool(2);
        assertFalse(builder.isOptimized());
        result = core.run(builder.buildComputer(), NothingDoingTest1.class, NothingDoingSuite.class);
        assertTrue(result.wasSuccessful());

        Class<?>[] classes = {NothingDoingTest1.class, NothingDoingSuite.class};

        builder.useOnePool(2).parallelSuites(1).parallelClasses(1);
        assertFalse(builder.isOptimized());
        result = core.run(builder.buildComputer(), classes);
        assertTrue(result.wasSuccessful());

        builder.useOnePool(2).parallelSuites(1).parallelClasses();
        assertFalse(builder.isOptimized());
        result = core.run(builder.buildComputer(), classes);
        assertTrue(result.wasSuccessful());

        classes = new Class<?>[] {
            NothingDoingSuite.class,
            NothingDoingSuite.class,
            NothingDoingTest1.class,
            NothingDoingTest2.class,
            NothingDoingTest3.class
        };

        builder.useOnePool(2).parallelSuites(1).parallelClasses(1);
        assertFalse(builder.isOptimized());
        result = core.run(builder.buildComputer(), classes);
        assertTrue(result.wasSuccessful());

        builder.useOnePool(2).parallelSuites(1).parallelClasses();
        assertFalse(builder.isOptimized());
        result = core.run(builder.buildComputer(), classes);
        assertTrue(result.wasSuccessful());
    }

    @Test
    public void keepBeforeAfterOneClass() {
        ParallelComputerBuilder builder = new ParallelComputerBuilder(LOGGER);
        builder.parallelMethods();
        assertFalse(builder.isOptimized());
        testKeepBeforeAfter(builder, NothingDoingTest1.class);
    }

    @Test
    public void keepBeforeAfterTwoClasses() {
        ParallelComputerBuilder builder = new ParallelComputerBuilder(LOGGER);
        builder.useOnePool(5).parallelClasses(1).parallelMethods(2);
        assertFalse(builder.isOptimized());
        testKeepBeforeAfter(builder, NothingDoingTest1.class, NothingDoingTest2.class);
    }

    @Test
    public void keepBeforeAfterTwoParallelClasses() {
        ParallelComputerBuilder builder = new ParallelComputerBuilder(LOGGER);
        builder.useOnePool(8).parallelClasses(2).parallelMethods(2);
        assertFalse(builder.isOptimized());
        JUnitCore core = new JUnitCore();
        NothingDoingTest1.METHODS.clear();
        Class<?>[] classes = {NothingDoingTest1.class, NothingDoingTest2.class, NothingDoingTest3.class};
        Result result = core.run(builder.buildComputer(), classes);
        assertTrue(result.wasSuccessful());
        ArrayList<String> methods = new ArrayList<>(NothingDoingTest1.METHODS);
        assertThat(methods.size(), is(12));
        assertThat(methods.subList(9, 12), is(not(Arrays.asList("deinit", "deinit", "deinit"))));
    }

    @Test
    public void notThreadSafeTest() {
        ParallelComputerBuilder builder = new ParallelComputerBuilder(LOGGER)
                .useOnePool(6)
                .optimize(true)
                .parallelClasses(3)
                .parallelMethods(3);
        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer();
        Result result = new JUnitCore().run(computer, NotThreadSafeTest1.class, NotThreadSafeTest2.class);
        assertTrue(result.wasSuccessful());
        assertThat(result.getRunCount(), is(2));
        assertNotNull(NotThreadSafeTest1.t);
        assertNotNull(NotThreadSafeTest2.t);
        assertSame(NotThreadSafeTest1.t, NotThreadSafeTest2.t);
        assertThat(computer.getNotParallelRunners().size(), is(2));
        assertTrue(computer.getSuites().isEmpty());
        assertTrue(computer.getClasses().isEmpty());
        assertTrue(computer.getNestedClasses().isEmpty());
        assertTrue(computer.getNestedSuites().isEmpty());
        assertFalse(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(6));
    }

    @Test
    public void mixedThreadSafety() {
        ParallelComputerBuilder builder = new ParallelComputerBuilder(LOGGER)
                .useOnePool(6)
                .optimize(true)
                .parallelClasses(3)
                .parallelMethods(3);
        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer();
        Result result = new JUnitCore().run(computer, NotThreadSafeTest1.class, NormalTest1.class);
        assertTrue(result.wasSuccessful());
        assertThat(result.getRunCount(), is(2));
        assertNotNull(NotThreadSafeTest1.t);
        assertNotNull(NormalTest1.t);
        assertThat(NormalTest1.t.getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        assertNotSame(NotThreadSafeTest1.t, NormalTest1.t);
        assertThat(computer.getNotParallelRunners().size(), is(1));
        assertTrue(computer.getSuites().isEmpty());
        assertThat(computer.getClasses().size(), is(1));
        assertTrue(computer.getNestedClasses().isEmpty());
        assertTrue(computer.getNestedSuites().isEmpty());
        assertFalse(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(6));
    }

    @Test
    public void notThreadSafeTestsInSuite() {
        ParallelComputerBuilder builder =
                new ParallelComputerBuilder(LOGGER).useOnePool(5).parallelMethods(3);
        assertFalse(builder.isOptimized());
        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer();
        Result result = new JUnitCore().run(computer, NotThreadSafeTestSuite.class);
        assertTrue(result.wasSuccessful());
        assertNotNull(NormalTest1.t);
        assertNotNull(NormalTest2.t);
        assertSame(NormalTest1.t, NormalTest2.t);
        assertThat(NormalTest1.t.getName(), is("maven-surefire-plugin@NotThreadSafe"));
        assertThat(NormalTest2.t.getName(), is("maven-surefire-plugin@NotThreadSafe"));
        assertThat(computer.getNotParallelRunners().size(), is(1));
        assertTrue(computer.getSuites().isEmpty());
        assertTrue(computer.getClasses().isEmpty());
        assertTrue(computer.getNestedClasses().isEmpty());
        assertTrue(computer.getNestedSuites().isEmpty());
        assertFalse(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(5));
    }

    @Test
    public void mixedThreadSafetyInSuite() {
        ParallelComputerBuilder builder = new ParallelComputerBuilder(LOGGER)
                .useOnePool(10)
                .optimize(true)
                .parallelSuites(2)
                .parallelClasses(3)
                .parallelMethods(3);
        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer();
        Result result = new JUnitCore().run(computer, MixedSuite.class);
        assertTrue(result.wasSuccessful());
        assertThat(result.getRunCount(), is(2));
        assertNotNull(NotThreadSafeTest1.t);
        assertNotNull(NormalTest1.t);
        assertThat(NormalTest1.t.getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        assertNotSame(NotThreadSafeTest1.t, NormalTest1.t);
        assertTrue(computer.getNotParallelRunners().isEmpty());
        assertThat(computer.getSuites().size(), is(1));
        assertTrue(computer.getClasses().isEmpty());
        assertThat(computer.getNestedClasses().size(), is(1));
        assertTrue(computer.getNestedSuites().isEmpty());
        assertFalse(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(10));
    }

    @Test
    public void inheritanceWithNotThreadSafe() {
        ParallelComputerBuilder builder = new ParallelComputerBuilder(LOGGER)
                .useOnePool(10)
                .optimize(true)
                .parallelSuites(2)
                .parallelClasses(3)
                .parallelMethods(3);
        ParallelComputerBuilder.PC computer = (ParallelComputerBuilder.PC) builder.buildComputer();
        Result result = new JUnitCore().run(computer, OverMixedSuite.class);
        assertTrue(result.wasSuccessful());
        assertThat(result.getRunCount(), is(2));
        assertNotNull(NotThreadSafeTest3.t);
        assertNotNull(NormalTest1.t);
        assertThat(NormalTest1.t.getName(), is("maven-surefire-plugin@NotThreadSafe"));
        assertSame(NotThreadSafeTest3.t, NormalTest1.t);
        assertThat(computer.getNotParallelRunners().size(), is(1));
        assertTrue(computer.getSuites().isEmpty());
        assertTrue(computer.getClasses().isEmpty());
        assertTrue(computer.getNestedClasses().isEmpty());
        assertTrue(computer.getNestedSuites().isEmpty());
        assertFalse(computer.isSplitPool());
        assertThat(computer.getPoolCapacity(), is(10));
    }

    @Test
    public void beforeAfterThreadChanges() throws InterruptedException {
        // try to GC dead Thread objects from previous tests
        for (int i = 0; i < 5; i++) {
            System.gc();
            TimeUnit.MILLISECONDS.sleep(500L);
        }
        Collection<Thread> expectedThreads = jvmThreads();
        ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER);
        parallelComputerBuilder.parallelMethods(3);
        ParallelComputer computer = parallelComputerBuilder.buildComputer();
        Result result = new JUnitCore().run(computer, TestWithBeforeAfter.class);
        assertTrue(result.wasSuccessful());
        // try to GC dead Thread objects
        for (int i = 0; i < 5 && expectedThreads.size() != jvmThreads().size(); i++) {
            System.gc();
            TimeUnit.MILLISECONDS.sleep(500L);
        }
        assertThat(jvmThreads(), is(expectedThreads));
    }

    private static Collection<Thread> jvmThreads() {
        Thread[] t = new Thread[1000];
        Thread.enumerate(t);
        ArrayList<Thread> appThreads = new ArrayList<>(t.length);
        Collections.addAll(appThreads, t);
        appThreads.removeAll(Collections.singleton((Thread) null));
        Collections.sort(appThreads, new Comparator<Thread>() {
            @Override
            public int compare(Thread t1, Thread t2) {
                return (int) Math.signum(t1.getId() - t2.getId());
            }
        });
        return appThreads;
    }

    private static class ShutdownTest {
        Result run(final boolean useInterrupt) {
            ParallelComputerBuilder parallelComputerBuilder = new ParallelComputerBuilder(LOGGER)
                    .useOnePool(8)
                    .parallelSuites(2)
                    .parallelClasses(3)
                    .parallelMethods(3);

            assertFalse(parallelComputerBuilder.isOptimized());

            final ParallelComputerBuilder.PC computer =
                    (ParallelComputerBuilder.PC) parallelComputerBuilder.buildComputer();
            shutdownTask = new Runnable() {
                @Override
                public void run() {
                    Collection<Description> startedTests =
                            computer.describeStopped(useInterrupt).getTriggeredTests();
                    assertThat(startedTests.size(), is(not(0)));
                }
            };
            return new JUnitCore().run(computer, TestSuite.class, Class2.class, Class3.class);
        }
    }

    /**
     *
     */
    public static class Class1 {
        static volatile int concurrentMethods = 0;

        static volatile int maxConcurrentMethods = 0;

        @Test
        public void test1() throws InterruptedException {
            synchronized (CLASS1BLOCK) {
                ++concurrentMethods;
                CLASS1BLOCK.wait(DELAY_MULTIPLIER * 500L);
                maxConcurrentMethods = Math.max(maxConcurrentMethods, concurrentMethods--);
            }
        }

        @Test
        public void test2() throws InterruptedException {
            test1();
            Runnable shutdownTask = ParallelComputerBuilderTest.shutdownTask;
            if (shutdownTask != null) {
                beforeShutdown = true;
                shutdownTask.run();
            }
        }
    }

    /**
     *
     */
    public static class Class2 extends Class1 {}

    /**
     *
     */
    public static class Class3 extends Class1 {}

    /**
     *
     */
    public static class NothingDoingTest1 {
        private static final Collection<String> METHODS = new ConcurrentLinkedQueue<>();

        @BeforeClass
        public static void init() {
            METHODS.add("init");
        }

        @AfterClass
        public static void deinit() {
            METHODS.add("deinit");
        }

        @Test
        public void a() throws InterruptedException {
            Thread.sleep(5);
            METHODS.add(getClass().getName() + "#a()");
        }

        @Test
        public void b() throws InterruptedException {
            Thread.sleep(5);
            METHODS.add(getClass().getName() + "#b()");
        }
    }

    /**
     *
     */
    public static class NothingDoingTest2 extends NothingDoingTest1 {}

    /**
     *
     */
    public static class NothingDoingTest3 extends NothingDoingTest1 {}

    /**
     *
     */
    @RunWith(Suite.class)
    @Suite.SuiteClasses({NothingDoingTest1.class, NothingDoingTest2.class})
    public static class NothingDoingSuite {}

    /**
     *
     */
    @RunWith(Suite.class)
    @Suite.SuiteClasses({Class2.class, Class1.class})
    public static class TestSuite {}

    /**
     *
     */
    public static class Test2 {
        @Test
        public void test() {}
    }

    /**
     *
     */
    @RunWith(ReportOneTestAtRuntimeRunner.class)
    public static class TestWithoutPrecalculatedChildren {}

    /**
     *
     */
    public static class ReportOneTestAtRuntimeRunner extends ParentRunner {
        private final Class<?> testClass;
        private final Description suiteDescription;
        private final Description myTestMethodDescr;

        @SuppressWarnings("unchecked")
        public ReportOneTestAtRuntimeRunner(Class<?> testClass) throws InitializationError {
            super(Object.class);
            this.testClass = testClass;
            suiteDescription = Description.createSuiteDescription(testClass);
            myTestMethodDescr = Description.createTestDescription(testClass, "my_test");
            //            suiteDescription.addChild(myTestMethodDescr); // let it be not known at start time
        }

        protected List getChildren() {
            throw new UnsupportedOperationException("workflow from ParentRunner not supported");
        }

        protected Description describeChild(Object child) {
            throw new UnsupportedOperationException("workflow from ParentRunner not supported");
        }

        protected void runChild(Object child, RunNotifier notifier) {
            throw new UnsupportedOperationException("workflow from ParentRunner not supported");
        }

        public Description getDescription() {
            return suiteDescription;
        }

        public void run(RunNotifier notifier) {
            notifier.fireTestStarted(myTestMethodDescr);
            notifier.fireTestFinished(Description.createTestDescription(testClass, "my_test"));
        }
    }

    /**
     *
     */
    @NotThreadSafe
    public static class NotThreadSafeTest1 {
        static volatile Thread t;

        @BeforeClass
        public static void beforeSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }

        @AfterClass
        public static void afterSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }

        @Test
        public void test() {
            t = Thread.currentThread();
            assertThat(t.getName(), is("maven-surefire-plugin@NotThreadSafe"));
        }
    }

    /**
     *
     */
    @NotThreadSafe
    public static class NotThreadSafeTest2 {
        static volatile Thread t;

        @BeforeClass
        public static void beforeSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }

        @AfterClass
        public static void afterSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }

        @Test
        public void test() {
            t = Thread.currentThread();
            assertThat(t.getName(), is("maven-surefire-plugin@NotThreadSafe"));
        }
    }

    /**
     *
     */
    @NotThreadSafe
    public static class NotThreadSafeTest3 {
        static volatile Thread t;

        @Test
        public void test() {
            t = Thread.currentThread();
            assertThat(t.getName(), is("maven-surefire-plugin@NotThreadSafe"));
        }
    }

    /**
     *
     */
    @RunWith(Suite.class)
    @Suite.SuiteClasses({NormalTest1.class, NormalTest2.class})
    @NotThreadSafe
    public static class NotThreadSafeTestSuite {
        @BeforeClass
        public static void beforeSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }

        @AfterClass
        public static void afterSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }
    }

    /**
     *
     */
    public static class NormalTest1 {
        static volatile Thread t;

        @Test
        public void test() {
            t = Thread.currentThread();
        }
    }

    /**
     *
     */
    public static class NormalTest2 {
        static volatile Thread t;

        @Test
        public void test() {
            t = Thread.currentThread();
        }
    }

    /**
     *
     */
    @RunWith(Suite.class)
    @Suite.SuiteClasses({NotThreadSafeTest1.class, NormalTest1.class})
    public static class MixedSuite {
        @BeforeClass
        public static void beforeSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }

        @AfterClass
        public static void afterSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }
    }

    /**
     *
     */
    @RunWith(Suite.class)
    @Suite.SuiteClasses({NotThreadSafeTest3.class, NormalTest1.class})
    @NotThreadSafe
    public static class OverMixedSuite {
        @BeforeClass
        public static void beforeSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }

        @AfterClass
        public static void afterSuite() {
            assertThat(Thread.currentThread().getName(), is(not("maven-surefire-plugin@NotThreadSafe")));
        }
    }

    /**
     *
     */
    public static class TestWithBeforeAfter {
        @BeforeClass
        public static void beforeClass() throws InterruptedException {
            System.out.println(new Date() + " BEG: beforeClass");
            sleepSeconds(1);
            System.out.println(new Date() + " END: beforeClass");
        }

        @Before
        public void before() throws InterruptedException {
            System.out.println(new Date() + " BEG: before");
            sleepSeconds(1);
            System.out.println(new Date() + " END: before");
        }

        @Test
        public void test() throws InterruptedException {
            System.out.println(new Date() + " BEG: test");
            sleepSeconds(1);
            System.out.println(new Date() + " END: test");
        }

        @After
        public void after() throws InterruptedException {
            System.out.println(new Date() + " BEG: after");
            sleepSeconds(1);
            System.out.println(new Date() + " END: after");
        }

        @AfterClass
        public static void afterClass() throws InterruptedException {
            System.out.println(new Date() + " BEG: afterClass");
            sleepSeconds(1);
            System.out.println(new Date() + " END: afterClass");
        }
    }

    private static long systemMillis() {
        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
    }

    private static void sleepSeconds(int seconds) throws InterruptedException {
        TimeUnit.SECONDS.sleep(seconds);
    }
}
