mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Add --watch flag to 'felt test' (#23727)
This commit is contained in:
parent
d4a7358351
commit
9bc776a841
@ -7,7 +7,7 @@
|
||||
`felt` supports multiple commands as follows:
|
||||
|
||||
1. **`felt check-licenses`**: Checks that all Dart and JS source code files contain the correct license headers.
|
||||
2. **`felt test`**: Runs all or some tests depending on the passed arguments.
|
||||
2. **`felt test`**: Runs all or some tests depending on the passed arguments. It supports a watch mode for convenience.
|
||||
3. **`felt build`**: Builds the engine locally so it can be used by Flutter apps. It also supports a watch mode for more convenience.
|
||||
|
||||
You could also run `felt help` or `felt help <command>` to get more information about the available commands and arguments.
|
||||
@ -43,6 +43,18 @@ To run all tests on Chrome. This will run both integration tests and the unit te
|
||||
felt test
|
||||
```
|
||||
|
||||
To run a specific test:
|
||||
|
||||
```
|
||||
felt test test/engine/util_test.dart
|
||||
```
|
||||
|
||||
To enable watch mode so that the test re-runs on every change:
|
||||
|
||||
```
|
||||
felt test --watch test/engine/util_test.dart
|
||||
```
|
||||
|
||||
To run unit tests only:
|
||||
|
||||
```
|
||||
|
||||
@ -6,12 +6,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import 'environment.dart';
|
||||
import 'utils.dart';
|
||||
import 'watcher.dart';
|
||||
|
||||
class BuildCommand extends Command<bool> with ArgUtils {
|
||||
BuildCommand() {
|
||||
@ -37,7 +36,7 @@ class BuildCommand extends Command<bool> with ArgUtils {
|
||||
final FilePath libPath = FilePath.fromWebUi('lib');
|
||||
final Pipeline buildPipeline = Pipeline(steps: <PipelineStep>[
|
||||
gn,
|
||||
() => ninja(),
|
||||
ninja,
|
||||
]);
|
||||
await buildPipeline.start();
|
||||
|
||||
@ -77,124 +76,3 @@ Future<void> ninja() {
|
||||
environment.hostDebugUnoptDir.path,
|
||||
]);
|
||||
}
|
||||
|
||||
enum PipelineStatus {
|
||||
idle,
|
||||
started,
|
||||
stopping,
|
||||
stopped,
|
||||
error,
|
||||
done,
|
||||
}
|
||||
|
||||
typedef PipelineStep = Future<void> Function();
|
||||
|
||||
class Pipeline {
|
||||
Pipeline({@required this.steps});
|
||||
|
||||
final Iterable<PipelineStep> steps;
|
||||
|
||||
Future<dynamic> _currentStepFuture;
|
||||
|
||||
PipelineStatus status = PipelineStatus.idle;
|
||||
|
||||
Future<void> start() async {
|
||||
status = PipelineStatus.started;
|
||||
try {
|
||||
for (PipelineStep step in steps) {
|
||||
if (status != PipelineStatus.started) {
|
||||
break;
|
||||
}
|
||||
_currentStepFuture = step();
|
||||
await _currentStepFuture;
|
||||
}
|
||||
status = PipelineStatus.done;
|
||||
} catch (error, stackTrace) {
|
||||
status = PipelineStatus.error;
|
||||
print('Error in the pipeline: $error');
|
||||
print(stackTrace);
|
||||
} finally {
|
||||
_currentStepFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() {
|
||||
status = PipelineStatus.stopping;
|
||||
return (_currentStepFuture ?? Future<void>.value(null)).then((_) {
|
||||
status = PipelineStatus.stopped;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
typedef WatchEventPredicate = bool Function(WatchEvent event);
|
||||
|
||||
class PipelineWatcher {
|
||||
PipelineWatcher({
|
||||
@required this.dir,
|
||||
@required this.pipeline,
|
||||
this.ignore,
|
||||
}) : watcher = DirectoryWatcher(dir);
|
||||
|
||||
/// The path of the directory to watch for changes.
|
||||
final String dir;
|
||||
|
||||
/// The pipeline to be executed when an event is fired by the watcher.
|
||||
final Pipeline pipeline;
|
||||
|
||||
/// Used to watch a directory for any file system changes.
|
||||
final DirectoryWatcher watcher;
|
||||
|
||||
/// A callback that determines whether to rerun the pipeline or not for a
|
||||
/// given [WatchEvent] instance.
|
||||
final WatchEventPredicate ignore;
|
||||
|
||||
void start() {
|
||||
watcher.events.listen(_onEvent);
|
||||
}
|
||||
|
||||
int _pipelineRunCount = 0;
|
||||
Timer _scheduledPipeline;
|
||||
|
||||
void _onEvent(WatchEvent event) {
|
||||
if (ignore != null && ignore(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String relativePath = path.relative(event.path, from: dir);
|
||||
print('- [${event.type}] ${relativePath}');
|
||||
|
||||
_pipelineRunCount++;
|
||||
_scheduledPipeline?.cancel();
|
||||
_scheduledPipeline = Timer(const Duration(milliseconds: 100), () {
|
||||
_scheduledPipeline = null;
|
||||
_runPipeline();
|
||||
});
|
||||
}
|
||||
|
||||
void _runPipeline() {
|
||||
int runCount;
|
||||
switch (pipeline.status) {
|
||||
case PipelineStatus.started:
|
||||
pipeline.stop().then((_) {
|
||||
runCount = _pipelineRunCount;
|
||||
pipeline.start().then((_) => _pipelineDone(runCount));
|
||||
});
|
||||
break;
|
||||
|
||||
case PipelineStatus.stopping:
|
||||
// We are already trying to stop the pipeline. No need to do anything.
|
||||
break;
|
||||
|
||||
default:
|
||||
runCount = _pipelineRunCount;
|
||||
pipeline.start().then((_) => _pipelineDone(runCount));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _pipelineDone(int pipelineRunCount) {
|
||||
if (pipelineRunCount == _pipelineRunCount) {
|
||||
print('*** Done! ***');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ import 'safari_installation.dart';
|
||||
import 'supported_browsers.dart';
|
||||
import 'test_platform.dart';
|
||||
import 'utils.dart';
|
||||
import 'watcher.dart';
|
||||
|
||||
/// The type of tests requested by the tool user.
|
||||
enum TestTypesRequested {
|
||||
@ -48,6 +49,12 @@ class TestCommand extends Command<bool> with ArgUtils {
|
||||
'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(
|
||||
'unit-tests-only',
|
||||
defaultsTo: false,
|
||||
@ -100,6 +107,8 @@ class TestCommand extends Command<bool> with ArgUtils {
|
||||
@override
|
||||
final String description = 'Run tests.';
|
||||
|
||||
bool get isWatchMode => boolArg('watch');
|
||||
|
||||
TestTypesRequested testTypesRequested = null;
|
||||
|
||||
/// How many dart2js build tasks are running at the same time.
|
||||
@ -146,25 +155,80 @@ class TestCommand extends Command<bool> with ArgUtils {
|
||||
await macOsInfo.printInformation();
|
||||
}
|
||||
|
||||
switch (testTypesRequested) {
|
||||
case TestTypesRequested.unit:
|
||||
return runUnitTests();
|
||||
case TestTypesRequested.integration:
|
||||
return runIntegrationTests();
|
||||
case TestTypesRequested.all:
|
||||
if (runAllTests && isIntegrationTestsAvailable) {
|
||||
bool unitTestResult = await runUnitTests();
|
||||
bool integrationTestResult = await runIntegrationTests();
|
||||
if (integrationTestResult != unitTestResult) {
|
||||
print('Tests run. Integration tests passed: $integrationTestResult '
|
||||
'unit tests passed: $unitTestResult');
|
||||
final Pipeline testPipeline = Pipeline(steps: <PipelineStep>[
|
||||
() async => clearTerminalScreen(),
|
||||
() => runTestsOfType(testTypesRequested),
|
||||
]);
|
||||
await testPipeline.start();
|
||||
|
||||
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('');
|
||||
PipelineWatcher(
|
||||
dir: dir.absolute,
|
||||
pipeline: testPipeline,
|
||||
ignore: (event) {
|
||||
// Ignore font files that are copied whenever tests run.
|
||||
if (event.path.endsWith('.ttf')) {
|
||||
return true;
|
||||
}
|
||||
return integrationTestResult && unitTestResult;
|
||||
} else {
|
||||
return await runUnitTests();
|
||||
|
||||
// Ignore auto-generated JS files.
|
||||
// The reason we are using `.contains()` instead of `.endsWith()` is
|
||||
// because the auto-generated files could end with any of the
|
||||
// following:
|
||||
//
|
||||
// - browser_test.dart.js
|
||||
// - browser_test.dart.js.map
|
||||
// - browser_test.dart.js.deps
|
||||
if (event.path.contains('browser_test.dart.js')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// React to changes in lib/ and test/ folders.
|
||||
final String relativePath = path.relative(event.path, from: dir.absolute);
|
||||
if (relativePath.startsWith('lib/') || relativePath.startsWith('test/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore anything else.
|
||||
return true;
|
||||
}
|
||||
).start();
|
||||
// Return a never-ending future.
|
||||
return Completer<bool>().future;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> runTestsOfType(TestTypesRequested testTypesRequested) async {
|
||||
try {
|
||||
switch (testTypesRequested) {
|
||||
case TestTypesRequested.unit:
|
||||
return runUnitTests();
|
||||
case TestTypesRequested.integration:
|
||||
return runIntegrationTests();
|
||||
case TestTypesRequested.all:
|
||||
if (runAllTests && isIntegrationTestsAvailable) {
|
||||
bool unitTestResult = await runUnitTests();
|
||||
bool integrationTestResult = await runIntegrationTests();
|
||||
if (integrationTestResult != unitTestResult) {
|
||||
print('Tests run. Integration tests passed: $integrationTestResult '
|
||||
'unit tests passed: $unitTestResult');
|
||||
}
|
||||
return integrationTestResult && unitTestResult;
|
||||
} else {
|
||||
return await runUnitTests();
|
||||
}
|
||||
}
|
||||
throw UnimplementedError('Unknown test type requested: $testTypesRequested');
|
||||
} on TestFailureException {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> runIntegrationTests() async {
|
||||
@ -499,7 +563,12 @@ class TestCommand extends Command<bool> with ArgUtils {
|
||||
|
||||
void _checkExitCode() {
|
||||
if (io.exitCode != 0) {
|
||||
throw ToolException('Process exited with exit code ${io.exitCode}.');
|
||||
if (isWatchMode) {
|
||||
io.exitCode = 0;
|
||||
throw TestFailureException();
|
||||
} else {
|
||||
throw ToolException('Process exited with exit code ${io.exitCode}.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -729,3 +798,5 @@ class TestBuildInput {
|
||||
|
||||
TestBuildInput(this.path, {this.forCanvasKit = false});
|
||||
}
|
||||
|
||||
class TestFailureException implements Exception {}
|
||||
|
||||
@ -14,6 +14,16 @@ import 'package:path/path.dart' as path;
|
||||
import 'environment.dart';
|
||||
import 'exceptions.dart';
|
||||
|
||||
/// 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.
|
||||
void clearTerminalScreen() {
|
||||
if (!io.Platform.isWindows) {
|
||||
// See: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
|
||||
print("\x1B[2J\x1B[1;2H");
|
||||
}
|
||||
}
|
||||
|
||||
class FilePath {
|
||||
FilePath.fromCwd(String relativePath)
|
||||
: _absolutePath = path.absolute(relativePath);
|
||||
|
||||
149
lib/web_ui/dev/watcher.dart
Normal file
149
lib/web_ui/dev/watcher.dart
Normal file
@ -0,0 +1,149 @@
|
||||
// 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.
|
||||
|
||||
// @dart = 2.6
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
enum PipelineStatus {
|
||||
idle,
|
||||
started,
|
||||
stopping,
|
||||
stopped,
|
||||
error,
|
||||
done,
|
||||
}
|
||||
|
||||
typedef PipelineStep = Future<void> Function();
|
||||
|
||||
/// Represents a sequence of asynchronous tasks to be executed.
|
||||
///
|
||||
/// The pipeline can be executed by calling [start] and stopped by calling
|
||||
/// [stop].
|
||||
///
|
||||
/// When a pipeline is stopped, it switches to the [PipelineStatus.stopping]
|
||||
/// state and waits until the current task finishes.
|
||||
class Pipeline {
|
||||
Pipeline({@required this.steps});
|
||||
|
||||
final Iterable<PipelineStep> steps;
|
||||
|
||||
Future<dynamic> _currentStepFuture;
|
||||
|
||||
PipelineStatus status = PipelineStatus.idle;
|
||||
|
||||
/// Starts executing tasks of the pipeline.
|
||||
Future<void> start() async {
|
||||
status = PipelineStatus.started;
|
||||
try {
|
||||
for (PipelineStep step in steps) {
|
||||
if (status != PipelineStatus.started) {
|
||||
break;
|
||||
}
|
||||
_currentStepFuture = step();
|
||||
await _currentStepFuture;
|
||||
}
|
||||
status = PipelineStatus.done;
|
||||
} catch (error, stackTrace) {
|
||||
status = PipelineStatus.error;
|
||||
print('Error in the pipeline: $error');
|
||||
print(stackTrace);
|
||||
} finally {
|
||||
_currentStepFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops executing any more tasks in the pipeline.
|
||||
///
|
||||
/// If a task is already being executed, it won't be interrupted.
|
||||
Future<void> stop() {
|
||||
status = PipelineStatus.stopping;
|
||||
return (_currentStepFuture ?? Future<void>.value(null)).then((_) {
|
||||
status = PipelineStatus.stopped;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature of functions to be called when a [WatchEvent] is received.
|
||||
typedef WatchEventPredicate = bool Function(WatchEvent event);
|
||||
|
||||
/// Responsible for watching a directory [dir] and executing the given
|
||||
/// [pipeline] whenever a change occurs in the directory.
|
||||
///
|
||||
/// The [ignore] callback can be used to customize the watching behavior to
|
||||
/// ignore certain files.
|
||||
class PipelineWatcher {
|
||||
PipelineWatcher({
|
||||
@required this.dir,
|
||||
@required this.pipeline,
|
||||
this.ignore,
|
||||
}) : watcher = DirectoryWatcher(dir);
|
||||
|
||||
/// The path of the directory to watch for changes.
|
||||
final String dir;
|
||||
|
||||
/// The pipeline to be executed when an event is fired by the watcher.
|
||||
final Pipeline pipeline;
|
||||
|
||||
/// Used to watch a directory for any file system changes.
|
||||
final DirectoryWatcher watcher;
|
||||
|
||||
/// A callback that determines whether to rerun the pipeline or not for a
|
||||
/// given [WatchEvent] instance.
|
||||
final WatchEventPredicate ignore;
|
||||
|
||||
/// Activates the watcher.
|
||||
void start() {
|
||||
watcher.events.listen(_onEvent);
|
||||
}
|
||||
|
||||
int _pipelineRunCount = 0;
|
||||
Timer _scheduledPipeline;
|
||||
|
||||
void _onEvent(WatchEvent event) {
|
||||
if (ignore != null && ignore(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String relativePath = path.relative(event.path, from: dir);
|
||||
print('- [${event.type}] ${relativePath}');
|
||||
|
||||
_pipelineRunCount++;
|
||||
_scheduledPipeline?.cancel();
|
||||
_scheduledPipeline = Timer(const Duration(milliseconds: 100), () {
|
||||
_scheduledPipeline = null;
|
||||
_runPipeline();
|
||||
});
|
||||
}
|
||||
|
||||
void _runPipeline() {
|
||||
int runCount;
|
||||
switch (pipeline.status) {
|
||||
case PipelineStatus.started:
|
||||
pipeline.stop().then((_) {
|
||||
runCount = _pipelineRunCount;
|
||||
pipeline.start().then((_) => _pipelineDone(runCount));
|
||||
});
|
||||
break;
|
||||
|
||||
case PipelineStatus.stopping:
|
||||
// We are already trying to stop the pipeline. No need to do anything.
|
||||
break;
|
||||
|
||||
default:
|
||||
runCount = _pipelineRunCount;
|
||||
pipeline.start().then((_) => _pipelineDone(runCount));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _pipelineDone(int pipelineRunCount) {
|
||||
if (pipelineRunCount == _pipelineRunCount) {
|
||||
print('*** Done! ***');
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user