Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkgs/checks/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- Require Dart 3.7
- Improve speed of pretty printing for large collections.
- Add extensions for reading fields and invoking methods of `RegExp` and related
classes.

## 0.3.1

Expand Down
8 changes: 7 additions & 1 deletion pkgs/checks/lib/checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ export 'src/extensions/function.dart' show FunctionChecks;
export 'src/extensions/iterable.dart' show IterableChecks;
export 'src/extensions/map.dart' show MapChecks;
export 'src/extensions/math.dart' show NumChecks;
export 'src/extensions/string.dart' show StringChecks;
export 'src/extensions/string.dart'
show
MatchChecks,
PatternChecks,
RegExpChecks,
RegExpMatchChecks,
StringChecks;
7 changes: 7 additions & 0 deletions pkgs/checks/lib/src/describe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ Iterable<String> _prettyPrint(
return prefixFirst("'", postfixLast("'", escaped));
} else if (object is Condition<Never>) {
return ['<A value that:', ...postfixLast('>', describe(object))];
} else if (object is Match) {
final escaped = const LineSplitter()
.convert(object[0] ?? 'null')
.map(escape)
.map((line) => line.replaceAll("'", r"\'"))
.toList();
return prefixFirst('Match[', postfixLast(']', escaped));
} else {
final value = const LineSplitter().convert(object.toString());
return isTopLevel ? prefixFirst('<', postfixLast('>', value)) : value;
Expand Down
158 changes: 158 additions & 0 deletions pkgs/checks/lib/src/extensions/string.dart
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,161 @@ String _collapseWhitespace(String string) {

bool _isWhitespace(String ch) =>
ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';

extension PatternChecks on Subject<Pattern> {
/// Extracts the matches found by [allMatches] for further expectations.
Subject<Iterable<Match>> hasAllMatchesFor(String input, [int start = 0]) {
return context.nest(() {
final label = literal(input);
return prefixFirst(
'has all matches for ',
start == 0 ? label : postfixLast(' starting at index $start', label),
);
}, (actual) => Extracted.value(actual.allMatches(input, start)));
}

/// Extracts the prefix match found by [matchAsPrefix] for further
/// expectations.
///
/// Expects that [input] matches this pattern as a prefix.
Subject<Match> hasPrefixMatchFor(String input, [int start = 0]) {
return context.nest(
() {
final label = literal(input);
return prefixFirst(
'has prefix match for ',
start == 0 ? label : postfixLast(' starting at index $start', label),
);
},
(actual) {
final match = actual.matchAsPrefix(input, start);
if (match == null) {
return Extracted.rejection(which: ['did not match as prefix']);
}
return Extracted.value(match);
},
);
}
}

extension RegExpChecks on Subject<RegExp> {
/// Extracts the [isCaseSensitive] property for further expectations.
Subject<bool> get isCaseSensitive =>
has((s) => s.isCaseSensitive, 'isCaseSensitive');

/// Extracts the [isDotAll] property for further expectations.
Subject<bool> get isDotAll => has((s) => s.isDotAll, 'isDotAll');

/// Extracts the [isMultiLine] property for further expectations.
Subject<bool> get isMultiLine => has((s) => s.isMultiLine, 'isMultiLine');

/// Extracts the [isUnicode] property for further expectations.
Subject<bool> get isUnicode => has((s) => s.isUnicode, 'isUnicode');

/// Extracts the [pattern] property for further expectations.
Subject<String> get pattern => has((s) => s.pattern, 'pattern');

/// Extracts the first match found by [firstMatch] for further expectations.
///
/// Expects that [input] matches this regular expression.
Subject<Match> hasFirstMatchFor(String input) {
return context.nest(
() => prefixFirst('has first match for ', literal(input)),
(actual) {
final match = actual.firstMatch(input);
if (match == null) {
return Extracted.rejection(which: ['did not match']);
}
return Extracted.value(match);
},
);
}

/// Extracts the string match found by [stringMatch] for further expectations.
///
/// Expects that [input] matches this regular expression.
Subject<String> hasStringMatchFor(String input) {
return context.nest(
() => prefixFirst('has string match for ', literal(input)),
(actual) {
final match = actual.stringMatch(input);
if (match == null) {
return Extracted.rejection(which: ['did not match']);
}
return Extracted.value(match);
},
);
}

/// Expects that this regular expression [hasMatch] for [input].
void hasMatchFor(String input) {
context.expect(() => prefixFirst('has match for ', literal(input)), (
actual,
) {
if (actual.hasMatch(input)) return null;
return Rejection(which: ['did not match']);
});
}

/// Expects that this regular expression does not have a match for [input]
/// according to [hasMatch].
void hasNoMatchFor(String input) {
context.expect(() => prefixFirst('has no match for ', literal(input)), (
actual,
) {
if (!actual.hasMatch(input)) return null;
return Rejection(which: ['matched']);
});
}
}

extension MatchChecks on Subject<Match> {
/// Extracts the [end] property for further expectations.
Subject<int> get end => has((m) => m.end, 'end');

/// Extracts the [groupCount] property for further expectations.
Subject<int> get groupCount => has((m) => m.groupCount, 'groupCount');

/// Extracts the [input] property for further expectations.
Subject<String> get input => has((m) => m.input, 'input');

/// Extracts the [pattern] property for further expectations.
Subject<Pattern> get pattern => has((m) => m.pattern, 'pattern');

/// Extracts the [start] property for further expectations.
Subject<int> get start => has((m) => m.start, 'start');

/// Extracts the group at [index] found by [group] for further expectations.
Subject<String?> hasGroup(int index) =>
has((m) => m.group(index), 'group $index');

/// Extracts the groups at [indices] found by [groups] for further
/// expectations.
Subject<List<String?>> hasGroups(List<int> indices) {
return context.nest(() => prefixFirst('has groups ', literal(indices)), (
actual,
) {
try {
return Extracted.value(actual.groups(indices));
} catch (e) {
return Extracted.rejection(
which: prefixFirst('threw while trying to read groups: ', literal(e)),
);
}
});
}
}

extension RegExpMatchChecks on Subject<RegExpMatch> {
/// Extracts the [groupNames] property for further expectations.
Subject<Iterable<String>> get groupNames =>
has((m) => m.groupNames, 'groupNames');

/// Extracts the [pattern] property for further expectations.
Subject<RegExp> get pattern => has((m) => m.pattern, 'pattern');

/// Extracts the named group [name] found by [namedGroup] for further
/// expectations.
Subject<String?> hasNamedGroup(String name) =>
has((m) => m.namedGroup(name), 'named group "$name"');
}
136 changes: 136 additions & 0 deletions pkgs/checks/test/extensions/string_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,140 @@ void main() {
});
});
});

