Skip to content

Serialization golden fixtures - #287

Merged
mbastian merged 5 commits into
masterfrom
serialization-golden-fixtures
Aug 19, 2026
Merged

Serialization golden fixtures#287
mbastian merged 5 commits into
masterfrom
serialization-golden-fixtures

Conversation

@mbastian

@mbastian mbastian commented Aug 19, 2026

Copy link
Copy Markdown
Member

Prevent future regressions with the serialization format

mbastian and others added 5 commits August 17, 2026 21:03
GraphAttributesImpl backed its map with a java.util.HashMap, and
Serialization.serializeGraphAttributes iterates that map's entrySet()
straight into the byte stream. HashMap iteration order is an
implementation detail, so the serialized bytes were a function of
insertion history rather than of content alone.

Switch the field to a TreeMap so iteration is sorted by key and the
output bytes become a pure function of the content. This is a
prerequisite for byte-pinned serialization fixtures.

TreeMap rejects null keys where HashMap accepted them, and throws NPE
from deep inside the map on get(null)/containsKey(null) where HashMap
returned null/false. Add explicit Objects.requireNonNull(key, "key")
guards to every public method taking a key so the failure is
intentional and well-messaged.

deepHashCode and deepEquals are unchanged and remain correct:
Map.hashCode() is specified as the order-independent sum of entry hash
codes, and deepEquals goes through MapDeepEquals which compares by key
lookup.

The read path is unaffected -- deserializeGraphAttributes just puts
entries into the map -- so previously written files still load
identically. Only the order of newly written graph-attribute entries
changes; the format itself is untouched and Serialization.VERSION is
not bumped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pins the on-disk serialization format against golden files so the
upcoming serializer dispatch refactor cannot silently change the bytes
we write or break reading older files.

Fixtures live in src/test/resources/serialization/, one directory per
MINOR version. 0.4 through 0.7 are real files produced by those
released versions, imported from the abandoned
graphstore-compatibility-testing repo (0.6.13 and 0.6.14 were verified
byte-identical, so only one copy is kept; 0.7 comes from a
0.7.0-SNAPSHOT build). Those are tiny and only cover the format
skeleton. 0.8 is generated by the committed
SerializationFixtureGenerator and carries the type-surface coverage: a
column of every supported type including Character, char[] and the
array types, both time representations, timestamp and interval maps of
every value type, graph attributes, views, edge types, mixed
directedness and dynamic edge weights.

SerializationCompatibilityTest enforces three contracts:

1. Backward compatibility - every fixture, from 0.4 up, deserializes
   and holds the expected content.
2. Format drift - for the current minor only, serializing the model
   built by the generator must reproduce the committed bytes exactly.
   Older minors are exempt: we no longer write those formats.
3. Determinism - the same content always serializes to the same bytes,
   independently of the order it was built in, including graph
   attributes inserted in different orders.

A round-trip-only test covers the surface that cannot be byte-pinned:
generic Map and Set attribute values, which Serialization.serializeMap
and serializeSet iterate in hash order.

Byte mismatches report the first differing offset and the surrounding
bytes rather than just "arrays differ".

Contract 2 compares generator output to the committed file rather than
re-serializing a model read back from it: deserialization is not
byte-idempotent here (GraphVersion counters and TimeIndexStore.countMap
are restored from the stream and then bumped again as elements are
re-inserted), so no fixture could ever be a byte-level fixed point. The
read path is covered by the content assertions of contract 1.

No production code is touched and Serialization.VERSION is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pins the on-disk serialization format against golden files so the
upcoming serializer dispatch refactor cannot silently change the bytes
we write or break reading older files.

Fixtures live in src/test/resources/serialization/, one directory per
MINOR version. 0.4 through 0.7 are real files produced by those
released versions, imported from the abandoned
graphstore-compatibility-testing repo (0.6.13 and 0.6.14 were verified
byte-identical, so only one copy is kept; 0.7 comes from a
0.7.0-SNAPSHOT build). Those are tiny and only cover the format
skeleton. 0.8 is generated by the committed
SerializationFixtureGenerator and carries the type-surface coverage: a
column of every supported type including Character, char[] and the
array types, both time representations, timestamp and interval maps of
every value type, graph attributes, views, edge types, mixed
directedness and dynamic edge weights.

SerializationCompatibilityTest enforces three contracts:

1. Backward compatibility - every fixture, from 0.4 up, deserializes
   and holds the expected content.
2. Format drift - for the current minor only, serializing the model
   built by the generator must reproduce the committed bytes exactly.
   Older minors are exempt: we no longer write those formats.
3. Determinism - the same content always serializes to the same bytes,
   independently of the order it was built in, including graph
   attributes inserted in different orders.

A round-trip-only test covers the surface that cannot be byte-pinned:
generic Map and Set attribute values, which Serialization.serializeMap
and serializeSet iterate in hash order.

Byte mismatches report the first differing offset and the surrounding
bytes rather than just "arrays differ".

Contract 2 compares generator output to the committed file rather than
re-serializing a model read back from it: deserialization is not
byte-idempotent here (GraphVersion counters and TimeIndexStore.countMap
are restored from the stream and then bumped again as elements are
re-inserted), so no fixture could ever be a byte-level fixed point. The
read path is covered by the content assertions of contract 1.

No production code is touched and Serialization.VERSION is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hstore into serialization-golden-fixtures

# Conflicts:
#	src/test/java/org/gephi/graph/impl/SerializationCompatibilityTest.java
#	src/test/java/org/gephi/graph/impl/SerializationFixtureGenerator.java
#	src/test/resources/serialization/README.md
@mbastian mbastian added this to the 0.8.7 milestone Aug 19, 2026
@mbastian
mbastian marked this pull request as ready for review August 19, 2026 15:40
@mbastian
mbastian merged commit 6d258db into master Aug 19, 2026
1 check passed
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.

1 participant