blob: 07f51470a0b3d3ef68158fa0ee37661aab411a0c [file] [edit]
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/* @test
* @summary Basic tests for StableFunction methods
* @enablePreview
* @run junit StableFunctionTest
*/
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Arrays;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
final class StableFunctionTest {
enum Value {
// Zero is here so that we have enums with ordinals before the first one
// actually used in input sets (i.e. ZERO is not in the input set)
ZERO(0),
ILLEGAL_BEFORE(-1),
// Valid values
THIRTEEN(13) {
@Override
public String toString() {
// getEnumConstants will be `null` for this enum as it is overridden
return super.toString()+" (Overridden)";
}
},
ILLEGAL_BETWEEN(-2),
FORTY_TWO(42),
// Illegal values (not in the input set)
ILLEGAL_AFTER(-3);
final int intValue;
Value(int intValue) {
this.intValue = intValue;
}
int asInt() {
return intValue;
}
}
private static final Function<Value, Integer> MAPPER = Value::asInt;
@ParameterizedTest
@MethodSource("allSets")
void factoryInvariants(Set<Value> inputs) {
assertThrows(NullPointerException.class, () -> StableValue.function(null, MAPPER));
assertThrows(NullPointerException.class, () -> StableValue.function(inputs, null));
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void basic(Set<Value> inputs) {
basic(inputs, MAPPER);
toStringTest(inputs, MAPPER);
basic(inputs, _ -> null);
toStringTest(inputs, _ -> null);
}
void basic(Set<Value> inputs, Function<Value, Integer> mapper) {
StableTestUtil.CountingFunction<Value, Integer> cif = new StableTestUtil.CountingFunction<>(mapper);
var cached = StableValue.function(inputs, cif);
assertEquals(mapper.apply(Value.FORTY_TWO), cached.apply(Value.FORTY_TWO));
assertEquals(1, cif.cnt());
assertEquals(mapper.apply(Value.FORTY_TWO), cached.apply(Value.FORTY_TWO));
assertEquals(1, cif.cnt());
var x0 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_BEFORE));
assertEquals("Input not allowed: ILLEGAL_BEFORE", x0.getMessage());
var x1 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_BETWEEN));
assertEquals("Input not allowed: ILLEGAL_BETWEEN", x1.getMessage());
var x2 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_AFTER));
assertEquals("Input not allowed: ILLEGAL_AFTER", x2.getMessage());
}
void toStringTest(Set<Value> inputs, Function<Value, Integer> mapper) {
var cached = StableValue.function(inputs, mapper);
cached.apply(Value.FORTY_TWO);
var toString = cached.toString();
assertTrue(toString.startsWith("{"));
// Key order is unspecified
assertTrue(toString.contains(Value.THIRTEEN + "=.unset"));
assertTrue(toString.contains(Value.FORTY_TWO + "=" + mapper.apply(Value.FORTY_TWO)));
assertTrue(toString.endsWith("}"));
// One between the values
assertEquals(1L, toString.chars().filter(ch -> ch == ',').count());
}
@ParameterizedTest
@MethodSource("emptySets")
void empty(Set<Value> inputs) {
Function<Value, Integer> f0 = StableValue.function(inputs, Value::asInt);
Function<Value, Integer> f1 = StableValue.function(inputs, Value::asInt);
assertEquals("{}", f0.toString());
assertThrows(NullPointerException.class, () -> f0.apply(null));
assertNotEquals(f0, f1);
assertNotEquals(null, f0);
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void exception(Set<Value> inputs) {
StableTestUtil.CountingFunction<Value, Integer> cif = new StableTestUtil.CountingFunction<>(_ -> {
throw new UnsupportedOperationException();
});
var cached = StableValue.function(inputs, cif);
assertThrows(UnsupportedOperationException.class, () -> cached.apply(Value.FORTY_TWO));
assertEquals(1, cif.cnt());
assertThrows(UnsupportedOperationException.class, () -> cached.apply(Value.FORTY_TWO));
assertEquals(2, cif.cnt());
var toString = cached.toString();
assertTrue(toString.startsWith("{"));
// Key order is unspecified
assertTrue(toString.contains(Value.THIRTEEN + "=.unset"));
assertTrue(toString.contains(Value.FORTY_TWO + "=.unset"));
assertTrue(toString.endsWith("}"));
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void circular(Set<Value> inputs) {
final AtomicReference<Function<?, ?>> ref = new AtomicReference<>();
Function<Value, Function<?, ?>> cached = StableValue.function(inputs, _ -> ref.get());
ref.set(cached);
cached.apply(Value.FORTY_TWO);
var toString = cached.toString();
assertTrue(toString.contains("FORTY_TWO=(this StableFunction)"), toString);
assertDoesNotThrow(cached::hashCode);
assertDoesNotThrow((() -> cached.equals(cached)));
}
@ParameterizedTest
@MethodSource("allSets")
void equality(Set<Value> inputs) {
Function<Value, Integer> mapper = Value::asInt;
Function<Value, Integer> f0 = StableValue.function(inputs, mapper);
Function<Value, Integer> f1 = StableValue.function(inputs, mapper);
// No function is equal to another function
assertNotEquals(f0, f1);
}
@ParameterizedTest
@MethodSource("allSets")
void hashCodeStable(Set<Value> inputs) {
Function<Value, Integer> f0 = StableValue.function(inputs, Value::asInt);
assertEquals(System.identityHashCode(f0), f0.hashCode());
if (!inputs.isEmpty()) {
f0.apply(Value.FORTY_TWO);
assertEquals(System.identityHashCode(f0), f0.hashCode());
}
}
@Test
void nullKeys() {
Set<Value> inputs = new HashSet<>();
inputs.add(Value.FORTY_TWO);
inputs.add(null);
assertThrows(NullPointerException.class, () -> StableValue.function(inputs, MAPPER));
}
@Test
void usesOptimizedVersion() {
Function<Value, Integer> enumFunction = StableValue.function(EnumSet.of(Value.FORTY_TWO), Value::asInt);
assertEquals("jdk.internal.lang.stable.StableEnumFunction", enumFunction.getClass().getName());
Function<Value, Integer> emptyFunction = StableValue.function(Set.of(), Value::asInt);
assertEquals("jdk.internal.lang.stable.StableFunction", emptyFunction.getClass().getName());
}
@Test
void overriddenEnum() {
final var overridden = Value.THIRTEEN;
Function<Value, Integer> enumFunction = StableValue.function(EnumSet.of(overridden), Value::asInt);
assertEquals(MAPPER.apply(overridden), enumFunction.apply(overridden));
}
private static Stream<Set<Value>> nonEmptySets() {
return Stream.of(
Set.of(Value.FORTY_TWO, Value.THIRTEEN),
linkedHashSet(Value.THIRTEEN, Value.FORTY_TWO),
treeSet(Value.FORTY_TWO, Value.THIRTEEN),
EnumSet.of(Value.FORTY_TWO, Value.THIRTEEN)
);
}
private static Stream<Set<Value>> emptySets() {
return Stream.of(
Set.of(),
linkedHashSet(),
treeSet(),
EnumSet.noneOf(Value.class)
);
}
private static Stream<Set<Value>> allSets() {
return Stream.concat(
nonEmptySets(),
emptySets()
);
}
static Set<Value> treeSet(Value... values) {
return populate(new TreeSet<>(Comparator.comparingInt(Value::asInt).reversed()),values);
}
static Set<Value> linkedHashSet(Value... values) {
return populate(new LinkedHashSet<>(), values);
}
static Set<Value> populate(Set<Value> set, Value... values) {
set.addAll(Arrays.asList(values));
return set;
}
}