| // 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; |
| |
| 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<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; |
| |
| 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<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; |
| } |