Skip to content

Commit f57a2e0

Browse files
authored
Rerun test task when test jdk crashed with System exit (#71881) (#72003)
Related to #52610 this PR introduces a rerun of all tests for a test task if the test jvm has crashed because of a system exit. We furthermore log potential tests that caused the System.exit based on which tests have been active at the time of the system exit. We also modified the build scan logic to track unexpected test jvm exists with the tag `unexpected-test-jvm-exit`
1 parent 2b14491 commit f57a2e0

File tree

10 files changed

+608
-0
lines changed

10 files changed

+608
-0
lines changed

build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ if (VersionProperties.elasticsearch.toString().endsWith('-SNAPSHOT')) {
5656
String elasticLicenseUrl = "https://raw.githubusercontent.com/elastic/elasticsearch/${licenseCommit}/licenses/ELASTIC-LICENSE-2.0.txt"
5757

5858
subprojects {
59+
apply plugin:'elasticsearch.internal-test-rerun'
60+
5961
// Default to the SSPL+Elastic dual license
6062
project.ext.projectLicenses = [
6163
'Server Side Public License, v 1': 'https://www.mongodb.com/licensing/server-side-public-license',
@@ -102,6 +104,7 @@ subprojects {
102104
project.licenseFile = project.rootProject.file('licenses/SSPL-1.0+ELASTIC-LICENSE-2.0.txt')
103105
project.noticeFile = project.rootProject.file('NOTICE.txt')
104106
}
107+
105108
}
106109

107110
/**

buildSrc/src/integTest/groovy/org/elasticsearch/gradle/fixtures/AbstractGradleFuncTest.groovy

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ abstract class AbstractGradleFuncTest extends Specification {
6767
return input.readLines()
6868
.collect { it.replace('\\', '/') }
6969
.collect {it.replace(normalizedPathPrefix , '.') }
70+
.collect {it.replaceAll(/Gradle Test Executor \d/ , 'Gradle Test Executor 1') }
7071
.join("\n")
7172
}
7273

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.gradle.internal.test.rerun
10+
11+
import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest
12+
13+
class InternalTestRerunPluginFuncTest extends AbstractGradleFuncTest {
14+
15+
def "does not rerun on failed tests"() {
16+
when:
17+
buildFile.text = """
18+
plugins {
19+
id 'java'
20+
id 'elasticsearch.internal-test-rerun'
21+
}
22+
23+
repositories {
24+
mavenCentral()
25+
}
26+
27+
dependencies {
28+
testImplementation 'junit:junit:4.13.1'
29+
}
30+
31+
tasks.named("test").configure {
32+
maxParallelForks = 4
33+
testLogging {
34+
events "standard_out", "failed"
35+
exceptionFormat "short"
36+
}
37+
}
38+
39+
"""
40+
createTest("SimpleTest")
41+
createTest("SimpleTest2")
42+
createTest("SimpleTest3")
43+
createTest("SimpleTest4")
44+
createTest("SimpleTest5")
45+
createFailedTest("SimpleTest6")
46+
createFailedTest("SimpleTest7")
47+
createFailedTest("SimpleTest8")
48+
createTest("SomeOtherTest")
49+
createTest("SomeOtherTest1")
50+
createTest("SomeOtherTest2")
51+
createTest("SomeOtherTest3")
52+
createTest("SomeOtherTest4")
53+
createTest("SomeOtherTest5")
54+
then:
55+
def result = gradleRunner("test").buildAndFail()
56+
result.output.contains("total executions: 2") == false
57+
and: "no jvm system exit tracing provided"
58+
normalized(result.output).contains("""Test jvm exited unexpectedly.
59+
Test jvm system exit trace:""") == false
60+
}
61+
62+
def "all tests are rerun when test jvm has crashed"() {
63+
when:
64+
settingsFile.text = """
65+
plugins {
66+
id "com.gradle.enterprise" version "3.6.1"
67+
}
68+
gradleEnterprise {
69+
server = 'https://gradle-enterprise.elastic.co/'
70+
}
71+
""" + settingsFile.text
72+
73+
buildFile.text = """
74+
plugins {
75+
id 'java'
76+
id 'elasticsearch.internal-test-rerun'
77+
}
78+
79+
repositories {
80+
mavenCentral()
81+
}
82+
83+
dependencies {
84+
testImplementation 'junit:junit:4.13.1'
85+
}
86+
87+
tasks.named("test").configure {
88+
maxParallelForks = 4
89+
testLogging {
90+
// set options for log level LIFECYCLE
91+
events "started", "passed", "standard_out", "failed"
92+
exceptionFormat "short"
93+
}
94+
}
95+
96+
"""
97+
createTest("AnotherTest")
98+
createFailedTest("AnotherTest1")
99+
createTest("AnotherTest2")
100+
createTest("AnotherTest3")
101+
createTest("AnotherTest4")
102+
createTest("AnotherTest5")
103+
createSystemExitTest("AnotherTest6")
104+
createTest("AnotherTest7")
105+
createTest("AnotherTest8")
106+
createFailedTest("AnotherTest9")
107+
createTest("AnotherTest10")
108+
createTest("SimpleTest")
109+
createTest("SimpleTest2")
110+
createTest("SimpleTest3")
111+
createTest("SimpleTest4")
112+
createTest("SimpleTest5")
113+
createTest("SimpleTest6")
114+
createTest("SimpleTest7")
115+
createTest("SimpleTest8")
116+
createTest("SomeOtherTest")
117+
then:
118+
def result = gradleRunner("test").build()
119+
result.output.contains("AnotherTest6 total executions: 2")
120+
// triggered only in the second overall run
121+
and: 'Tracing is provided'
122+
normalized(result.output).contains("""================
123+
Test jvm exited unexpectedly.
124+
Test jvm system exit trace (run: 1)
125+
Gradle Test Executor 1 > AnotherTest6 > someTest
126+
================""")
127+
}
128+
129+
def "reruns tests till max rerun count is reached"() {
130+
when:
131+
buildFile.text = """
132+
plugins {
133+
id 'java'
134+
id 'elasticsearch.internal-test-rerun'
135+
}
136+
137+
repositories {
138+
mavenCentral()
139+
}
140+
141+
dependencies {
142+
testImplementation 'junit:junit:4.13.1'
143+
}
144+
145+
tasks.named("test").configure {
146+
rerun {
147+
maxReruns = 4
148+
}
149+
testLogging {
150+
// set options for log level LIFECYCLE
151+
events "standard_out", "failed"
152+
exceptionFormat "short"
153+
}
154+
}
155+
"""
156+
createSystemExitTest("JdkKillingTest", 5)
157+
then:
158+
def result = gradleRunner("test").buildAndFail()
159+
result.output.contains("JdkKillingTest total executions: 4")
160+
result.output.contains("Max retries(4) hit")
161+
and: 'Tracing is provided'
162+
normalized(result.output).contains("Test jvm system exit trace (run: 1)")
163+
normalized(result.output).contains("Test jvm system exit trace (run: 2)")
164+
normalized(result.output).contains("Test jvm system exit trace (run: 3)")
165+
normalized(result.output).contains("Test jvm system exit trace (run: 4)")
166+
}
167+
168+
private String testMethodContent(boolean withSystemExit, boolean fail, int timesFailing = 1) {
169+
return """
170+
int count = countExecutions();
171+
System.out.println(getClass().getSimpleName() + " total executions: " + count);
172+
173+
${withSystemExit ? """
174+
if(count <= ${timesFailing}) {
175+
System.exit(1);
176+
}
177+
""" : ''
178+
}
179+
180+
${fail ? """
181+
if(count <= ${timesFailing}) {
182+
Assert.fail();
183+
}
184+
""" : ''
185+
}
186+
"""
187+
}
188+
189+
private File createSystemExitTest(String clazzName, timesFailing = 1) {
190+
createTest(clazzName, testMethodContent(true, false, timesFailing))
191+
}
192+
private File createFailedTest(String clazzName) {
193+
createTest(clazzName, testMethodContent(false, true, 1))
194+
}
195+
196+
private File createTest(String clazzName, String content = testMethodContent(false, false, 1)) {
197+
file("src/test/java/org/acme/${clazzName}.java") << """
198+
import org.junit.Test;
199+
import org.junit.Before;
200+
import org.junit.After;
201+
import org.junit.Assert;
202+
import java.nio.*;
203+
import java.nio.file.*;
204+
import java.io.IOException;
205+
206+
public class $clazzName {
207+
Path executionLogPath = Paths.get("test-executions" + getClass().getSimpleName() +".log");
208+
209+
@Before
210+
public void beforeTest() {
211+
logExecution();
212+
}
213+
214+
@After
215+
public void afterTest() {
216+
}
217+
218+
@Test
219+
public void someTest() {
220+
${content}
221+
}
222+
223+
int countExecutions() {
224+
try {
225+
return Files.readAllLines(executionLogPath).size();
226+
}
227+
catch(IOException e) {
228+
return 0;
229+
}
230+
}
231+
232+
void logExecution() {
233+
try {
234+
Files.write(executionLogPath, "Test executed\\n".getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
235+
} catch (IOException e) {
236+
// exception handling
237+
}
238+
}
239+
}
240+
"""
241+
}
242+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.gradle.internal.test.rerun;
10+
11+
import org.gradle.api.Plugin;
12+
import org.gradle.api.Project;
13+
import org.gradle.api.model.ObjectFactory;
14+
import org.gradle.api.tasks.testing.Test;
15+
16+
import javax.inject.Inject;
17+
18+
import static org.elasticsearch.gradle.internal.test.rerun.TestTaskConfigurer.configureTestTask;
19+
20+
public class TestRerunPlugin implements Plugin<Project> {
21+
22+
private final ObjectFactory objectFactory;
23+
24+
@Inject
25+
TestRerunPlugin(ObjectFactory objectFactory) {
26+
this.objectFactory = objectFactory;
27+
}
28+
29+
@Override
30+
public void apply(Project project) {
31+
project.getTasks().withType(Test.class).configureEach(task -> configureTestTask(task, objectFactory));
32+
}
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.gradle.internal.test.rerun;
10+
11+
import org.gradle.api.model.ObjectFactory;
12+
import org.gradle.api.provider.Property;
13+
import org.gradle.api.tasks.testing.Test;
14+
15+
import javax.inject.Inject;
16+
17+
/**
18+
* Allows configuring test rerun mechanics.
19+
* <p>
20+
* This extension is added with the name 'rerun' to all {@link Test} tasks.
21+
*/
22+
public class TestRerunTaskExtension {
23+
24+
/**
25+
* The default number of reruns we allow for a test task.
26+
*/
27+
public static final Integer DEFAULT_MAX_RERUNS = 1;
28+
29+
/**
30+
* The name of the extension added to each test task.
31+
*/
32+
public static String NAME = "rerun";
33+
34+
private final Property<Integer> maxReruns;
35+
36+
private final Property<Boolean> didRerun;
37+
38+
@Inject
39+
public TestRerunTaskExtension(ObjectFactory objects) {
40+
this.maxReruns = objects.property(Integer.class).convention(DEFAULT_MAX_RERUNS);
41+
this.didRerun = objects.property(Boolean.class).convention(Boolean.FALSE);
42+
}
43+
44+
/**
45+
* The maximum number of times to rerun all tests.
46+
* <p>
47+
* This setting defaults to {@code 0}, which results in no retries.
48+
* Any value less than 1 disables rerunning.
49+
*
50+
* @return the maximum number of times to rerun all tests of a task
51+
*/
52+
public Property<Integer> getMaxReruns() {
53+
return maxReruns;
54+
}
55+
56+
/**
57+
/**
58+
* @return whether tests tests have been rerun or not. Defaults to false.
59+
*/
60+
public Property<Boolean> getDidRerun() {
61+
return didRerun;
62+
}
63+
64+
}

0 commit comments

Comments
 (0)