blob: 2ed079877887263214260d933af79958a286aed4 [file] [log] [blame]
// 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) {
String debugString() => => 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.dependencies, this.rootUri, this.sources,
this.isPackage = false,
this.isMain = false,
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 ${}, which is not.");
if (isShared && !dependency.isShared) {
throw InvalidModularTestError(
"invalid dependency: $name is a shared module "
"but it depends on ${}, 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 "
"${}, but sdk modules are not expected to "
"have dependencies.");
String toString() => '[module $name]';
String debugString() {
var buffer = StringBuffer();
buffer.write(' ');
buffer.write(': ');
buffer.write(isPackage ? 'package' : '(not package)');
buffer.write(', deps: {${ =>", ")}}');
if (isSdk) {
buffer.write(', sources: {...omitted ${sources.length} sources...}');
} else {
buffer.write(', sources: {${ => "$u").join(', ')}}');
return '$buffer';
final RegExp _validModuleName = 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);
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;
String toString() => "Invalid modular test: $message";