|  | // 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(); | 
|  | } |