Skip to content

Commit

Permalink
issues/163: Support for JUnit 5s @nested test classes
Browse files Browse the repository at this point in the history
issues/163: Support for JUnit 5s @nested test classes
  • Loading branch information
fmck3516 committed Oct 20, 2024
1 parent 9b8ea4a commit 3ecdbc4
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 6 deletions.
73 changes: 67 additions & 6 deletions skippy-core/src/main/java/io/skippy/core/SkippyTestApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

import static io.skippy.core.JacocoUtil.mergeExecutionData;
import static io.skippy.core.JacocoUtil.swallowJacocoExceptions;
import static io.skippy.core.SkippyConstants.PREDICTIONS_LOG_FILE;
import static java.lang.System.lineSeparator;
import static java.nio.file.StandardOpenOption.*;
import static java.util.Arrays.asList;

/**
* API that is used by Skippy's JUnit libraries to query for skip-or-execute predictions and to trigger the generation of .exec files.
Expand All @@ -48,6 +51,47 @@ public final class SkippyTestApi {
private final SkippyConfiguration skippyConfiguration;
private final Map<String, Prediction> predictions = new ConcurrentHashMap<>();

/**
* Stack that keeps track of the execution data across nested test classes.
* <br /><br />
* Example:
* <pre>
* {@literal @}PredictWithSkippy
* public class Level1 {
*
* {@literal @}Nested
* class Level2 {
*
* {@literal @}Nested
* class Level3 {
*
* {@literal @}Test
* void testSomething() {
* }
*
* }
*
* }
*
* }
* </pre>
*
* By the time <code>testSomething</code> is executed, the stack would be populated as follows:
* <br /><br />
* <pre>
* frame 2 = { execution data for Level1$Level2$Level3.class }
* frame 1 = { execution data for Level1$Level2.class }
* frame 0 = { execution data for Level1.class }
* </pre>
*
* The stack is used for two purposes:
* <ul>
* <li>It prevents the loss of execution data when the control flow changes from a parent to a nested test class.</li>
* <li>It allows nested tests classes to contribute their execution data back to the parents.</li>
* </ul>
*/
private final Stack<List<byte[]>> executionDataStack = new Stack<>();

