Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 2331297

Browse files
committed
[web] Add --watch flag to 'felt test'
1 parent 3073402 commit 2331297

File tree

5 files changed

+244
-142
lines changed

5 files changed

+244
-142
lines changed

lib/web_ui/dev/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
`felt` supports multiple commands as follows:
88

99
1. **`felt check-licenses`**: Checks that all Dart and JS source code files contain the correct license headers.
10-
2. **`felt test`**: Runs all or some tests depending on the passed arguments.
10+
2. **`felt test`**: Runs all or some tests depending on the passed arguments. It supports a watch mode for convenience.
1111
3. **`felt build`**: Builds the engine locally so it can be used by Flutter apps. It also supports a watch mode for more convenience.
1212

1313
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
4343
felt test
4444
```
4545

46+
To run a specific test:
47+
48+
```
49+
felt test test/engine/util_test.dart
50+
```
51+
52+
To enable watch mode so that the test re-runs on every change:
53+
54+
```
55+
felt test --watch test/engine/util_test.dart
56+
```
57+
4658
To run unit tests only:
4759

4860
```

lib/web_ui/dev/build.dart

Lines changed: 2 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
import 'dart:async';
77

88
import 'package:args/command_runner.dart';
9-
import 'package:meta/meta.dart';
109
import 'package:path/path.dart' as path;
11-
import 'package:watcher/watcher.dart';
1210

1311
import 'environment.dart';
1412
import 'utils.dart';
13+
import 'watcher.dart';
1514

