Skip to content

Commit

Permalink
Add JUnit5 support for test rules. (#2426)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com>
  • Loading branch information
bstopp and timja committed Dec 4, 2023
1 parent 05c5d96 commit 0ccc3b6
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 0 deletions.
8 changes: 8 additions & 0 deletions test-harness/pom.xml
Expand Up @@ -67,6 +67,14 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>jackson2-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
</dependency>
<!-- Needed for AgentProtocolsTest -->
<dependency>
<groupId>org.jenkins-ci.modules</groupId>
Expand Down
@@ -0,0 +1,35 @@
package io.jenkins.plugins.casc.misc.junit.jupiter;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.runner.Description;
import org.jvnet.hudson.test.JenkinsRecipe;

/**
* Provides JUnit 5 compatibility for {@link JenkinsConfiguredWithCodeRule}.
*/
class JUnit5JenkinsConfiguredWithCodeRule extends JenkinsConfiguredWithCodeRule {

JUnit5JenkinsConfiguredWithCodeRule(@NonNull ExtensionContext extensionContext, Annotation... annotations) {
this.testDescription = Description.createTestDescription(
extensionContext.getTestClass().map(Class::getName).orElse(null),
extensionContext.getTestMethod().map(Method::getName).orElse(null),
annotations);
}

@Override
public void recipe() throws Exception {
final JenkinsRecipe recipe = this.testDescription.getAnnotation(JenkinsRecipe.class);
if (recipe == null) {
return;
}
@SuppressWarnings("unchecked")
final JenkinsRecipe.Runner<JenkinsRecipe> runner = (JenkinsRecipe.Runner<JenkinsRecipe>)
recipe.value().getDeclaredConstructor().newInstance();
recipes.add(runner);
tearDowns.add(() -> runner.tearDown(this, recipe));
}
}
@@ -0,0 +1,112 @@
package io.jenkins.plugins.casc.misc.junit.jupiter;

import static org.junit.platform.commons.support.ReflectionSupport.findFields;

import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
import java.lang.reflect.Field;
import java.util.function.Predicate;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContextException;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.HierarchyTraversalMode;
import org.junit.platform.commons.support.ModifierSupport;

/**
* JUnit 5 extension providing {@link JenkinsConfiguredWithCodeRule} integration.
*
* @see WithJenkinsConfiguredWithCode
*/
class JenkinsConfiguredWithCodeExtension
implements BeforeAllCallback, AfterAllCallback, ParameterResolver, AfterEachCallback {

private static final String KEY = "jenkins-instance";
private static final ExtensionContext.Namespace NAMESPACE =
ExtensionContext.Namespace.create(JenkinsConfiguredWithCodeExtension.class);

@Override
public void beforeAll(ExtensionContext extensionContext) throws Exception {
Class<?> clazz = extensionContext.getRequiredTestClass();
Predicate<Field> predicate = (field -> ModifierSupport.isStatic(field)
&& JenkinsConfiguredWithCodeRule.class.isAssignableFrom(field.getType()));
Field field = findFields(clazz, predicate, HierarchyTraversalMode.BOTTOM_UP).stream()
.findFirst()
.orElse(null);
if (field == null) {
return;
}

final JenkinsConfiguredWithCodeRule rule =
new JUnit5JenkinsConfiguredWithCodeRule(extensionContext, field.getDeclaredAnnotations());

extensionContext
.getStore(NAMESPACE)
.getOrComputeIfAbsent(KEY, key -> rule, JenkinsConfiguredWithCodeRule.class);
try {
rule.before();
} catch (Throwable e) {
throw new ExtensionContextException(e.getMessage(), e);
}
field.setAccessible(true);
field.set(null, rule);
}

@Override
public void afterAll(ExtensionContext extensionContext) throws Exception {
Class<?> clazz = extensionContext.getRequiredTestClass();
Predicate<Field> predicate = (field -> ModifierSupport.isStatic(field)
&& JenkinsConfiguredWithCodeRule.class.isAssignableFrom(field.getType()));
Field field = findFields(clazz, predicate, HierarchyTraversalMode.BOTTOM_UP).stream()
.findFirst()
.orElse(null);
if (field != null) {
final JenkinsConfiguredWithCodeRule rule =
extensionContext.getStore(NAMESPACE).get(KEY, JenkinsConfiguredWithCodeRule.class);
if (rule == null) {
return;
}
rule.after();
}
}

@Override
public void afterEach(ExtensionContext context) throws Exception {
final JenkinsConfiguredWithCodeRule rule =
context.getStore(NAMESPACE).remove(KEY, JenkinsConfiguredWithCodeRule.class);
if (rule == null) {
return;
}
rule.after();
}

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return parameterContext.getParameter().getType().equals(JenkinsConfiguredWithCodeRule.class);
}

@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {

final JenkinsConfiguredWithCodeRule rule = extensionContext
.getStore(NAMESPACE)
.getOrComputeIfAbsent(
KEY,
key -> new JUnit5JenkinsConfiguredWithCodeRule(
extensionContext,
extensionContext.getRequiredTestMethod().getDeclaredAnnotations()),
JenkinsConfiguredWithCodeRule.class);

try {
rule.before();
return rule;
} catch (Throwable t) {
throw new ParameterResolutionException(t.getMessage(), t);
}
}
}
@@ -0,0 +1,90 @@
package io.jenkins.plugins.casc.misc.junit.jupiter;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
import org.jvnet.hudson.test.JenkinsRule;

