| // Copyright (c) 2018, 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. |
| |
| 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({}); |
| test(new LinkedHashMap()); |
| test(new HashMap()); |
| test(new LinkedHashMap.identity()); |
| test(new HashMap.identity()); |
| test(new MapView(new HashMap())); |
| test(new MapBaseMap()); |
| test(new MapMixinMap()); |
| test(newJsonMap()); |
| test(newJsonMapCustomReviver()); |
| testNonNull(new SplayTreeMap()); |
| testNonNull(new SplayTreeMap(Comparable.compare)); |
| testNonNull(new MapView(new SplayTreeMap())); |
| } |
| |
| void test(Map<Comparable, Object> map) { |
| testNonNull(map); |
| |
| // Also works with null keys and values (omitted for splay-tree based maps) |
| map.clear(); |
| map.update(null, unreachable, ifAbsent: () => null); |
| Expect.mapEquals({null: null}, map); |
| map.update(null, (v) => "$v", ifAbsent: unreachable); |
| Expect.mapEquals({null: "null"}, map); |
| map.update(null, (v) => null, ifAbsent: unreachable); |
| Expect.mapEquals({null: null}, map); |
| } |
| |
| void testNonNull(Map<Comparable, Object> map) { |
| // Only use literal String keys since JSON maps only accept strings, |
| // and literals works with identity-maps, and it's comparable for SplayTreeMap |
| // maps. |
| Expect.mapEquals({}, map); |
| map.update("key1", unreachable, ifAbsent: () => 42); |
| Expect.mapEquals({"key1": 42}, map); |
| map.clear(); |
| map["key1"] = 42; |
| map.update("key1", (v) => 1 + v, ifAbsent: unreachable); |
| Expect.mapEquals({"key1": 43}, map); |
| map.clear(); |
| |
| // Operations on maps with multiple elements. |
| var multi = { |
| "k1": 1, |
| "k2": 2, |
| "k3": 3, |
| "k4": 4, |
| "k5": 5, |
| "k6": 6, |
| "k7": 7, |
| "k8": 8, |
| "k9": 9, |
| "k10": 10, |
| }; |
| map.addAll(multi); |
| Expect.mapEquals(multi, map); |
| map.update("k3", (v) => 13); |
| map.update("k6", (v) => 16); |
| map.update("k11", unreachable, ifAbsent: () => 21); |
| Expect.mapEquals({ |
| "k1": 1, |
| "k2": 2, |
| "k3": 13, |
| "k4": 4, |
| "k5": 5, |
| "k6": 16, |
| "k7": 7, |
| "k8": 8, |
| "k9": 9, |
| "k10": 10, |
| "k11": 21, |
| }, map); |
| |
| map.clear(); |
| map.updateAll((k, v) => throw "unreachable"); |
| Expect.mapEquals({}, map); |
| |
| map.addAll(multi); |
| map.updateAll((k, v) => "$k:$v"); |
| Expect.mapEquals({ |
| "k1": "k1:1", |
| "k2": "k2:2", |
| "k3": "k3:3", |
| "k4": "k4:4", |
| "k5": "k5:5", |
| "k6": "k6:6", |
| "k7": "k7:7", |
| "k8": "k8:8", |
| "k9": "k9:9", |
| "k10": "k10:10", |
| }, map); |
| |
| map.clear(); |
| Expect.throws( |
| () => map.update("key1", unreachable, ifAbsent: () => throw "expected"), |
| (t) => t == "expected"); |
| |
| map["key1"] = 42; |
| Expect.throws(() => map.update("key1", (_) => throw "expected"), |
| (t) => t == "expected"); |
| |
| // No ifAbsent means throw if key not there. |
| Expect.throws(() => map.update("key-not", unreachable), (e) => e is Error); |
| |
| Expect.throws(() => map.update("key1", null), (e) => e is Error); |
| |
| // Works with null values. |
| map.clear(); |
| map.update("key1", unreachable, ifAbsent: () => null); |
| Expect.mapEquals({"key1": null}, map); |
| map.update("key1", (v) => "$v", ifAbsent: unreachable); |
| Expect.mapEquals({"key1": "null"}, map); |
| map.update("key1", (v) => null, ifAbsent: unreachable); |
| Expect.mapEquals({"key1": null}, map); |
| } |
| |
| // Slow implementation of Map based on MapBase. |
| abstract 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; |
| } |
| |
| Null unreachable([_, __]) => throw "unreachable"; |