| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // This test runs `flutter test` on the `trivial_widget_test.dart` four times. |
| // |
| // The first time, the result is ignored, on the basis that it's warming the |
| // cache. |
| // |
| // The second time tests how long a regular test takes to run. |
| // |
| // Before the third time, a change is made to the implementation of one of the |
| // files that the test depends on (indirectly). |
| // |
| // Before the fourth time, a change is made to the interface in that same file. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:flutter_devicelab/framework/framework.dart'; |
| import 'package:flutter_devicelab/framework/task_result.dart'; |
| import 'package:flutter_devicelab/framework/utils.dart'; |
| import 'package:path/path.dart' as path; |
| |
| // Matches the output of the "test" package, e.g.: "00:01 +1 loading foo" |
| final RegExp testOutputPattern = RegExp(r'^[0-9][0-9]:[0-9][0-9] \+[0-9]+: (.+?) *$'); |
| |
| enum TestStep { |
| starting, |
| buildingFlutterTool, |
| runningPubGet, |
| testWritesFirstCarriageReturn, |
| testLoading, |
| testRunning, |
| testPassed, |
| } |
| |
| Future<int> runTest({bool coverage = false, bool noPub = false}) async { |
| final Stopwatch clock = Stopwatch()..start(); |
| final List<String> arguments = flutterCommandArgs('test', <String>[ |
| if (coverage) '--coverage', |
| if (noPub) '--no-pub', |
| path.join('flutter_test', 'trivial_widget_test.dart'), |
| ]); |
| final Process analysis = await startProcess( |
| path.join(flutterDirectory.path, 'bin', 'flutter'), |
| arguments, |
| workingDirectory: path.join(flutterDirectory.path, 'dev', 'automated_tests'), |
| ); |
| int badLines = 0; |
| TestStep step = TestStep.starting; |
| |
| analysis.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((String entry) { |
| print('test stdout ($step): $entry'); |
| if (step == TestStep.starting && entry == 'Building flutter tool...') { |
| // ignore this line |
| step = TestStep.buildingFlutterTool; |
| } else if (step == TestStep.testPassed && entry.contains('Collecting coverage information...')) { |
| // ignore this line |
| } else if (step.index < TestStep.runningPubGet.index && entry == 'Running "flutter pub get" in automated_tests...') { |
| // ignore this line |
| step = TestStep.runningPubGet; |
| } else if (step.index <= TestStep.testWritesFirstCarriageReturn.index && entry.trim() == '') { |
| // we have a blank line at the start |
| step = TestStep.testWritesFirstCarriageReturn; |
| } else { |
| final Match? match = testOutputPattern.matchAsPrefix(entry); |
| if (match == null) { |
| badLines += 1; |
| } else { |
| if (step.index >= TestStep.testWritesFirstCarriageReturn.index && step.index <= TestStep.testLoading.index && match.group(1)!.startsWith('loading ')) { |
| // first the test loads |
| step = TestStep.testLoading; |
| } else if (step.index <= TestStep.testRunning.index && match.group(1) == 'A trivial widget test') { |
| // then the test runs |
| step = TestStep.testRunning; |
| } else if (step.index < TestStep.testPassed.index && match.group(1) == 'All tests passed!') { |
| // then the test finishes |
| step = TestStep.testPassed; |
| } else { |
| badLines += 1; |
| } |
| } |
| } |
| }); |
| analysis.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((String entry) { |
| print('test stderr: $entry'); |
| badLines += 1; |
| }); |
| final int result = await analysis.exitCode; |
| clock.stop(); |
| if (result != 0) { |
| throw Exception('flutter test failed with exit code $result'); |
| } |
| if (badLines > 0) { |
| throw Exception('flutter test rendered unexpected output ($badLines bad lines)'); |
| } |
| if (step != TestStep.testPassed) { |
| throw Exception('flutter test did not finish (only reached step $step)'); |
| } |
| print('elapsed time: ${clock.elapsedMilliseconds}ms'); |
| return clock.elapsedMilliseconds; |
| } |
| |
| Future<void> pubGetDependencies(List<Directory> directories) async { |
| for (final Directory directory in directories) { |
| await inDirectory<void>(directory, () async { |
| await flutter('pub', options: <String>['get']); |
| }); |
| } |
| } |
| |
| void main() { |
| task(() async { |
| final File nodeSourceFile = File(path.join( |
| flutterDirectory.path, 'packages', 'flutter', 'lib', 'src', 'foundation', 'node.dart', |
| )); |
| await pubGetDependencies(<Directory>[Directory(path.join(flutterDirectory.path, 'dev', 'automated_tests')),]); |
| final String originalSource = await nodeSourceFile.readAsString(); |
| try { |
| await runTest(noPub: true); // first number is meaningless; could have had to build the tool, run pub get, have a cache, etc |
| final int withoutChange = await runTest(noPub: true); // run test again with no change |
| await nodeSourceFile.writeAsString( // only change implementation |
| originalSource |
| .replaceAll('_owner', '_xyzzy') |
| ); |
| final int implementationChange = await runTest(noPub: true); // run test again with implementation changed |
| await nodeSourceFile.writeAsString( // change interface as well |
| originalSource |
| .replaceAll('_owner', '_xyzzy') |
| .replaceAll('owner', '_owner') |
| .replaceAll('_xyzzy', 'owner') |
| ); |
| final int interfaceChange = await runTest(noPub: true); // run test again with interface changed |
| // run test with coverage enabled. |
| final int withCoverage = await runTest(coverage: true, noPub: true); |
| final Map<String, dynamic> data = <String, dynamic>{ |
| 'without_change_elapsed_time_ms': withoutChange, |
| 'implementation_change_elapsed_time_ms': implementationChange, |
| 'interface_change_elapsed_time_ms': interfaceChange, |
| 'with_coverage_time_ms': withCoverage, |
| }; |
| return TaskResult.success(data, benchmarkScoreKeys: data.keys.toList()); |
| } finally { |
| await nodeSourceFile.writeAsString(originalSource); |
| } |
| }); |
| } |