import 'dart:math'; import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:petitparser/petitparser.dart'; import 'package:petitparser/src/parser/character/predicate/char.dart'; import 'package:petitparser/src/parser/character/predicate/constant.dart'; import 'package:petitparser/src/parser/character/predicate/digit.dart'; import 'package:petitparser/src/parser/character/predicate/letter.dart'; import 'package:petitparser/src/parser/character/predicate/lookup.dart'; import 'package:petitparser/src/parser/character/predicate/lowercase.dart'; import 'package:petitparser/src/parser/character/predicate/not.dart'; import 'package:petitparser/src/parser/character/predicate/range.dart'; import 'package:petitparser/src/parser/character/predicate/ranges.dart'; import 'package:petitparser/src/parser/character/predicate/uppercase.dart'; import 'package:petitparser/src/parser/character/predicate/whitespace.dart'; import 'package:petitparser/src/parser/character/predicate/word.dart'; import 'package:test/test.dart' hide anyOf; import 'utils/assertions.dart'; import 'utils/matchers.dart'; @isTestGroup void variation

( String label, Parser parser, { Iterable accept = const [], Iterable reject = const [], dynamic message = anything, dynamic predicate = anything, }) { group(label, () { expectParserInvariants(parser); if (accept.isNotEmpty) { test('accept', () { for (final char in accept) { expect(parser, isParseSuccess(char, result: char)); } }); } if (reject.isNotEmpty) { test('reject', () { for (final char in reject) { expect(parser, isParseFailure(char, message: message)); } }); } test('empty', () { expect(parser, isParseFailure('', message: message)); }); test('state', () { expect( parser, isCharacterParser

(message: message, predicate: predicate), ); }); }); } void main() { group('any', () { variation( 'default', any(), accept: ['a', 'z', '9', '\u3211'], message: 'input expected', predicate: const ConstantCharPredicate(true), ); variation( 'message', any(message: 'something expected'), accept: ['a', 'z', '9', '\u3211'], message: 'something expected', predicate: const ConstantCharPredicate(true), ); variation( 'unicode', any(unicode: true), accept: ['a', 'b', 'c', '🤔', '🤐'], message: 'input expected', predicate: const ConstantCharPredicate(true), ); }); group('anyOf', () { variation( 'default', anyOf('uncopyrightable'), accept: ['c', 'g', 'h', 'i', 'o', 'p', 'r', 't', 'y'], reject: ['x', 'z', 'C'], message: 'any of "uncopyrightable" expected', predicate: LookupCharPredicate(97, 121, Uint32List.fromList([18541015])), ); variation( 'message', anyOf('02468', message: 'even digit'), accept: ['0', '2', '4', '6', '8'], reject: ['1', '3', '5', '7', '9'], message: 'even digit', predicate: LookupCharPredicate(48, 56, Uint32List.fromList([341])), ); variation( 'ignore-case', anyOf('aB0', ignoreCase: true), accept: ['a', 'A', 'b', 'B', '0'], reject: ['c', '1'], message: 'any of "aB0" (case-insensitive) expected', predicate: LookupCharPredicate( 48, 98, Uint32List.fromList([393217, 393216]), ), ); variation( 'unicode', anyOf('abc🤔🤐', unicode: true), accept: ['a', 'b', 'c', '🤔', '🤐'], reject: ['0', 'd', '🙄'], message: 'any of "abc🤔🤐" expected', predicate: isA(), ); }); group('char', () { variation( 'default', char('y'), accept: ['y'], reject: ['x', '%', '\r', 'Y'], message: '"y" expected', predicate: const SingleCharPredicate(121), ); variation( 'message', char('y', message: 'lowercase y'), accept: ['y'], reject: ['x', '5', '\x00'], message: 'lowercase y', predicate: const SingleCharPredicate(121), ); variation( 'ignore-case', char('y', ignoreCase: true), accept: ['y', 'Y'], reject: ['x', 'z', 'X', 'Z'], message: '"y" (case-insensitive) expected', predicate: LookupCharPredicate(89, 121, Uint32List.fromList([1, 1])), ); variation( 'unicode', char('🙄', unicode: true), accept: ['🙄'], reject: ['🤐', '🤔', 'a', '0'], message: '"🙄" expected', predicate: const SingleCharPredicate(128580), ); test('invalid character', () { expect(() => char('ab'), throwsA(isAssertionError)); expect(() => char('🙄'), throwsA(isAssertionError)); }, skip: !hasAssertionsEnabled()); }); group('digit', () { variation( 'default', digit(), accept: ['0', '8', '9'], reject: ['a', 'X', '\b'], message: 'digit expected', predicate: const DigitCharPredicate(), ); variation( 'message', digit(message: 'number expected'), accept: ['1', '2', '3'], reject: ['e', '#', '*'], message: 'number expected', predicate: const DigitCharPredicate(), ); }); group('letter', () { variation( 'default', letter(), accept: ['a', 'X', 'n'], reject: ['6', '#', '\n'], message: 'letter expected', predicate: const LetterCharPredicate(), ); variation( 'message', letter(message: 'word constituent'), accept: ['y', 'Z', 'R'], reject: ['0', '&', '^'], message: 'word constituent', predicate: const LetterCharPredicate(), ); }); group('lowercase', () { variation( 'default', lowercase(), accept: ['a', 'l', 'r'], reject: ['3', 'Z', '\t'], message: 'lowercase letter expected', predicate: const LowercaseCharPredicate(), ); variation( 'message', lowercase(message: 'lowercase only'), accept: ['x', 'y', 'z'], reject: ['0', '&', '\x00'], message: 'lowercase only', predicate: const LowercaseCharPredicate(), ); }); group('noneOf', () { variation( 'default', noneOf('uncopyrightable'), accept: ['x', 'z'], reject: ['c', 'g', 'h', 'i', 'o', 'p', 'r', 't', 'y'], message: 'none of "uncopyrightable" expected', predicate: NotCharPredicate( LookupCharPredicate(97, 121, Uint32List.fromList([18541015])), ), ); variation( 'message', noneOf('02468', message: 'no even digit'), accept: ['1', '3', '5', '7', '9'], reject: ['0', '2', '4', '6', '8'], message: 'no even digit', predicate: NotCharPredicate( LookupCharPredicate(48, 56, Uint32List.fromList([341])), ), ); variation( 'ignore-case', noneOf('aB0', ignoreCase: true), accept: ['c', 'C', 'x', '1'], reject: ['a', 'A', 'b', 'B', '0'], message: 'none of "aB0" (case-insensitive) expected', predicate: NotCharPredicate( LookupCharPredicate(48, 98, Uint32List.fromList([393217, 393216])), ), ); variation( 'unicode', noneOf('abc🤔🤐', unicode: true), accept: ['0', 'd', '🙄'], reject: ['a', 'b', 'c', '🤔', '🤐'], message: 'none of "abc🤔🤐" expected', ); }); group('pattern', () { group('single', () { variation( 'default', pattern('y'), accept: ['y'], reject: ['x', 'z', '5', 'Y', '\x00', '😮'], message: '[y] expected', predicate: const SingleCharPredicate(121), ); variation( 'ignore-case', pattern('a', ignoreCase: true), accept: ['a', 'A'], reject: ['b', 'B', '\x00', '&'], predicate: LookupCharPredicate(65, 97, Uint32List.fromList([1, 1])), ); variation( 'unicode', pattern('😮', unicode: true), accept: ['😮'], reject: ['x', 'z', '5', '\x00', '😃'], message: '[😮] expected', predicate: const SingleCharPredicate(128558), ); variation( 'negated', pattern('^y'), accept: ['x', 'z', '5', '\x00'], reject: ['y'], message: '[^y] expected', predicate: const NotCharPredicate(SingleCharPredicate(121)), ); }); group('multiple', () { variation( 'default', pattern('ab-'), accept: ['a', 'b', '-'], reject: ['d', 'e', 'A', 'B', 'f'], message: '[ab-] expected', predicate: LookupCharPredicate( 45, 98, Uint32List.fromList([1, 3145728]), ), ); variation( 'ignore-case', pattern('ab-', ignoreCase: true), accept: ['a', 'A', 'b', 'B', '-'], reject: ['c', 'C', '\x00', '&'], predicate: LookupCharPredicate( 45, 98, Uint32List.fromList([3145729, 3145728]), ), ); variation( 'unicode', pattern('y😃💕', unicode: true), accept: ['y', '😃', '💕'], reject: ['x', 'z', '💞'], message: '[y😃💕] expected', predicate: isA(), ); variation( 'negated', pattern('^ab-'), accept: ['d', 'e', 'f'], reject: ['a', 'b', '-'], message: '[^ab-] expected', predicate: NotCharPredicate( LookupCharPredicate(45, 98, Uint32List.fromList([1, 3145728])), ), ); }); group('range', () { variation( 'default', pattern('a-c'), accept: ['a', 'b', 'c'], reject: ['d', 'e', 'f'], message: '[a-c] expected', predicate: const RangeCharPredicate(97, 99), ); variation( 'negated', pattern('^a-c'), accept: ['d', 'e', 'f'], reject: ['a', 'b', 'c'], message: '[^a-c] expected', predicate: const NotCharPredicate(RangeCharPredicate(97, 99)), ); variation( 'overlapping', pattern('b-da-c'), accept: ['a', 'b', 'c', 'd'], reject: ['e', 'f', 'g'], message: '[b-da-c] expected', predicate: const RangeCharPredicate(97, 100), ); variation( 'adjacent', pattern('c-ea-c'), accept: ['a', 'b', 'c', 'd', 'e'], reject: ['f'], message: '[c-ea-c] expected', predicate: const RangeCharPredicate(97, 101), ); variation( 'prefix', pattern('a-ea-c'), accept: ['a', 'b', 'c', 'd', 'e'], reject: ['f'], message: '[a-ea-c] expected', predicate: const RangeCharPredicate(97, 101), ); variation( 'postfix', pattern('a-ec-e'), accept: ['a', 'b', 'c', 'd', 'e'], reject: ['f'], message: '[a-ec-e] expected', predicate: const RangeCharPredicate(97, 101), ); variation( 'repeated', pattern('a-ea-e'), accept: ['a', 'b', 'c', 'd', 'e'], reject: ['f'], message: '[a-ea-e] expected', predicate: const RangeCharPredicate(97, 101), ); variation( 'composed', pattern('ac-df-'), accept: ['a', 'c', 'd', 'f', '-'], reject: ['b', 'e', 'g'], message: '[ac-df-] expected', predicate: LookupCharPredicate( 45, 102, Uint32List.fromList([1, 47185920]), ), ); }); group('everything', () { variation( 'default', pattern('\u{0000}-\u{ffff}'), accept: ['\u{0000}', '\u{ffff}'], reject: [], message: '[\\x00-\u{ffff}] expected', predicate: const ConstantCharPredicate(true), ); variation( 'ignore-case', pattern('\u{0000}-\u{ffff}', ignoreCase: true), accept: ['\u{0000}', '\u{ffff}'], reject: [], message: '[\\x00-￿] (case-insensitive) expected', predicate: const ConstantCharPredicate(true), ); variation( 'unicode', pattern('\u{0000}-\u{10ffff}', unicode: true), accept: ['\u{0000}', '\u{ffff}', '\u{10ffff}'], reject: [], message: '[\\x00-\u{10ffff}] expected', predicate: const ConstantCharPredicate(true), ); variation( 'negated', pattern('^\u{0000}-\u{ffff}'), accept: [], reject: ['\u{0000}', '\u{ffff}'], message: '[^\\x00-\u{ffff}] expected', predicate: const ConstantCharPredicate(false), ); variation( 'negated, unicode', pattern('^\u{0000}-\u{10ffff}', unicode: true), accept: [], reject: ['\u{0000}', '\u{ffff}', '\u{10ffff}'], message: '[^\\x00-\u{10ffff}] expected', predicate: const ConstantCharPredicate(false), ); }); group('nothing', () { variation( 'default', pattern(''), accept: [], reject: ['\u{0000}', '\u{ffff}'], message: '[] expected', predicate: const ConstantCharPredicate(false), ); variation( 'unicode', pattern('', unicode: true), accept: [], reject: ['\u{0000}', '\u{ffff}'], message: '[] expected', predicate: const ConstantCharPredicate(false), ); variation( 'negated', pattern('^'), accept: ['\u{0000}', '\u{ffff}'], reject: [], message: '[^] expected', predicate: const ConstantCharPredicate(true), ); variation( 'negated, unicode', pattern('^', unicode: true), accept: ['\u{0000}', '\u{10ffff}'], reject: [], message: '[^] expected', predicate: const ConstantCharPredicate(true), ); }); // special variation( 'large range', pattern('\u2200-\u22ff\u27c0-\u27ef\u2980-\u29ff'), accept: ['∉', '⟃', '⦻'], reject: ['a', '9', '*'], message: '[\u2200-\u22ff\u27c0-\u27ef\u2980-\u29ff] expected', predicate: isA(), ); // errors test('invalid range', () { expect(() => pattern('c-a'), throwsA(isAssertionError)); }, skip: !hasAssertionsEnabled()); }); group('range', () { variation( 'default', range('e', 'o'), accept: ['e', 'i', 'o'], reject: ['p', 'd', '9'], message: '[e-o] expected', predicate: const RangeCharPredicate(101, 111), ); variation( 'message', range('x', 'z', message: 'variable expected'), accept: ['x', 'y', 'z'], reject: ['p', 'd', '9'], message: 'variable expected', predicate: const RangeCharPredicate(120, 122), ); variation( 'unicode', range('😁', '😄', unicode: true), accept: ['😁', '😃', '😄'], reject: ['😀', '😅', '9'], message: '[😁-😄] expected', predicate: const RangeCharPredicate(128513, 128516), ); test('invalid range', () { expect(() => range('o', 'e'), throwsA(isAssertionError)); }, skip: !hasAssertionsEnabled()); test('invalid character', () { expect(() => range('😃', '😍'), throwsA(isAssertionError)); }, skip: !hasAssertionsEnabled()); }); group('uppercase', () { variation( 'default', uppercase(), accept: ['A', 'L', 'R'], reject: ['3', 'z', '\t'], message: 'uppercase letter expected', predicate: const UppercaseCharPredicate(), ); variation( 'message', uppercase(message: 'only uppercase'), accept: ['X', 'Y', 'Z'], reject: ['0', '&', '\x00'], message: 'only uppercase', predicate: const UppercaseCharPredicate(), ); }); group('whitespace', () { const whitespaceCharCodes = { 9, 10, 11, 12, 13, 32, 133, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279, // }; final accept = [ for (var c = 0; c <= 0x10000; c++) if (whitespaceCharCodes.contains(c)) String.fromCharCode(c), ]; final reject = [ for (var c = 0; c <= 0x10000; c++) if (!whitespaceCharCodes.contains(c)) String.fromCharCode(c), ]; variation( 'default', whitespace(), accept: accept, reject: reject, message: 'whitespace expected', predicate: const WhitespaceCharPredicate(), ); variation( 'message', whitespace(message: 'only blanks'), accept: [' ', '\t', '\r', '\f', '\r', '\n'], reject: ['3', 'z', '#', '0', '&', '\x00'], message: 'only blanks', predicate: const WhitespaceCharPredicate(), ); }); group('word', () { variation( 'default', word(), accept: ['a', 'z', 'A', 'Z', '0', '9', '_'], reject: ['-', '#', '('], message: 'letter or digit expected', predicate: const WordCharPredicate(), ); variation( 'message', word(message: 'only word'), accept: ['L', 'F', 'R', '7'], reject: ['@', ':', '\x00'], message: 'only word', predicate: const WordCharPredicate(), ); }); group('stress', () { void stress( CharacterPredicate Function(List) factory, { int repeat = 1000, int size = 1000, int maxGap = 100, int maxRange = 100, int seed = 81728392, }) { final random = Random(seed); for (var i = 0; i < repeat; i++) { var start = random.nextInt(maxGap); final ranges = []; final included = List.filled(size + 1, false); while (true) { final end = start + random.nextInt(maxRange); if (end > size) break; ranges.add(RangeCharPredicate(start, end)); included.fillRange(start, end + 1, true); start = random.nextInt(maxGap) + end + 1; } final predicate = factory(ranges); for (var i = 0; i <= size; i++) { expect(predicate.test(i), included[i]); } } } test('lookup', () => stress(LookupCharPredicate.fromRanges)); test('ranges', () => stress(RangesCharPredicate.fromRanges)); }); }