All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.junitpioneer.jupiter.DisableIfTestFailsExtension Maven / Gradle / Ivy

There is a newer version: 2.3.0
Show newest version
/*
 * Copyright 2016-2022 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 v2.0 which
 * accompanies this distribution and is available at
 *
 * http://www.eclipse.org/legal/epl-v20.html
 */

package org.junitpioneer.jupiter;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import org.junit.platform.commons.support.AnnotationSupport;
import org.opentest4j.TestAbortedException;

class DisableIfTestFailsExtension implements TestExecutionExceptionHandler, ExecutionCondition {

	/*
	 * Basic approach:
	 *  - in `handleTestExecutionException`: if need to deactivate other tests, add that info to the store
	 *  - in `evaluateExecutionCondition`: check store for that information
	 *
	 * Because the test method that failed and the ones that need to be disabled are different methods,
	 * the information to disable can't be in a store that belongs to any specific test method. Instead
	 * add it to the store that belongs to the container where the extension is applied.
	 *
	 * Setting the information needs to be thread safe, so only positive results (i.e. tests must be disabled)
	 * will be set. The easiest way to do that is to simply use absence/presence of a key in the store
	 * as indicator, which means the specific value doesn't matter.
	 */

	private static final Namespace NAMESPACE = Namespace.create(DisableIfTestFailsExtension.class);
	private static final String DISABLED_KEY = "DISABLED_KEY";
	private static final String DISABLED_VALUE = "";

	@Override
	public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
		boolean disabled = context.getStore(NAMESPACE).get(DISABLED_KEY) != null;
		if (disabled)
			return ConditionEvaluationResult.disabled("Another failed with one of the specified exceptions.");
		else
			return ConditionEvaluationResult.enabled("No test failed with one of the specified exceptions (yet).");
	}

	@Override
	public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
		// we assume `context` belongs to a test method, so in order to associate annotations with the
		// correct extension context (i.e. the one belonging to the test class), get the parent
		// (which should hence always exist!)
		ExtensionContext testClassContext = context.getParent().orElseThrow(IllegalStateException::new);
		findConfigurations(testClassContext)
				.filter(configuration -> configuration.shouldDisable(throwable))
				.forEach(
					configuration -> configuration.context().getStore(NAMESPACE).put(DISABLED_KEY, DISABLED_VALUE));
		throw throwable;
	}

	private static Stream findConfigurations(ExtensionContext context) {
		Optional> type = context.getTestClass();
		// type may not be present because of recursion to the parent context
		if (!type.isPresent())
			return Stream.empty();

		List annotations = findAnnotationOn(type.get()).collect(toList());
		Stream onClassConfig = createConfigurationFor(context, annotations);
		Stream onParentClassConfigs = context
				.getParent()
				.map(DisableIfTestFailsExtension::findConfigurations)
				.orElse(Stream.empty());

		List configurations = Stream.concat(onClassConfig, onParentClassConfigs).collect(toList());
		return configurations.stream();
	}

	private static Stream createConfigurationFor(ExtensionContext context,
			List annotations) {
		// annotations can be empty if a nested class isn't annotated itself (but an outer class is)
		if (annotations.isEmpty())
			return Stream.empty();

		Set> onClassExceptions = annotations
				.stream()
				.map(DisableIfTestFails::with)
				// If the exceptions array is empty, we later need to disable on all exceptions.
				// The easiest way to achieve that is by replacing the empty array with a Throwable.class
				// because all exceptions extend it.
				.flatMap(exceptions -> exceptions.length == 0 ? Stream.of(Throwable.class) : Arrays.stream(exceptions))
				.collect(toSet());
		boolean disableOnAssertions = annotations.stream().anyMatch(DisableIfTestFails::onAssertion);
		Configuration onClassConfig = new Configuration(context, onClassExceptions, disableOnAssertions);

		return Stream.of(onClassConfig);
	}

	private static Stream findAnnotationOn(Class element) {
		if (element == null || element == Object.class)
			return Stream.empty();

		Stream onElement = AnnotationSupport
				.findAnnotation(element, DisableIfTestFails.class)
				// turn Optional into Stream
				.map(Stream::of)
				.orElse(Stream.empty());
		Stream onInterfaces = Arrays
				.stream(element.getInterfaces())
				.flatMap(DisableIfTestFailsExtension::findAnnotationOn);
		Stream onSuperclass = findAnnotationOn(element.getSuperclass());
		return Stream.of(onElement, onInterfaces, onSuperclass).flatMap(s -> s);
	}

	private static class Configuration {

		private final ExtensionContext context;
		private final Set> disableOnExceptions;
		private final boolean disableOnAssertions;

		public Configuration(ExtensionContext context, Set> disableOnExceptions,
				boolean disableOnAssertions) {
			this.context = context;
			this.disableOnExceptions = disableOnExceptions;
			if (disableOnExceptions.isEmpty())
				throw new IllegalArgumentException("List of exceptions to disable on must not be empty.");
			this.disableOnAssertions = disableOnAssertions;
		}

		public boolean shouldDisable(Throwable exception) {
			// don't disable on failed assumptions
			if (exception instanceof TestAbortedException)
				return false;
			if (exception instanceof AssertionError)
				return disableOnAssertions;
			return disableOnExceptions.stream().anyMatch(type -> type.isInstance(exception));
		}

		public ExtensionContext context() {
			return context;
		}

	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy