| // Copyright (c) 2019, 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. |
| |
| /// Abstraction for a compilation pipeline. |
| /// |
| /// A pipeline defines how modular steps are excuted and ensures that a step |
| /// only has access to the data it declares. |
| /// |
| /// The abstract implementation validates how the data is declared, and the |
| /// underlying implementations enforce the access to data in different ways. |
| /// |
| /// The IO-based implementation ensures hermeticity by copying data to different |
| /// directories. The memory-based implementation ensures hemeticity by filtering |
| /// out the data before invoking the next step. |
| import 'suite.dart'; |
| |
| /// Describes a step in a modular compilation pipeline. |
| class ModularStep { |
| /// Whether this step needs to read the source files in the module. |
| final bool needsSources; |
| |
| /// Data that this step needs to read about dependencies. |
| /// |
| /// This can be data produced on a previous stage of the pipeline |
| /// or produced by this same step when it was run on a dependency. |
| /// |
| /// If this list includes any data from [resultData], then the modular-step |
| /// has to be run on dependencies before it is run on a module. Otherwise, it |
| /// could be run in parallel. |
| final List<DataId> dependencyDataNeeded; |
| |
| /// Data that this step needs to read about the module itself. |
| /// |
| /// This is meant to be data produced in earlier stages of the modular |
| /// pipeline. It is an error to include any id from [resultData] in this list. |
| final List<DataId> moduleDataNeeded; |
| |
| /// Data that this step produces. |
| final List<DataId> resultData; |
| |
| /// Whether this step is only executed on the main module. |
| final bool onlyOnMain; |
| |
| /// Whether this step is only exceuted on the SDK. |
| final bool onlyOnSdk; |
| |
| /// Whether this step is not executed on the SDK. |
| final bool notOnSdk; |
| |
| ModularStep( |
| {this.needsSources: true, |
| this.dependencyDataNeeded: const [], |
| this.moduleDataNeeded: const [], |
| this.resultData: const [], |
| this.onlyOnMain: false, |
| this.onlyOnSdk: false, |
| this.notOnSdk: false}); |
| |
| /// Notifies that the step was not executed, but cached instead. |
| void notifyCached(Module module) {} |
| } |
| |
| /// An object to uniquely identify modular data produced by a modular step. |
| /// |
| /// Two modular steps on the same pipeline cannot emit the same data. |
| class DataId { |
| final String name; |
| |
| const DataId(this.name); |
| |
| @override |
| String toString() => name; |
| } |
| |
| abstract class Pipeline<S extends ModularStep> { |
| /// Whether to cache the result of shared modules (e.g. shard packages and sdk |
| /// libraries) when multiple tests are run by this pipeline. |
| final bool cacheSharedModules; |
| |
| final List<S> steps; |
| |
| Pipeline(this.steps, this.cacheSharedModules) { |
| _validate(); |
| } |
| |
| void _validate() { |
| // Whether or not two steps run on mutually exclusive input data. |
| bool areMutuallyExclusive(S a, S b) { |
| return (a.onlyOnSdk && b.notOnSdk) || (b.onlyOnSdk && a.notOnSdk); |
| } |
| |
| // Ensure that steps consume only data that was produced by previous steps |
| // or by the same step on a dependency. |
| Map<DataId, S> previousKinds = {}; |
| for (var step in steps) { |
| if (step.resultData.isEmpty) { |
| _validationError( |
| "'${step.runtimeType}' needs to declare what data it produces."); |
| } |
| for (var resultKind in step.resultData) { |
| if (previousKinds.containsKey(resultKind) && |
| !areMutuallyExclusive(step, previousKinds[resultKind]!)) { |
| _validationError("Cannot produce the same data on two modular steps." |
| " '$resultKind' was previously produced by " |
| "'${previousKinds[resultKind].runtimeType}' but " |
| "'${step.runtimeType}' also produces the same data."); |
| } |
| previousKinds[resultKind] = step; |
| for (var dataId in step.dependencyDataNeeded) { |
| if (!previousKinds.containsKey(dataId)) { |
| _validationError( |
| "Step '${step.runtimeType}' needs data '${dataId}', but the " |
| "data is not produced by this or a preceding step."); |
| } |
| } |
| for (var dataId in step.moduleDataNeeded) { |
| if (!previousKinds.containsKey(dataId)) { |
| _validationError( |
| "Step '${step.runtimeType}' needs data '${dataId}', but the " |
| "data is not produced by a preceding step."); |
| } |
| if (dataId == resultKind) { |
| _validationError("Circular dependency on '$dataId' " |
| "in step '${step.runtimeType}'"); |
| } |
| } |
| } |
| } |
| } |
| |
| void _validationError(String s) => throw InvalidPipelineError(s); |
| |
| Future<void> run(ModularTest test) async { |
| // TODO(sigmund): validate that [ModularTest] has no cycles. |
| Map<Module, Set<DataId>> computedData = {}; |
| for (var step in steps) { |
| await _recursiveRun(step, test.mainModule, computedData, {}, test.flags); |
| } |
| } |
| |
| Future<void> _recursiveRun( |
| S step, |
| Module module, |
| Map<Module, Set<DataId>> computedData, |
| Map<Module, Set<Module>> transitiveDependencies, |
| List<String> flags) async { |
| if (transitiveDependencies.containsKey(module)) return; |
| var deps = transitiveDependencies[module] = {}; |
| for (var dependency in module.dependencies) { |
| await _recursiveRun( |
| step, dependency, computedData, transitiveDependencies, flags); |
| deps.add(dependency); |
| deps.addAll(transitiveDependencies[dependency]!); |
| } |
| |
| if ((step.onlyOnMain && !module.isMain) || |
| (step.onlyOnSdk && !module.isSdk) || |
| (step.notOnSdk && module.isSdk)) return; |
| // Include only requested data from transitive dependencies. |
| Map<Module, Set<DataId>> visibleData = {}; |
| |
| deps.forEach((dep) { |
| visibleData[dep] = {}; |
| for (var dataId in step.dependencyDataNeeded) { |
| if (computedData[dep]!.contains(dataId)) { |
| visibleData[dep]!.add(dataId); |
| } |
| } |
| }); |
| visibleData[module] = {}; |
| for (var dataId in step.moduleDataNeeded) { |
| if (computedData[module]!.contains(dataId)) { |
| visibleData[module]!.add(dataId); |
| } |
| } |
| await runStep(step, module, visibleData, flags); |
| (computedData[module] ??= {}).addAll(step.resultData); |
| } |
| |
| Future<void> runStep(S step, Module module, |
| Map<Module, Set<DataId>> visibleData, List<String> flags); |
| } |
| |
| class InvalidPipelineError extends Error { |
| final String message; |
| InvalidPipelineError(this.message); |
| String toString() => "Invalid pipeline: $message"; |
| } |