Skip to content

Commit d5e9f9f

Browse files
Keep parser test sessions alive through semantic assertions
1 parent 87270f2 commit d5e9f9f

98 files changed

Lines changed: 708 additions & 110 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎java-checks-common/src/test/java/org/sonar/java/checks/helpers/ExpressionsHelperTest.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.junit.jupiter.params.ParameterizedTest;
2323
import org.junit.jupiter.params.provider.CsvSource;
2424
import org.junit.jupiter.params.provider.ValueSource;
25+
import org.sonar.java.test.TestResourceScope;
2526
import org.sonar.plugins.java.api.semantic.Symbol;
2627
import org.sonar.plugins.java.api.semantic.Type;
2728
import org.sonar.plugins.java.api.tree.CompilationUnitTree;
@@ -37,6 +38,7 @@
3738
import static org.mockito.Mockito.mock;
3839
import static org.mockito.Mockito.when;
3940

41+
@TestResourceScope.Scoped
4042
class ExpressionsHelperTest extends JParserTestUtils {
4143

4244
@Test

‎java-checks-common/src/test/java/org/sonar/java/checks/helpers/JParserTestUtils.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
import java.util.List;
2525
import java.util.stream.Collectors;
2626

27-
import org.sonar.java.model.JParser;
27+
import org.sonar.java.model.FrontendParser;
2828
import org.sonar.java.model.JParserConfig;
29+
import org.sonar.java.test.TestResourceScope;
2930
import org.sonar.plugins.java.api.JavaVersion;
3031
import org.sonar.plugins.java.api.tree.*;
3132

33+
@TestResourceScope.Scoped
3234
public abstract class JParserTestUtils {
3335

3436
static IdentifierTree variableFromLastReturnStatement(List<StatementTree> statements) {
@@ -77,7 +79,7 @@ public static CompilationUnitTree parse(File file) {
7779
public static CompilationUnitTree parse(String source) {
7880
List<File> classpath = Arrays.asList(new File("target/test-classes"), new File("target/classes"));
7981
JavaVersion version = JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION;
80-
return JParser.parse(JParserConfig.Mode.FILE_BY_FILE.create(version, classpath).astParser(), version.toString(), "test", source);
82+
return TestResourceScope.retain(FrontendParser.parse(version, classpath, "test", source)).tree();
8183
}
8284

8385
}

‎java-checks-common/src/test/java/org/sonar/java/checks/helpers/ReassignmentFinderTest.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
import java.util.List;
2121

2222
import org.junit.jupiter.api.Test;
23+
import org.sonar.java.test.TestResourceScope;
2324
import org.sonar.plugins.java.api.semantic.Symbol;
2425
import org.sonar.plugins.java.api.tree.*;
2526

2627
import static java.lang.reflect.Modifier.isFinal;
2728
import static java.lang.reflect.Modifier.isPrivate;
2829
import static org.assertj.core.api.Assertions.assertThat;
2930

31+
@TestResourceScope.Scoped
3032
class ReassignmentFinderTest extends JParserTestUtils {
3133

3234
@Test

‎java-checks-common/src/test/java/org/sonar/java/checks/helpers/TreeHelperTest.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.junit.jupiter.params.provider.Arguments;
2525
import org.junit.jupiter.params.provider.MethodSource;
2626
import org.sonar.java.model.statement.BlockTreeImpl;
27+
import org.sonar.java.test.TestResourceScope;
2728
import org.sonar.plugins.java.api.tree.BlockTree;
2829
import org.sonar.plugins.java.api.tree.ExpressionStatementTree;
2930
import org.sonar.plugins.java.api.tree.ForEachStatement;
@@ -36,6 +37,7 @@
3637

3738
import static org.assertj.core.api.Assertions.assertThat;
3839

40+
@TestResourceScope.Scoped
3941
class TreeHelperTest extends JParserTestUtils {
4042

4143
@Test

‎java-checks-test-sources/test-classpath-reader/pom.xml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
<description>Code Analyzer for Java :: Test Classpath Reader</description>
1515

1616
<dependencies>
17+
<dependency>
18+
<groupId>org.junit.jupiter</groupId>
19+
<artifactId>junit-jupiter-api</artifactId>
20+
<scope>provided</scope>
21+
</dependency>
1722
<dependency>
1823
<groupId>com.google.code.findbugs</groupId>
1924
<artifactId>jsr305</artifactId>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.test;
18+
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Inherited;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
import java.util.ArrayDeque;
25+
import java.util.Deque;
26+
import org.junit.jupiter.api.extension.AfterAllCallback;
27+
import org.junit.jupiter.api.extension.BeforeAllCallback;
28+
import org.junit.jupiter.api.extension.ExtendWith;
29+
import org.junit.jupiter.api.extension.ExtensionContext;
30+
import org.junit.jupiter.api.parallel.Execution;
31+
import org.junit.jupiter.api.parallel.ExecutionMode;
32+
33+
/** Retains lazy fixtures through instance initialization, nested tests and all assertions in a test class. */
34+
public final class TestResourceScope implements BeforeAllCallback, AfterAllCallback {
35+
36+
private static final ThreadLocal<Deque<Scope>> SCOPES = ThreadLocal.withInitial(ArrayDeque::new);
37+
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(TestResourceScope.class);
38+
39+
@Inherited
40+
@Retention(RetentionPolicy.RUNTIME)
41+
@Target(ElementType.TYPE)
42+
@ExtendWith(TestResourceScope.class)
43+
@Execution(ExecutionMode.SAME_THREAD)
44+
public @interface Scoped {
45+
}
46+
47+
public static Scope open() {
48+
Scope scope = new Scope();
49+
SCOPES.get().push(scope);
50+
return scope;
51+
}
52+
53+
public static <T extends AutoCloseable> T retain(T resource) {
54+
Scope scope = SCOPES.get().peek();
55+
if (scope == null) {
56+
IllegalStateException failure = new IllegalStateException("Parser fixtures require @TestResourceScope.Scoped or an explicit scope");
57+
try {
58+
resource.close();
59+
} catch (Exception cleanupFailure) {
60+
failure.addSuppressed(cleanupFailure);
61+
}
62+
throw failure;
63+
}
64+
scope.resources.push(resource);
65+
return resource;
66+
}
67+
68+
@Override
69+
public void beforeAll(ExtensionContext context) {
70+
context.getStore(NAMESPACE).put("scope", open());
71+
}
72+
73+
@Override
74+
public void afterAll(ExtensionContext context) throws Exception {
75+
Scope scope = context.getStore(NAMESPACE).remove("scope", Scope.class);
76+
if (scope != null) {
77+
scope.close();
78+
}
79+
}
80+
81+
public static final class Scope implements AutoCloseable {
82+
private final Deque<AutoCloseable> resources = new ArrayDeque<>();
83+
private boolean closed;
84+
85+
private Scope() {
86+
}
87+
88+
/** Attempts every fixture even after errors, then rethrows the first failure with later failures suppressed. */
89+
@Override
90+
public void close() throws Exception {
91+
if (closed) {
92+
return;
93+
}
94+
if (SCOPES.get().peek() != this) {
95+
throw new IllegalStateException("Test resource scopes must close in reverse order on their owning thread");
96+
}
97+
closed = true;
98+
SCOPES.get().pop();
99+
if (SCOPES.get().isEmpty()) {
100+
SCOPES.remove();
101+
}
102+
Throwable failure = null;
103+
try {
104+
while (!resources.isEmpty()) {
105+
try {
106+
resources.pop().close();
107+
} catch (Exception | Error e) {
108+
if (failure == null) {
109+
failure = e;
110+
} else if (failure != e) {
111+
failure.addSuppressed(e);
112+
}
113+
}
114+
}
115+
} finally {
116+
resources.clear();
117+
}
118+
if (failure instanceof Error error) {
119+
throw error;
120+
}
121+
if (failure instanceof Exception exception) {
122+
throw exception;
123+
}
124+
}
125+
}
126+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.test;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import org.junit.jupiter.api.Test;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
25+
26+
class TestResourceScopeTest {
27+
28+
@Test
29+
void nested_scopes_close_only_their_own_resources_after_assertions() throws Exception {
30+
List<String> calls = new ArrayList<>();
31+
try (TestResourceScope.Scope outer = TestResourceScope.open()) {
32+
TestResourceScope.retain(() -> calls.add("outer"));
33+
try (TestResourceScope.Scope inner = TestResourceScope.open()) {
34+
TestResourceScope.retain(() -> calls.add("inner"));
35+
assertThat(calls).isEmpty();
36+
}
37+
assertThat(calls).containsExactly("inner");
38+
}
39+
assertThat(calls).containsExactly("inner", "outer");
40+
}
41+
42+
@Test
43+
void scope_closes_every_resource_in_reverse_order_even_when_cleanup_fails() throws Exception {
44+
TestResourceScope.Scope scope = TestResourceScope.open();
45+
List<String> calls = new ArrayList<>();
46+
IllegalStateException first = new IllegalStateException("first");
47+
Exception second = new Exception("second");
48+
TestResourceScope.retain(() -> calls.add("last"));
49+
TestResourceScope.retain(() -> { calls.add("second"); throw second; });
50+
TestResourceScope.retain(() -> { calls.add("first"); throw first; });
51+
assertThatThrownBy(scope::close).isSameAs(first).hasSuppressedException(second);
52+
scope.close();
53+
assertThat(calls).containsExactly("first", "second", "last");
54+
}
55+
56+
@Test
57+
void scope_cleanup_attempts_every_resource_and_suppresses_errors() throws Exception {
58+
TestResourceScope.Scope scope = TestResourceScope.open();
59+
List<String> calls = new ArrayList<>();
60+
AssertionError failure = new AssertionError("cleanup");
61+
TestResourceScope.retain(() -> calls.add("last"));
62+
TestResourceScope.retain(() -> { throw failure; });
63+
Exception first = new Exception("first");
64+
TestResourceScope.retain(() -> { throw first; });
65+
66+
assertThatThrownBy(scope::close).isSameAs(first).hasSuppressedException(failure);
67+
scope.close();
68+
assertThat(calls).containsExactly("last");
69+
try (TestResourceScope.Scope next = TestResourceScope.open()) {
70+
TestResourceScope.retain(() -> calls.add("next"));
71+
}
72+
assertThat(calls).containsExactly("last", "next");
73+
}
74+
75+
@Test
76+
void scope_cleanup_rethrows_first_error_after_attempting_every_resource() throws Exception {
77+
TestResourceScope.Scope scope = TestResourceScope.open();
78+
List<String> calls = new ArrayList<>();
79+
AssertionError first = new AssertionError("first");
80+
Exception second = new Exception("second");
81+
TestResourceScope.retain(() -> calls.add("last"));
82+
TestResourceScope.retain(() -> { throw second; });
83+
TestResourceScope.retain(() -> { throw first; });
84+
TestResourceScope.retain(() -> { throw first; });
85+
86+
assertThatThrownBy(scope::close).isSameAs(first).hasSuppressedException(second);
87+
scope.close();
88+
assertThat(calls).containsExactly("last");
89+
}
90+
91+
@Test
92+
void missing_scope_fails_loudly_and_closes_the_unretained_fixture() {
93+
List<String> calls = new ArrayList<>();
94+
assertThatThrownBy(() -> TestResourceScope.retain(() -> calls.add("closed")))
95+
.isInstanceOf(IllegalStateException.class).hasMessageContaining("@TestResourceScope.Scoped");
96+
assertThat(calls).containsExactly("closed");
97+
}
98+
99+
@Test
100+
void missing_scope_preserves_cleanup_exceptions_as_suppressed() {
101+
Exception failure = new Exception("cleanup failed");
102+
assertThatThrownBy(() -> TestResourceScope.retain(() -> { throw failure; }))
103+
.isInstanceOf(IllegalStateException.class)
104+
.hasMessageContaining("@TestResourceScope.Scoped")
105+
.hasSuppressedException(failure);
106+
}
107+
108+
@Test
109+
void missing_scope_propagates_cleanup_errors() {
110+
AssertionError failure = new AssertionError("cleanup failed");
111+
assertThatThrownBy(() -> TestResourceScope.retain(() -> { throw failure; }))
112+
.isSameAs(failure);
113+
}
114+
}

‎java-checks/src/test/java/org/sonar/java/checks/ImportDeclarationOrderCheckTest.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import org.junit.jupiter.params.provider.MethodSource;
2626
import org.sonar.java.checks.helpers.JParserTestUtils;
2727
import org.sonar.java.checks.verifier.CheckVerifier;
28+
import org.sonar.java.test.TestResourceScope;
2829
import org.sonar.plugins.java.api.tree.CompilationUnitTree;
2930
import org.sonar.plugins.java.api.tree.ImportTree;
3031
import org.sonar.plugins.java.api.tree.Tree;
3132

3233
import static org.assertj.core.api.Assertions.assertThat;
3334
import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
3435

36+
@TestResourceScope.Scoped
3537
class ImportDeclarationOrderCheckTest {
3638
@Test
3739
@DisplayName("No issues with properly ordered imports including modules")

‎java-checks/src/test/java/org/sonar/java/checks/helpers/ClassPatternsUtilsTest.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818

1919
import org.junit.jupiter.api.Test;
2020
import org.sonar.java.model.JavaVersionImpl;
21+
import org.sonar.java.test.TestResourceScope;
2122
import org.sonar.plugins.java.api.JavaVersion;
2223
import org.sonar.plugins.java.api.tree.ClassTree;
2324
import org.sonar.plugins.java.api.tree.CompilationUnitTree;
2425

2526
import static org.junit.jupiter.api.Assertions.assertFalse;
2627
import static org.junit.jupiter.api.Assertions.assertTrue;
2728

29+
@TestResourceScope.Scoped
2830
class ClassPatternsUtilsTest {
2931

3032
@Test

‎java-checks/src/test/java/org/sonar/java/checks/helpers/DeprecatedCheckerHelperTest.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import org.junit.jupiter.params.ParameterizedTest;
2222
import org.junit.jupiter.params.provider.Arguments;
2323
import org.junit.jupiter.params.provider.MethodSource;
24+
import org.sonar.java.test.TestResourceScope;
2425
import org.sonar.plugins.java.api.tree.ClassTree;
2526
import org.sonar.plugins.java.api.tree.CompilationUnitTree;
2627

2728
import static org.assertj.core.api.Assertions.assertThat;
2829
import static org.junit.jupiter.api.Assertions.assertEquals;
2930

31+
@TestResourceScope.Scoped
3032
class DeprecatedCheckerHelperTest {
3133

3234
@Test

0 commit comments

Comments
 (0)