| // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| // for details. 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'; |
| |
| import 'package:analyzer/analyzer.dart' hide Configuration; |
| import 'package:async/async.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:source_span/source_span.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import '../backend/compiler.dart'; |
| import '../backend/group.dart'; |
| import '../backend/invoker.dart'; |
| import '../backend/test_platform.dart'; |
| import '../util/io.dart'; |
| import 'browser/platform.dart'; |
| import 'configuration.dart'; |
| import 'configuration/suite.dart'; |
| import 'load_exception.dart'; |
| import 'load_suite.dart'; |
| import 'node/platform.dart'; |
| import 'parse_metadata.dart'; |
| import 'plugin/customizable_platform.dart'; |
| import 'plugin/environment.dart'; |
| import 'plugin/hack_register_platform.dart'; |
| import 'plugin/platform.dart'; |
| import 'runner_suite.dart'; |
| import 'vm/platform.dart'; |
| |
| // TODO(nweiz): Use inline function types when sdk#30858 is fixed. |
| typedef FutureOr<PlatformPlugin> _PlatformPluginFunction(); |
| |
| /// A class for finding test files and loading them into a runnable form. |
| class Loader { |
| /// The test runner configuration. |
| final _config = Configuration.current; |
| |
| /// All suites that have been created by the loader. |
| final _suites = new Set<RunnerSuite>(); |
| |
| /// Memoizers for platform plugins, indexed by the platforms they support. |
| final _platformPlugins = <TestPlatform, AsyncMemoizer<PlatformPlugin>>{}; |
| |
| /// The functions to use to load [_platformPlugins]. |
| /// |
| /// These are passed to the plugins' async memoizers when a plugin is needed. |
| final _platformCallbacks = <TestPlatform, _PlatformPluginFunction>{}; |
| |
| /// A map of all platforms registered in [_platformCallbacks], indexed by |
| /// their string identifiers. |
| final _platformsByIdentifier = <String, TestPlatform>{}; |
| |
| /// The user-provided settings for platforms, as a list of settings that will |
| /// be merged together using [CustomizablePlatform.mergePlatformSettings]. |
| final _platformSettings = <TestPlatform, List<YamlMap>>{}; |
| |
| /// The user-provided settings for platforms. |
| final _parsedPlatformSettings = <TestPlatform, Object>{}; |
| |
| /// All plaforms supported by this [Loader]. |
| List<TestPlatform> get allPlatforms => |
| new List.unmodifiable(_platformCallbacks.keys); |
| |
| /// The platform variables supported by this loader, in addition the default |
| /// variables that are always supported. |
| Iterable<String> get _platformVariables => |
| _platformCallbacks.keys.map((platform) => platform.identifier); |
| |
| /// Creates a new loader that loads tests on platforms defined in |
| /// [Configuration.current]. |
| /// |
| /// [root] is the root directory that will be served for browser tests. It |
| /// defaults to the working directory. |
| /// |
| /// The [plugins] register [PlatformPlugin]s that are associated with the |
| /// provided platforms. When the runner first requests that a suite be loaded |
| /// for one of the given platforms, the lodaer will call the associated |
| /// callback to load the platform plugin. That plugin is then preserved and |
| /// used to load all suites for all matching platforms. Platform plugins may |
| /// override built-in platforms. |
| Loader( |
| {String root, |
| Map<Iterable<TestPlatform>, _PlatformPluginFunction> plugins}) { |
| _registerPlatformPlugin([TestPlatform.vm], () => new VMPlatform()); |
| _registerPlatformPlugin([TestPlatform.nodeJS], () => new NodePlatform()); |
| _registerPlatformPlugin([ |
| TestPlatform.dartium, |
| TestPlatform.contentShell, |
| TestPlatform.chrome, |
| TestPlatform.phantomJS, |
| TestPlatform.firefox, |
| TestPlatform.safari, |
| TestPlatform.internetExplorer |
| ], () => BrowserPlatform.start(root: root)); |
| |
| platformCallbacks.forEach((platform, plugin) { |
| _registerPlatformPlugin([platform], plugin); |
| }); |
| |
| plugins?.forEach(_registerPlatformPlugin); |
| |
| _registerCustomPlatforms(); |
| |
| _config.validatePlatforms(allPlatforms); |
| |
| _registerPlatformOverrides(); |
| } |
| |
| /// Registers a [PlatformPlugin] for [platforms]. |
| void _registerPlatformPlugin( |
| Iterable<TestPlatform> platforms, FutureOr<PlatformPlugin> getPlugin()) { |
| var memoizer = new AsyncMemoizer<PlatformPlugin>(); |
| for (var platform in platforms) { |
| _platformPlugins[platform] = memoizer; |
| _platformCallbacks[platform] = getPlugin; |
| _platformsByIdentifier[platform.identifier] = platform; |
| } |
| } |
| |
| /// Registers user-defined platforms from [Configuration.definePlatforms]. |
| void _registerCustomPlatforms() { |
| for (var customPlatform in _config.definePlatforms.values) { |
| if (_platformsByIdentifier.containsKey(customPlatform.identifier)) { |
| throw new SourceSpanFormatException( |
| wordWrap( |
| 'The platform "${customPlatform.identifier}" already exists. ' |
| 'Use override_platforms to override it.'), |
| customPlatform.identifierSpan); |
| } |
| |
| var parent = _platformsByIdentifier[customPlatform.parent]; |
| if (parent == null) { |
| throw new SourceSpanFormatException( |
| 'Unknown platform.', customPlatform.parentSpan); |
| } |
| |
| var platform = |
| parent.extend(customPlatform.name, customPlatform.identifier); |
| _platformPlugins[platform] = _platformPlugins[parent]; |
| _platformCallbacks[platform] = _platformCallbacks[parent]; |
| _platformsByIdentifier[platform.identifier] = platform; |
| |
| _platformSettings[platform] = [customPlatform.settings]; |
| } |
| } |
| |
| /// Registers users' platform settings from [Configuration.overridePlatforms]. |
| void _registerPlatformOverrides() { |
| for (var settings in _config.overridePlatforms.values) { |
| var platform = _platformsByIdentifier[settings.identifier]; |
| |
| // This is officially validated in [Configuration.validatePlatforms]. |
| assert(platform != null); |
| |
| _platformSettings |
| .putIfAbsent(platform, () => []) |
| .addAll(settings.settings); |
| } |
| } |
| |
| /// Returns the [TestPlatform] registered with this loader that's identified |
| /// by [identifier], or `null` if none can be found. |
| TestPlatform findTestPlatform(String identifier) => |
| _platformsByIdentifier[identifier]; |
| |
| /// Loads all test suites in [dir] according to [suiteConfig]. |
| /// |
| /// This will load tests from files that match the global configuration's |
| /// filename glob. Any tests that fail to load will be emitted as |
| /// [LoadException]s. |
| /// |
| /// This emits [LoadSuite]s that must then be run to emit the actual |
| /// [RunnerSuite]s defined in the file. |
| Stream<LoadSuite> loadDir(String dir, SuiteConfiguration suiteConfig) { |
| return StreamGroup |
| .merge(new Directory(dir).listSync(recursive: true).map((entry) { |
| if (entry is! File) return new Stream.fromIterable([]); |
| |
| if (!_config.filename.matches(p.basename(entry.path))) { |
| return new Stream.fromIterable([]); |
| } |
| |
| if (p.split(entry.path).contains('packages')) { |
| return new Stream.fromIterable([]); |
| } |
| |
| return loadFile(entry.path, suiteConfig); |
| })); |
| } |
| |
| /// Loads a test suite from the file at [path] according to [suiteConfig]. |
| /// |
| /// This emits [LoadSuite]s that must then be run to emit the actual |
| /// [RunnerSuite]s defined in the file. |
| /// |
| /// This will emit a [LoadException] if the file fails to load. |
| Stream<LoadSuite> loadFile( |
| String path, SuiteConfiguration suiteConfig) async* { |
| try { |
| suiteConfig = suiteConfig.merge(new SuiteConfiguration.fromMetadata( |
| parseMetadata(path, _platformVariables.toSet()))); |
| } on AnalyzerErrorGroup catch (_) { |
| // Ignore the analyzer's error, since its formatting is much worse than |
| // the VM's or dart2js's. |
| } on FormatException catch (error, stackTrace) { |
| yield new LoadSuite.forLoadException( |
| new LoadException(path, error), suiteConfig, |
| stackTrace: stackTrace); |
| return; |
| } |
| |
| if (_config.suiteDefaults.excludeTags.evaluate(suiteConfig.metadata.tags)) { |
| return; |
| } |
| |
| if (_config.pubServeUrl != null && !p.isWithin('test', path)) { |
| yield new LoadSuite.forLoadException( |
| new LoadException( |
| path, 'When using "pub serve", all test files must be in test/.'), |
| suiteConfig); |
| return; |
| } |
| |
| for (var platformName in suiteConfig.platforms) { |
| var platform = findTestPlatform(platformName); |
| assert(platform != null, 'Unknown platform "$platformName".'); |
| |
| if (!platform.isJS) { |
| var suite = await _loadFileOnPlatform( |
| path, suiteConfig, platform, Compiler.none); |
| if (suite != null) yield suite; |
| } else { |
| for (var compiler in suiteConfig.compilers) { |
| var suite = |
| await _loadFileOnPlatform(path, suiteConfig, platform, compiler); |
| if (suite != null) yield suite; |
| } |
| } |
| } |
| } |
| |
| /// Loads a single suite from the file at [path] according to [suiteConfig] on |
| /// [platform], compiled with [compiler]. |
| /// |
| /// If the suite doesn't support [platform] and/or [compiler], returns `null`. |
| Future<LoadSuite> _loadFileOnPlatform( |
| String path, |
| SuiteConfiguration suiteConfig, |
| TestPlatform platform, |
| Compiler compiler) async { |
| if (!suiteConfig.metadata.testOn |
| .evaluate(platform, os: currentOS, compiler: compiler)) { |
| return null; |
| } |
| |
| var platformConfig = |
| suiteConfig.forPlatform(platform, os: currentOS, compiler: compiler); |
| |
| // Don't load a skipped suite. |
| if (platformConfig.metadata.skip && !platformConfig.runSkipped) { |
| return new LoadSuite.forSuite(new RunnerSuite( |
| const PluginEnvironment(), |
| platformConfig, |
| new Group.root( |
| [new LocalTest("(suite)", platformConfig.metadata, () {})], |
| metadata: platformConfig.metadata), |
| path: path, |
| platform: platform)); |
| } |
| |
| var name = (platform.isJS && compiler != Compiler.build |
| ? "compiling " |
| : "loading ") + |
| path; |
| return new LoadSuite(name, platformConfig, () async { |
| var memo = _platformPlugins[platform]; |
| |
| try { |
| var plugin = await memo.runOnce(_platformCallbacks[platform]); |
| _customizePlatform(plugin, platform); |
| var suite = await plugin.load(path, platform, platformConfig, { |
| "compiler": platform.isJS ? compiler.identifier : null, |
| "platformVariables": _platformVariables.toList() |
| }); |
| if (suite != null) _suites.add(suite); |
| return suite; |
| } catch (error, stackTrace) { |
| if (error is LoadException) rethrow; |
| await new Future.error(new LoadException(path, error), stackTrace); |
| return null; |
| } |
| }, path: path, platform: platform); |
| } |
| |
| /// Passes user-defined settings to [plugin] if necessary. |
| void _customizePlatform(PlatformPlugin plugin, TestPlatform platform) { |
| var parsed = _parsedPlatformSettings[platform]; |
| if (parsed != null) { |
| (plugin as CustomizablePlatform).customizePlatform(platform, parsed); |
| return; |
| } |
| |
| var settings = _platformSettings[platform]; |
| if (settings == null) return; |
| |
| if (plugin is CustomizablePlatform) { |
| parsed = settings |
| .map(plugin.parsePlatformSettings) |
| .reduce(plugin.mergePlatformSettings); |
| plugin.customizePlatform(platform, parsed); |
| _parsedPlatformSettings[platform] = parsed; |
| } else { |
| String identifier; |
| SourceSpan span; |
| if (platform.isChild) { |
| identifier = platform.parent.identifier; |
| span = _config.definePlatforms[platform.identifier].parentSpan; |
| } else { |
| identifier = platform.identifier; |
| span = _config.overridePlatforms[platform.identifier].identifierSpan; |
| } |
| |
| throw new SourceSpanFormatException( |
| 'The "$identifier" platform can\'t be customized.', span); |
| } |
| } |
| |
| Future closeEphemeral() async { |
| await Future.wait(_platformPlugins.values.map((memo) async { |
| if (!memo.hasRun) return; |
| await (await memo.future).closeEphemeral(); |
| })); |
| } |
| |
| /// Closes the loader and releases all resources allocated by it. |
| Future close() => _closeMemo.runOnce(() async { |
| await Future.wait([ |
| Future.wait(_platformPlugins.values.map((memo) async { |
| if (!memo.hasRun) return; |
| await (await memo.future).close(); |
| })), |
| Future.wait(_suites.map((suite) => suite.close())) |
| ]); |
| |
| _platformPlugins.clear(); |
| _platformCallbacks.clear(); |
| _suites.clear(); |
| }); |
| final _closeMemo = new AsyncMemoizer(); |
| } |