Skip to content

Commit 21f5da1

Browse files
jiahaogEgor
authored and
Egor
committed
[e2e] Fix incorrect test results when one test passes then another fails (flutter#2866)
* [e2e] Fix incorrect test results when one test passes then another fails For example, the following test will result in an error reported for the first test case. ``` void main() { testWidgets('a test that passes', (tester) async { expect(true, true); }); testWidgets('a test that fails', (tester) async { expect(true, false); }); } ``` We need to reset `reportTestException` back to the previous value after completion of `runTest`, or repeated failures will cause the exception handler for a previous test to be invoked, as they "stack". Instead of reseting it, however, do this once in the constructor because the test description is already provided by the function signature. * Add a mechanism for testing test results
1 parent ee8712b commit 21f5da1

8 files changed

+178
-17
lines changed

packages/e2e/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
## 0.6.2+1
2+
3+
* Fix incorrect test results when one test passes then another fails
4+
15
## 0.6.2
26

3-
* Fix `setSurfaceSize` for e2e tests.
7+
* Fix `setSurfaceSize` for e2e tests
48

59
## 0.6.1
610

packages/e2e/lib/e2e.dart

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,26 @@ class E2EWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding {
3232
}
3333
await _channel.invokeMethod<void>(
3434
'allTestsFinished',
35-
<String, dynamic>{'results': _results},
35+
<String, dynamic>{'results': results},
3636
);
3737
} on MissingPluginException {
3838
print('Warning: E2E test plugin was not detected.');
3939
}
4040
if (!_allTestsPassed.isCompleted) _allTestsPassed.complete(true);
4141
});
42+
43+
// TODO(jackson): Report the results individually instead of all at once
44+
// See https://github.com/flutter/flutter/issues/38985
45+
final TestExceptionReporter oldTestExceptionReporter = reportTestException;
46+
reportTestException =
47+
(FlutterErrorDetails details, String testDescription) {
48+
results[testDescription] = 'failed';
49+
_failureMethodsDetails.add(Failure(testDescription, details.toString()));
50+
if (!_allTestsPassed.isCompleted) {
51+
_allTestsPassed.complete(false);
52+
}
53+
oldTestExceptionReporter(details, testDescription);
54+
};
4255
}
4356

4457
// TODO(dnfield): Remove the ignore once we bump the minimum Flutter version
@@ -100,7 +113,12 @@ class E2EWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding {
100113

101114
static const MethodChannel _channel = MethodChannel('plugins.flutter.io/e2e');
102115

103-
static Map<String, String> _results = <String, String>{};
116+
/// Test results that will be populated after the tests have completed.
117+
///
118+
/// Keys are the test descriptions, and values are either `success` or
119+
/// `failed`.
120+
@visibleForTesting
121+
Map<String, String> results = <String, String>{};
104122

105123
/// The extra data for the reported result.
106124
///
@@ -158,24 +176,12 @@ class E2EWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding {
158176
String description = '',
159177
Duration timeout,
160178
}) async {
161-
// TODO(jackson): Report the results individually instead of all at once
162-
// See https://github.com/flutter/flutter/issues/38985
163-
final TestExceptionReporter oldTestExceptionReporter = reportTestException;
164-
reportTestException =
165-
(FlutterErrorDetails details, String testDescription) {
166-
_results[description] = 'failed';
167-
_failureMethodsDetails.add(Failure(testDescription, details.toString()));
168-
if (!_allTestsPassed.isCompleted) {
169-
_allTestsPassed.complete(false);
170-
}
171-
oldTestExceptionReporter(details, testDescription);
172-
};
173179
await super.runTest(
174180
testBody,
175181
invariantTester,
176182
description: description,
177183
timeout: timeout,
178184
);
179-
_results[description] ??= 'success';
185+
results[description] ??= 'success';
180186
}
181187
}

packages/e2e/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: e2e
22
description: Runs tests that use the flutter_test API as integration tests.
3-
version: 0.6.2
3+
version: 0.6.2+1
44
homepage: https://github.com/flutter/plugins/tree/master/packages/e2e
55

