Web test reorganization (flutter/engine#39984)

Web test reorganization
This commit is contained in:
Jackson Gardner
2023-03-27 17:08:48 -07:00
committed by GitHub
parent b9fe5d10f1
commit 82886d52b3
78 changed files with 4101 additions and 1429 deletions

View File

@@ -292,31 +292,6 @@ targets:
release_build: "true"
config_name: linux_web_engine
- name: Linux Web Engine
recipe: engine/web_engine
properties:
add_recipes_cq: "true"
cores: "32"
gcs_goldens_bucket: flutter_logs
gclient_variables: >-
{"download_emsdk": true}
dependencies: >-
[
{"dependency": "chrome_and_driver", "version": "version:111.0"},
{"dependency": "firefox", "version": "version:106.0"},
{"dependency": "goldctl", "version": "git_revision:3a77d0b12c697a840ca0c7705208e8622dc94603"}
]
no_goma: "true"
timeout: 60
runIf:
- DEPS
- .ci.yaml
- lib/web_ui/**
- web_sdk/**
- tools/**
- ci/**
- flutter_frontend_server/**
- name: Linux Web Framework tests
recipe: engine/web_engine_framework
enabled_branches:
@@ -448,28 +423,6 @@ targets:
ios_debug: "true"
timeout: 60
- name: Mac Web Engine
recipe: engine/web_engine
properties:
add_recipes_cq: "true"
gcs_goldens_bucket: flutter_logs
gclient_variables: >-
{"download_emsdk": true}
dependencies: >-
[
{"dependency": "goldctl", "version": "git_revision:3a77d0b12c697a840ca0c7705208e8622dc94603"}
]
no_goma: "true"
timeout: 60
runIf:
- DEPS
- .ci.yaml
- lib/web_ui/**
- web_sdk/**
- tools/**
- ci/**
- flutter_frontend_server/**
- name: Mac mac_ios_engine
recipe: engine_v2/engine_v2
timeout: 60
@@ -530,24 +483,6 @@ targets:
add_recipes_cq: "true"
timeout: 75
- name: Windows Web Engine
recipe: engine/web_engine
properties:
gclient_variables: >-
{"download_emsdk": true}
gcs_goldens_bucket: flutter_logs
dependencies: >-
[
{"dependency": "chrome_and_driver", "version": "version:111.0"}
]
no_goma: "true"
timeout: 60
runIf:
- DEPS
- .ci.yaml
- lib/web_ui/**
- web_sdk/**
- name: Mac iOS Engine Profile
recipe: engine/engine
properties:

File diff suppressed because it is too large Load Diff

View File

@@ -22,75 +22,92 @@ To tell `felt` to do anything you call `felt SUBCOMMAND`, where `SUBCOMMAND` is
one of the available subcommands, which can be listed by running `felt help`. To
get help for a specific subcommand, run `felt help SUBCOMMAND`.
The most useful subcommands are:
- `felt build` - builds a local Flutter Web engine ready to be used by the
Flutter framework. To use a locally built web sdk, build with `felt build`,
then pass `--local-web-sdk=wasm_release` to the `flutter` command, or to
`dev/bots/test.dart` when running a web shard, such as `web_tests`.
- `felt test` - runs web engine tests. By default, this runs all tests using
Chromium. Passing one or more paths to specific tests would run just the
specified tests. Run `felt help test` for more options.
`build` and `test` take the `--watch` option, which automatically reruns the
subcommand when a source file changes. This is handy when you are iterating
quickly.
#### Examples
Builds the web engine, the runs a Flutter app using it:
#### `felt build`
The `build` subcommand builds web engine gn/ninja targets. Targets can be
individually specified in the command line invocation, or if none are specified,
all web engine targets are built. Common targets are as follows:
* `sdk` - The flutter_web_sdk itself.
* `canvaskit` - Flutter's version of canvakit.
* `canvaskit_chromium` - A version of canvaskit optimized for use with
chromium-based browsers.
* `skwasm` - Builds experimental skia wasm module renderer.
The output of these steps is used in unit tests, and can be used with the flutter
command via the `--local-web-sdk=wasm_release` command.
##### Examples
Builds all web engine targets, then runs a Flutter app using it:
```
felt build
cd path/to/some/app
flutter --local-web-sdk=wasm_release run -d chrome
```
Runs all tests in Chromium:
Builds only the `sdk` and the `canvaskit` targets:
```
felt build sdk canvaskit
```
#### `felt test`
The `test` subcommand will compile and/or run web engine unit test suites. For
information on how test suites are structured, see the test configuration
[readme][2].
By default, `felt test` compiles and runs all suites that are compatible with the
host system. Some useful flags supported by this command:
* `--compile` will only perform compilation of these suites without running them.
* `--run` will only run the tests and not compile them, and assume they have been
compiled in a previous run of the tool.
* `--list` will list all the test suites and test bundles and exit without
compiling or running anything.
* `--verbose` will output some extra information that may be useful for debugging.
* `--debug` will open a browser window and pause the tests before starting so that
breakpoints can be set before starting the test suites.
Several other flags can be passed that filter which test suites should be run:
* `--browser` runs only the test suites that test on the browsers passed. Valid
values for this are `chrome`, `firefox`, `safari`, or `edge`.
* `--compiler` runs only the test suites that use a particular compiler. Valid
values for this are `dart2js` or `dart2wasm`
* `--renderer` runs only the test suites that use a particular renderer. Valid
values for this are `html`, `canvakit`, or `skwasm`
* `--suite` runs a suite by name.
* `--bundle` runs suites that target a particular test bundle.
Filters of different types are logically ANDed together, but multiple filter flags
of the same type are logically ORed together.
The `test` command will also accept a list of paths to specific test files to be
compiled and run. If none of these paths are specified, all tests are run, otherwise
only the tests that are specified will run.
##### Examples
Runs all test suites in all compatible browsers:
```
felt test
```
Runs a specific test:
Runs a specific test on all compatible browsers:
```
felt test test/engine/util_test.dart
```
Runs multiple specific tests:
Runs multiple specific tests on all compatible browsers:
```
felt test test/engine/util_test.dart test/alarm_clock_test.dart
felt test test/engine/util_test.dart test/engine/alarm_clock_test.dart
```
Enable watch mode so that the test re-runs every time a source file changes:
Runs only test suites that compile via dart2wasm:
```
felt test --watch test/engine/util_test.dart
felt test --compiler dart2wasm
```
Runs tests in Firefox (requires a Linux computer):
Runs only test suites that run in Chrome and Safari:
```
felt test --browser=firefox
```
Chromium and Firefox support debugging tests using the browser's developer
tools. To run tests in debug mode add `--debug` to the `test` command, e.g.:
```
felt test --debug --browser=firefox test/alarm_clock_test.dart
felt test --browser chrome --browser safari
```
### Optimizing local builds
Concurrency of various build steps can be configured via environment variables:
- `FELT_DART2JS_CONCURRENCY` specifies the number of concurrent `dart2js`
- `FELT_COMPILE_CONCURRENCY` specifies the number of concurrent compiler
processes used to compile tests. Default value is 8.
- `FELT_TEST_CONCURRENCY` specifies the number of tests run concurrently.
Default value is 10.
If you are a Google employee, you can use an internal instance of Goma (go/ma)
to parallelize your ninja builds. Because Goma compiles code on remote servers,
@@ -99,7 +116,7 @@ this option is particularly effective for building on low-powered laptops.
### Test browsers
Chromium, Firefox, and Safari for iOS are version-locked using the
[browser_lock.yaml][2] configuration file. Safari for macOS is supplied by the
[browser_lock.yaml][3] configuration file. Safari for macOS is supplied by the
computer's operating system. Tests can be run in Edge locally, but Edge is not
enabled on LUCI. Chromium is used as a proxy for Chrome, Edge, and other
Chromium-based browsers.
@@ -273,7 +290,8 @@ Once you know the version for the Emscripten SDK, change the line in
[1]: https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment
[2]: https://github.com/flutter/engine/blob/main/lib/web_ui/dev/browser_lock.yaml
[2]: https://github.com/flutter/flutter/blob/main/lib/web_ui/test/README
[3]: https://github.com/flutter/engine/blob/main/lib/web_ui/dev/browser_lock.yaml
[4]: https://chrome-infra-packages.appspot.com/p/flutter_internal
[5]: https://cs.opensource.google/flutter/recipes/+/master:recipes/engine/web_engine.py
[6]: https://chromium.googlesource.com/chromium/src.git/+/main/docs/cipd_and_3pp.md#What-is-CIPD

View File

@@ -11,6 +11,7 @@ import 'browser_lock.dart';
import 'chrome.dart';
import 'edge.dart';
import 'environment.dart';
import 'felt_config.dart';
import 'firefox.dart';
import 'safari_macos.dart';
@@ -261,16 +262,15 @@ const List<String> kAllBrowserNames = <String>[
/// Creates an environment for a browser.
///
/// The [browserName] matches the browser name passed as the `--browser` option.
BrowserEnvironment getBrowserEnvironment(String browserName, { required bool enableWasmGC }) {
BrowserEnvironment getBrowserEnvironment(BrowserName browserName, { required bool enableWasmGC }) {
switch (browserName) {
case kChrome:
case BrowserName.chrome:
return ChromeEnvironment(enableWasmGC);
case kEdge:
case BrowserName.edge:
return EdgeEnvironment();
case kFirefox:
case BrowserName.firefox:
return FirefoxEnvironment();
case kSafari:
case BrowserName.safari:
return SafariMacOsEnvironment();
}
throw UnsupportedError('Browser $browserName is not supported.');
}

View File

@@ -148,12 +148,17 @@ class Environment {
/// Path to the "build" directory, generated by "package:build_runner".
///
/// This is where compiled output goes.
/// This is where compiled test output goes.
io.Directory get webUiBuildDir => io.Directory(pathlib.join(
outDir.path,
'web_tests',
));
io.Directory get webTestsArtifactsDir => io.Directory(pathlib.join(
webUiBuildDir.path,
'artifacts',
));
/// Path to the ".dart_tool" directory, generated by various Dart tools.
io.Directory get webUiDartToolDir => io.Directory(pathlib.join(
webUiRootDir.path,

View File

@@ -12,7 +12,6 @@ import 'clean.dart';
import 'exceptions.dart';
import 'generate_fallback_font_data.dart';
import 'licenses.dart';
import 'run.dart';
import 'test_runner.dart';
import 'utils.dart';
@@ -25,7 +24,6 @@ CommandRunner<bool> runner = CommandRunner<bool>(
..addCommand(CleanCommand())
..addCommand(GenerateFallbackFontDataCommand())
..addCommand(LicensesCommand())
..addCommand(RunCommand())
..addCommand(TestCommand());
Future<void> main(List<String> rawArgs) async {

View File

@@ -0,0 +1,248 @@
// 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:yaml/yaml.dart';
enum Compiler {
dart2js,
dart2wasm
}
enum Renderer {
html,
canvaskit,
skwasm,
}
class CompileConfiguration {
CompileConfiguration(this.name, this.compiler, this.renderer);
final String name;
final Compiler compiler;
final Renderer renderer;
}
class TestSet {
TestSet(this.name, this.directory);
final String name;
final String directory;
}
class TestBundle {
TestBundle(this.name, this.testSet, this.compileConfig);
final String name;
final TestSet testSet;
final CompileConfiguration compileConfig;
}
enum CanvasKitVariant {
full,
chromium,
}
enum BrowserName {
chrome,
edge,
firefox,
safari,
}
class RunConfiguration {
RunConfiguration(this.name, this.browser, this.variant);
final String name;
final BrowserName browser;
final CanvasKitVariant? variant;
}
class ArtifactDependencies {
ArtifactDependencies({
required this.canvasKit,
required this.canvasKitChromium,
required this.skwasm
});
ArtifactDependencies.none() :
canvasKit = false,
canvasKitChromium = false,
skwasm = false;
final bool canvasKit;
final bool canvasKitChromium;
final bool skwasm;
ArtifactDependencies operator|(ArtifactDependencies other) {
return ArtifactDependencies(
canvasKit: canvasKit || other.canvasKit,
canvasKitChromium: canvasKitChromium || other.canvasKitChromium,
skwasm: skwasm || other.skwasm,
);
}
ArtifactDependencies operator&(ArtifactDependencies other) {
return ArtifactDependencies(
canvasKit: canvasKit && other.canvasKit,
canvasKitChromium: canvasKitChromium && other.canvasKitChromium,
skwasm: skwasm && other.skwasm,
);
}
}
class TestSuite {
TestSuite(
this.name,
this.testBundle,
this.runConfig,
this.artifactDependencies
);
String name;
TestBundle testBundle;
RunConfiguration runConfig;
ArtifactDependencies artifactDependencies;
}
class FeltConfig {
FeltConfig(
this.compileConfigs,
this.testSets,
this.testBundles,
this.runConfigs,
this.testSuites,
);
factory FeltConfig.fromFile(String filePath) {
final io.File configFile = io.File(filePath);
final YamlMap yaml = loadYaml(configFile.readAsStringSync()) as YamlMap;
final List<CompileConfiguration> compileConfigs = <CompileConfiguration>[];
final Map<String, CompileConfiguration> compileConfigsByName = <String, CompileConfiguration>{};
for (final dynamic node in yaml['compile-configs'] as YamlList) {
final YamlMap configYaml = node as YamlMap;
final String name = configYaml['name'] as String;
final Compiler compiler = Compiler.values.byName(configYaml['compiler'] as String);
final Renderer renderer = Renderer.values.byName(configYaml['renderer'] as String);
final CompileConfiguration config = CompileConfiguration(name, compiler, renderer);
compileConfigs.add(config);
if (compileConfigsByName.containsKey(name)) {
throw AssertionError('Duplicate compile config name: $name');
}
compileConfigsByName[name] = config;
}
final List<TestSet> testSets = <TestSet>[];
final Map<String, TestSet> testSetsByName = <String, TestSet>{};
for (final dynamic node in yaml['test-sets'] as YamlList) {
final YamlMap testSetYaml = node as YamlMap;
final String name = testSetYaml['name'] as String;
final String directory = testSetYaml['directory'] as String;
final TestSet testSet = TestSet(name, directory);
testSets.add(testSet);
if (testSetsByName.containsKey(name)) {
throw AssertionError('Duplicate test set name: $name');
}
testSetsByName[name] = testSet;
}
final List<TestBundle> testBundles = <TestBundle>[];
final Map<String, TestBundle> testBundlesByName = <String, TestBundle>{};
for (final dynamic node in yaml['test-bundles'] as YamlList) {
final YamlMap testBundleYaml = node as YamlMap;
final String name = testBundleYaml['name'] as String;
final String testSetName = testBundleYaml['test-set'] as String;
final TestSet? testSet = testSetsByName[testSetName];
if (testSet == null) {
throw AssertionError('Test set not found with name: `$testSetName` (referenced by test bundle: `$name`)');
}
final String compileConfigName = testBundleYaml['compile-config'] as String;
final CompileConfiguration? compileConfig = compileConfigsByName[compileConfigName];
if (compileConfig == null) {
throw AssertionError('Compile config not found with name: `$compileConfigName` (referenced by test bundle: `$name`)');
}
final TestBundle bundle = TestBundle(name, testSet, compileConfig);
testBundles.add(bundle);
if (testBundlesByName.containsKey(name)) {
throw AssertionError('Duplicate test bundle name: $name');
}
testBundlesByName[name] = bundle;
}
final List<RunConfiguration> runConfigs = <RunConfiguration>[];
final Map<String, RunConfiguration> runConfigsByName = <String, RunConfiguration>{};
for (final dynamic node in yaml['run-configs'] as YamlList) {
final YamlMap runConfigYaml = node as YamlMap;
final String name = runConfigYaml['name'] as String;
final BrowserName browser = BrowserName.values.byName(runConfigYaml['browser'] as String);
final dynamic variantNode = runConfigYaml['canvaskit-variant'];
final CanvasKitVariant? variant = variantNode == null
? null
: CanvasKitVariant.values.byName(variantNode as String);
final RunConfiguration runConfig = RunConfiguration(name, browser, variant);
runConfigs.add(runConfig);
if (runConfigsByName.containsKey(name)) {
throw AssertionError('Duplicate run config name: $name');
}
runConfigsByName[name] = runConfig;
}
final List<TestSuite> testSuites = <TestSuite>[];
for (final dynamic node in yaml['test-suites'] as YamlList) {
final YamlMap testSuiteYaml = node as YamlMap;
final String name = testSuiteYaml['name'] as String;
final String testBundleName = testSuiteYaml['test-bundle'] as String;
final TestBundle? bundle = testBundlesByName[testBundleName];
if (bundle == null) {
throw AssertionError('Test bundle not found with name: `$testBundleName` (referenced by test suite: `$name`)');
}
final String runConfigName = testSuiteYaml['run-config'] as String;
final RunConfiguration? runConfig = runConfigsByName[runConfigName];
if (runConfig == null) {
throw AssertionError('Run config not found with name: `$runConfigName` (referenced by test suite: `$name`)');
}
bool canvasKit = false;
bool canvasKitChromium = false;
bool skwasm = false;
final dynamic depsNode = testSuiteYaml['artifact-deps'];
if (depsNode != null) {
for (final dynamic dep in depsNode as YamlList) {
switch (dep as String) {
case 'canvaskit':
if (canvasKit) {
throw AssertionError('Artifact dep $dep listed twice in suite $name.');
}
canvasKit = true;
case 'canvaskit_chromium':
if (canvasKitChromium) {
throw AssertionError('Artifact dep $dep listed twice in suite $name.');
}
canvasKitChromium = true;
case 'skwasm':
if (skwasm) {
throw AssertionError('Artifact dep $dep listed twice in suite $name.');
}
skwasm = true;
default:
throw AssertionError('Unrecognized artifact dependency: $dep');
}
}
}
final ArtifactDependencies artifactDeps = ArtifactDependencies(
canvasKit: canvasKit,
canvasKitChromium: canvasKitChromium,
skwasm: skwasm
);
final TestSuite suite = TestSuite(name, bundle, runConfig, artifactDeps);
testSuites.add(suite);
}
return FeltConfig(compileConfigs, testSets, testBundles, runConfigs, testSuites);
}
List<CompileConfiguration> compileConfigs;
List<TestSet> testSets;
List<TestBundle> testBundles;
List<RunConfiguration> runConfigs;
List<TestSuite> testSuites;
}

View File

@@ -0,0 +1,173 @@
// 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:convert';
import 'felt_config.dart';
String generateBuilderJson(FeltConfig config) {
final Map<String, dynamic> outputJson = <String, dynamic>{
'builds': <dynamic>[
_getArtifactBuildStep(),
for (final TestBundle bundle in config.testBundles)
_getBundleBuildStep(bundle),
],
'tests': _getAllTestSteps(config.testSuites)
};
return const JsonEncoder.withIndent(' ').convert(outputJson);
}
Map<String, dynamic> _getArtifactBuildStep() {
return <String, dynamic>{
'name': 'web_tests/artifacts',
'drone_dimensions': <String>[
'device_type=none',
'os=Linux',
'cores=32'
],
'gclient_variables': <String, dynamic>{
'download_android_deps': false,
'download_emsdk': true,
},
'gn': <String>[
'--web',
'--runtime-mode=release',
'--no-goma',
],
'ninja': <String, dynamic>{
'config': 'wasm_release',
'targets': <String>[
'flutter/web_sdk:flutter_web_sdk_archive'
]
},
'archives': <dynamic>[
<String, dynamic>{
'name': 'wasm_release',
'base_path': 'out/wasm_release/zip_archives/',
'type': 'gcs',
'include_paths': <String>[
'out/wasm_release/zip_archives/flutter-web-sdk.zip'
],
'realm': 'production',
}
],
'generators': <String, dynamic>{
'tasks': <dynamic>[
<String, dynamic>{
'name': 'check licenses',
'parameters': <String>[
'check-licenses'
],
'scripts': <String>[ 'flutter/lib/web_ui/dev/felt' ],
},
<String, dynamic>{
'name': 'web engine analysis',
'parameters': <String>[
'analyze'
],
'scripts': <String>[ 'flutter/lib/web_ui/dev/felt' ],
},
<String, dynamic>{
'name': 'copy artifacts for web tests',
'parameters': <String>[
'test',
'--copy-artifacts',
],
'scripts': <String>[ 'flutter/lib/web_ui/dev/felt' ],
},
]
},
};
}
Map<String, dynamic> _getBundleBuildStep(TestBundle bundle) {
return <String, dynamic>{
'name': 'web_tests/test_bundles/${bundle.name}',
'drone_dimensions': <String>[
'device_type=none',
'os=Linux',
'cores=32',
],
'generators': <String, dynamic>{
'tasks': <dynamic>[
<String, dynamic>{
'name': 'compile bundle ${bundle.name}',
'parameters': <String>[
'test',
'--compile',
'--bundle=${bundle.name}',
],
'scripts': <String>[ 'flutter/lib/web_ui/dev/felt' ],
}
]
},
};
}
Iterable<dynamic> _getAllTestSteps(List<TestSuite> suites) {
return <dynamic>[
..._getTestStepsForPlatform(suites, 'Linux', <BrowserName>{
BrowserName.chrome,
BrowserName.firefox,
}),
..._getTestStepsForPlatform(suites, 'Mac', <BrowserName>{
BrowserName.safari,
}),
..._getTestStepsForPlatform(suites, 'Windows', <BrowserName>{
BrowserName.chrome,
}),
];
}
Iterable<dynamic> _getTestStepsForPlatform(
List<TestSuite> suites,
String platform,
Set<BrowserName> browsers) {
return suites
.where((TestSuite suite) => browsers.contains(suite.runConfig.browser))
.map((TestSuite suite) => <String, dynamic>{
'name': '$platform run ${suite.name} suite',
'recipe': 'engine_v2/tester_engine',
'drone_dimensions': <String>[
'device_type=none',
'os=$platform',
],
'gclient_variables': <String, dynamic>{
'download_android_deps': false,
},
'dependencies': <String>[
'web_tests/artifacts',
'web_tests/test_bundles/${suite.testBundle.name}',
],
'test_dependencies': <dynamic>[
<String, dynamic>{
'dependency': 'goldctl',
'version': 'git_revision:3a77d0b12c697a840ca0c7705208e8622dc94603',
},
if (suite.runConfig.browser == BrowserName.chrome)
<String, dynamic>{
'dependency': 'chrome_and_driver',
'version': 'version:111.0',
},
if (suite.runConfig.browser == BrowserName.firefox)
<String, dynamic>{
'dependency': 'firefox',
'version': 'version:106.0',
}
],
'tasks': <dynamic>[
<String, dynamic>{
'name': 'run suite ${suite.name}',
'parameters': <String>[
'test',
'--run',
'--suite=${suite.name}'
],
'script': 'flutter/lib/web_ui/dev/felt',
}
]
}
);
}

View File

@@ -82,7 +82,6 @@ class LicensesCommand extends Command<bool> {
// This is the old path that tests used to be built into. Ignore anything
// within this path.
final String legacyBuildPath = path.join(environment.webUiRootDir.path, 'build');
return directory.listSync(recursive: true).whereType<io.File>().where((io.File f) {
if (!f.path.endsWith('.dart') && !f.path.endsWith('.js')) {
// Not a source file we're checking.

View File

@@ -8,6 +8,7 @@ import 'dart:io' as io;
import 'package:path/path.dart' as path;
import 'package:watcher/watcher.dart';
import 'exceptions.dart';
import 'utils.dart';
/// Describes what [Pipeline] is currently doing.
@@ -87,6 +88,13 @@ abstract class ProcessStep implements PipelineStep {
}
}
class _PipelineStepFailure {
_PipelineStepFailure(this.step, this.error);
final PipelineStep step;
final Object error;
}
/// Executes a sequence of asynchronous tasks, typically as part of a build/test
/// process.
///
@@ -112,27 +120,34 @@ class Pipeline {
///
/// Returns a future that resolves after all steps have been performed.
///
/// The future resolves to an error as soon as any of the steps fails.
/// If any steps fail, the pipeline attempts to continue to subsequent steps,
/// but will fail at the end.
///
/// The pipeline may be interrupted by calling [stop] before the future
/// resolves.
Future<void> run() async {
_status = PipelineStatus.started;
try {
for (final PipelineStep step in steps) {
if (status != PipelineStatus.started) {
break;
}
_currentStep = step;
_currentStepFuture = step.run();
final List<_PipelineStepFailure> failures = <_PipelineStepFailure>[];
for (final PipelineStep step in steps) {
_currentStep = step;
_currentStepFuture = step.run();
try {
await _currentStepFuture;
} catch (e) {
failures.add(_PipelineStepFailure(step, e));
} finally {
_currentStep = null;
}
}
if (failures.isEmpty) {
_status = PipelineStatus.done;
} catch (_) {
} else {
_status = PipelineStatus.error;
rethrow;
} finally {
_currentStep = null;
print('Pipeline experienced the following failures:');
for (final _PipelineStepFailure failure in failures) {
print(' "${failure.step.description}": ${failure.error}');
}
throw ToolExit('Test pipeline failed.');
}
}

View File

@@ -1,111 +0,0 @@
// 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 'common.dart';
import 'pipeline.dart';
import 'steps/compile_tests_step.dart';
import 'steps/run_tests_step.dart';
import 'utils.dart';
/// Runs build and test steps.
///
/// This command is designed to be invoked by the LUCI build graph. However, it
/// is also usable locally.
///
/// Usage:
///
/// felt run name_of_build_step
class RunCommand extends Command<bool> with ArgUtils<bool> {
RunCommand() {
argParser.addFlag(
'list',
abbr: 'l',
help: 'Lists all available build steps.',
);
argParser.addFlag(
'require-skia-gold',
help: 'Whether we require Skia Gold to be available or not. When this '
'flag is true, the tests will fail if Skia Gold is not available.',
);
argParser.addFlag(
'wasm',
help: 'Whether the test we are running are compiled to webassembly.'
);
}
@override
String get name => 'run';
bool get isWasm => boolArg('wasm');
bool get isListSteps => boolArg('list');
/// When running screenshot tests, require Skia Gold to be available and
/// reachable.
bool get requireSkiaGold => boolArg('require-skia-gold');
@override
String get description => 'Runs a build step.';
/// Build steps to run, in order specified.
List<String> get stepNames => argResults!.rest;
@override
FutureOr<bool> run() async {
// All available build steps.
final Map<String, PipelineStep> buildSteps = <String, PipelineStep>{
'compile_tests': CompileTestsStep(),
for (final String browserName in kAllBrowserNames)
'run_tests_$browserName': RunTestsStep(
browserName: browserName,
isDebug: false,
isWasm: isWasm,
doUpdateScreenshotGoldens: false,
requireSkiaGold: requireSkiaGold,
overridePathToCanvasKit: null,
),
};
if (isListSteps) {
buildSteps.keys.forEach(print);
return true;
}
if (stepNames.isEmpty) {
throw UsageException('No build steps specified.', argParser.usage);
}
final List<String> unrecognizedStepNames = <String>[];
for (final String stepName in stepNames) {
if (!buildSteps.containsKey(stepName)) {
unrecognizedStepNames.add(stepName);
}
}
if (unrecognizedStepNames.isNotEmpty) {
io.stderr.writeln(
'Unknown build steps specified: ${unrecognizedStepNames.join(', ')}',
);
return false;
}
final List<PipelineStep> steps = <PipelineStep>[];
print('Running steps ${steps.join(', ')}');
for (final String stepName in stepNames) {
steps.add(buildSteps[stepName]!);
}
final Stopwatch stopwatch = Stopwatch()..start();
final Pipeline pipeline = Pipeline(steps: steps);
await pipeline.run();
stopwatch.stop();
print('Finished running steps in ${stopwatch.elapsedMilliseconds / 1000} seconds.');
return true;
}
}

View File

@@ -0,0 +1,308 @@
// 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:convert';
import 'dart:io' as io;
import 'package:path/path.dart' as pathlib;
import 'package:pool/pool.dart';
import '../environment.dart';
import '../exceptions.dart';
import '../felt_config.dart';
import '../pipeline.dart';
import '../utils.dart' show AnsiColors, FilePath, ProcessManager, cleanup, getBundleBuildDirectory, startProcess;
/// Compiles a web test bundle into web_ui/build/test_bundles/<bundle-name>.
class CompileBundleStep implements PipelineStep {
CompileBundleStep({
required this.bundle,
required this.isVerbose,
this.testFiles,
});
final TestBundle bundle;
final bool isVerbose;
final Set<FilePath>? testFiles;
// Maximum number of concurrent compile processes to use.
static final int _compileConcurrency = int.parse(io.Platform.environment['FELT_COMPILE_CONCURRENCY'] ?? '8');
final Pool compilePool = Pool(_compileConcurrency);
@override
String get description => 'compile_bundle';
@override
bool get isSafeToInterrupt => true;
@override
Future<void> interrupt() async {
await cleanup();
}
io.Directory get testSetDirectory => io.Directory(
pathlib.join(environment.webUiTestDir.path, bundle.testSet.directory)
);
io.Directory get outputBundleDirectory => getBundleBuildDirectory(bundle);
List<FilePath> _findTestFiles() {
final io.Directory testDirectory = testSetDirectory;
if (!testDirectory.existsSync()) {
throw ToolExit('Test directory "${testDirectory.path}" for bundle ${bundle.name.ansiMagenta} does not exist.');
}
return testDirectory
.listSync(recursive: true)
.whereType<io.File>()
.where((io.File f) => f.path.endsWith('_test.dart'))
.map<FilePath>((io.File f) => FilePath.fromWebUi(
pathlib.relative(f.path, from: environment.webUiRootDir.path)))
.toList();
}
TestCompiler _createCompiler() {
switch (bundle.compileConfig.compiler) {
case Compiler.dart2js:
return Dart2JSCompiler(
testSetDirectory,
outputBundleDirectory,
renderer: bundle.compileConfig.renderer,
isVerbose: isVerbose,
);
case Compiler.dart2wasm:
return Dart2WasmCompiler(
testSetDirectory,
outputBundleDirectory,
renderer: bundle.compileConfig.renderer,
isVerbose: isVerbose,
);
}
}
@override
Future<void> run() async {
print('Compiling test bundle ${bundle.name.ansiMagenta}...');
final List<FilePath> allTests = _findTestFiles();
final TestCompiler compiler = _createCompiler();
final Stopwatch stopwatch = Stopwatch()..start();
final String testSetDirectoryPath = testSetDirectory.path;
// Clear out old bundle compilations, if they exist
if (outputBundleDirectory.existsSync()) {
outputBundleDirectory.deleteSync(recursive: true );
}
final List<Future<MapEntry<String, CompileResult>>> pendingResults = <Future<MapEntry<String, CompileResult>>>[];
for (final FilePath testFile in allTests) {
final String relativePath = pathlib.relative(
testFile.absolute,
from: testSetDirectoryPath);
final Future<MapEntry<String, CompileResult>> result = compilePool.withResource(() async {
if (testFiles != null && !testFiles!.contains(testFile)) {
return MapEntry<String, CompileResult>(relativePath, CompileResult.filtered);
}
final bool success = await compiler.compileTest(testFile);
const int maxTestNameLength = 80;
final String truncatedPath = relativePath.length > maxTestNameLength
? relativePath.replaceRange(maxTestNameLength - 3, relativePath.length, '...')
: relativePath;
final String expandedPath = truncatedPath.padRight(maxTestNameLength);
io.stdout.write('\r ${success ? expandedPath.ansiGreen : expandedPath.ansiRed}');
return success
? MapEntry<String, CompileResult>(relativePath, CompileResult.success)
: MapEntry<String, CompileResult>(relativePath, CompileResult.compilationFailure);
});
pendingResults.add(result);
}
final Map<String, CompileResult> results = Map<String, CompileResult>.fromEntries(await Future.wait(pendingResults));
stopwatch.stop();
final String resultsJson = const JsonEncoder.withIndent(' ').convert(<String, dynamic>{
'name': bundle.name,
'directory': bundle.testSet.directory,
'compiler': bundle.compileConfig.compiler.name,
'renderer': bundle.compileConfig.renderer.name,
'compileTimeInMs': stopwatch.elapsedMilliseconds,
'results': results.map((String k, CompileResult v) => MapEntry<String, String>(k, v.name)),
});
final io.File outputResultsFile = io.File(pathlib.join(
outputBundleDirectory.path,
'results.json',
));
outputResultsFile.writeAsStringSync(resultsJson);
final List<String> failedFiles = <String>[];
results.forEach((String fileName, CompileResult result) {
if (result == CompileResult.compilationFailure) {
failedFiles.add(fileName);
}
});
if (failedFiles.isEmpty) {
print('\rCompleted compilation of ${bundle.name.ansiMagenta} in ${stopwatch.elapsedMilliseconds}ms.'.padRight(82));
} else {
print('\rThe bundle ${bundle.name.ansiMagenta} compiled with some failures in ${stopwatch.elapsedMilliseconds}ms.');
print('Compilation failures:');
for (final String fileName in failedFiles) {
print(' $fileName');
}
throw ToolExit('Failed to compile ${bundle.name.ansiMagenta}.');
}
}
}
enum CompileResult {
success,
compilationFailure,
filtered,
}
abstract class TestCompiler {
TestCompiler(
this.inputTestSetDirectory,
this.outputTestBundleDirectory,
{
required this.renderer,
required this.isVerbose,
}
);
final io.Directory inputTestSetDirectory;
final io.Directory outputTestBundleDirectory;
final Renderer renderer;
final bool isVerbose;
Future<bool> compileTest(FilePath input);
}
class Dart2JSCompiler extends TestCompiler {
Dart2JSCompiler(
super.inputTestSetDirectory,
super.outputTestBundleDirectory,
{
required super.renderer,
required super.isVerbose,
}
);
@override
Future<bool> compileTest(FilePath input) async {
final String relativePath = pathlib.relative(
input.absolute,
from: inputTestSetDirectory.path
);
final String targetFileName = pathlib.join(
outputTestBundleDirectory.path,
'$relativePath.browser_test.dart.js',
);
final io.Directory outputDirectory = io.File(targetFileName).parent;
if (!outputDirectory.existsSync()) {
outputDirectory.createSync(recursive: true);
}
final List<String> arguments = <String>[
'compile',
'js',
'--no-minify',
'--disable-inlining',
'--enable-asserts',
// We do not want to auto-select a renderer in tests. As of today, tests
// are designed to run in one specific mode. So instead, we specify the
// renderer explicitly.
'-DFLUTTER_WEB_AUTO_DETECT=false',
'-DFLUTTER_WEB_USE_SKIA=${renderer == Renderer.canvaskit}',
'-DFLUTTER_WEB_USE_SKWASM=${renderer == Renderer.skwasm}',
'-O2',
'-o',
targetFileName, // target path.
relativePath, // current path.
];
final ProcessManager process = await startProcess(
environment.dartExecutable,
arguments,
workingDirectory: inputTestSetDirectory.path,
failureIsSuccess: true,
evalOutput: !isVerbose,
);
final int exitCode = await process.wait();
if (exitCode != 0) {
io.stderr.writeln('ERROR: Failed to compile test $input. '
'Dart2js exited with exit code $exitCode');
return false;
} else {
return true;
}
}
}
class Dart2WasmCompiler extends TestCompiler {
Dart2WasmCompiler(
super.inputTestSetDirectory,
super.outputTestBundleDirectory,
{
required super.renderer,
required super.isVerbose,
}
);
@override
Future<bool> compileTest(FilePath input) async {
final String relativePath = pathlib.relative(
input.absolute,
from: inputTestSetDirectory.path
);
final String targetFileName = pathlib.join(
outputTestBundleDirectory.path,
'$relativePath.browser_test.dart.wasm',
);
final io.Directory outputDirectory = io.File(targetFileName).parent;
if (!outputDirectory.existsSync()) {
outputDirectory.createSync(recursive: true);
}
final List<String> arguments = <String>[
environment.dart2wasmSnapshotPath,
'--dart-sdk=${environment.dartSdkDir.path}',
'--enable-asserts',
// We do not want to auto-select a renderer in tests. As of today, tests
// are designed to run in one specific mode. So instead, we specify the
// renderer explicitly.
'-DFLUTTER_WEB_AUTO_DETECT=false',
'-DFLUTTER_WEB_USE_SKIA=${renderer == Renderer.canvaskit}',
'-DFLUTTER_WEB_USE_SKWASM=${renderer == Renderer.skwasm}',
if (renderer == Renderer.skwasm) ...<String>[
'--import-shared-memory',
'--shared-memory-max-pages=32768',
],
relativePath, // current path.
targetFileName, // target path.
];
final ProcessManager process = await startProcess(
environment.dartAotRuntimePath,
arguments,
workingDirectory: inputTestSetDirectory.path,
failureIsSuccess: true,
evalOutput: true,
);
final int exitCode = await process.wait();
if (exitCode != 0) {
io.stderr.writeln('ERROR: Failed to compile test $input. '
'dart2wasm exited with exit code $exitCode');
return false;
} else {
return true;
}
}
}

View File

@@ -1,504 +0,0 @@
// 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:convert' show JsonEncoder;
import 'dart:io' as io;
import 'package:path/path.dart' as pathlib;
import 'package:pool/pool.dart';
import '../environment.dart';
import '../exceptions.dart';
import '../pipeline.dart';
import '../utils.dart';
/// Compiles web tests and their dependencies into web_ui/build/.
///
/// Outputs of this step:
///
/// * canvaskit/ - CanvasKit artifacts
/// * assets/ - test fonts
/// * host/ - compiled test host page and static artifacts
/// * test/ - compiled test code
/// * test_images/ - test images copied from Skis sources.
class CompileTestsStep implements PipelineStep {
CompileTestsStep({
this.testFiles,
this.useLocalCanvasKit = false,
this.isWasm = false
});
final List<FilePath>? testFiles;
final bool isWasm;
final bool useLocalCanvasKit;
@override
String get description => 'compile_tests';
@override
bool get isSafeToInterrupt => true;
@override
Future<void> interrupt() async {
await cleanup();
}
@override
Future<void> run() async {
await environment.webUiBuildDir.create(recursive: true);
if (isWasm) {
await copyDart2WasmTestScript();
await copySkwasm();
}
await copyCanvasKitFiles(useLocalCanvasKit: useLocalCanvasKit);
await buildHostPage();
await copyTestFonts();
await copySkiaTestImages();
await compileTests(testFiles ?? findAllTests(), isWasm);
}
}
const Map<String, String> _kTestFonts = <String, String>{
'Ahem': 'ahem.ttf',
'Roboto': 'Roboto-Regular.ttf',
'RobotoVariable': 'RobotoSlab-VariableFont_wght.ttf',
'Noto Naskh Arabic UI': 'NotoNaskhArabic-Regular.ttf',
'Noto Color Emoji': 'NotoColorEmoji.ttf',
};
Future<void> copyTestFonts() async {
final String fontsPath = pathlib.join(
environment.flutterDirectory.path,
'third_party',
'txt',
'third_party',
'fonts',
);
final List<dynamic> fontManifest = <dynamic>[];
for (final MapEntry<String, String> fontEntry in _kTestFonts.entries) {
final String family = fontEntry.key;
final String fontFile = fontEntry.value;
fontManifest.add(<String, dynamic>{
'family': family,
'fonts': <dynamic>[
<String, String>{
'asset': 'fonts/$fontFile',
},
],
});
final io.File sourceTtf = io.File(pathlib.join(fontsPath, fontFile));
final io.File destinationTtf = io.File(pathlib.join(
environment.webUiBuildDir.path,
'assets',
'fonts',
fontFile,
));
await destinationTtf.create(recursive: true);
await sourceTtf.copy(destinationTtf.path);
}
final io.File fontManifestFile = io.File(pathlib.join(
environment.webUiBuildDir.path,
'assets',
'FontManifest.json',
));
await fontManifestFile.create(recursive: true);
await fontManifestFile.writeAsString(
const JsonEncoder.withIndent(' ').convert(fontManifest),
);
}
Future<void> copySkiaTestImages() async {
final io.Directory testImagesDir = io.Directory(pathlib.join(
environment.engineSrcDir.path,
'third_party',
'skia',
'resources',
'images',
));
for (final io.File imageFile in testImagesDir.listSync(recursive: true).whereType<io.File>()) {
final io.File destination = io.File(pathlib.join(
environment.webUiBuildDir.path,
'test_images',
pathlib.relative(imageFile.path, from: testImagesDir.path),
));
destination.createSync(recursive: true);
await imageFile.copy(destination.path);
}
}
Future<void> copyDart2WasmTestScript() async {
final io.File sourceFile = io.File(pathlib.join(
environment.webUiDevDir.path,
'test_dart2wasm.js',
));
final io.File targetFile = io.File(pathlib.join(
environment.webUiBuildDir.path,
'test_dart2wasm.js',
));
await sourceFile.copy(targetFile.path);
}
Future<void> copySkwasm() async {
final io.Directory targetDir = io.Directory(pathlib.join(
environment.webUiBuildDir.path,
'skwasm',
));
await targetDir.create(recursive: true);
for (final String fileName in <String>[
'skwasm.wasm',
'skwasm.js',
'skwasm.worker.js',
]) {
final io.File sourceFile = io.File(pathlib.join(
environment.wasmReleaseOutDir.path,
fileName,
));
final io.File targetFile = io.File(pathlib.join(
targetDir.path,
fileName,
));
await sourceFile.copy(targetFile.path);
}
}
final io.Directory _localCanvasKitDir = io.Directory(pathlib.join(
environment.wasmReleaseOutDir.path,
'canvaskit',
));
final io.File _localCanvasKitWasm = io.File(pathlib.join(
_localCanvasKitDir.path,
'canvaskit.wasm',
));
Future<void> copyCanvasKitFiles({bool useLocalCanvasKit = false}) async {
// If CanvasKit has been built locally, use that instead of the CIPD version.
final bool localCanvasKitExists = _localCanvasKitWasm.existsSync();
if (useLocalCanvasKit && !localCanvasKitExists) {
throw ArgumentError('Requested to use local CanvasKit but could not find the '
'built CanvasKit at ${_localCanvasKitWasm.path}. Falling back to '
'CanvasKit from CIPD.');
}
final io.Directory targetDir = io.Directory(pathlib.join(
environment.webUiBuildDir.path,
'canvaskit',
));
if (useLocalCanvasKit) {
final Iterable<io.File> canvasKitFiles =
_localCanvasKitDir.listSync(recursive: true).whereType<io.File>();
for (final io.File file in canvasKitFiles) {
if (!file.path.endsWith('.wasm') && !file.path.endsWith('.js')) {
// We only need the .wasm and .js files.
continue;
}
final String relativePath =
pathlib.relative(file.path, from: _localCanvasKitDir.path);
final io.File normalTargetFile =
io.File(pathlib.join(targetDir.path, relativePath));
await normalTargetFile.create(recursive: true);
await file.copy(normalTargetFile.path);
}
} else {
final io.Directory canvasKitDir = io.Directory(pathlib.join(
environment.engineSrcDir.path,
'third_party',
'web_dependencies',
'canvaskit',
));
final Iterable<io.File> canvasKitFiles = canvasKitDir
.listSync(recursive: true)
.whereType<io.File>();
for (final io.File file in canvasKitFiles) {
final String relativePath =
pathlib.relative(file.path, from: canvasKitDir.path);
final io.File targetFile = io.File(pathlib.join(
targetDir.path,
relativePath,
));
await targetFile.create(recursive: true);
await file.copy(targetFile.path);
}
}
}
/// Compiles the specified unit tests.
Future<void> compileTests(List<FilePath> testFiles, bool isWasm) async {
final Stopwatch stopwatch = Stopwatch()..start();
final TestsByRenderer sortedTests = sortTestsByRenderer(testFiles, isWasm);
await Future.wait(<Future<void>>[
if (sortedTests.htmlTests.isNotEmpty)
_compileTestsInParallel(targets: sortedTests.htmlTests, isWasm: isWasm),
if (sortedTests.canvasKitTests.isNotEmpty)
_compileTestsInParallel(targets: sortedTests.canvasKitTests, renderer: Renderer.canvasKit, isWasm: isWasm),
if (sortedTests.skwasmTests.isNotEmpty)
_compileTestsInParallel(targets: sortedTests.skwasmTests, renderer: Renderer.skwasm, isWasm: isWasm),
]);
stopwatch.stop();
final int targetCount = sortedTests.numTargetsToCompile;
print(
'Built $targetCount tests in ${stopwatch.elapsedMilliseconds ~/ 1000} '
'seconds using $_dart2jsConcurrency concurrent compile processes.',
);
}
// Maximum number of concurrent dart2js processes to use.
int _dart2jsConcurrency = int.parse(io.Platform.environment['FELT_DART2JS_CONCURRENCY'] ?? '8');
final Pool _dart2jsPool = Pool(_dart2jsConcurrency);
/// Spawns multiple dart2js processes to compile [targets] in parallel.
Future<void> _compileTestsInParallel({
required List<FilePath> targets,
Renderer renderer = Renderer.html,
bool isWasm = false,
}) async {
final Stream<bool> results = _dart2jsPool.forEach(
targets,
(FilePath file) => compileUnitTest(file, renderer: renderer, isWasm: isWasm),
);
await for (final bool isSuccess in results) {
if (!isSuccess) {
throw ToolExit('Failed to compile tests.');
}
}
}
Future<bool> compileUnitTest(FilePath input, {required Renderer renderer, required bool isWasm}) async {
return isWasm ? compileUnitTestToWasm(input, renderer: renderer)
: compileUnitTestToJS(input, renderer: renderer);
}
/// Compiles one unit test using `dart2js`.
///
/// When building for CanvasKit we have to use extra argument
/// `DFLUTTER_WEB_USE_SKIA=true`.
///
/// Dart2js creates the following outputs:
/// - target.browser_test.dart.js
/// - target.browser_test.dart.js.deps
/// - target.browser_test.dart.js.map
/// under the same directory with test file. If all these files are not in
/// the same directory, Chrome dev tools cannot load the source code during
/// debug.
///
/// All the files under test already copied from /test directory to /build
/// directory before test are build. See [_copyFilesFromTestToBuild].
///
/// Later the extra files will be deleted in [_cleanupExtraFilesUnderTestDir].
Future<bool> compileUnitTestToJS(FilePath input, {required Renderer renderer}) async {
// Compile to different directories for different renderers. This allows us
// to run the same test in multiple renderers.
final String targetFileName = pathlib.join(
environment.webUiBuildDir.path,
getBuildDirForRenderer(renderer),
'${input.relativeToWebUi}.browser_test.dart.js',
);
final io.Directory directoryToTarget = io.Directory(pathlib.join(
environment.webUiBuildDir.path,
getBuildDirForRenderer(renderer),
pathlib.dirname(input.relativeToWebUi)));
if (!directoryToTarget.existsSync()) {
directoryToTarget.createSync(recursive: true);
}
final List<String> arguments = <String>[
'compile',
'js',
'--no-minify',
'--disable-inlining',
'--enable-asserts',
// We do not want to auto-select a renderer in tests. As of today, tests
// are designed to run in one specific mode. So instead, we specify the
// renderer explicitly.
'-DFLUTTER_WEB_AUTO_DETECT=false',
'-DFLUTTER_WEB_USE_SKIA=${renderer == Renderer.canvasKit}',
'-DFLUTTER_WEB_USE_SKWASM=${renderer == Renderer.skwasm}',
'-O2',
'-o',
targetFileName, // target path.
input.relativeToWebUi, // current path.
];
final int exitCode = await runProcess(
environment.dartExecutable,
arguments,
workingDirectory: environment.webUiRootDir.path,
);
if (exitCode != 0) {
io.stderr.writeln('ERROR: Failed to compile test $input. '
'Dart2js exited with exit code $exitCode');
return false;
} else {
return true;
}
}
Future<bool> compileUnitTestToWasm(FilePath input, {required Renderer renderer}) async {
final String targetFileName = pathlib.join(
environment.webUiBuildDir.path,
getBuildDirForRenderer(renderer),
'${input.relativeToWebUi}.browser_test.dart.wasm',
);
final io.Directory directoryToTarget = io.Directory(pathlib.join(
environment.webUiBuildDir.path,
getBuildDirForRenderer(renderer),
pathlib.dirname(input.relativeToWebUi)));
if (!directoryToTarget.existsSync()) {
directoryToTarget.createSync(recursive: true);
}
final List<String> arguments = <String>[
environment.dart2wasmSnapshotPath,
'--dart-sdk=${environment.dartSdkDir.path}',
'--enable-asserts',
// We do not want to auto-select a renderer in tests. As of today, tests
// are designed to run in one specific mode. So instead, we specify the
// renderer explicitly.
'-DFLUTTER_WEB_AUTO_DETECT=false',
'-DFLUTTER_WEB_USE_SKIA=${renderer == Renderer.canvasKit}',
'-DFLUTTER_WEB_USE_SKWASM=${renderer == Renderer.skwasm}',
if (renderer == Renderer.skwasm)
...<String>[
'--import-shared-memory',
'--shared-memory-max-pages=32768',
],
input.relativeToWebUi, // current path.
targetFileName, // target path.
];
final int exitCode = await runProcess(
environment.dartAotRuntimePath,
arguments,
workingDirectory: environment.webUiRootDir.path,
);
if (exitCode != 0) {
io.stderr.writeln('ERROR: Failed to compile test $input. '
'dart2wasm exited with exit code $exitCode');
return false;
} else {
return true;
}
}
Future<void> buildHostPage() async {
final String hostDartPath = pathlib.join('lib', 'static', 'host.dart');
final io.File hostDartFile = io.File(pathlib.join(
environment.webEngineTesterRootDir.path,
hostDartPath,
));
final String targetDirectoryPath = pathlib.join(
environment.webUiBuildDir.path,
'host',
);
io.Directory(targetDirectoryPath).createSync(recursive: true);
final String targetFilePath = pathlib.join(
targetDirectoryPath,
'host.dart',
);
const List<String> staticFiles = <String>[
'favicon.ico',
'host.css',
'index.html',
];
for (final String staticFilePath in staticFiles) {
final io.File source = io.File(pathlib.join(
environment.webEngineTesterRootDir.path,
'lib',
'static',
staticFilePath,
));
final io.File destination = io.File(pathlib.join(
targetDirectoryPath,
staticFilePath,
));
await source.copy(destination.path);
}
final io.File timestampFile = io.File(pathlib.join(
environment.webEngineTesterRootDir.path,
'$targetFilePath.js.timestamp',
));
final String timestamp =
hostDartFile.statSync().modified.millisecondsSinceEpoch.toString();
if (timestampFile.existsSync()) {
final String lastBuildTimestamp = timestampFile.readAsStringSync();
if (lastBuildTimestamp == timestamp) {
// The file is still fresh. No need to rebuild.
return;
} else {
// Record new timestamp, but don't return. We need to rebuild.
print('${hostDartFile.path} timestamp changed. Rebuilding.');
}
} else {
print('Building ${hostDartFile.path}.');
}
int exitCode = await runProcess(
environment.dartExecutable,
<String>[
'pub',
'get',
],
workingDirectory: environment.webEngineTesterRootDir.path
);
if (exitCode != 0) {
throw ToolExit(
'Failed to run pub get for web_engine_tester, exit code $exitCode',
exitCode: exitCode,
);
}
exitCode = await runProcess(
environment.dartExecutable,
<String>[
'compile',
'js',
hostDartPath,
'-o',
'$targetFilePath.js',
],
workingDirectory: environment.webEngineTesterRootDir.path,
);
if (exitCode != 0) {
throw ToolExit(
'Failed to compile ${hostDartFile.path}. Compiler '
'exited with exit code $exitCode',
exitCode: exitCode,
);
}
// Record the timestamp to avoid rebuilding unless the file changes.
timestampFile.writeAsStringSync(timestamp);
}

View File

@@ -0,0 +1,293 @@
// 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:convert' show JsonEncoder;
import 'dart:io' as io;
import 'package:path/path.dart' as pathlib;
import '../environment.dart';
import '../exceptions.dart';
import '../felt_config.dart';
import '../pipeline.dart';
import '../utils.dart';
class CopyArtifactsStep implements PipelineStep {
CopyArtifactsStep(this.artifactDeps, { required this.isProfile });
final ArtifactDependencies artifactDeps;
final bool isProfile;
@override
String get description => 'copy_artifacts';
@override
bool get isSafeToInterrupt => true;
@override
Future<void> interrupt() async {
await cleanup();
}
@override
Future<void> run() async {
await environment.webTestsArtifactsDir.create(recursive: true);
await copyDart2WasmTestScript();
await buildHostPage();
await copyTestFonts();
await copySkiaTestImages();
if (artifactDeps.canvasKit) {
print('Copying CanvasKit...');
await copyCanvasKitFiles('canvaskit', 'canvaskit');
}
if (artifactDeps.canvasKitChromium) {
print('Copying CanvasKit (Chromium)...');
await copyCanvasKitFiles('canvaskit_chromium', 'canvaskit/chromium');
}
if (artifactDeps.skwasm) {
print('Copying Skwasm...');
await copySkwasm();
}
}
Future<void> copyDart2WasmTestScript() async {
final io.File sourceFile = io.File(pathlib.join(
environment.webUiDevDir.path,
'test_dart2wasm.js',
));
final io.File targetFile = io.File(pathlib.join(
environment.webTestsArtifactsDir.path,
'test_dart2wasm.js',
));
await sourceFile.copy(targetFile.path);
}
Future<void> copyTestFonts() async {
const Map<String, String> testFonts = <String, String>{
'Ahem': 'ahem.ttf',
'Roboto': 'Roboto-Regular.ttf',
'RobotoVariable': 'RobotoSlab-VariableFont_wght.ttf',
'Noto Naskh Arabic UI': 'NotoNaskhArabic-Regular.ttf',
'Noto Color Emoji': 'NotoColorEmoji.ttf',
};
final String fontsPath = pathlib.join(
environment.flutterDirectory.path,
'third_party',
'txt',
'third_party',
'fonts',
);
final List<dynamic> fontManifest = <dynamic>[];
for (final MapEntry<String, String> fontEntry in testFonts.entries) {
final String family = fontEntry.key;
final String fontFile = fontEntry.value;
fontManifest.add(<String, dynamic>{
'family': family,
'fonts': <dynamic>[
<String, String>{
'asset': 'fonts/$fontFile',
},
],
});
final io.File sourceTtf = io.File(pathlib.join(fontsPath, fontFile));
final io.File destinationTtf = io.File(pathlib.join(
environment.webTestsArtifactsDir.path,
'assets',
'fonts',
fontFile,
));
await destinationTtf.create(recursive: true);
await sourceTtf.copy(destinationTtf.path);
}
final io.File fontManifestFile = io.File(pathlib.join(
environment.webTestsArtifactsDir.path,
'assets',
'FontManifest.json',
));
await fontManifestFile.create(recursive: true);
await fontManifestFile.writeAsString(
const JsonEncoder.withIndent(' ').convert(fontManifest),
);
}
Future<void> copySkiaTestImages() async {
final io.Directory testImagesDir = io.Directory(pathlib.join(
environment.engineSrcDir.path,
'third_party',
'skia',
'resources',
'images',
));
for (final io.File imageFile in testImagesDir.listSync(recursive: true).whereType<io.File>()) {
final io.File destination = io.File(pathlib.join(
environment.webTestsArtifactsDir.path,
'test_images',
pathlib.relative(imageFile.path, from: testImagesDir.path),
));
destination.createSync(recursive: true);
await imageFile.copy(destination.path);
}
}
Future<void> copyCanvasKitFiles(String sourcePath, String destinationPath) async {
final String sourceDirectoryPath = pathlib.join(
outBuildPath,
sourcePath,
);
final String targetDirectoryPath = pathlib.join(
environment.webTestsArtifactsDir.path,
destinationPath,
);
for (final String filename in <String>[
'canvaskit.js',
'canvaskit.wasm',
]) {
final io.File sourceFile = io.File(pathlib.join(
sourceDirectoryPath,
filename,
));
final io.File targetFile = io.File(pathlib.join(
targetDirectoryPath,
filename,
));
if (!sourceFile.existsSync()) {
throw ToolExit('Built CanvasKit artifact not found at path "$sourceFile".');
}
await targetFile.create(recursive: true);
await sourceFile.copy(targetFile.path);
}
}
String get outBuildPath => isProfile
? environment.wasmProfileOutDir.path
: environment.wasmReleaseOutDir.path;
Future<void> copySkwasm() async {
final io.Directory targetDir = io.Directory(pathlib.join(
environment.webTestsArtifactsDir.path,
'canvaskit',
));
await targetDir.create(recursive: true);
for (final String fileName in <String>[
'skwasm.wasm',
'skwasm.js',
'skwasm.worker.js',
]) {
final io.File sourceFile = io.File(pathlib.join(
outBuildPath,
fileName,
));
final io.File targetFile = io.File(pathlib.join(
targetDir.path,
fileName,
));
await sourceFile.copy(targetFile.path);
}
}
Future<void> buildHostPage() async {
final String hostDartPath = pathlib.join('lib', 'static', 'host.dart');
final io.File hostDartFile = io.File(pathlib.join(
environment.webEngineTesterRootDir.path,
hostDartPath,
));
final String targetDirectoryPath = pathlib.join(
environment.webTestsArtifactsDir.path,
'host',
);
io.Directory(targetDirectoryPath).createSync(recursive: true);
final String targetFilePath = pathlib.join(
targetDirectoryPath,
'host.dart',
);
const List<String> staticFiles = <String>[
'favicon.ico',
'host.css',
'index.html',
];
for (final String staticFilePath in staticFiles) {
final io.File source = io.File(pathlib.join(
environment.webEngineTesterRootDir.path,
'lib',
'static',
staticFilePath,
));
final io.File destination = io.File(pathlib.join(
targetDirectoryPath,
staticFilePath,
));
await source.copy(destination.path);
}
final io.File timestampFile = io.File(pathlib.join(
environment.webEngineTesterRootDir.path,
'$targetFilePath.js.timestamp',
));
final String timestamp =
hostDartFile.statSync().modified.millisecondsSinceEpoch.toString();
if (timestampFile.existsSync()) {
final String lastBuildTimestamp = timestampFile.readAsStringSync();
if (lastBuildTimestamp == timestamp) {
// The file is still fresh. No need to rebuild.
return;
} else {
// Record new timestamp, but don't return. We need to rebuild.
print('${hostDartFile.path} timestamp changed. Rebuilding.');
}
} else {
print('Building ${hostDartFile.path}.');
}
int exitCode = await runProcess(
environment.dartExecutable,
<String>[
'pub',
'get',
],
workingDirectory: environment.webEngineTesterRootDir.path
);
if (exitCode != 0) {
throw ToolExit(
'Failed to run pub get for web_engine_tester, exit code $exitCode',
exitCode: exitCode,
);
}
exitCode = await runProcess(
environment.dartExecutable,
<String>[
'compile',
'js',
hostDartPath,
'-o',
'$targetFilePath.js',
],
workingDirectory: environment.webEngineTesterRootDir.path,
);
if (exitCode != 0) {
throw ToolExit(
'Failed to compile ${hostDartFile.path}. Compiler '
'exited with exit code $exitCode',
exitCode: exitCode,
);
}
// Record the timestamp to avoid rebuilding unless the file changes.
timestampFile.writeAsStringSync(timestamp);
}
}

View File

@@ -0,0 +1,216 @@
// 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:convert';
import 'dart:io' as io;
import 'package:path/path.dart' as pathlib;
// TODO(yjbanov): remove hacks when this is fixed:
// https://github.com/dart-lang/test/issues/1521
import 'package:skia_gold_client/skia_gold_client.dart';
import 'package:test_api/src/backend/runtime.dart' as hack;
import 'package:test_core/src/executable.dart' as test;
import 'package:test_core/src/runner/hack_register_platform.dart' as hack;
import '../browser.dart';
import '../common.dart';
import '../environment.dart';
import '../exceptions.dart';
import '../felt_config.dart';
import '../pipeline.dart';
import '../test_platform.dart';
import '../utils.dart';
/// Runs a test suite.
///
/// Assumes the artifacts from previous steps are available, either from
/// running them prior to this step locally, or by having the build graph copy
/// them from another bot.
class RunSuiteStep implements PipelineStep {
RunSuiteStep(this.suite, {
required this.isDebug,
required this.isVerbose,
required this.doUpdateScreenshotGoldens,
required this.requireSkiaGold,
this.testFiles,
required this.overridePathToCanvasKit,
});
final TestSuite suite;
final Set<FilePath>? testFiles;
final bool isDebug;
final bool isVerbose;
final bool doUpdateScreenshotGoldens;
final String? overridePathToCanvasKit;
/// Require Skia Gold to be available and reachable.
final bool requireSkiaGold;
bool get isWasm => suite.testBundle.compileConfig.compiler == Compiler.dart2wasm;
@override
String get description => 'run_suite';
@override
bool get isSafeToInterrupt => true;
@override
Future<void> interrupt() async {}
@override
Future<void> run() async {
_prepareTestResultsDirectory();
final BrowserEnvironment browserEnvironment = getBrowserEnvironment(
suite.runConfig.browser,
enableWasmGC: isWasm);
await browserEnvironment.prepare();
final SkiaGoldClient? skiaClient = await _createSkiaClient();
final String configurationFilePath = pathlib.join(
environment.webUiRootDir.path,
browserEnvironment.packageTestConfigurationYamlFile,
);
final String bundleBuildPath = getBundleBuildDirectory(suite.testBundle).path;
final List<String> testArgs = <String>[
...<String>['-r', 'compact'],
// Disable concurrency. Running with concurrency proved to be flaky.
'--concurrency=1',
if (isDebug) '--pause-after-load',
'--platform=${browserEnvironment.packageTestRuntime.identifier}',
'--precompiled=$bundleBuildPath',
'--configuration=$configurationFilePath',
'--',
..._collectTestPaths(),
];
hack.registerPlatformPlugin(<hack.Runtime>[
browserEnvironment.packageTestRuntime,
], () {
return BrowserPlatform.start(
suite,
browserEnvironment: browserEnvironment,
doUpdateScreenshotGoldens: doUpdateScreenshotGoldens,
skiaClient: skiaClient,
overridePathToCanvasKit: overridePathToCanvasKit,
isWasm: isWasm,
isVerbose: isVerbose,
);
});
print('[${suite.name.ansiCyan}] Running...');
// We want to run tests with the test set's directory as a working directory.
final io.Directory testSetDirectory = io.Directory(pathlib.join(
environment.webUiTestDir.path,
suite.testBundle.testSet.directory,
));
final dynamic originalCwd = io.Directory.current;
io.Directory.current = testSetDirectory;
try {
await test.main(testArgs);
} finally {
io.Directory.current = originalCwd;
}
await browserEnvironment.cleanup();
if (io.exitCode != 0) {
print('[${suite.name.ansiCyan}] ${'Some tests failed.'.ansiRed}');
io.exitCode = 0;
} else {
print('[${suite.name.ansiCyan}] ${'All tests passed!'.ansiGreen}');
}
}
io.Directory _prepareTestResultsDirectory() {
final io.Directory resultsDirectory = io.Directory(pathlib.join(
environment.webUiTestResultsDirectory.path,
suite.name,
));
if (resultsDirectory.existsSync()) {
resultsDirectory.deleteSync(recursive: true);
}
resultsDirectory.createSync(recursive: true);
return resultsDirectory;
}
List<String> _collectTestPaths() {
final io.Directory bundleBuild = getBundleBuildDirectory(suite.testBundle);
final io.File resultsJsonFile = io.File(pathlib.join(
bundleBuild.path,
'results.json',
));
if (!resultsJsonFile.existsSync()) {
throw ToolExit('Could not find built bundle ${suite.testBundle.name.ansiMagenta} for suite ${suite.name.ansiCyan}.');
}
final String jsonString = resultsJsonFile.readAsStringSync();
final dynamic jsonContents = const JsonDecoder().convert(jsonString);
final dynamic results = jsonContents['results'];
final List<String> testPaths = <String>[];
results.forEach((dynamic k, dynamic v) {
final String result = v as String;
final String testPath = k as String;
if (testFiles != null) {
if (!testFiles!.contains(FilePath.fromTestSet(suite.testBundle.testSet, testPath))) {
return;
}
}
if (result == 'success') {
testPaths.add(testPath);
}
});
return testPaths;
}
Future<SkiaGoldClient?> _createSkiaClient() async {
final Renderer renderer = suite.testBundle.compileConfig.renderer;
final CanvasKitVariant? variant = suite.runConfig.variant;
final SkiaGoldClient skiaClient = SkiaGoldClient(
environment.webUiSkiaGoldDirectory,
dimensions: <String, String> {
'Browser': suite.runConfig.browser.name,
if (isWasm) 'Wasm': 'true',
'Renderer': renderer.name,
if (variant != null) 'CanvasKitVariant': variant.name,
},
);
if (await _checkSkiaClient(skiaClient)) {
return skiaClient;
}
if (requireSkiaGold) {
throw ToolExit('Skia Gold is required but is unavailable.');
}
return null;
}
/// Checks whether the Skia Client is usable in this environment.
Future<bool> _checkSkiaClient(SkiaGoldClient skiaClient) async {
// Now let's check whether Skia Gold is reachable or not.
if (isLuci) {
if (isSkiaGoldClientAvailable) {
try {
await skiaClient.auth();
return true;
} catch (e) {
print(e);
}
}
} else {
try {
// Check if we can reach Gold.
await skiaClient.getExpectationForTest('');
return true;
} on io.OSError catch (_) {
print('OSError occurred, could not reach Gold.');
} on io.SocketException catch (_) {
print('SocketException occurred, could not reach Gold.');
}
}
return false;
}
}

View File

@@ -1,290 +0,0 @@
// 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:path/path.dart' as pathlib;
// TODO(yjbanov): remove hacks when this is fixed:
// https://github.com/dart-lang/test/issues/1521
import 'package:skia_gold_client/skia_gold_client.dart';
import 'package:test_api/src/backend/group.dart' as hack;
import 'package:test_api/src/backend/live_test.dart' as hack;
import 'package:test_api/src/backend/runtime.dart' as hack;
import 'package:test_core/src/executable.dart' as test;
import 'package:test_core/src/runner/configuration/reporters.dart' as hack;
import 'package:test_core/src/runner/engine.dart' as hack;
import 'package:test_core/src/runner/hack_register_platform.dart' as hack;
import 'package:test_core/src/runner/reporter.dart' as hack;
import '../browser.dart';
import '../common.dart';
import '../environment.dart';
import '../exceptions.dart';
import '../pipeline.dart';
import '../test_platform.dart';
import '../utils.dart';
/// Runs web tests.
///
/// Assumes the artifacts from [CompileTestsStep] are available, either from
/// running it prior to this step locally, or by having the build graph copy
/// them from another bot.
class RunTestsStep implements PipelineStep {
RunTestsStep({
required this.browserName,
required this.isDebug,
required this.doUpdateScreenshotGoldens,
required this.requireSkiaGold,
this.testFiles,
required this.overridePathToCanvasKit,
required this.isWasm
});
final String browserName;
final List<FilePath>? testFiles;
final bool isDebug;
final bool isWasm;
final bool doUpdateScreenshotGoldens;
final String? overridePathToCanvasKit;
/// Require Skia Gold to be available and reachable.
final bool requireSkiaGold;
@override
String get description => 'run_tests';
@override
bool get isSafeToInterrupt => true;
@override
Future<void> interrupt() async {}
@override
Future<void> run() async {
await _prepareTestResultsDirectory();
final BrowserEnvironment browserEnvironment = getBrowserEnvironment(browserName, enableWasmGC: isWasm);
await browserEnvironment.prepare();
final SkiaGoldClient? skiaClient = await _createSkiaClient();
final List<FilePath> testFiles = this.testFiles ?? findAllTests();
final TestsByRenderer sortedTests = sortTestsByRenderer(testFiles, isWasm);
bool testsPassed = true;
if (sortedTests.htmlTests.isNotEmpty) {
await _runTestBatch(
testFiles: sortedTests.htmlTests,
renderer: Renderer.html,
browserEnvironment: browserEnvironment,
expectFailure: false,
isDebug: isDebug,
isWasm: isWasm,
doUpdateScreenshotGoldens: doUpdateScreenshotGoldens,
skiaClient: skiaClient,
overridePathToCanvasKit: overridePathToCanvasKit,
);
testsPassed &= io.exitCode == 0;
}
if (sortedTests.canvasKitTests.isNotEmpty) {
await _runTestBatch(
testFiles: sortedTests.canvasKitTests,
renderer: Renderer.canvasKit,
browserEnvironment: browserEnvironment,
expectFailure: false,
isDebug: isDebug,
isWasm: isWasm,
doUpdateScreenshotGoldens: doUpdateScreenshotGoldens,
skiaClient: skiaClient,
overridePathToCanvasKit: overridePathToCanvasKit,
);
testsPassed &= io.exitCode == 0;
}
// TODO(jacksongardner): enable this test suite on safari
// For some reason, Safari is flaky when running the Skwasm test suite
// See https://github.com/flutter/flutter/issues/115312
if (browserName != kSafari && sortedTests.skwasmTests.isNotEmpty) {
await _runTestBatch(
testFiles: sortedTests.skwasmTests,
renderer: Renderer.skwasm,
browserEnvironment: browserEnvironment,
expectFailure: false,
isDebug: isDebug,
isWasm: isWasm,
doUpdateScreenshotGoldens: doUpdateScreenshotGoldens,
skiaClient: skiaClient,
overridePathToCanvasKit: overridePathToCanvasKit,
);
testsPassed &= io.exitCode == 0;
}
await browserEnvironment.cleanup();
if (!testsPassed) {
throw ToolExit('Some tests failed');
}
}
Future<SkiaGoldClient?> _createSkiaClient() async {
final SkiaGoldClient skiaClient = SkiaGoldClient(
environment.webUiSkiaGoldDirectory,
dimensions: <String, String> {
'Browser': browserName,
if (isWasm) 'Wasm': 'true',
},
);
if (await _checkSkiaClient(skiaClient)) {
return skiaClient;
}
if (requireSkiaGold) {
throw ToolExit('Skia Gold is required but is unavailable.');
}
return null;
}
/// Checks whether the Skia Client is usable in this environment.
Future<bool> _checkSkiaClient(SkiaGoldClient skiaClient) async {
// Now let's check whether Skia Gold is reachable or not.
if (isLuci) {
if (isSkiaGoldClientAvailable) {
try {
await skiaClient.auth();
return true;
} catch (e) {
print(e);
}
}
} else {
try {
// Check if we can reach Gold.
await skiaClient.getExpectationForTest('');
return true;
} on io.OSError catch (_) {
print('OSError occurred, could not reach Gold.');
} on io.SocketException catch (_) {
print('SocketException occurred, could not reach Gold.');
}
}
return false;
}
}
Future<void> _prepareTestResultsDirectory() async {
if (environment.webUiTestResultsDirectory.existsSync()) {
environment.webUiTestResultsDirectory.deleteSync(recursive: true);
}
environment.webUiTestResultsDirectory.createSync(recursive: true);
}
/// 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({
required List<FilePath> testFiles,
required Renderer renderer,
required bool isDebug,
required bool isWasm,
required BrowserEnvironment browserEnvironment,
required bool doUpdateScreenshotGoldens,
required bool expectFailure,
required SkiaGoldClient? skiaClient,
required String? overridePathToCanvasKit,
}) async {
final String configurationFilePath = pathlib.join(
environment.webUiRootDir.path,
browserEnvironment.packageTestConfigurationYamlFile,
);
final String precompiledBuildDir = pathlib.join(
environment.webUiBuildDir.path,
getBuildDirForRenderer(renderer),
);
final List<String> testArgs = <String>[
...<String>['-r', 'compact'],
// Disable concurrency. Running with concurrency proved to be flaky.
'--concurrency=1',
if (isDebug) '--pause-after-load',
// Don't pollute logs with output from tests that are expected to fail.
if (expectFailure)
'--reporter=name-only',
'--platform=${browserEnvironment.packageTestRuntime.identifier}',
'--precompiled=$precompiledBuildDir',
'--configuration=$configurationFilePath',
'--',
...testFiles.map((FilePath f) => f.relativeToWebUi),
];
if (expectFailure) {
hack.registerReporter(
'name-only',
hack.ReporterDetails(
'Prints the name of the test, but suppresses all other test output.',
(_, hack.Engine engine, __) => NameOnlyReporter(engine)),
);
}
hack.registerPlatformPlugin(<hack.Runtime>[
browserEnvironment.packageTestRuntime,
], () {
return BrowserPlatform.start(
browserEnvironment: browserEnvironment,
renderer: renderer,
// It doesn't make sense to update a screenshot for a test that is
// expected to fail.
doUpdateScreenshotGoldens: !expectFailure && doUpdateScreenshotGoldens,
skiaClient: skiaClient,
overridePathToCanvasKit: overridePathToCanvasKit,
isWasm: isWasm,
);
});
// We want to run tests with `web_ui` as a working directory.
final dynamic originalCwd = io.Directory.current;
io.Directory.current = environment.webUiRootDir.path;
try {
await test.main(testArgs);
} finally {
io.Directory.current = originalCwd;
}
if (expectFailure) {
if (io.exitCode != 0) {
// It failed, as expected.
print('Test successfully failed, as expected.');
io.exitCode = 0;
} else {
io.stderr.writeln(
'Tests ${testFiles.join(', ')} did not fail. Expected failure.',
);
io.exitCode = 1;
}
}
}
/// Prints the name of the test, but suppresses all other test output.
///
/// This is useful to prevent pollution of logs by tests that are expected to
/// fail.
class NameOnlyReporter implements hack.Reporter {
NameOnlyReporter(hack.Engine testEngine) {
testEngine.onTestStarted.listen(_printTestName);
}
void _printTestName(hack.LiveTest test) {
print('Running ${test.groups.map((hack.Group group) => group.name).join(' ')} ${test.individualName}');
}
@override
void pause() {}
@override
void resume() {}
}

View File

@@ -0,0 +1,123 @@
// 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 'felt_config.dart';
class SuiteFilterResult {
SuiteFilterResult.accepted();
SuiteFilterResult.rejected(String reason) : rejectReason = reason;
String? rejectReason;
bool get isAccepted => rejectReason == null;
}
abstract class SuiteFilter {
SuiteFilterResult filterSuite(TestSuite suite);
}
abstract class AllowListSuiteFilter<T> implements SuiteFilter {
AllowListSuiteFilter({ required this.allowList });
final Set<T> allowList;
T getAttributeForSuite(TestSuite suite);
String rejectReason(TestSuite suite) {
return '${getAttributeForSuite(suite)} does not match filter.';
}
@override
SuiteFilterResult filterSuite(TestSuite suite) {
if (allowList.contains(getAttributeForSuite(suite))) {
return SuiteFilterResult.accepted();
} else {
return SuiteFilterResult.rejected(rejectReason(suite));
}
}
}
class BrowserSuiteFilter extends AllowListSuiteFilter<BrowserName> {
BrowserSuiteFilter({required super.allowList});
@override
BrowserName getAttributeForSuite(TestSuite suite) => suite.runConfig.browser;
}
class SuiteNameFilter extends AllowListSuiteFilter<String> {
SuiteNameFilter({required super.allowList});
@override
String getAttributeForSuite(TestSuite suite) => suite.name;
}
class BundleNameFilter extends AllowListSuiteFilter<String> {
BundleNameFilter({required super.allowList});
@override
String getAttributeForSuite(TestSuite suite) => suite.testBundle.name;
}
class FileFilter extends BundleNameFilter {
FileFilter({required super.allowList});
@override
String rejectReason(TestSuite suite) {
return "Doesn't contain any of the indicated files.";
}
}
class CompilerFilter extends AllowListSuiteFilter<Compiler> {
CompilerFilter({required super.allowList});
@override
Compiler getAttributeForSuite(TestSuite suite) => suite.testBundle.compileConfig.compiler;
}
class RendererFilter extends AllowListSuiteFilter<Renderer> {
RendererFilter({required super.allowList});
@override
Renderer getAttributeForSuite(TestSuite suite) => suite.testBundle.compileConfig.renderer;
}
class CanvasKitVariantFilter extends AllowListSuiteFilter<CanvasKitVariant> {
CanvasKitVariantFilter({required super.allowList});
@override
// TODO(jackson): Is this the right default?
CanvasKitVariant getAttributeForSuite(TestSuite suite) => suite.runConfig.variant ?? CanvasKitVariant.full;
}
Set<BrowserName> get _supportedPlatformBrowsers {
if (io.Platform.isLinux) {
return <BrowserName>{
BrowserName.chrome,
BrowserName.firefox
};
} else if (io.Platform.isMacOS) {
return <BrowserName>{
BrowserName.chrome,
BrowserName.firefox,
BrowserName.safari,
};
} else if (io.Platform.isWindows) {
return <BrowserName>{
BrowserName.chrome,
BrowserName.edge,
};
} else {
throw AssertionError('Unsupported OS: ${io.Platform.operatingSystem}');
}
}
class PlatformBrowserFilter extends BrowserSuiteFilter {
PlatformBrowserFilter() : super(allowList: _supportedPlatformBrowsers);
@override
String rejectReason(TestSuite suite) =>
'Current platform (${io.Platform.operatingSystem}) does not support browser ${suite.runConfig.browser}';
}

View File

@@ -57,7 +57,7 @@ window.onload = async function () {
const isSkwasm = link.hasAttribute('skwasm');
const imports = isSkwasm ? new Promise((resolve) => {
const skwasmScript = document.createElement('script');
skwasmScript.src = '/skwasm/skwasm.js';
skwasmScript.src = '/canvaskit/skwasm.js';
document.body.appendChild(skwasmScript);
skwasmScript.addEventListener('load', async () => {

View File

@@ -38,6 +38,7 @@ import 'package:web_test_utils/image_compare.dart';
import 'browser.dart';
import 'environment.dart' as env;
import 'felt_config.dart';
import 'utils.dart';
const Map<String, String> coopCoepHeaders = <String, String>{
@@ -47,12 +48,11 @@ const Map<String, String> coopCoepHeaders = <String, String>{
/// Custom test platform that serves web engine unit tests.
class BrowserPlatform extends PlatformPlugin {
BrowserPlatform._({
BrowserPlatform._(this.suite, {
required this.browserEnvironment,
required this.server,
required this.renderer,
required this.isDebug,
required this.isWasm,
required this.isVerbose,
required this.doUpdateScreenshotGoldens,
required this.packageConfig,
required this.skiaClient,
@@ -74,8 +74,14 @@ class BrowserPlatform extends PlatformPlugin {
.add(_packageUrlHandler)
.add(_canvasKitOverrideHandler)
// Serves files from the out/web_tests/ directory at the root (/) URL path.
.add(buildDirectoryHandler)
// Serves files from the bundle's output build directory
.add(createSimpleDirectoryHandler(getBundleBuildDirectory(suite.testBundle)))
// Serves files from the out/web_tests/artifacts directory at the root (/) URL path.
.add(createSimpleDirectoryHandler(env.environment.webTestsArtifactsDir))
// Serves files from thes test set directory
.add(createSimpleDirectoryHandler(getTestSetDirectory(suite.testBundle.testSet)))
.add(_testImageListingHandler)
// Serves the initial HTML for the test.
@@ -112,22 +118,22 @@ class BrowserPlatform extends PlatformPlugin {
///
/// If [doUpdateScreenshotGoldens] is true updates screenshot golden files
/// instead of failing the test on screenshot mismatches.
static Future<BrowserPlatform> start({
static Future<BrowserPlatform> start(TestSuite suite, {
required BrowserEnvironment browserEnvironment,
required Renderer renderer,
required bool doUpdateScreenshotGoldens,
required SkiaGoldClient? skiaClient,
required String? overridePathToCanvasKit,
required bool isWasm,
required bool isVerbose,
}) async {
final shelf_io.IOServer server =
shelf_io.IOServer(await HttpMultiServer.loopback(0));
return BrowserPlatform._(
suite,
browserEnvironment: browserEnvironment,
renderer: renderer,
server: server,
isDebug: Configuration.current.pauseAfterLoad,
isWasm: isWasm,
isVerbose: isVerbose,
doUpdateScreenshotGoldens: doUpdateScreenshotGoldens,
packageConfig: await loadPackageConfigUri((await Isolate.packageConfig)!),
skiaClient: skiaClient,
@@ -135,12 +141,14 @@ class BrowserPlatform extends PlatformPlugin {
);
}
final TestSuite suite;
/// If true, runs the browser with a visible windows (i.e. not headless) and
/// pauses before running the tests to give the developer a chance to set
/// breakpoints in the code.
final bool isDebug;
final bool isWasm;
final bool isVerbose;
/// The underlying server.
final shelf.Server server;
@@ -148,12 +156,12 @@ class BrowserPlatform extends PlatformPlugin {
/// Provides the environment for the browser running tests.
final BrowserEnvironment browserEnvironment;
/// The renderer that tests are running under.
final Renderer renderer;
/// The URL for this server.
Uri get url => server.url.resolve('/');
bool get isWasm => suite.testBundle.compileConfig.compiler == Compiler.dart2wasm;
bool get needsCrossOriginIsolated => isWasm && suite.testBundle.compileConfig.renderer == Renderer.skwasm;
/// A [OneOffHandler] for servicing WebSocket connections for
/// [BrowserManager]s.
///
@@ -230,7 +238,7 @@ class BrowserPlatform extends PlatformPlugin {
}
final Directory testImageDirectory = Directory(p.join(
env.environment.webUiBuildDir.path,
env.environment.webTestsArtifactsDir.path,
'test_images',
));
@@ -252,7 +260,9 @@ class BrowserPlatform extends PlatformPlugin {
}
Future<shelf.Response> _fileNotFoundCatcher(shelf.Request request) async {
print('HTTP 404: ${request.url}');
if (isVerbose) {
print('HTTP 404: ${request.url}');
}
return shelf.Response.notFound('File not found');
}
@@ -383,10 +393,12 @@ class BrowserPlatform extends PlatformPlugin {
final String filename = requestData['filename'] as String;
if (!(await browserManager).supportsScreenshots) {
print(
'Skipping screenshot check for $filename. Current browser/OS '
'combination does not support screenshots.',
);
if (isVerbose) {
print(
'Skipping screenshot check for $filename. Current browser/OS '
'combination does not support screenshots.',
);
}
return shelf.Response.ok(json.encode('OK'));
}
@@ -419,6 +431,7 @@ class BrowserPlatform extends PlatformPlugin {
filename,
skiaClient,
isCanvaskitTest: isCanvaskitTest,
verbose: isVerbose,
);
}
@@ -443,52 +456,55 @@ class BrowserPlatform extends PlatformPlugin {
'.woff2': 'font/woff2',
};
/// A simple file handler that serves files whose URLs and paths are
/// Creates a simple file handler that serves files whose URLs and paths are
/// statically known.
///
/// This is used for trivial use-cases, such as `favicon.ico`, host pages, etc.
shelf.Response buildDirectoryHandler(shelf.Request request) {
File fileInBuild = File(p.join(
env.environment.webUiBuildDir.path,
getBuildDirForRenderer(renderer),
request.url.path,
));
// If we can't find the file in the renderer-specific `build` subdirectory,
// then it may be in the top-level `build` subdirectory.
if (!fileInBuild.existsSync()) {
fileInBuild = File(p.join(
env.environment.webUiBuildDir.path,
shelf.Handler createSimpleDirectoryHandler(Directory directory) {
return (shelf.Request request) {
final File fileInDirectory = File(p.join(
directory.path,
request.url.path,
));
if (!fileInDirectory.existsSync()) {
return shelf.Response.notFound('File not found: ${request.url.path}');
}
final String extension = p.extension(fileInDirectory.path);
final String? contentType = contentTypes[extension];
if (contentType == null) {
final String error =
'Failed to determine Content-Type for "${request.url.path}".';
stderr.writeln(error);
return shelf.Response.internalServerError(body: error);
}
final bool isScript =
extension == '.js' ||
extension == '.mjs' ||
extension == '.html';
return shelf.Response.ok(
fileInDirectory.readAsBytesSync(),
headers: <String, Object>{
HttpHeaders.contentTypeHeader: contentType,
if (isScript && needsCrossOriginIsolated)
...coopCoepHeaders,
},
);
};
}
String getCanvasKitVariant() {
switch (suite.runConfig.variant) {
case CanvasKitVariant.full:
return 'full';
case CanvasKitVariant.chromium:
return 'chromium';
case null:
return 'auto';
}
if (!fileInBuild.existsSync()) {
return shelf.Response.notFound('File not found: ${request.url.path}');
}
final String extension = p.extension(fileInBuild.path);
final String? contentType = contentTypes[extension];
if (contentType == null) {
final String error =
'Failed to determine Content-Type for "${request.url.path}".';
stderr.writeln(error);
return shelf.Response.internalServerError(body: error);
}
final bool needsCoopCoep =
extension == '.js' ||
extension == '.mjs' ||
extension == '.html';
return shelf.Response.ok(
fileInBuild.readAsBytesSync(),
headers: <String, Object>{
HttpHeaders.contentTypeHeader: contentType,
if (needsCoopCoep && isWasm && renderer == Renderer.skwasm)
...coopCoepHeaders,
},
);
}
/// Serves the HTML file that bootstraps the test.
@@ -498,12 +514,14 @@ class BrowserPlatform extends PlatformPlugin {
if (path.endsWith('.html')) {
final String test = '${p.withoutExtension(path)}.dart';
final bool linkSkwasm = suite.testBundle.compileConfig.renderer == Renderer.skwasm;
// Link to the Dart wrapper.
final String scriptBase = htmlEscape.convert(p.basename(test));
final String link = '<link rel="x-dart-test" href="$scriptBase"${renderer == Renderer.skwasm ? " skwasm" : ""}>';
final String link = '<link rel="x-dart-test" href="$scriptBase"${linkSkwasm ? " skwasm" : ""}>';
final String testRunner = isWasm ? '/test_dart2wasm.js' : 'packages/test/dart.js';
return shelf.Response.ok('''
<!DOCTYPE html>
<html>
@@ -512,7 +530,8 @@ class BrowserPlatform extends PlatformPlugin {
<meta name="assetBase" content="/">
<script>
window.flutterConfiguration = {
canvasKitBaseUrl: "/canvaskit/"
canvasKitBaseUrl: "/canvaskit/",
canvasKitVariant: "${getCanvasKitVariant()}",
};
</script>
$link
@@ -521,7 +540,7 @@ class BrowserPlatform extends PlatformPlugin {
</html>
''', headers: <String, String>{
'Content-Type': 'text/html',
if (isWasm && renderer == Renderer.skwasm)
if (needsCrossOriginIsolated)
...coopCoepHeaders
});
}
@@ -554,8 +573,7 @@ class BrowserPlatform extends PlatformPlugin {
}
_checkNotClosed();
final Uri suiteUrl = url.resolveUri(p.toUri('${p.withoutExtension(
p.relative(path, from: env.environment.webUiRootDir.path))}.html'));
final Uri suiteUrl = url.resolveUri(p.toUri('${p.withoutExtension(path)}.html'));
_checkNotClosed();
final BrowserManager? browserManager = await _startBrowserManager();
@@ -565,10 +583,10 @@ class BrowserPlatform extends PlatformPlugin {
}
_checkNotClosed();
final RunnerSuite suite =
final RunnerSuite runnerSuite =
await browserManager.load(path, suiteUrl, suiteConfig, message);
_checkNotClosed();
return suite;
return runnerSuite;
}
Future<BrowserManager?>? _browserManager;
@@ -598,9 +616,8 @@ class BrowserPlatform extends PlatformPlugin {
url: hostUrl,
future: completer.future,
packageConfig: packageConfig,
isWasm: isWasm,
debug: isDebug,
renderer: renderer,
sourceMapDirectory: isWasm ? null : getBundleBuildDirectory(suite.testBundle),
);
// Store null values for browsers that error out so we know not to load them
@@ -696,7 +713,7 @@ class BrowserManager {
/// Creates a new BrowserManager that communicates with the browser over
/// [webSocket].
BrowserManager._(this.packageConfig, this._browser, this._browserEnvironment,
this._renderer, this._isWasm, WebSocketChannel webSocket) {
this._sourceMapDirectory, WebSocketChannel webSocket) {
// The duration should be short enough that the debugging console is open as
// soon as the user is done setting breakpoints, but long enough that a test
// doing a lot of synchronous work doesn't trigger a false positive.
@@ -742,8 +759,8 @@ class BrowserManager {
/// The browser environment for this test.
final BrowserEnvironment _browserEnvironment;
/// The renderer for this test.
final Renderer _renderer;
/// The directory containing sourcemaps for test files
final Directory? _sourceMapDirectory;
/// The channel used to communicate with the browser.
///
@@ -768,9 +785,6 @@ class BrowserManager {
/// Whether the channel to the browser has closed.
bool _closed = false;
/// Whether we are running tests that have been compiled to WebAssembly.
final bool _isWasm;
/// The completer for [_BrowserEnvironment.displayPause].
///
/// This will be `null` as long as the browser isn't displaying a pause
@@ -812,8 +826,7 @@ class BrowserManager {
required Uri url,
required Future<WebSocketChannel> future,
required PackageConfig packageConfig,
required Renderer renderer,
required bool isWasm,
Directory? sourceMapDirectory,
bool debug = false,
}) async {
final Browser browser =
@@ -824,8 +837,7 @@ class BrowserManager {
future: future,
packageConfig: packageConfig,
browser: browser,
renderer: renderer,
isWasm: isWasm,
sourceMapDirectory: sourceMapDirectory,
debug: debug);
}
@@ -835,8 +847,7 @@ class BrowserManager {
required Future<WebSocketChannel> future,
required PackageConfig packageConfig,
required Browser browser,
required Renderer renderer,
required bool isWasm,
Directory? sourceMapDirectory,
bool debug = false,
}) {
final Completer<BrowserManager> completer = Completer<BrowserManager>();
@@ -858,7 +869,7 @@ class BrowserManager {
return;
}
completer.complete(BrowserManager._(
packageConfig, browser, browserEnvironment, renderer, isWasm, webSocket));
packageConfig, browser, browserEnvironment, sourceMapDirectory, webSocket));
}).catchError((Object error, StackTrace stackTrace) {
browser.close();
if (completer.isCompleted) {
@@ -942,7 +953,7 @@ class BrowserManager {
suiteChannel,
message);
if (_isWasm) {
if (_sourceMapDirectory == null) {
// We don't have mapping for wasm yet. But we should send a message
// to let the host page move forward.
controller!.channel('test.browser.mapper').sink.add(null);
@@ -951,8 +962,11 @@ class BrowserManager {
'${p.basename(path)}.browser_test.dart.js.map';
final String pathToTest = p.dirname(path);
final String mapPath = p.join(env.environment.webUiBuildDir.path,
getBuildDirForRenderer(_renderer), pathToTest, sourceMapFileName);
final String mapPath = p.join(
_sourceMapDirectory!.path,
pathToTest,
sourceMapFileName
);
final Map<String, Uri> packageMap = <String, Uri>{
for (Package p in packageConfig.packages) p.name: p.packageUriRoot

View File

@@ -10,9 +10,15 @@ import 'package:path/path.dart' as path;
import 'package:watcher/src/watch_event.dart';
import 'environment.dart';
import 'exceptions.dart';
import 'felt_config.dart';
import 'generate_builder_json.dart';
import 'pipeline.dart';
import 'steps/compile_tests_step.dart';
import 'steps/run_tests_step.dart';
import 'steps/compile_bundle_step.dart';
import 'steps/copy_artifacts_step.dart';
import 'steps/run_suite_step.dart';
import 'suite_filter.dart';
import 'utils.dart';
/// Runs tests.
@@ -25,6 +31,11 @@ class TestCommand extends Command<bool> with ArgUtils<bool> {
'opportunity to add breakpoints or inspect loaded code before '
'running the code.',
)
..addFlag(
'verbose',
abbr: 'v',
help: 'Enable verbose output.'
)
..addFlag(
'watch',
abbr: 'w',
@@ -32,16 +43,43 @@ class TestCommand extends Command<bool> with ArgUtils<bool> {
'made.',
)
..addFlag(
'use-system-flutter',
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.')
'list',
help:
'Lists the bundles that would be compiled and the suites that '
'will be run as part of this invocation, without actually '
'compiling or running them.'
)
..addFlag(
'generate-builder-json',
help:
'Generates JSON for the engine_v2 builders to build and copy all'
'artifacts, compile all test bundles, and run all test suites on'
'all platforms.'
)
..addFlag(
'compile',
help:
'Compile test bundles. If this is specified on its own, we will '
'only compile and not run the suites.'
)
..addFlag(
'run',
help:
'Run test suites. If this is specified on its own, we will only '
'run the suites and not compile the bundles.'
)
..addFlag(
'copy-artifacts',
help:
'Copy artifacts needed for test suites. If this is specified on '
'its own, we will only copy the artifacts and not compile or run'
'the tests bundles or suites.'
)
..addFlag(
'profile',
help:
'Use artifacts from the profile build instead of release.'
)
..addFlag(
'require-skia-gold',
help:
@@ -55,11 +93,29 @@ class TestCommand extends Command<bool> with ArgUtils<bool> {
'.dart_tool/goldens. Use this option to bulk-update all screenshots, '
'for example, when a new browser version affects pixels.',
)
..addOption(
..addMultiOption(
'browser',
defaultsTo: 'chrome',
help: 'An option to choose a browser to run the tests. By default '
'tests run in Chrome.',
help: 'Filter test suites by browser.',
)
..addMultiOption(
'compiler',
help: 'Filter test suites by compiler.',
)
..addMultiOption(
'renderer',
help: 'Filter test suites by renderer.',
)
..addMultiOption(
'canvaskit-variant',
help: 'Filter test suites by CanvasKit variant.',
)
..addMultiOption(
'suite',
help: 'Filter test suites by suite name.',
)
..addMultiOption(
'bundle',
help: 'Filter test suites by bundle name.',
)
..addFlag(
'fail-early',
@@ -77,11 +133,6 @@ class TestCommand extends Command<bool> with ArgUtils<bool> {
..addFlag(
'wasm',
help: 'Whether the test we are running are compiled to webassembly.'
)
..addFlag(
'use-local-canvaskit',
help: 'Optional. Whether or not to use the locally built version of '
'CanvasKit in the tests.',
);
}
@@ -93,9 +144,9 @@ class TestCommand extends Command<bool> with ArgUtils<bool> {
bool get isWatchMode => boolArg('watch');
bool get failEarly => boolArg('fail-early');
bool get isList => boolArg('list');
bool get isWasm => boolArg('wasm');
bool get failEarly => boolArg('fail-early');
/// Whether to start the browser in debug mode.
///
@@ -103,17 +154,10 @@ class TestCommand extends Command<bool> with ArgUtils<bool> {
/// 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;
bool get isVerbose => boolArg('verbose');
/// 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 browserName => stringArg('browser');
List<FilePath> get targetFiles => argResults!.rest.map((String t) => FilePath.fromCwd(t)).toList();
/// When running screenshot tests, require Skia Gold to be available and
/// reachable.
@@ -126,31 +170,219 @@ class TestCommand extends Command<bool> with ArgUtils<bool> {
/// Path to a CanvasKit build. Overrides the default CanvasKit.
String? get overridePathToCanvasKit => argResults!['canvaskit-path'] as String?;
/// Whether or not to use the locally built version of CanvasKit.
bool get useLocalCanvasKit => boolArg('use-local-canvaskit');
final FeltConfig config = FeltConfig.fromFile(
path.join(environment.webUiTestDir.path, 'felt_config.yaml')
);
BrowserSuiteFilter? makeBrowserFilter() {
final List<String>? browserArgs = argResults!['browser'] as List<String>?;
if (browserArgs == null || browserArgs.isEmpty) {
return null;
}
final Set<BrowserName> browserNames = Set<BrowserName>.from(browserArgs.map((String arg) => BrowserName.values.byName(arg)));
return BrowserSuiteFilter(allowList: browserNames);
}
CompilerFilter? makeCompilerFilter() {
final List<String>? compilerArgs = argResults!['compiler'] as List<String>?;
if (compilerArgs == null || compilerArgs.isEmpty) {
return null;
}
final Set<Compiler> compilers = Set<Compiler>.from(compilerArgs.map((String arg) => Compiler.values.byName(arg)));
return CompilerFilter(allowList: compilers);
}
RendererFilter? makeRendererFilter() {
final List<String>? rendererArgs = argResults!['renderer'] as List<String>?;
if (rendererArgs == null || rendererArgs.isEmpty) {
return null;
}
final Set<Renderer> renderers = Set<Renderer>.from(rendererArgs.map((String arg) => Renderer.values.byName(arg)));
return RendererFilter(allowList: renderers);
}
CanvasKitVariantFilter? makeCanvasKitVariantFilter() {
final List<String>? variantArgs = argResults!['canvaskit-variant'] as List<String>?;
if (variantArgs == null || variantArgs.isEmpty) {
return null;
}
final Set<CanvasKitVariant> variants = Set<CanvasKitVariant>.from(variantArgs.map((String arg) => CanvasKitVariant.values.byName(arg)));
return CanvasKitVariantFilter(allowList: variants);
}
SuiteNameFilter? makeSuiteNameFilter() {
final List<String>? suiteNameArgs = argResults!['suite'] as List<String>?;
if (suiteNameArgs == null || suiteNameArgs.isEmpty) {
return null;
}
final Iterable<String> allSuiteNames = config.testSuites.map((TestSuite suite) => suite.name);
for (final String suiteName in suiteNameArgs) {
if (!allSuiteNames.contains(suiteName)) {
throw ToolExit('No suite found named $suiteName');
}
}
return SuiteNameFilter(allowList: Set<String>.from(suiteNameArgs));
}
BundleNameFilter? makeBundleNameFilter() {
final List<String>? bundleNameArgs = argResults!['bundle'] as List<String>?;
if (bundleNameArgs == null || bundleNameArgs.isEmpty) {
return null;
}
final Iterable<String> allBundleNames = config.testSuites.map(
(TestSuite suite) => suite.testBundle.name
);
for (final String bundleName in bundleNameArgs) {
if (!allBundleNames.contains(bundleName)) {
throw ToolExit('No bundle found named $bundleName');
}
}
return BundleNameFilter(allowList: Set<String>.from(bundleNameArgs));
}
FileFilter? makeFileFilter() {
final List<FilePath> tests = targetFiles;
if (tests.isEmpty) {
return null;
}
final Set<String> bundleNames = <String>{};
for (final FilePath testPath in tests) {
if (!io.File(testPath.absolute).existsSync()) {
throw ToolExit('Test path not found: $testPath');
}
bool bundleFound = false;
for (final TestBundle bundle in config.testBundles) {
final String testSetPath = getTestSetDirectory(bundle.testSet).path;
if (path.isWithin(testSetPath, testPath.absolute)) {
bundleFound = true;
bundleNames.add(bundle.name);
}
}
if (!bundleFound) {
throw ToolExit('Test path not in any known test bundle: $testPath');
}
}
return FileFilter(allowList: bundleNames);
}
List<SuiteFilter> get suiteFilters {
final BrowserSuiteFilter? browserFilter = makeBrowserFilter();
final CompilerFilter? compilerFilter = makeCompilerFilter();
final RendererFilter? rendererFilter = makeRendererFilter();
final CanvasKitVariantFilter? canvaskitVariantFilter = makeCanvasKitVariantFilter();
final SuiteNameFilter? suiteNameFilter = makeSuiteNameFilter();
final BundleNameFilter? bundleNameFilter = makeBundleNameFilter();
final FileFilter? fileFilter = makeFileFilter();
return <SuiteFilter>[
PlatformBrowserFilter(),
if (browserFilter != null) browserFilter,
if (compilerFilter != null) compilerFilter,
if (rendererFilter != null) rendererFilter,
if (canvaskitVariantFilter != null) canvaskitVariantFilter,
if (suiteNameFilter != null) suiteNameFilter,
if (bundleNameFilter != null) bundleNameFilter,
if (fileFilter != null) fileFilter,
];
}
List<TestSuite> _filterTestSuites() {
if (isVerbose) {
print('Filtering suites...');
}
final List<SuiteFilter> filters = suiteFilters;
final List<TestSuite> filteredSuites = config.testSuites.where((TestSuite suite) {
for (final SuiteFilter filter in filters) {
final SuiteFilterResult result = filter.filterSuite(suite);
if (!result.isAccepted) {
if (isVerbose) {
print(' ${suite.name.ansiCyan} rejected for reason: ${result.rejectReason}');
}
return false;
}
}
return true;
}).toList();
return filteredSuites;
}
List<TestBundle> _filterBundlesForSuites(List<TestSuite> suites) {
final Set<TestBundle> seenBundles =
Set<TestBundle>.from(suites.map((TestSuite suite) => suite.testBundle));
return config.testBundles.where((TestBundle bundle) => seenBundles.contains(bundle)).toList();
}
ArtifactDependencies _artifactsForSuites(List<TestSuite> suites) {
return suites.fold(ArtifactDependencies.none(),
(ArtifactDependencies deps, TestSuite suite) => deps | suite.artifactDependencies);
}
@override
Future<bool> run() async {
final List<FilePath> testFiles = runAllTests
? findAllTests()
: targetFiles;
final List<TestSuite> filteredSuites = _filterTestSuites();
final List<TestBundle> bundles = _filterBundlesForSuites(filteredSuites);
final ArtifactDependencies artifacts = _artifactsForSuites(filteredSuites);
if (boolArg('generate-builder-json')) {
print(generateBuilderJson(config));
return true;
}
if (isList || isVerbose) {
print('Suites:');
for (final TestSuite suite in filteredSuites) {
print(' ${suite.name.ansiCyan}');
}
print('Bundles:');
for (final TestBundle bundle in bundles) {
print(' ${bundle.name.ansiMagenta}');
}
print('Artifacts:');
if (artifacts.canvasKit) {
print(' canvaskit'.ansiYellow);
}
if (artifacts.canvasKitChromium) {
print(' canvaskit_chromium'.ansiYellow);
}
if (artifacts.skwasm) {
print(' skwasm'.ansiYellow);
}
}
if (isList) {
return true;
}
bool shouldRun = boolArg('run');
bool shouldCompile = boolArg('compile');
bool shouldCopyArtifacts = boolArg('copy-artifacts');
if (!shouldRun && !shouldCompile && !shouldCopyArtifacts) {
// If none of these is specified, we should assume we need to do all of them.
shouldRun = true;
shouldCompile = true;
shouldCopyArtifacts = true;
}
final Set<FilePath>? testFiles = targetFiles.isEmpty ? null : Set<FilePath>.from(targetFiles);
final Pipeline testPipeline = Pipeline(steps: <PipelineStep>[
if (isWatchMode) ClearTerminalScreenStep(),
CompileTestsStep(
testFiles: testFiles,
useLocalCanvasKit: useLocalCanvasKit,
isWasm: isWasm
),
RunTestsStep(
browserName: browserName,
testFiles: testFiles,
isDebug: isDebug,
isWasm: isWasm,
doUpdateScreenshotGoldens: doUpdateScreenshotGoldens,
requireSkiaGold: requireSkiaGold,
overridePathToCanvasKit: overridePathToCanvasKit,
),
if (shouldCopyArtifacts) CopyArtifactsStep(artifacts, isProfile: boolArg('profile')),
if (shouldCompile)
for (final TestBundle bundle in bundles)
CompileBundleStep(
bundle: bundle,
isVerbose: isVerbose,
testFiles: testFiles,
),
if (shouldRun)
for (final TestSuite suite in filteredSuites)
RunSuiteStep(
suite,
isDebug: isDebug,
isVerbose: isVerbose,
doUpdateScreenshotGoldens: doUpdateScreenshotGoldens,
requireSkiaGold: requireSkiaGold,
overridePathToCanvasKit: overridePathToCanvasKit,
testFiles: testFiles,
),
]);
try {

View File

@@ -12,12 +12,15 @@ import 'package:path/path.dart' as path;
import 'environment.dart';
import 'exceptions.dart';
import 'felt_config.dart';
class FilePath {
FilePath.fromCwd(String relativePath)
: _absolutePath = path.absolute(relativePath);
FilePath.fromWebUi(String relativePath)
: _absolutePath = path.join(environment.webUiRootDir.path, relativePath);
FilePath.fromTestSet(TestSet testSet, String relativePath)
: _absolutePath = path.join(getTestSetDirectory(testSet).path, relativePath);
final String _absolutePath;
@@ -368,82 +371,43 @@ Future<void> cleanup() async {
}
}
/// Scans the test/ directory for test files and returns them.
List<FilePath> findAllTests() {
return environment.webUiTestDir
.listSync(recursive: true)
.whereType<io.File>()
.where((io.File f) => f.path.endsWith('_test.dart'))
.map<FilePath>((io.File f) => FilePath.fromWebUi(
path.relative(f.path, from: environment.webUiRootDir.path)))
.toList();
io.Directory getTestSetDirectory(TestSet testSet) {
return io.Directory(
path.join(
environment.webUiTestDir.path,
testSet.directory,
)
);
}
/// The renderer used to run the test.
enum Renderer {
html,
canvasKit,
skwasm,
io.Directory getBundleBuildDirectory(TestBundle bundle) {
return io.Directory(
path.join(
environment.webUiBuildDir.path,
'test_bundles',
bundle.name,
)
);
}
/// The `FilePath`s for all the tests, organized by renderer.
class TestsByRenderer {
TestsByRenderer(this.htmlTests, this.canvasKitTests, this.skwasmTests);
extension AnsiColors on String {
static bool shouldEscape = io.stdout.hasTerminal && io.stdout.supportsAnsiEscapes;
/// Tests which should be run with the HTML renderer.
final List<FilePath> htmlTests;
static const String _noColorCode = '\u001b[39m';
/// Tests which should be run with the CanvasKit renderer.
final List<FilePath> canvasKitTests;
String _wrapText(String prefix, String suffix) => shouldEscape
? '$prefix$this$suffix' : this;
/// Tests which should be run with the Skwasm renderer.
final List<FilePath> skwasmTests;
String _colorText(String colorCode) => _wrapText(colorCode, _noColorCode);
/// The total number of targets to compile.
///
/// The number of uiTests is doubled since they are compiled twice: once for
/// the HTML renderer and once for the CanvasKit renderer.
int get numTargetsToCompile => htmlTests.length + canvasKitTests.length + skwasmTests.length;
}
/// Given a list of test files, organizes them by which renderer should run them.
TestsByRenderer sortTestsByRenderer(List<FilePath> testFiles, bool forWasm) {
final List<FilePath> htmlTargets = <FilePath>[];
final List<FilePath> canvasKitTargets = <FilePath>[];
final List<FilePath> skwasmTargets = <FilePath>[];
final String canvasKitTestDirectory =
path.join(environment.webUiTestDir.path, 'canvaskit');
final String skwasmTestDirectory =
path.join(environment.webUiTestDir.path, 'skwasm');
final String uiTestDirectory =
path.join(environment.webUiTestDir.path, 'ui');
for (final FilePath testFile in testFiles) {
if (path.isWithin(canvasKitTestDirectory, testFile.absolute)) {
canvasKitTargets.add(testFile);
} else if (path.isWithin(skwasmTestDirectory, testFile.absolute)) {
skwasmTargets.add(testFile);
} else if (path.isWithin(uiTestDirectory, testFile.absolute)) {
htmlTargets.add(testFile);
canvasKitTargets.add(testFile);
if (forWasm) {
// Only add these tests in wasm mode, since JS mode has a stub renderer.
skwasmTargets.add(testFile);
}
} else {
htmlTargets.add(testFile);
}
}
return TestsByRenderer(htmlTargets, canvasKitTargets, skwasmTargets);
}
/// The build directory to compile a test into given the renderer.
String getBuildDirForRenderer(Renderer renderer) {
switch (renderer) {
case Renderer.html:
return 'html_tests';
case Renderer.canvasKit:
return 'canvaskit_tests';
case Renderer.skwasm:
return 'skwasm_tests';
}
String get ansiBlack => _colorText('\u001b[30m');
String get ansiRed => _colorText('\u001b[31m');
String get ansiGreen => _colorText('\u001b[32m');
String get ansiYellow => _colorText('\u001b[33m');
String get ansiBlue => _colorText('\u001b[34m');
String get ansiMagenta => _colorText('\u001b[35m');
String get ansiCyan => _colorText('\u001b[36m');
String get ansiWhite => _colorText('\u001b[37m');
String get ansiBold => _wrapText('\u001b[1m', '\u001b[0m');
}

View File

@@ -0,0 +1,50 @@
...............................................................................
# Flutter Web Engine Test Suites
The flutter engine unit tests can be run with a number of different
configuration options that affect both compile time and run time. The
permutations of these options are specified in the `felt_config.yaml` file that
is colocated with this README. Here is an overview of the way the test suite
configurations are structured:
## `compile-configs`
Specifies how the tests should be compiled. Each compile config specifies the
following:
* `name` - The name of the compile configuration.
* `compiler` - What compiler is used to compile the tests. Currently we support
`dart2js` and `dart2wasm` as values.
* `renderer` - Which renderer to use when compiling the tests. Currently we
support `html`, `canvaskit`, and `skwasm`.
## `test-sets`
A group of files that contain unit tests. Each test set specifies the following:
* `name` - The name of the test set.
* `directory` - The name of the directory under `flutter/lib/web_ui/test` that
contains all the test files.
## `test-bundles`
Specifies a group of tests and a compile configuration of those tests. The output
of the test bundles appears in `flutter/lib/web_ui/build/test_bundles/<name>`
where `<name>` is replaced by the name of the bundle. Each test bundle may be used
by multiple test suites. Each test bundle specifies the following:
* `name` - The name of the test bundle.
* `test-set` - The name of the test set that contains the tests to be compiled.
* `compile-config` - The name of the compile configuration to use.
## `run-configs`
Specifies the test environment that should be provided to a unit test. Each run
config specifies the following:
* `name` - Name of the run configuration.
* `browser` - The browser with which to run the tests. Valid values for this are
`chrome`, `firefox`, `safari` or `edge`.
* `canvaskit-variant` - An optionally supplied argument that forces the tests to
use a particular variant of CanvasKit, either `full` or `chromium`. If none
is specified, the engine will select the variant based on its normal selection
logic.
## `test-suites`
This is a fully specified run of a group of unit tests. They specify the following:
* `name` - Name of the test suite.
* `test-bundle` - Which compiled test bundle to use when running the suite.
* `run-config` - Which run configuration to use when runnin the tests.
* `artifact-deps` - Which gn/ninja build artifacts are needed to run the suite.
Valid values are `canvaskit`, `canvaskit_chromium` or `skwasm`.

View File

@@ -13,7 +13,7 @@ import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
import '../matchers.dart';
import '../common/matchers.dart';
import 'common.dart';
import 'test_data.dart';

View File

@@ -6,7 +6,7 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import '../matchers.dart';
import '../common/matchers.dart';
import 'canvaskit_api_test.dart';
final bool isBlink = browserEngine == BrowserEngine.blink;

View File

@@ -5,7 +5,7 @@
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import '../frame_timings_common.dart';
import '../common/frame_timings_common.dart';
import 'common.dart';
void main() {

View File

@@ -11,7 +11,7 @@ import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
import '../matchers.dart';
import '../common/matchers.dart';
import 'common.dart';
import 'test_data.dart';

View File

@@ -12,8 +12,8 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import '../matchers.dart';
import '../spy.dart';
import '../common/matchers.dart';
import '../common/spy.dart';
import 'common.dart';
void main() {

View File

@@ -8,7 +8,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'mock_engine_canvas.dart';
import '../common/mock_engine_canvas.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -10,7 +10,7 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import '../matchers.dart';
import '../common/matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -13,7 +13,7 @@ import 'package:test/test.dart';
import 'package:ui/ui.dart';
import 'matchers.dart';
import '../common/matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -16,7 +16,7 @@ import 'package:ui/src/engine/navigation.dart';
import 'package:ui/src/engine/services.dart';
import 'package:ui/src/engine/test_embedding.dart';
import '../spy.dart';
import '../common/spy.dart';
Map<String, dynamic> _wrapOriginState(dynamic state) {
return <String, dynamic>{'origin': true, 'state': state};

View File

@@ -9,7 +9,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'keyboard_test_common.dart';
import '../common/keyboard_test_common.dart';
const int kLocationStandard = 0;
const int kLocationLeft = 1;

View File

@@ -7,7 +7,7 @@ import 'package:test/test.dart';
import 'package:ui/ui.dart';
import 'matchers.dart';
import '../common/matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -6,7 +6,7 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import '../../matchers.dart';
import '../../common/matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -9,7 +9,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../keyboard_converter_test.dart';
import 'keyboard_converter_test.dart';
const int _kNoButtonChange = -1;
const PointerSupportDetector _defaultSupportDetector = PointerSupportDetector();

View File

@@ -8,7 +8,7 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import '../spy.dart';
import '../common/spy.dart';
@JS('window._flutter_internal_on_benchmark')
external set _onBenchmark (JSAny? object);

View File

@@ -7,8 +7,8 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import '../common/mock_engine_canvas.dart';
import '../html/screenshot.dart';
import '../mock_engine_canvas.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -10,8 +10,8 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart' hide window;
import 'package:ui/ui.dart' as ui;
import 'engine/history_test.dart';
import 'matchers.dart';
import '../common/matchers.dart';
import 'history_test.dart';
const MethodCodec codec = JSONMethodCodec();

View File

@@ -14,7 +14,7 @@ import 'package:ui/src/engine/util.dart';
import 'package:ui/src/engine/vector_math.dart';
import 'package:ui/ui.dart' as ui;
import '../../matchers.dart';
import '../../common/matchers.dart';
/// Gets the DOM host where the Flutter app is being rendered.
///

View File

@@ -6,7 +6,7 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import '../../frame_timings_common.dart';
import '../../common/frame_timings_common.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -9,7 +9,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import '../../matchers.dart';
import '../../common/matchers.dart';
const MethodCodec codec = StandardMethodCodec();
final EngineSingletonFlutterWindow window = EngineSingletonFlutterWindow(0, EnginePlatformDispatcher.instance);

View File

@@ -13,7 +13,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../../matchers.dart';
import '../../common/matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -21,7 +21,7 @@ import 'package:ui/src/engine/text_editing/text_editing.dart';
import 'package:ui/src/engine/util.dart';
import 'package:ui/src/engine/vector_math.dart';
import 'spy.dart';
import '../common/spy.dart';
/// The `keyCode` of the "Enter" key.
const int _kReturnKeyCode = 13;

View File

@@ -0,0 +1,270 @@
# See the `README.md` in this directory for documentation on the structure of
# this file.
compile-configs:
- name: dart2js-html
compiler: dart2js
renderer: html
- name: dart2js-canvaskit
compiler: dart2js
renderer: canvaskit
- name: dart2js-skwasm
compiler: dart2js
renderer: skwasm
- name: dart2wasm-html
compiler: dart2wasm
renderer: html
- name: dart2wasm-canvaskit
compiler: dart2wasm
renderer: canvaskit
- name: dart2wasm-skwasm
compiler: dart2wasm
renderer: skwasm
test-sets:
# Tests for non-renderer logic
- name: engine
directory: engine
# Tests for canvaskit-renderer-specific functionality
- name: canvaskit
directory: canvaskit
# Tests for html-renderer-specific functionality
- name: html
directory: html
# Tests for renderer functionality that can be run on any renderer
- name: ui
directory: ui
# This just has a single test that makes sure the skwasm stub renderer is
# included when compiling to JS
- name: skwasm_stub
directory: skwasm_stub
test-bundles:
- name: dart2js-html-engine
test-set: engine
compile-config: dart2js-html
- name: dart2js-html-html
test-set: html
compile-config: dart2js-html
- name: dart2js-html-ui
test-set: ui
compile-config: dart2js-html
- name: dart2js-canvaskit-canvaskit
test-set: canvaskit
compile-config: dart2js-canvaskit
- name: dart2js-canvaskit-ui
test-set: ui
compile-config: dart2js-canvaskit
- name: dart2js-skwasm-skwasm_stub
test-set: skwasm_stub
compile-config: dart2js-skwasm
- name: dart2wasm-html-engine
test-set: engine
compile-config: dart2wasm-html
- name: dart2wasm-html-html
test-set: html
compile-config: dart2wasm-html
- name: dart2wasm-html-ui
test-set: ui
compile-config: dart2wasm-html
- name: dart2wasm-canvaskit-canvaskit
test-set: canvaskit
compile-config: dart2wasm-canvaskit
- name: dart2wasm-canvaskit-ui
test-set: ui
compile-config: dart2wasm-canvaskit
- name: dart2wasm-skwasm-ui
test-set: ui
compile-config: dart2wasm-skwasm
run-configs:
- name: chrome
browser: chrome
canvaskit-variant: chromium
- name: chrome-full
browser: chrome
canvaskit-variant: full
- name: edge
browser: edge
canvaskit-variant: chromium
- name: edge-full
browser: edge
canvaskit-variant: full
- name: firefox
browser: firefox
- name: safari
browser: safari
test-suites:
- name: chrome-dart2js-html-engine
test-bundle: dart2js-html-engine
run-config: chrome
- name: chrome-dart2js-html-html
test-bundle: dart2js-html-html
run-config: chrome
- name: chrome-dart2js-html-ui
test-bundle: dart2js-html-ui
run-config: chrome
- name: chrome-dart2js-canvaskit-canvaskit
test-bundle: dart2js-canvaskit-canvaskit
run-config: chrome
artifact-deps: [ canvaskit_chromium ]
- name: chrome-dart2js-canvaskit-ui
test-bundle: dart2js-canvaskit-ui
run-config: chrome
artifact-deps: [ canvaskit_chromium ]
- name: chrome-dart2js-skwasm-skwasm_stub
test-bundle: dart2js-skwasm-skwasm_stub
run-config: chrome
- name: chrome-full-dart2js-canvaskit-canvaskit
test-bundle: dart2js-canvaskit-canvaskit
run-config: chrome-full
artifact-deps: [ canvaskit ]
- name: chrome-full-dart2js-canvaskit-ui
test-bundle: dart2js-canvaskit-ui
run-config: chrome-full
artifact-deps: [ canvaskit ]
- name: edge-dart2js-html-engine
test-bundle: dart2js-html-engine
run-config: edge
- name: edge-dart2js-html-html
test-bundle: dart2js-html-html
run-config: edge
- name: edge-dart2js-html-ui
test-bundle: dart2js-html-ui
run-config: edge
- name: edge-dart2js-canvaskit-canvaskit
test-bundle: dart2js-canvaskit-canvaskit
run-config: edge
artifact-deps: [ canvaskit_chromium ]
- name: edge-dart2js-canvaskit-ui
test-bundle: dart2js-canvaskit-ui
run-config: edge
artifact-deps: [ canvaskit_chromium ]
- name: edge-full-dart2js-canvaskit-canvaskit
test-bundle: dart2js-canvaskit-canvaskit
run-config: edge-full
artifact-deps: [ canvaskit ]
- name: edge-full-dart2js-canvaskit-ui
test-bundle: dart2js-canvaskit-ui
run-config: edge-full
artifact-deps: [ canvaskit ]
- name: firefox-dart2js-html-engine
test-bundle: dart2js-html-engine
run-config: firefox
- name: firefox-dart2js-html-html
test-bundle: dart2js-html-html
run-config: firefox
- name: firefox-dart2js-html-ui
test-bundle: dart2js-html-ui
run-config: firefox
- name: firefox-dart2js-canvaskit-canvaskit
test-bundle: dart2js-canvaskit-canvaskit
run-config: firefox
artifact-deps: [ canvaskit ]
- name: firefox-dart2js-canvaskit-ui
test-bundle: dart2js-canvaskit-ui
run-config: firefox
artifact-deps: [ canvaskit ]
- name: safari-dart2js-html-engine
test-bundle: dart2js-html-engine
run-config: safari
- name: safari-dart2js-html-html
test-bundle: dart2js-html-html
run-config: safari
- name: safari-dart2js-html-ui
test-bundle: dart2js-html-ui
run-config: safari
- name: safari-dart2js-canvaskit-canvaskit
test-bundle: dart2js-canvaskit-canvaskit
run-config: safari
artifact-deps: [ canvaskit ]
- name: safari-dart2js-canvaskit-ui
test-bundle: dart2js-canvaskit-ui
run-config: safari
artifact-deps: [ canvaskit ]
- name: chrome-dart2wasm-html-engine
test-bundle: dart2wasm-html-engine
run-config: chrome
- name: chrome-dart2wasm-html-html
test-bundle: dart2wasm-html-html
run-config: chrome
- name: chrome-dart2wasm-html-ui
test-bundle: dart2wasm-html-ui
run-config: chrome
- name: chrome-dart2wasm-canvaskit-canvaskit
test-bundle: dart2wasm-canvaskit-canvaskit
run-config: chrome
artifact-deps: [ canvaskit_chromium ]
- name: chrome-dart2wasm-canvaskit-ui
test-bundle: dart2wasm-canvaskit-ui
run-config: chrome
artifact-deps: [ canvaskit_chromium ]
- name: chrome-dart2wasm-skwasm-ui
test-bundle: dart2wasm-skwasm-ui
run-config: chrome
artifact-deps: [ skwasm ]
- name: chrome-full-dart2wasm-canvaskit-canvaskit
test-bundle: dart2wasm-canvaskit-canvaskit
run-config: chrome-full
artifact-deps: [ canvaskit ]
- name: chrome-full-dart2wasm-canvaskit-ui
test-bundle: dart2wasm-canvaskit-ui
run-config: chrome-full
artifact-deps: [ canvaskit ]

View File

@@ -10,7 +10,7 @@ import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
import '../../matchers.dart';
import '../../common/matchers.dart';
const ui.Rect region = ui.Rect.fromLTWH(0, 0, 500, 100);

View File

@@ -7,7 +7,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import '../matchers.dart';
import '../common/matchers.dart';
import 'screenshot.dart';
void main() {

View File

@@ -10,7 +10,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide window;
import 'matchers.dart';
import '../common/matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -11,7 +11,7 @@ import 'package:ui/src/engine.dart' hide ColorSpace;
import 'package:ui/ui.dart' hide TextStyle;
import 'package:web_engine_tester/golden_tester.dart';
import '../matchers.dart';
import '../common/matchers.dart';
import 'screenshot.dart';
void main() {

View File

@@ -11,8 +11,8 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'html/paragraph/helper.dart';
import 'matchers.dart';
import '../common/matchers.dart';
import 'paragraph/helper.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);

View File

@@ -8,7 +8,6 @@ import 'dart:async';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/browser_detection.dart';
import 'package:ui/src/engine/renderer.dart';
import 'package:ui/src/engine/skwasm/skwasm_stub/renderer.dart';
@@ -23,8 +22,6 @@ Future<void> testMain() async {
expect(() {
renderer.initialize();
}, throwsUnimplementedError);
}, skip: isWasm);
// This test is specifically designed for the JS case, to make sure we
// compile to the skwasm stub renderer.
});
});
}

View File

@@ -10,7 +10,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../matchers.dart';
import '../common/matchers.dart';
import 'utils.dart';
void main() {
@@ -18,7 +18,7 @@ void main() {
}
Future<void> testMain() async {
setUpUiTest();
await setUpUiTest();
final bool deviceClipRoundsOut = renderer is! HtmlRenderer;
runCanvasTests(deviceClipRoundsOut: deviceClipRoundsOut);

View File

@@ -16,8 +16,8 @@ class NotAColor extends Color {
const NotAColor(super.value);
}
void testMain() {
setUpUiTest();
Future<void> testMain() async {
await setUpUiTest();
test('color accessors should work', () {
const Color foo = Color(0x12345678);

View File

@@ -7,11 +7,15 @@ import 'package:test/test.dart';
import 'package:ui/ui.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
Future<void> testMain() async {
await setUpUiTest();
test('Gradient.radial with no focal point', () {
expect(
Gradient.radial(
@@ -22,7 +26,7 @@ void testMain() {
TileMode.mirror),
isNotNull,
);
});
}, skip: isSkwasm);
// this is just a radial gradient, focal point is discarded.
test('radial center and focal == Offset.zero and focalRadius == 0.0 is ok',
@@ -38,7 +42,7 @@ void testMain() {
Offset.zero,
),
isNotNull);
});
}, skip: isSkwasm);
test('radial center != focal and focalRadius == 0.0 is ok', () {
expect(
@@ -52,7 +56,7 @@ void testMain() {
const Offset(2.0, 2.0),
),
isNotNull);
});
}, skip: isSkwasm);
// this would result in div/0 on skia side.
test('radial center and focal == Offset.zero and focalRadius != 0.0 assert',
@@ -70,5 +74,5 @@ void testMain() {
),
throwsA(const TypeMatcher<AssertionError>()),
);
});
}, skip: isSkwasm);
}

View File

@@ -6,12 +6,14 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/ui.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
await webOnlyInitializePlatform();
await setUpUiTest();
test('Should be able to build and layout a paragraph', () {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle());
@@ -22,14 +24,7 @@ Future<void> testMain() async {
paragraph.layout(const ParagraphConstraints(width: 800.0));
expect(paragraph.width, isNonZero);
expect(paragraph.height, isNonZero);
});
test('pushStyle should not segfault after build()', () {
final ParagraphBuilder paragraphBuilder =
ParagraphBuilder(ParagraphStyle());
paragraphBuilder.build();
paragraphBuilder.pushStyle(TextStyle());
});
}, skip: isSkwasm);
test('the presence of foreground style should not throw', () {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle());
@@ -39,5 +34,5 @@ Future<void> testMain() async {
builder.addText('hi');
expect(() => builder.build(), returnsNormally);
});
}, skip: isSkwasm);
}

View File

@@ -8,7 +8,7 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/ui.dart';
import '../matchers.dart';
import '../common/matchers.dart';
import 'utils.dart';
const double kTolerance = 0.1;
@@ -17,8 +17,8 @@ void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
setUpUiTest();
Future<void> testMain() async {
await setUpUiTest();
group('PathMetric length', () {
test('empty path', () {
final Path path = Path();

View File

@@ -14,8 +14,8 @@ void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
setUpUiTest();
Future<void> testMain() async {
await setUpUiTest();
test('path getBounds', () {
const Rect r = Rect.fromLTRB(1.0, 3.0, 5.0, 7.0);
final Path p = Path()..addRect(r);

View File

@@ -13,7 +13,7 @@ void main() {
}
Future<void> testMain() async {
setUpUiTest();
await setUpUiTest();
test('Picture construction invokes onCreate once', () async {
int onCreateInvokedCount = 0;

View File

@@ -12,8 +12,8 @@ void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
setUpUiTest();
Future<void> testMain() async {
await setUpUiTest();
test('rect accessors', () {
const Rect r = Rect.fromLTRB(1.0, 3.0, 5.0, 7.0);
expect(r.left, equals(1.0));

View File

@@ -12,8 +12,8 @@ void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
setUpUiTest();
Future<void> testMain() async {
await setUpUiTest();
test('RRect.contains()', () {
final RRect rrect = RRect.fromRectAndCorners(
const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),

View File

@@ -13,8 +13,8 @@ void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
setUpUiTest();
Future<void> testMain() async {
await setUpUiTest();
const MethodCodec codec = JSONMethodCodec();

View File

@@ -3,13 +3,16 @@
// found in the LICENSE file.
import 'package:ui/src/engine.dart';
import 'package:ui/src/engine/skwasm/skwasm_stub.dart' if (dart.library.ffi) 'package:ui/src/engine/skwasm/skwasm_impl.dart';
import '../canvaskit/common.dart';
/// Initializes the renderer for this test.
void setUpUiTest() {
if (renderer is CanvasKitRenderer) {
Future<void> setUpUiTest() async {
if (isCanvasKit) {
setUpCanvasKitTest();
} else if (isHtml) {
await initializeEngine();
}
}
@@ -18,3 +21,5 @@ bool get isCanvasKit => renderer is CanvasKitRenderer;
/// Returns [true] if this test is running in the HTML renderer.
bool get isHtml => renderer is HtmlRenderer;
bool get isSkwasm => renderer is SkwasmRenderer;

View File

@@ -24,6 +24,7 @@ Future<String> compareImage(
String filename,
SkiaGoldClient? skiaClient, {
required bool isCanvaskitTest,
required bool verbose,
}) async {
if (skiaClient == null) {
return 'OK';
@@ -69,12 +70,13 @@ Future<String> compareImage(
// At the moment, we don't support local screenshot testing because we use
// Skia Gold to handle our screenshots and diffing. In the future, we might
// implement local screenshot testing if there's a need.
print('Screenshot generated: file://$screenshotPath'); // ignore: avoid_print
return 'OK';
}
// TODO(mdebbar): Use the Gold tool to locally diff the golden.
if (verbose) {
print('Screenshot generated: file://$screenshotPath'); // ignore: avoid_print
}
return 'OK';
}