Skip to content

Commit

Permalink
FailsAt annotation savecommit
Browse files Browse the repository at this point in the history
Documentation  still missing
  • Loading branch information
Bukama committed Apr 20, 2024
1 parent ce8599d commit 0bb8a50
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 0 deletions.
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"
//
// 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() {

}

}

}

}

0 comments on commit 0bb8a50

Please sign in to comment.