Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FailsAt annotation #814

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/FailAt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2024 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 java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.extension.ExtendWith;

/**
* {@code @FailAt} is a JUnit Jupiter extension to mark tests that shouldn't be executed after a given date,
* essentially failing a test when the date is reached. The date is given as an ISO 8601 string.
*
* <p>It may optionally be declared with a reason to document why the annotated test class or test
* method should fail at the given date.</p>
*
* <p>{@code @FailAt} can be used on the method and class level. It can only be used once per method or class,
* but is inherited from higher-level containers.</p>
*
* <p><strong>WARNING:</strong> This annotation allows the user to move an assumption out of one or multiple test
* method's code into an annotation. But this comes at a cost - applying {@code @FailAt} can make the test suite
* non-reproducible. If a passing test is run again after the "failAt" date that build would fail. A report entry is
* issued for every test that does not fail until a certain date.</p>
*
* @since 2.3.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
@Inherited
@ExtendWith(FailAtExtension.class)
public @interface FailAt {

/**
* The reason this annotated test class or test method should fail as soon as the given date is reached.
*/
String reason() default "";

/**
* The date from which this annotated test class or test method should fail as an ISO 8601 string in the
* format yyyy-MM-dd, e.g. 2023-05-28.
* The test will be executed regularly if that date is not yet reached.
*/
String date();

}
80 changes: 80 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/FailAtExtension.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2024 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.lang.String.format;
import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled;
import static org.junitpioneer.internal.PioneerAnnotationUtils.findClosestEnclosingAnnotation;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Optional;

import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;

/**
* This class implements the functionality for the {@code @FailAt} annotation.
*
* @see FailAt
*/
class FailAtExtension implements ExecutionCondition {

private static final DateTimeFormatter ISO_8601 = DateTimeFormatter.ISO_DATE;

@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
return getFailAtDateFromAnnotation(context)
.map(failAtDate -> evaluateFailAtDate(context, failAtDate))
.orElse(enabled("No @FailAt annotation found on element"));
}

private Optional<LocalDate> getFailAtDateFromAnnotation(ExtensionContext context) {
return findClosestEnclosingAnnotation(context, FailAt.class).map(FailAt::date).map(this::parseDate);
}

private LocalDate parseDate(String dateString) {
try {
return LocalDate.parse(dateString, ISO_8601);
}
catch (DateTimeParseException ex) {
throw new ExtensionConfigurationException(
"The `failAtDate` string '" + dateString + "' is no valid ISO-8601 string.", ex);
}
}

private ConditionEvaluationResult evaluateFailAtDate(ExtensionContext context, LocalDate failAtDate) {
LocalDate today = LocalDate.now();
boolean isBefore = today.isBefore(failAtDate);

if (isBefore) {
String reportEntry = format(
"The `date` %s is after the current date %s, so `@FailAt` did not fails the test \"%s\". It will do so when the date is reached.",
failAtDate.format(ISO_8601), today.format(ISO_8601), context.getUniqueId());
context.publishReportEntry("FailAt", reportEntry);
return enabled(reportEntry);
} else {
String reportEntry = format(
"The current date %s is after or on the `date` %s, so `@FailAt` fails the test \"%s\". Please remove the annotation.",
failAtDate.format(ISO_8601), today.format(ISO_8601), context.getUniqueId());
context.publishReportEntry(FailAtExtension.class.getSimpleName(), reportEntry);

String message = format("The current date %s is after or on the `date` %s", today.format(ISO_8601),
failAtDate.format(ISO_8601));

throw new ExtensionConfigurationException(message);
}
}

}
122 changes: 122 additions & 0 deletions src/test/java/org/junitpioneer/jupiter/FailAtExtensionTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2024 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 org.junitpioneer.testkit.assertion.PioneerAssert.assertThat;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junitpioneer.testkit.ExecutionResults;
import org.junitpioneer.testkit.PioneerTestKit;

@DisplayName("Tests for the FailAt extension")
class FailAtExtensionTests {

@Test
@DisplayName("Should not fail test without annotation")
void shouldNotFailTestWithoutAnnotation() {
final ExecutionResults results = PioneerTestKit.executeTestMethod(FailAtTestCases.class, "testNoAnnotation");
assertThat(results).hasSingleStartedTest();
assertThat(results).hasSingleSucceededTest();
assertThat(results).hasNumberOfSkippedTests(0);
assertThat(results).hasNoReportEntries();
}

@Test
@DisplayName("Should enable test with unparsable `date`` string")
void shouldEnableTestWithUnparsableUntilDateString() {
final ExecutionResults results = PioneerTestKit
.executeTestMethod(FailAtTestCases.class, "testUnparsableUntilDateString");
assertThat(results).hasSingleStartedTest();
assertThat(results).hasSingleFailedTest();
assertThat(results).hasNoReportEntries();
}

@Test
@DisplayName("Should fail test with `date` in the past")
void shouldFailTestWithFailAtDateInThePast() {
final ExecutionResults results = PioneerTestKit
.executeTestMethod(FailAtTestCases.class, "testIsAnnotatedWithDateInThePast");
assertThat(results).hasSingleStartedTest();
assertThat(results).hasSingleFailedTest();
assertThat(results).hasSingleReportEntry().firstValue().contains("is after or on the `date`");

}

@Test
@DisplayName("Should not fail a test with `date` in the future")
void shouldNotFailTestWithFailAtDateInTheFuture() {
final ExecutionResults results = PioneerTestKit
.executeTestMethod(FailAtTestCases.class, "testIsAnnotatedWithDateInTheFuture");
assertThat(results).hasSingleSucceededTest();
assertThat(results).hasSingleReportEntry().firstValue().contains("2199-01-01", "did not fails the test");
}

@Test
@DisplayName("Should fail nested test with `date` in the past when meta annotated by higher level container")
void shouldFailNestedTestWithFailAtDateInThPastWhenMetaAnnotated() {
final ExecutionResults results = PioneerTestKit
.executeTestMethod(FailAtTestCases.NestedTestCases.class, "shouldRetrieveFromClass");
assertThat(results).hasSingleFailedContainer();
assertThat(results).hasNumberOfStartedTests(0);
assertThat(results).hasSingleReportEntry().firstValue().contains("is after or on the `date`");
}

static class FailAtTestCases {

@Test
void testNoAnnotation() {

}

@Test
@FailAt(reason = "I don't know how to write dates!", date = "xxxx-yy-zz")
void testUnparsableUntilDateString() {

}

@Test
@FailAt(reason = "Everything was better in the past!", date = "1993-01-01")
void testIsAnnotatedWithDateInThePast() {

}

// How can we test today? This way gives "attribute must be constant compile error"
Bukama marked this conversation as resolved.
Show resolved Hide resolved
//
// private static final String DATE_NOW = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
//
// @Test
// @FailAt(reason = "Everything was better yesterday!", date = DATE_NOW)
// void testIsAnnotatedWithDateToday() {
//
// }

@Test
@FailAt(reason = "Keep on running!", date = "2199-01-01")
void testIsAnnotatedWithDateInTheFuture() {

}

@Nested
@FailAt(reason = "My child is younger than me, but still old!", date = "1993-01-01")
class NestedTestCases {

@Test
void shouldRetrieveFromClass() {

}

}

}

}