group('PatternChecks', () {
final pattern = RegExp('a+');
test('hasAllMatchesFor', () {
check(pattern).hasAllMatchesFor('a aa aaa').length.equals(3);
check(pattern).hasAllMatchesFor('b').isEmpty();
});

test('hasPrefixMatchFor', () {
check(pattern).hasPrefixMatchFor('ab').hasGroup(0).equals('a');
check(pattern).isRejectedBy(
(it) => it.hasPrefixMatchFor('ba'),
which: ['did not match as prefix'],
);
});
});

group('RegExpChecks', () {
final regExp = RegExp('a+', caseSensitive: false);
test('getters', () {
check(regExp)
..isCaseSensitive.isFalse()
..isDotAll.isFalse()
..isMultiLine.isFalse()
..isUnicode.isFalse()
..pattern.equals('a+');
});

test('hasFirstMatchFor', () {
check(regExp).hasFirstMatchFor('ba').hasGroup(0).equals('a');
check(regExp).isRejectedBy(
(it) => it.hasFirstMatchFor('b'),
which: ['did not match'],
);
});

test('hasStringMatchFor', () {
check(regExp).hasStringMatchFor('ba').equals('a');
check(regExp).isRejectedBy(
(it) => it.hasStringMatchFor('b'),
which: ['did not match'],
);
});

test('hasMatchFor', () {
check(regExp).hasMatchFor('ba');
check(
regExp,
).isRejectedBy((it) => it.hasMatchFor('b'), which: ['did not match']);
});

test('hasNoMatchFor', () {
check(regExp).hasNoMatchFor('b');
check(
regExp,
).isRejectedBy((it) => it.hasNoMatchFor('ba'), which: ['matched']);
});
});

group('MatchChecks', () {
final match = RegExp('a(b)?(c)?').firstMatch('zabz')!;
test('getters', () {
check(match)
..end.equals(3)
..groupCount.equals(2)
..input.equals('zabz')
..pattern.isA<RegExp>()
..start.equals(1);
});
test('hasGroup', () {
check(match).hasGroup(0).isNotNull().equals('ab');
check(match).hasGroup(1).isNotNull().equals('b');
check(match).hasGroup(2).isNull();

check(match).isRejectedBy(
(it) => it.hasGroup(0).equals('wrong'),
actual: ['\'ab\''],
which: ['are not equal'],
);
check(
match,
).isRejectedBy((it) => it.hasGroup(2).isNotNull(), actual: ['<null>']);
});
test('hasGroups', () {
check(match).hasGroups([0, 1, 2]).deepEquals(['ab', 'b', null]);

check(match).isRejectedBy(
(it) => it.hasGroups([0, 1]).deepEquals(['ab', 'wrong']),
actual: ['[\'ab\', \'b\']'],
which: ['at [<1>] is \'b\'', 'which does not equal \'wrong\''],
);

check(match).isRejectedBy(
(it) => it.hasGroups([10]),
actual: ['Match[ab]'],
which: [
'threw while trying to read groups: <RangeError: Value not in range: 10>',
],
);
});
});

group('RegExpMatchChecks', () {
final match = RegExp('a(?<foo>b)').firstMatch('zabz') as RegExpMatch;
test('getters', () {
check(match)
..groupNames.deepEquals(['foo'])
..pattern.isA<RegExp>();
});
test('hasNamedGroup', () {
check(match).hasNamedGroup('foo').isNotNull().equals('b');

check(match).isRejectedBy(
(it) => it.hasNamedGroup('foo').equals('wrong'),
actual: ['\'b\''],
which: ['are not equal'],
);
});
});

group('ConditionChecks', () {
test('descriptions', () {
check(
(Subject<Match> it) => it.hasGroup(1).equals('b'),
).description.deepEquals([' has group 1 that:', ' equals \'b\'']);
check(
(Subject<Match> it) => it.hasGroups([0, 1]).deepEquals(['ab', 'b']),
).description.deepEquals([
' has groups [0, 1] that:',
' is deeply equal to [\'ab\', \'b\']',
]);
check((Subject<RegExpMatch> it) => it.hasNamedGroup('foo').equals('b'))
.description
.deepEquals([' has named group "foo" that:', ' equals \'b\'']);
});
});
}
Loading