Felt add integration (flutter/engine#17190)

* adding arguments to felt for running different type of tests

* adding test type enums to felt

* more changes to felt for testing branches

* more additions to code

* more changes to felt

* adding code for cloning web_drivers

* validating test directories. running the integration tests. update the readme file partially

* Removing todo lists used for development

* addressing reviewers comments

* remove unused imports

* addressing more reviewer comments

* addressing more reviewer comments

* addressing reviewer comments.

* addressing reviewer comments.

* fixing typos

* using chromedriverinstaller as a library. Fixing the commit number used from web_installers repo

* clean drivers directory after tests finish

* addressing more reviewer comments

* throwing all critical exceptions instead of logging and returning boolean flags

* adding todos for items we can do for making felt easier to use for local development

* do not run integration tests on CIs. Added an issue to the TODO.

* changing todo's with issues.
This commit is contained in:
Nurhan Turgut 2020-03-20 19:24:13 -07:00 committed by GitHub
parent 5c0f8a3da9
commit 9f6ffe557c
9 changed files with 532 additions and 26 deletions

View File

@ -31,12 +31,24 @@ felt build [-w] -j 100
If you are a Google employee, you can use an internal instance of Goma to parallelize your builds. Because Goma compiles code on remote servers, this option is effective even on low-powered laptops.
## Running web engine tests
To run all tests on Chrome:
To run all tests on Chrome. This will run both integration tests and the unit tests:
```
felt test
```
To run unit tests only:
```
felt test --unit-tests-only
```
To run integration tests only. For now these tests are only available on Chrome Desktop browsers.
```
felt test --integration-tests-only
```
To run tests on Firefox (this will work only on a Linux device):
```
@ -55,7 +67,7 @@ To run tests on Safari use the following command. It works on MacOS devices and
felt test --browser=safari
```
To run tests on Windows Edge use the following command. It works on Windows devices and it uses the Edge installed on the OS.
To run tests on Windows Edge use the following command. It works on Windows devices and it uses the Edge installed on the OS.
```
felt_windows.bat test --browser=edge

View File

@ -27,6 +27,15 @@ class BrowserInstallerException implements Exception {
String toString() => message;
}
class DriverException implements Exception {
DriverException(this.message);
final String message;
@override
String toString() => message;
}
abstract class PlatformBinding {
static PlatformBinding get instance {
if (_instance == null) {
@ -77,11 +86,10 @@ class _WindowsBinding implements PlatformBinding {
@override
String getFirefoxDownloadUrl(String version) =>
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/win64/en-US/'
'${getFirefoxDownloadFilename(version)}';
'${getFirefoxDownloadFilename(version)}';
@override
String getFirefoxDownloadFilename(String version) =>
'firefox-${version}.exe';
String getFirefoxDownloadFilename(String version) => 'firefox-${version}.exe';
@override
String getFirefoxExecutablePath(io.Directory versionDir) =>
@ -117,7 +125,7 @@ class _LinuxBinding implements PlatformBinding {
@override
String getFirefoxDownloadUrl(String version) =>
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/'
'${getFirefoxDownloadFilename(version)}';
'${getFirefoxDownloadFilename(version)}';
@override
String getFirefoxDownloadFilename(String version) =>
@ -161,16 +169,15 @@ class _MacBinding implements PlatformBinding {
@override
String getFirefoxDownloadUrl(String version) =>
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/'
'${getFirefoxDownloadFilename(version)}';
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/'
'${getFirefoxDownloadFilename(version)}';
@override
String getFirefoxDownloadFilename(String version) =>
'Firefox ${version}.dmg';
String getFirefoxDownloadFilename(String version) => 'Firefox ${version}.dmg';
@override
String getFirefoxExecutablePath(io.Directory versionDir) =>
path.join(versionDir.path, 'Firefox.app','Contents','MacOS', 'firefox');
path.join(versionDir.path, 'Firefox.app', 'Contents', 'MacOS', 'firefox');
@override
String getFirefoxLatestVersionUrl() =>
@ -243,3 +250,14 @@ class DevNull implements StringSink {
}
bool get isCirrus => io.Platform.environment['CIRRUS_CI'] == 'true';
/// There might be proccesses started during the tests.
///
/// Use this list to store those Processes, for cleaning up before shutdown.
final List<io.Process> processesToCleanUp = List<io.Process>();
/// There might be temporary directories created during the tests.
///
/// Use this list to store those directories and for deleteing them before
/// shutdown.
final List<io.Directory> temporaryDirectories = List<io.Directory>();

View File

@ -22,6 +22,7 @@ class Environment {
final io.Directory hostDebugUnoptDir = io.Directory(pathlib.join(outDir.path, 'host_debug_unopt'));
final io.Directory dartSdkDir = io.Directory(pathlib.join(hostDebugUnoptDir.path, 'dart-sdk'));
final io.Directory webUiRootDir = io.Directory(pathlib.join(engineSrcDir.path, 'flutter', 'lib', 'web_ui'));
final io.Directory integrationTestsDir = io.Directory(pathlib.join(engineSrcDir.path, 'flutter', 'e2etests', 'web'));
for (io.Directory expectedDirectory in <io.Directory>[engineSrcDir, outDir, hostDebugUnoptDir, dartSdkDir, webUiRootDir]) {
if (!expectedDirectory.existsSync()) {
@ -34,6 +35,7 @@ class Environment {
self: self,
webUiRootDir: webUiRootDir,
engineSrcDir: engineSrcDir,
integrationTestsDir: integrationTestsDir,
outDir: outDir,
hostDebugUnoptDir: hostDebugUnoptDir,
dartSdkDir: dartSdkDir,
@ -44,6 +46,7 @@ class Environment {
this.self,
this.webUiRootDir,
this.engineSrcDir,
this.integrationTestsDir,
this.outDir,
this.hostDebugUnoptDir,
this.dartSdkDir,
@ -58,6 +61,9 @@ class Environment {
/// Path to the engine's "src" directory.
final io.Directory engineSrcDir;
/// Path to the web integration tests.
final io.Directory integrationTestsDir;
/// Path to the engine's "out" directory.
///
/// This is where you'll find the ninja output, such as the Dart SDK.

View File

@ -56,9 +56,9 @@ install_deps() {
KERNEL_NAME=`uname`
if [[ $KERNEL_NAME == *"Darwin"* ]]
then
echo "Running on MacOS. Increase the user limits"
ulimit -n 50000
ulimit -u 4096
echo "Running on MacOS. Increase the user limits"
ulimit -n 50000
ulimit -u 4096
fi
if [[ "$FELT_USE_SNAPSHOT" == "false" || "$FELT_USE_SNAPSHOT" == "0" ]]; then

View File

@ -9,6 +9,7 @@ import 'package:args/command_runner.dart';
import 'build.dart';
import 'clean.dart';
import 'common.dart';
import 'licenses.dart';
import 'test_runner.dart';
@ -41,12 +42,29 @@ void main(List<String> args) async {
io.exit(64); // Exit code 64 indicates a usage error.
} catch (e) {
rethrow;
} finally {
_cleanup();
}
// Sometimes the Dart VM refuses to quit.
io.exit(io.exitCode);
}
void _cleanup() {
// Cleanup remaining processes if any.
if (processesToCleanUp.length > 0) {
for (io.Process process in processesToCleanUp) {
process.kill();
}
}
// Delete temporary directories.
if (temporaryDirectories.length > 0) {
for (io.Directory directory in temporaryDirectories) {
directory.deleteSync(recursive: true);
}
}
}
void _listenToShutdownSignals() {
io.ProcessSignal.sigint.watch().listen((_) {
print('Received SIGINT. Shutting down.');

View File

@ -0,0 +1,350 @@
// 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;
import 'package:web_driver_installer/chrome_driver_installer.dart';
import 'common.dart';
import 'environment.dart';
import 'utils.dart';
class IntegrationTestsManager {
final String _browser;
/// Installation directory for browser's driver.
///
/// Always re-install since driver can change frequently.
/// It usually changes with each the browser version changes.
/// A better solution would be installing the browser and the driver at the
/// same time.
// TODO(nurhan): change the web installers to install driver and the browser
// at the same time.
final io.Directory _browserDriverDir;
/// This is the parent directory for all drivers.
///
/// This directory is saved to [temporaryDirectories] and deleted before
/// tests shutdown.
final io.Directory _drivers;
IntegrationTestsManager(this._browser)
: this._browserDriverDir = io.Directory(
pathlib.join(environment.webUiRootDir.path, 'drivers', _browser)),
this._drivers = io.Directory(
pathlib.join(environment.webUiRootDir.path, 'drivers'));
Future<bool> runTests() async {
if (_browser != 'chrome') {
print('WARNING: integration tests are only supported on chrome for now');
return false;
} else {
await prepareDriver();
// TODO(nurhan): https://github.com/flutter/flutter/issues/52987
return await _runTests();
}
}
void _cloneWebInstallers() async {
final int exitCode = await runProcess(
'git',
<String>[
'clone',
'https://github.com/flutter/web_installers.git',
],
workingDirectory: _browserDriverDir.path,
);
if (exitCode != 0) {
io.stderr.writeln('ERROR: '
'Failed to clone web installers. Exited with exit code $exitCode');
throw DriverException('ERROR: '
'Failed to clone web installers. Exited with exit code $exitCode');
}
}
Future<bool> _runPubGet(String workingDirectory) async {
final String executable = isCirrus ? environment.pubExecutable : 'flutter';
final List<String> arguments = isCirrus
? <String>[
'get',
]
: <String>[
'pub',
'get',
];
final int exitCode = await runProcess(
executable,
arguments,
workingDirectory: workingDirectory,
);
if (exitCode != 0) {
io.stderr.writeln(
'ERROR: Failed to run pub get. Exited with exit code $exitCode');
return false;
} else {
return true;
}
}
void _runDriver() async {
final int exitCode = await runProcess(
environment.dartExecutable,
<String>[
'lib/web_driver_installer.dart',
'${_browser}driver',
'--install-only',
],
workingDirectory: pathlib.join(
_browserDriverDir.path, 'web_installers', 'packages', 'web_drivers'),
);
if (exitCode != 0) {
io.stderr.writeln(
'ERROR: Failed to run driver. Exited with exit code $exitCode');
throw DriverException(
'ERROR: Failed to run driver. Exited with exit code $exitCode');
}
startProcess(
'./chromedriver/chromedriver',
['--port=4444'],
workingDirectory: pathlib.join(
_browserDriverDir.path, 'web_installers', 'packages', 'web_drivers'),
);
print('INFO: Driver started');
}
void prepareDriver() async {
final io.Directory priorCurrentDirectory = io.Directory.current;
if (_browserDriverDir.existsSync()) {
_browserDriverDir.deleteSync(recursive: true);
}
_browserDriverDir.createSync(recursive: true);
temporaryDirectories.add(_drivers);
// TODO(nurhan): We currently need git clone for getting the driver lock
// file. Remove this after making changes in web_installers.
await _cloneWebInstallers();
// Change the directory to the driver_lock.yaml file's directory.
io.Directory.current = pathlib.join(
_browserDriverDir.path, 'web_installers', 'packages', 'web_drivers');
// Chrome is the only browser supporting integration tests for now.
ChromeDriverInstaller chromeDriverInstaller = ChromeDriverInstaller();
bool installation = await chromeDriverInstaller.install();
if (installation) {
io.Directory.current = priorCurrentDirectory;
await _runDriver();
} else {
throw DriverException('ERROR: Installing driver failed');
}
}
/// Runs all the web tests under e2e_tests/web.
Future<bool> _runTests() async {
// Only list the files under e2e_tests/web.
final List<io.FileSystemEntity> entities =
environment.integrationTestsDir.listSync(followLinks: false);
bool allTestsPassed = true;
for (io.FileSystemEntity e in entities) {
// The tests should be under this directories.
if (e is io.Directory) {
allTestsPassed = allTestsPassed && await _validateAndRunTests(e);
}
}
return allTestsPassed;
}
/// Run tests in a single directory under: e2e_tests/web.
///
/// Run `flutter pub get` as the first step.
///
/// Validate the directory before running the tests. Each directory is
/// expected to be a test project which includes a `pubspec.yaml` file
/// and a `test_driver` directory.
Future<bool> _validateAndRunTests(io.Directory directory) async {
_validateTestDirectory(directory);
await _runPubGet(directory.path);
final bool testResults = await _runTestsInDirectory(directory);
return testResults;
}
Future<bool> _runTestsInDirectory(io.Directory directory) async {
final io.Directory testDirectory =
io.Directory(pathlib.join(directory.path, 'test_driver'));
final List<io.FileSystemEntity> entities = testDirectory
.listSync(followLinks: false)
.whereType<io.File>()
.toList();
final List<String> e2eTestsToRun = List<String>();
// The following loops over the contents of the directory and saves an
// expected driver file name for each e2e test assuming any dart file
// not ending with `_test.dart` is an e2e test.
// Other files are not considered since developers can add files such as
// README.
for (io.File f in entities) {
final String basename = pathlib.basename(f.path);
if (!basename.contains('_test.dart') && basename.endsWith('.dart')) {
e2eTestsToRun.add(basename);
}
}
print(
'INFO: In project ${directory} ${e2eTestsToRun.length} tests to run.');
int numberOfPassedTests = 0;
int numberOfFailedTests = 0;
for (String fileName in e2eTestsToRun) {
final bool testResults =
await _runTestsInProfileMode(directory, fileName);
if (testResults) {
numberOfPassedTests++;
} else {
numberOfFailedTests++;
}
}
final int numberOfTestsRun = numberOfPassedTests + numberOfFailedTests;
print('INFO: ${numberOfTestsRun} tests run. ${numberOfPassedTests} passed '
'and ${numberOfFailedTests} failed.');
return numberOfFailedTests == 0;
}
Future<bool> _runTestsInProfileMode(
io.Directory directory, String testName) async {
final int exitCode = await runProcess(
'flutter',
<String>[
'drive',
'--target=test_driver/${testName}',
'-d',
'web-server',
'--profile',
'--browser-name=$_browser',
'--local-engine=host_debug_unopt',
],
workingDirectory: directory.path,
);
if (exitCode != 0) {
final String statementToRun = 'flutter drive '
'--target=test_driver/${testName} -d web-server --profile '
'--browser-name=$_browser --local-engine=host_debug_unopt';
io.stderr
.writeln('ERROR: Failed to run test. Exited with exit code $exitCode'
'. Statement to run $testName locally use the following '
'command:\n\n$statementToRun');
return false;
} else {
return true;
}
}
/// Validate the directory has a `pubspec.yaml` file and a `test_driver`
/// directory.
///
/// Also check the validity of files under `test_driver` directory calling
/// [_checkE2ETestsValidity] method.
void _validateTestDirectory(io.Directory directory) {
final List<io.FileSystemEntity> entities =
directory.listSync(followLinks: false);
// Whether the project has the pubspec.yaml file.
bool pubSpecFound = false;
// The test directory 'test_driver'.
io.Directory testDirectory = null;
for (io.FileSystemEntity e in entities) {
// The tests should be under this directories.
final String baseName = pathlib.basename(e.path);
if (e is io.Directory && baseName == 'test_driver') {
testDirectory = e;
}
if (e is io.File && baseName == 'pubspec.yaml') {
pubSpecFound = true;
}
}
if (!pubSpecFound) {
throw StateError('ERROR: pubspec.yaml file not found in the test project '
'in the directory ${directory.path}.');
}
if (testDirectory == null) {
throw StateError(
'ERROR: test_driver folder not found in the test project.'
'in the directory ${directory.path}.');
} else {
_checkE2ETestsValidity(testDirectory);
}
}
/// Checks if each e2e test file in the directory has a driver test
/// file to run it.
///
/// Prints informative message to the developer if an error has found.
/// For each e2e test which has name {name}.dart there will be a driver
/// file which drives it. The driver file should be named:
/// {name}_test.dart
void _checkE2ETestsValidity(io.Directory testDirectory) {
final Iterable<io.Directory> directories =
testDirectory.listSync(followLinks: false).whereType<io.Directory>();
if (directories.length > 0) {
throw StateError('${testDirectory.path} directory should not contain '
'any sub-directories');
}
final Iterable<io.File> entities =
testDirectory.listSync(followLinks: false).whereType<io.File>();
final Set<String> expectedDriverFileNames = Set<String>();
final Set<String> foundDriverFileNames = Set<String>();
int numberOfTests = 0;
// The following loops over the contents of the directory and saves an
// expected driver file name for each e2e test assuming any file
// not ending with `_test.dart` is an e2e test.
for (io.File f in entities) {
final String basename = pathlib.basename(f.path);
if (basename.contains('_test.dart')) {
// First remove this from expectedSet if not there add to the foundSet.
if (!expectedDriverFileNames.remove(basename)) {
foundDriverFileNames.add(basename);
}
} else if (basename.contains('.dart')) {
// Only run on dart files.
final String e2efileName = pathlib.basenameWithoutExtension(f.path);
final String expectedDriverName = '${e2efileName}_test.dart';
numberOfTests++;
// First remove this from foundSet if not there add to the expectedSet.
if (!foundDriverFileNames.remove(expectedDriverName)) {
expectedDriverFileNames.add(expectedDriverName);
}
}
}
if (numberOfTests == 0) {
throw StateError(
'WARNING: No tests to run in this directory ${testDirectory.path}');
}
// TODO(nurhan): In order to reduce the work required from team members,
// remove the need for driver file, by using the same template file.
// Some driver files are missing.
if (expectedDriverFileNames.length > 0) {
for (String expectedDriverName in expectedDriverFileNames) {
print('ERROR: Test driver file named has ${expectedDriverName} '
'not found under directory ${testDirectory.path}. Stopping the '
'integration tests. Please add ${expectedDriverName}. Check to '
'README file on more details on how to setup integration tests.');
}
throw StateError('Error in test files. Check the logs for '
'further instructions');
}
}
}

View File

@ -15,11 +15,24 @@ import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_im
import 'package:test_core/src/executable.dart'
as test; // ignore: implementation_imports
import 'integration_tests_manager.dart';
import 'supported_browsers.dart';
import 'test_platform.dart';
import 'environment.dart';
import 'utils.dart';
/// The type of tests requested by the tool user.
enum TestTypesRequested {
/// For running the unit tests only.
unit,
/// For running the integration tests only.
integration,
/// For running both unit and integration tests.
all,
}
class TestCommand extends Command<bool> {
TestCommand() {
argParser
@ -29,6 +42,19 @@ class TestCommand extends Command<bool> {
'opportunity to add breakpoints or inspect loaded code before '
'running the code.',
)
..addFlag(
'unit-tests-only',
defaultsTo: false,
help: 'felt test command runs the unit tests and the integration tests '
'at the same time. If this flag is set, only run the unit tests.',
)
..addFlag(
'integration-tests-only',
defaultsTo: false,
help: 'felt test command runs the unit tests and the integration tests '
'at the same time. If this flag is set, only run the integration '
'tests.',
)
..addFlag(
'update-screenshot-goldens',
defaultsTo: false,
@ -54,11 +80,62 @@ class TestCommand extends Command<bool> {
@override
final String description = 'Run tests.';
TestTypesRequested testTypesRequested = null;
/// Check the flags to see what type of tests are requested.
TestTypesRequested findTestType() {
if (argResults['unit-tests-only'] && argResults['integration-tests-only']) {
throw ArgumentError('Conflicting arguments: unit-tests-only and '
'integration-tests-only are both set');
} else if (argResults['unit-tests-only']) {
print('Running the unit tests only');
return TestTypesRequested.unit;
} else if (argResults['integration-tests-only']) {
if (!isChrome) {
throw UnimplementedError(
'Integration tests are only available on Chrome Desktop for now');
}
return TestTypesRequested.integration;
} else {
return TestTypesRequested.all;
}
}
@override
Future<bool> run() async {
SupportedBrowsers.instance
..argParsers.forEach((t) => t.parseOptions(argResults));
// Check the flags to see what type of integration tests are requested.
testTypesRequested = findTestType();
switch (testTypesRequested) {
case TestTypesRequested.unit:
return runUnitTests();
case TestTypesRequested.integration:
return runIntegrationTests();
case TestTypesRequested.all:
bool integrationTestResult = await runIntegrationTests();
bool unitTestResult = await runUnitTests();
if (integrationTestResult != unitTestResult) {
print('Tests run. Integration tests passed: $integrationTestResult '
'unit tests passed: $unitTestResult');
}
return integrationTestResult && unitTestResult;
}
return false;
}
Future<bool> runIntegrationTests() async {
// TODO(nurhan): https://github.com/flutter/flutter/issues/52983
if (io.Platform.environment['LUCI_CONTEXT'] != null || isCirrus) {
return true;
}
return IntegrationTestsManager(browser).runTests();
}
Future<bool> runUnitTests() async {
_copyTestFontsIntoWebUi();
await _buildHostPage();
if (io.Platform.isWindows) {
@ -252,18 +329,18 @@ class TestCommand extends Command<bool> {
Future<void> _buildTests({List<FilePath> targets}) async {
List<String> arguments = <String>[
'run',
'build_runner',
'build',
'test',
'-o',
'build',
if (targets != null)
for (FilePath path in targets) ...[
'--build-filter=${path.relativeToWebUi}.js',
'--build-filter=${path.relativeToWebUi}.browser_test.dart.js',
],
];
'run',
'build_runner',
'build',
'test',
'-o',
'build',
if (targets != null)
for (FilePath path in targets) ...[
'--build-filter=${path.relativeToWebUi}.js',
'--build-filter=${path.relativeToWebUi}.browser_test.dart.js',
],
];
final int exitCode = await runProcess(
environment.pubExecutable,
arguments,

View File

@ -9,6 +9,7 @@ import 'dart:io' as io;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'common.dart';
import 'environment.dart';
class FilePath {
@ -62,6 +63,25 @@ Future<int> runProcess(
return exitCode;
}
/// Runs [executable]. Do not follow the exit code or the output.
void startProcess(
String executable,
List<String> arguments, {
String workingDirectory,
bool mustSucceed: false,
}) async {
final io.Process process = await io.Process.start(
executable,
arguments,
workingDirectory: workingDirectory,
// Running the process in a system shell for Windows. Otherwise
// the process is not able to get Dart from path.
runInShell: io.Platform.isWindows,
mode: io.ProcessStartMode.inheritStdio,
);
processesToCleanUp.add(process);
}
/// Runs [executable] and returns its standard output as a string.
///
/// If the process fails, throws a [ProcessException].

View File

@ -21,3 +21,8 @@ dev_dependencies:
watcher: 0.9.7+12
web_engine_tester:
path: ../../web_sdk/web_engine_tester
web_driver_installer:
git:
url: git://github.com/flutter/web_installers.git
path: packages/web_drivers/
ref: dae38d8839cc39f997fb4229f1382680b8758b4f