0bd3dfa398
Was adding key to _keymap before calling `ifAbsent`, so `containsKey` saw it during the `ifAbsent` call. Modified to allow concurrent modification inthe `ifAbsent` function, like other platforms already do. Documentation updated to match. Fixes #47852 CoreLibraryReviewExempt: DDC only change, other than docs. Bug: https://dartbug.com/47852 Change-Id: Ia289f426b1c9ad37ce89eda5119f9086dc5d39b4 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/222200 Commit-Queue: Lasse Nielsen <lrn@google.com> Reviewed-by: Nate Biggs <natebiggs@google.com>
410 lines
12 KiB
Dart
410 lines
12 KiB
Dart
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
// Checks that `putIfAbsent(key, ifAbsent)` works like `[key]=ifAbsent()`
|
|
// if the key is not in the map, and as `[key]` if the key is in the map.
|
|
|
|
import "package:expect/expect.dart";
|
|
import 'dart:collection';
|
|
|
|
void main() {
|
|
testMaps<Object?, Object?>();
|
|
testMaps<Object, Object?>();
|
|
testMaps<Object?, Object>();
|
|
testMaps<Object, Object>();
|
|
}
|
|
|
|
void testMaps<K, V>() {
|
|
// Testing `putIfAbsent` of the platform maps.
|
|
test(() => HashMap<K, V>());
|
|
test(() => LinkedHashMap<K, V>());
|
|
test(() => SplayTreeMap<K, V>(), ordered: true);
|
|
test(() => SplayTreeMap<K, V>(compareComparable), ordered: true);
|
|
// The identity version.
|
|
test(() => Map<K, V>.identity(), identity: true);
|
|
test(() => HashMap<K, V>.identity(), identity: true);
|
|
test(() => LinkedHashMap<K, V>.identity(), identity: true);
|
|
// And the configured versions, with equality.
|
|
test(
|
|
() => HashMap<K, V>(equals: (x, y) => x == y, hashCode: (x) => x.hashCode),
|
|
);
|
|
test(
|
|
() => LinkedHashMap<K, V>(
|
|
equals: (x, y) => x == y,
|
|
hashCode: (x) => x.hashCode,
|
|
),
|
|
);
|
|
// With identity, recognizable as such, and not.
|
|
test(
|
|
() => HashMap<K, V>(equals: identical, hashCode: identityHashCode),
|
|
identity: true,
|
|
);
|
|
test(
|
|
() => LinkedHashMap<K, V>(equals: identical, hashCode: identityHashCode),
|
|
identity: true,
|
|
);
|
|
test(
|
|
() => HashMap<K, V>(
|
|
equals: (x, y) => identical(x, y),
|
|
hashCode: (x) => identityHashCode(x),
|
|
),
|
|
identity: true,
|
|
);
|
|
test(
|
|
() => LinkedHashMap<K, V>(
|
|
equals: (x, y) => identical(x, y),
|
|
hashCode: (x) => identityHashCode(x),
|
|
),
|
|
identity: true,
|
|
);
|
|
|
|
// Test that [MapView] doesn't break anything.
|
|
test(() => MapView(HashMap<K, V>()));
|
|
test(() => MapView(SplayTreeMap<K, V>()), ordered: true);
|
|
|
|
// Test the `putIfAbsent` implementation of `MapBase` and `MapMixin`.
|
|
test(() => MapBaseMap<K, V>());
|
|
test(() => MapMixinMap<K, V>());
|
|
}
|
|
|
|
// Key and value types may be `Object?` or `Object`.
|
|
// If [identity] is true, the map uses identical as equality.
|
|
// If [ordered] is true, the map uses a `Comparable` as equality
|
|
// (and cannot accept `null` as key, even if the key type is `Object?`).
|
|
// Otherwise it uses `==` and maybe `hashCode`.
|
|
void test<K, V>(
|
|
Map<K, V> Function() map, {
|
|
bool identity = false,
|
|
bool ordered = false,
|
|
}) {
|
|
// Different kinds of keys.
|
|
// Every key type implements `Comparable`.
|
|
|
|
// Custom key objects overriding `==`.
|
|
var key1 = Key(1) as K;
|
|
var key1B = Key(1) as K; // Equal, not identical to key1.
|
|
var key2 = Key(2) as K; // Different from, but comparable to, key1.
|
|
testKeys<K, V>(
|
|
map(),
|
|
key1,
|
|
key1B,
|
|
key2,
|
|
identity: identity,
|
|
ordered: ordered,
|
|
);
|
|
|
|
// Custom key objects not overriding `==`.
|
|
// (Cannot have equal-but-not-identical value.)
|
|
var idKey1 = IdKey(1) as K;
|
|
var idKey2 = IdKey(2) as K;
|
|
testKeys<K, V>(
|
|
map(),
|
|
idKey1,
|
|
idKey1,
|
|
idKey2,
|
|
identity: identity,
|
|
ordered: ordered,
|
|
);
|
|
|
|
// Built-in/"native" keys.
|
|
var num1 = 1 as K;
|
|
var num1B = 1.0 as K; // Equal, but sometimes not identical, to num1.
|
|
var num2 = 2 as K; // Different from, but comparable to, key1.
|
|
testKeys<K, V>(
|
|
map(),
|
|
num1,
|
|
num1B,
|
|
num2,
|
|
identity: identity,
|
|
ordered: ordered,
|
|
);
|
|
|
|
// String keys (because web treats those specially).
|
|
var str1 = "abc" as K;
|
|
// Obfuscate that it's always "abc". Try to make it not canonicalized.
|
|
var str1B = String.fromCharCodes(("abcmore").codeUnits.take(3)) as K;
|
|
var str2 = "def" as K;
|
|
Expect.equals(str1, str1B, "Something's badly wrong with str1B?");
|
|
testKeys<K, V>(
|
|
map(),
|
|
str1,
|
|
str1B,
|
|
str2,
|
|
identity: identity,
|
|
ordered: ordered,
|
|
);
|
|
}
|
|
|
|
void testKeys<K, V>(
|
|
Map<K, V> map,
|
|
K k1,
|
|
K k1b,
|
|
K k2, {
|
|
required bool identity,
|
|
required bool ordered,
|
|
}) {
|
|
var ifAbsentCalled = 0;
|
|
|
|
var v42 = 42 as V;
|
|
var v37 = 37 as V;
|
|
var v87 = 87 as V;
|
|
var vb = "BANANA" as V;
|
|
|
|
V Function() add(K key, V value) => () {
|
|
expectNotIn(map, key);
|
|
ifAbsentCalled++;
|
|
return value;
|
|
};
|
|
|
|
V? result;
|
|
|
|
Expect.mapEquals({}, map);
|
|
result = map.putIfAbsent(k1, add(k1, v42));
|
|
Expect.mapEquals({k1: 42}, map); // Added k1:42
|
|
Expect.equals(1, ifAbsentCalled); // Called add(42).
|
|
Expect.equals(42, result); // Returned 42.
|
|
|
|
ifAbsentCalled = 0;
|
|
|
|
// Using same key again doesn't change map or call `ifAbsent`.
|
|
result = map.putIfAbsent(k1, add(k1, v87));
|
|
Expect.mapEquals({k1: 42}, map); // Did not change map.
|
|
Expect.equals(0, ifAbsentCalled); // Did not call add(87)
|
|
Expect.equals(42, result); // Returned existing value.
|
|
|
|
// Same for equal, but not identical, key.
|
|
if (!identity) {
|
|
result = map.putIfAbsent(k1b, add(k1b, v87));
|
|
Expect.mapEquals({k1: 42}, map); // Did not change map.
|
|
Expect.equals(0, ifAbsentCalled); // Did not call add(87)
|
|
Expect.equals(42, result); // Returned existing value.
|
|
}
|
|
|
|
if (map is! Map<Object?, Object>) {
|
|
var vNull = null as V;
|
|
// Allows null value. Check that a null value isn't the same as no key.
|
|
Expect.mapEquals({k1: 42}, map);
|
|
result = map.putIfAbsent(k2, add(k2, vNull));
|
|
Expect.mapEquals({k1: 42, k2: null}, map); // Added k2:null.
|
|
Expect.equals(1, ifAbsentCalled); // Called add(null).
|
|
Expect.equals(null, result); // Returned null.
|
|
ifAbsentCalled = 0;
|
|
|
|
result = map.putIfAbsent(k2, add(k2, v87));
|
|
Expect.mapEquals({k1: 42, k2: null}, map); // Did not change map.
|
|
Expect.equals(0, ifAbsentCalled); // Did not call add(87)
|
|
Expect.equals(null, result); // Returned existing value.
|
|
|
|
map.remove(k2);
|
|
}
|
|
|
|
if (!ordered && map is! Map<Object, Object?>) {
|
|
// Allows null key.
|
|
Expect.mapEquals({k1: 42}, map);
|
|
var kNull = null as K;
|
|
result = map.putIfAbsent(kNull, add(kNull, v37));
|
|
Expect.mapEquals({k1: 42, null: 37}, map); // Added null:37
|
|
Expect.equals(1, ifAbsentCalled); // Called add(37).
|
|
Expect.equals(37, result); // Returned 37.
|
|
ifAbsentCalled = 0;
|
|
|
|
result = map.putIfAbsent(kNull, add(kNull, v87));
|
|
Expect.mapEquals({k1: 42, null: 37}, map); // Did not change map.
|
|
Expect.equals(0, ifAbsentCalled); // Did not call add(87)
|
|
Expect.equals(37, result); // Returned existing value.
|
|
|
|
map.remove(null);
|
|
}
|
|
Expect.mapEquals({k1: 42}, map);
|
|
|
|
// Concurrent modification allowed.
|
|
// If `ifAbsent` modifies map, the returned value is still added.
|
|
|
|
// Remove inside ifAbsent.
|
|
result = map.putIfAbsent(k2, () {
|
|
expectNotIn(map, k2);
|
|
ifAbsentCalled++;
|
|
return map.remove(k1) as V; // Remove inside putIfAbsent.
|
|
});
|
|
Expect.mapEquals({k2: 42}, map);
|
|
Expect.equals(1, ifAbsentCalled);
|
|
Expect.equals(42, result);
|
|
ifAbsentCalled = 0;
|
|
|
|
// Add other key inside ifAbsent.
|
|
map.clear();
|
|
Expect.mapEquals({}, map);
|
|
result = map.putIfAbsent(k1, () {
|
|
expectNotIn(map, k1);
|
|
map[k2] = v87; // Add other key.
|
|
ifAbsentCalled++;
|
|
return v42;
|
|
});
|
|
Expect.mapEquals({k1: 42, k2: 87}, map);
|
|
Expect.equals(1, ifAbsentCalled);
|
|
Expect.equals(42, result);
|
|
ifAbsentCalled = 0;
|
|
|
|
map.remove(k1);
|
|
Expect.mapEquals({k2: 87}, map);
|
|
|
|
// Add same key inside ifAbsent.
|
|
result = map.putIfAbsent(k1, () {
|
|
expectNotIn(map, k1);
|
|
map[k1] = vb; // Add value for same key.
|
|
Expect.mapEquals({k1: "BANANA", k2: 87}, map);
|
|
ifAbsentCalled++;
|
|
return v42;
|
|
});
|
|
Expect.mapEquals({k1: 42, k2: 87}, map); // Value was overwritten.
|
|
Expect.equals(1, ifAbsentCalled);
|
|
Expect.equals(42, result);
|
|
ifAbsentCalled = 0;
|
|
|
|
map.remove(k1);
|
|
Expect.mapEquals({k2: 87}, map);
|
|
|
|
// Add *and* remove same key inside ifAbsent.
|
|
result = map.putIfAbsent(k1, () {
|
|
expectNotIn(map, k1);
|
|
map[k1] = vb; // Add value for same key.
|
|
Expect.mapEquals({k1: "BANANA", k2: 87}, map);
|
|
map.remove(k1);
|
|
Expect.mapEquals({k2: 87}, map);
|
|
ifAbsentCalled++;
|
|
return v42;
|
|
});
|
|
Expect.mapEquals({k1: 42, k2: 87}, map); // Value was overwritten.
|
|
Expect.equals(1, ifAbsentCalled);
|
|
Expect.equals(42, result);
|
|
ifAbsentCalled = 0;
|
|
|
|
// Add same key inside ifAbsent using `putIfAbsent`.
|
|
map.remove(k1);
|
|
Expect.mapEquals({k2: 87}, map);
|
|
result = map.putIfAbsent(k1, () {
|
|
expectNotIn(map, k1);
|
|
result = map.putIfAbsent(k1, add(k1, vb)); // Add value for same key.
|
|
Expect.equals(1, ifAbsentCalled);
|
|
Expect.mapEquals({k1: "BANANA", k2: 87}, map);
|
|
ifAbsentCalled++;
|
|
return v42;
|
|
});
|
|
Expect.mapEquals({k1: 42, k2: 87}, map); // Value was overwritten.
|
|
Expect.equals(2, ifAbsentCalled);
|
|
Expect.equals(42, result);
|
|
ifAbsentCalled = 0;
|
|
|
|
// Throw inside ifAbsent.
|
|
map.remove(k2);
|
|
Expect.mapEquals({k1: 42}, map);
|
|
try {
|
|
result = map.putIfAbsent(k2, () {
|
|
expectNotIn(map, k2);
|
|
ifAbsentCalled++;
|
|
throw "EXIT";
|
|
});
|
|
} on String catch (e) {
|
|
Expect.equals("EXIT", e);
|
|
}
|
|
expectNotIn(map, k2);
|
|
Expect.mapEquals({k1: 42}, map);
|
|
|
|
// Throw inside ifAbsent after doing modification. Modification stays.
|
|
map.remove(k2);
|
|
Expect.mapEquals({k1: 42}, map);
|
|
try {
|
|
result = map.putIfAbsent(k2, () {
|
|
expectNotIn(map, k2);
|
|
result = map.putIfAbsent(k2, add(k2, v87));
|
|
ifAbsentCalled++;
|
|
throw "EXIT";
|
|
});
|
|
} on String catch (e) {
|
|
Expect.equals("EXIT", e);
|
|
}
|
|
Expect.mapEquals({k1: 42, k2: 87}, map);
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Helper classes and functions.
|
|
|
|
// Key class that does not override `operator==`.
|
|
class IdKey implements Comparable<IdKey> {
|
|
final int id;
|
|
const IdKey(this.id);
|
|
int compareTo(IdKey other) => id.compareTo(other.id);
|
|
String toString() => "IdKey($id)";
|
|
}
|
|
|
|
// Key class that does override `operator==`.
|
|
class Key extends IdKey {
|
|
const Key(super.id);
|
|
int get hashCode => id.hashCode ^ 0x3a5f731;
|
|
bool operator ==(Object other) => other is Key && id == other.id;
|
|
String toString() => "Key($id)";
|
|
}
|
|
|
|
int compareComparable(Object? v1, Object? v2) =>
|
|
(v1 as Comparable<Object?>).compareTo(v2);
|
|
|
|
// Slow implementation of Map based on MapBase.
|
|
// Taken from `map_test.dart`.
|
|
mixin class MapBaseOperations<K, V> {
|
|
final List<K> _keys = <K>[];
|
|
final List<V> _values = <V>[];
|
|
|
|
V? operator [](Object? key) {
|
|
if (key is! K) return null;
|
|
int index = _keys.indexOf(key);
|
|
if (index < 0) return null;
|
|
return _values[index];
|
|
}
|
|
|
|
// Not testing this, so not caring if it recognizes concurrent modifications.
|
|
Iterable<K> get keys => _keys.skip(0);
|
|
|
|
void operator []=(K key, V value) {
|
|
int index = _keys.indexOf(key);
|
|
if (index >= 0) {
|
|
_values[index] = value;
|
|
} else {
|
|
_keys.add(key);
|
|
_values.add(value);
|
|
}
|
|
}
|
|
|
|
V? remove(Object? key) {
|
|
if (key is! K) return null;
|
|
int index = _keys.indexOf(key);
|
|
if (index >= 0) {
|
|
var result = _values[index];
|
|
key = _keys.removeLast();
|
|
var value = _values.removeLast();
|
|
if (index != _keys.length) {
|
|
_keys[index] = key;
|
|
_values[index] = value;
|
|
}
|
|
return result;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void clear() {
|
|
// Clear cannot be based on remove, since remove won't remove keys that
|
|
// are not equal to themselves.
|
|
_keys.clear();
|
|
_values.clear();
|
|
}
|
|
}
|
|
|
|
class MapBaseMap<K, V> = MapBase<K, V> with MapBaseOperations<K, V>;
|
|
class MapMixinMap<K, V> = MapBaseOperations<K, V> with MapMixin<K, V>;
|
|
|
|
void expectNotIn(Map<Object?, Object?> map, Object? key) {
|
|
Expect.isFalse(map.containsKey(key));
|
|
Expect.isNull(map[key]);
|
|
Expect.isFalse(map.keys.contains(key));
|
|
}
|