private SkippyTestApi(TestImpactAnalysis testImpactAnalysis, SkippyConfiguration skippyConfiguration, SkippyRepository skippyRepository) {
this.testImpactAnalysis = testImpactAnalysis;
this.skippyRepository = skippyRepository;
Expand Down Expand Up @@ -110,15 +154,18 @@ public boolean testNeedsToBeExecuted(Class<?> test) {
* @param testClass a test class
*/
public void prepareExecFileGeneration(Class<?> testClass) {
Profiler.profile("SkippyTestApi#prepareCoverageDataCaptureFor", () -> {
Profiler.profile("SkippyTestApi#prepareExecFileGeneration", () -> {
swallowJacocoExceptions(() -> {
IAgent agent = RT.getAgent();
if (isNestedTest()) {
addExecutionDataToParent(asList(agent.getExecutionData(false)));
}
agent.reset();
executionDataStack.push(new ArrayList<>());
});
});
}


/**
* Writes a JaCoCo execution data file after all tests in for {@code testClass} have been executed.
*
Expand All @@ -128,10 +175,24 @@ public void writeExecFile(Class<?> testClass) {
Profiler.profile("SkippyTestApi#writeExecFile", () -> {
swallowJacocoExceptions(() -> {
IAgent agent = RT.getAgent();
byte[] executionData = agent.getExecutionData(true);
skippyRepository.saveTemporaryJaCoCoExecutionDataForCurrentBuild(testClass.getName(), executionData);
var executionData = executionDataStack.lastElement();
executionData.add(agent.getExecutionData(true));
skippyRepository.saveTemporaryJaCoCoExecutionDataForCurrentBuild(testClass.getName(), mergeExecutionData(executionData));
executionDataStack.pop();
if (isNestedTest()) {
addExecutionDataToParent(executionData);
}
});
});
}

}
private boolean isNestedTest() {
return ! executionDataStack.isEmpty();
}

private void addExecutionDataToParent(List<byte[]> executionData) {
var parent = executionDataStack.lastElement();
parent.addAll(executionData);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.skippy.core;

import org.junit.jupiter.api.Test;

import static io.skippy.core.Prediction.EXECUTE;
import static io.skippy.core.Prediction.SKIP;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class TestImpactAnalysisPredictNestedTestsTest {

@Test
void testPredictNoChange() {
var testImpactAnalysis = TestImpactAnalysis.parse("""
{
"id": "F87679D196B903DFE24335295E13DEED",
"classes": {
"0": {
"name": "com.example.ClassA",
"path": "io/skippy/core/nested/ClassA.class",
"outputFolder": "src/test/resources",
"hash": "3041DC84"
},
"1": {
"name": "com.example.ClassB",
"path": "io/skippy/core/nested/ClassB.class",
"outputFolder": "src/test/resources",
"hash": "37FA6DE7"
},
"2": {
"name": "com.example.ClassC",
"path": "io/skippy/core/nested/ClassC.class",
"outputFolder": "src/test/resources",
"hash": "B17DF734"
},
"3": {
"name": "com.example.ClassD",
"path": "io/skippy/core/nested/ClassD.class",
"outputFolder": "src/test/resources",
"hash": "41C10CEE"
},
"4": {
"name": "com.example.NestedTestsTest",
"path": "io/skippy/core/nested/NestedTestsTest.class",
"outputFolder": "src/test/resources",
"hash": "C3A3737F"
},
"5": {
"name": "com.example.NestedTestsTest$Level2BarTest",
"path": "io/skippy/core/nested/NestedTestsTest$Level2BarTest.class",
"outputFolder": "src/test/resources",
"hash": "5780C183"
},
"6": {
"name": "com.example.NestedTestsTest$Level2FooTest",
"path": "io/skippy/core/nested/NestedTestsTest$Level2FooTest.class",
"outputFolder": "src/test/resources",
"hash": "3E35762B"
},
"7": {
"name": "com.example.NestedTestsTest$Level2FooTest$Level3Test",
"path": "io/skippy/core/nested/NestedTestsTest$Level2FooTest$Level3Test.class",
"outputFolder": "src/test/resources",
"hash": "FA83D453"
}
},
"tests": [
{
"class": 4,
"result": "PASSED",
"coveredClasses": [0,1,2,3,4,5,6,7]
},
{
"class": 5,
"result": "PASSED",
"coveredClasses": [3,4,5]
},
{
"class": 6,
"result": "PASSED",
"coveredClasses": [1,2,4,6,7]
},
{
"class": 7,
"result": "PASSED",
"coveredClasses": [2,4,6,7]
}
]
}
""");
assertEquals(SKIP, testImpactAnalysis.predict("com.example.NestedTestsTest", SkippyConfiguration.DEFAULT, SkippyRepository.getInstance(SkippyConfiguration.DEFAULT)).prediction());
assertEquals(SKIP, testImpactAnalysis.predict("com.example.NestedTestsTest$Level2BarTest", SkippyConfiguration.DEFAULT, SkippyRepository.getInstance(SkippyConfiguration.DEFAULT)).prediction());
assertEquals(SKIP, testImpactAnalysis.predict("com.example.NestedTestsTest$Level2FooTest", SkippyConfiguration.DEFAULT, SkippyRepository.getInstance(SkippyConfiguration.DEFAULT)).prediction());
assertEquals(SKIP, testImpactAnalysis.predict("com.example.NestedTestsTest$Level2FooTest$Level3Test", SkippyConfiguration.DEFAULT, SkippyRepository.getInstance(SkippyConfiguration.DEFAULT)).prediction());
}

@Test
void testPredictChangeInClassCoveredByNestedTest() {
var testImpactAnalysis = TestImpactAnalysis.parse("""
{
"id": "F87679D196B903DFE24335295E13DEED",
"classes": {
"0": {
"name": "com.example.ClassA",
"path": "io/skippy/core/nested/ClassA.class",
"outputFolder": "src/test/resources",
"hash": "3041DC84"
},
"1": {
"name": "com.example.ClassB",
"path": "io/skippy/core/nested/ClassB.class",
"outputFolder": "src/test/resources",
"hash": "37FA6DE7"
},
"2": {
"name": "com.example.ClassC",
"path": "io/skippy/core/nested/ClassC.class",
"outputFolder": "src/test/resources",
"hash": "00000000"
},
"3": {
"name": "com.example.ClassD",
"path": "io/skippy/core/nested/ClassD.class",
"outputFolder": "src/test/resources",
"hash": "41C10CEE"
},
"4": {
"name": "com.example.NestedTestsTest",
"path": "io/skippy/core/nested/NestedTestsTest.class",
"outputFolder": "src/test/resources",
"hash": "C3A3737F"
},
"5": {
"name": "com.example.NestedTestsTest$Level2BarTest",
"path": "io/skippy/core/nested/NestedTestsTest$Level2BarTest.class",
"outputFolder": "src/test/resources",
"hash": "5780C183"
},
"6": {
"name": "com.example.NestedTestsTest$Level2FooTest",
"path": "io/skippy/core/nested/NestedTestsTest$Level2FooTest.class",
"outputFolder": "src/test/resources",
"hash": "3E35762B"
},
"7": {
"name": "com.example.NestedTestsTest$Level2FooTest$Level3Test",
"path": "io/skippy/core/nested/NestedTestsTest$Level2FooTest$Level3Test.class",
"outputFolder": "src/test/resources",
"hash": "FA83D453"
}
},
"tests": [
{
"class": 4,
"result": "PASSED",
"coveredClasses": [0,1,2,3,4,5,6,7]
},
{
"class": 5,
"result": "PASSED",
"coveredClasses": [3,4,5]
},
{
"class": 6,
"result": "PASSED",
"coveredClasses": [1,2,4,6,7]
},
{
"class": 7,
"result": "PASSED",
"coveredClasses": [2,4,6,7]
}
]
}
""");
assertEquals(EXECUTE, testImpactAnalysis.predict("com.example.NestedTestsTest", SkippyConfiguration.DEFAULT, SkippyRepository.getInstance(SkippyConfiguration.DEFAULT)).prediction());
assertEquals(SKIP, testImpactAnalysis.predict("com.example.NestedTestsTest$Level2BarTest", SkippyConfiguration.DEFAULT, SkippyRepository.getInstance(SkippyConfiguration.DEFAULT)).prediction());
assertEquals(EXECUTE, testImpactAnalysis.predict("com.example.NestedTestsTest$Level2FooTest", SkippyConfiguration.DEFAULT, SkippyRepository.getInstance(SkippyConfiguration.DEFAULT)).prediction());
assertEquals(EXECUTE, testImpactAnalysis.predict("com.example.NestedTestsTest$Level2FooTest$Level3Test", SkippyConfiguration.DEFAULT, SkippyRepository.getInstance(SkippyConfiguration.DEFAULT)).prediction());
}

}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 3ecdbc4

Please sign in to comment.