blob: 7039038bd18ff19a1e03055a4dc3a89c110bc430 [file] [log] [blame]
// Copyright (c) 2019, 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 "dart:math";
import "package:test/test.dart";
import "package:characters/characters.dart";
import "src/unicode_tests.dart";
import "src/unicode_grapheme_tests.dart";
import "src/various_tests.dart";
Random random;
void main([List<String> args]) {
// Ensure random seed is part of every test failure message,
// and that it can be reapplied for testing.
var seed = (args != null && args.isNotEmpty)
? int.parse(args[0])
: Random().nextInt(0x3FFFFFFF);
random = Random(seed);
group("[Random Seed: $seed]", tests);
group("index", () {
test("simple", () {
var flag = "\u{1F1E9}\u{1F1F0}";
var string = "Hi $flag!"; // Regional Indications "DK".
expect(string.length, 8);
expect(gc(string).toList(), ["H", "i", " ", flag, "!"]);
expect(gc(string).indexOf(gc("")), 0);
expect(gc(string).indexOf(gc(""), 3), 3);
expect(gc(string).indexOf(gc(""), 4), 7);
expect(gc(string).indexOf(gc(flag)), 3);
expect(gc(string).indexOf(gc(flag), 3), 3);
expect(gc(string).indexOf(gc(flag), 4), lessThan(0));
expect(gc(string).indexAfter(gc("")), 0);
expect(gc(string).indexAfter(gc(""), 3), 3);
expect(gc(string).indexAfter(gc(""), 4), 7);
expect(gc(string).indexAfter(gc(flag)), 7);
expect(gc(string).indexAfter(gc(flag), 7), 7);
expect(gc(string).indexAfter(gc(flag), 8), lessThan(0));
expect(gc(string).lastIndexOf(gc("")), string.length);
expect(gc(string).lastIndexOf(gc(""), 7), 7);
expect(gc(string).lastIndexOf(gc(""), 6), 3);
expect(gc(string).lastIndexOf(gc(""), 0), 0);
expect(gc(string).lastIndexOf(gc(flag)), 3);
expect(gc(string).lastIndexOf(gc(flag), 6), 3);
expect(gc(string).lastIndexOf(gc(flag), 2), lessThan(0));
expect(gc(string).lastIndexAfter(gc("")), string.length);
expect(gc(string).lastIndexAfter(gc(""), 7), 7);
expect(gc(string).lastIndexAfter(gc(""), 6), 3);
expect(gc(string).lastIndexAfter(gc(""), 0), 0);
expect(gc(string).lastIndexAfter(gc(flag)), 7);
expect(gc(string).lastIndexAfter(gc(flag), 7), 7);
expect(gc(string).lastIndexAfter(gc(flag), 6), lessThan(0));
});
test("multiple", () {
var flag = "\u{1F1E9}\u{1F1F0}"; // DK.
var revFlag = "\u{1F1F0}\u{1F1E9}"; // KD.
var string = "-${flag}-$flag$flag-";
expect(gc(string).indexOf(gc(flag)), 1);
expect(gc(string).indexOf(gc(flag), 2), 6);
expect(gc(string).indexOf(gc(flag), 6), 6);
expect(gc(string).indexOf(gc(flag), 7), 10);
expect(gc(string).indexOf(gc(flag), 10), 10);
expect(gc(string).indexOf(gc(flag), 11), lessThan(0));
expect(gc(string).indexOf(gc(revFlag)), lessThan(0));
});
test("nonBoundary", () {
// Composite pictogram example, from https://en.wikipedia.org/wiki/Zero-width_joiner.
var flag = "\u{1f3f3}"; // U+1F3F3, Flag, waving. Category Pictogram.
var white = "\ufe0f"; // U+FE0F, Variant selector 16. Category Extend.
var zwj = "\u200d"; // U+200D, ZWJ
var rainbow = "\u{1f308}"; // U+1F308, Rainbow. Category Pictogram
var flagRainbow = "$flag$white$zwj$rainbow";
expect(gc(flagRainbow).length, 1);
for (var part in [flag, white, zwj, rainbow]) {
expect(gc(flagRainbow).indexOf(gc(part)), lessThan(0));
expect(gc(flagRainbow).indexAfter(gc(part)), lessThan(0));
expect(gc(flagRainbow).lastIndexOf(gc(part)), lessThan(0));
expect(gc(flagRainbow).lastIndexAfter(gc(part)), lessThan(0));
}
expect(gc(flagRainbow + flagRainbow).indexOf(gc(flagRainbow)), 0);
expect(gc(flagRainbow + flagRainbow).indexAfter(gc(flagRainbow)), 6);
expect(gc(flagRainbow + flagRainbow).lastIndexOf(gc(flagRainbow)), 6);
expect(gc(flagRainbow + flagRainbow).lastIndexAfter(gc(flagRainbow)), 12);
// 1 11 11 11 2
// indices 0 67 90 12 34 67 3
var partsAndWhole =
"$flagRainbow $flag $white $zwj $rainbow $flagRainbow";
// Flag and rainbow are independent graphemes.
expect(gc(partsAndWhole).toList(), [
flagRainbow,
" ",
flag,
" $white", // Other + Extend
" $zwj", // Other + ZWJ
" ",
rainbow,
" ",
flagRainbow
]);
expect(gc(partsAndWhole).indexOf(gc(flag)), 7);
expect(gc(partsAndWhole).indexAfter(gc(flag)), 9);
expect(gc(partsAndWhole).lastIndexOf(gc(flag)), 7);
expect(gc(partsAndWhole).lastIndexAfter(gc(flag)), 9);
expect(gc(partsAndWhole).indexOf(gc(rainbow)), 14);
expect(gc(partsAndWhole).indexAfter(gc(rainbow)), 16);
expect(gc(partsAndWhole).lastIndexOf(gc(rainbow)), 14);
expect(gc(partsAndWhole).lastIndexAfter(gc(rainbow)), 16);
expect(gc(partsAndWhole).indexOf(gc(white)), lessThan(0));
expect(gc(partsAndWhole).indexAfter(gc(white)), lessThan(0));
expect(gc(partsAndWhole).lastIndexOf(gc(white)), lessThan(0));
expect(gc(partsAndWhole).lastIndexAfter(gc(white)), lessThan(0));
expect(gc(partsAndWhole).indexOf(gc(" $white")), 9);
expect(gc(partsAndWhole).indexAfter(gc(" $white")), 11);
expect(gc(partsAndWhole).lastIndexOf(gc(" $white")), 9);
expect(gc(partsAndWhole).lastIndexAfter(gc(" $white")), 11);
expect(gc(partsAndWhole).indexOf(gc(zwj)), lessThan(0));
expect(gc(partsAndWhole).indexAfter(gc(zwj)), lessThan(0));
expect(gc(partsAndWhole).lastIndexOf(gc(zwj)), lessThan(0));
expect(gc(partsAndWhole).lastIndexAfter(gc(zwj)), lessThan(0));
expect(gc(partsAndWhole).indexOf(gc(" $zwj")), 11);
expect(gc(partsAndWhole).indexAfter(gc(" $zwj")), 13);
expect(gc(partsAndWhole).lastIndexOf(gc(" $zwj")), 11);
expect(gc(partsAndWhole).lastIndexAfter(gc(" $zwj")), 13);
});
});
}
void tests() {
test("empty", () {
expectGC(gc(""), []);
});
group("gc-ASCII", () {
for (var text in [
"",
"A",
"123456abcdefab",
]) {
test('"$text"', () {
expectGC(gc(text), charsOf(text));
});
}
test("CR+NL", () {
expectGC(gc("a\r\nb"), ["a", "\r\n", "b"]);
expectGC(gc("a\n\rb"), ["a", "\n", "\r", "b"]);
});
});
group("Non-ASCII single-code point", () {
for (var text in [
"à la mode",
"rødgrød-æble-ål",
]) {
test('"$text"', () {
expectGC(gc(text), charsOf(text));
});
}
});
group("Combining marks", () {
var text = "a\u0300 la mode";
test('"$text"', () {
expectGC(gc(text), ["a\u0300", " ", "l", "a", " ", "m", "o", "d", "e"]);
});
var text2 = "æble-a\u030Al";
test('"$text2"', () {
expectGC(gc(text2), ["æ", "b", "l", "e", "-", "a\u030A", "l"]);
});
});
group("Regional Indicators", () {
test('"🇦🇩🇰🇾🇪🇸"', () {
// Andorra, Cayman Islands, Spain.
expectGC(gc("🇦🇩🇰🇾🇪🇸"), ["🇦🇩", "🇰🇾", "🇪🇸"]);
});
test('"X🇦🇩🇰🇾🇪🇸"', () {
// Other, Andorra, Cayman Islands, Spain.
expectGC(gc("X🇦🇩🇰🇾🇪🇸"), ["X", "🇦🇩", "🇰🇾", "🇪🇸"]);
});
test('"🇩🇰🇾🇪🇸"', () {
// Denmark, Yemen, unmatched S.
expectGC(gc("🇩🇰🇾🇪🇸"), ["🇩🇰", "🇾🇪", "🇸"]);
});
test('"X🇩🇰🇾🇪🇸"', () {
// Other, Denmark, Yemen, unmatched S.
expectGC(gc("X🇩🇰🇾🇪🇸"), ["X", "🇩🇰", "🇾🇪", "🇸"]);
});
});
group("Hangul", () {
// Individual characters found on Wikipedia. Not expected to make sense.
test('"읍쌍된밟"', () {
expectGC(gc("읍쌍된밟"), ["읍", "쌍", "된", "밟"]);
});
});
group("Unicode test", () {
for (var gcs in splitTests) {
test("[${testDescription(gcs)}]", () {
expectGC(gc(gcs.join()), gcs);
});
}
});
group("Emoji test", () {
for (var gcs in emojis) {
test("[${testDescription(gcs)}]", () {
expectGC(gc(gcs.join()), gcs);
});
}
});
group("Zalgo test", () {
for (var gcs in zalgo) {
test("[${testDescription(gcs)}]", () {
expectGC(gc(gcs.join()), gcs);
});
}
});
}
// Converts text with no multi-code-point grapheme clusters into
// list of grapheme clusters.
List<String> charsOf(String text) =>
text.runes.map((r) => String.fromCharCode(r)).toList();
void expectGC(Characters actual, List<String> expected) {
var text = expected.join();
// Iterable operations.
expect(actual.string, text);
expect(actual.toString(), text);
expect(actual.toList(), expected);
expect(actual.length, expected.length);
if (expected.isNotEmpty) {
expect(actual.first, expected.first);
expect(actual.last, expected.last);
} else {
expect(() => actual.first, throwsStateError);
expect(() => actual.last, throwsStateError);
}
if (expected.length == 1) {
expect(actual.single, expected.single);
} else {
expect(() => actual.single, throwsStateError);
}
expect(actual.isEmpty, expected.isEmpty);
expect(actual.isNotEmpty, expected.isNotEmpty);
expect(actual.contains(""), false);
for (var char in expected) {
expect(actual.contains(char), true);
}
for (int i = 1; i < expected.length; i++) {
expect(actual.contains(expected[i - 1] + expected[i]), false);
}
expect(actual.skip(1).toList(), expected.skip(1).toList());
expect(actual.take(1).toList(), expected.take(1).toList());
expect(actual.skip(1).toString(), expected.skip(1).join());
expect(actual.take(1).toString(), expected.take(1).join());
if (expected.isNotEmpty) {
expect(actual.skipLast(1).toList(),
expected.take(expected.length - 1).toList());
expect(actual.takeLast(1).toList(),
expected.skip(expected.length - 1).toList());
expect(actual.skipLast(1).toString(),
expected.take(expected.length - 1).join());
expect(actual.takeLast(1).toString(),
expected.skip(expected.length - 1).join());
expect(actual.indexOf(gc(expected.first)), 0);
expect(actual.indexAfter(gc(expected.first)), expected.first.length);
expect(actual.lastIndexOf(gc(expected.last)),
text.length - expected.last.length);
expect(actual.lastIndexAfter(gc(expected.last)), text.length);
if (expected.length > 1) {
if (expected[0] != expected[1]) {
expect(actual.indexOf(gc(expected[1])), expected[0].length);
}
}
}
expect(actual.getRange(1, 3).toString(), expected.take(3).skip(1).join());
expect(actual.getRange(1, 3).toString(), expected.take(3).skip(1).join());
bool isEven(String s) => s.length.isEven;
expect(
actual.skipWhile(isEven).toList(), expected.skipWhile(isEven).toList());
expect(
actual.takeWhile(isEven).toList(), expected.takeWhile(isEven).toList());
expect(
actual.skipWhile(isEven).toString(), expected.skipWhile(isEven).join());
expect(
actual.takeWhile(isEven).toString(), expected.takeWhile(isEven).join());
expect(actual.skipLastWhile(isEven).toString(),
expected.toList().reversed.skipWhile(isEven).toList().reversed.join());
expect(actual.takeLastWhile(isEven).toString(),
expected.toList().reversed.takeWhile(isEven).toList().reversed.join());
expect(actual.where(isEven).toString(), expected.where(isEven).join());
expect((actual + actual).toString(), actual.string + actual.string);
List<int> accumulatedLengths = [0];
for (int i = 0; i < expected.length; i++) {
accumulatedLengths.add(accumulatedLengths.last + expected[i].length);
}
// Iteration.
var it = actual.iterator;
expect(it.start, 0);
expect(it.end, 0);
for (var i = 0; i < expected.length; i++) {
expect(it.moveNext(), true);
expect(it.start, accumulatedLengths[i]);
expect(it.end, accumulatedLengths[i + 1]);
expect(it.current, expected[i]);
expect(actual.elementAt(i), expected[i]);
expect(actual.skip(i).first, expected[i]);
}
expect(it.moveNext(), false);
expect(it.start, accumulatedLengths.last);
expect(it.end, accumulatedLengths.last);
for (var i = expected.length - 1; i >= 0; i--) {
expect(it.movePrevious(), true);
expect(it.start, accumulatedLengths[i]);
expect(it.end, accumulatedLengths[i + 1]);
expect(it.current, expected[i]);
}
expect(it.movePrevious(), false);
expect(it.start, 0);
expect(it.end, 0);
// GraphemeClusters operations.
expect(actual.toUpperCase().toString(), text.toUpperCase());
expect(actual.toLowerCase().toString(), text.toLowerCase());
if (text.isNotEmpty) {
expect(actual.insertAt(1, gc("abc")).toString(),
text.replaceRange(1, 1, "abc"));
expect(actual.replaceSubstring(0, 1, gc("abc")).toString(),
text.replaceRange(0, 1, "abc"));
expect(actual.substring(0, 1).string, actual.string.substring(0, 1));
}
expect(actual.string, text);
expect(actual.containsAll(gc("")), true);
expect(actual.containsAll(actual), true);
if (expected.isNotEmpty) {
int steps = min(5, expected.length);
for (int s = 0; s <= steps; s++) {
int i = expected.length * s ~/ steps;
expect(actual.startsWith(gc(expected.sublist(0, i).join())), true);
expect(actual.endsWith(gc(expected.sublist(i).join())), true);
for (int t = s + 1; t <= steps; t++) {
int j = expected.length * t ~/ steps;
int start = accumulatedLengths[i];
int end = accumulatedLengths[j];
var slice = expected.sublist(i, j).join();
var gcs = gc(slice);
expect(actual.containsAll(gcs), true);
expect(actual.startsWith(gcs, start), true);
expect(actual.endsWith(gcs, end), true);
}
}
if (accumulatedLengths.last > expected.length) {
int i = expected.indexWhere((s) => s.length != 1);
assert(accumulatedLengths[i + 1] > accumulatedLengths[i] + 1);
expect(
actual.startsWith(gc(text.substring(0, accumulatedLengths[i] + 1))),
false);
expect(actual.endsWith(gc(text.substring(accumulatedLengths[i] + 1))),
false);
if (i > 0) {
expect(
actual.startsWith(
gc(text.substring(1, accumulatedLengths[i] + 1)), 1),
false);
}
if (i < expected.length - 1) {
int secondToLast = accumulatedLengths[expected.length - 1];
expect(
actual.endsWith(
gc(text.substring(accumulatedLengths[i] + 1, secondToLast)),
secondToLast),
false);
}
}
}
{
// Random walk back and forth.
var it = actual.iterator;
int pos = -1;
if (random.nextBool()) {
pos = expected.length;
it.reset(text.length);
}
int steps = 5 + random.nextInt(expected.length * 2 + 1);
bool lastMove = false;
while (true) {
bool back = false;
if (pos < 0) {
expect(lastMove, false);
expect(it.start, 0);
expect(it.end, 0);
} else if (pos >= expected.length) {
expect(lastMove, false);
expect(it.start, text.length);
expect(it.end, text.length);
back = true;
} else {
expect(lastMove, true);
expect(it.current, expected[pos]);
expect(it.start, accumulatedLengths[pos]);
expect(it.end, accumulatedLengths[pos + 1]);
back = random.nextBool();
}
if (--steps < 0) break;
if (back) {
lastMove = it.movePrevious();
pos -= 1;
} else {
lastMove = it.moveNext();
pos += 1;
}
}
}
}
Characters gc(String string) => Characters(string);