blob: 46206ffe1fec0f66100384922856ed741a6e638f [file] [log] [blame]
// 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 'chrome_installer.dart';
import 'environment.dart';
import 'exceptions.dart';
import 'common.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): https://github.com/flutter/flutter/issues/53179. Partly
// solved. Remaining local integration tests using the locked Chrome version.
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;
final bool _useSystemFlutter;
IntegrationTestsManager(this._browser, this._useSystemFlutter)
: this._browserDriverDir = io.Directory(pathlib.join(
environment.webUiDartToolDir.path,
'drivers',
_browser,
'${_browser}driver-${io.Platform.operatingSystem.toString()}')),
this._drivers = io.Directory(
pathlib.join(environment.webUiDartToolDir.path, 'drivers'));
Future<bool> runTests() async {
if (_browser != 'chrome') {
print('WARNING: integration tests are only supported on chrome for now');
return false;
} else {
if (!isLuci) {
// LUCI installs driver from CIPD, so we skip installing it on LUCI.
await _prepareDriver();
} else {
await _verifyDriverForLUCI();
}
await _startDriver(_browserDriverDir.path);
// TODO(nurhan): https://github.com/flutter/flutter/issues/52987
return await _runTests();
}
}
Future<void> _runPubGet(String workingDirectory) async {
if (!_useSystemFlutter) {
await _cloneFlutterRepo();
await _enableWeb(workingDirectory);
}
await runFlutter(workingDirectory, <String>['pub', 'get'],
useSystemFlutter: _useSystemFlutter);
}
/// Clone flutter repository, use the youngest commit older than the engine
/// commit.
///
/// Use engine/src/flutter/.dart_tools to clone the Flutter repo.
/// TODO(nurhan): Use git pull instead if repo exists.
Future<void> _cloneFlutterRepo() async {
// Delete directory if exists.
if (environment.engineDartToolDir.existsSync()) {
environment.engineDartToolDir.deleteSync(recursive: true);
}
environment.engineDartToolDir.createSync();
final int exitCode = await runProcess(
environment.cloneFlutterScript.path,
<String>[
environment.engineDartToolDir.path,
],
workingDirectory: environment.webUiRootDir.path,
);
if (exitCode != 0) {
throw ToolException('ERROR: Failed to clone flutter repo. Exited with '
'exit code $exitCode');
}
}
Future<void> _enableWeb(String workingDirectory) async {
await runFlutter(workingDirectory, <String>['config', '--enable-web'],
useSystemFlutter: _useSystemFlutter);
}
/// Driver should already exist on LUCI as a CIPD package.
///
/// Throw an error if directory does not exists.
void _verifyDriverForLUCI() {
if (!_browserDriverDir.existsSync()) {
throw StateError('Failed to locate Chrome driver on LUCI on path:'
'${_browserDriverDir.path}');
}
}
void _startDriver(String workingDirectory) async {
await startProcess('./chromedriver/chromedriver', ['--port=4444'],
workingDirectory: workingDirectory);
print('INFO: Driver started');
}
void _prepareDriver() async {
if (_browserDriverDir.existsSync()) {
_browserDriverDir.deleteSync(recursive: true);
}
_browserDriverDir.createSync(recursive: true);
temporaryDirectories.add(_drivers);
io.Directory temp = io.Directory.current;
io.Directory.current = _browserDriverDir;
// TODO(nurhan): https://github.com/flutter/flutter/issues/53179
final String chromeDriverVersion = await queryChromeDriverVersion();
ChromeDriverInstaller chromeDriverInstaller =
ChromeDriverInstaller.withVersion(chromeDriverVersion);
await chromeDriverInstaller.install(alwaysInstall: true);
io.Directory.current = temp;
}
/// 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.File> entities = testDirectory
.listSync(followLinks: false)
.whereType<io.File>()
.toList();
final List<String> e2eTestsToRun = List<String>();
final List<String> blackList =
blackListsMap[getBlackListMapKey(_browser)] ?? <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')) {
// Do not add the basename if it is in the blacklist.
if (!blackList.contains(basename)) {
e2eTestsToRun.add(basename);
} else {
print('INFO: Test $basename is skipped since it is blacklisted for '
'${getBlackListMapKey(_browser)}');
}
}
}
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 String executable =
_useSystemFlutter ? 'flutter' : environment.flutterCommand.path;
final int exitCode = await runProcess(
executable,
<String>[
'drive',
'--target=test_driver/${testName}',
'-d',
'web-server',
'--profile',
'--browser-name=$_browser',
if (isLuci) '--chrome-binary=${preinstalledChromeExecutable()}',
if (isLuci) '--headless',
'--local-engine=host_debug_unopt',
],
workingDirectory: directory.path,
);
if (exitCode != 0) {
String statementToRun = 'flutter drive '
'--target=test_driver/${testName} -d web-server --profile '
'--browser-name=$_browser --local-engine=host_debug_unopt';
if (isLuci) {
statementToRun = '$statementToRun --chrome-binary='
'${preinstalledChromeExecutable()}';
}
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');
}
}
}
/// Prepares a key for the [blackList] map.
///
/// Uses the browser name and the operating system name.
String getBlackListMapKey(String browser) =>
'${browser}-${io.Platform.operatingSystem}';
/// Tests that should be skipped run for a specific platform-browser
/// combination.
///
/// These tests might be failing or might have been implemented for a specific
/// configuration.
///
/// For example a mobile browser only tests will be added to blacklist for
/// `chrome-linux`, `safari-macos` and `chrome-macos`.
///
/// Note that integration tests are only running on chrome for now.
const Map<String, List<String>> blackListsMap = <String, List<String>>{
'chrome-linux': [
'target_platform_ios_e2e.dart',
'target_platform_macos_e2e.dart',
],
'chrome-macos': [
'target_platform_ios_e2e.dart',
'target_platform_android_e2e.dart',
],
};