From bf834f11fd2e2cb985b080bb5f9884ac7fca37f8 Mon Sep 17 00:00:00 2001 From: noconnor Date: Sat, 17 Jun 2023 11:59:24 +0100 Subject: [PATCH] Adding example suite support (#89) * Adding support for juint5 Suites and applying suite level JUnitPerf annotations --------- Co-authored-by: noconnor --- .github/workflows/publish.yml | 1 - README.md | 6 - docs/junit5.md | 37 ++++ junit5-examples/pom.xml | 1 + .../examples/ExampleTestSuiteUsage.java | 39 +++++ .../examples/existing/TestClassOne.java | 17 ++ .../examples/existing/TestClassThree.java | 17 ++ .../examples/existing/TestClassTwo.java | 12 ++ .../noconnor/junitperf/JUnitPerfTest.java | 2 +- .../junitperf/JUnitPerfTestRequirement.java | 2 +- .../junitperf/data/EvaluationContext.java | 4 +- .../reporting/providers/utils/ViewData.java | 8 +- .../junitperf/statements/EvaluationTask.java | 8 +- .../junitperf/data/EvaluationContextTest.java | 12 ++ .../providers/utils/ViewDataTest.java | 8 + .../statements/EvaluationTaskTest.java | 22 ++- junitperf-junit5/pom.xml | 7 +- .../junitperf/JUnitPerfInterceptor.java | 82 ++++++--- .../junitperf/suite/SuiteRegistry.java | 120 +++++++++++++ .../org.junit.jupiter.api.extension.Extension | 1 + .../junitperf/JUnitPerfInterceptorTest.java | 90 +++++++++- .../junitperf/suite/SuiteRegistryTest.java | 165 ++++++++++++++++++ 22 files changed, 614 insertions(+), 47 deletions(-) create mode 100644 junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/ExampleTestSuiteUsage.java create mode 100644 junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassOne.java create mode 100644 junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassThree.java create mode 100644 junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassTwo.java create mode 100644 junitperf-junit5/src/main/java/com/github/noconnor/junitperf/suite/SuiteRegistry.java create mode 100644 junitperf-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 junitperf-junit5/src/test/java/com/github/noconnor/junitperf/suite/SuiteRegistryTest.java diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ad58603..d8fef29 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,6 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - ref: ${{ github.ref_name }} # https://github.com/actions/checkout/issues/317 - name: Set up Maven Central Repository uses: actions/setup-java@v3 with: diff --git a/README.md b/README.md index d461f6a..49382c4 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,6 @@ This will override the `durationMs` set in the `@JUnitPerfTest` annotation. [CSV Reporting](#csv-reporting) -[Custom Reporting](#custom-reporting) - -[Multiple Reports](#multiple-reports) - -[Grouping Reports](#grouping-reports) -
#### HTML Reports diff --git a/docs/junit5.md b/docs/junit5.md index 32564b6..d74b8fa 100644 --- a/docs/junit5.md +++ b/docs/junit5.md @@ -13,6 +13,8 @@ Back to [index](../README.md) page. [Overriding Statistic Capturing](#overriding-statistic-capturing) +[Test Suites](#test-suite-configuration) +
### Install Instructions @@ -312,3 +314,38 @@ private final static JUnitPerfReportingConfig PERF_CONFIG = JUnitPerfReportingCo ``` For each `@Test` instance, the `statisticsCalculatorSupplier` will be called to generate a new `StatisticsCalculator` instance + +
+ +### Test Suite Configuration + +JUnitPerf annotations can also be applied at the suite level.
+When running in a test suite, annotation resolution follows the following precedence order: + +* Test method level annotations will take precedence over all other annotation configurations +* If no method level annotations are available, class level annotations will be considered next +* If no class level annotations are available, an attempt will be made to identify suite level annotations (if the test is currently running as part of a suite) + +If order to apply the JUnitPerfTestInterceptor to all tests in a suite, jupiter global extensions must be enabled.
+This can be done by: +* Adding a run time VM arg: `-Djunit.jupiter.extensions.autodetection.enabled=true` +* Specifying the extensions param as a ConfigurationParameter at the suite level: i.e. add this annotation to your suite `@ConfigurationParameter(key = "junit.jupiter.extensions.autodetection.enabled", value = "true")` + +The `junit-platform-suite-engine` dependency must also be added to your project: +``` + + org.junit.platform + junit-platform-suite-engine + LATEST_VERSION + test + +``` + + +An example test case can be found in the [junit5-examples](../junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/ExampleTestSuiteUsage.java) module + +To run the example suite test you can use the following maven commands +* from inside the junitperf project root directory: `mvn clean install -Dgpg.skip` + * This will install all junitperf snapshot dependencies to your local maven repo +* from inside the `junit5-examples` folder: `mvn -Dtest=ExampleTestSuiteUsage -DskipTests=false test` + * Location of test reports will be printed to console (default location is the `junit5-examples/build/reports/` directory) diff --git a/junit5-examples/pom.xml b/junit5-examples/pom.xml index 1161b65..d5a4302 100644 --- a/junit5-examples/pom.xml +++ b/junit5-examples/pom.xml @@ -25,6 +25,7 @@ org.junit.platform junit-platform-suite-engine 1.9.3 + test diff --git a/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/ExampleTestSuiteUsage.java b/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/ExampleTestSuiteUsage.java new file mode 100644 index 0000000..b210a41 --- /dev/null +++ b/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/ExampleTestSuiteUsage.java @@ -0,0 +1,39 @@ +package com.github.noconnor.junitperf.examples; + +import com.github.noconnor.junitperf.JUnitPerfReportingConfig; +import com.github.noconnor.junitperf.JUnitPerfTest; +import com.github.noconnor.junitperf.JUnitPerfTestActiveConfig; +import com.github.noconnor.junitperf.JUnitPerfTestRequirement; +import com.github.noconnor.junitperf.examples.existing.TestClassOne; +import com.github.noconnor.junitperf.examples.existing.TestClassTwo; +import org.junit.platform.suite.api.ConfigurationParameter; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +import static com.github.noconnor.junitperf.examples.utils.ReportingUtils.newHtmlReporter; + + +// +// To run suite: mvn -Dtest=ExampleTestSuiteUsage -DskipTests=false test +// + +@Suite +//@SelectPackages({ +// "com.github.noconnor.junitperf.examples.existing" +//}) +@SelectClasses({ + TestClassOne.class, + TestClassTwo.class +}) +// ConfigurationParameter: Required to enable Test Suite Interceptor Reference: https://www.baeldung.com/junit-5-extensions#1-automatic-extension-registration +@ConfigurationParameter(key = "junit.jupiter.extensions.autodetection.enabled", value = "true") +@JUnitPerfTest(totalExecutions = 100) +@JUnitPerfTestRequirement(allowedErrorPercentage = 0.01F) +public class ExampleTestSuiteUsage { + + @JUnitPerfTestActiveConfig + public static JUnitPerfReportingConfig config = JUnitPerfReportingConfig.builder() + .reportGenerator(newHtmlReporter("suite_reporter.html")) + .build(); + +} diff --git a/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassOne.java b/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassOne.java new file mode 100644 index 0000000..097491e --- /dev/null +++ b/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassOne.java @@ -0,0 +1,17 @@ +package com.github.noconnor.junitperf.examples.existing; + +import org.junit.jupiter.api.Test; + +public class TestClassOne { + @Test + public void sample_test1_class1() throws InterruptedException { + Thread.sleep(5); + } + + @Test + public void sample_test2_class1() throws InterruptedException { + // Mock some processing logic + Thread.sleep(1); + } + +} diff --git a/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassThree.java b/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassThree.java new file mode 100644 index 0000000..92300a6 --- /dev/null +++ b/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassThree.java @@ -0,0 +1,17 @@ +package com.github.noconnor.junitperf.examples.existing; + +import org.junit.jupiter.api.Test; + +public class TestClassThree { + @Test + public void sample_test1_class3() throws InterruptedException { + Thread.sleep(5); + } + + @Test + public void sample_test2_class3() throws InterruptedException { + // Mock some processing logic + Thread.sleep(1); + } + +} diff --git a/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassTwo.java b/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassTwo.java new file mode 100644 index 0000000..245752d --- /dev/null +++ b/junit5-examples/src/test/java/com/github/noconnor/junitperf/examples/existing/TestClassTwo.java @@ -0,0 +1,12 @@ +package com.github.noconnor.junitperf.examples.existing; + +import org.junit.jupiter.api.Test; + +public class TestClassTwo { + + @Test + public void sample_test1_class2() throws InterruptedException { + // Mock some processing logic + Thread.sleep(1); + } +} diff --git a/junitperf-core/src/main/java/com/github/noconnor/junitperf/JUnitPerfTest.java b/junitperf-core/src/main/java/com/github/noconnor/junitperf/JUnitPerfTest.java index 46da55f..f1d8e17 100644 --- a/junitperf-core/src/main/java/com/github/noconnor/junitperf/JUnitPerfTest.java +++ b/junitperf-core/src/main/java/com/github/noconnor/junitperf/JUnitPerfTest.java @@ -6,7 +6,7 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.TYPE}) public @interface JUnitPerfTest { // Total number of threads to use during the test evaluations diff --git a/junitperf-core/src/main/java/com/github/noconnor/junitperf/JUnitPerfTestRequirement.java b/junitperf-core/src/main/java/com/github/noconnor/junitperf/JUnitPerfTestRequirement.java index edaab01..1dd034a 100644 --- a/junitperf-core/src/main/java/com/github/noconnor/junitperf/JUnitPerfTestRequirement.java +++ b/junitperf-core/src/main/java/com/github/noconnor/junitperf/JUnitPerfTestRequirement.java @@ -6,7 +6,7 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.TYPE}) public @interface JUnitPerfTestRequirement { // Expected target percentile distribution in the format "percentile1:expected_value_ms,percentile2:expected_value_ms,..." diff --git a/junitperf-core/src/main/java/com/github/noconnor/junitperf/data/EvaluationContext.java b/junitperf-core/src/main/java/com/github/noconnor/junitperf/data/EvaluationContext.java index fbdf678..61c8647 100644 --- a/junitperf-core/src/main/java/com/github/noconnor/junitperf/data/EvaluationContext.java +++ b/junitperf-core/src/main/java/com/github/noconnor/junitperf/data/EvaluationContext.java @@ -17,7 +17,6 @@ import com.google.common.primitives.Floats; import com.google.common.primitives.Ints; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import lombok.Getter; import lombok.Setter; @@ -104,6 +103,9 @@ public class EvaluationContext { private final String testName; @Getter private final String startTime; + @Getter + @Setter + private String groupName; public EvaluationContext(String testName, long startTimeNs) { this(testName, startTimeNs, false); diff --git a/junitperf-core/src/main/java/com/github/noconnor/junitperf/reporting/providers/utils/ViewData.java b/junitperf-core/src/main/java/com/github/noconnor/junitperf/reporting/providers/utils/ViewData.java index 30e6997..5413701 100644 --- a/junitperf-core/src/main/java/com/github/noconnor/junitperf/reporting/providers/utils/ViewData.java +++ b/junitperf-core/src/main/java/com/github/noconnor/junitperf/reporting/providers/utils/ViewData.java @@ -10,6 +10,8 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static java.util.Objects.nonNull; + @Getter public class ViewData { @@ -56,7 +58,7 @@ public static final class RequiredPercentilesData { private final List requiredPercentiles; public ViewData(EvaluationContext context) { - this.testName = context.getTestName(); + this.testName = buildTestName(context); this.testNameColour = context.isSuccessful() ? SUCCESS_COLOUR : FAILED_COLOUR; this.chartData = buildChartData(context); this.csvData = buildCsvData(context); @@ -85,6 +87,10 @@ public ViewData(EvaluationContext context) { this.requiredPercentiles = buildRequiredPercentileData(context); } + private static String buildTestName(EvaluationContext context) { + return nonNull(context.getGroupName()) ? context.getGroupName() + " : " + context.getTestName() : context.getTestName(); + } + private List buildRequiredPercentileData(EvaluationContext context) { return context.getRequiredPercentiles().entrySet() .stream() diff --git a/junitperf-core/src/main/java/com/github/noconnor/junitperf/statements/EvaluationTask.java b/junitperf-core/src/main/java/com/github/noconnor/junitperf/statements/EvaluationTask.java index b186c1b..23705e3 100644 --- a/junitperf-core/src/main/java/com/github/noconnor/junitperf/statements/EvaluationTask.java +++ b/junitperf-core/src/main/java/com/github/noconnor/junitperf/statements/EvaluationTask.java @@ -96,9 +96,11 @@ private void evaluateStatement(long startMeasurements) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Throwable throwable) { - stats.incrementEvaluationCount(); - stats.incrementErrorCount(); - stats.addLatencyMeasurement(nanoTime() - startTimeNs); + if (!(throwable.getCause() instanceof InterruptedException)) { + stats.incrementEvaluationCount(); + stats.incrementErrorCount(); + stats.addLatencyMeasurement(nanoTime() - startTimeNs); + } } try { diff --git a/junitperf-core/src/test/java/com/github/noconnor/junitperf/data/EvaluationContextTest.java b/junitperf-core/src/test/java/com/github/noconnor/junitperf/data/EvaluationContextTest.java index 4e90512..5ad1924 100644 --- a/junitperf-core/src/test/java/com/github/noconnor/junitperf/data/EvaluationContextTest.java +++ b/junitperf-core/src/test/java/com/github/noconnor/junitperf/data/EvaluationContextTest.java @@ -4,6 +4,7 @@ import static com.github.noconnor.junitperf.data.EvaluationContext.JUNITPERF_MAX_EXECUTIONS_PER_SECOND; import static com.github.noconnor.junitperf.data.EvaluationContext.JUNITPERF_RAMP_UP_PERIOD_MS; import static com.github.noconnor.junitperf.data.EvaluationContext.JUNITPERF_THREADS; +import static com.github.noconnor.junitperf.data.EvaluationContext.JUNITPERF_TOTAL_EXECUTIONS; import static com.github.noconnor.junitperf.data.EvaluationContext.JUNITPERF_WARM_UP_MS; import static java.lang.System.nanoTime; import static java.util.Collections.emptyMap; @@ -61,6 +62,7 @@ public void tearDown(){ System.clearProperty(JUNITPERF_WARM_UP_MS); System.clearProperty(JUNITPERF_MAX_EXECUTIONS_PER_SECOND); System.clearProperty(JUNITPERF_RAMP_UP_PERIOD_MS); + System.clearProperty(JUNITPERF_TOTAL_EXECUTIONS); } @Test @@ -71,6 +73,7 @@ public void whenLoadingJUnitPerfTestSettings_thenAppropriateContextSettingsShoul assertEquals(perfTestAnnotation.threads(), context.getConfiguredThreads()); assertEquals(perfTestAnnotation.warmUpMs(), context.getConfiguredWarmUp()); assertEquals(perfTestAnnotation.rampUpPeriodMs(), context.getConfiguredRampUpPeriodMs()); + assertEquals(perfTestAnnotation.totalExecutions(), context.getConfiguredExecutionTarget()); } @Test @@ -80,12 +83,14 @@ public void whenLoadingJUnitPerfTestSettings_andEnvironmentHasOverrides_thenAppr System.setProperty(JUNITPERF_WARM_UP_MS, "2000"); System.setProperty(JUNITPERF_MAX_EXECUTIONS_PER_SECOND, "55"); System.setProperty(JUNITPERF_RAMP_UP_PERIOD_MS, "1000"); + System.setProperty(JUNITPERF_TOTAL_EXECUTIONS, "400"); context.loadConfiguration(perfTestAnnotation); assertEquals(60000, context.getConfiguredDuration()); assertEquals(55, context.getConfiguredRateLimit()); assertEquals(45, context.getConfiguredThreads()); assertEquals(2000, context.getConfiguredWarmUp()); assertEquals(1000, context.getConfiguredRampUpPeriodMs()); + assertEquals(400, context.getConfiguredExecutionTarget()); } @Test @@ -401,6 +406,12 @@ public void whenSpecifyingAsyncFlag_thenIsAsyncShouldBeTrue() { assertTrue(context.isAsyncEvaluation()); } + @Test + public void whenGroupNameIsSet_thenGroupNameShouldBeConfigured() { + context.setGroupName("unittest"); + assertEquals("unittest", context.getGroupName()); + } + @Test public void whenDurationIsValid_thenDurationShouldBeFormatted() { context.setFinishTimeNs(startTimeNs + 100_000_000_000_000L); @@ -423,6 +434,7 @@ private void initialisePerfTestAnnotation() { when(perfTestAnnotation.threads()).thenReturn(50); when(perfTestAnnotation.warmUpMs()).thenReturn(5); when(perfTestAnnotation.rampUpPeriodMs()).thenReturn(4); + when(perfTestAnnotation.totalExecutions()).thenReturn(345); } private void initialisePerfTestRequirementAnnotation() { diff --git a/junitperf-core/src/test/java/com/github/noconnor/junitperf/reporting/providers/utils/ViewDataTest.java b/junitperf-core/src/test/java/com/github/noconnor/junitperf/reporting/providers/utils/ViewDataTest.java index 6ebaca5..fb7abbf 100644 --- a/junitperf-core/src/test/java/com/github/noconnor/junitperf/reporting/providers/utils/ViewDataTest.java +++ b/junitperf-core/src/test/java/com/github/noconnor/junitperf/reporting/providers/utils/ViewDataTest.java @@ -140,6 +140,14 @@ public void whenRequiredPercentilesIsNotSet_thenViewDataShouldBeMappedCorrectly( assertEquals(Collections.emptyList(), viewData.getRequiredPercentiles()); } + @Test + public void whenGroupNameIsSet_thenTestNameShouldBeScopedByGroupName() { + EvaluationContext context = buildMockContext(1234F, true); + when(context.getGroupName()).thenReturn("ClassName"); + ViewData viewData = new ViewData(context); + assertEquals("ClassName : Unittest", viewData.getTestName()); + } + private EvaluationContext buildMockContext(float dummyLatency, boolean isSuccessful) { EvaluationContext context = mock(EvaluationContext.class); when(context.getTestName()).thenReturn("Unittest"); diff --git a/junitperf-core/src/test/java/com/github/noconnor/junitperf/statements/EvaluationTaskTest.java b/junitperf-core/src/test/java/com/github/noconnor/junitperf/statements/EvaluationTaskTest.java index ba2bd8d..2e46a7a 100644 --- a/junitperf-core/src/test/java/com/github/noconnor/junitperf/statements/EvaluationTaskTest.java +++ b/junitperf-core/src/test/java/com/github/noconnor/junitperf/statements/EvaluationTaskTest.java @@ -83,6 +83,16 @@ public void whenRunning_andStatementEvaluationThrowsAnException_thenLatencyMeasu verify(statsMock, times(10)).addLatencyMeasurement(anyLong()); } + @Test + public void whenRunning_andStatementEvaluationThrowsAnInterruptException_thenNoMeasurementsShouldBeTakenTaken() throws Throwable { + setExecutionCount(10); + mockNestedInterruptAfter(9); + task.run(); + verify(statsMock, times(9)).addLatencyMeasurement(anyLong()); + verify(statsMock, times(9)).incrementEvaluationCount(); + verify(statsMock, never()).incrementErrorCount(); + } + @Test public void whenRunning_thenAnAttemptShouldBeMadeToRetrieveAPermit() { setExecutionCount(10); @@ -200,7 +210,6 @@ private void mockEvaluationFailures(int desiredFailureCount) throws Throwable { } return null; }).when(statementMock).evaluate(); - } private void mockInterruptAfter(int desiredSuccessfulInvocations) throws Throwable { @@ -213,6 +222,17 @@ private void mockInterruptAfter(int desiredSuccessfulInvocations) throws Throwab }).when(statementMock).evaluate(); } + private void mockNestedInterruptAfter(int desiredSuccessfulInvocations) throws Throwable { + AtomicInteger executions = new AtomicInteger(); + doAnswer(invocation -> { + if (executions.getAndIncrement() >= desiredSuccessfulInvocations) { + throw new RuntimeException("Nest interrupt", new InterruptedException("mock exception")); + } + return null; + }).when(statementMock).evaluate(); + } + + private void mockTerminateAfter(int desiredSuccessfulInvocations) throws Throwable { AtomicInteger executions = new AtomicInteger(); doAnswer(invocation -> { diff --git a/junitperf-junit5/pom.xml b/junitperf-junit5/pom.xml index 98d6005..5c4f05e 100644 --- a/junitperf-junit5/pom.xml +++ b/junitperf-junit5/pom.xml @@ -11,6 +11,7 @@ 5.9.0 + 1.9.3 3.6.28 @@ -32,9 +33,9 @@ test - org.junit.jupiter - junit-jupiter-engine - ${junit.jupiter.version} + org.junit.platform + junit-platform-suite-api + ${junit.jupiter.suite.api.version} test diff --git a/junitperf-junit5/src/main/java/com/github/noconnor/junitperf/JUnitPerfInterceptor.java b/junitperf-junit5/src/main/java/com/github/noconnor/junitperf/JUnitPerfInterceptor.java index f069ce5..f8be057 100644 --- a/junitperf-junit5/src/main/java/com/github/noconnor/junitperf/JUnitPerfInterceptor.java +++ b/junitperf-junit5/src/main/java/com/github/noconnor/junitperf/JUnitPerfInterceptor.java @@ -8,8 +8,15 @@ import com.github.noconnor.junitperf.statements.SimpleTestStatement; import com.github.noconnor.junitperf.statistics.StatisticsCalculator; import com.github.noconnor.junitperf.statistics.providers.DescriptiveStatisticsCalculator; +import com.github.noconnor.junitperf.suite.SuiteRegistry; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.extension.*; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -29,26 +36,26 @@ public class JUnitPerfInterceptor implements InvocationInterceptor, TestInstancePostProcessor, ParameterResolver { protected static final ReportGenerator DEFAULT_REPORTER = new ConsoleReportGenerator(); - protected static final Map, LinkedHashSet> ACTIVE_CONTEXTS = new ConcurrentHashMap<>(); + protected static final Map> ACTIVE_CONTEXTS = new ConcurrentHashMap<>(); protected Collection activeReporters; protected StatisticsCalculator activeStatisticsCalculator; protected long measurementsStartTimeMs; protected PerformanceEvaluationStatementBuilder statementBuilder; + @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { - // Will be called for every instance of @Test - - for (Field field : testInstance.getClass().getDeclaredFields()) { - if (field.isAnnotationPresent(JUnitPerfTestActiveConfig.class)) { - warnIfNonStatic(field); - field.setAccessible(true); - JUnitPerfReportingConfig reportingConfig = (JUnitPerfReportingConfig) field.get(testInstance); - activeReporters = reportingConfig.getReportGenerators(); - activeStatisticsCalculator = reportingConfig.getStatisticsCalculatorSupplier().get(); - } + + SuiteRegistry.register(context); + + JUnitPerfReportingConfig reportingConfig = findTestActiveConfigField(testInstance, context); + + if (nonNull(reportingConfig)) { + activeReporters = reportingConfig.getReportGenerators(); + activeStatisticsCalculator = reportingConfig.getStatisticsCalculatorSupplier().get(); } + // Defaults if no overrides provided if (isNull(activeReporters) || activeReporters.isEmpty()) { activeReporters = singletonList(DEFAULT_REPORTER); @@ -64,11 +71,10 @@ public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { // Will be called for every instance of @Test - Method method = extensionContext.getRequiredTestMethod(); - JUnitPerfTest perfTestAnnotation = method.getAnnotation(JUnitPerfTest.class); - JUnitPerfTestRequirement requirementsAnnotation = method.getAnnotation(JUnitPerfTestRequirement.class); + JUnitPerfTest perfTestAnnotation = getJUnitPerfTestDetails(method, extensionContext); + JUnitPerfTestRequirement requirementsAnnotation = getJUnitPerfTestRequirementDetails(method, extensionContext); if (nonNull(perfTestAnnotation)) { measurementsStartTimeMs = currentTimeMillis() + perfTestAnnotation.warmUpMs(); @@ -78,8 +84,8 @@ public void interceptTestMethod(Invocation invocation, context.loadConfiguration(perfTestAnnotation); context.loadRequirements(requirementsAnnotation); - ACTIVE_CONTEXTS.putIfAbsent(extensionContext.getRequiredTestClass(), new LinkedHashSet<>()); - ACTIVE_CONTEXTS.get(extensionContext.getRequiredTestClass()).add(context); + ACTIVE_CONTEXTS.putIfAbsent(extensionContext.getUniqueId(), new LinkedHashSet<>()); + ACTIVE_CONTEXTS.get(extensionContext.getUniqueId()).add(context); SimpleTestStatement testStatement = () -> method.invoke( extensionContext.getRequiredTestInstance(), @@ -90,7 +96,7 @@ public void interceptTestMethod(Invocation invocation, .baseStatement(testStatement) .statistics(activeStatisticsCalculator) .context(context) - .listener(complete -> updateReport(method.getDeclaringClass())) + .listener(complete -> updateReport(extensionContext)) .build(); parallelExecution.runParallelEvaluation(); @@ -113,13 +119,34 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte return new TestContextSupplier(measurementsStartTimeMs, activeStatisticsCalculator); } - private EvaluationContext createEvaluationContext(Method method, boolean isAsync) { - return new EvaluationContext(method.getName(), nanoTime(), isAsync); + + protected JUnitPerfTestRequirement getJUnitPerfTestRequirementDetails(Method method, ExtensionContext ctxt) { + JUnitPerfTestRequirement methodAnnotation = method.getAnnotation(JUnitPerfTestRequirement.class); + JUnitPerfTestRequirement classAnnotation = method.getDeclaringClass().getAnnotation(JUnitPerfTestRequirement.class); + JUnitPerfTestRequirement suiteAnnotation = SuiteRegistry.getPerfRequirements(ctxt); + // Precedence: method, then class, then suite + JUnitPerfTestRequirement specifiedAnnotation = nonNull(methodAnnotation) ? methodAnnotation : classAnnotation; + return nonNull(specifiedAnnotation) ? specifiedAnnotation : suiteAnnotation; } - private synchronized void updateReport(Class clazz) { + protected JUnitPerfTest getJUnitPerfTestDetails(Method method, ExtensionContext ctxt) { + JUnitPerfTest methodAnnotation = method.getAnnotation(JUnitPerfTest.class); + JUnitPerfTest classAnnotation = method.getDeclaringClass().getAnnotation(JUnitPerfTest.class); + JUnitPerfTest suiteAnnotation = SuiteRegistry.getPerfTestData(ctxt); + // Precedence: method, then class, then suite + JUnitPerfTest specifiedAnnotation = nonNull(methodAnnotation) ? methodAnnotation : classAnnotation; + return nonNull(specifiedAnnotation) ? specifiedAnnotation : suiteAnnotation; + } + + protected EvaluationContext createEvaluationContext(Method method, boolean isAsync) { + EvaluationContext ctx = new EvaluationContext(method.getName(), nanoTime(), isAsync); + ctx.setGroupName(method.getDeclaringClass().getSimpleName()); + return ctx; + } + + private synchronized void updateReport(ExtensionContext ctxt) { activeReporters.forEach(r -> { - r.generateReport(ACTIVE_CONTEXTS.get(clazz)); + r.generateReport(ACTIVE_CONTEXTS.get(ctxt.getUniqueId())); }); } @@ -130,4 +157,15 @@ private static void warnIfNonStatic(Field field) { } } + private static JUnitPerfReportingConfig findTestActiveConfigField(Object testInstance, ExtensionContext ctxt) throws IllegalAccessException { + for (Field field : testInstance.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(JUnitPerfTestActiveConfig.class)) { + warnIfNonStatic(field); + field.setAccessible(true); + return (JUnitPerfReportingConfig) field.get(testInstance); + } + } + return SuiteRegistry.getReportingConfig(ctxt); + } + } diff --git a/junitperf-junit5/src/main/java/com/github/noconnor/junitperf/suite/SuiteRegistry.java b/junitperf-junit5/src/main/java/com/github/noconnor/junitperf/suite/SuiteRegistry.java new file mode 100644 index 0000000..a849e3c --- /dev/null +++ b/junitperf-junit5/src/main/java/com/github/noconnor/junitperf/suite/SuiteRegistry.java @@ -0,0 +1,120 @@ +package com.github.noconnor.junitperf.suite; + +import com.github.noconnor.junitperf.JUnitPerfReportingConfig; +import com.github.noconnor.junitperf.JUnitPerfTest; +import com.github.noconnor.junitperf.JUnitPerfTestActiveConfig; +import com.github.noconnor.junitperf.JUnitPerfTestRequirement; +import lombok.Builder; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +@Slf4j +public class SuiteRegistry { + + private static final Map settingsCache = new HashMap<>(); + private static final Pattern suiteClassPattern = Pattern.compile(".*\\[suite:([^\\]]*)\\].*"); + + public static void register(ExtensionContext context) { + + String rootUniqueId = getRootId(context); + Class clazz = getSuiteClass(rootUniqueId); + + if (isNull(clazz) || settingsCache.containsKey(rootUniqueId)) { + return; + } + + JUnitPerfTest testSpec = clazz.getAnnotation(JUnitPerfTest.class); + JUnitPerfTestRequirement requirements = clazz.getAnnotation(JUnitPerfTestRequirement.class); + JUnitPerfReportingConfig reportingConfig = Arrays.stream(clazz.getFields()) + .filter(f -> f.isAnnotationPresent(JUnitPerfTestActiveConfig.class)) + .map(f -> { + warnIfNonStatic(f); + return getFieldValue(f); + }) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + SuiteSettings suiteSettings = SuiteSettings.builder() + .perfTestSpec(testSpec) + .requirements(requirements) + .reportingConfig(reportingConfig) + .build(); + + settingsCache.put(rootUniqueId, suiteSettings); + } + + public static void clearRegistry() { + settingsCache.clear(); + } + + public static JUnitPerfReportingConfig getReportingConfig(ExtensionContext context) { + SuiteSettings s = settingsCache.get(getRootId(context)); + return nonNull(s) ? s.getReportingConfig() : null; + } + + public static JUnitPerfTest getPerfTestData(ExtensionContext context) { + SuiteSettings s = settingsCache.get(getRootId(context)); + return nonNull(s) ? s.getPerfTestSpec() : null; + } + + public static JUnitPerfTestRequirement getPerfRequirements(ExtensionContext context) { + SuiteSettings s = settingsCache.get(getRootId(context)); + return nonNull(s) ? s.getRequirements() : null; + } + + private static String getRootId(ExtensionContext context) { + if (nonNull(context) && nonNull(context.getRoot())) { + return context.getRoot().getUniqueId(); + } + return ""; + } + + private static Class getSuiteClass(String rootUniqueId) { + Matcher m = suiteClassPattern.matcher(rootUniqueId); + if (m.find()) { + try { + return Class.forName(m.group(1)); + } catch (ClassNotFoundException e) { + log.warn("Suite class not found: {}", rootUniqueId); + } + } + return null; + } + + private static JUnitPerfReportingConfig getFieldValue(Field f) { + try { + f.setAccessible(true); + return (JUnitPerfReportingConfig) f.get(null); + } catch (Exception e) { + log.error("Unable to access JUnitPerfReportingConfig, make sure config is a static variable", e); + } + return null; + } + + private static void warnIfNonStatic(Field f) { + if (!Modifier.isStatic(f.getModifiers())) { + log.warn("JUnitPerfReportingConfig must be static for test suites"); + } + } + + @Value + @Builder + private static class SuiteSettings { + JUnitPerfTest perfTestSpec; + JUnitPerfTestRequirement requirements; + JUnitPerfReportingConfig reportingConfig; + } +} diff --git a/junitperf-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/junitperf-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 0000000..d27e54e --- /dev/null +++ b/junitperf-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +com.github.noconnor.junitperf.JUnitPerfInterceptor \ No newline at end of file diff --git a/junitperf-junit5/src/test/java/com/github/noconnor/junitperf/JUnitPerfInterceptorTest.java b/junitperf-junit5/src/test/java/com/github/noconnor/junitperf/JUnitPerfInterceptorTest.java index 19844d0..9dd2ba2 100644 --- a/junitperf-junit5/src/test/java/com/github/noconnor/junitperf/JUnitPerfInterceptorTest.java +++ b/junitperf-junit5/src/test/java/com/github/noconnor/junitperf/JUnitPerfInterceptorTest.java @@ -2,17 +2,22 @@ import com.github.noconnor.junitperf.data.EvaluationContext; import com.github.noconnor.junitperf.reporting.ReportGenerator; +import com.github.noconnor.junitperf.reporting.providers.ConsoleReportGenerator; import com.github.noconnor.junitperf.reporting.providers.HtmlReportGenerator; import com.github.noconnor.junitperf.statements.PerformanceEvaluationStatement; import com.github.noconnor.junitperf.statements.PerformanceEvaluationStatement.PerformanceEvaluationStatementBuilder; import com.github.noconnor.junitperf.statistics.StatisticsCalculator; import com.github.noconnor.junitperf.statistics.providers.DescriptiveStatisticsCalculator; +import com.github.noconnor.junitperf.suite.SuiteRegistry; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.platform.suite.api.Suite; import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -22,6 +27,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; import static java.lang.System.currentTimeMillis; import static java.util.Collections.singletonList; @@ -38,9 +44,15 @@ class JUnitPerfInterceptorTest { @BeforeEach void setup() { + SuiteRegistry.clearRegistry(); JUnitPerfInterceptor.ACTIVE_CONTEXTS.clear(); interceptor = new JUnitPerfInterceptor(); } + + @AfterEach + void teardown() { + SuiteRegistry.clearRegistry(); + } @Test void whenATestClassHasNoReportingOverrides_thenDefaultReportingConfigsShouldBeSet() throws Exception { @@ -102,7 +114,7 @@ void whenTestHasNotBeenAnnotatedWithPerfAnnotations_thenTestWillBeExecutedOnce() Method methodMock = test.getClass().getMethod("someTestMethod"); Invocation invocationMock = mock(Invocation.class); ReflectiveInvocationContext invocationContextMock = mock(ReflectiveInvocationContext.class); - ExtensionContext extensionContextMock = mock(ExtensionContext.class); + ExtensionContext extensionContextMock = mockTestContext(); when(extensionContextMock.getRequiredTestMethod()).thenReturn(methodMock); interceptor.postProcessTestInstance(test, null); @@ -122,7 +134,7 @@ void whenTestHasBeenAnnotatedWithPerfAnnotations_thenTestStatementShouldBeBuilt( PerformanceEvaluationStatement statementMock = mock(PerformanceEvaluationStatement.class); Invocation invocationMock = mock(Invocation.class); ReflectiveInvocationContext invocationContextMock = mock(ReflectiveInvocationContext.class); - ExtensionContext extensionContextMock = mock(ExtensionContext.class); + ExtensionContext extensionContextMock = mockTestContext(); when(extensionContextMock.getRequiredTestMethod()).thenReturn(methodMock); when(extensionContextMock.getRequiredTestClass()).thenReturn((Class) test.getClass()); @@ -135,7 +147,7 @@ void whenTestHasBeenAnnotatedWithPerfAnnotations_thenTestStatementShouldBeBuilt( verify(invocationMock).proceed(); verify(statementMock).runParallelEvaluation(); - assertEquals(1, JUnitPerfInterceptor.ACTIVE_CONTEXTS.get(test.getClass()).size()); + assertEquals(1, JUnitPerfInterceptor.ACTIVE_CONTEXTS.get(extensionContextMock.getUniqueId()).size()); EvaluationContext context = captureEvaluationContext(); assertFalse(context.isAsyncEvaluation()); } @@ -149,7 +161,7 @@ void whenTestHasBeenAnnotatedWithPerfAnnotations_thenMeasurementStartMsShouldBeC PerformanceEvaluationStatement statementMock = mock(PerformanceEvaluationStatement.class); Invocation invocationMock = mock(Invocation.class); ReflectiveInvocationContext invocationContextMock = mock(ReflectiveInvocationContext.class); - ExtensionContext extensionContextMock = mock(ExtensionContext.class); + ExtensionContext extensionContextMock = mockTestContext(); when(extensionContextMock.getRequiredTestMethod()).thenReturn(methodMock); when(extensionContextMock.getRequiredTestClass()).thenReturn((Class) test.getClass()); @@ -162,7 +174,7 @@ void whenTestHasBeenAnnotatedWithPerfAnnotations_thenMeasurementStartMsShouldBeC assertTrue(interceptor.measurementsStartTimeMs > 0); assertTrue(interceptor.measurementsStartTimeMs <= currentTimeMillis() + 100); // see warmUpMs in annotation } - + @SuppressWarnings("unchecked") @Test void whenAsyncTestHasBeenAnnotatedWithPerfAnnotations_thenContextShouldBeMarkedAsAsync() throws Throwable { @@ -172,11 +184,11 @@ void whenAsyncTestHasBeenAnnotatedWithPerfAnnotations_thenContextShouldBeMarkedA PerformanceEvaluationStatement statementMock = mock(PerformanceEvaluationStatement.class); Invocation invocationMock = mock(Invocation.class); ReflectiveInvocationContext invocationContextMock = mock(ReflectiveInvocationContext.class); - ExtensionContext extensionContextMock = mock(ExtensionContext.class); + ExtensionContext extensionContextMock = mockTestContext(); when(invocationContextMock.getArguments()).thenReturn(mockAsyncArgs()); when(extensionContextMock.getRequiredTestMethod()).thenReturn(methodMock); - when(extensionContextMock.getRequiredTestClass()).thenReturn((Class) test.getClass()); + when(extensionContextMock.getUniqueId()).thenReturn(test.getClass().getSimpleName()); when(statementBuilderMock.build()).thenReturn(statementMock); interceptor.postProcessTestInstance(test, null); @@ -187,6 +199,47 @@ void whenAsyncTestHasBeenAnnotatedWithPerfAnnotations_thenContextShouldBeMarkedA assertTrue(context.isAsyncEvaluation()); } + @Test + void whenASuiteAnnotationsAreAvailable_thenSuiteAnnotationsShouldBeUsed() throws Throwable { + SampleNoAnnotationsTest test = new SampleNoAnnotationsTest(); + + Method methodMock = test.getClass().getMethod("someTestMethod"); + PerformanceEvaluationStatement statementMock = mock(PerformanceEvaluationStatement.class); + Invocation invocationMock = mock(Invocation.class); + ReflectiveInvocationContext invocationContextMock = mock(ReflectiveInvocationContext.class); + ExtensionContext extensionContextMock = mockTestContext(); + mockActiveSuite(extensionContextMock, SuiteSampleTest.class); + + + when(extensionContextMock.getRequiredTestMethod()).thenReturn(methodMock); + when(extensionContextMock.getRequiredTestClass()).thenReturn((Class) test.getClass()); + when(statementBuilderMock.build()).thenReturn(statementMock); + + interceptor.postProcessTestInstance(test, extensionContextMock); + interceptor.statementBuilder = statementBuilderMock; + interceptor.interceptTestMethod(invocationMock, invocationContextMock, extensionContextMock); + + assertTrue(interceptor.measurementsStartTimeMs > 0); + + assertEquals(1, interceptor.activeReporters.size()); + assertEquals(SuiteSampleTest.config.getReportGenerators(), interceptor.activeReporters); + + EvaluationContext context = captureEvaluationContext(); + assertEquals(100, context.getConfiguredExecutionTarget()); + assertEquals(5, context.getConfiguredThreads()); + assertEquals(1, context.getRequiredThroughput()); + + } + + private static void mockActiveSuite(ExtensionContext extensionContextMock, Class suiteClass) { + when(extensionContextMock.getRoot()).thenReturn(extensionContextMock); + when(extensionContextMock.getUniqueId()).thenReturn(buildSuiteId(suiteClass)); + } + + private static String buildSuiteId(Class clazz) { + return "[engine:junit-platform-suite]/[suite:" + clazz.getName() + "]/[engine:junit-jupiter]"; + } + @Test void whenInterceptorSupportsParameterIsCalled_thenParameterTypeShouldBeChecked() throws NoSuchMethodException { assertTrue(interceptor.supportsParameter(mockTestContextSupplierParameterType(), null)); @@ -225,6 +278,13 @@ private EvaluationContext captureEvaluationContext() { return context; } + private static ExtensionContext mockTestContext() { + ExtensionContext ctxt = mock(ExtensionContext.class); + when(ctxt.getUniqueId()).thenReturn("unitest" + ThreadLocalRandom.current().nextInt()); + return ctxt; + } + + public static class SampleTestNoReportingOverrides { } @@ -253,6 +313,7 @@ public static class SampleTestWithReportingOverridesMissingAnnotation { .build(); } + @Disabled public static class SampleNoAnnotationsTest { @Test public void someTestMethod() { @@ -260,6 +321,7 @@ public void someTestMethod() { } } + @Disabled public static class SampleAnnotatedTest { @Test @JUnitPerfTest(threads = 1, durationMs = 1_000, maxExecutionsPerSecond = 1_000, warmUpMs = 100) @@ -268,6 +330,8 @@ public void someTestMethod() { } } + + @Disabled public static class SampleAsyncAnnotatedTest { @Test @JUnitPerfTest(threads = 1, durationMs = 1_000, maxExecutionsPerSecond = 1_000, warmUpMs = 100) @@ -280,4 +344,16 @@ public void someOtherTestMethod(String param) { assertTrue(true); } } + + @Disabled + @Suite + @JUnitPerfTest( threads = 5, totalExecutions = 100) + @JUnitPerfTestRequirement(executionsPerSec = 1) + public static class SuiteSampleTest { + @JUnitPerfTestActiveConfig + public static JUnitPerfReportingConfig config = JUnitPerfReportingConfig.builder() + .reportGenerator(new ConsoleReportGenerator()) + .build(); + } + } \ No newline at end of file diff --git a/junitperf-junit5/src/test/java/com/github/noconnor/junitperf/suite/SuiteRegistryTest.java b/junitperf-junit5/src/test/java/com/github/noconnor/junitperf/suite/SuiteRegistryTest.java new file mode 100644 index 0000000..70c1119 --- /dev/null +++ b/junitperf-junit5/src/test/java/com/github/noconnor/junitperf/suite/SuiteRegistryTest.java @@ -0,0 +1,165 @@ +package com.github.noconnor.junitperf.suite; + +import com.github.noconnor.junitperf.JUnitPerfReportingConfig; +import com.github.noconnor.junitperf.JUnitPerfTest; +import com.github.noconnor.junitperf.JUnitPerfTestActiveConfig; +import com.github.noconnor.junitperf.JUnitPerfTestRequirement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.suite.api.Suite; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SuiteRegistryTest { + + @BeforeEach + void setup() { + SuiteRegistry.clearRegistry(); + } + + @Test + void whenNoTestSuiteClassIsConfigured_thenSuiteShouldBeIdentified() { + ExtensionContext context = createMockExtensionContext("[engine:junit-jupiter]"); + SuiteRegistry.register(context); + assertNull(SuiteRegistry.getPerfTestData(context)); + assertNull(SuiteRegistry.getPerfRequirements(context)); + assertNull(SuiteRegistry.getReportingConfig(context)); + } + + @Test + void whenInvalidTestSuiteClassIsConfigured_thenSuiteShouldNotBeIdentified() { + ExtensionContext context = createMockExtensionContext(buildSuiteId("com.does.not.Exist")); + SuiteRegistry.register(context); + assertNull(SuiteRegistry.getPerfTestData(context)); + assertNull(SuiteRegistry.getPerfRequirements(context)); + assertNull(SuiteRegistry.getReportingConfig(context)); + } + + @Test + void whenTestSuiteClassIsConfigured_butSuiteHasNoAnnotations_thenSuiteShouldBeIdentifiedButNoPerfDataShouldBeAvailable() { + ExtensionContext context = createMockExtensionContext(buildSuiteId(DummySuiteNoAnnotations.class)); + SuiteRegistry.register(context); + assertNull(SuiteRegistry.getPerfTestData(context)); + assertNull(SuiteRegistry.getPerfRequirements(context)); + assertNull(SuiteRegistry.getReportingConfig(context)); + } + + @Test + void whenTestSuiteClassIsConfigured_andSuiteHasPerfAnnotation_thenSuitePerfDataShouldBeAvailable() { + ExtensionContext context = createMockExtensionContext(buildSuiteId(DummySuitePerfTestAnnotation.class)); + SuiteRegistry.register(context); + JUnitPerfTest testSpec = SuiteRegistry.getPerfTestData(context); + + assertNotNull(testSpec); + assertNull(SuiteRegistry.getPerfRequirements(context)); + assertNull(SuiteRegistry.getReportingConfig(context)); + assertEquals(40, testSpec.totalExecutions()); + } + + @Test + void whenTestSuiteClassIsConfigured_andSuiteHasAllPerfAnnotations_thenSuitePerfDataShouldBeAvailable() { + ExtensionContext context = createMockExtensionContext(buildSuiteId(DummySuitePerfTestAllAnnotations.class)); + SuiteRegistry.register(context); + JUnitPerfTest testSpec = SuiteRegistry.getPerfTestData(context); + JUnitPerfTestRequirement requirements = SuiteRegistry.getPerfRequirements(context); + + assertNotNull(testSpec); + assertNotNull(requirements); + assertNull(SuiteRegistry.getReportingConfig(context)); + + assertEquals(3, testSpec.totalExecutions()); + assertEquals(0.03F, requirements.allowedErrorPercentage()); + } + + @Test + void whenTestSuiteClassIsConfigured_andSuiteHasAllPerfAnnotationsAndReportingConfig_thenSuitePerfDataShouldBeAvailable() { + ExtensionContext context = createMockExtensionContext(buildSuiteId(DummySuiteAllConfigs.class)); + SuiteRegistry.register(context); + JUnitPerfTest testSpec = SuiteRegistry.getPerfTestData(context); + JUnitPerfTestRequirement requirements = SuiteRegistry.getPerfRequirements(context); + JUnitPerfReportingConfig reportConfig = SuiteRegistry.getReportingConfig(context); + + assertNotNull(testSpec); + assertNotNull(requirements); + assertNotNull(reportConfig); + + assertEquals(53, testSpec.totalExecutions()); + assertEquals(0.13F, requirements.allowedErrorPercentage()); + assertEquals(DummySuiteAllConfigs.config, reportConfig); + } + + @Test + void whenTestSuiteClassIsConfigured_andSuiteHasBadReporterConfig_thenSuitePerfDataShouldBeAvailable_butReporterConfigWillBeMissing() { + ExtensionContext context = createMockExtensionContext(buildSuiteId(DummySuiteBadReporterConfigs.class)); + SuiteRegistry.register(context); + JUnitPerfTest testSpec = SuiteRegistry.getPerfTestData(context); + JUnitPerfTestRequirement requirements = SuiteRegistry.getPerfRequirements(context); + JUnitPerfReportingConfig reportConfig = SuiteRegistry.getReportingConfig(context); + + assertNotNull(testSpec); + assertNotNull(requirements); + assertNull(reportConfig); + + assertEquals(345, testSpec.totalExecutions()); + assertEquals(0.168F, requirements.allowedErrorPercentage()); + } + + private static String buildSuiteId(Class clazz) { + return buildSuiteId(clazz.getName()); + } + + private static String buildSuiteId(String clazz) { + return "[engine:junit-platform-suite]/[suite:" + clazz + "]/[engine:junit-jupiter]"; + } + + private static ExtensionContext createMockExtensionContext(String rootId) { + ExtensionContext childContext = mock(ExtensionContext.class); + ExtensionContext rootContext = mock(ExtensionContext.class); + when(childContext.getRoot()).thenReturn(rootContext); + when(rootContext.getUniqueId()).thenReturn(rootId); + return childContext; + } + + + @Disabled + @Suite + public static class DummySuiteNoAnnotations { + } + + @Disabled + @Suite + @JUnitPerfTest(totalExecutions = 40) + public static class DummySuitePerfTestAnnotation { + } + + @Disabled + @Suite + @JUnitPerfTest(totalExecutions = 3) + @JUnitPerfTestRequirement(allowedErrorPercentage = 0.03F) + public static class DummySuitePerfTestAllAnnotations { + } + + @Disabled + @Suite + @JUnitPerfTest(totalExecutions = 53) + @JUnitPerfTestRequirement(allowedErrorPercentage = 0.13F) + public static class DummySuiteAllConfigs { + @JUnitPerfTestActiveConfig + public static JUnitPerfReportingConfig config = JUnitPerfReportingConfig.builder().build(); + } + + @Disabled + @Suite + @JUnitPerfTest(totalExecutions = 345) + @JUnitPerfTestRequirement(allowedErrorPercentage = 0.168F) + public static class DummySuiteBadReporterConfigs { + @JUnitPerfTestActiveConfig // not static - should be dropped + public JUnitPerfReportingConfig config = JUnitPerfReportingConfig.builder().build(); + } +} \ No newline at end of file