| // Copyright (c) 2012, 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. | |
| /** | |
| * Tests for the toString methods on collections and maps. | |
| */ | |
| library collection_to_string; | |
| import "package:expect/expect.dart"; | |
| import 'dart:collection' show Queue, LinkedHashMap; | |
| import 'dart:math' as Math; | |
| // TODO(jjb): seed random number generator when API allows it | |
| const int NUM_TESTS = 300; | |
| const int MAX_COLLECTION_SIZE = 7; | |
| final rand = Math.Random(); | |
| main() { | |
| smokeTest(); | |
| exactTest(); | |
| inexactTest(); | |
| } | |
| /** | |
| * Test a few simple examples. | |
| */ | |
| void smokeTest() { | |
| // Non-const lists | |
| Expect.equals([].toString(), '[]'); | |
| Expect.equals([1].toString(), '[1]'); | |
| Expect.equals(['Elvis'].toString(), '[Elvis]'); | |
| Expect.equals([null].toString(), '[null]'); | |
| Expect.equals([1, 2].toString(), '[1, 2]'); | |
| Expect.equals(['I', 'II'].toString(), '[I, II]'); | |
| Expect.equals( | |
| [ | |
| [1, 2], | |
| [3, 4], | |
| [5, 6], | |
| ].toString(), | |
| '[[1, 2], [3, 4], [5, 6]]', | |
| ); | |
| // Const lists | |
| Expect.equals((const []).toString(), '[]'); | |
| Expect.equals((const [1]).toString(), '[1]'); | |
| Expect.equals((const ['Elvis']).toString(), '[Elvis]'); | |
| Expect.equals((const [null]).toString(), '[null]'); | |
| Expect.equals((const [1, 2]).toString(), '[1, 2]'); | |
| Expect.equals((const ['I', 'II']).toString(), '[I, II]'); | |
| Expect.equals( | |
| (const [ | |
| const [1, 2], | |
| const [3, 4], | |
| const [5, 6], | |
| ]).toString(), | |
| '[[1, 2], [3, 4], [5, 6]]', | |
| ); | |
| // Non-const maps - Note that all keys are strings; the spec currently demands this | |
| Expect.equals({}.toString(), '{}'); | |
| Expect.equals({'Elvis': 'King'}.toString(), '{Elvis: King}'); | |
| Expect.equals({'Elvis': null}.toString(), '{Elvis: null}'); | |
| Expect.equals({'I': 1, 'II': 2}.toString(), '{I: 1, II: 2}'); | |
| Expect.equals( | |
| { | |
| 'X': {'I': 1, 'II': 2}, | |
| 'Y': {'III': 3, 'IV': 4}, | |
| 'Z': {'V': 5, 'VI': 6}, | |
| }.toString(), | |
| '{X: {I: 1, II: 2}, Y: {III: 3, IV: 4}, Z: {V: 5, VI: 6}}', | |
| ); | |
| // Const maps | |
| Expect.equals(const {}.toString(), '{}'); | |
| Expect.equals(const {'Elvis': 'King'}.toString(), '{Elvis: King}'); | |
| Expect.equals({'Elvis': null}.toString(), '{Elvis: null}'); | |
| Expect.equals(const {'I': 1, 'II': 2}.toString(), '{I: 1, II: 2}'); | |
| Expect.equals( | |
| const { | |
| 'X': const {'I': 1, 'II': 2}, | |
| 'Y': const {'III': 3, 'IV': 4}, | |
| 'Z': const {'V': 5, 'VI': 6}, | |
| }.toString(), | |
| '{X: {I: 1, II: 2}, Y: {III: 3, IV: 4}, Z: {V: 5, VI: 6}}', | |
| ); | |
| } | |
| // SERIOUS "BASHER" TESTS | |
| /** | |
| * Generate a bunch of random collections (including Maps), and test that | |
| * there string form is as expected. The collections include collections | |
| * as elements, keys, and values, and include recursive references. | |
| * | |
| * This test restricts itself to collections with well-defined iteration | |
| * orders (i.e., no HashSet, HashMap). | |
| */ | |
| void exactTest() { | |
| for (int i = 0; i < NUM_TESTS; i++) { | |
| // Choose a size from 0 to MAX_COLLECTION_SIZE, favoring larger sizes | |
| int size = Math.sqrt(random(MAX_COLLECTION_SIZE * MAX_COLLECTION_SIZE)) | |
| .toInt(); | |
| StringBuffer stringRep = new StringBuffer(); | |
| Object o = randomCollection(size, stringRep, exact: true); | |
| print(stringRep); | |
| print(o); | |
| Expect.equals(o.toString(), stringRep.toString()); | |
| } | |
| } | |
| /** | |
| * Generate a bunch of random collections (including Maps), and test that | |
| * there string form is as expected. The collections include collections | |
| * as elements, keys, and values, and include recursive references. | |
| * | |
| * This test includes collections with ill-defined iteration orders (i.e., | |
| * HashSet, HashMap). As a consequence, it can't use equality tests on the | |
| * string form. Instead, it performs equality tests on their "alphagrams." | |
| * This might allow false positives, but it does give a fair amount of | |
| * confidence. | |
| */ | |
| void inexactTest() { | |
| for (int i = 0; i < NUM_TESTS; i++) { | |
| // Choose a size from 0 to MAX_COLLECTION_SIZE, favoring larger sizes | |
| int size = Math.sqrt(random(MAX_COLLECTION_SIZE * MAX_COLLECTION_SIZE)) | |
| .toInt(); | |
| StringBuffer stringRep = new StringBuffer(); | |
| Object o = randomCollection(size, stringRep, exact: false); | |
| print(stringRep); | |
| print(o); | |
| Expect.equals(alphagram(o.toString()), alphagram(stringRep.toString())); | |
| } | |
| } | |
| /** | |
| * Return a random collection (or Map) of the specified size, placing its | |
| * string representation into the given string buffer. | |
| * | |
| * If exact is true, the returned collections will not be, and will not contain | |
| * a collection with ill-defined iteration order (i.e., a HashSet or HashMap). | |
| */ | |
| Object randomCollection( | |
| int size, | |
| StringBuffer stringRep, { | |
| bool exact = false, | |
| }) { | |
| return randomCollectionHelper(size, exact, stringRep, []); | |
| } | |
| /** | |
| * Return a random collection (or map) of the specified size, placing its | |
| * string representation into the given string buffer. The beingMade | |
| * parameter is a list of collections currently under construction, i.e., | |
| * candidates for recursive references. | |
| * | |
| * If exact is true, the returned collections will not be, and will not contain | |
| * a collection with ill-defined iteration order (i.e., a HashSet or HashMap). | |
| */ | |
| Object randomCollectionHelper( | |
| int size, | |
| bool exact, | |
| StringBuffer stringRep, | |
| List beingMade, | |
| ) { | |
| double interfaceFrac = rand.nextDouble(); | |
| if (exact) { | |
| if (interfaceFrac < 1 / 3) { | |
| return randomList(size, exact, stringRep, beingMade); | |
| } else if (interfaceFrac < 2 / 3) { | |
| return randomQueue(size, exact, stringRep, beingMade); | |
| } else { | |
| return randomMap(size, exact, stringRep, beingMade); | |
| } | |
| } else { | |
| if (interfaceFrac < 1 / 4) { | |
| return randomList(size, exact, stringRep, beingMade); | |
| } else if (interfaceFrac < 2 / 4) { | |
| return randomQueue(size, exact, stringRep, beingMade); | |
| } else if (interfaceFrac < 3 / 4) { | |
| return randomSet(size, exact, stringRep, beingMade); | |
| } else { | |
| return randomMap(size, exact, stringRep, beingMade); | |
| } | |
| } | |
| } | |
| /** | |
| * Return a random List of the specified size, placing its string | |
| * representation into the given string buffer. The beingMade | |
| * parameter is a list of collections currently under construction, i.e., | |
| * candidates for recursive references. | |
| * | |
| * If exact is true, the returned collections will not be, and will not contain | |
| * a collection with ill-defined iteration order (i.e., a HashSet or HashMap). | |
| */ | |
| List randomList(int size, bool exact, StringBuffer stringRep, List beingMade) { | |
| return populateRandomCollection(size, exact, stringRep, beingMade, [], "[]"); | |
| } | |
| /** | |
| * Like randomList, but returns a queue. | |
| */ | |
| Queue randomQueue( | |
| int size, | |
| bool exact, | |
| StringBuffer stringRep, | |
| List beingMade, | |
| ) { | |
| return populateRandomCollection( | |
| size, | |
| exact, | |
| stringRep, | |
| beingMade, | |
| new Queue(), | |
| "{}", | |
| ); | |
| } | |
| /** | |
| * Like randomList, but returns a Set. | |
| */ | |
| Set randomSet(int size, bool exact, StringBuffer stringRep, List beingMade) { | |
| // Until we have LinkedHashSet, method will only be called with exact==true | |
| return populateRandomSet(size, exact, stringRep, beingMade, new Set()); | |
| } | |
| /** | |
| * Like randomList, but returns a map. | |
| */ | |
| Map randomMap(int size, bool exact, StringBuffer stringRep, List beingMade) { | |
| if (exact) { | |
| return populateRandomMap( | |
| size, | |
| exact, | |
| stringRep, | |
| beingMade, | |
| new LinkedHashMap(), | |
| ); | |
| } else { | |
| return populateRandomMap( | |
| size, | |
| exact, | |
| stringRep, | |
| beingMade, | |
| randomBool() ? new Map() : new LinkedHashMap(), | |
| ); | |
| } | |
| } | |
| /** | |
| * Populates the given empty collection with elements, emitting the string | |
| * representation of the collection to stringRep. The beingMade parameter is | |
| * a list of collections currently under construction, i.e., candidates for | |
| * recursive references. | |
| * | |
| * If exact is true, the elements of the returned collections will not be, | |
| * and will not contain a collection with ill-defined iteration order | |
| * (i.e., a HashSet or HashMap). | |
| */ | |
| populateRandomCollection( | |
| int size, | |
| bool exact, | |
| StringBuffer stringRep, | |
| List beingMade, | |
| coll, | |
| String delimiters, | |
| ) { | |
| beingMade.add(coll); | |
| int start = stringRep.length; | |
| stringRep.write(delimiters[0]); | |
| List<int> indices = []; | |
| for (int i = 0; i < size; i++) { | |
| indices.add(stringRep.length); | |
| if (i != 0) stringRep.write(', '); | |
| coll.add(randomElement(random(size), exact, stringRep, beingMade)); | |
| } | |
| if (size > 5 && delimiters == "()") { | |
| const int MAX_LENGTH = 80; | |
| const int MIN_COUNT = 3; | |
| const int MAX_COUNT = 100; | |
| // It's an iterable, it may omit some elements. | |
| int end = stringRep.length; | |
| if (size > MAX_COUNT) { | |
| // Last two elements are also omitted, just find the first three or | |
| // first 60 characters. | |
| for (int i = MIN_COUNT; i < size; i++) { | |
| int startIndex = indices[i]; | |
| if (startIndex - start > MAX_LENGTH - 6) { | |
| // Limit - ", ...)".length. | |
| String prefix = stringRep.toString().substring(0, startIndex); | |
| stringRep.clear(); | |
| stringRep.write(prefix); | |
| stringRep.write(", ..."); | |
| } | |
| } | |
| } else if (stringRep.length - start > MAX_LENGTH - 1) { | |
| // 80 - ")".length. | |
| // Last two elements are always included. Middle ones may be omitted. | |
| int lastTwoLength = end - indices[indices.length - 2]; | |
| // Try to find first element to omit. | |
| for (int i = 3; i <= size - 3; i++) { | |
| int elementEnd = indices[i + 1]; | |
| int lengthAfter = elementEnd - start; | |
| int ellipsisSize = 5; // ", ...".length | |
| if (i == size - 3) ellipsisSize = 0; // No ellipsis if we hit the end. | |
| if (lengthAfter + ellipsisSize + lastTwoLength > MAX_LENGTH - 1) { | |
| // Omit this element and everything up to the last two. | |
| int elementStart = indices[i]; | |
| // Rewrite string buffer by copying it out, clearing, and putting | |
| // the parts back in. | |
| String buffer = stringRep.toString(); | |
| String prefix = buffer.substring(0, elementStart); | |
| String suffix = buffer.substring(end - lastTwoLength, end); | |
| stringRep.clear(); | |
| stringRep.write(prefix); | |
| stringRep.write(", ..."); | |
| stringRep.write(suffix); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| stringRep.write(delimiters[1]); | |
| beingMade.removeLast(); | |
| return coll; | |
| } | |
| /** Like populateRandomCollection, but for sets (elements must be hashable) */ | |
| Set populateRandomSet( | |
| int size, | |
| bool exact, | |
| StringBuffer stringRep, | |
| List beingMade, | |
| Set set, | |
| ) { | |
| stringRep.write('{'); | |
| for (int i = 0; i < size; i++) { | |
| if (i != 0) stringRep.write(', '); | |
| set.add(i); | |
| stringRep.write(i); | |
| } | |
| stringRep.write('}'); | |
| return set; | |
| } | |
| /** Like populateRandomCollection, but for maps. */ | |
| Map populateRandomMap( | |
| int size, | |
| bool exact, | |
| StringBuffer stringRep, | |
| List beingMade, | |
| Map map, | |
| ) { | |
| beingMade.add(map); | |
| stringRep.write('{'); | |
| for (int i = 0; i < size; i++) { | |
| if (i != 0) stringRep.write(', '); | |
| int key = i; // Ensures no duplicates | |
| stringRep.write(key); | |
| stringRep.write(': '); | |
| Object val = randomElement(random(size), exact, stringRep, beingMade); | |
| map[key] = val; | |
| } | |
| stringRep.write('}'); | |
| beingMade.removeLast(); | |
| return map; | |
| } | |
| /** | |
| * Generates a random element which can be an int, a collection, or a map, | |
| * and emits it to StringRep. The beingMade parameter is a list of collections | |
| * currently under construction, i.e., candidates for recursive references. | |
| * | |
| * If exact is true, the returned element will not be, and will not contain | |
| * a collection with ill-defined iteration order (i.e., a HashSet or HashMap). | |
| */ | |
| Object randomElement( | |
| int size, | |
| bool exact, | |
| StringBuffer stringRep, | |
| List beingMade, | |
| ) { | |
| Object result; | |
| double elementTypeFrac = rand.nextDouble(); | |
| if (elementTypeFrac < 1 / 3) { | |
| result = random(1000); | |
| stringRep.write(result); | |
| } else if (elementTypeFrac < 2 / 3) { | |
| // Element is a random (new) collection | |
| result = randomCollectionHelper(size, exact, stringRep, beingMade); | |
| } else { | |
| // Element is a random recursive ref | |
| result = beingMade[random(beingMade.length)]; | |
| if (result is List) { | |
| stringRep.write('[...]'); | |
| } else if (result is Set || result is Map || result is Queue) { | |
| stringRep.write('{...}'); | |
| } else { | |
| stringRep.write('(...)'); | |
| } | |
| } | |
| return result; | |
| } | |
| /** Returns a random int on [0, max) */ | |
| int random(int max) { | |
| return rand.nextInt(max); | |
| } | |
| /** Returns a random boolean value. */ | |
| bool randomBool() { | |
| return rand.nextBool(); | |
| } | |
| /** Returns the alphabetized characters in a string. */ | |
| String alphagram(String s) { | |
| // Calling [toList] to convert unmodifiable list to normal list. | |
| List<int> chars = s.codeUnits.toList(); | |
| chars.sort((int a, int b) => a - b); | |
| return new String.fromCharCodes(chars); | |
| } |