Skip to content
Merged
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
@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2026 The Refinery Authors <https://refinery.tools/>
*
* SPDX-License-Identifier: EPL-2.0
*/
package tools.refinery.store.map;

import java.util.AbstractMap;
import java.util.AbstractMap.SimpleEntry;
import java.util.HashMap;
import java.util.Objects;

class ConsolidatedDiffCursor<K, V> implements DiffCursor<K, V> {

private final DiffCursor<K, V> wrappedDiffCursor;
private DiffEntry<K, V>[] diff;
private int cursorIndex = -1;

private record DiffEntry<K, V>(K key, V from, V to) {
}

public ConsolidatedDiffCursor(DiffCursor<K, V> diffCursor) {
wrappedDiffCursor = diffCursor;
}

@Override
public K getKey() {
return diff != null && 0 <= cursorIndex && cursorIndex < diff.length ? diff[cursorIndex].key : null;
}

@Override
public V getValue() {
return getToValue();
}

@Override
public boolean isTerminated() {
return diff != null && cursorIndex >= diff.length;
}

@Override
public boolean move() {
if (diff == null) {
consolidate();
}

cursorIndex++;
return cursorIndex < diff.length;
}

@Override
public V getFromValue() {
return diff != null && 0 <= cursorIndex && cursorIndex < diff.length ? diff[cursorIndex].from : null;
}

@Override
public V getToValue() {
return diff != null && 0 <= cursorIndex && cursorIndex < diff.length ? diff[cursorIndex].to : null;
}

private void consolidate() {
HashMap<K, AbstractMap.SimpleEntry<V, V>> consolidatedChanges = new HashMap<>();
while (wrappedDiffCursor.move()) {
var storedChange = consolidatedChanges.get(wrappedDiffCursor.getKey());
V fromValue;
if (storedChange != null) {
if (!Objects.equals(storedChange.getValue(), wrappedDiffCursor.getFromValue())) {
throw new IllegalStateException("Inconsistent diff cursor: mismatched previous value and from value");
}
fromValue = storedChange.getKey();
} else {
fromValue = wrappedDiffCursor.getFromValue();
}
V toValue = wrappedDiffCursor.getToValue();

if (Objects.equals(fromValue, toValue)) {
consolidatedChanges.remove(wrappedDiffCursor.getKey());
} else {
consolidatedChanges.put(wrappedDiffCursor.getKey(), new SimpleEntry<>(fromValue, toValue));
}
}

diff = new DiffEntry[consolidatedChanges.size()];
int i = 0;
for (var entry : consolidatedChanges.entrySet()) {
diff[i] = new DiffEntry<>(entry.getKey(), entry.getValue().getKey(), entry.getValue().getValue());
i++;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
* SPDX-FileCopyrightText: 2021-2026 The Refinery Authors <https://refinery.tools/>
*
* SPDX-License-Identifier: EPL-2.0
*/
Expand All @@ -17,4 +17,11 @@ public non-sealed interface VersionedMap<K, V> extends AnyVersionedMap {
void putAll(Cursor<K, V> cursor);

DiffCursor<K, V> getDiffCursor(Version state);

default DiffCursor<K, V> getDiffCursor(Version state, boolean consolidate) {
if (!consolidate) {
return getDiffCursor(state);
}
return new ConsolidatedDiffCursor<>(getDiffCursor(state));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
* SPDX-FileCopyrightText: 2023-2026 The Refinery Authors <https://refinery.tools/>
*
* SPDX-License-Identifier: EPL-2.0
*/
Expand Down Expand Up @@ -82,7 +82,7 @@ public void putAll(Cursor<K, V> cursor) {
while (keyIterator.hasNext()) {
var key = keyIterator.next();
var value = valueIterator.next();
this.put(key,value);
this.put(key, value);
}
} else {
while (cursor.move()) {
Expand Down Expand Up @@ -122,6 +122,10 @@ public DiffCursor<K, V> getDiffCursor(Version toVersion) {
return new MapDiffCursor<>(this.defaultValue, fromCursor, toCursor);
}

@Override
public DiffCursor<K, V> getDiffCursor(Version state, boolean consolidate) {
return getDiffCursor(state);
}

@Override
public Version commit() {
Expand Down Expand Up @@ -157,7 +161,7 @@ public void checkIntegrity() {
@Override
public int contentHashCode(ContentHashCode mode) {
// Calculating the root hashCode is always fast, because {@link Node} caches its hashCode.
if(root == null) {
if (root == null) {
return 0;
} else {
return root.hashCode();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
* SPDX-FileCopyrightText: 2021-2026 The Refinery Authors <https://refinery.tools/>
*
* SPDX-License-Identifier: EPL-2.0
*/
Expand All @@ -25,7 +25,11 @@ public non-sealed interface Interpretation<T> extends AnyInterpretation {

void putAll(Cursor<Tuple, T> cursor);

DiffCursor<Tuple, T> getDiffCursor(Version to);
default DiffCursor<Tuple, T> getDiffCursor(Version to) {
return getDiffCursor(to, false);
}

DiffCursor<Tuple, T> getDiffCursor(Version to, boolean consolidate);

void addListener(InterpretationListener<T> listener, boolean alsoWhenRestoring);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
* SPDX-FileCopyrightText: 2021-2026 The Refinery Authors <https://refinery.tools/>
*
* SPDX-License-Identifier: EPL-2.0
*/
Expand Down Expand Up @@ -29,7 +29,11 @@ default AnyInterpretation getInterpretation(AnySymbol symbol) {

<T> Interpretation<T> getInterpretation(Symbol<T> symbol);

ModelDiffCursor getDiffCursor(Version to);
default ModelDiffCursor getDiffCursor(Version to) {
return getDiffCursor(to, false);
}

ModelDiffCursor getDiffCursor(Version to, boolean consolidate);

<T extends ModelAdapter> Optional<T> tryGetAdapter(Class<? extends T> adapterType);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
* SPDX-FileCopyrightText: 2021-2026 The Refinery Authors <https://refinery.tools/>
*
* SPDX-License-Identifier: EPL-2.0
*/
Expand Down Expand Up @@ -60,11 +60,12 @@ public <T> Interpretation<T> getInterpretation(Symbol<T> symbol) {
}

@Override
public ModelDiffCursor getDiffCursor(Version to) {
public ModelDiffCursor getDiffCursor(Version to, boolean consolidate) {
var diffCursors = HashMap.<AnySymbol, DiffCursor<?, ?>>newHashMap(interpretations.size());
int i = 0;
for (var entry : interpretations.entrySet()) {
diffCursors.put(entry.getKey(), entry.getValue().getDiffCursor(ModelVersion.getInternalVersion(to, i++)));
diffCursors.put(entry.getKey(), entry.getValue().getDiffCursor(ModelVersion.getInternalVersion(to, i++),
consolidate));
}
return new ModelDiffCursor(diffCursors);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
* SPDX-FileCopyrightText: 2021-2026 The Refinery Authors <https://refinery.tools/>
*
* SPDX-License-Identifier: EPL-2.0
*/
Expand Down Expand Up @@ -106,8 +106,8 @@ public void putAll(Cursor<Tuple, T> cursor) {
}

@Override
public DiffCursor<Tuple, T> getDiffCursor(Version to) {
return map.getDiffCursor(to);
public DiffCursor<Tuple, T> getDiffCursor(Version to, boolean consolidate) {
return map.getDiffCursor(to, consolidate);
}

Version commit() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
* SPDX-FileCopyrightText: 2021-2026 The Refinery Authors <https://refinery.tools/>
*
* SPDX-License-Identifier: EPL-2.0
*/
Expand All @@ -10,7 +10,11 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import tools.refinery.store.map.*;
import tools.refinery.store.map.DiffCursor;
import tools.refinery.store.map.Version;
import tools.refinery.store.map.VersionedMap;
import tools.refinery.store.map.VersionedMapStore;
import tools.refinery.store.map.VersionedMapStoreFactoryBuilder;
import tools.refinery.store.map.tests.fuzz.utils.FuzzTestUtils;
import tools.refinery.store.map.tests.utils.MapTestEnvironment;

Expand All @@ -19,13 +23,20 @@
import java.util.Random;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail;
import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.*;
import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.commitFrequencyOptions;
import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.keyCounts;
import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.nullDefaultOptions;
import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.randomSeedOptions;
import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.storeConfigs;
import static tools.refinery.store.map.tests.fuzz.utils.FuzzTestCollections.valueCounts;

class DiffCursorFuzzTest {
private void runFuzzTest(String scenario, int seed, int steps, int maxKey, int maxValue, boolean nullDefault,
int commitFrequency, boolean commitBeforeDiffCursor,
VersionedMapStoreFactoryBuilder<Integer, String> builder) {
int commitFrequency, boolean commitBeforeDiffCursor,
VersionedMapStoreFactoryBuilder<Integer, String> builder) {
String[] values = MapTestEnvironment.prepareValues(maxValue, nullDefault);

VersionedMapStore<Integer, String> store = builder.defaultValue(values[0]).build().createOne();
Expand All @@ -34,11 +45,11 @@ private void runFuzzTest(String scenario, int seed, int steps, int maxKey, int m
}

private void iterativeRandomPutsAndCommitsThenDiffCursor(String scenario, VersionedMapStore<Integer, String> store,
int steps, int maxKey, String[] values, int seed,
int commitFrequency, boolean commitBeforeDiffCursor) {
int steps, int maxKey, String[] values, int seed,
int commitFrequency, boolean commitBeforeDiffCursor) {

int largestCommit = -1;
Map<Integer,Version> index2Version = new HashMap<>();
Map<Integer, Version> index2Version = new HashMap<>();

{
// 1. build a map with versions
Expand All @@ -56,7 +67,7 @@ private void iterativeRandomPutsAndCommitsThenDiffCursor(String scenario, Versio
}
if (index % commitFrequency == 0) {
Version version = versioned.commit();
index2Version.put(index,version);
index2Version.put(index, version);
largestCommit = index;
}
if (index % 10000 == 0)
Expand All @@ -78,13 +89,28 @@ private void iterativeRandomPutsAndCommitsThenDiffCursor(String scenario, Versio

VersionedMap<Integer, String> oracle = store.createMap(index2Version.get(travelToVersion));

if(commitBeforeDiffCursor) {
if (commitBeforeDiffCursor) {
moving.commit();
}
DiffCursor<Integer, String> diffCursor = moving.getDiffCursor(index2Version.get(travelToVersion));

DiffCursor<Integer, String> consolidatedDiffCursor =
moving.getDiffCursor(index2Version.get(travelToVersion), true);
HashMap<Integer, String> consolidatedToValues = new HashMap<>();
while (consolidatedDiffCursor.move()) {
assertEquals(moving.get(consolidatedDiffCursor.getKey()),
consolidatedDiffCursor.getFromValue());
assertFalse(consolidatedToValues.containsKey(consolidatedDiffCursor.getKey()));
consolidatedToValues.put(consolidatedDiffCursor.getKey(), consolidatedDiffCursor.getToValue());
}

moving.putAll(diffCursor);
moving.commit();

for (var entry : consolidatedToValues.entrySet()) {
assertEquals(moving.get(entry.getKey()), entry.getValue());
}

MapTestEnvironment.compareTwoMaps(scenario + ":c" + index, oracle, moving);

moving.restore(index2Version.get(travelToVersion));
Expand Down Expand Up @@ -114,23 +140,23 @@ private void iterativeRandomPutsAndCommitsThenDiffCursor(String scenario, Versio
@Timeout(value = 10)
@Tag("fuzz")
void parametrizedFuzz(int ignoredTests, int steps, int noKeys, int noValues, boolean nullDefault,
int commitFrequency, int seed, boolean commitBeforeDiffCursor,
VersionedMapStoreFactoryBuilder<Integer, String> builder) {
int commitFrequency, int seed, boolean commitBeforeDiffCursor,
VersionedMapStoreFactoryBuilder<Integer, String> builder) {
runFuzzTest("DiffCursorS" + steps + "K" + noKeys + "V" + noValues + "s" + seed, seed, steps,
noKeys, noValues, nullDefault, commitFrequency, commitBeforeDiffCursor, builder);
}

static Stream<Arguments> parametrizedFuzz() {
return FuzzTestUtils.permutationWithSize(new Object[]{100}, keyCounts, valueCounts, nullDefaultOptions,
commitFrequencyOptions, randomSeedOptions, new Object[]{false,true}, storeConfigs);
commitFrequencyOptions, randomSeedOptions, new Object[]{false, true}, storeConfigs);
}

@ParameterizedTest(name = title)
@MethodSource
@Tag("fuzz")
@Tag("slow")
void parametrizedSlowFuzz(int ignoredTests, int steps, int noKeys, int noValues, boolean nullDefault, int commitFrequency,
int seed, boolean commitBeforeDiffCursor, VersionedMapStoreFactoryBuilder<Integer, String> builder) {
int seed, boolean commitBeforeDiffCursor, VersionedMapStoreFactoryBuilder<Integer, String> builder) {
runFuzzTest("DiffCursorS" + steps + "K" + noKeys + "V" + noValues + "s" + seed, seed, steps, noKeys, noValues,
nullDefault, commitFrequency, commitBeforeDiffCursor, builder);
}
Expand Down
Loading