Skip to content

Conversation

@mpkorstanje
Copy link
Contributor

@mpkorstanje mpkorstanje commented Jan 2, 2026

Adopts the SystemPropertyExtension from JUnit Pioneer into JUnit Jupiter.

While this is a faithful port, the following changes have been made:

  • The AbstractEntryBasedExtension has been merged into the SystemPropertyExtension for clarity. Should the EnvironmentVariableExtension ever be contributed we can undo that.
  • The extension configuration exception has been improved to include the annotated element providing the duplicate properties in the exception message.
  • The PropertiesAssertions has been trimmed down to the essentials and does not require reflection to work
  • The SystemPropertyExtensionUtils methods have been in lined or moved to JupiterPropertyUtils.
  • Fixed a bug where a cleared a property with an non-string value was not restored after the test completed.

Closes: #4726


I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done

@testlens-app
Copy link

testlens-app bot commented Jan 2, 2026

✅ All tests passed ✅

⚠️ TestLens detected flakiness ⚠️

Test Summary

Check Task Test Runs
CI / Linux :platform-tooling-support-tests:test MavenStarterTests > runOnlyOneMethodInClassTemplate(OutputFiles) ⚠️ 🚫
CI / Linux :platform-tooling-support-tests:test MavenStarterTests > verifyJupiterStarterProject(OutputFiles, Snapshot) ⚠️ 🚫
CI / Linux :platform-tooling-support-tests:test VintageGradleIntegrationTests > unsupportedVersion(OutputFiles) ⚠️ 🚫
CI / Linux :platform-tooling-support-tests:test VintageMavenIntegrationTests > supportedVersions(String, OutputFiles) > 4.12 ⚠️ 🚫
CI / Linux :platform-tooling-support-tests:test VintageMavenIntegrationTests > supportedVersions(String, OutputFiles) > 4.13.2 ⚠️ 🚫
CI / Linux :platform-tooling-support-tests:test VintageMavenIntegrationTests > unsupportedVersion(OutputFiles) ⚠️ 🚫
CI / Windows :platform-tooling-support-tests:test MavenStarterTests > runOnlyOneMethodInClassTemplate(OutputFiles) ⚠️ ✅
CI / Windows :platform-tooling-support-tests:test MavenStarterTests > verifyJupiterStarterProject(OutputFiles, Snapshot) ⚠️ ✅
CI / Windows :platform-tooling-support-tests:test VintageGradleIntegrationTests > unsupportedVersion(OutputFiles) ⚠️ ✅
CI / Windows :platform-tooling-support-tests:test VintageMavenIntegrationTests > supportedVersions(String, OutputFiles) > 4.12 ⚠️ ✅
CI / Windows :platform-tooling-support-tests:test VintageMavenIntegrationTests > supportedVersions(String, OutputFiles) > 4.13.2 ⚠️ ✅
CI / Windows :platform-tooling-support-tests:test VintageMavenIntegrationTests > unsupportedVersion(OutputFiles) ⚠️ ✅
Cross-Version / OpenJDK 26 (leyden) :platform-tooling-support-tests:test MavenStarterTests > verifyJupiterStarterProject(OutputFiles, Snapshot) ❌ ✅ ✅
Cross-Version / OpenJDK 26 (leyden) :platform-tooling-support-tests:test MavenSurefireCompatibilityTests > testMavenSurefireCompatibilityProject(Path, OutputFiles) ❌ ✅ ✅
Cross-Version / OpenJDK 26 (leyden) :platform-tooling-support-tests:test UnalignedClasspathTests > verifyErrorMessageForUnalignedClasspath(JRE, Path, Path, OutputFiles) > [1] jre = JAVA_17, javaHome = /usr/lib/jvm/temurin-17-jdk-amd64 ❌ ✅ ✅
Cross-Version / OpenJDK 26 (leyden) :platform-tooling-support-tests:test UnalignedClasspathTests > verifyErrorMessageForUnalignedClasspath(JRE, Path, Path, OutputFiles) > [2] jre = JAVA_26, javaHome = /opt/hostedtoolcache/Java_jdkfile_jdk/1545981957/x64 ❌ ✅ ✅
Cross-Version / OpenJDK 26 (leyden) :platform-tooling-support-tests:test VintageMavenIntegrationTests > supportedVersions(String, OutputFiles) > 4.12 ❌ ✅ ✅
Cross-Version / OpenJDK 26 (leyden) :platform-tooling-support-tests:test VintageMavenIntegrationTests > supportedVersions(String, OutputFiles) > 4.13.2 ❌ ✅ ✅
Cross-Version / OpenJDK 26 (leyden) :platform-tooling-support-tests:test VintageMavenIntegrationTests > unsupportedVersion(OutputFiles) ❌ ✅ ✅
Cross-Version / OpenJDK 26 (valhalla) :platform-tooling-support-tests:test MavenStarterTests > runOnlyOneMethodInClassTemplate(OutputFiles) ❌ ❌ ✅
Cross-Version / OpenJDK 26 (valhalla) :platform-tooling-support-tests:test MavenStarterTests > verifyJupiterStarterProject(OutputFiles, Snapshot) ❌ ❌ ✅
Cross-Version / OpenJDK 26 (valhalla) :platform-tooling-support-tests:test MavenSurefireCompatibilityTests > testMavenSurefireCompatibilityProject(Path, OutputFiles) ❌ ❌ ✅
Cross-Version / OpenJDK 26 (valhalla) :platform-tooling-support-tests:test UnalignedClasspathTests > verifyErrorMessageForUnalignedClasspath(JRE, Path, Path, OutputFiles) > [1] jre = JAVA_17, javaHome = /usr/lib/jvm/temurin-17-jdk-amd64 ❌ ❌ ✅
Cross-Version / OpenJDK 26 (valhalla) :platform-tooling-support-tests:test UnalignedClasspathTests > verifyErrorMessageForUnalignedClasspath(JRE, Path, Path, OutputFiles) > [2] jre = JAVA_26, javaHome = /opt/hostedtoolcache/Java_jdkfile_jdk/1373149213/x64 ❌ ❌ ✅
Cross-Version / OpenJDK 26 (valhalla) :platform-tooling-support-tests:test VintageMavenIntegrationTests > supportedVersions(String, OutputFiles) > 4.12 ❌ ❌ ✅
Cross-Version / OpenJDK 26 (valhalla) :platform-tooling-support-tests:test VintageMavenIntegrationTests > supportedVersions(String, OutputFiles) > 4.13.2 ❌ ❌ ✅
Cross-Version / OpenJDK 26 (valhalla) :platform-tooling-support-tests:test VintageMavenIntegrationTests > unsupportedVersion(OutputFiles) ❌ ❌ ✅

