This is an automated email from the ASF dual-hosted git repository.
chia7712 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git
The following commit(s) were added to refs/heads/trunk by this push:
new 5f4cbd4aa42 KAFKA-17767 Automatically quarantine new tests [5/n]
(#17725)
5f4cbd4aa42 is described below
commit 5f4cbd4aa4239d887ad9f5d381181621bdb58e7c
Author: David Arthur <[email protected]>
AuthorDate: Mon Nov 18 20:56:36 2024 -0500
KAFKA-17767 Automatically quarantine new tests [5/n] (#17725)
Reviewers: Chia-Ping Tsai <[email protected]>
---
.github/actions/run-gradle/action.yml | 75 +++++++++
.github/scripts/junit.py | 52 ++++--
.github/workflows/build.yml | 54 +++----
.github/workflows/ci-complete.yml | 3 +-
build.gradle | 49 ++++--
.../java/org/apache/kafka/common/UuidTest.java | 2 +-
settings.gradle | 3 +-
.../src/{test => main}/resources/log4j.properties | 0
.../test/junit/AutoQuarantinedTestFilter.java | 172 ++++++++++++++++++++
.../test/junit/QuarantinedPostDiscoveryFilter.java | 87 ++++++++++
...rg.junit.platform.launcher.PostDiscoveryFilter} | 3 +-
.../src/main}/resources/junit-platform.properties | 1 +
.../test/junit/AutoQuarantinedTestFilterTest.java | 82 ++++++++++
.../junit/QuarantinedPostDiscoveryFilterTest.java | 175 +++++++++++++++++++++
14 files changed, 698 insertions(+), 60 deletions(-)
diff --git a/.github/actions/run-gradle/action.yml
b/.github/actions/run-gradle/action.yml
new file mode 100644
index 00000000000..a5bc4e55237
--- /dev/null
+++ b/.github/actions/run-gradle/action.yml
@@ -0,0 +1,75 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# http://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.
+#
+---
+name: "Gradle Setup"
+description: "Setup Java and Gradle"
+inputs:
+ # Composite actions do not support typed parameters. Everything is treated
as a string
+ # See: https://github.com/actions/runner/issues/2238
+ test-task:
+ description: "The test suite to run. Either 'test' or 'quarantinedTest'."
+ required: true
+ timeout-minutes:
+ description: "The timeout for the tests, in minutes."
+ required: true
+ test-catalog-path:
+ description: "The file path of the test catalog file."
+ required: true
+ build-scan-artifact-name:
+ description: "The name to use for archiving the build scan."
+ required: true
+outputs:
+ gradle-exitcode:
+ description: "The result of the Gradle test task."
+ value: ${{ steps.run-tests.outputs.exitcode }}
+runs:
+ using: "composite"
+ steps:
+ - name: Run JUnit Tests (${{ inputs.test-task }})
+ # Gradle flags
+ # --build-cache: Let Gradle restore the build cache
+ # --no-scan: Don't attempt to publish the scan yet. We want to
archive it first.
+ # --continue: Keep running even if a test fails
+ # -PcommitId Prevent the Git SHA being written into the jar files
(which breaks caching)
+ shell: bash
+ id: run-tests
+ env:
+ TIMEOUT_MINUTES: ${{ inputs.timeout-minutes}}
+ TEST_CATALOG: ${{ inputs.test-catalog-path }}
+ TEST_TASK: ${{ inputs.test-task }}
+ run: |
+ set +e
+ ./.github/scripts/thread-dump.sh &
+ timeout ${TIMEOUT_MINUTES}m ./gradlew --build-cache --continue
--no-scan \
+ -PtestLoggingEvents=started,passed,skipped,failed \
+ -PmaxParallelForks=2 \
+ -PmaxTestRetries=1 -PmaxTestRetryFailures=3 \
+ -PmaxQuarantineTestRetries=3 -PmaxQuarantineTestRetryFailures=0 \
+ -Pkafka.test.catalog.file=$TEST_CATALOG \
+ -PcommitId=xxxxxxxxxxxxxxxx \
+ $TEST_TASK
+ exitcode="$?"
+ echo "exitcode=$exitcode" >> $GITHUB_OUTPUT
+ - name: Archive build scan (${{ inputs.test-task }})
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ inputs.build-scan-artifact-name }}
+ path: ~/.gradle/build-scan-data
+ compression-level: 9
+ if-no-files-found: ignore
\ No newline at end of file
diff --git a/.github/scripts/junit.py b/.github/scripts/junit.py
index 48d2f528a26..2c7092e9145 100644
--- a/.github/scripts/junit.py
+++ b/.github/scripts/junit.py
@@ -142,6 +142,7 @@ def parse_report(workspace_path, report_path, fp) ->
Iterable[TestSuite]:
cur_suite: Optional[TestSuite] = None
partial_test_case = None
test_case_failed = False
+ test_case_skipped = False
for (event, elem) in xml.etree.ElementTree.iterparse(fp, events=["start",
"end"]):
if event == "start":
if elem.tag == "testsuite":
@@ -171,11 +172,12 @@ def parse_report(workspace_path, report_path, fp) ->
Iterable[TestSuite]:
elif elem.tag == "skipped":
skipped = partial_test_case(None, None, None)
cur_suite.skipped_tests.append(skipped)
+ test_case_skipped = True
else:
pass
elif event == "end":
if elem.tag == "testcase":
- if not test_case_failed:
+ if not test_case_failed and not test_case_skipped:
passed = partial_test_case(None, None, None)
cur_suite.passed_tests.append(passed)
partial_test_case = None
@@ -303,7 +305,7 @@ if __name__ == "__main__":
logger.debug(f"Found skipped test: {skipped_test}")
skipped_table.append((simple_class_name,
skipped_test.test_name))
- # Collect all tests that were run as part of quarantinedTest
+ # Only collect quarantined tests from the "quarantinedTest"
task
if task == "quarantinedTest":
for test in all_suite_passed.values():
simple_class_name = test.class_name.split(".")[-1]
@@ -329,53 +331,75 @@ if __name__ == "__main__":
# The stdout (print) goes to the workflow step console output.
# The stderr (logger) is redirected to GITHUB_STEP_SUMMARY which becomes
part of the HTML job summary.
report_url = get_env("JUNIT_REPORT_URL")
- report_md = f"Download [HTML report]({report_url})."
- summary = (f"{total_run} tests cases run in {duration}. "
+ if report_url:
+ report_md = f"Download [HTML report]({report_url})."
+ else:
+ report_md = "No report available. JUNIT_REPORT_URL was missing."
+ summary = (f"{total_run} tests cases run in {duration}.\n\n"
f"{total_success} {PASSED}, {total_failures} {FAILED}, "
- f"{total_flaky} {FLAKY}, {total_skipped} {SKIPPED}, and
{total_errors} errors.")
+ f"{total_flaky} {FLAKY}, {total_skipped} {SKIPPED},
{len(quarantined_table)} {QUARANTINED}, and {total_errors} errors.<br/>")
print("## Test Summary\n")
- print(f"{summary} {report_md}\n")
+ print(f"{summary}\n\n{report_md}\n")
+
+ # Failed
if len(failed_table) > 0:
- logger.info(f"Found {len(failed_table)} test failures:")
- print("### Failed Tests\n")
+ print("<details open=\"true\">")
+ print(f"<summary>Failed Tests {FAILED}
({len(failed_table)})</summary>\n")
print(f"| Module | Test | Message | Time |")
print(f"| ------ | ---- | ------- | ---- |")
+ logger.info(f"Found {len(failed_table)} test failures:")
for row in failed_table:
logger.info(f"{FAILED} {row[0]} > {row[1]}")
row_joined = " | ".join(row)
print(f"| {row_joined} |")
+ print("\n</details>")
print("\n")
+
+ # Flaky
if len(flaky_table) > 0:
- logger.info(f"Found {len(flaky_table)} flaky test failures:")
- print("### Flaky Tests\n")
+ print("<details open=\"true\">")
+ print(f"<summary>Flaky Tests {FLAKY} ({len(flaky_table)})</summary>\n")
print(f"| Module | Test | Message | Time |")
print(f"| ------ | ---- | ------- | ---- |")
+ logger.info(f"Found {len(flaky_table)} flaky test failures:")
for row in flaky_table:
logger.info(f"{FLAKY} {row[0]} > {row[1]}")
row_joined = " | ".join(row)
print(f"| {row_joined} |")
+ print("\n</details>")
print("\n")
+
+ # Skipped
if len(skipped_table) > 0:
print("<details>")
- print(f"<summary>{len(skipped_table)} Skipped Tests</summary>\n")
+ print(f"<summary>Skipped Tests {SKIPPED}
({len(skipped_table)})</summary>\n")
print(f"| Module | Test |")
print(f"| ------ | ---- |")
+ logger.debug(f"::group::Found {len(skipped_table)} skipped tests")
for row in skipped_table:
row_joined = " | ".join(row)
print(f"| {row_joined} |")
+ logger.debug(f"{row[0]} > {row[1]}")
print("\n</details>")
+ logger.debug("::endgroup::")
+ print("\n")
+ # Quarantined
if len(quarantined_table) > 0:
- logger.info(f"Ran {len(quarantined_table)} quarantined test:")
print("<details>")
- print(f"<summary>{len(quarantined_table)} Quarantined
Tests</summary>\n")
+ print(f"<summary>Quarantined Tests {QUARANTINED}
({len(quarantined_table)})</summary>\n")
print(f"| Module | Test |")
print(f"| ------ | ---- |")
+ logger.debug(f"::group::Found {len(quarantined_table)} quarantined
tests")
for row in quarantined_table:
- logger.info(f"{QUARANTINED} {row[0]} > {row[1]}")
row_joined = " | ".join(row)
print(f"| {row_joined} |")
+ logger.debug(f"{row[0]} > {row[1]}")
print("\n</details>")
+ logger.debug("::endgroup::")
+
+ # Create a horizontal rule
+ print("-"*80)
# Print special message if there was a timeout
exit_code = get_env("GRADLE_EXIT_CODE", int)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index fb08bb2a9f5..0d6e76d50c2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -165,32 +165,30 @@ jobs:
# If the load-catalog job failed, we won't be able to download the
artifact. Since we don't want this to fail
# the overall workflow, so we'll continue here without a test catalog.
- name: Load Test Catalog
+ id: load-test-catalog
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: combined-test-catalog
- - name: Test
- # Gradle flags
- # --build-cache: Let Gradle restore the build cache
- # --no-scan: Don't attempt to publish the scan yet. We want to
archive it first.
- # --continue: Keep running even if a test fails
- # -PcommitId Prevent the Git SHA being written into the jar files
(which breaks caching)
+ - name: JUnit Quarantined Tests
+ id: junit-quarantined-test
+ uses: ./.github/actions/run-gradle
+ with:
+ test-task: quarantinedTest
+ timeout-minutes: 30
+ test-catalog-path: ${{ steps.load-test-catalog.outputs.download-path
}}/combined-test-catalog.txt
+ build-scan-artifact-name: build-scan-quarantined-test-${{
matrix.java }}
+
+ - name: JUnit Tests
id: junit-test
- env:
- TIMEOUT_MINUTES: 180 # 3 hours
- run: |
- set +e
- ./.github/scripts/thread-dump.sh &
- timeout ${TIMEOUT_MINUTES}m ./gradlew --build-cache --continue
--no-scan \
- -PtestLoggingEvents=started,passed,skipped,failed \
- -PmaxParallelForks=2 \
- -PmaxTestRetries=1 -PmaxTestRetryFailures=3 \
- -PmaxQuarantineTestRetries=3 -PmaxQuarantineTestRetryFailures=0 \
- -PcommitId=xxxxxxxxxxxxxxxx \
- quarantinedTest test
- exitcode="$?"
- echo "exitcode=$exitcode" >> $GITHUB_OUTPUT
+ uses: ./.github/actions/run-gradle
+ with:
+ test-task: test
+ timeout-minutes: 180 # 3 hours
+ test-catalog-path: ${{ steps.load-test-catalog.outputs.download-path
}}/combined-test-catalog.txt
+ build-scan-artifact-name: build-scan-test-${{ matrix.java }}
+
- name: Archive JUnit HTML reports
uses: actions/upload-artifact@v4
id: junit-upload-artifact
@@ -200,6 +198,7 @@ jobs:
**/build/reports/tests/*
compression-level: 9
if-no-files-found: ignore
+
- name: Archive JUnit XML
uses: actions/upload-artifact@v4
with:
@@ -208,9 +207,10 @@ jobs:
build/junit-xml/**/*.xml
compression-level: 9
if-no-files-found: ignore
+
- name: Archive Thread Dumps
id: thread-dump-upload-artifact
- if: always() && steps.junit-test.outputs.exitcode == '124'
+ if: always() && (steps.junit-test.outputs.gradle-exitcode == '124' ||
steps.junit-quarantined-test.outputs.gradle-exitcode == '124')
uses: actions/upload-artifact@v4
with:
name: junit-thread-dumps-${{ matrix.java }}
@@ -218,13 +218,15 @@ jobs:
thread-dumps/*
compression-level: 9
if-no-files-found: ignore
+
- name: Parse JUnit tests
run: python .github/scripts/junit.py --export-test-catalog
./test-catalog >> $GITHUB_STEP_SUMMARY
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
JUNIT_REPORT_URL: ${{
steps.junit-upload-artifact.outputs.artifact-url }}
THREAD_DUMP_URL: ${{
steps.thread-dump-upload-artifact.outputs.artifact-url }}
- GRADLE_EXIT_CODE: ${{ steps.junit-test.outputs.exitcode }}
+ GRADLE_EXIT_CODE: ${{ steps.junit-test.outputs.gradle-exitcode }}
+
- name: Archive Test Catalog
if: ${{ always() && matrix.java == '23' }}
uses: actions/upload-artifact@v4
@@ -233,14 +235,6 @@ jobs:
path: test-catalog
compression-level: 9
if-no-files-found: ignore
- - name: Archive Build Scan
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: build-scan-test-${{ matrix.java }}
- path: ~/.gradle/build-scan-data
- compression-level: 9
- if-no-files-found: ignore
update-test-catalog:
name: Update Test Catalog
diff --git a/.github/workflows/ci-complete.yml
b/.github/workflows/ci-complete.yml
index 6478ae2c6da..cc5188c1f78 100644
--- a/.github/workflows/ci-complete.yml
+++ b/.github/workflows/ci-complete.yml
@@ -44,6 +44,7 @@ jobs:
fail-fast: false
matrix:
java: [ 23, 11 ]
+ artifact-prefix: [ "build-scan-test-", "build-scan-quarantined-test-"]
steps:
- name: Env
run: printenv
@@ -66,7 +67,7 @@ jobs:
with:
github-token: ${{ github.token }}
run-id: ${{ github.event.workflow_run.id }}
- name: build-scan-test-${{ matrix.java }}
+ name: ${{ matrix.artifact-prefix }}-${{ matrix.java }}
path: ~/.gradle/build-scan-data # This is where Gradle buffers
unpublished build scan data when --no-scan is given
- name: Handle missing scan
if: ${{ steps.download-build-scan.outcome == 'failure' }}
diff --git a/build.gradle b/build.gradle
index 3d107f3e5c4..4c67ec95119 100644
--- a/build.gradle
+++ b/build.gradle
@@ -135,6 +135,7 @@ ext {
runtimeTestLibs = [
libs.slf4jReload4j,
libs.junitPlatformLanucher,
+ project(":test-common:test-common-runtime")
]
}
@@ -483,6 +484,8 @@ subprojects {
// KAFKA-17433 Used by deflake.yml github action to repeat individual tests
systemProperty("kafka.cluster.test.repeat",
project.findProperty("kafka.cluster.test.repeat"))
+ systemProperty("kafka.test.catalog.file",
project.findProperty("kafka.test.catalog.file"))
+ systemProperty("kafka.test.run.quarantined", "false")
testLogging {
events = userTestLoggingEvents ?: testLoggingEvents
@@ -553,6 +556,8 @@ subprojects {
// KAFKA-17433 Used by deflake.yml github action to repeat individual tests
systemProperty("kafka.cluster.test.repeat",
project.findProperty("kafka.cluster.test.repeat"))
+ systemProperty("kafka.test.catalog.file",
project.findProperty("kafka.test.catalog.file"))
+ systemProperty("kafka.test.run.quarantined", "true")
testLogging {
events = userTestLoggingEvents ?: testLoggingEvents
@@ -564,7 +569,6 @@ subprojects {
useJUnitPlatform {
includeEngines 'junit-jupiter'
- includeTags 'flaky'
}
develocity {
@@ -945,7 +949,7 @@ project(':server') {
testImplementation libs.junitJupiter
testImplementation libs.slf4jReload4j
- testRuntimeOnly libs.junitPlatformLanucher
+ testRuntimeOnly runtimeTestLibs
}
task createVersionFile() {
@@ -1004,7 +1008,7 @@ project(':share') {
testImplementation libs.mockitoCore
testImplementation libs.slf4jReload4j
- testRuntimeOnly libs.junitPlatformLanucher
+ testRuntimeOnly runtimeTestLibs
}
checkstyle {
@@ -1114,7 +1118,7 @@ project(':core') {
testImplementation libs.slf4jReload4j
testImplementation libs.caffeine
- testRuntimeOnly libs.junitPlatformLanucher
+ testRuntimeOnly runtimeTestLibs
}
if (userEnableTestCoverage) {
@@ -1367,7 +1371,7 @@ project(':metadata') {
testImplementation project(':raft').sourceSets.test.output
testImplementation project(':server-common').sourceSets.test.output
- testRuntimeOnly libs.junitPlatformLanucher
+ testRuntimeOnly runtimeTestLibs
generator project(':generator')
}
@@ -1536,6 +1540,7 @@ project(':group-coordinator') {
}
project(':test-common') {
+ // Test framework stuff. Implementations that support test-common-api
base {
archivesName = "kafka-test-common"
}
@@ -1564,11 +1569,11 @@ project(':test-common') {
}
project(':test-common:test-common-api') {
+ // Interfaces, config classes, and other test APIs
base {
archivesName = "kafka-test-common-api"
}
-
dependencies {
implementation project(':clients')
implementation project(':core')
@@ -1596,6 +1601,28 @@ project(':test-common:test-common-api') {
}
}
+project(':test-common:test-common-runtime') {
+ // Runtime-only test code including JUnit extentions
+ base {
+ archivesName = "kafka-test-common-runtime"
+ }
+
+ dependencies {
+ implementation libs.slf4jApi
+ implementation libs.junitPlatformLanucher
+ implementation libs.junitJupiterApi
+ implementation libs.junitJupiter
+ }
+
+ checkstyle {
+ configProperties =
checkstyleConfigProperties("import-control-test-common-api.xml")
+ }
+
+ javadoc {
+ enabled = false
+ }
+}
+
project(':transaction-coordinator') {
base {
archivesName = "kafka-transaction-coordinator"
@@ -1789,7 +1816,7 @@ project(':generator') {
implementation libs.jacksonJaxrsJsonProvider
testImplementation libs.junitJupiter
- testRuntimeOnly libs.junitPlatformLanucher
+ testRuntimeOnly runtimeTestLibs
}
javadoc {
@@ -2094,7 +2121,6 @@ project(':server-common') {
testImplementation libs.mockitoCore
testRuntimeOnly runtimeTestLibs
- testRuntimeOnly project(":test-common")
}
task createVersionFile() {
@@ -2325,7 +2351,7 @@ project(':tools:tools-api') {
dependencies {
implementation project(':clients')
testImplementation libs.junitJupiter
- testRuntimeOnly libs.junitPlatformLanucher
+ testRuntimeOnly runtimeTestLibs
}
task createVersionFile() {
@@ -2425,9 +2451,8 @@ project(':tools') {
testImplementation libs.apachedsProtocolLdap
testImplementation libs.apachedsLdifPartition
- testRuntimeOnly libs.junitPlatformLanucher
+ testRuntimeOnly runtimeTestLibs
testRuntimeOnly libs.hamcrest
- testRuntimeOnly project(':test-common')
}
javadoc {
@@ -2859,7 +2884,7 @@ project(':streams:examples') {
testImplementation libs.junitJupiter
testImplementation libs.hamcrest
- testRuntimeOnly libs.junitPlatformLanucher
+ testRuntimeOnly runtimeTestLibs
}
javadoc {
diff --git a/clients/src/test/java/org/apache/kafka/common/UuidTest.java
b/clients/src/test/java/org/apache/kafka/common/UuidTest.java
index f5067a953cd..65316469c69 100644
--- a/clients/src/test/java/org/apache/kafka/common/UuidTest.java
+++ b/clients/src/test/java/org/apache/kafka/common/UuidTest.java
@@ -77,7 +77,7 @@ public class UuidTest {
assertEquals(Uuid.fromString(zeroIdString), Uuid.ZERO_UUID);
}
- @RepeatedTest(100)
+ @RepeatedTest(value = 100, name = RepeatedTest.LONG_DISPLAY_NAME)
public void testRandomUuid() {
Uuid randomID = Uuid.randomUuid();
diff --git a/settings.gradle b/settings.gradle
index 9d08ac68ca2..abfeca92705 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -118,7 +118,8 @@ include 'clients',
'transaction-coordinator',
'trogdor',
'test-common',
- 'test-common:test-common-api'
+ 'test-common:test-common-api',
+ 'test-common:test-common-runtime'
project(":storage:api").name = "storage-api"
rootProject.name = 'kafka'
diff --git a/test-common/src/test/resources/log4j.properties
b/test-common/src/main/resources/log4j.properties
similarity index 100%
rename from test-common/src/test/resources/log4j.properties
rename to test-common/src/main/resources/log4j.properties
diff --git
a/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/AutoQuarantinedTestFilter.java
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/AutoQuarantinedTestFilter.java
new file mode 100644
index 00000000000..a236c05b957
--- /dev/null
+++
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/AutoQuarantinedTestFilter.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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 org.apache.kafka.common.test.junit;
+
+import org.junit.platform.engine.Filter;
+import org.junit.platform.engine.FilterResult;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.support.descriptor.MethodSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+public class AutoQuarantinedTestFilter implements Filter<TestDescriptor> {
+
+ private static final Filter<TestDescriptor> INCLUDE_ALL_TESTS =
testDescriptor -> FilterResult.included(null);
+ private static final Filter<TestDescriptor> EXCLUDE_ALL_TESTS =
testDescriptor -> FilterResult.excluded(null);
+
+ private static final Logger log =
LoggerFactory.getLogger(AutoQuarantinedTestFilter.class);
+
+ private final Set<TestAndMethod> testCatalog;
+ private final boolean includeQuarantined;
+
+ AutoQuarantinedTestFilter(Set<TestAndMethod> testCatalog, boolean
includeQuarantined) {
+ this.testCatalog = Collections.unmodifiableSet(testCatalog);
+ this.includeQuarantined = includeQuarantined;
+ }
+
+ @Override
+ public FilterResult apply(TestDescriptor testDescriptor) {
+ Optional<TestSource> sourceOpt = testDescriptor.getSource();
+ if (sourceOpt.isEmpty()) {
+ return FilterResult.included(null);
+ }
+
+ TestSource source = sourceOpt.get();
+ if (!(source instanceof MethodSource)) {
+ return FilterResult.included(null);
+ }
+
+ MethodSource methodSource = (MethodSource) source;
+
+ TestAndMethod testAndMethod = new
TestAndMethod(methodSource.getClassName(), methodSource.getMethodName());
+ if (includeQuarantined) {
+ if (testCatalog.contains(testAndMethod)) {
+ return FilterResult.excluded("exclude non-quarantined");
+ } else {
+ return FilterResult.included("auto-quarantined");
+ }
+ } else {
+ if (testCatalog.contains(testAndMethod)) {
+ return FilterResult.included(null);
+ } else {
+ return FilterResult.excluded("auto-quarantined");
+ }
+ }
+ }
+
+ private static Filter<TestDescriptor> defaultFilter(boolean
includeQuarantined) {
+ if (includeQuarantined) {
+ return EXCLUDE_ALL_TESTS;
+ } else {
+ return INCLUDE_ALL_TESTS;
+ }
+ }
+
+ /**
+ * Create a filter that excludes tests that are missing from a given test
catalog file.
+ * If no test catalog is given, the default behavior depends on {@code
includeQuarantined}.
+ * If true, this filter will exclude all tests. If false, this filter will
include all tests.
+ * <p>
+ * The format of the test catalog is a text file where each line has the
format of:
+ *
+ * <pre>
+ * FullyQualifiedClassName "#" MethodName "\n"
+ * </pre>
+ *
+ * @param testCatalogFileName path to a test catalog file
+ * @param includeQuarantined true if this filter should include only the
auto-quarantined tests
+ */
+ public static Filter<TestDescriptor> create(String testCatalogFileName,
boolean includeQuarantined) {
+ if (testCatalogFileName == null || testCatalogFileName.isEmpty()) {
+ log.debug("No test catalog specified, will not quarantine any
recently added tests.");
+ return defaultFilter(includeQuarantined);
+ }
+ Path path = Paths.get(testCatalogFileName);
+ log.debug("Loading test catalog file {}.", path);
+
+ if (!Files.exists(path)) {
+ log.error("Test catalog file {} does not exist, will not
quarantine any recently added tests.", path);
+ return defaultFilter(includeQuarantined);
+ }
+
+ Set<TestAndMethod> allTests = new HashSet<>();
+ try (BufferedReader reader = Files.newBufferedReader(path,
Charset.defaultCharset())) {
+ String line = reader.readLine();
+ while (line != null) {
+ String[] toks = line.split("#", 2);
+ allTests.add(new TestAndMethod(toks[0], toks[1]));
+ line = reader.readLine();
+ }
+ } catch (IOException e) {
+ log.error("Error while reading test catalog file, will not
quarantine any recently added tests.", e);
+ return defaultFilter(includeQuarantined);
+ }
+
+ if (allTests.isEmpty()) {
+ log.error("Loaded an empty test catalog, will not quarantine any
recently added tests.");
+ return defaultFilter(includeQuarantined);
+ } else {
+ log.debug("Loaded {} test methods from test catalog file {}.",
allTests.size(), path);
+ return new AutoQuarantinedTestFilter(allTests, includeQuarantined);
+ }
+ }
+
+ public static class TestAndMethod {
+ private final String testClass;
+ private final String testMethod;
+
+ public TestAndMethod(String testClass, String testMethod) {
+ this.testClass = testClass;
+ this.testMethod = testMethod;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TestAndMethod that = (TestAndMethod) o;
+ return Objects.equals(testClass, that.testClass) &&
Objects.equals(testMethod, that.testMethod);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(testClass, testMethod);
+ }
+
+ @Override
+ public String toString() {
+ return "TestAndMethod{" +
+ "testClass='" + testClass + '\'' +
+ ", testMethod='" + testMethod + '\'' +
+ '}';
+ }
+ }
+}
diff --git
a/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/QuarantinedPostDiscoveryFilter.java
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/QuarantinedPostDiscoveryFilter.java
new file mode 100644
index 00000000000..f56c44d36ec
--- /dev/null
+++
b/test-common/test-common-runtime/src/main/java/org/apache/kafka/common/test/junit/QuarantinedPostDiscoveryFilter.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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 org.apache.kafka.common.test.junit;
+
+import org.junit.platform.engine.Filter;
+import org.junit.platform.engine.FilterResult;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestTag;
+import org.junit.platform.launcher.PostDiscoveryFilter;
+
+/**
+ * A JUnit test filter which can include or exclude discovered tests before
+ * they are sent off to the test engine for execution. The behavior of this
+ * filter is controlled by the system property "kafka.test.run.quarantined".
+ * If the property is set to "true", then only auto-quarantined and explicitly
+ * {@code @Flaky} tests will be included. If the property is set to "false",
then
+ * only non-quarantined tests will be run.
+ * <p>
+ * This filter is registered with JUnit using SPI. The test-common-runtime
module
+ * includes a META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter
+ * service file which registers this class.
+ */
+public class QuarantinedPostDiscoveryFilter implements PostDiscoveryFilter {
+
+ private static final TestTag FLAKY_TEST_TAG = TestTag.create("flaky");
+
+ public static final String RUN_QUARANTINED_PROP =
"kafka.test.run.quarantined";
+
+ public static final String CATALOG_FILE_PROP = "kafka.test.catalog.file";
+
+ private final Filter<TestDescriptor> autoQuarantinedFilter;
+ private final boolean runQuarantined;
+
+ // No-arg public constructor for SPI
+ @SuppressWarnings("unused")
+ public QuarantinedPostDiscoveryFilter() {
+ runQuarantined = System.getProperty(RUN_QUARANTINED_PROP, "false")
+ .equalsIgnoreCase("true");
+
+ String testCatalogFileName = System.getProperty(CATALOG_FILE_PROP);
+ autoQuarantinedFilter =
AutoQuarantinedTestFilter.create(testCatalogFileName, runQuarantined);
+ }
+
+ // Visible for tests
+ QuarantinedPostDiscoveryFilter(Filter<TestDescriptor>
autoQuarantinedFilter, boolean runQuarantined) {
+ this.autoQuarantinedFilter = autoQuarantinedFilter;
+ this.runQuarantined = runQuarantined;
+ }
+
+ @Override
+ public FilterResult apply(TestDescriptor testDescriptor) {
+ boolean hasTag = testDescriptor.getTags().contains(FLAKY_TEST_TAG);
+ FilterResult result = autoQuarantinedFilter.apply(testDescriptor);
+ if (runQuarantined) {
+ // If selecting quarantined tests, we first check for explicitly
flaky tests. If no
+ // flaky tag is set, check the auto-quarantined filter. In the
case of a missing test
+ // catalog, the auto-quarantined filter will exclude all tests.
+ if (hasTag) {
+ return FilterResult.included("flaky");
+ } else {
+ return result;
+ }
+ } else {
+ // If selecting non-quarantined tests, we exclude auto-quarantined
tests and flaky tests
+ if (result.included() && hasTag) {
+ return FilterResult.excluded("flaky");
+ } else {
+ return result;
+ }
+ }
+ }
+}
diff --git a/test-common/src/test/resources/junit-platform.properties
b/test-common/test-common-runtime/src/main/resources/META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter
similarity index 90%
rename from test-common/src/test/resources/junit-platform.properties
rename to
test-common/test-common-runtime/src/main/resources/META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter
index 05069923a7f..45209e1fde4 100644
--- a/test-common/src/test/resources/junit-platform.properties
+++
b/test-common/test-common-runtime/src/main/resources/META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter
@@ -12,4 +12,5 @@
# 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.
-junit.jupiter.params.displayname.default = "{displayName}.{argumentsWithNames}"
+
+org.apache.kafka.common.test.junit.QuarantinedPostDiscoveryFilter
\ No newline at end of file
diff --git a/clients/src/test/resources/junit-platform.properties
b/test-common/test-common-runtime/src/main/resources/junit-platform.properties
similarity index 94%
rename from clients/src/test/resources/junit-platform.properties
rename to
test-common/test-common-runtime/src/main/resources/junit-platform.properties
index 05069923a7f..551f6c42cb8 100644
--- a/clients/src/test/resources/junit-platform.properties
+++
b/test-common/test-common-runtime/src/main/resources/junit-platform.properties
@@ -13,3 +13,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
junit.jupiter.params.displayname.default = "{displayName}.{argumentsWithNames}"
+junit.jupiter.extensions.autodetection.enabled = true
\ No newline at end of file
diff --git
a/test-common/test-common-runtime/src/test/java/org/apache/kafka/common/test/junit/AutoQuarantinedTestFilterTest.java
b/test-common/test-common-runtime/src/test/java/org/apache/kafka/common/test/junit/AutoQuarantinedTestFilterTest.java
new file mode 100644
index 00000000000..390132d3484
--- /dev/null
+++
b/test-common/test-common-runtime/src/test/java/org/apache/kafka/common/test/junit/AutoQuarantinedTestFilterTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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 org.apache.kafka.common.test.junit;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.platform.engine.Filter;
+import org.junit.platform.engine.TestDescriptor;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AutoQuarantinedTestFilterTest {
+
+ private TestDescriptor descriptor(String className, String methodName) {
+ return new
QuarantinedPostDiscoveryFilterTest.MockTestDescriptor(className, methodName);
+ }
+
+ @Test
+ public void testLoadCatalog(@TempDir Path tempDir) throws IOException {
+ Path catalog = tempDir.resolve("catalog.txt");
+ List<String> lines = new ArrayList<>();
+ lines.add("o.a.k.Foo#testBar1");
+ lines.add("o.a.k.Foo#testBar2");
+ lines.add("o.a.k.Spam#testEggs");
+ Files.write(catalog, lines);
+
+ Filter<TestDescriptor> filter =
AutoQuarantinedTestFilter.create(catalog.toString(), false);
+ assertTrue(filter.apply(descriptor("o.a.k.Foo",
"testBar1")).included());
+ assertTrue(filter.apply(descriptor("o.a.k.Foo",
"testBar2")).included());
+ assertTrue(filter.apply(descriptor("o.a.k.Spam",
"testEggs")).included());
+ assertTrue(filter.apply(descriptor("o.a.k.Spam",
"testNew")).excluded());
+
+ filter = AutoQuarantinedTestFilter.create(catalog.toString(), true);
+ assertTrue(filter.apply(descriptor("o.a.k.Foo",
"testBar1")).excluded());
+ assertTrue(filter.apply(descriptor("o.a.k.Foo",
"testBar2")).excluded());
+ assertTrue(filter.apply(descriptor("o.a.k.Spam",
"testEggs")).excluded());
+ assertTrue(filter.apply(descriptor("o.a.k.Spam",
"testNew")).included());
+ }
+
+ @Test
+ public void testEmptyCatalog(@TempDir Path tempDir) throws IOException {
+ Path catalog = tempDir.resolve("catalog.txt");
+ Files.write(catalog, Collections.emptyList());
+
+ Filter<TestDescriptor> filter =
AutoQuarantinedTestFilter.create(catalog.toString(), false);
+ assertTrue(filter.apply(descriptor("o.a.k.Foo",
"testBar1")).included());
+ assertTrue(filter.apply(descriptor("o.a.k.Foo",
"testBar2")).included());
+ assertTrue(filter.apply(descriptor("o.a.k.Spam",
"testEggs")).included());
+ assertTrue(filter.apply(descriptor("o.a.k.Spam",
"testNew")).included());
+ }
+
+ @Test
+ public void testMissingCatalog() {
+ Filter<TestDescriptor> filter =
AutoQuarantinedTestFilter.create("does-not-exist.txt", false);
+ assertTrue(filter.apply(descriptor("o.a.k.Foo",
"testBar1")).included());
+ assertTrue(filter.apply(descriptor("o.a.k.Foo",
"testBar2")).included());
+ assertTrue(filter.apply(descriptor("o.a.k.Spam",
"testEggs")).included());
+ assertTrue(filter.apply(descriptor("o.a.k.Spam",
"testNew")).included());
+ }
+}
diff --git
a/test-common/test-common-runtime/src/test/java/org/apache/kafka/common/test/junit/QuarantinedPostDiscoveryFilterTest.java
b/test-common/test-common-runtime/src/test/java/org/apache/kafka/common/test/junit/QuarantinedPostDiscoveryFilterTest.java
new file mode 100644
index 00000000000..4ce628594f5
--- /dev/null
+++
b/test-common/test-common-runtime/src/test/java/org/apache/kafka/common/test/junit/QuarantinedPostDiscoveryFilterTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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 org.apache.kafka.common.test.junit;
+
+import org.junit.jupiter.api.Test;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.TestTag;
+import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.support.descriptor.MethodSource;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class QuarantinedPostDiscoveryFilterTest {
+
+ static class MockTestDescriptor implements TestDescriptor {
+
+ private final MethodSource methodSource;
+ private final Set<TestTag> testTags;
+
+ MockTestDescriptor(String className, String methodName, String...
tags) {
+ this.methodSource = MethodSource.from(className, methodName);
+ this.testTags = new HashSet<>();
+ Arrays.stream(tags).forEach(tag ->
testTags.add(TestTag.create(tag)));
+ }
+
+ @Override
+ public UniqueId getUniqueId() {
+ return null;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "";
+ }
+
+ @Override
+ public Set<TestTag> getTags() {
+ return this.testTags;
+ }
+
+ @Override
+ public Optional<TestSource> getSource() {
+ return Optional.of(this.methodSource);
+ }
+
+ @Override
+ public Optional<TestDescriptor> getParent() {
+ return Optional.empty();
+ }
+
+ @Override
+ public void setParent(TestDescriptor testDescriptor) {
+
+ }
+
+ @Override
+ public Set<? extends TestDescriptor> getChildren() {
+ return Set.of();
+ }
+
+ @Override
+ public void addChild(TestDescriptor testDescriptor) {
+
+ }
+
+ @Override
+ public void removeChild(TestDescriptor testDescriptor) {
+
+ }
+
+ @Override
+ public void removeFromHierarchy() {
+
+ }
+
+ @Override
+ public Type getType() {
+ return null;
+ }
+
+ @Override
+ public Optional<? extends TestDescriptor> findByUniqueId(UniqueId
uniqueId) {
+ return Optional.empty();
+ }
+ }
+
+ QuarantinedPostDiscoveryFilter setupFilter(boolean runQuarantined) {
+ Set<AutoQuarantinedTestFilter.TestAndMethod> testCatalog = new
HashSet<>();
+ testCatalog.add(new
AutoQuarantinedTestFilter.TestAndMethod("o.a.k.Foo", "testBar1"));
+ testCatalog.add(new
AutoQuarantinedTestFilter.TestAndMethod("o.a.k.Foo", "testBar2"));
+ testCatalog.add(new
AutoQuarantinedTestFilter.TestAndMethod("o.a.k.Spam", "testEggs"));
+
+ AutoQuarantinedTestFilter autoQuarantinedTestFilter = new
AutoQuarantinedTestFilter(testCatalog, runQuarantined);
+ return new QuarantinedPostDiscoveryFilter(autoQuarantinedTestFilter,
runQuarantined);
+ }
+
+ @Test
+ public void testQuarantinedExistingTestNonFlaky() {
+ QuarantinedPostDiscoveryFilter filter = setupFilter(true);
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar1")).excluded());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar2")).excluded());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Spam",
"testEggs")).excluded());
+ }
+
+ @Test
+ public void testQuarantinedExistingTestFlaky() {
+ QuarantinedPostDiscoveryFilter filter = setupFilter(true);
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar1", "flaky")).included());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar2", "flaky")).included());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Spam",
"testEggs", "flaky", "integration")).included());
+ }
+
+ @Test
+ public void testQuarantinedNewTest() {
+ QuarantinedPostDiscoveryFilter filter = setupFilter(true);
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar3")).included());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Spam",
"testEggz", "flaky")).included());
+ }
+
+ @Test
+ public void testExistingTestNonFlaky() {
+ QuarantinedPostDiscoveryFilter filter = setupFilter(false);
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar1")).included());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar2")).included());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Spam",
"testEggs")).included());
+ }
+
+
+ @Test
+ public void testExistingTestFlaky() {
+ QuarantinedPostDiscoveryFilter filter = setupFilter(false);
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar1", "flaky")).excluded());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar2", "flaky")).excluded());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Spam",
"testEggs", "flaky", "integration")).excluded());
+ }
+
+ @Test
+ public void testNewTest() {
+ QuarantinedPostDiscoveryFilter filter = setupFilter(false);
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar3")).excluded());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Spam",
"testEggz", "flaky")).excluded());
+ }
+
+ @Test
+ public void testNoCatalogQuarantinedTest() {
+ QuarantinedPostDiscoveryFilter filter = new
QuarantinedPostDiscoveryFilter(
+ AutoQuarantinedTestFilter.create(null, true),
+ true
+ );
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar1", "flaky")).included());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Foo",
"testBar2", "flaky")).included());
+ assertTrue(filter.apply(new MockTestDescriptor("o.a.k.Spam",
"testEggs")).excluded());
+ }
+}