dart-sdk/tests/corelib/map_test.dart
Lasse R.H. Nielsen d8fa6fe255 Fix bug in VM _CompactHashIterable.
The iterable is used for the `keys` and `values` of `LinkedHashMap`.
The original code remembered the internal data list and used-count
when the iterable was created, and if the iterable was modified
between creating and iterating, the iterated values would not match
the map.
The solution is to not cache those values, and read them from the
hash table when creting the `Iterator` instead.

Fixes #48282

Tested: Added regression test to corelib/map_test
Bug: https://dartbug.com/48282
Change-Id: I79310615e7090556e6f45b0d7f297755951ef046
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/312263
Commit-Queue: Lasse Nielsen <lrn@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
2023-07-04 11:00:50 +00:00

1174 lines
35 KiB
Dart

// Copyright (c) 2011, 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.
library map_test;
import "package:expect/expect.dart";
import 'dart:collection';
import 'dart:convert' show json;
Map<String, dynamic> newJsonMap() => json.decode('{}');
Map<String, dynamic> newJsonMapCustomReviver() =>
json.decode('{}', reviver: (key, value) => value);
void main() {
test(new HashMap());
test(new LinkedHashMap());
test(new SplayTreeMap());
test(new SplayTreeMap(Comparable.compare));
test(new MapView(new HashMap()));
test(new MapView(new SplayTreeMap()));
test(new MapBaseMap());
test(new MapMixinMap());
test(newJsonMap());
test(newJsonMapCustomReviver());
testLinkedHashMap();
testMapLiteral();
testNullValue();
testTypes();
testUnmodifiableMaps();
testWeirdStringKeys(new Map());
testWeirdStringKeys(new Map<String, String>());
testWeirdStringKeys(new HashMap());
testWeirdStringKeys(new HashMap<String, String>());
testWeirdStringKeys(new LinkedHashMap());
testWeirdStringKeys(new LinkedHashMap<String, String>());
testWeirdStringKeys(new SplayTreeMap());
testWeirdStringKeys(new SplayTreeMap<String, String>());
testWeirdStringKeys(new MapBaseMap<String, String>());
testWeirdStringKeys(new MapMixinMap<String, String>());
testWeirdStringKeys(newJsonMap());
testWeirdStringKeys(newJsonMapCustomReviver());
testNumericKeys(new Map());
testNumericKeys(new Map<num, String>());
testNumericKeys(new HashMap());
testNumericKeys(new HashMap<num, String>());
testNumericKeys(new HashMap.identity());
testNumericKeys(new HashMap<num, String>.identity());
testNumericKeys(new LinkedHashMap());
testNumericKeys(new LinkedHashMap<num, String>());
testNumericKeys(new LinkedHashMap.identity());
testNumericKeys(new LinkedHashMap<num, String>.identity());
testNumericKeys(new MapBaseMap<num, String>());
testNumericKeys(new MapMixinMap<num, String>());
// NaN maps need to have nullable value types because the forEach method
// cannot look up the value and therefore might find `null` instead of the
// actuall value. See MapMixin.forEach in dart:collection/maps.dart
testNaNKeys(new Map());
testNaNKeys(new Map<num, String?>());
testNaNKeys(new HashMap());
testNaNKeys(new HashMap<num, String?>());
testNaNKeys(new LinkedHashMap());
testNaNKeys(new LinkedHashMap<num, String?>());
testNaNKeys(new MapBaseMap<num, String?>());
testNaNKeys(new MapMixinMap<num, String?>());
// Identity maps fail the NaN-keys tests because the test assumes that
// NaN is not equal to NaN.
testIdentityMap(new Map.identity());
testIdentityMap(new HashMap.identity());
testIdentityMap(new LinkedHashMap.identity());
testIdentityMap(new HashMap(equals: identical, hashCode: identityHashCode));
testIdentityMap(
new LinkedHashMap(equals: identical, hashCode: identityHashCode));
testIdentityMap(new HashMap(
equals: (x, y) => identical(x, y), hashCode: (x) => identityHashCode(x)));
testIdentityMap(new LinkedHashMap(
equals: (x, y) => identical(x, y), hashCode: (x) => identityHashCode(x)));
testCustomMap(new HashMap(
equals: myEquals,
hashCode: myHashCode,
isValidKey: (v) => v is Customer));
testCustomMap(new LinkedHashMap(
equals: myEquals,
hashCode: myHashCode,
isValidKey: (v) => v is Customer));
testCustomMap(
new HashMap<Customer, dynamic>(equals: myEquals, hashCode: myHashCode));
testCustomMap(new LinkedHashMap<Customer, dynamic>(
equals: myEquals, hashCode: myHashCode));
testIterationOrder(new LinkedHashMap());
testIterationOrder(new LinkedHashMap.identity());
testIterationOrder(newJsonMap());
testIterationOrder(newJsonMapCustomReviver());
testOtherKeys(new SplayTreeMap<int, int>());
testOtherKeys(
new SplayTreeMap<int, int>((int a, int b) => a - b, (v) => v is int));
testOtherKeys(new SplayTreeMap((int a, int b) => a - b, (v) => v is int));
testOtherKeys(new HashMap<int, int>());
testOtherKeys(new HashMap<int, int>.identity());
testOtherKeys(new HashMap<int, int>(
hashCode: (v) => v.hashCode, isValidKey: (v) => v is int));
testOtherKeys(new HashMap(
equals: (int x, int y) => x == y,
hashCode: (int v) => v.hashCode,
isValidKey: (v) => v is int));
testOtherKeys(new LinkedHashMap<int, int>());
testOtherKeys(new LinkedHashMap<int, int>.identity());
testOtherKeys(new LinkedHashMap<int, int>(
hashCode: (v) => v.hashCode, isValidKey: (v) => v is int));
testOtherKeys(new LinkedHashMap(
equals: (int x, int y) => x == y,
hashCode: (int v) => v.hashCode,
isValidKey: (v) => v is int));
testOtherKeys(new MapBaseMap<int, int>());
testOtherKeys(new MapMixinMap<int, int>());
testUnmodifiableMap(const {1: 37});
testUnmodifiableMap(new UnmodifiableMapView({1: 37}));
testUnmodifiableMap(new UnmodifiableMapBaseMap([1, 37]));
testTypeAnnotations(new HashMap());
testTypeAnnotations(new LinkedHashMap());
testTypeAnnotations(new HashMap(equals: identical));
testTypeAnnotations(new LinkedHashMap(equals: identical));
testTypeAnnotations(new HashMap(
equals: (int a, int b) => a == b,
hashCode: (int a) => a.hashCode,
isValidKey: (a) => a is int));
testTypeAnnotations(new LinkedHashMap(
equals: (int a, int b) => a == b,
hashCode: (int a) => a.hashCode,
isValidKey: (a) => a is int));
testFrom();
testLazyKeysValueEntries();
}
void test<K, V>(Map<K, V> map) {
testDeletedElement(map);
if (map is Map<int, dynamic>) {
testMap(map, 1, 2, 3, 4, 5, 6, 7, 8);
} else {
map.clear();
testMap(map, "value1", "value2", "value3", "value4", "value5", "value6",
"value7", "value8");
}
}
void testLinkedHashMap() {
LinkedHashMap map = new LinkedHashMap();
Expect.isFalse(map.containsKey(1));
map[1] = 1;
map[1] = 2;
testLength(1, map);
}
void testMap<K, V>(
Map<K, V> typedMap, key1, key2, key3, key4, key5, key6, key7, key8) {
dynamic map = typedMap;
int value1 = 10;
int value2 = 20;
int value3 = 30;
int value4 = 40;
int value5 = 50;
int value6 = 60;
int value7 = 70;
int value8 = 80;
testLength(0, map);
map[key1] = value1;
Expect.equals(value1, map[key1]);
map[key1] = value2;
Expect.isFalse(map.containsKey(key2));
testLength(1, map);
map[key1] = value1;
Expect.equals(value1, map[key1]);
// Add enough entries to make sure the table grows.
map[key2] = value2;
Expect.equals(value2, map[key2]);
testLength(2, map);
map[key3] = value3;
Expect.equals(value2, map[key2]);
Expect.equals(value3, map[key3]);
map[key4] = value4;
Expect.equals(value3, map[key3]);
Expect.equals(value4, map[key4]);
map[key5] = value5;
Expect.equals(value4, map[key4]);
Expect.equals(value5, map[key5]);
map[key6] = value6;
Expect.equals(value5, map[key5]);
Expect.equals(value6, map[key6]);
map[key7] = value7;
Expect.equals(value6, map[key6]);
Expect.equals(value7, map[key7]);
map[key8] = value8;
Expect.equals(value1, map[key1]);
Expect.equals(value2, map[key2]);
Expect.equals(value3, map[key3]);
Expect.equals(value4, map[key4]);
Expect.equals(value5, map[key5]);
Expect.equals(value6, map[key6]);
Expect.equals(value7, map[key7]);
Expect.equals(value8, map[key8]);
testLength(8, map);
map.remove(key4);
Expect.isFalse(map.containsKey(key4));
testLength(7, map);
// Test clearing the table.
map.clear();
testLength(0, map);
Expect.isFalse(map.containsKey(key1));
Expect.isFalse(map.containsKey(key2));
Expect.isFalse(map.containsKey(key3));
Expect.isFalse(map.containsKey(key4));
Expect.isFalse(map.containsKey(key5));
Expect.isFalse(map.containsKey(key6));
Expect.isFalse(map.containsKey(key7));
Expect.isFalse(map.containsKey(key8));
// Test adding and removing again.
map[key1] = value1;
Expect.equals(value1, map[key1]);
testLength(1, map);
map[key2] = value2;
Expect.equals(value2, map[key2]);
testLength(2, map);
map[key3] = value3;
Expect.equals(value3, map[key3]);
map.remove(key3);
testLength(2, map);
map[key4] = value4;
Expect.equals(value4, map[key4]);
map.remove(key4);
testLength(2, map);
map[key5] = value5;
Expect.equals(value5, map[key5]);
map.remove(key5);
testLength(2, map);
map[key6] = value6;
Expect.equals(value6, map[key6]);
map.remove(key6);
testLength(2, map);
map[key7] = value7;
Expect.equals(value7, map[key7]);
map.remove(key7);
testLength(2, map);
map[key8] = value8;
Expect.equals(value8, map[key8]);
map.remove(key8);
testLength(2, map);
Expect.isTrue(map.containsKey(key1));
Expect.isTrue(map.containsValue(value1));
// Test Map.forEach.
Map otherMap = new Map<K, V>();
void testForEachMap(key, value) {
otherMap[key] = value;
}
map.forEach(testForEachMap);
Expect.isTrue(otherMap.containsKey(key1));
Expect.isTrue(otherMap.containsKey(key2));
Expect.isTrue(otherMap.containsValue(value1));
Expect.isTrue(otherMap.containsValue(value2));
Expect.equals(2, otherMap.length);
otherMap.clear();
Expect.equals(0, otherMap.length);
// Test Collection.keys.
void testForEachKey(key) {
otherMap[key] = null;
}
Iterable keys = map.keys;
keys.forEach(testForEachKey);
Expect.isTrue(otherMap.containsKey(key1));
Expect.isTrue(otherMap.containsKey(key2));
Expect.isFalse(otherMap.containsKey(value1));
Expect.isFalse(otherMap.containsKey(value2));
Expect.isTrue(otherMap.containsValue(null));
Expect.isFalse(otherMap.containsValue(value1));
Expect.isFalse(otherMap.containsValue(value2));
Expect.equals(2, otherMap.length);
otherMap.clear();
Expect.equals(0, otherMap.length);
// Test Collection.values.
void testForEachValue(value) {
if (value == value1) {
otherMap[key1] = value;
} else if (value == value2) {
otherMap[key2] = value;
} else {
otherMap[key3] = null;
}
}
Iterable values = map.values;
values.forEach(testForEachValue);
Expect.isTrue(otherMap.containsKey(key1));
Expect.isTrue(otherMap.containsKey(key2));
Expect.isFalse(otherMap.containsKey(value1));
Expect.isFalse(otherMap.containsKey(value2));
Expect.isTrue(otherMap.containsValue(value1));
Expect.isTrue(otherMap.containsValue(value2));
Expect.isFalse(otherMap.containsValue(value3));
Expect.isFalse(otherMap.containsValue(key1));
Expect.isFalse(otherMap.containsValue(null));
Expect.equals(2, otherMap.length);
otherMap.clear();
Expect.equals(0, otherMap.length);
// Test Map.putIfAbsent.
map.clear();
Expect.isFalse(map.containsKey(key1));
map.putIfAbsent(key1, () => 10);
Expect.isTrue(map.containsKey(key1));
Expect.equals(10, map[key1]);
Expect.equals(10, map.putIfAbsent(key1, () => 11));
// Test Map.addAll.
map.clear();
otherMap.clear();
otherMap['99'] = 1;
otherMap['50'] = 50;
otherMap['1'] = 99;
map.addAll(otherMap);
Expect.equals(3, map.length);
Expect.equals(1, map['99']);
Expect.equals(50, map['50']);
Expect.equals(99, map['1']);
otherMap['50'] = 42;
map.addAll(new HashMap<K, V>.from(otherMap));
Expect.equals(3, map.length);
Expect.equals(1, map['99']);
Expect.equals(42, map['50']);
Expect.equals(99, map['1']);
otherMap['99'] = 7;
map.addAll(new SplayTreeMap<K, V>.from(otherMap));
Expect.equals(3, map.length);
Expect.equals(7, map['99']);
Expect.equals(42, map['50']);
Expect.equals(99, map['1']);
otherMap.remove('99');
map['99'] = 0;
map.addAll(otherMap);
Expect.equals(3, map.length);
Expect.equals(0, map['99']);
Expect.equals(42, map['50']);
Expect.equals(99, map['1']);
map.clear();
otherMap.clear();
map.addAll(otherMap);
Expect.equals(0, map.length);
}
void testDeletedElement(Map map) {
map.clear();
for (int i = 0; i < 100; i++) {
map['1'] = 2;
testLength(1, map);
map.remove('1');
testLength(0, map);
}
testLength(0, map);
}
void testMapLiteral() {
var m = {"a": 1, "b": 2, "c": 3};
Expect.equals(3, m.length);
int sum = 0;
m.forEach((a, b) {
sum += b;
});
Expect.equals(6, sum);
List values = m.keys.toList();
Expect.equals(3, values.length);
String first = values[0];
String second = values[1];
String third = values[2];
String all = "${first}${second}${third}";
Expect.equals(3, all.length);
Expect.isTrue(all.contains("a", 0));
Expect.isTrue(all.contains("b", 0));
Expect.isTrue(all.contains("c", 0));
}
void testNullValue() {
Map m = {"a": 1, "b": null, "c": 3};
Expect.equals(null, m["b"]);
Expect.isTrue(m.containsKey("b"));
Expect.equals(3, m.length);
m["a"] = null;
m["c"] = null;
Expect.equals(null, m["a"]);
Expect.isTrue(m.containsKey("a"));
Expect.equals(null, m["c"]);
Expect.isTrue(m.containsKey("c"));
Expect.equals(3, m.length);
m.remove("a");
Expect.equals(2, m.length);
Expect.equals(null, m["a"]);
Expect.isFalse(m.containsKey("a"));
}
void testTypes() {
testMap(Map<num, String> map) {
Expect.isTrue(map is Map<num, String>);
Expect.isTrue(map is! Map<String, dynamic>);
Expect.isTrue(map is! Map<dynamic, int>);
// Use with properly typed keys and values.
map[42] = "text1";
map[43] = "text2";
map[42] = "text3";
Expect.equals("text3", map.remove(42));
Expect.equals(null, map[42]);
map[42] = "text4";
// Ensure that "containsKey", "containsValue" and "remove"
// accepts any object.
for (var object in [true, null, new Object()]) {
Expect.isFalse(map.containsKey(object));
Expect.isFalse(map.containsValue(object));
Expect.isNull(map.remove(object));
Expect.isNull(map[object]);
}
}
testMap(new HashMap<int, String>());
testMap(new LinkedHashMap<int, String>());
testMap(new SplayTreeMap<int, String>());
testMap(new SplayTreeMap<int, String>(Comparable.compare));
testMap(new SplayTreeMap<int, String>((int a, int b) => a.compareTo(b)));
testMap(new HashMap<num, String>());
testMap(new LinkedHashMap<num, String>());
testMap(new SplayTreeMap<num, String>());
testMap(new SplayTreeMap<num, String>(Comparable.compare));
testMap(new SplayTreeMap<num, String>((num a, num b) => a.compareTo(b)));
}
void testWeirdStringKeys(Map map) {
// Test weird keys.
var weirdKeys = const [
'hasOwnProperty',
'constructor',
'toLocaleString',
'propertyIsEnumerable',
'__defineGetter__',
'__defineSetter__',
'__lookupGetter__',
'__lookupSetter__',
'isPrototypeOf',
'toString',
'valueOf',
'__proto__',
'__count__',
'__parent__',
''
];
Expect.isTrue(map.isEmpty);
for (var key in weirdKeys) {
Expect.isFalse(map.containsKey(key));
Expect.equals(null, map[key]);
var value = 'value:$key';
map[key] = value;
Expect.isTrue(map.containsKey(key));
Expect.equals(value, map[key]);
Expect.equals(value, map.remove(key));
Expect.isFalse(map.containsKey(key));
Expect.equals(null, map[key]);
}
Expect.isTrue(map.isEmpty);
}
void testNumericKeys(Map map) {
var numericKeys = const [
double.infinity,
double.negativeInfinity,
0,
0.0,
-0.0
];
Expect.isTrue(map.isEmpty);
for (var key in numericKeys) {
Expect.isFalse(map.containsKey(key));
Expect.equals(null, map[key]);
var value = 'value:$key';
map[key] = value;
Expect.isTrue(map.containsKey(key));
Expect.equals(value, map[key]);
Expect.equals(value, map.remove(key));
Expect.isFalse(map.containsKey(key));
Expect.equals(null, map[key]);
}
Expect.isTrue(map.isEmpty);
}
void testNaNKeys(Map map) {
Object nan = double.nan;
// Skip this test on platforms that use native-JS NaN semantics for speed.
if (!identical(nan, nan)) return;
Expect.isTrue(map.isEmpty);
// Test NaN.
Expect.isFalse(map.containsKey(nan));
Expect.equals(null, map[nan]);
map[nan] = 'value:0';
Expect.isFalse(map.containsKey(nan));
Expect.equals(null, map[nan]);
testLength(1, map);
map[nan] = 'value:1';
Expect.isFalse(map.containsKey(nan));
Expect.equals(null, map[nan]);
testLength(2, map);
Expect.equals(null, map.remove(nan));
testLength(2, map);
var count = 0;
map.forEach((key, value) {
if (key.isNaN) count++;
});
Expect.equals(2, count);
map.clear();
Expect.isTrue(map.isEmpty);
}
void testLength(int length, Map map) {
Expect.equals(length, map.length);
Expect.equals(length, map.keys.length);
Expect.equals(length, map.values.length);
// Check being-empty.
var ifEmpty = (length == 0) ? Expect.isTrue : Expect.isFalse;
var ifNotEmpty = (length != 0) ? Expect.isTrue : Expect.isFalse;
ifEmpty(map.isEmpty);
ifNotEmpty(map.isNotEmpty);
ifEmpty(map.keys.isEmpty);
ifNotEmpty(map.keys.isNotEmpty);
ifEmpty(map.values.isEmpty);
ifNotEmpty(map.values.isNotEmpty);
// Test key/value iterators match their isEmpty/isNotEmpty.
ifNotEmpty(map.keys.iterator.moveNext());
ifNotEmpty(map.values.iterator.moveNext());
if (length == 0) {
for (var k in map.keys) Expect.fail("contains key when iterating: $k");
for (var v in map.values) Expect.fail("contains values when iterating: $v");
}
}
testIdentityMap<K, V>(Map<K, V> typedMap) {
Map map = typedMap;
Expect.isTrue(map.isEmpty);
var nan = double.nan;
// TODO(11551): Remove guard when dart2js makes identical(NaN, NaN) true.
if (identical(nan, nan)) {
map[nan] = 42;
testLength(1, map);
Expect.isTrue(map.containsKey(nan));
Expect.equals(42, map[nan]);
map[nan] = 37;
testLength(1, map);
Expect.equals(37, map[nan]);
Expect.equals(37, map.remove(nan));
testLength(0, map);
}
Vampire v1 = const Vampire(1);
Vampire v2 = const Vampire(2);
Expect.isFalse(v1 == v1);
Expect.isFalse(v2 == v2);
Expect.isTrue(v2 == v1); // Snob!
map[v1] = 1;
map[v2] = 2;
testLength(2, map);
Expect.isTrue(map.containsKey(v1));
Expect.isTrue(map.containsKey(v2));
Expect.equals(1, map[v1]);
Expect.equals(2, map[v2]);
Expect.equals(1, map.remove(v1));
testLength(1, map);
Expect.isFalse(map.containsKey(v1));
Expect.isTrue(map.containsKey(v2));
Expect.isNull(map.remove(v1));
Expect.equals(2, map.remove(v2));
testLength(0, map);
var eq01 = new Equalizer(0);
var eq02 = new Equalizer(0);
var eq11 = new Equalizer(1);
var eq12 = new Equalizer(1);
// Sanity.
Expect.equals(eq01, eq02);
Expect.equals(eq02, eq01);
Expect.equals(eq11, eq12);
Expect.equals(eq12, eq11);
Expect.notEquals(eq01, eq11);
Expect.notEquals(eq01, eq12);
Expect.notEquals(eq02, eq11);
Expect.notEquals(eq02, eq12);
Expect.notEquals(eq11, eq01);
Expect.notEquals(eq11, eq02);
Expect.notEquals(eq12, eq01);
Expect.notEquals(eq12, eq02);
map[eq01] = 0;
map[eq02] = 1;
map[eq11] = 2;
map[eq12] = 3;
testLength(4, map);
Expect.equals(0, map[eq01]);
Expect.equals(1, map[eq02]);
Expect.equals(2, map[eq11]);
Expect.equals(3, map[eq12]);
Expect.isTrue(map.containsKey(eq01));
Expect.isTrue(map.containsKey(eq02));
Expect.isTrue(map.containsKey(eq11));
Expect.isTrue(map.containsKey(eq12));
Expect.equals(1, map.remove(eq02));
Expect.equals(3, map.remove(eq12));
testLength(2, map);
Expect.isTrue(map.containsKey(eq01));
Expect.isFalse(map.containsKey(eq02));
Expect.isTrue(map.containsKey(eq11));
Expect.isFalse(map.containsKey(eq12));
Expect.equals(0, map[eq01]);
Expect.equals(null, map[eq02]);
Expect.equals(2, map[eq11]);
Expect.equals(null, map[eq12]);
Expect.equals(0, map.remove(eq01));
Expect.equals(2, map.remove(eq11));
testLength(0, map);
map[eq01] = 0;
map[eq02] = 1;
map[eq11] = 2;
map[eq12] = 3;
testLength(4, map);
// Transfer to equality-based map will collapse elements.
Map eqMap = new HashMap<K, V>();
eqMap.addAll(map);
testLength(2, eqMap);
Expect.isTrue(eqMap.containsKey(eq01));
Expect.isTrue(eqMap.containsKey(eq02));
Expect.isTrue(eqMap.containsKey(eq11));
Expect.isTrue(eqMap.containsKey(eq12));
// Changing objects will not affect identity map.
map.clear();
var m1 = new Mutable(1);
var m2 = new Mutable(2);
var m3 = new Mutable(3);
map[m1] = 1;
map[m2] = 2;
map[m3] = 3;
Expect.equals(3, map.length);
Expect.isTrue(map.containsKey(m1));
Expect.isTrue(map.containsKey(m2));
Expect.isTrue(map.containsKey(m3));
Expect.notEquals(m1, m3);
m3.id = 1;
Expect.equals(m1, m3);
// Even if keys are equal, they are still not identical.
// Even if hashcode of m3 changed, it can still be found.
Expect.equals(1, map[m1]);
Expect.equals(3, map[m3]);
}
/** Class of objects that are equal if they hold the same id. */
class Equalizer {
int id;
Equalizer(this.id);
int get hashCode => id;
bool operator ==(Object other) => other is Equalizer && id == other.id;
}
/**
* Objects that are not reflexive.
*
* They think they are better than their equals.
*/
class Vampire {
final int generation;
const Vampire(this.generation);
int get hashCode => generation;
// The double-fang operator falsely claims that a vampire is equal to
// any of its sire's generation.
bool operator ==(Object other) =>
other is Vampire && generation - 1 == other.generation;
}
void testCustomMap<K, V>(Map<K, V> typedMap) {
Map map = typedMap;
testLength(0, map);
var c11 = const Customer(1, 1);
var c12 = const Customer(1, 2);
var c21 = const Customer(2, 1);
var c22 = const Customer(2, 2);
// Sanity.
Expect.equals(c11, c12);
Expect.notEquals(c11, c21);
Expect.notEquals(c11, c22);
Expect.equals(c21, c22);
Expect.notEquals(c21, c11);
Expect.notEquals(c21, c12);
Expect.isTrue(myEquals(c11, c21));
Expect.isFalse(myEquals(c11, c12));
Expect.isFalse(myEquals(c11, c22));
Expect.isTrue(myEquals(c12, c22));
Expect.isFalse(myEquals(c12, c11));
Expect.isFalse(myEquals(c12, c21));
map[c11] = 42;
testLength(1, map);
Expect.isTrue(map.containsKey(c11));
Expect.isTrue(map.containsKey(c21));
Expect.isFalse(map.containsKey(c12));
Expect.isFalse(map.containsKey(c22));
Expect.equals(42, map[c11]);
Expect.equals(42, map[c21]);
map[c21] = 37;
testLength(1, map);
Expect.isTrue(map.containsKey(c11));
Expect.isTrue(map.containsKey(c21));
Expect.isFalse(map.containsKey(c12));
Expect.isFalse(map.containsKey(c22));
Expect.equals(37, map[c11]);
Expect.equals(37, map[c21]);
map[c22] = 42;
testLength(2, map);
Expect.isTrue(map.containsKey(c11));
Expect.isTrue(map.containsKey(c21));
Expect.isTrue(map.containsKey(c12));
Expect.isTrue(map.containsKey(c22));
Expect.equals(37, map[c11]);
Expect.equals(37, map[c21]);
Expect.equals(42, map[c12]);
Expect.equals(42, map[c22]);
Expect.equals(42, map.remove(c12));
testLength(1, map);
Expect.isTrue(map.containsKey(c11));
Expect.isTrue(map.containsKey(c21));
Expect.isFalse(map.containsKey(c12));
Expect.isFalse(map.containsKey(c22));
Expect.equals(37, map[c11]);
Expect.equals(37, map[c21]);
Expect.equals(37, map.remove(c11));
testLength(0, map);
}
void testUnmodifiableMap(Map map) {
Expect.isTrue(map.containsKey(1));
testLength(1, map);
Expect.equals(1, map.keys.first);
Expect.equals(37, map.values.first);
Expect.throws(map.clear);
Expect.throws(() {
map.remove(1);
});
Expect.throws(() {
map[2] = 42;
});
Expect.throws(() {
map.addAll({2: 42});
});
}
class Customer {
final int id;
final int secondId;
const Customer(this.id, this.secondId);
int get hashCode => id;
bool operator ==(Object other) {
if (other is! Customer) return false;
Customer otherCustomer = other;
return id == otherCustomer.id;
}
}
int myHashCode(Customer c) => c.secondId;
bool myEquals(Customer a, Customer b) => a.secondId == b.secondId;
void testIterationOrder(Map map) {
var order = ['0', '6', '4', '2', '7', '9', '7', '1', '2', '5', '3'];
for (int i = 0; i < order.length; i++) map[order[i]] = i;
Expect.listEquals(
map.keys.toList(), ['0', '6', '4', '2', '7', '9', '1', '5', '3']);
Expect.listEquals(map.values.toList(), [0, 1, 2, 8, 6, 5, 7, 9, 10]);
}
void testOtherKeys(Map<int, int> map) {
// Test that non-int keys are allowed in containsKey/remove/lookup.
// Custom hash sets and tree sets must be constructed so they don't
// use the equality/comparator on incompatible objects.
// This should not throw in either checked or unchecked mode.
Expect.isFalse(map.containsKey("not an int"));
Expect.isFalse(map.containsKey(1.5));
Expect.isNull(map.remove("not an int"));
Expect.isNull(map.remove(1.5));
Expect.isNull(map["not an int"]);
Expect.isNull(map[1.5]);
}
class Mutable {
int id;
Mutable(this.id);
int get hashCode => id;
bool operator ==(other) => other is Mutable && other.id == id;
}
// Slow implementation of Map based on MapBase.
mixin class MapBaseOperations<K, V> {
final List _keys = <K>[];
final List _values = <V>[];
int _modCount = 0;
V? operator [](Object? key) {
int index = _keys.indexOf(key);
if (index < 0) return null;
return _values[index];
}
Iterable<K> get keys => new TestKeyIterable<K>(this);
void operator []=(K key, V value) {
int index = _keys.indexOf(key);
if (index >= 0) {
_values[index] = value;
} else {
_modCount++;
_keys.add(key);
_values.add(value);
}
}
V? remove(Object? key) {
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;
}
_modCount++;
return result;
}
return null;
}
void clear() {
// Clear cannot be based on remove, since remove won't remove keys that
// are not equal to themselves. It will fail the testNaNKeys test.
_keys.clear();
_values.clear();
_modCount++;
}
}
class MapBaseMap<K, V> = MapBase<K, V> with MapBaseOperations<K, V>;
class MapMixinMap<K, V> = MapBaseOperations<K, V> with MapMixin<K, V>;
class TestKeyIterable<K> extends IterableBase<K> {
final _map;
TestKeyIterable(this._map);
int get length => _map._keys.length;
Iterator<K> get iterator => new TestKeyIterator<K>(_map);
}
class TestKeyIterator<K> implements Iterator<K> {
final _map;
final int _modCount;
int _index = 0;
var _current;
TestKeyIterator(map)
: _map = map,
_modCount = map._modCount;
bool moveNext() {
if (_modCount != _map._modCount) {
throw new ConcurrentModificationError(_map);
}
if (_index == _map._keys.length) {
_current = null;
return false;
}
_current = _map._keys[_index++];
return true;
}
K get current => _current;
}
// Slow implementation of Map based on MapBase.
class UnmodifiableMapBaseMap<K, V> extends UnmodifiableMapBase<K, V> {
final List<K> _keys = [];
final List<V> _values = [];
UnmodifiableMapBaseMap(List pairs) {
for (int i = 0; i < pairs.length; i += 2) {
_keys.add(pairs[i]);
_values.add(pairs[i + 1]);
}
}
int get _modCount => 0;
V? operator [](Object? key) {
int index = _keys.indexOf(key as K);
if (index < 0) return null;
return _values[index];
}
Iterable<K> get keys => _keys.skip(0);
}
abstract class Super implements Comparable {}
abstract class Interface implements Comparable {}
class Sub extends Super implements Interface, Comparable {
int compareTo(dynamic other) => 0;
int get hashCode => 0;
bool operator ==(other) => other is Sub;
}
expectMap(Map expect, Map actual) {
Expect.equals(expect.length, actual.length, "length");
for (var key in expect.keys) {
Expect.isTrue(actual.containsKey(key), "containsKey $key");
Expect.equals(expect[key], actual[key]);
}
}
void testFrom() {
// Check contents.
for (var map in [
{},
{1: 1},
{1: 2, 3: 4, 5: 6, 7: 8}
]) {
expectMap(map, new Map.from(map));
expectMap(map, new HashMap.from(map));
expectMap(map, new LinkedHashMap.from(map));
expectMap(map, new SplayTreeMap.from(map));
}
// Test type combinations allowed.
Map<int, int> intMap = <int, int>{1: 2, 3: 4};
Map<num, num> numMap = <num, num>{1: 2, 3: 4};
expectMap(intMap, new Map<int, int>.from(numMap));
expectMap(intMap, new Map<num, num>.from(intMap));
expectMap(intMap, new HashMap<int, int>.from(numMap));
expectMap(intMap, new HashMap<num, num>.from(intMap));
expectMap(intMap, new LinkedHashMap<int, int>.from(numMap));
expectMap(intMap, new LinkedHashMap<num, num>.from(intMap));
expectMap(intMap, new SplayTreeMap<int, int>.from(numMap));
expectMap(intMap, new SplayTreeMap<num, num>.from(intMap));
var sub = new Sub();
Map<Super, Super> superMap = <Super, Super>{sub: sub};
Map<Interface, Interface> interfaceMap = <Interface, Interface>{sub: sub};
expectMap(superMap, new Map<Super, Super>.from(interfaceMap));
expectMap(superMap, new Map<Interface, Interface>.from(superMap));
expectMap(superMap, new HashMap<Super, Super>.from(interfaceMap));
expectMap(superMap, new HashMap<Interface, Interface>.from(superMap));
expectMap(superMap, new LinkedHashMap<Super, Super>.from(interfaceMap));
expectMap(superMap, new LinkedHashMap<Interface, Interface>.from(superMap));
expectMap(superMap, new SplayTreeMap<Super, Super>.from(interfaceMap));
expectMap(superMap, new SplayTreeMap<Interface, Interface>.from(superMap));
}
void testTypeAnnotations(Map<int, int> map) {
map[0] = 100;
map[999] = 101;
map[0x800000000] = 102;
map[0x20000000000000] = 103;
Expect.isFalse(map.containsKey("not an it"));
Expect.isNull(map.remove("not an it"));
testLength(4, map);
Expect.equals(101, map.remove(999));
testLength(3, map);
Expect.equals(102, map.remove(0x800000000));
testLength(2, map);
Expect.equals(103, map.remove(0x20000000000000));
testLength(1, map);
}
void testUnmodifiableMaps() {
void checkUnmodifiable(Map<int, int> map) {
Expect.throws(() => map[0] = 0);
Expect.throws(() => map.addAll({0: 0}));
Expect.throws(() => map.addEntries({0: 0}.entries));
Expect.throws(() => map.clear());
Expect.throws(() => map.putIfAbsent(0, () => 0));
Expect.throws(() => map.remove(0));
Expect.throws(() => map.removeWhere((k, v) => true));
Expect.throws(() => map.update(0, (v) => v, ifAbsent: () => 0));
Expect.throws(() => map.updateAll((k, v) => v));
}
checkUnmodifiable(const {1: 1});
checkUnmodifiable(Map.unmodifiable({1: 1}));
checkUnmodifiable(UnmodifiableMapView({1: 1}));
checkUnmodifiable(const MapView({1: 1}));
}
void testLazyKeysValueEntries() {
// Regression test for https://dartbug.com/48282
//
// Checks that the keys, values and entries iterables are lazy iterables
// backed by the map.
//
// Creates a fresh map, then fills in entries, and then removes them again.
// At each step, check that the `keys`, `values` and `entries` iterables
// contain the expected elements. (Not checking ordering, since HashMap
// doesn't guarantee one.)
const mapSize = 129;
void testWithKeyType<K extends Comparable<Object>>(
String keyTypeName, K Function(int) toKey) {
void testWithMap(String mapType, Map<K, int> map) {
// This test does a lot of linear work per element, so too large
// mapSize makes it slow.
// It prints a diagnostic at the end if a test is particularly slow.
var sw = Stopwatch()..start();
String testName = "$mapType<$keyTypeName, int>";
var keys = map.keys;
var values = map.values;
var entries = map.entries;
Expect.equals(0, map.length, testName);
Expect.equals(0, keys.length, testName);
Expect.equals(0, values.length, testName);
Expect.equals(0, entries.length, testName);
Expect.isTrue(keys.toList().isEmpty, testName);
Expect.isTrue(values.toList().isEmpty, testName);
Expect.isTrue(entries.toList().isEmpty, testName);
for (var i = 0; i < mapSize; i++) {
map[toKey(i)] = i;
Expect.equals(i + 1, map.length, testName);
Expect.equals(i + 1, keys.length, testName);
Expect.equals(i + 1, values.length, testName);
Expect.equals(i + 1, entries.length, testName);
Expect.listEquals(
[for (var j = 0; j <= i; j++) toKey(j)]..sort(),
keys.map((x) => x).toList()..sort(),
testName,
);
Expect.listEquals(
[for (var j = 0; j <= i; j++) j],
values.map((x) => x).toList()..sort(),
testName,
);
{
// No `operator==` on `MapEntry`.
var currentEntries = entries.map((x) => x).toList()
..sort((e1, e2) => e1.value.compareTo(e2.value));
for (var j = 0; j <= i; j++) {
var currentEntry = currentEntries[j];
Expect.equals(toKey(j), currentEntry.key, testName);
Expect.equals(j, currentEntry.value, testName);
}
}
Expect.equals(map.keys.last, keys.last);
Expect.equals(map.values.last, values.last);
Expect.equals(map.keys.last, entries.last.key);
Expect.equals(map.values.last, entries.last.value);
}
for (var i = 0; i < mapSize; i++) {
Expect.equals(map.keys.first, keys.first);
Expect.equals(map.values.first, values.first);
Expect.equals(map.keys.first, entries.first.key);
Expect.equals(map.values.first, entries.first.value);
var removed = map.remove(toKey(i));
Expect.equals(i, removed, testName);
Expect.equals(mapSize - 1 - i, map.length, testName);
Expect.equals(mapSize - 1 - i, keys.length, testName);
Expect.equals(mapSize - 1 - i, values.length, testName);
Expect.equals(mapSize - 1 - i, entries.length, testName);
Expect.listEquals(
[for (var j = i + 1; j < mapSize; j++) toKey(j)]..sort(),
keys.map((x) => x).toList()..sort(),
testName,
);
Expect.listEquals(
[for (var j = i + 1; j < mapSize; j++) j],
values.map((x) => x).toList()..sort(),
testName,
);
{
// No `operator==` on `MapEntry`.
var currentEntries = entries.map((x) => x).toList()
..sort((e1, e2) => e1.value.compareTo(e2.value));
for (var j = i + 1; j < mapSize; j++) {
var currentEntry = currentEntries[j - i - 1];
Expect.equals(toKey(j), currentEntry.key, testName);
Expect.equals(j, currentEntry.value, testName);
}
}
}
Expect.isTrue(keys.isEmpty, testName);
Expect.isTrue(values.isEmpty, testName);
Expect.isTrue(entries.isEmpty, testName);
var elapsed = sw.elapsedMilliseconds;
if (elapsed > 200) {
print("$testName: $elapsed ms");
}
}
testWithMap("SplayTreeMap", SplayTreeMap<K, int>());
testWithMap("HashMap", HashMap<K, int>());
testWithMap("LinkedHashMap", LinkedHashMap<K, int>());
testWithMap("LinkedHashMap-literal", <K, int>{});
}
testWithKeyType<int>("int", (int x) => x);
testWithKeyType<String>("String", (int x) => "$x");
testWithKeyType<Key>("Key", Key.new);
}
class Key implements Comparable<Key> {
final int id;
Key(this.id);
int get hashCode => id.hashCode ^ 1023;
bool operator ==(Object other) => other is Key && id == other.id;
int compareTo(Key other) => id.compareTo(other.id);
}