🏷️ Commit: 0cea1ed
▶️ Tests: 26059 executed
⚪️ Checks: 15/15 completed


Learn more about TestLens at testlens.app.

@mpkorstanje mpkorstanje force-pushed the 4726_passsystempropext branch from 5115788 to 8426118 Compare January 2, 2026 21:44
Copy link
Contributor

@jbduncan jbduncan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple of nits for consideration, but otherwise this draft is looking fantastic so far!

@mpkorstanje mpkorstanje force-pushed the 4726_passsystempropext branch from 62cf959 to 7e2eb7c Compare January 2, 2026 22:58

The system `Properties` object is normally just a hashmap of strings, however, it is technically possible to store non-string values and create {jdk-javadoc-base-url}/java.base/java/util/Properties.html#%3Cinit%3E(java.util.Properties)[nested `Properties` with inherited default values].
`@RestoreSystemProperties` restores the original `Properties` object with all of its potential richness _after_ the annotated scope is complete.
However, for use during the test _within_ the test scope it provides a cloned `Properties` object with these limitations:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For team discussion: Are these limitations acceptable?

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Jan 3, 2026

For discussion:

As originally written the SystemPropertyExtension can restore system properties to their original state after testing. It does this by replacing the Properties with an effective clone before a test starts, then restoring the original.

This effective clone is limited to the observable entries and defaults of the original properties object. This means that default values that are hidden by an entry are not included. In essence this test will fail:

var defaults = new Properties();
defaults.setProperty("a", "a");

var properties = new Properties(defaults);
properties.put("a", "b");

var clone = createEffectiveClone(properties);

assertThat(clone.getProperty("a")).isEqualTo("b");
clone.remove("a");
assertThat(clone.getProperty("a")).isEqualTo("a"); // fails: actually null.

This can be avoided by changing the cloning process such that the entries from the original properties object are removed to expose the default values, and then restoring the entries after the defaults have been copied. But this requires mutating the original object. Is that acceptable?

@jbduncan
Copy link
Contributor

jbduncan commented Jan 3, 2026

For discussion:

As originally written the SystemPropertyExtension can restore system properties to their original state after testing. It does this by replacing the Properties with an effective clone before a test starts, then restoring the original.

This effective clone is limited to the observable entries and defaults of the original properties object. This means that default values that are hidden by an entry are not included. In essence this test will fail:

var defaults = new Properties();
defaults.setProperty("a", "a");

var properties = new Properties(defaults);
properties.put("a", "b");

var clone = createEffectiveClone(properties);

assertThat(clone.getProperty("a")).isEqualTo("b");
clone.remove("a");
assertThat(clone.getProperty("a")).isEqualTo("a"); // fails: actually null.

This can be avoided by changing the cloning process such that the entries from the original properties object are removed to expose the default values, and then restoring the entries after the defaults have been copied. But this requires mutating the original object. Is that acceptable?

I checked the docs for Properties (not a power user of it, myself) and the store()/load() methods looked like a tempting solution, but they specifically don't serialize/deserialize default values, annoyingly. 😓

I can't believe I'm saying this, but would clone() or Java serialization work? Though I'm not holding my breath, given that Properties extends Hashtable.