/**
* JUnit 5 meta annotation providing {@link JenkinsRule JenkinsRule} integration.
*
* <p>Test methods using the rule extension need to accept it by {@link JenkinsRule JenkinsRule} parameter;
* each test case gets a new rule object.
* An annotated method without a {@link JenkinsRule JenkinsRule} parameter behaves as if it were not annotated.
*
* <p>Annotating a <em>class</em> provides access for all of its tests.
* Unrelated test cases can omit the parameter.
*
* <blockquote>
*
* <pre>
* &#64;WithJenkinsConfiguredWithCode
* class ExampleJUnit5Test {
*
* &#64;Test
* public void example(JenkinsConfiguredWithCodeRule r) {
* // use 'r' ...
* }
*
* &#64;Test
* public void exampleNotUsingRule() {
* // ...
* }
* }
* </pre>
*
* </blockquote>
*
* <p>Annotating a <i>method</i> limits access to the method.
*
* <blockquote>
*
* <pre>
* class ExampleJUnit5Test {
*
* &#64;WithJenkinsConfiguredWithCode
* &#64;Test
* public void example(JenkinsConfiguredWithCodeRule r) {
* // use 'r' ...
* }
* }
* </pre>
*
* </blockquote>
*
* <p>Class static fields are also supported</p>
*
* <blockquote>
*
* <pre>
* class ExampleJUnit5Test {
*
* &#64;WithJenkinsConfiguredWithCode
* static JenkinsConfiguredWithCodeRule r;
*
* &#64;Test
* public void example() {
* // use 'r' ...
* }
*
* &#64;Test
* public void anotherExample() {
* // use 'r' ...
* }
*
* }
* </pre>
*
* </blockquote>
*
* @see JenkinsConfiguredWithCodeExtension
* @see ExtendWith
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(JenkinsConfiguredWithCodeExtension.class)
public @interface WithJenkinsConfiguredWithCode {}
@@ -0,0 +1,22 @@
package io.jenkins.plugins.casc.junit.jupiter;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import hudson.model.User;
import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
import io.jenkins.plugins.casc.misc.junit.jupiter.WithJenkinsConfiguredWithCode;
import java.util.Collections;
import org.junit.jupiter.api.Test;

public class JenkinsConfiguredWithCodeMethodRuleTest {

@Test
@WithJenkinsConfiguredWithCode
@ConfiguredWithCode("admin.yml")
public void user_created(JenkinsConfiguredWithCodeRule rule) {
assertNotNull(rule);
User admin = User.get("admin", false, Collections.emptyMap());
assertNotNull(admin);
}
}
@@ -0,0 +1,28 @@
package io.jenkins.plugins.casc.junit.jupiter;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import hudson.model.User;
import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
import io.jenkins.plugins.casc.misc.junit.jupiter.WithJenkinsConfiguredWithCode;
import java.util.Collections;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

@WithJenkinsConfiguredWithCode
public class JenkinsConfiguredWithCodeRuleClassRuleTest {
@ConfiguredWithCode("admin.yml")
public static JenkinsConfiguredWithCodeRule j;

@BeforeAll
public static void beforeAll() {
assertNotNull(j);
}

@Test
public void user_created() {
User admin = User.get("admin", false, Collections.emptyMap());
assertNotNull(admin);
}
}
@@ -0,0 +1,8 @@
jenkins:
securityRealm:
local:
allowsSignup: false
users:
- id: "admin"
password: "admin"
authorizationStrategy: loggedInUsersCanDoAnything

0 comments on commit 0ccc3b6

Please sign in to comment.