| // Copyright 2013 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. | |
| import 'dart:async'; | |
| import 'dart:io' as io; | |
| import 'package:args/command_runner.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:watcher/src/watch_event.dart'; | |
| import 'pipeline.dart'; | |
| import 'steps/compile_tests_step.dart'; | |
| import 'steps/run_tests_step.dart'; | |
| import 'utils.dart'; | |
| /// Runs tests. | |
| class TestCommand extends Command<bool> with ArgUtils<bool> { | |
| TestCommand() { | |
| argParser | |
| ..addFlag( | |
| 'debug', | |
| help: 'Pauses the browser before running a test, giving you an ' | |
| 'opportunity to add breakpoints or inspect loaded code before ' | |
| 'running the code.', | |
| ) | |
| ..addFlag( | |
| 'watch', | |
| abbr: 'w', | |
| help: 'Run in watch mode so the tests re-run whenever a change is ' | |
| 'made.', | |
| ) | |
| ..addFlag( | |
| 'use-system-flutter', | |
| help: 'integration tests are using flutter repository for various tasks' | |
| ', such as flutter drive, flutter pub get. If this flag is set, felt ' | |
| 'will use flutter command without cloning the repository. This flag ' | |
| 'can save internet bandwidth. However use with caution. Note that ' | |
| 'since flutter repo is always synced to youngest commit older than ' | |
| 'the engine commit for the tests running in CI, the tests results ' | |
| "won't be consistent with CIs when this flag is set. flutter " | |
| 'command should be set in the PATH for this flag to be useful.' | |
| 'This flag can also be used to test local Flutter changes.') | |
| ..addFlag( | |
| 'require-skia-gold', | |
| help: | |
| 'Whether we require Skia Gold to be available or not. When this ' | |
| 'flag is true, the tests will fail if Skia Gold is not available.', | |
| ) | |
| ..addFlag( | |
| 'update-screenshot-goldens', | |
| help: | |
| 'When running screenshot tests writes them to the file system into ' | |
| '.dart_tool/goldens. Use this option to bulk-update all screenshots, ' | |
| 'for example, when a new browser version affects pixels.', | |
| ) | |
| ..addOption( | |
| 'browser', | |
| defaultsTo: 'chrome', | |
| help: 'An option to choose a browser to run the tests. By default ' | |
| 'tests run in Chrome.', | |
| ) | |
| ..addFlag( | |
| 'fail-early', | |
| help: 'If set, causes the test runner to exit upon the first test ' | |
| 'failure. If not set, the test runner will continue running ' | |
| 'test despite failures and will report them after all tests ' | |
| 'finish.', | |
| ) | |
| ..addOption( | |
| 'canvaskit-path', | |
| help: 'Optional. The path to a local build of CanvasKit to use in ' | |
| 'tests. If omitted, the test runner uses the default CanvasKit ' | |
| 'build.', | |
| ) | |
| ..addFlag( | |
| 'wasm', | |
| help: 'Whether the test we are running are compiled to webassembly.' | |
| ) | |
| ..addFlag( | |
| 'use-local-canvaskit', | |
| help: 'Optional. Whether or not to use the locally built version of ' | |
| 'CanvasKit in the tests.', | |
| ); | |
| } | |
| @override | |
| final String name = 'test'; | |
| @override | |
| final String description = 'Run tests.'; | |
| bool get isWatchMode => boolArg('watch'); | |
| bool get failEarly => boolArg('fail-early'); | |
| bool get isWasm => boolArg('wasm'); | |
| /// Whether to start the browser in debug mode. | |
| /// | |
| /// In this mode the browser pauses before running the test to allow | |
| /// you set breakpoints or inspect the code. | |
| bool get isDebug => boolArg('debug'); | |
| /// Paths to targets to run, e.g. a single test. | |
| List<String> get targets => argResults!.rest; | |
| /// The target test files to run. | |
| List<FilePath> get targetFiles => targets.map((String t) => FilePath.fromCwd(t)).toList(); | |
| /// Whether all tests should run. | |
| bool get runAllTests => targets.isEmpty; | |
| /// The name of the browser to run tests in. | |
| String get browserName => stringArg('browser'); | |
| /// When running screenshot tests, require Skia Gold to be available and | |
| /// reachable. | |
| bool get requireSkiaGold => boolArg('require-skia-gold'); | |
| /// When running screenshot tests writes them to the file system into | |
| /// ".dart_tool/goldens". | |
| bool get doUpdateScreenshotGoldens => boolArg('update-screenshot-goldens'); | |
| /// Path to a CanvasKit build. Overrides the default CanvasKit. | |
| String? get overridePathToCanvasKit => argResults!['canvaskit-path'] as String?; | |
| /// Whether or not to use the locally built version of CanvasKit. | |
| bool get useLocalCanvasKit => boolArg('use-local-canvaskit'); | |
| @override | |
| Future<bool> run() async { | |
| final List<FilePath> testFiles = runAllTests | |
| ? findAllTests() | |
| : targetFiles; | |
| final Pipeline testPipeline = Pipeline(steps: <PipelineStep>[ | |
| if (isWatchMode) ClearTerminalScreenStep(), | |
| CompileTestsStep(testFiles: testFiles, useLocalCanvasKit: useLocalCanvasKit, isWasm: isWasm), | |
| RunTestsStep( | |
| browserName: browserName, | |
| testFiles: testFiles, | |
| isDebug: isDebug, | |
| isWasm: isWasm, | |
| doUpdateScreenshotGoldens: doUpdateScreenshotGoldens, | |
| requireSkiaGold: requireSkiaGold, | |
| overridePathToCanvasKit: overridePathToCanvasKit, | |
| ), | |
| ]); | |
| try { | |
| await testPipeline.run(); | |
| if (isWatchMode) { | |
| print(''); | |
| print('Initial test succeeded!'); | |
| } | |
| } catch(error, stackTrace) { | |
| if (isWatchMode) { | |
| // The error is printed but not rethrown in watch mode because | |
| // failures are expected. The idea is that the developer corrects the | |
| // error, saves the file, and the pipeline reruns. | |
| print(''); | |
| print('Initial test failed!\n'); | |
| print(error); | |
| print(stackTrace); | |
| } else { | |
| rethrow; | |
| } | |
| } | |
| if (isWatchMode) { | |
| final FilePath dir = FilePath.fromWebUi(''); | |
| print(''); | |
| print( | |
| 'Watching ${dir.relativeToCwd}/lib and ${dir.relativeToCwd}/test to re-run tests'); | |
| print(''); | |
| await PipelineWatcher( | |
| dir: dir.absolute, | |
| pipeline: testPipeline, | |
| ignore: (WatchEvent event) { | |
| // Ignore font files that are copied whenever tests run. | |
| if (event.path.endsWith('.ttf')) { | |
| return true; | |
| } | |
| // React to changes in lib/ and test/ folders. | |
| final String relativePath = | |
| path.relative(event.path, from: dir.absolute); | |
| if (path.isWithin('lib', relativePath) || | |
| path.isWithin('test', relativePath)) { | |
| return false; | |
| } | |
| // Ignore anything else. | |
| return true; | |
| }).start(); | |
| } | |
| return true; | |
| } | |
| } | |
| /// Clears the terminal screen and places the cursor at the top left corner. | |
| /// | |
| /// This works on Linux and Mac. On Windows, it's a no-op. | |
| class ClearTerminalScreenStep implements PipelineStep { | |
| @override | |
| String get description => 'clearing terminal screen'; | |
| @override | |
| bool get isSafeToInterrupt => false; | |
| @override | |
| Future<void> interrupt() async {} | |
| @override | |
| Future<void> run() async { | |
| if (!io.Platform.isWindows) { | |
| // See: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences | |
| print('\x1B[2J\x1B[1;2H'); | |
| } | |
| } | |
| } |