未验证 提交 968c3aa1 编写于 作者: M Mouad Debbar 提交者: GitHub

Refactor and polish the 'felt' tool (#12258)

1. Various functionalities offered by this tool are now organized into commands (e.g. `felt test`, `felt check-licenses`).
2. The felt tool can now be invoked from anywhere, not necessarily from the web_ui directory.
3. This new structure helps us scale better as we add more commands (e.g. soon a `build/watch` command is coming).
上级 2c4ed36c
......@@ -41,7 +41,9 @@ task:
$ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/pub get
cd $ENGINE_PATH/src/flutter/lib/web_ui
$ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/pub get
CHROME_NO_SANDBOX=true $ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/dart dev/felt.dart
export FELT="$ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/dart dev/felt.dart"
$FELT check-licenses
CHROME_NO_SANDBOX=true $FELT test
fetch_framework_script: |
mkdir -p $FRAMEWORK_PATH
cd $FRAMEWORK_PATH
......
......@@ -4,19 +4,30 @@
import 'dart:io' as io;
import 'package:args/args.dart';
import 'package:http/http.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'environment.dart';
void main(List<String> args) async {
Environment.commandLineArguments = args;
try {
await getOrInstallChrome();
} on ChromeInstallerException catch (error) {
io.stderr.writeln(error.toString());
}
void addChromeVersionOption(ArgParser argParser) {
final String pinnedChromeVersion =
io.File(path.join(environment.webUiRootDir.path, 'dev', 'chrome.lock'))
.readAsStringSync()
.trim();
argParser
..addOption(
'chrome-version',
defaultsTo: pinnedChromeVersion,
help: 'The Chrome version to use while running tests. If the requested '
'version has not been installed, it will be downloaded and installed '
'automatically. A specific Chrome build version number, such as 695653 '
'this use that version of Chrome. Value "latest" will use the latest '
'available build of Chrome, installing it if necessary. Value "system" '
'will use the manually installed version of Chrome on this computer.',
);
}
/// Returns the installation of Chrome, installing it if necessary.
......@@ -31,8 +42,10 @@ void main(List<String> args) async {
/// exact build nuber, such as 695653. Build numbers can be found here:
///
/// https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/
Future<ChromeInstallation> getOrInstallChrome({String requestedVersion, StringSink infoLog}) async {
requestedVersion ??= environment.chromeVersion;
Future<ChromeInstallation> getOrInstallChrome(
String requestedVersion, {
StringSink infoLog,
}) async {
infoLog ??= io.stdout;
if (requestedVersion == 'system') {
......
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:io' as io;
import 'package:args/args.dart' as args;
import 'package:path/path.dart' as pathlib;
/// Contains various environment variables, such as common file paths and command-line options.
......@@ -13,51 +12,9 @@ Environment get environment {
}
Environment _environment;
args.ArgParser get _argParser {
return args.ArgParser()
..addMultiOption(
'target',
abbr: 't',
help: 'The path to the target to run. When omitted, runs all targets.',
)
..addMultiOption(
'shard',
abbr: 's',
help: 'The category of tasks to run.',
)
..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.',
)
..addOption(
'chrome-version',
help: 'The Chrome version to use while running tests. If the requested '
'version has not been installed, it will be downloaded and installed '
'automatically. A specific Chrome build version number, such as 695653 '
'this use that version of Chrome. Value "latest" will use the latest '
'available build of Chrome, installing it if necessary. Value "system" '
'will use the manually installed version of Chrome on this computer.',
);
}
/// Contains various environment variables, such as common file paths and command-line options.
class Environment {
/// Command-line arguments.
static List<String> commandLineArguments;
factory Environment() {
if (commandLineArguments == null) {
io.stderr.writeln('Command-line arguments not set.');
io.exit(1);
}
final args.ArgResults options = _argParser.parse(commandLineArguments);
final List<String> shards = options['shard'];
final bool isDebug = options['debug'];
final List<String> targets = options['target'];
final io.File self = io.File.fromUri(io.Platform.script);
final io.Directory engineSrcDir = self.parent.parent.parent.parent.parent;
final io.Directory outDir = io.Directory(pathlib.join(engineSrcDir.path, 'out'));
......@@ -72,9 +29,6 @@ class Environment {
}
}
final String pinnedChromeVersion = io.File(pathlib.join(webUiRootDir.path, 'dev', 'chrome.lock')).readAsStringSync().trim();
final String chromeVersion = options['chrome-version'] ?? pinnedChromeVersion;
return Environment._(
self: self,
webUiRootDir: webUiRootDir,
......@@ -82,10 +36,6 @@ class Environment {
outDir: outDir,
hostDebugUnoptDir: hostDebugUnoptDir,
dartSdkDir: dartSdkDir,
requestedShards: shards,
isDebug: isDebug,
targets: targets,
chromeVersion: chromeVersion,
);
}
......@@ -96,10 +46,6 @@ class Environment {
this.outDir,
this.hostDebugUnoptDir,
this.dartSdkDir,
this.requestedShards,
this.isDebug,
this.targets,
this.chromeVersion,
});
/// The Dart script that's currently running.
......@@ -122,33 +68,6 @@ class Environment {
/// The root of the Dart SDK.
final io.Directory dartSdkDir;
/// Shards specified on the command-line.
final List<String> requestedShards;
/// 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.
final bool isDebug;
/// Paths to targets to run, e.g. a single test.
final List<String> targets;
/// The Chrome version used for testing.
///
/// The value must be one of:
///
/// - "system", which indicates the Chrome installed on the local machine.
/// - "latest", which indicates the latest available Chrome build specified by:
/// https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2FLAST_CHANGE?alt=media
/// - A build number pointing at a pre-built version of Chrome available at:
/// https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/
///
/// The "system" Chrome is assumed to be already properly installed and will be invoked directly.
///
/// The "latest" or a specific build number will be downloaded and cached in [webUiDartToolDir].
final String chromeVersion;
/// The "dart" executable file.
String get dartExecutable => pathlib.join(dartSdkDir.path, 'bin', 'dart');
......
#!/bin/bash
set -e
# felt: a command-line utility for building and testing Flutter web engine.
# It stands for Flutter Engine Local Tester.
......@@ -44,4 +45,4 @@ then
ninja -C $HOST_DEBUG_UNOPT_DIR
fi
(cd $WEB_UI_DIR && $DART_SDK_DIR/bin/dart dev/felt.dart $@)
$DART_SDK_DIR/bin/dart "$DEV_DIR/felt.dart" $@
......@@ -4,119 +4,38 @@
import 'dart:io' as io;
import 'package:path/path.dart' as pathlib;
import 'package:args/command_runner.dart';
import 'environment.dart';
import 'licenses.dart';
import 'test_runner.dart';
// A "shard" is a named subset of tasks this script runs. If not specified,
// it runs all shards. That's what we do on CI.
const Map<String, Function> _kShardNameToCode = <String, Function>{
'licenses': _checkLicenseHeaders,
'tests': runTests,
};
CommandRunner runner = CommandRunner<bool>(
'felt',
'Command-line utility for building and testing Flutter web engine.',
)
..addCommand(LicensesCommand())
..addCommand(TestsCommand());
void main(List<String> args) async {
Environment.commandLineArguments = args;
if (io.Directory.current.absolute.path != environment.webUiRootDir.absolute.path) {
io.stderr.writeln('Current directory is not the root of the web_ui package directory.');
io.stderr.writeln('web_ui directory is: ${environment.webUiRootDir.absolute.path}');
io.stderr.writeln('current directory is: ${io.Directory.current.absolute.path}');
io.exit(1);
if (args.isEmpty) {
// The felt tool was invoked with no arguments. Print usage.
runner.printUsage();
io.exit(64); // Exit code 64 indicates a usage error.
}
_copyAhemFontIntoWebUi();
final List<String> shardsToRun = environment.requestedShards.isNotEmpty
? environment.requestedShards
: _kShardNameToCode.keys.toList();
for (String shard in shardsToRun) {
print('Running shard $shard');
if (!_kShardNameToCode.containsKey(shard)) {
io.stderr.writeln('''
ERROR:
Unsupported test shard: $shard.
Supported test shards: ${_kShardNameToCode.keys.join(', ')}
TESTS FAILED
'''.trim());
try {
final bool result = await runner.run(args);
if (result == false) {
print('Sub-command returned false: `${args.join(' ')}`');
io.exit(1);
}
await _kShardNameToCode[shard]();
} on UsageException catch (e) {
print(e);
io.exit(64); // Exit code 64 indicates a usage error.
} catch (e) {
rethrow;
}
// Sometimes the Dart VM refuses to quit.
io.exit(io.exitCode);
}
void _checkLicenseHeaders() {
final List<io.File> allSourceFiles = _flatListSourceFiles(environment.webUiRootDir);
_expect(allSourceFiles.isNotEmpty, 'Dart source listing of ${environment.webUiRootDir.path} must not be empty.');
final List<String> allDartPaths = allSourceFiles.map((f) => f.path).toList();
for (String expectedDirectory in const <String>['lib', 'test', 'dev', 'tool']) {
final String expectedAbsoluteDirectory = pathlib.join(environment.webUiRootDir.path, expectedDirectory);
_expect(
allDartPaths.where((p) => p.startsWith(expectedAbsoluteDirectory)).isNotEmpty,
'Must include the $expectedDirectory/ directory',
);
}
allSourceFiles.forEach(_expectLicenseHeader);
print('License headers OK!');
}
final _copyRegex = RegExp(r'// Copyright 2013 The Flutter Authors\. All rights reserved\.');
void _expectLicenseHeader(io.File file) {
List<String> head = file.readAsStringSync().split('\n').take(3).toList();
_expect(head.length >= 3, 'File too short: ${file.path}');
_expect(
_copyRegex.firstMatch(head[0]) != null,
'Invalid first line of license header in file ${file.path}',
);
_expect(
head[1] == '// Use of this source code is governed by a BSD-style license that can be',
'Invalid second line of license header in file ${file.path}',
);
_expect(
head[2] == '// found in the LICENSE file.',
'Invalid second line of license header in file ${file.path}',
);
}
void _expect(bool value, String requirement) {
if (!value) {
throw Exception('Test failed: ${requirement}');
}
}
List<io.File> _flatListSourceFiles(io.Directory directory) {
return directory
.listSync(recursive: true)
.whereType<io.File>()
.where((f) {
if (!f.path.endsWith('.dart') && !f.path.endsWith('.js')) {
// Not a source file we're checking.
return false;
}
if (pathlib.isWithin(environment.webUiBuildDir.path, f.path) ||
pathlib.isWithin(environment.webUiDartToolDir.path, f.path)) {
// Generated files.
return false;
}
return true;
})
.toList();
}
void _copyAhemFontIntoWebUi() {
final io.File sourceAhemTtf = io.File(pathlib.join(
environment.flutterDirectory.path, 'third_party', 'txt', 'third_party', 'fonts', 'ahem.ttf'
));
final String destinationAhemTtfPath = pathlib.join(
environment.webUiRootDir.path, 'lib', 'assets', 'ahem.ttf'
);
sourceAhemTtf.copySync(destinationAhemTtfPath);
}
// 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:io' as io;
import 'package:args/command_runner.dart';
import 'package:path/path.dart' as path;
import 'environment.dart';
class LicensesCommand extends Command<bool> {
@override
final String name = 'check-licenses';
@override
final String description = 'Check license headers.';
@override
bool run() {
_checkLicenseHeaders();
return true;
}
void _checkLicenseHeaders() {
final List<io.File> allSourceFiles =
_flatListSourceFiles(environment.webUiRootDir);
_expect(allSourceFiles.isNotEmpty,
'Dart source listing of ${environment.webUiRootDir.path} must not be empty.');
final List<String> allDartPaths =
allSourceFiles.map((f) => f.path).toList();
for (String expectedDirectory in const <String>[
'lib',
'test',
'dev',
'tool'
]) {
final String expectedAbsoluteDirectory =
path.join(environment.webUiRootDir.path, expectedDirectory);
_expect(
allDartPaths
.where((p) => p.startsWith(expectedAbsoluteDirectory))
.isNotEmpty,
'Must include the $expectedDirectory/ directory',
);
}
allSourceFiles.forEach(_expectLicenseHeader);
print('License headers OK!');
}
final _copyRegex =
RegExp(r'// Copyright 2013 The Flutter Authors\. All rights reserved\.');
void _expectLicenseHeader(io.File file) {
List<String> head = file.readAsStringSync().split('\n').take(3).toList();
_expect(head.length >= 3, 'File too short: ${file.path}');
_expect(
_copyRegex.firstMatch(head[0]) != null,
'Invalid first line of license header in file ${file.path}',
);
_expect(
head[1] ==
'// Use of this source code is governed by a BSD-style license that can be',
'Invalid second line of license header in file ${file.path}',
);
_expect(
head[2] == '// found in the LICENSE file.',
'Invalid second line of license header in file ${file.path}',
);
}
void _expect(bool value, String requirement) {
if (!value) {
throw Exception('Test failed: ${requirement}');
}
}
List<io.File> _flatListSourceFiles(io.Directory directory) {
return directory.listSync(recursive: true).whereType<io.File>().where((f) {
if (!f.path.endsWith('.dart') && !f.path.endsWith('.js')) {
// Not a source file we're checking.
return false;
}
if (path.isWithin(environment.webUiBuildDir.path, f.path) ||
path.isWithin(environment.webUiDartToolDir.path, f.path)) {
// Generated files.
return false;
}
return true;
}).toList();
}
}
......@@ -850,12 +850,15 @@ class Chrome extends Browser {
@override
final Future<Uri> remoteDebuggerUrl;
static String version;
/// Starts a new instance of Chrome open to the given [url], which may be a
/// [Uri] or a [String].
factory Chrome(Uri url, {bool debug = false}) {
assert(version != null);
var remoteDebuggerCompleter = Completer<Uri>.sync();
return Chrome._(() async {
final ChromeInstallation installation = await getOrInstallChrome(infoLog: _DevNull());
final ChromeInstallation installation = await getOrInstallChrome(version, infoLog: _DevNull());
final bool isChromeNoSandbox = Platform.environment['CHROME_NO_SANDBOX'] == 'true';
var dir = createTempDir();
......
......@@ -5,32 +5,80 @@
import 'dart:async';
import 'dart:io' as io;
import 'package:args/command_runner.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:test_core/src/runner/hack_register_platform.dart' as hack; // ignore: implementation_imports
import 'package:test_core/src/runner/hack_register_platform.dart'
as hack; // ignore: implementation_imports
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports
import 'package:test_core/src/executable.dart'
as test; // ignore: implementation_imports
import 'chrome_installer.dart';
import 'test_platform.dart';
import 'environment.dart';
import 'utils.dart';
class TestsCommand extends Command<bool> {
TestsCommand() {
argParser
..addMultiOption(
'target',
abbr: 't',
help: 'The path to the target to run. When omitted, runs all targets.',
)
..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.',
);
addChromeVersionOption(argParser);
}
@override
final String name = 'test';
@override
final String description = 'Run tests.';
Future<void> runTests() async {
@override
Future<bool> run() async {
Chrome.version = chromeVersion;
_copyAhemFontIntoWebUi();
await _buildHostPage();
await _buildTests();
if (environment.targets.isEmpty) {
final List<FilePath> targets =
this.targets.map((t) => FilePath.fromCwd(t)).toList();
if (targets.isEmpty) {
await _runAllTests();
} else {
await _runSingleTest(environment.targets);
await _runTargetTests(targets);
}
}
return true;
}
/// 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 => argResults['debug'];
Future<void> _runSingleTest(List<String> targets) async {
/// Paths to targets to run, e.g. a single test.
List<String> get targets => argResults['target'];
/// See [ChromeInstallerCommand.chromeVersion].
String get chromeVersion => argResults['chrome-version'];
Future<void> _runTargetTests(List<FilePath> targets) async {
await _runTestBatch(targets, concurrency: 1, expectFailure: false);
_checkExitCode();
}
}
Future<void> _runAllTests() async {
Future<void> _runAllTests() async {
final io.Directory testDir = io.Directory(path.join(
environment.webUiRootDir.path,
'test',
......@@ -39,21 +87,24 @@ Future<void> _runAllTests() async {
// Separate screenshot tests from unit-tests. Screenshot tests must run
// one at a time. Otherwise, they will end up screenshotting each other.
// This is not an issue for unit-tests.
const String failureSmokeTestPath = 'test/golden_tests/golden_failure_smoke_test.dart';
final List<String> screenshotTestFiles = <String>[];
final List<String> unitTestFiles = <String>[];
final FilePath failureSmokeTestPath = FilePath.fromWebUi(
'test/golden_tests/golden_failure_smoke_test.dart',
);
final List<FilePath> screenshotTestFiles = <FilePath>[];
final List<FilePath> unitTestFiles = <FilePath>[];
for (io.File testFile in testDir.listSync(recursive: true).whereType<io.File>()) {
final String testFilePath = path.relative(testFile.path, from: environment.webUiRootDir.path);
if (!testFilePath.endsWith('_test.dart')) {
for (io.File testFile
in testDir.listSync(recursive: true).whereType<io.File>()) {
final FilePath testFilePath = FilePath.fromCwd(testFile.path);
if (!testFilePath.absolute.endsWith('_test.dart')) {
// Not a test file at all. Skip.
continue;
}
if (testFilePath.endsWith(failureSmokeTestPath)) {
if (testFilePath == failureSmokeTestPath) {
// A smoke test that fails on purpose. Skip.
continue;
}
if (path.split(testFilePath).contains('golden_tests')) {
if (path.split(testFilePath.relativeToWebUi).contains('golden_tests')) {
screenshotTestFiles.add(testFilePath);
} else {
unitTestFiles.add(testFilePath);
......@@ -63,7 +114,7 @@ Future<void> _runAllTests() async {
// This test returns a non-zero exit code on purpose. Run it separately.
if (io.Platform.environment['CIRRUS_CI'] != 'true') {
await _runTestBatch(
<String>[failureSmokeTestPath],
<FilePath>[failureSmokeTestPath],
concurrency: 1,
expectFailure: true,
);
......@@ -75,22 +126,26 @@ Future<void> _runAllTests() async {
_checkExitCode();
// Run screenshot tests one at a time.
for (String testFilePath in screenshotTestFiles) {
await _runTestBatch(<String>[testFilePath], concurrency: 1, expectFailure: false);
for (FilePath testFilePath in screenshotTestFiles) {
await _runTestBatch(
<FilePath>[testFilePath],
concurrency: 1,
expectFailure: false,
);
_checkExitCode();
}
}
}
void _checkExitCode() {
void _checkExitCode() {
if (io.exitCode != 0) {
io.stderr.writeln('Process exited with exit code ${io.exitCode}.');
io.exit(1);
}
}
}
// TODO(yjbanov): skip rebuild if host.dart hasn't changed.
Future<void> _buildHostPage() async {
final io.Process pubRunTest = await io.Process.start(
// TODO(yjbanov): skip rebuild if host.dart hasn't changed.
Future<void> _buildHostPage() async {
final int exitCode = await runProcess(
environment.dart2jsExecutable,
<String>[
'lib/static/host.dart',
......@@ -100,21 +155,16 @@ Future<void> _buildHostPage() async {
workingDirectory: environment.goldenTesterRootDir.path,
);
final StreamSubscription stdoutSub = pubRunTest.stdout.listen(io.stdout.add);
final StreamSubscription stderrSub = pubRunTest.stderr.listen(io.stderr.add);
final int exitCode = await pubRunTest.exitCode;
stdoutSub.cancel();
stderrSub.cancel();
if (exitCode != 0) {
io.stderr.writeln('Failed to compile tests. Compiler exited with exit code $exitCode');
io.stderr.writeln(
'Failed to compile tests. Compiler exited with exit code $exitCode');
io.exit(1);
}
}
}
Future<void> _buildTests() async {
Future<void> _buildTests() async {
// TODO(yjbanov): learn to build only requested tests: https://github.com/flutter/flutter/issues/37810
final io.Process pubRunTest = await io.Process.start(
final int exitCode = await runProcess(
environment.pubExecutable,
<String>[
'run',
......@@ -124,47 +174,43 @@ Future<void> _buildTests() async {
'-o',
'build',
],
workingDirectory: environment.webUiRootDir.path,
);
final StreamSubscription stdoutSub = pubRunTest.stdout.listen(io.stdout.add);
final StreamSubscription stderrSub = pubRunTest.stderr.listen(io.stderr.add);
final int exitCode = await pubRunTest.exitCode;
stdoutSub.cancel();
stderrSub.cancel();
if (exitCode != 0) {
io.stderr.writeln('Failed to compile tests. Compiler exited with exit code $exitCode');
io.stderr.writeln(
'Failed to compile tests. Compiler exited with exit code $exitCode');
io.exit(1);
}
}
}
/// Runs a batch of tests.
///
/// Unless [expectFailure] is set to false, sets [io.exitCode] to a non-zero value if any tests fail.
Future<void> _runTestBatch(
List<String> testFiles, {
/// Runs a batch of tests.
///
/// Unless [expectFailure] is set to false, sets [io.exitCode] to a non-zero value if any tests fail.
Future<void> _runTestBatch(
List<FilePath> testFiles, {
@required int concurrency,
@required bool expectFailure,
}
) async {
}) async {
final List<String> testArgs = <String>[
'--no-color',
...<String>['-r', 'compact'],
'--concurrency=$concurrency',
if (environment.isDebug)
'--pause-after-load',
if (isDebug) '--pause-after-load',
'--platform=chrome',
'--precompiled=${environment.webUiRootDir.path}/build',
'--',
...testFiles,
...testFiles.map((f) => f.relativeToWebUi).toList(),
];
hack.registerPlatformPlugin(
<Runtime>[Runtime.chrome],
() {
hack.registerPlatformPlugin(<Runtime>[Runtime.chrome], () {
return BrowserPlatform.start(root: io.Directory.current.path);
}
);
});
// We want to run tests with `web_ui` as a working directory.
final dynamic backupCwd = io.Directory.current;
io.Directory.current = environment.webUiRootDir.path;
await test.main(testArgs);
io.Directory.current = backupCwd;
if (expectFailure) {
if (io.exitCode != 0) {
......@@ -177,4 +223,18 @@ Future<void> _runTestBatch(
io.exitCode = 1;
}
}
}
}
void _copyAhemFontIntoWebUi() {
final io.File sourceAhemTtf = io.File(path.join(
environment.flutterDirectory.path,
'third_party',
'txt',
'third_party',
'fonts',
'ahem.ttf'));
final String destinationAhemTtfPath =
path.join(environment.webUiRootDir.path, 'lib', 'assets', 'ahem.ttf');
sourceAhemTtf.copySync(destinationAhemTtfPath);
}
// 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:path/path.dart' as path;
import 'environment.dart';
class FilePath {
FilePath.fromCwd(String relativePath)
: _absolutePath = path.absolute(relativePath);
FilePath.fromWebUi(String relativePath)
: _absolutePath = path.join(environment.webUiRootDir.path, relativePath);
final String _absolutePath;
String get absolute => _absolutePath;
String get relativeToCwd => path.relative(_absolutePath);
String get relativeToWebUi =>
path.relative(_absolutePath, from: environment.webUiRootDir.path);
@override
bool operator ==(dynamic other) {
return other is FilePath && _absolutePath == other._absolutePath;
}
@override
String toString() => _absolutePath;
}
Future<int> runProcess(
String executable,
List<String> arguments, {
String workingDirectory,
}) async {
final io.Process process = await io.Process.start(
executable,
arguments,
workingDirectory: workingDirectory,
);
return _forwardIOAndWait(process);
}
Future<int> _forwardIOAndWait(io.Process process) {
final StreamSubscription stdoutSub = process.stdout.listen(io.stdout.add);
final StreamSubscription stderrSub = process.stderr.listen(io.stderr.add);
return process.exitCode.then<int>((int exitCode) {
stdoutSub.cancel();
stderrSub.cancel();
return exitCode;
});
}
......@@ -86,6 +86,7 @@ void commitScene(PersistedScene scene) {
_debugPrintSurfaceStats(scene, _debugFrameNumber);
_debugRepaintSurfaceStatsOverlay(scene);
}
assert(() {
final List<String> validationErrors = <String>[];
scene.debugValidate(validationErrors);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册