blob: 01a80d4707677540b4aab5256365769e1cf0ce6e [file] [edit]
// Copyright (c) 2020, 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 'configuration.dart';
/// A step that is run on a builder to build and test certain configurations of
/// the Dart SDK.
///
/// Each step on a builder runs a script the with provided arguments. If the
/// script is 'tools/test.py' (which is the default if no script is given in
/// the test matrix), or `testRunner == true`, the step is called a 'test
/// step'. Test steps must include the '--named_configuration' (for short
/// '-n') option to select the named [Configuration] to test.
///
/// Test steps are expected to produce test results that are collected during
/// the run of the builder and checked against the expected results to determine
/// the success or failure of the build.
class Step {
final String name;
final String script;
final List<String> arguments;
final Map<String, String> environment;
final String? fileSet;
final int? shards;
final bool isTestRunner;
final Configuration? testedConfiguration;
new(
this.name,
String? script,
this.arguments,
this.environment,
this.fileSet,
this.shards,
this.isTestRunner,
this.testedConfiguration,
) : script = script ?? testScriptName;
static const testScriptName = "tools/test.py";
bool get isTestStep => script == testScriptName || isTestRunner;
/// Create a [Step] from the 'step template' [map], values for supported
/// variables [configuration], and the list of supported named configurations.
static Step parse(
Map map,
Map<String, String?> configuration,
List<Configuration> configurations,
) {
var arguments = (map["arguments"] as List? ?? [])
.map((argument) => _expandVariables(argument as String, configuration))
.toList();
var testedConfigurations = <Configuration>[];
var script = map["script"] as String? ?? testScriptName;
var isTestRunner = map["testRunner"] as bool? ?? false;
if (script == testScriptName || isTestRunner) {
// TODO(karlklose): replace with argument parser that can handle all
// arguments to test.py.
for (var argument in arguments) {
var names = <String>[];
if (argument.startsWith("--named_configuration")) {
names.addAll(
argument
.substring("--named_configuration".length)
.split(",")
.map((s) => s.trim()),
);
} else if (argument.startsWith("-n")) {
names.addAll(
argument.substring("-n".length).split(",").map((s) => s.trim()),
);
} else {
continue;
}
for (var name in names) {
var matchingConfigurations = configurations.where(
(c) => c.name == name,
);
if (matchingConfigurations.isEmpty) {
throw FormatException("Undefined configuration: $name");
}
testedConfigurations.add(matchingConfigurations.single);
}
}
if (testedConfigurations.length > 1) {
throw FormatException("Step tests multiple configurations: $arguments");
}
}
var environment = map["environment"] as Map<Object?, Object?>?;
return Step(
map["name"] as String,
script,
arguments,
{...?environment?.cast<String, String>()},
map["fileset"] as String?,
map["shards"] as int?,
isTestRunner,
testedConfigurations.isEmpty ? null : testedConfigurations.single,
);
}
}
/// A builder runs a list of [Step]s to build and test certain configurations of
/// the Dart SDK.
///
/// Groups of builders are defined in the 'builder_configurations' section of
/// the test matrix.
class Builder {
final String name;
final String? description;
final List<Step> steps;
final System? system;
final Mode? mode;
final Architecture? arch;
final Sanitizer? sanitizer;
final Runtime? runtime;
final Set<Configuration> testedConfigurations;
new(
this.name,
this.description,
this.steps,
this.system,
this.mode,
this.arch,
this.sanitizer,
this.runtime,
this.testedConfigurations,
);
/// Create a [Builder] from its name, a list of 'step templates', the
/// supported named configurations and a description.
///
/// The 'step templates' can contain the variables `${system}`, `${mode}`,
/// `${arch}`, and `${runtime}. The values for these variables are inferred
/// from the builder's name.
static Builder parse(
String builderName,
List<Map> steps,
List<Configuration> configurations,
String? description,
) {
var builderParts = builderName.split("-");
var systemName = _findPart(builderParts, System.names, 'linux');
var modeName = _findPart(builderParts, Mode.names, 'release');
var archName = _findPart(builderParts, Architecture.names, 'x64');
var sanitizerName = _findPart(builderParts, Sanitizer.names);
var runtimeName = _findPart(builderParts, Runtime.names);
var parsedSteps = steps
.map(
(step) => Step.parse(step, {
"system": systemName,
"mode": modeName,
"arch": archName,
"sanitizer": sanitizerName,
"runtime": runtimeName,
}, configurations),
)
.toList();
var testedConfigurations = _getTestedConfigurations(parsedSteps);
return Builder(
builderName,
description,
parsedSteps,
_findIfNotNull(System.find, systemName),
_findIfNotNull(Mode.find, modeName),
_findIfNotNull(Architecture.find, archName),
_findIfNotNull(Sanitizer.find, sanitizerName),
_findIfNotNull(Runtime.find, runtimeName),
testedConfigurations,
);
}
}
/// Tries to replace a variable named [variableName] with [value] and throws
/// and exception if the variable is used but `value == null`.
String _tryReplace(String string, String variableName, String? value) {
var variable = "\${$variableName}";
if (string.contains(variable)) {
if (value == null) {
throw FormatException("Undefined value for '$variableName' in '$string'");
}
return string.replaceAll(variable, value);
} else {
return string;
}
}
/// Replace the use of supported variable names with the their value given
/// in [values] and throws an exception if an unsupported variable name is used.
String _expandVariables(String string, Map<String, String?> values) {
for (var variable in ["system", "mode", "arch", "sanitizer", "runtime"]) {
string = _tryReplace(string, variable, values[variable]);
}
return string;
}
Set<Configuration> _getTestedConfigurations(List<Step> steps) {
return steps
.where((step) => step.isTestStep)
.map((step) => step.testedConfiguration)
.whereType<Configuration>()
.toSet();
}
T? _findIfNotNull<T>(T Function(String) find, String? name) {
return name != null ? find(name) : null;
}
String? _findPart(
List<String> builderParts,
List<String> parts, [
String? fallback,
]) {
return builderParts.cast<String?>().firstWhere(
(part) => parts.contains(part),
orElse: () => fallback,
);
}
List<Builder> parseBuilders(
List<Map> builderConfigurations,
List<Configuration> configurations,
) {
var builders = <Builder>[];
var names = <String>{};
for (var builderConfiguration in builderConfigurations) {
var meta = builderConfiguration["meta"] as Map? ?? <String, String>{};
var builderNames = builderConfiguration["builders"] as List<Object?>?;
if (builderNames != null) {
var steps = ((builderConfiguration["steps"] ?? []) as List<Object?>)
.cast<Map<Object?, Object?>>();
for (var builderName in builderNames.cast<String>()) {
if (!names.add(builderName)) {
throw FormatException('Duplicate builder name: "$builderName"');
}
builders.add(
Builder.parse(
builderName,
steps,
configurations,
meta["description"] as String?,
),
);
}
}
}
return builders;
}