66
environment:
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import 'dart:async';
2+
import 'dart:io';
3+
import 'dart:convert';
4+
5+
import 'package:flutter_test/flutter_test.dart';
6+
7+
// Assumes that the flutter command is in `$PATH`.
8+
const String _flutterBin = 'flutter';
9+
const String _e2eResultsPrefix = 'E2EWidgetsFlutterBinding test results:';
10+
11+
void main() async {
12+
group('E2E binding result', () {
13+
test('when multiple tests pass', () async {
14+
final Map<String, dynamic> results =
15+
await _runTest('test/data/pass_test_script.dart');
16+
17+
expect(
18+
results,
19+
equals({
20+
'passing test 1': 'success',
21+
'passing test 2': 'success',
22+
}));
23+
});
24+
25+
test('when multiple tests fail', () async {
26+
final Map<String, dynamic> results =
27+
await _runTest('test/data/fail_test_script.dart');
28+
29+
expect(
30+
results,
31+
equals({
32+
'failing test 1': 'failed',
33+
'failing test 2': 'failed',
34+
}));
35+
});
36+
37+
test('when one test passes, then another fails', () async {
38+
final Map<String, dynamic> results =
39+
await _runTest('test/data/pass_then_fail_test_script.dart');
40+
41+
expect(
42+
results,
43+
equals({
44+
'passing test': 'success',
45+
'failing test': 'failed',
46+
}));
47+
});
48+
});
49+
}
50+
51+
/// Runs a test script and returns the [E2EWidgetsFlutterBinding.result].
52+
///
53+
/// [scriptPath] is relative to the package root.
54+
Future<Map<String, dynamic>> _runTest(String scriptPath) async {
55+
final Process process =
56+
await Process.start(_flutterBin, ['test', '--machine', scriptPath]);
57+
58+
// In the test [tearDownAll] block, the test results are encoded into JSON and
59+
// are printed with the [_e2eResultsPrefix] prefix.
60+
//
61+
// See the following for the test event spec which we parse the printed lines
62+
// out of: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md
63+
final String testResults = (await process.stdout
64+
.transform(utf8.decoder)
65+
.expand((String text) => text.split('\n'))
66+
.map((String line) {
67+
try {
68+
return jsonDecode(line);
69+
} on FormatException {
70+
// Only interested in test events which are JSON.
71+
}
72+
})
73+
.where((dynamic testEvent) =>
74+
testEvent != null && testEvent['type'] == 'print')
75+
.map((dynamic printEvent) => printEvent['message'] as String)
76+
.firstWhere(
77+
(String message) => message.startsWith(_e2eResultsPrefix)))
78+
.replaceAll(_e2eResultsPrefix, '');
79+
80+
return jsonDecode(testResults);
81+
}

packages/e2e/test/data/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Files in this directory are not invoked directly by the test command.
2+
3+
They are used as inputs for the other test files outside of this directory, so
4+
that failures can be tested.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'dart:convert';
2+
3+
import 'package:e2e/e2e.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
void main() async {
7+
final E2EWidgetsFlutterBinding binding =
8+
E2EWidgetsFlutterBinding.ensureInitialized();
9+
10+
testWidgets('failing test 1', (WidgetTester tester) async {
11+
expect(true, false);
12+
});
13+
14+
testWidgets('failing test 2', (WidgetTester tester) async {
15+
expect(true, false);
16+
});
17+
18+
tearDownAll(() {
19+
print(
20+
'E2EWidgetsFlutterBinding test results: ${jsonEncode(binding.results)}');
21+
});
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'dart:convert';
2+
3+
import 'package:e2e/e2e.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
void main() async {
7+
final E2EWidgetsFlutterBinding binding =
8+
E2EWidgetsFlutterBinding.ensureInitialized();
9+
10+
testWidgets('passing test 1', (WidgetTester tester) async {
11+
expect(true, true);
12+
});
13+
14+
testWidgets('passing test 2', (WidgetTester tester) async {
15+
expect(true, true);
16+
});
17+
18+
tearDownAll(() {
19+
print(
20+
'E2EWidgetsFlutterBinding test results: ${jsonEncode(binding.results)}');
21+
});
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'dart:convert';
2+
3+
import 'package:e2e/e2e.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
void main() async {
7+
final E2EWidgetsFlutterBinding binding =
8+
E2EWidgetsFlutterBinding.ensureInitialized();
9+
10+
testWidgets('passing test', (WidgetTester tester) async {
11+
expect(true, true);
12+
});
13+
14+
testWidgets('failing test', (WidgetTester tester) async {
15+
expect(true, false);
16+
});
17+
18+
tearDownAll(() {
19+
print(
20+
'E2EWidgetsFlutterBinding test results: ${jsonEncode(binding.results)}');
21+
});
22+
}

0 commit comments

Comments
 (0)