Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion lib/web_ui/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:

```
Expand Down
126 changes: 2 additions & 124 deletions lib/web_ui/dev/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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();

Expand Down Expand Up @@ -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! ***');
}
}
}
105 changes: 88 additions & 17 deletions lib/web_ui/dev/test_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}

// 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;
}
return integrationTestResult && unitTestResult;
} else {
return await runUnitTests();

// 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 {
Expand Down Expand Up @@ -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}.');
}
}
}

Expand Down Expand Up @@ -729,3 +798,5 @@ class TestBuildInput {

TestBuildInput(this.path, {this.forCanvasKit = false});
}

class TestFailureException implements Exception {}
10 changes: 10 additions & 0 deletions lib/web_ui/dev/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading