blob: 1022f4c2d08297310aec309f1518d6778777c04c [file] [log] [blame]
// 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 'browser.dart';
import 'common.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',
defaultsTo: false,
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',
defaultsTo: false,
abbr: 'w',
help: 'Run in watch mode so the tests re-run whenever a change is '
'made.',
)
..addFlag(
'unit-tests-only',
defaultsTo: false,
help: 'felt test command runs the unit tests and the integration tests '
'at the same time. If this flag is set, only run the unit tests.',
)
..addFlag('use-system-flutter',
defaultsTo: false,
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(
'update-screenshot-goldens',
defaultsTo: false,
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.',
)
..addFlag(
'skip-goldens-repo-fetch',
defaultsTo: false,
help: 'If set reuses the existig flutter/goldens repo clone. Use this '
'to avoid overwriting local changes when iterating on golden '
'tests. This is off by default.',
)
..addOption(
'browser',
defaultsTo: 'chrome',
help: 'An option to choose a browser to run the tests. Tests only work '
' on Chrome for now.',
)
..addFlag(
'fail-early',
defaultsTo: false,
negatable: true,
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.',
);
}
@override
final String name = 'test';
@override
final String description = 'Run tests.';
bool get isWatchMode => boolArg('watch');
bool get failEarly => boolArg('fail-early');
/// 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 browser => stringArg('browser');
/// When running screenshot tests writes them to the file system into
/// ".dart_tool/goldens".
bool get doUpdateScreenshotGoldens => boolArg('update-screenshot-goldens');
/// Whether to fetch the goldens repo prior to running tests.
bool get skipGoldensRepoFetch => boolArg('skip-goldens-repo-fetch');
@override
Future<bool> run() async {
final BrowserEnvironment browserEnvironment = getBrowserEnvironment(browser);
final List<FilePath> testFiles = runAllTests
? findAllTests()
: targetFiles;
final Pipeline testPipeline = Pipeline(steps: <PipelineStep>[
if (isWatchMode) ClearTerminalScreenStep(),
CompileTestsStep(
skipGoldensRepoFetch: skipGoldensRepoFetch,
testFiles: testFiles,
),
RunTestsStep(
browserEnvironment: browserEnvironment,
testFiles: testFiles,
isDebug: isDebug,
doUpdateScreenshotGoldens: doUpdateScreenshotGoldens,
),
]);
await testPipeline.run();
if (isWatchMode) {
final FilePath dir = FilePath.fromWebUi('');
print('');
print('Initial test run is done!');
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');
}
}
}