Merge branch 'master' into raw-test-api
diff --git a/pkgs/checks/lib/src/checks.dart b/pkgs/checks/lib/src/checks.dart
index c01a6a3..bca3071 100644
--- a/pkgs/checks/lib/src/checks.dart
+++ b/pkgs/checks/lib/src/checks.dart
@@ -74,7 +74,7 @@
].join('\n'));
},
allowAsync: true,
- allowLateFailure: true,
+ allowUnawaited: true,
));
/// Checks whether [value] satisfies all expectations invoked in [condition].
@@ -92,7 +92,7 @@
failure = f;
},
allowAsync: false,
- allowLateFailure: false,
+ allowUnawaited: false,
));
condition.apply(check);
return failure;
@@ -114,7 +114,7 @@
failure = f;
},
allowAsync: true,
- allowLateFailure: false,
+ allowUnawaited: false,
));
await condition.applyAsync(check);
return failure;
@@ -135,7 +135,7 @@
throw UnimplementedError();
},
allowAsync: false,
- allowLateFailure: true,
+ allowUnawaited: true,
);
condition.apply(Check._(context));
return context.detail(context).expected.skip(1);
@@ -323,19 +323,19 @@
final void Function(CheckFailure) _fail;
final bool _allowAsync;
- final bool _allowLateFailure;
+ final bool _allowUnawaited;
_TestContext._root({
required _Optional<T> value,
required void Function(CheckFailure) fail,
required bool allowAsync,
- required bool allowLateFailure,
+ required bool allowUnawaited,
String? label,
}) : _value = value,
_label = label ?? '',
_fail = fail,
_allowAsync = allowAsync,
- _allowLateFailure = allowLateFailure,
+ _allowUnawaited = allowUnawaited,
_parent = null,
_clauses = [],
_aliases = [];
@@ -346,7 +346,7 @@
_aliases = original._aliases,
_fail = original._fail,
_allowAsync = original._allowAsync,
- _allowLateFailure = original._allowLateFailure,
+ _allowUnawaited = original._allowUnawaited,
// Never read from an aliased context because they are never present in
// `_clauses`.
_label = '';
@@ -355,7 +355,7 @@
: _parent = parent,
_fail = parent._fail,
_allowAsync = parent._allowAsync,
- _allowLateFailure = parent._allowLateFailure,
+ _allowUnawaited = parent._allowUnawaited,
_clauses = [],
_aliases = [];
@@ -387,7 +387,7 @@
@override
void expectUnawaited(Iterable<String> Function() clause,
void Function(T actual, void Function(Rejection) reject) predicate) {
- if (!_allowLateFailure) {
+ if (!_allowUnawaited) {
throw StateError('Late expectations cannot be used for soft checks');
}
_clauses.add(_StringClause(clause));
@@ -658,6 +658,10 @@
@override
final _ReplayContext<T> _context = _ReplayContext();
+
+ String toString() {
+ return ['A value that:', ...describe(_context)].join('\n');
+ }
}
class _ReplayContext<T> implements Context<T>, Condition<T> {
diff --git a/pkgs/checks/lib/src/collection_equality.dart b/pkgs/checks/lib/src/collection_equality.dart
new file mode 100644
index 0000000..fff7b04
--- /dev/null
+++ b/pkgs/checks/lib/src/collection_equality.dart
@@ -0,0 +1,320 @@
+// Copyright (c) 2023, 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:collection';
+
+import 'package:checks/context.dart';
+
+/// Returns a rejection if the elements of [actual] are unequal to the elements
+/// of [expected].
+///
+/// {@template deep_collection_equals}
+/// Elements, keys, or values, which are a collections are deeply compared for
+/// equality, and do not use the native identity based equality or custom
+/// equality operator overrides.
+/// Elements, keys, or values, which are a [Condition] instances are checked
+/// against actual values.
+/// All other value or key types use `operator ==`.
+///
+/// Comparing sets or maps will have a runtime which is polynomial on the the
+/// size of those collections. Does not use [Set.contains] or [Map.containsKey],
+/// there will not be runtime benefits from hashing. Custom collection behavior
+/// is ignored. For example, it is not possible to distinguish between a `Set`
+/// and a `Set.identity`.
+///
+/// Collections may be nested to a maximum depth of 1000. Recursive collections
+/// are not allowed.
+/// {@endtemplate}
+Rejection? deepCollectionEquals(Object actual, Object expected) {
+ try {
+ return _deepCollectionEquals(actual, expected, 0);
+ } on _ExceededDepthError {
+ return Rejection(
+ actual: literal(actual),
+ which: ['exceeds the depth limit of $_maxDepth']);
+ }
+}
+
+const _maxDepth = 1000;
+
+class _ExceededDepthError extends Error {}
+
+Rejection? _deepCollectionEquals(Object actual, Object expected, int depth) {
+ assert(actual is Iterable || actual is Map);
+ assert(expected is Iterable || expected is Map);
+
+ final queue = Queue.of([_Search(_Path.root(), actual, expected, depth)]);
+ while (queue.isNotEmpty) {
+ final toCheck = queue.removeFirst();
+ final currentActual = toCheck.actual;
+ final currentExpected = toCheck.expected;
+ final path = toCheck.path;
+ final currentDepth = toCheck.depth;
+ Iterable<String>? rejectionWhich;
+ if (currentExpected is Set) {
+ rejectionWhich = _findSetDifference(
+ currentActual, currentExpected, path, currentDepth);
+ } else if (currentExpected is Iterable) {
+ rejectionWhich = _findIterableDifference(
+ currentActual, currentExpected, path, queue, currentDepth);
+ } else {
+ currentExpected as Map;
+ rejectionWhich = _findMapDifference(
+ currentActual, currentExpected, path, currentDepth);
+ }
+ if (rejectionWhich != null) {
+ return Rejection(actual: literal(actual), which: rejectionWhich);
+ }
+ }
+ return null;
+}
+
+List<String>? _findIterableDifference(Object? actual,
+ Iterable<Object?> expected, _Path path, Queue<_Search> queue, int depth) {
+ if (actual is! Iterable) {
+ return ['${path}is not an Iterable'];
+ }
+ var actualIterator = actual.iterator;
+ var expectedIterator = expected.iterator;
+ for (var index = 0;; index++) {
+ var actualNext = actualIterator.moveNext();
+ var expectedNext = expectedIterator.moveNext();
+ if (!expectedNext && !actualNext) break;
+ if (!expectedNext) {
+ return [
+ '${path}has more elements than expected',
+ 'expected an iterable with $index element(s)'
+ ];
+ }
+ if (!actualNext) {
+ return [
+ '${path}has too few elements',
+ 'expected an iterable with at least ${index + 1} element(s)'
+ ];
+ }
+ var actualValue = actualIterator.current;
+ var expectedValue = expectedIterator.current;
+ if (expectedValue is Iterable || expectedValue is Map) {
+ if (depth + 1 > _maxDepth) throw _ExceededDepthError();
+ queue.addLast(
+ _Search(path.append(index), actualValue, expectedValue, depth + 1));
+ } else if (expectedValue is Condition) {
+ final failure = softCheck(actualValue, expectedValue);
+ if (failure != null) {
+ final which = failure.rejection.which;
+ return [
+ 'has an element ${path.append(index)}that:',
+ ...indent(failure.detail.actual.skip(1)),
+ ...indent(prefixFirst('Actual: ', failure.rejection.actual),
+ failure.detail.depth + 1),
+ if (which != null)
+ ...indent(prefixFirst('which ', which), failure.detail.depth + 1)
+ ];
+ }
+ } else {
+ if (actualValue != expectedValue) {
+ return [
+ ...prefixFirst('${path.append(index)}is ', literal(actualValue)),
+ ...prefixFirst('which does not equal ', literal(expectedValue))
+ ];
+ }
+ }
+ }
+ return null;
+}
+
+bool _elementMatches(Object? actual, Object? expected, int depth) {
+ if (expected == null) return actual == null;
+ if (expected is Iterable || expected is Map) {
+ if (++depth > _maxDepth) throw _ExceededDepthError();
+ return actual != null &&
+ _deepCollectionEquals(actual, expected, depth) == null;
+ }
+ if (expected is Condition) {
+ return softCheck(actual, expected) == null;
+ }
+ return expected == actual;
+}
+
+Iterable<String>? _findSetDifference(
+ Object? actual, Set<Object?> expected, _Path path, int depth) {
+ if (actual is! Set) {
+ return ['${path}is not a Set'];
+ }
+ final indexedExpected = expected.toList();
+ final indexedActual = actual.toList();
+ final adjacency = <List<int>>[];
+
+ for (final expectedElement in indexedExpected) {
+ final pairs = [
+ for (var j = 0; j < indexedActual.length; j++)
+ if (_elementMatches(indexedActual[j], expectedElement, depth)) j,
+ ];
+ if (pairs.isEmpty) {
+ return prefixFirst(
+ '${path}has no element to match ', literal(expectedElement));
+ }
+ adjacency.add(pairs);
+ }
+ if (indexedActual.length != indexedExpected.length) {
+ return [
+ '${path}has ${indexedActual.length} element(s),',
+ 'expected a set with ${indexedExpected.length} element(s)'
+ ];
+ }
+ if (!_hasPerfectMatching(adjacency)) {
+ return prefixFirst(
+ '${path}cannot be matched with the elements of ', literal(expected));
+ }
+ return null;
+}
+
+Iterable<String>? _findMapDifference(
+ Object? actual, Map<Object?, Object?> expected, _Path path, int depth) {
+ if (actual is! Map) {
+ return ['${path}is not a Map'];
+ }
+ final expectedEntries = expected.entries.toList();
+ final actualEntries = actual.entries.toList();
+ final adjacency = <List<int>>[];
+ for (final expectedEntry in expectedEntries) {
+ final potentialPairs = [
+ for (var i = 0; i < actualEntries.length; i++)
+ if (_elementMatches(actualEntries[i].key, expectedEntry.key, depth)) i
+ ];
+ if (potentialPairs.isEmpty) {
+ return prefixFirst(
+ '${path}has no key to match ', literal(expectedEntry.key));
+ }
+ final matchingPairs = [
+ for (var i in potentialPairs)
+ if (_elementMatches(actualEntries[i].value, expectedEntry.value, depth))
+ i
+ ];
+ if (matchingPairs.isEmpty) {
+ return prefixFirst(
+ '${path.append(expectedEntry.key)}has no value to match ',
+ literal(expectedEntry.value));
+ }
+ adjacency.add(matchingPairs);
+ }
+ if (expectedEntries.length != actualEntries.length) {
+ return [
+ '${path}has ${actualEntries.length} entries,',
+ 'expected a Map with ${expectedEntries.length} entries'
+ ];
+ }
+ if (!_hasPerfectMatching(adjacency)) {
+ return prefixFirst(
+ '${path}cannot be matched with the entries of ', literal(expected));
+ }
+ return null;
+}
+
+class _Path {
+ final _Path? parent;
+ final Object? index;
+ _Path._(this.parent, this.index);
+ _Path.root()
+ : parent = null,
+ index = '';
+ _Path append(Object? index) => _Path._(this, index);
+ String toString() {
+ if (parent == null && index == '') return '';
+ final stack = Queue.of([this]);
+ var current = this.parent;
+ while (current?.parent != null) {
+ stack.addLast(current!);
+ current = current.parent;
+ }
+ final result = StringBuffer('at ');
+ while (stack.isNotEmpty) {
+ result.write('[');
+ result.write(literal(stack.removeLast().index).join(r'\n'));
+ result.write(']');
+ }
+ result.write(' ');
+ return result.toString();
+ }
+}
+
+class _Search {
+ final _Path path;
+ final Object? actual;
+ final Object? expected;
+ final int depth;
+ _Search(this.path, this.actual, this.expected, this.depth);
+}
+
+/// Returns true if [adjacency] represents a bipartite graph that has a perfect
+/// pairing without unpaired elements in either set.
+///
+/// Vertices are represented as integers - a vertice in `u` is an index in
+/// [adjacency], and a vertice in `v` is a value in list at that index. An edge
+/// from `U[n]` to `V[m]` is represented by the value `m` being present in the
+/// list at index `n`.
+/// Assumes that there are an equal number of values in both sets, equal to the
+/// length of [adjacency].
+///
+/// Uses the Hopcroft–Karp algorithm based on pseudocode from
+/// https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm
+bool _hasPerfectMatching(List<List<int>> adjacency) {
+ final length = adjacency.length;
+ // The index [length] represents a "dummy vertex"
+ final distances = List<num>.filled(length + 1, double.infinity);
+ // Initially, everything is paired with the "dummy vertex"
+ final leftPairs = List.filled(length, length);
+ final rightPairs = List.filled(length, length);
+ bool bfs() {
+ final queue = Queue<int>();
+ for (int leftIndex = 0; leftIndex < length; leftIndex++) {
+ if (leftPairs[leftIndex] == length) {
+ distances[leftIndex] = 0;
+ queue.add(leftIndex);
+ } else {
+ distances[leftIndex] = double.infinity;
+ }
+ }
+ distances.last = double.infinity;
+ while (queue.isNotEmpty) {
+ final current = queue.removeFirst();
+ if (distances[current] < distances[length]) {
+ for (final rightIndex in adjacency[current]) {
+ if (distances[rightPairs[rightIndex]].isInfinite) {
+ distances[rightPairs[rightIndex]] = distances[current] + 1;
+ queue.addLast(rightPairs[rightIndex]);
+ }
+ }
+ }
+ }
+ return !distances.last.isInfinite;
+ }
+
+ bool dfs(int leftIndex) {
+ if (leftIndex == length) return true;
+ for (final rightIndex in adjacency[leftIndex]) {
+ if (distances[rightPairs[rightIndex]] == distances[leftIndex] + 1) {
+ if (dfs(rightPairs[rightIndex])) {
+ leftPairs[leftIndex] = rightIndex;
+ rightPairs[rightIndex] = leftIndex;
+ return true;
+ }
+ }
+ }
+ distances[leftIndex] = double.infinity;
+ return false;
+ }
+
+ var matching = 0;
+ while (bfs()) {
+ for (int leftIndex = 0; leftIndex < length; leftIndex++) {
+ if (leftPairs[leftIndex] == length) {
+ if (dfs(leftIndex)) {
+ matching++;
+ }
+ }
+ }
+ }
+ return matching == length;
+}
diff --git a/pkgs/checks/lib/src/extensions/iterable.dart b/pkgs/checks/lib/src/extensions/iterable.dart
index 5448088..ca49a3b 100644
--- a/pkgs/checks/lib/src/extensions/iterable.dart
+++ b/pkgs/checks/lib/src/extensions/iterable.dart
@@ -4,6 +4,7 @@
import 'package:checks/context.dart';
+import '../collection_equality.dart';
import 'core.dart';
extension IterableChecks<T> on Check<Iterable<T>> {
@@ -92,6 +93,14 @@
});
}
+ /// Expects that the iterable contains elements that are deeply equal to the
+ /// elements of [expected].
+ ///
+ /// {@macro deep_collection_equals}
+ void deepEquals(Iterable<Object?> expected) => context.expect(
+ () => prefixFirst('is deeply equal to ', literal(expected)),
+ (actual) => deepCollectionEquals(actual, expected));
+
/// Expects that the iterable contains elements that correspond by the
/// [elementCondition] exactly to each element in [expected].
///
diff --git a/pkgs/checks/lib/src/extensions/map.dart b/pkgs/checks/lib/src/extensions/map.dart
index e87ecae..2f2ef72 100644
--- a/pkgs/checks/lib/src/extensions/map.dart
+++ b/pkgs/checks/lib/src/extensions/map.dart
@@ -4,6 +4,7 @@
import 'package:checks/context.dart';
+import '../collection_equality.dart';
import 'core.dart';
extension MapChecks<K, V> on Check<Map<K, V>> {
@@ -98,4 +99,12 @@
actual: literal(actual), which: ['Contains no matching value']);
});
}
+
+ /// Expects that the map contains entries that are deeply equal to the entries
+ /// of [expected].
+ ///
+ /// {@macro deep_collection_equals}
+ void deepEquals(Map<Object?, Object?> expected) => context.expect(
+ () => prefixFirst('is deeply equal to ', literal(expected)),
+ (actual) => deepCollectionEquals(actual, expected));
}
diff --git a/pkgs/checks/test/extensions/collection_equality_test.dart b/pkgs/checks/test/extensions/collection_equality_test.dart
new file mode 100644
index 0000000..4bf66c8
--- /dev/null
+++ b/pkgs/checks/test/extensions/collection_equality_test.dart
@@ -0,0 +1,166 @@
+// Copyright (c) 2023, 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:checks/checks.dart';
+import 'package:checks/src/collection_equality.dart';
+import 'package:test/scaffolding.dart';
+
+import '../test_shared.dart';
+
+void main() {
+ group('deepCollectionEquals', () {
+ test('allows nested collections with equal elements', () {
+ checkThat(deepCollectionEquals([
+ 'a',
+ {'b': 1},
+ {'c', 'd'},
+ [
+ ['e']
+ ],
+ ], [
+ 'a',
+ {'b': 1},
+ {'c', 'd'},
+ [
+ ['e']
+ ],
+ ])).isNull();
+ });
+
+ test('allows collections inside sets', () {
+ checkThat(deepCollectionEquals({
+ {'a': 1}
+ }, {
+ {'a': 1}
+ })).isNull();
+ });
+
+ test('allows collections as Map keys', () {
+ checkThat(deepCollectionEquals([
+ {
+ {'a': 1}: {'b': 2}
+ }
+ ], [
+ {
+ {'a': 1}: {'b': 2}
+ }
+ ])).isNull();
+ });
+
+ test('allows conditions in place of elements in lists', () {
+ checkThat(deepCollectionEquals([
+ 'a',
+ 'b'
+ ], [
+ it()
+ ..isA<String>().that(it()
+ ..startsWith('a')
+ ..length.isLessThan(2)),
+ it()..isA<String>().startsWith('b')
+ ])).isNull();
+ });
+
+ test('allows conditions in place of values in maps', () {
+ checkThat(deepCollectionEquals([
+ {'a': 'b'}
+ ], [
+ {'a': it()..isA<String>().startsWith('b')}
+ ])).isNull();
+ });
+
+ test('allows conditions in place of elements in sets', () {
+ checkThat(deepCollectionEquals(
+ {'b', 'a'}, {'a', it()..isA<String>().startsWith('b')})).isNull();
+ });
+
+ test('allows conditions in place of keys in maps', () {
+ checkThat(deepCollectionEquals(
+ {'a': 'b'}, {it()..isA<String>().startsWith('a'): 'b'})).isNull();
+ });
+
+ test('reports non-Set elements', () {
+ checkThat(deepCollectionEquals([
+ ['a']
+ ], [
+ {'a'}
+ ])).isARejection(which: ['at [<0>] is not a Set']);
+ });
+
+ test('reports long iterables', () {
+ checkThat(deepCollectionEquals([0], [])).isARejection(which: [
+ 'has more elements than expected',
+ 'expected an iterable with 0 element(s)'
+ ]);
+ });
+
+ test('reports short iterables', () {
+ checkThat(deepCollectionEquals([], [0])).isARejection(which: [
+ 'has too few elements',
+ 'expected an iterable with at least 1 element(s)'
+ ]);
+ });
+
+ test('reports unequal elements in iterables', () {
+ checkThat(deepCollectionEquals([0], [1]))
+ .isARejection(which: ['at [<0>] is <0>', 'which does not equal <1>']);
+ });
+
+ test('reports unmet conditions in iterables', () {
+ checkThat(deepCollectionEquals([0], [it()..isA<int>().isGreaterThan(0)]))
+ .isARejection(which: [
+ 'has an element at [<0>] that:',
+ ' Actual: <0>',
+ ' which is not greater than <0>'
+ ]);
+ });
+
+ test('reports unmet conditions in map values', () {
+ checkThat(deepCollectionEquals(
+ {'a': 'b'}, {'a': it()..isA<String>().startsWith('a')}))
+ .isARejection(which: [
+ "at ['a'] has no value to match <A value that:",
+ ' is a String',
+ " starts with 'a'>",
+ ]);
+ });
+
+ test('reports unmet conditions in map keys', () {
+ checkThat(deepCollectionEquals(
+ {'b': 'a'}, {it()..isA<String>().startsWith('a'): 'a'}))
+ .isARejection(which: [
+ 'has no key to match <A value that:',
+ ' is a String',
+ " starts with 'a'>",
+ ]);
+ });
+
+ test('reports recursive lists', () {
+ var l = [];
+ l.add(l);
+ checkThat(deepCollectionEquals(l, l))
+ .isARejection(which: ['exceeds the depth limit of 1000']);
+ });
+
+ test('reports recursive sets', () {
+ var s = <Object>{};
+ s.add(s);
+ checkThat(deepCollectionEquals(s, s))
+ .isARejection(which: ['exceeds the depth limit of 1000']);
+ });
+
+ test('reports maps with recursive keys', () {
+ var m = <Object, Object>{};
+ m[m] = 0;
+ checkThat(deepCollectionEquals(m, m))
+ .isARejection(which: ['exceeds the depth limit of 1000']);
+ });
+
+ test('reports maps with recursive values', () {
+ var m = <Object, Object>{};
+ m[0] = m;
+ checkThat(deepCollectionEquals(m, m))
+ .isARejection(which: ['exceeds the depth limit of 1000']);
+ });
+ });
+}
diff --git a/pkgs/checks/test/test_shared.dart b/pkgs/checks/test/test_shared.dart
index 10b49ce..7d17164 100644
--- a/pkgs/checks/test/test_shared.dart
+++ b/pkgs/checks/test/test_shared.dart
@@ -5,33 +5,30 @@
import 'package:checks/checks.dart';
import 'package:checks/context.dart';
-extension TestIterableCheck on Check<Iterable<String>?> {
- // TODO: remove this once we have a deepEquals or equivalent
- void toStringEquals(List<String>? other) {
- final otherToString = other.toString();
- context.expect(
- () => prefixFirst('toString equals ', literal(otherToString)),
- (actual) {
- final actualToString = actual.toString();
- return actualToString == otherToString
- ? null
- : Rejection(
- actual: literal(actualToString),
- which: ['does not have a matching toString'],
- );
- },
- );
+extension FailureCheck on Check<CheckFailure?> {
+ void isARejection({List<String>? which, List<String>? actual}) {
+ isNotNull()
+ .has((f) => f.rejection, 'rejection')
+ ._hasActualWhich(actual: actual, which: which);
}
}
-extension RejectionCheck on Check<CheckFailure?> {
+extension RejectionCheck on Check<Rejection?> {
void isARejection({List<String>? which, List<String>? actual}) {
- final rejection = this.isNotNull().has((f) => f.rejection, 'rejection');
+ isNotNull()._hasActualWhich(actual: actual, which: which);
+ }
+}
+
+extension _RejectionCheck on Check<Rejection> {
+ void _hasActualWhich({List<String>? which, List<String>? actual}) {
if (actual != null) {
- rejection
- .has((p0) => p0.actual.toList(), 'actual')
- .toStringEquals(actual);
+ has((r) => r.actual.toList(), 'actual').deepEquals(actual);
}
- rejection.has((p0) => p0.which?.toList(), 'which').toStringEquals(which);
+ final whichCheck = has((r) => r.which?.toList(), 'which');
+ if (which == null) {
+ whichCheck.isNull();
+ } else {
+ whichCheck.isNotNull().deepEquals(which);
+ }
}
}
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index ac91092..5e7c905 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -1,5 +1,8 @@
## 1.22.3-dev
+* Avoid empty expandable groups for tests without extra output in Github
+ reporter.
+
## 1.22.2
* Don't run `tearDown` until the test body and outstanding work is complete,
diff --git a/pkgs/test/pubspec.yaml b/pkgs/test/pubspec.yaml
index 68439f1..3113be8 100644
--- a/pkgs/test/pubspec.yaml
+++ b/pkgs/test/pubspec.yaml
@@ -33,7 +33,7 @@
yaml: ^3.0.0
# Use an exact version until the test_api and test_core package are stable.
test_api: 0.4.18
- test_core: 0.4.22
+ test_core: 0.4.23
dev_dependencies:
fake_async: ^1.0.0
diff --git a/pkgs/test/test/runner/configuration/top_level_error_test.dart b/pkgs/test/test/runner/configuration/top_level_error_test.dart
index 7f15f7e..ea2ffbe 100644
--- a/pkgs/test/test/runner/configuration/top_level_error_test.dart
+++ b/pkgs/test/test/runner/configuration/top_level_error_test.dart
@@ -390,15 +390,15 @@
.file(
'dart_test.yaml',
jsonEncode({
- 'paths': ['[invalid]']
+ 'paths': [':invalid']
}))
.create();
var test = await runTest(['test.dart']);
expect(test.stderr,
- containsInOrder(['Invalid path: Invalid character', '^^^^^^^^^']));
+ containsInOrder(['Invalid path: Invalid empty scheme', '^^^^^^^^']));
await test.shouldExit(exit_codes.data);
- }, skip: 'Broken by sdk#34988');
+ });
});
group('filename', () {
diff --git a/pkgs/test/test/runner/github_reporter_test.dart b/pkgs/test/test/runner/github_reporter_test.dart
index de3a7d8..ba094fb 100644
--- a/pkgs/test/test/runner/github_reporter_test.dart
+++ b/pkgs/test/test/runner/github_reporter_test.dart
@@ -27,22 +27,17 @@
test('success 1', () {});
test('success 2', () {});
test('success 3', () {});''', '''
- ::group::✅ success 1
- ::endgroup::
- ::group::✅ success 2
- ::endgroup::
- ::group::✅ success 3
- ::endgroup::
+ ✅ success 1
+ ✅ success 2
+ ✅ success 3
🎉 3 tests passed.''');
});
test('includes the platform name when multiple platforms are run', () {
return _expectReportLines('''
test('success 1', () {});''', [
- '::group::✅ [VM] success 1',
- '::endgroup::',
- '::group::✅ [Chrome] success 1',
- '::endgroup::',
+ '✅ [VM] success 1',
+ '✅ [Chrome] success 1',
'🎉 2 tests passed.',
], args: [
'-p',
@@ -97,14 +92,12 @@
oh no
test.dart 6:33 main.<fn>
::endgroup::
- ::group::✅ success 1
- ::endgroup::
+ ✅ success 1
::group::❌ failure 2 (failed)
oh no
test.dart 8:33 main.<fn>
::endgroup::
- ::group::✅ success 2
- ::endgroup::
+ ✅ success 2
::error::2 tests passed, 2 failed.''');
});
@@ -115,9 +108,7 @@
'really gosh dang long test name. Even longer than that. No, yet '
'longer. A little more... okay, that should do it.',
() {});''',
- '''
- ::group::✅ really gosh dang long test name. Even longer than that. No, yet longer. A little more... okay, that should do it.
- ::endgroup::''',
+ '✅ really gosh dang long test name. Even longer than that. No, yet longer. A little more... okay, that should do it.',
useContains: true,
);
});
@@ -142,8 +133,7 @@
third error
test.dart 12:34 main.<fn>.<fn>
::endgroup::
- ::group::✅ wait
- ::endgroup::
+ ✅ wait
::error::1 test passed, 1 failed.''');
});
@@ -158,8 +148,7 @@
test('second test so that the first failure is reported', () {});''',
'''
- ::group::✅ fail after completion
- ::endgroup::
+ ✅ fail after completion
::group::❌ fail after completion (failed after test completion)
foo
test.dart 8:62 main.<fn>.<fn>
@@ -169,8 +158,7 @@
or the [completes] matcher when testing async code.
test.dart 8:62 main.<fn>.<fn>
::endgroup::
- ::group::✅ second test so that the first failure is reported
- ::endgroup::
+ ✅ second test so that the first failure is reported
::error::1 test passed, 1 failed.''',
);
});
@@ -216,14 +204,12 @@
waitStarted.complete();
return testDone.future;
});''', '''
- ::group::✅ test
- ::endgroup::
+ ✅ test
one
two
three
four
- ::group::✅ wait
- ::endgroup::
+ ✅ wait
🎉 2 tests passed.''');
});
});
@@ -234,12 +220,9 @@
test('skip 1', () {}, skip: true);
test('skip 2', () {}, skip: true);
test('skip 3', () {}, skip: true);''', '''
- ::group::❎ skip 1 (skipped)
- ::endgroup::
- ::group::❎ skip 2 (skipped)
- ::endgroup::
- ::group::❎ skip 3 (skipped)
- ::endgroup::
+ ❎ skip 1 (skipped)
+ ❎ skip 2 (skipped)
+ ❎ skip 3 (skipped)
🎉 0 tests passed, 3 skipped.''');
});
@@ -250,12 +233,9 @@
test('test 2', () {});
test('test 3', () {});
}, skip: true);''', '''
- ::group::❎ skip test 1 (skipped)
- ::endgroup::
- ::group::❎ skip test 2 (skipped)
- ::endgroup::
- ::group::❎ skip test 3 (skipped)
- ::endgroup::
+ ❎ skip test 1 (skipped)
+ ❎ skip test 2 (skipped)
+ ❎ skip test 3 (skipped)
🎉 0 tests passed, 3 skipped.''');
});
@@ -265,14 +245,10 @@
test('success 1', () {});
test('skip 2', () {}, skip: true);
test('success 2', () {});''', '''
- ::group::❎ skip 1 (skipped)
- ::endgroup::
- ::group::✅ success 1
- ::endgroup::
- ::group::❎ skip 2 (skipped)
- ::endgroup::
- ::group::✅ success 2
- ::endgroup::
+ ❎ skip 1 (skipped)
+ ✅ success 1
+ ❎ skip 2 (skipped)
+ ✅ success 2
🎉 2 tests passed, 2 skipped.''');
});
@@ -288,18 +264,14 @@
oh no
test.dart 6:35 main.<fn>
::endgroup::
- ::group::❎ skip 1 (skipped)
- ::endgroup::
- ::group::✅ success 1
- ::endgroup::
+ ❎ skip 1 (skipped)
+ ✅ success 1
::group::❌ failure 2 (failed)
oh no
test.dart 9:35 main.<fn>
::endgroup::
- ::group::❎ skip 2 (skipped)
- ::endgroup::
- ::group::✅ success 2
- ::endgroup::
+ ❎ skip 2 (skipped)
+ ✅ success 2
::error::2 tests passed, 2 failed, 2 skipped.''');
});
@@ -324,8 +296,7 @@
tearDownAll(() {/* nothing to do here */});
test('test 1', () {});
});''', '''
- ::group::✅ one test 1
- ::endgroup::
+ ✅ one test 1
🎉 1 test passed.''');
});
@@ -339,8 +310,7 @@
::group::✅ one (setUpAll)
one
::endgroup::
- ::group::✅ one test 1
- ::endgroup::
+ ✅ one test 1
::group::✅ one (tearDownAll)
two
::endgroup::
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index b40d72e..3cfc241 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -1,5 +1,8 @@
# 0.4.23-dev
+* Avoid empty expandable groups for tests without extra output in Github
+ reporter.
+
# 0.4.22
* Don't run `tearDown` until the test body and outstanding work is complete,
diff --git a/pkgs/test_core/lib/src/runner/reporter/github.dart b/pkgs/test_core/lib/src/runner/reporter/github.dart
index 5adf17e..2c291d6 100644
--- a/pkgs/test_core/lib/src/runner/reporter/github.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/github.dart
@@ -154,15 +154,19 @@
if (_printPlatform) {
name = '[${test.suite.platform.runtime.name}] $name';
}
- _sink.writeln(_GithubMarkup.startGroup('$prefix $name$statusSuffix'));
- for (var message in messages) {
- _sink.writeln(message.text);
+ if (messages.isEmpty && errors.isEmpty) {
+ _sink.writeln('$prefix $name$statusSuffix');
+ } else {
+ _sink.writeln(_GithubMarkup.startGroup('$prefix $name$statusSuffix'));
+ for (var message in messages) {
+ _sink.writeln(message.text);
+ }
+ for (var error in errors) {
+ _sink.writeln('${error.error}');
+ _sink.writeln(error.stackTrace.toString().trimRight());
+ }
+ _sink.writeln(_GithubMarkup.endGroup);
}
- for (var error in errors) {
- _sink.writeln('${error.error}');
- _sink.writeln(error.stackTrace.toString().trimRight());
- }
- _sink.writeln(_GithubMarkup.endGroup);
}
/// A callback called when [test] throws [error].