| // 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. |
| |
| /// Model for a modular test. |
| |
| /// A modular test declares the structure of the test code: what files are |
| /// grouped as a module and how modules depend on one another. |
| class ModularTest { |
| /// Modules that will be compiled by for modular test |
| final List<Module> modules; |
| |
| /// The module containing the main entry method. |
| final Module mainModule; |
| |
| /// Flags provided to tools that compile and execute the test. |
| final List<String> flags; |
| |
| ModularTest(this.modules, this.mainModule, this.flags) { |
| if (modules.isEmpty) throw ArgumentError("modules cannot be empty"); |
| for (var module in modules) { |
| module._validate(); |
| } |
| } |
| |
| String debugString() => modules.map((m) => m.debugString()).join('\n'); |
| } |
| |
| /// A single module in a modular test. |
| class Module { |
| /// A short name to identify this module. |
| final String name; |
| |
| /// Other modules that need to be compiled first and whose result may be |
| /// necessary in order to compile this module. |
| final List<Module> dependencies; |
| |
| /// Root under which all sources in the module can be found. |
| final Uri rootUri; |
| |
| /// Source files that are part of this module only. Stored as a relative [Uri] |
| /// from [rootUri]. |
| final List<Uri> sources; |
| |
| /// The file containing the main entry method, if any. Stored as a relative |
| /// [Uri] from [rootUri]. |
| final Uri? mainSource; |
| |
| /// Whether this module is also available as a package import, where the |
| /// package name matches the module name. |
| bool isPackage; |
| |
| /// Whether this module represents part of the sdk. |
| bool isSdk; |
| |
| /// When [isPackage], the base where all package URIs are resolved against. |
| /// Stored as a relative [Uri] from [rootUri]. |
| final Uri? packageBase; |
| |
| /// Whether this is the main entry module of a test. |
| bool isMain; |
| |
| /// Whether this module is test specific or shared across tests. Usually this |
| /// will be true only for the SDK and shared packages like `package:expect`. |
| bool isShared; |
| |
| Module(this.name, this.dependencies, this.rootUri, this.sources, |
| {this.mainSource, |
| this.isPackage: false, |
| this.isMain: false, |
| this.packageBase, |
| this.isShared: false, |
| this.isSdk: false}) { |
| if (!_validModuleName.hasMatch(name)) { |
| throw ArgumentError("invalid module name: $name"); |
| } |
| } |
| |
| void _validate() { |
| if (!isPackage && !isShared && !isSdk) return; |
| |
| // Note: we validate this now and not in the constructor because loader.dart |
| // may update `isPackage` after the module is created. |
| if (isSdk && isPackage) { |
| throw InvalidModularTestError("invalid module: $name is an sdk " |
| "module but was also marked as a package module."); |
| } |
| |
| for (var dependency in dependencies) { |
| if (isPackage && !dependency.isPackage && !dependency.isSdk) { |
| throw InvalidModularTestError("invalid dependency: $name is a package " |
| "but it depends on ${dependency.name}, which is not."); |
| } |
| if (isShared && !dependency.isShared) { |
| throw InvalidModularTestError( |
| "invalid dependency: $name is a shared module " |
| "but it depends on ${dependency.name}, which is not."); |
| } |
| if (isSdk) { |
| // TODO(sigmund): we should allow to split sdk modules in smaller |
| // pieces. This requires a bit of work: |
| // - allow to compile subsets of the sdk (see #30957 regarding |
| // extraRequiredLibraries in CFE) |
| // - add logic to specify sdk dependencies. |
| throw InvalidModularTestError( |
| "invalid dependency: $name is an sdk module that depends on " |
| "${dependency.name}, but sdk modules are not expected to " |
| "have dependencies."); |
| } |
| } |
| } |
| |
| @override |
| String toString() => '[module $name]'; |
| |
| String debugString() { |
| var buffer = new StringBuffer(); |
| buffer.write(' '); |
| buffer.write(name); |
| buffer.write(': '); |
| buffer.write(isPackage ? 'package' : '(not package)'); |
| buffer.write(', deps: {${dependencies.map((d) => d.name).join(", ")}}'); |
| if (isSdk) { |
| buffer.write(', sources: {...omitted ${sources.length} sources...}'); |
| } else { |
| buffer.write(', sources: {${sources.map((u) => "$u").join(', ')}}'); |
| } |
| return '$buffer'; |
| } |
| } |
| |
| final RegExp _validModuleName = new RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$'); |
| |
| /// Helper to compute transitive dependencies from [module]. |
| Set<Module> computeTransitiveDependencies(Module module) { |
| Set<Module> deps = {}; |
| helper(Module m) { |
| if (deps.add(m)) m.dependencies.forEach(helper); |
| } |
| |
| module.dependencies.forEach(helper); |
| return deps; |
| } |
| |
| /// A registry that can map a test configuration to a simple id. |
| /// |
| /// This is used to help determine whether two tests are run with the same set |
| /// of flags (the same configuration), and thus pipelines could reuse the |
| /// results of shared modules from the first test when running the second test. |
| class ConfigurationRegistry { |
| Map<String, int> _configurationId = {}; |
| |
| /// Compute an id to identify the configuration of a modular test. |
| /// |
| /// A configuration is defined in terms of the set of flags provided to a |
| /// test. If two test provided to this registry share the same set of flags, |
| /// the resulting ids are the same. Similarly, if the flags are different, |
| /// their ids will be different as well. |
| int computeConfigurationId(ModularTest test) { |
| return _configurationId[test.flags.join(' ')] ??= _configurationId.length; |
| } |
| } |
| |
| class InvalidModularTestError extends Error { |
| final String message; |
| InvalidModularTestError(this.message); |
| String toString() => "Invalid modular test: $message"; |
| } |