// 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 arguments; final Map environment; final String fileSet; final int shards; final bool isTestRunner; final Configuration testedConfiguration; Step(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 configuration, List configurations) { var arguments = (map["arguments"] as List ?? []) .map((argument) => _expandVariables(argument as String, configuration)) .toList(); var testedConfigurations = []; 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 = []; 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"); } } return Step( map["name"] as String, script, arguments, {...?map["environment"]}, 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 steps; final System system; final Mode mode; final Architecture arch; final Sanitizer sanitizer; final Runtime runtime; final Set testedConfigurations; Builder(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 steps, List configurations, String description) { var builderParts = builderName.split("-"); var systemName = _findPart(builderParts, System.names); var modeName = _findPart(builderParts, Mode.names); var archName = _findPart(builderParts, Architecture.names); 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 values) { for (var variable in ["system", "mode", "arch", "sanitizer", "runtime"]) { string = _tryReplace(string, variable, values[variable]); } return string; } Set _getTestedConfigurations(List steps) { return steps .where((step) => step.isTestStep) .map((step) => step.testedConfiguration) .toSet(); } T _findIfNotNull(T Function(String) find, String name) { return name != null ? find(name) : null; } String _findPart(List builderParts, List parts) { return builderParts.firstWhere((part) => parts.contains(part), orElse: () => null); } List parseBuilders( List builderConfigurations, List configurations) { var builders = []; var names = {}; for (var builderConfiguration in builderConfigurations) { var meta = builderConfiguration["meta"] as Map ?? {}; var builderNames = [...?builderConfiguration["builders"]]; var steps = [...?builderConfiguration["steps"]]; for (var builderName in builderNames) { if (!names.add(builderName)) { throw FormatException('Duplicate builder name: "$builderName"'); } builders.add(Builder.parse( builderName, steps, configurations, meta["description"] as String)); } } return builders; }