/*
* Copyright 2015-2017 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.junit.platform.launcher;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableSet;
import static org.junit.platform.commons.meta.API.Usage.Experimental;
import static org.junit.platform.commons.meta.API.Usage.Internal;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.PreconditionViolationException;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestDescriptor.Visitor;
/**
* {@code TestPlan} describes the tree of tests and containers as discovered
* by a {@link Launcher}.
*
* <p>Tests and containers are represented by {@link TestIdentifier} instances.
* The complete set of identifiers comprises a tree-like structure. However,
* each identifier only stores the unique ID of its parent. This class provides
* a number of helpful methods to retrieve the
* {@linkplain #getParent(TestIdentifier) parent},
* {@linkplain #getChildren(TestIdentifier) children}, and
* {@linkplain #getDescendants(TestIdentifier) descendants} of an identifier.
*
* <p>While the contained instances of {@link TestIdentifier} are immutable,
* instances of this class contain mutable state. For example, when a dynamic
* test is registered at runtime, it is added to the original test plan and
* reported to {@link TestExecutionListener} implementations.
*
* @since 1.0
* @see Launcher
* @see TestExecutionListener
*/
@API(Experimental)
public final class TestPlan {
private final Set<TestIdentifier> roots = Collections.synchronizedSet(new LinkedHashSet<>(4));
private final Map<String, Set<TestIdentifier>> children = new ConcurrentHashMap<>(32);
private final Map<String, TestIdentifier> allIdentifiers = new ConcurrentHashMap<>(32);
/**
* Construct a new {@code TestPlan} from the supplied collection of
* {@link TestDescriptor TestDescriptors}.
*
* <p>Each supplied {@code TestDescriptor} is expected to be a descriptor
* for a {@link org.junit.platform.engine.TestEngine TestEngine}.
*
* @param engineDescriptors the engine test descriptors from which the test
* plan should be created; never {@code null}
* @return a new test plan
*/
@API(Internal)
public static TestPlan from(Collection<TestDescriptor> engineDescriptors) {
Preconditions.notNull(engineDescriptors, "Cannot create TestPlan from a null collection of TestDescriptors");
TestPlan testPlan = new TestPlan();
Visitor visitor = descriptor -> testPlan.add(TestIdentifier.from(descriptor));
engineDescriptors.forEach(engineDescriptor -> engineDescriptor.accept(visitor));
return testPlan;
}
private TestPlan() {
/* no-op */
}
/**
* Add the supplied {@link TestIdentifier} to this test plan.
*
* @param testIdentifier the identifier to add; never {@code null}
*/
public void add(TestIdentifier testIdentifier) {
Preconditions.notNull(testIdentifier, "testIdentifier must not be null");
allIdentifiers.put(testIdentifier.getUniqueId(), testIdentifier);
if (testIdentifier.getParentId().isPresent()) {
String parentId = testIdentifier.getParentId().get();
Set<TestIdentifier> directChildren = children.computeIfAbsent(parentId,
key -> Collections.synchronizedSet(new LinkedHashSet<>(16)));
directChildren.add(testIdentifier);
}
else {
roots.add(testIdentifier);
}
}
/**
* Get the root {@link TestIdentifier TestIdentifiers} for this test plan.
*
* @return an unmodifiable set of the root identifiers
*/
public Set<TestIdentifier> getRoots() {
return unmodifiableSet(roots);
}
/**
* Get the parent of the supplied {@link TestIdentifier}.
*
* @param child the identifier to look up the parent for; never {@code null}
* @return an {@code Optional} containing the parent, if present
*/
public Optional<TestIdentifier> getParent(TestIdentifier child) {
Preconditions.notNull(child, "child must not be null");
Optional<String> optionalParentId = child.getParentId();
if (optionalParentId.isPresent()) {
return Optional.of(getTestIdentifier(optionalParentId.get()));
}
return Optional.empty();
}
/**
* Get the children of the supplied {@link TestIdentifier}.
*
* @param parent the identifier to look up the children for; never {@code null}
* @return an unmodifiable set of the parent's children, potentially empty
* @see #getChildren(String)
*/
public Set<TestIdentifier> getChildren(TestIdentifier parent) {
Preconditions.notNull(parent, "parent must not be null");
return getChildren(parent.getUniqueId());
}
/**
* Get the children of the supplied unique ID.
*
* @param parentId the unique ID to look up the children for; never
* {@code null} or blank
* @return an unmodifiable set of the parent's children, potentially empty
* @see #getChildren(TestIdentifier)
*/
public Set<TestIdentifier> getChildren(String parentId) {
Preconditions.notBlank(parentId, "parent ID must not be null or blank");
return children.containsKey(parentId) ? unmodifiableSet(children.get(parentId)) : emptySet();
}
/**
* Get the {@link TestIdentifier} with the supplied unique ID.
*
* @param uniqueId the unique ID to look up the identifier for; never
* {@code null} or blank
* @return the identifier with the supplied unique ID; never {@code null}
* @throws PreconditionViolationException if no {@code TestIdentifier}
* with the supplied unique ID is present in this test plan
*/
public TestIdentifier getTestIdentifier(String uniqueId) throws PreconditionViolationException {
Preconditions.notBlank(uniqueId, "unique ID must not be null or blank");
Preconditions.condition(allIdentifiers.containsKey(uniqueId),
() -> "No TestIdentifier with unique ID [" + uniqueId + "] has been added to this TestPlan.");
return allIdentifiers.get(uniqueId);
}
/**
* Count all {@link TestIdentifier TestIdentifiers} that satisfy the
* given {@linkplain Predicate predicate}.
*
* @param predicate a predicate which returns {@code true} for identifiers
* to be counted; never {@code null}
* @return the number of identifiers that satisfy the supplied predicate
*/
public long countTestIdentifiers(Predicate<? super TestIdentifier> predicate) {
Preconditions.notNull(predicate, "Predicate must not be null");
return allIdentifiers.values().stream().filter(predicate).count();
}
/**
* Get all descendants of the supplied {@link TestIdentifier} (i.e.,
* all of its children and their children, recursively).
*
* @param parent the identifier to look up the descendants for; never {@code null}
* @return an unmodifiable set of the parent's descendants, potentially empty
*/
public Set<TestIdentifier> getDescendants(TestIdentifier parent) {
Preconditions.notNull(parent, "parent must not be null");
Set<TestIdentifier> result = new LinkedHashSet<>(16);
Set<TestIdentifier> children = getChildren(parent);
result.addAll(children);
for (TestIdentifier child : children) {
result.addAll(getDescendants(child));
}
return unmodifiableSet(result);
}
}