Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions JavaReleases/src/test/java/pl/mperor/lab/java/Java25.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import org.junit.jupiter.api.Test;
import pl.mperor.lab.common.TestUtils;

import java.util.NoSuchElementException;

/// Java 25 (September 2025)
/// [JDK 25](https://openjdk.org/projects/jdk/25)
///
Expand Down Expand Up @@ -98,4 +100,52 @@ void describe() {
}
}

static final ScopedValue<String> USER = ScopedValue.newInstance();

@Test
public void testScopedValueIsBoundOnlyInsideRunScope() {
Assertions.assertFalse(USER.isBound());

ScopedValue.where(USER, "John Doe").run(() -> {
Assertions.assertTrue(USER.isBound());
Assertions.assertEquals("John Doe", USER.get());
});

// the binding is torn down as soon as run() returns
Assertions.assertFalse(USER.isBound());
Assertions.assertThrows(NoSuchElementException.class, USER::get);
}

@Test
public void testNestedRebindingRestoresOuterValueAfterward() {
ScopedValue.where(USER, "John Doe").run(() -> {
Assertions.assertEquals("John Doe", USER.get());

ScopedValue.where(USER, "Jane Doe").run(() -> Assertions.assertEquals("Jane Doe", USER.get()));

// the nested run() only rebinds USER for its own body; the outer binding is unaffected
Assertions.assertEquals("John Doe", USER.get());
});
}

@Test
public void testCallReturnsValueComputedUnderTheBinding() throws Exception {
String greeting = ScopedValue.where(USER, "John Doe").call(() -> "Hello, " + USER.get());
Assertions.assertEquals("Hello, John Doe", greeting);
}

@Test
public void testScopedValueHasNoSetUnlikeThreadLocal() {
var legacy = new ThreadLocal<String>();
legacy.set("John Doe");
legacy.set("Jane Doe"); // ThreadLocal is mutable: set() silently replaces the value, anywhere, anytime
Assertions.assertEquals("Jane Doe", legacy.get());
legacy.remove(); // must be cleared by hand, or it leaks for the thread's lifetime (e.g. in a pool)

// ScopedValue has no set()/remove(): the only way to associate a value is where(...).run()/.call(...),
// and the binding is automatically torn down when that call returns - no leak is possible.
ScopedValue.where(USER, "John Doe").run(() -> Assertions.assertEquals("John Doe", USER.get()));
Assertions.assertFalse(USER.isBound());
}

}
Loading