1615
class BuildCommand extends Command<bool> with ArgUtils {
1716
BuildCommand() {
@@ -37,7 +36,7 @@ class BuildCommand extends Command<bool> with ArgUtils {
3736
final FilePath libPath = FilePath.fromWebUi('lib');
3837
final Pipeline buildPipeline = Pipeline(steps: <PipelineStep>[
3938
gn,
40-
() => ninja(),
39+
ninja,
4140
]);
4241
await buildPipeline.start();
4342

@@ -77,124 +76,3 @@ Future<void> ninja() {
7776
environment.hostDebugUnoptDir.path,
7877
]);
7978
}
80-
81-
enum PipelineStatus {
82-
idle,
83-
started,
84-
stopping,
85-
stopped,
86-
error,
87-
done,
88-
}
89-
90-
typedef PipelineStep = Future<void> Function();
91-
92-
class Pipeline {
93-
Pipeline({@required this.steps});
94-
95-
final Iterable<PipelineStep> steps;
96-
97-
Future<dynamic> _currentStepFuture;
98-
99-
PipelineStatus status = PipelineStatus.idle;
100-
101-
Future<void> start() async {
102-
status = PipelineStatus.started;
103-
try {
104-
for (PipelineStep step in steps) {
105-
if (status != PipelineStatus.started) {
106-
break;
107-
}
108-
_currentStepFuture = step();
109-
await _currentStepFuture;
110-
}
111-
status = PipelineStatus.done;
112-
} catch (error, stackTrace) {
113-
status = PipelineStatus.error;
114-
print('Error in the pipeline: $error');
115-
print(stackTrace);
116-
} finally {
117-
_currentStepFuture = null;
118-
}
119-
}
120-
121-
Future<void> stop() {
122-
status = PipelineStatus.stopping;
123-
return (_currentStepFuture ?? Future<void>.value(null)).then((_) {
124-
status = PipelineStatus.stopped;
125-
});
126-
}
127-
}
128-
129-
typedef WatchEventPredicate = bool Function(WatchEvent event);
130-
131-
class PipelineWatcher {
132-
PipelineWatcher({
133-
@required this.dir,
134-
@required this.pipeline,
135-
this.ignore,
136-
}) : watcher = DirectoryWatcher(dir);
137-
138-
/// The path of the directory to watch for changes.
139-
final String dir;
140-
141-
/// The pipeline to be executed when an event is fired by the watcher.
142-
final Pipeline pipeline;
143-
144-
/// Used to watch a directory for any file system changes.
145-
final DirectoryWatcher watcher;
146-
147-
/// A callback that determines whether to rerun the pipeline or not for a
148-
/// given [WatchEvent] instance.
149-
final WatchEventPredicate ignore;
150-
151-
void start() {
152-
watcher.events.listen(_onEvent);
153-
}
154-
155-
int _pipelineRunCount = 0;
156-
Timer _scheduledPipeline;
157-
158-
void _onEvent(WatchEvent event) {
159-
if (ignore != null && ignore(event)) {
160-
return;
161-
}
162-
163-
final String relativePath = path.relative(event.path, from: dir);
164-
print('- [${event.type}] ${relativePath}');
165-
166-
_pipelineRunCount++;
167-
_scheduledPipeline?.cancel();
168-
_scheduledPipeline = Timer(const Duration(milliseconds: 100), () {
169-
_scheduledPipeline = null;
170-
_runPipeline();
171-
});
172-
}
173-
174-
void _runPipeline() {
175-
int runCount;
176-
switch (pipeline.status) {
177-
case PipelineStatus.started:
178-
pipeline.stop().then((_) {
179-
runCount = _pipelineRunCount;
180-
pipeline.start().then((_) => _pipelineDone(runCount));
181-
});
182-
break;
183-
184-
case PipelineStatus.stopping:
185-
// We are already trying to stop the pipeline. No need to do anything.
186-
break;
187-
188-
default:
189-
runCount = _pipelineRunCount;
190-
pipeline.start().then((_) => _pipelineDone(runCount));
191-
break;
192-
}
193-
}
194-
195-
void _pipelineDone(int pipelineRunCount) {
196-
if (pipelineRunCount == _pipelineRunCount) {
197-
print('*** Done! ***');
198-
}
199-
}
200-
}

lib/web_ui/dev/test_runner.dart

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import 'safari_installation.dart';
2626
import 'supported_browsers.dart';
2727
import 'test_platform.dart';
2828
import 'utils.dart';
29+
import 'watcher.dart';
2930

3031
/// The type of tests requested by the tool user.
3132
enum TestTypesRequested {
@@ -48,6 +49,12 @@ class TestCommand extends Command<bool> with ArgUtils {
4849
'opportunity to add breakpoints or inspect loaded code before '
4950
'running the code.',
5051
)
52+
..addFlag(
53+
'watch',
54+
abbr: 'w',
55+
help: 'Run in watch mode so the tests re-run whenever a change is '
56+
'made.',
57+
)
5158
..addFlag(
5259
'unit-tests-only',
5360
defaultsTo: false,
@@ -100,6 +107,8 @@ class TestCommand extends Command<bool> with ArgUtils {
100107
@override
101108
final String description = 'Run tests.';
102109

110+
bool get isWatchMode => boolArg('watch');
111+
103112
TestTypesRequested testTypesRequested = null;
104113

105114
/// How many dart2js build tasks are running at the same time.
@@ -146,25 +155,80 @@ class TestCommand extends Command<bool> with ArgUtils {
146155
await macOsInfo.printInformation();
147156
}
148157

149-
switch (testTypesRequested) {
150-
case TestTypesRequested.unit:
151-
return runUnitTests();
152-
case TestTypesRequested.integration:
153-
return runIntegrationTests();
154-
case TestTypesRequested.all:
155-
if (runAllTests && isIntegrationTestsAvailable) {
156-
bool unitTestResult = await runUnitTests();
157-
bool integrationTestResult = await runIntegrationTests();
158-
if (integrationTestResult != unitTestResult) {
159-
print('Tests run. Integration tests passed: $integrationTestResult '
160-
'unit tests passed: $unitTestResult');
158+
final Pipeline testPipeline = Pipeline(steps: <PipelineStep>[
159+
() async => clearTerminalScreen(),
160+
() => runTestsOfType(testTypesRequested),
161+
]);
162+
await testPipeline.start();
163+
164+
if (isWatchMode) {
165+
final FilePath dir = FilePath.fromWebUi('');
166+
print('');
167+
print('Initial test run is done!');
168+
print('Watching ${dir.relativeToCwd}/lib and ${dir.relativeToCwd}/test to re-run tests');
169+
print('');
170+
PipelineWatcher(
171+
dir: dir.absolute,
172+
pipeline: testPipeline,
173+
ignore: (event) {
174+
// Ignore font files that are copied whenever tests run.
175+
if (event.path.endsWith('.ttf')) {
176+
return true;
177+
}
178+
179+
// Ignore auto-generated JS files.
180+
// The reason we are using `.contains()` instead of `.endsWith()` is
181+
// because the auto-generated files could end with any of the
182+
// following:
183+
//
184+
// - browser_test.dart.js
185+
// - browser_test.dart.js.map
186+
// - browser_test.dart.js.deps
187+
if (event.path.contains('browser_test.dart.js')) {
188+
return true;
161189
}
162-
return integrationTestResult && unitTestResult;
163-
} else {
164-
return await runUnitTests();
190+
191+
// React to changes in lib/ and test/ folders.
192+
final String relativePath = path.relative(event.path, from: dir.absolute);
193+
if (relativePath.startsWith('lib/') || relativePath.startsWith('test/')) {
194+
return false;
195+
}
196+
197+
// Ignore anything else.
198+
return true;
165199
}
200+
).start();
201+
// Return a never-ending future.
202+
return Completer<bool>().future;
203+
} else {
204+
return true;
205+
}
206+
}
207+
208+
Future<bool> runTestsOfType(TestTypesRequested testTypesRequested) async {
209+
try {
210+
switch (testTypesRequested) {
211+
case TestTypesRequested.unit:
212+
return runUnitTests();
213+
case TestTypesRequested.integration:
214+
return runIntegrationTests();
215+
case TestTypesRequested.all:
216+
if (runAllTests && isIntegrationTestsAvailable) {
217+
bool unitTestResult = await runUnitTests();
218+
bool integrationTestResult = await runIntegrationTests();
219+
if (integrationTestResult != unitTestResult) {
220+
print('Tests run. Integration tests passed: $integrationTestResult '
221+
'unit tests passed: $unitTestResult');
222+
}
223+
return integrationTestResult && unitTestResult;
224+
} else {
225+
return await runUnitTests();
226+
}
227+
}
228+
throw UnimplementedError('Unknown test type requested: $testTypesRequested');
229+
} on TestFailureException {
230+
return true;
166231
}
167-
return false;
168232
}
169233

170234
Future<bool> runIntegrationTests() async {
@@ -499,7 +563,12 @@ class TestCommand extends Command<bool> with ArgUtils {
499563

500564
void _checkExitCode() {
501565
if (io.exitCode != 0) {
502-
throw ToolException('Process exited with exit code ${io.exitCode}.');
566+
if (isWatchMode) {
567+
io.exitCode = 0;
568+
throw TestFailureException();
569+
} else {
570+
throw ToolException('Process exited with exit code ${io.exitCode}.');
571+
}
503572
}
504573
}
505574

@@ -729,3 +798,5 @@ class TestBuildInput {
729798

730799
TestBuildInput(this.path, {this.forCanvasKit = false});
731800
}
801+
802+
class TestFailureException implements Exception {}

lib/web_ui/dev/utils.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ import 'package:path/path.dart' as path;
1414
import 'environment.dart';
1515
import 'exceptions.dart';
1616

17+
/// Clears the terminal screen and places the cursor at the top left corner.
18+
///
19+
/// This works on Linux and Mac. On Windows, it's a no-op.
20+
void clearTerminalScreen() {
21+
if (!io.Platform.isWindows) {
22+
// See: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
23+
print("\x1B[2J\x1B[1;2H");
24+
}
25+
}
26+
1727
class FilePath {
1828
FilePath.fromCwd(String relativePath)
1929
: _absolutePath = path.absolute(relativePath);

0 commit comments

Comments
 (0)