The way I see it, there is a small window for tests to flake if this extension mutates the system properties during cloning whilst other tests running in parallel are using them. So whereas this mutation would be acceptable to me because I'm acutely aware of the perils of global state and race conditions and I would avoid calling System.(get|set)Properties outside of this extension (or even better use IoC/dependency injection to fake the system properties), I wouldn't count on other engineers knowing this, so I suggest removing the mutation, even if it means things aren't perfect.

@mpkorstanje
Copy link
Contributor Author

I can't believe I'm saying this, but would clone() or Java serialization work?

No. Clone doesn't include the defaults. And serialization would duplicate any objects in a corrupted properties object. If they're serializable in the first place...

The way I see it, there is a small window for tests to flake if this extension mutates the system properties during cloning whilst other tests running in parallel are using them.

Good point.

@mpkorstanje mpkorstanje force-pushed the 4726_passsystempropext branch from b8bb156 to 0cea1ed Compare January 4, 2026 00:11
@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Jan 4, 2026

Note: Manually set DCO check to pass. Originally #5184 was contributed before the DCO bot was enabled.

@mpkorstanje mpkorstanje marked this pull request as ready for review January 4, 2026 00:44
@mpkorstanje mpkorstanje mentioned this pull request Jan 4, 2026
6 tasks
@marcphilipp
Copy link
Member

The way I see it, there is a small window for tests to flake if this extension mutates the system properties during cloning whilst other tests running in parallel are using them.

Good point.

I also think we should avoid mutating it

[[writing-tests-built-in-extensions-SystemProperty]]
==== `@ClearSystemProperty` and `@SetSystemProperty`

The `@ClearSystemProperty` and `@SetSystemProperty` annotations can be used to clear and set, respectively, the values of system properties for a test execution.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our usual convention is to link the first occurrence of a class in a "section" to its Javadoc page (see above for {DefaultLocale}).

`@DefaultLocale` or `@DefaultTimeZone`.

[[writing-tests-built-in-extensions-SystemProperty]]
==== `@ClearSystemProperty` and `@SetSystemProperty`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code snippets in headers look weird in Antora's theme so we should avoid them.

Suggested change
==== `@ClearSystemProperty` and `@SetSystemProperty`
== @ClearSystemProperty and @SetSystemProperty

A class-level configuration means that the specified system properties are cleared/set before and reset after each individual test in the annotated class.
====

== `@RestoreSystemProperties`
Copy link
Member

@marcphilipp marcphilipp Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Suggested change
== `@RestoreSystemProperties`
[[RestoreSystemProperties]]
== @RestoreSystemProperties

'Restore', is only needed if system properties are modified in some way _other than_ Clear and Set during a test.
====

=== `@RestoreSystemProperties` Limitations
Copy link
Member

@marcphilipp marcphilipp Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be enough since it's below @RestoreSystemProperties, right?

Suggested change
=== `@RestoreSystemProperties` Limitations
[[RestoreSystemProperties-limitations]]
=== Limitations

== Thread-Safety

Since system properties are global state, reading and writing them during <<writing-tests-parallel-execution, parallel test execution>> can lead to unpredictable results and flaky tests.
The system property extension is prepared for that and tests annotated with `@ClearSystemProperty`, `@SetSystemProperty`, or `@RestoreSystemProperties` will never execute in parallel (thanks to https://docs.junit.org/current/api[resource locks]) to guarantee correct test results.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The system property extension is prepared for that and tests annotated with `@ClearSystemProperty`, `@SetSystemProperty`, or `@RestoreSystemProperties` will never execute in parallel (thanks to https://docs.junit.org/current/api[resource locks]) to guarantee correct test results.
The system property extension is prepared for that and tests annotated with `@ClearSystemProperty`, `@SetSystemProperty`, or `@RestoreSystemProperties` will never execute in parallel (thanks to xref:writing-tests/parallel-execution.adoc#synchronization[resource locks]) to guarantee correct test results.

List<Extension> extensions = registry.getExtensions(Extension.class);

assertEquals(NUM_DEFAULT_EXTENSIONS, extensions.size());
assertEquals(NUM_DEFAULT_EXTENSIONS + 1, extensions.size());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why +1?

* Marks tests that read system properties but don't use the system property extension themselves.
*
* <p>During
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
* <a href="https://docs.junit.org/current/writing-tests/parallel-execution.html" target="_top">parallel test execution</a>,

* annotated with {@code RestoreSystemProperties}.
*
* <p>During
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
* <a href="https://docs.junit.org/current/writing-tests/parallel-execution.html" target="_top">parallel test execution</a>,

* configurations.</p>
*
* <p>During
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
* <a href="https://docs.junit.org/current/writing-tests/parallel-execution.html" target="_top">parallel test execution</a>,

* Marks tests that write system properties but don't use the system property extension themselves.
*
* <p>During
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
* <a href="https://docs.junit.org/current/writing-tests/parallel-execution.html" target="_top">parallel test execution</a>,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support read / write of system properties during test execution

5 participants