From 3ecdbc433f8df4f2d2038ec1e437d2864b142ac8 Mon Sep 17 00:00:00 2001 From: Florian McKee <84742327+fmck3516@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:35:12 -0500 Subject: [PATCH] issues/163: Support for JUnit 5s @Nested test classes issues/163: Support for JUnit 5s @Nested test classes --- .../java/io/skippy/core/SkippyTestApi.java | 73 ++++++- ...tImpactAnalysisPredictNestedTestsTest.java | 197 ++++++++++++++++++ .../io/skippy/core/nested/ClassA.class | Bin 0 -> 816 bytes .../io/skippy/core/nested/ClassB.class | Bin 0 -> 816 bytes .../io/skippy/core/nested/ClassC.class | Bin 0 -> 816 bytes .../io/skippy/core/nested/ClassD.class | Bin 0 -> 816 bytes .../NestedTestsTest$Level2BarTest.class | Bin 0 -> 945 bytes ...edTestsTest$Level2FooTest$Level3Test.class | Bin 0 -> 1063 bytes .../NestedTestsTest$Level2FooTest.class | Bin 0 -> 1024 bytes .../skippy/core/nested/NestedTestsTest.class | Bin 0 -> 987 bytes 10 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 skippy-core/src/test/java/io/skippy/core/TestImpactAnalysisPredictNestedTestsTest.java create mode 100644 skippy-core/src/test/resources/io/skippy/core/nested/ClassA.class create mode 100644 skippy-core/src/test/resources/io/skippy/core/nested/ClassB.class create mode 100644 skippy-core/src/test/resources/io/skippy/core/nested/ClassC.class create mode 100644 skippy-core/src/test/resources/io/skippy/core/nested/ClassD.class create mode 100644 skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest$Level2BarTest.class create mode 100644 skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest$Level2FooTest$Level3Test.class create mode 100644 skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest$Level2FooTest.class create mode 100644 skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest.class diff --git a/skippy-core/src/main/java/io/skippy/core/SkippyTestApi.java b/skippy-core/src/main/java/io/skippy/core/SkippyTestApi.java index ef5f7579..94d32d7e 100644 --- a/skippy-core/src/main/java/io/skippy/core/SkippyTestApi.java +++ b/skippy-core/src/main/java/io/skippy/core/SkippyTestApi.java @@ -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. @@ -48,6 +51,47 @@ public final class SkippyTestApi { private final SkippyConfiguration skippyConfiguration; private final Map predictions = new ConcurrentHashMap<>(); + /** + * Stack that keeps track of the execution data across nested test classes. + *

+ * Example: + *
+     * {@literal @}PredictWithSkippy
+     *  public class Level1 {
+     *
+     *     {@literal @}Nested
+     *      class Level2 {
+     *
+     *         {@literal @}Nested
+     *          class Level3 {
+     *
+     *             {@literal @}Test
+     *              void testSomething() {
+     *              }
+     *
+     *         }
+     *
+     *     }
+     *
+     * }
+     * 
+ * + * By the time testSomething is executed, the stack would be populated as follows: + *

+ *
+     *  frame 2 = { execution data for Level1$Level2$Level3.class }
+     *  frame 1 = { execution data for Level1$Level2.class }
+     *  frame 0 = { execution data for Level1.class }
+     *  
+ * + * The stack is used for two purposes: + * + */ + private final Stack> executionDataStack = new Stack<>(); + private SkippyTestApi(TestImpactAnalysis testImpactAnalysis, SkippyConfiguration skippyConfiguration, SkippyRepository skippyRepository) { this.testImpactAnalysis = testImpactAnalysis; this.skippyRepository = skippyRepository; @@ -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. * @@ -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); + } }); }); } -} \ No newline at end of file + private boolean isNestedTest() { + return ! executionDataStack.isEmpty(); + } + + private void addExecutionDataToParent(List executionData) { + var parent = executionDataStack.lastElement(); + parent.addAll(executionData); + } + +} diff --git a/skippy-core/src/test/java/io/skippy/core/TestImpactAnalysisPredictNestedTestsTest.java b/skippy-core/src/test/java/io/skippy/core/TestImpactAnalysisPredictNestedTestsTest.java new file mode 100644 index 00000000..8d11aea8 --- /dev/null +++ b/skippy-core/src/test/java/io/skippy/core/TestImpactAnalysisPredictNestedTestsTest.java @@ -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()); + } + +} \ No newline at end of file diff --git a/skippy-core/src/test/resources/io/skippy/core/nested/ClassA.class b/skippy-core/src/test/resources/io/skippy/core/nested/ClassA.class new file mode 100644 index 0000000000000000000000000000000000000000..a74eaba72a57c989a446c9a62e971072a35967f2 GIT binary patch literal 816 zcmaJ<&2G~`5dJ1f-Y_JGAXfCP~$kPIABl|xS(XVq-u>{|AwqHn@u zzzL}b9)O2J%*K(D$`zJ8GduIm>^C#}=kL{T0B^A8qlUVNMt~JG32W!#LPV+1XVI6z zxlAmf`C4gZ-xBKk2PYwCwF2CNPuQG@vFsV02>Vsp5nF{7+7^Vp{diHQZ*!&3x(8L4 zhky_!W)jJ7Vv?mY>ZPJ6-Vqu-Gn9mOthD?*oeX4tECwli!q_AtJrTKb=ee_CM~ZLV zh^wW#%wJ@g)I-9G(%F<73ad;Ae&0;?)3%WBEvrE7v`^4=I**j!|vq#c=I(QabP z$3r~w@Yv1b38Ay-SLq8gmQmSuIi4etSd(88zAq1E4lIj)5PF!(q7xf4o@U)D#U;{fcaiL!?dVf+73 zUPjp;o?(?Ap8K5Srw77qwj$Q#JcqwPKWE`PEL|9=qs=lX0a(L2pIQjvv&SXBV(oC= s;T1gK2b(o4(wj@^9`0g;<8|Ddq3&aob&Jy<@W0LcFf02UJJ`k6Kjg{G7XSbN literal 0 HcmV?d00001 diff --git a/skippy-core/src/test/resources/io/skippy/core/nested/ClassB.class b/skippy-core/src/test/resources/io/skippy/core/nested/ClassB.class new file mode 100644 index 0000000000000000000000000000000000000000..8976974f4df8b9e3f9b37e49100ffa02ccdad272 GIT binary patch literal 816 zcmaJ<&2G~`5dJ1f{|AwqHn@u zzzL}b9)O2J%*K(D$`zJ8GduIm>^C#}=kN7z0B^DHqlUVNMt~JG32PVPQbeiH=h5fE zg-k4=`9^7F-x2Buhi4&YwF2CMPuQA>vFsV02>V6Z5nF{7+7^WUgLqM=Z*!&3yN6Ym zhky_!W)jJ7Vv?mY>ZPJ6jtPyP8A?JsR$6|VP6jeR6@!#LVQdnSo{3z!^W536BgMCF z#??|?<}b2L>LFo8>1@gkg;gd5zi*~_B2SdNWwqq-$~D0=IX1=?HW%4PX-8&Qw42!W z@d%GSJaMylO6V;5Rr=D5WmL9Zj^{)q*5p@&@5_Uk1IwcCg&wA|=)}g1r&+g3amjNd z@$@Q_x4^$H;mb|l*ucC*Po!yIS=nu2mrx@feegkRna>6w3lA>{dzId9qwJwW*!e${ zmr?eIXISNj=RPO->49*Ut%x-_&(SZ?&sq2$OBV*}XtNAT0M@Y1rxrr^>~YDjSv#C} sbPW&q!DbDM^yX5!hYf6UypH=b)B|j>ZgKiU{&$!kW@Vq_CHAoW5A4Ux8vpR=YYs_1*6N0}^pi7}H|-mgAtZe9&4J^8v`x!*jxJt+(qad*~5%{txA4 zRQ=&8*7@PN&q;oIAlzgtVolC-_zU!N9=^rWg@GozEQ1Pw4cz8a2O)g+xa3!?9nL$v rga`a!vw>xLdnMh&9c*&EiMw;uJ#4Y=aQc1zcbFgMWuM^%cCq~r^u^2{ literal 0 HcmV?d00001 diff --git a/skippy-core/src/test/resources/io/skippy/core/nested/ClassD.class b/skippy-core/src/test/resources/io/skippy/core/nested/ClassD.class new file mode 100644 index 0000000000000000000000000000000000000000..01673ad17536a7cdc267c0e32d660d79489e0ebf GIT binary patch literal 816 zcmaJ<&2G~`5dJ1f-Y_JGAXz=5C&Bm;+3<`_0V$`Fr&nz#HuOsG;tm5nu&P!rHmG5K$`hS@dOa zE)z>=zE)b2z3?-o*D=j}yCj*%ui$ThsFgA%uPeiWVdG2i3k>Xo7 z;%ccb^A}kr^^mZlbT;LN!YUJj-#61dkw?nivRd+Z>6+k~yfelYHW%3^X-8&Qw42!S z@eq$ZJa)5qLg*~|Rr6H^5Jq@Z~12ZD3xaC(^X9tn9Y%lu#oceeh9hna>6w3lA>{yOrK49*Yt%x-_&*3l7&sq2mOBV*}XtNAT0M@Y1rxrr^>~YDjSUa3| scm)sm!DbDM^yX5!hr8I|cpdjfgc5Ph4bvFkWAHKkD6(n3QUJ5VETC{U@iAdsvgkt+91vPw5{*2W(de~Sa6 z;=m8!MV{N^YhpD9{^tAUdJBs}Qs&)Tv;D(sa4Ube5^4zUDsupiPS10`u^MPK%f&cz6o@ E27z_wYybcN literal 0 HcmV?d00001 diff --git a/skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest$Level2FooTest$Level3Test.class b/skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest$Level2FooTest$Level3Test.class new file mode 100644 index 0000000000000000000000000000000000000000..110496d18dd7521af122ddf54e46f3740968432f GIT binary patch literal 1063 zcmb7DZEq4m5Pp_YjviNwXRY?dwkoz9Sk9_3F=&&9*wo}w6KVLqz=ke4cJc0L{9Ar7 zHh%C2_@j)o2L@VSQa|kM%(FAk?9R-7|MB%3fY;bxKn8ObvJU2vV|d=t17CdPgJCHA z6A>q(drC4kq7;a85k5WAdTKp0Dnl+glW}REp&DG9pl&0N1q-%=0vv{7pPzF-zF_+dAs`{SjS-TL~0c?IFyKt zHy=hkBy)E*&-gXd9NBM3l~eh%M9@ynpy# zo^E>irD><@r*>KwY}2R7)Bj<`qWLzYWoQ*}5A+1m4#+l$U8#Nst9*g=DfQVRnIj)y zjW~dnu@~;+0a@gqvYGRTaOli%y`l~+?kTuDZwTMZl#3I*mXi!qA-*0 h5$##*kj!8gztA$-UW!%ln0A*=_fUbKVtaT3`~-eQ4mAJ( literal 0 HcmV?d00001 diff --git a/skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest$Level2FooTest.class b/skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest$Level2FooTest.class new file mode 100644 index 0000000000000000000000000000000000000000..8d30a97d966d79b944e78fd0affbb6316d289a78 GIT binary patch literal 1024 zcmaJ=+iuf95Iviwaq2iUn37W3LV(cb0(H5(pg^Uzq5{b(5~=dONml7b&Tg=sir?Y^ zQSrbB@KK0aJE%#4Ja~4_*_kumncbhizW)I52G3TI!IFingJt9x>ODR3#AiMl2f{lQ zQ7rmrWFup$zPJ#<^AoKNV#vkkGOF$~Z2JqcS~l`nv0ytWz+os2`33g^t_I$R?ojlM z&}*q=e88|&t9SCaLCm=b0)3Q6iD5bMZMe8)Va>s9lo{OFipK$uq9e-UTWpIGv2TB0S^WfEd@;Jsxy;C=H&HS<`%m z<|5hofwdTlF%7P*M}m^ofMMg~M8$F>Ix>j%ky`&=DG?^47Ig69+-0=jF}z%mKi`TJ zy-2UIohT&$6?zSMdKj!!GVg)B41EP`gHA)<0b#?~jpkRd#y41Bl9>Bsa})!*r4_Q} zGzt&!kPyWuYG(ft9NIHluV^()!7aM0m$-R}^(!$u5hzpQdLrngp;N&srI~V%Y0YAX dYzDjd!`2Ab6Rv?Lw7RsLK?6_8-y?4y_zhCa|E2%{ literal 0 HcmV?d00001 diff --git a/skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest.class b/skippy-core/src/test/resources/io/skippy/core/nested/NestedTestsTest.class new file mode 100644 index 0000000000000000000000000000000000000000..18b1cfa566d2d0cce6d9b69af72bc6dce60077c9 GIT binary patch literal 987 zcma)5*>2N76g`v8!Eu@r3Z)wq2u)m|mRKH804Y>dLNW!BDm+h;Q8K}qF}72|XYoYE z10TRgA?`SahD8NSzBA`8=bk$rfBp9P3xLP)Y~+x)P;gL0iD7xdkGU6cHS%8eCt{E= zlpaYXlP3)MmfI_%OrEg_0^KRYW+7EvO#iWwYg zMBv%G84n0H7yHboX)m?L>wbwJ1$f@YbSQ1%BDV^|av`zo- z;~RNIWq0)~8i)gFG_Cw5f6tsYL&KNai{HvH{4hVq2i~hl4CNqsBa?AA4G>kuRKE~r z#@NCRgL}pl4PP9K;Od{}lwK`CM_v6z z$4{J9()pzEv=-N}N_XBoInez@iR>C7k5)!&arYD0$27c7)`S5Y8)Th%0yeQl2)IJg zTw44vB?BsTtA6eTl@pvlr7-^(utX%a1)54mbCqJI$u_M8QfG8s#}Drf!VP-M$m1rO XaB+*!dWu$YoAynb=MJqd?e>80V7>S@ literal 0 HcmV?d00001