Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ repository on GitHub.
[[v6.1.0-M2-junit-platform-new-features-and-improvements]]
==== New Features and Improvements

* ❓
* The `UniqueId.uniqueIdFormat` field has been removed, reducing the size of `UniqueId`
objects.

[[v6.1.0-M2-junit-jupiter]]
=== JUnit Jupiter
Expand All @@ -48,6 +49,7 @@ repository on GitHub.
`DefaultTimeZoneExtension` are now part of the JUnit Jupiter. Find examples in the
xref:writing-tests/built-in-extensions.adoc#DefaultLocaleAndTimeZone[User Guide].


[[v6.1.0-M2-junit-vintage]]
=== JUnit Vintage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@

import static org.apiguardian.api.API.Status.STABLE;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamField;
import java.io.Serial;
import java.io.Serializable;
import java.lang.ref.SoftReference;
Expand Down Expand Up @@ -40,6 +45,11 @@ public final class UniqueId implements Cloneable, Serializable {
@Serial
private static final long serialVersionUID = 1L;

@Serial
@SuppressWarnings("UnusedVariable")
private static final ObjectStreamField[] serialPersistentFields = ObjectStreamClass.lookup(
SerializedForm.class).getFields();

private static final String ENGINE_SEGMENT_TYPE = "engine";

/**
Expand Down Expand Up @@ -79,33 +89,28 @@ public static UniqueId forEngine(String engineId) {
* @see #forEngine(String)
*/
public static UniqueId root(String segmentType, String value) {
return new UniqueId(UniqueIdFormat.getDefault(), new Segment(segmentType, value));
return new UniqueId(new Segment(segmentType, value));
}

private final UniqueIdFormat uniqueIdFormat;

@SuppressWarnings({ "serial", "RedundantSuppression" }) // always used with serializable implementation (List.copyOf())
private final List<Segment> segments;
@SuppressWarnings({ "serial", "RedundantSuppression" })
// always used with serializable implementation (List.copyOf())
// This is effectively final but not technically due to late initialization when deserializing
private /* final */ List<Segment> segments;

// lazily computed
private transient int hashCode;

// lazily computed
private transient @Nullable SoftReference<String> toString;

private UniqueId(UniqueIdFormat uniqueIdFormat, Segment segment) {
this(uniqueIdFormat, List.of(segment));
private UniqueId(Segment segment) {
this(List.of(segment));
}

/**
* Initialize a {@code UniqueId} instance.
*
* @implNote A defensive copy of the segment list is <b>not</b> created by
* this implementation. All callers should immediately drop the reference
* to the list instance that they pass into this constructor.
*/
UniqueId(UniqueIdFormat uniqueIdFormat, List<Segment> segments) {
this.uniqueIdFormat = uniqueIdFormat;
UniqueId(List<Segment> segments) {
this.segments = List.copyOf(segments);
}

Expand Down Expand Up @@ -164,7 +169,7 @@ public UniqueId append(Segment segment) {
List<Segment> baseSegments = new ArrayList<>(this.segments.size() + 1);
baseSegments.addAll(this.segments);
baseSegments.add(segment);
return new UniqueId(this.uniqueIdFormat, baseSegments);
return new UniqueId(baseSegments);
}

/**
Expand Down Expand Up @@ -215,7 +220,7 @@ public boolean hasPrefix(UniqueId potentialPrefix) {
@API(status = STABLE, since = "1.5")
public UniqueId removeLastSegment() {
Preconditions.condition(this.segments.size() > 1, "Cannot remove last remaining segment");
return new UniqueId(uniqueIdFormat, List.copyOf(this.segments.subList(0, this.segments.size() - 1)));
return new UniqueId(this.segments.subList(0, this.segments.size() - 1));
}

/**
Expand All @@ -234,6 +239,18 @@ protected Object clone() throws CloneNotSupportedException {
return super.clone();
}

@Serial
private void writeObject(ObjectOutputStream s) throws IOException {
SerializedForm serializedForm = new SerializedForm(this);
serializedForm.serialize(s);
}

@Serial
private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
SerializedForm serializedForm = SerializedForm.deserialize(s);
segments = serializedForm.segments;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down Expand Up @@ -277,7 +294,7 @@ public String toString() {
SoftReference<String> s = this.toString;
String value = s == null ? null : s.get();
if (value == null) {
value = this.uniqueIdFormat.format(this);
value = UniqueIdFormat.getDefault().format(this);
// this is a benign race like String#hash
// we potentially read and write values from multiple threads
// without a happens-before relationship
Expand Down Expand Up @@ -361,4 +378,42 @@ public String toString() {

}

/**
* Represents the serialized output of {@code UniqueId}. The fields on this
* class match the fields that {@code UniqueId} had prior to 6.1.
*/
private static final class SerializedForm implements Serializable {

@Serial
private static final long serialVersionUID = 1L;

@SuppressWarnings({ "serial", "RedundantSuppression" })
// always used with serializable implementation (List.copyOf())
private final List<Segment> segments;
private final UniqueIdFormat uniqueIdFormat;

SerializedForm(UniqueId uniqueId) {
this.segments = uniqueId.segments;
this.uniqueIdFormat = UniqueIdFormat.getDefault();
}

@SuppressWarnings("unchecked")
private SerializedForm(ObjectInputStream.GetField fields) throws IOException, ClassNotFoundException {
this.segments = (List<Segment>) fields.get("segments", null);
this.uniqueIdFormat = UniqueIdFormat.getDefault();
}

void serialize(ObjectOutputStream s) throws IOException {
ObjectOutputStream.PutField fields = s.putFields();
fields.put("segments", segments);
fields.put("uniqueIdFormat", uniqueIdFormat);
s.writeFields();
}

static SerializedForm deserialize(ObjectInputStream s) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
return new SerializedForm(fields);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private static String encode(char c) {
UniqueId parse(String source) throws JUnitException {
String[] parts = source.split(String.valueOf(this.segmentDelimiter));
List<Segment> segments = Arrays.stream(parts).map(this::createSegment).toList();
return new UniqueId(this, segments);
return new UniqueId(segments);
}

private Segment createSegment(String segmentString) throws JUnitException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,21 @@ public String getEngineUid() {
return "[engine:junit-jupiter]";
}

@Override
public String getDefaultEngineUid() {
return getEngineUid();
}

@Override
public String getMethodUid() {
return "[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]";
}

@Override
public String getDefaultMethodUid() {
return getMethodUid();
}

}

@Nested
Expand All @@ -90,11 +100,21 @@ public String getEngineUid() {
return "{engine=junit-jupiter}";
}

@Override
public String getDefaultEngineUid() {
return "[engine:junit-jupiter]";
}

@Override
public String getMethodUid() {
return "{engine=junit-jupiter},{class=MyClass},{method=myMethod}";
}

@Override
public String getDefaultMethodUid() {
return "[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]";
}

}

// -------------------------------------------------------------------------
Expand All @@ -110,8 +130,12 @@ interface ParsingTestTrait {

String getEngineUid();

String getDefaultEngineUid();

String getMethodUid();

String getDefaultMethodUid();

@Test
default void parseMalformedUid() {
Throwable throwable = assertThrows(JUnitException.class, () -> getFormat().parse("malformed UID"));
Expand All @@ -123,7 +147,7 @@ default void parseEngineUid() {
var parsedId = getFormat().parse(getEngineUid());
assertSegment(parsedId.getSegments().getFirst(), "engine", "junit-jupiter");
assertEquals(getEngineUid(), getFormat().format(parsedId));
assertEquals(getEngineUid(), parsedId.toString());
assertEquals(getDefaultEngineUid(), parsedId.toString());
}

@Test
Expand All @@ -133,7 +157,7 @@ default void parseMethodUid() {
assertSegment(parsedId.getSegments().get(1), "class", "MyClass");
assertSegment(parsedId.getSegments().get(2), "method", "myMethod");
assertEquals(getMethodUid(), getFormat().format(parsedId));
assertEquals(getMethodUid(), parsedId.toString());
assertEquals(getDefaultMethodUid(), parsedId.toString());
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.platform.commons.test.PreconditionAssertions.assertPreconditionViolationFor;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;
import java.util.Optional;

import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -292,6 +298,89 @@ void removesLastSegment() {

}

@Nested
class Serialization {

final UniqueId uniqueId = UniqueId.root("engine", "junit-jupiter");

@Test
void roundTrip() throws IOException, ClassNotFoundException {
var bytesOut = new ByteArrayOutputStream();
var out = new ObjectOutputStream(bytesOut);
out.writeObject(uniqueId);

var bytesIn = new ByteArrayInputStream(bytesOut.toByteArray());
var in = new ObjectInputStream(bytesIn);
var actual = in.readObject();

assertEquals(uniqueId, actual);
assertEquals(uniqueId.toString(), actual.toString());
}

@Test
void deserializeFromJunit60() throws IOException, ClassNotFoundException {
/*
Serialized representation of:
new UniqueId(
new UniqueIdFormat('[', ':', ']', '/'),
List.of(new Segment("engine", "junit-jupiter"))
);
*/
var uniqueIdFromJunit60 = Base64.getMimeDecoder().decode("""
rO0ABXNyACJvcmcuanVuaXQucGxhdGZvcm0uZW5naW5lLlVuaXF1ZUlkAAAAAAAAAAECAAJMAAhzZWdtZW50c3QAEExqYXZhL3V0
aWwvTGlzdDtMAA51bmlxdWVJZEZvcm1hdHQAKkxvcmcvanVuaXQvcGxhdGZvcm0vZW5naW5lL1VuaXF1ZUlkRm9ybWF0O3hwc3IA
EWphdmEudXRpbC5Db2xsU2VyV46rtjobqBEDAAFJAAN0YWd4cAAAAAF3BAAAAAFzcgAqb3JnLmp1bml0LnBsYXRmb3JtLmVuZ2lu
ZS5VbmlxdWVJZCRTZWdtZW50AAAAAAAAAAECAAJMAAR0eXBldAASTGphdmEvbGFuZy9TdHJpbmc7TAAFdmFsdWVxAH4AB3hwdAAG
ZW5naW5ldAANanVuaXQtanVwaXRlcnhzcgAob3JnLmp1bml0LnBsYXRmb3JtLmVuZ2luZS5VbmlxdWVJZEZvcm1hdAAAAAAAAAAB
AgAGQwAMY2xvc2VTZWdtZW50QwALb3BlblNlZ21lbnRDABBzZWdtZW50RGVsaW1pdGVyQwASdHlwZVZhbHVlU2VwYXJhdG9yTAAT
ZW5jb2RlZENoYXJhY3Rlck1hcHQAE0xqYXZhL3V0aWwvSGFzaE1hcDtMAA5zZWdtZW50UGF0dGVybnQAGUxqYXZhL3V0aWwvcmVn
ZXgvUGF0dGVybjt4cABdAFsALwA6c3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNo
b2xkeHA/QAAAAAAADHcIAAAAEAAAAAZzcgATamF2YS5sYW5nLkNoYXJhY3RlcjSLR9lrGiZ4AgABQwAFdmFsdWV4cAAldAADJTI1
c3EAfgARADp0AAMlM0FzcQB+ABEAW3QAAyU1QnNxAH4AEQArdAADJTJCc3EAfgARAF10AAMlNURzcQB+ABEAL3QAAyUyRnhzcgAX
amF2YS51dGlsLnJlZ2V4LlBhdHRlcm5GZ9VrbkkCDQIAAkkABWZsYWdzTAAHcGF0dGVybnEAfgAHeHAAAAAgdAAXXFFbXEUoLisp
XFE6XEUoLispXFFdXEU=""");

var bytesIn = new ByteArrayInputStream(uniqueIdFromJunit60);
var in = new ObjectInputStream(bytesIn);
var actual = in.readObject();

assertEquals(uniqueId, actual);
assertEquals(uniqueId.toString(), actual.toString());
}

@Test
void deserializeFromJunit60IgnoresCustomFormat() throws IOException, ClassNotFoundException {
/*
Serialized representation of:
new UniqueId(
new UniqueIdFormat('{', '=', '}', ','),
List.of(new Segment("engine", "junit-jupiter"))
);
*/
var uniqueIdWithCustomFormatFromJunit60 = Base64.getMimeDecoder().decode("""
rO0ABXNyACJvcmcuanVuaXQucGxhdGZvcm0uZW5naW5lLlVuaXF1ZUlkAAAAAAAAAAECAAJMAAhzZWdtZW50c3QAEExqYXZhL3V0
aWwvTGlzdDtMAA51bmlxdWVJZEZvcm1hdHQAKkxvcmcvanVuaXQvcGxhdGZvcm0vZW5naW5lL1VuaXF1ZUlkRm9ybWF0O3hwc3IA
EWphdmEudXRpbC5Db2xsU2VyV46rtjobqBEDAAFJAAN0YWd4cAAAAAF3BAAAAAFzcgAqb3JnLmp1bml0LnBsYXRmb3JtLmVuZ2lu
ZS5VbmlxdWVJZCRTZWdtZW50AAAAAAAAAAECAAJMAAR0eXBldAASTGphdmEvbGFuZy9TdHJpbmc7TAAFdmFsdWVxAH4AB3hwdAAG
ZW5naW5ldAANanVuaXQtanVwaXRlcnhzcgAob3JnLmp1bml0LnBsYXRmb3JtLmVuZ2luZS5VbmlxdWVJZEZvcm1hdAAAAAAAAAAB
AgAGQwAMY2xvc2VTZWdtZW50QwALb3BlblNlZ21lbnRDABBzZWdtZW50RGVsaW1pdGVyQwASdHlwZVZhbHVlU2VwYXJhdG9yTAAT
ZW5jb2RlZENoYXJhY3Rlck1hcHQAE0xqYXZhL3V0aWwvSGFzaE1hcDtMAA5zZWdtZW50UGF0dGVybnQAGUxqYXZhL3V0aWwvcmVn
ZXgvUGF0dGVybjt4cAB9AHsALAA9c3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNo
b2xkeHA/QAAAAAAADHcIAAAAEAAAAAZzcgATamF2YS5sYW5nLkNoYXJhY3RlcjSLR9lrGiZ4AgABQwAFdmFsdWV4cAAldAADJTI1
c3EAfgARAHt0AAMlN0JzcQB+ABEAK3QAAyUyQnNxAH4AEQAsdAADJTJDc3EAfgARAH10AAMlN0RzcQB+ABEAPXQAAyUzRHhzcgAX
amF2YS51dGlsLnJlZ2V4LlBhdHRlcm5GZ9VrbkkCDQIAAkkABWZsYWdzTAAHcGF0dGVybnEAfgAHeHAAAAAgdAAXXFF7XEUoLisp
XFE9XEUoLispXFF9XEU=""");

var bytesIn = new ByteArrayInputStream(uniqueIdWithCustomFormatFromJunit60);
var in = new ObjectInputStream(bytesIn);
var actual = in.readObject();

assertEquals(uniqueId, actual);
assertEquals(uniqueId.toString(), actual.toString());
}

}

private static void assertSegment(Segment segment, String expectedType, String expectedValue) {
assertEquals(expectedType, segment.getType(), "segment type");
assertEquals(expectedValue, segment.getValue(), "segment value");
Expand Down