[baseline] Add tool to baseline builders
Fixes: b/200911194
Change-Id: I215c1e2b55f924aeaa8be25117da6b9c643d7b66
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/214048
Commit-Queue: Alexander Thomas <athom@google.com>
Reviewed-by: William Hesse <whesse@google.com>
diff --git a/baseline/.gitignore b/baseline/.gitignore
new file mode 100644
index 0000000..e6ff0e2
--- /dev/null
+++ b/baseline/.gitignore
@@ -0,0 +1,10 @@
+# Files and directories created by pub.
+.dart_tool/
+.packages
+
+# Conventional directory for build output.
+build/
+
+# Code coverage
+coverage/
+lcov.info
diff --git a/baseline/README.md b/baseline/README.md
new file mode 100644
index 0000000..bcb87c5
--- /dev/null
+++ b/baseline/README.md
@@ -0,0 +1,7 @@
+A CLI application that copies test results from one builder to another.
+
+Example:
+```
+# Initialize debug-mac builder with results from debug-linux on main and dev:
+$ bin/baseline.dart -n -debug-linux:debug-mac -cmain,dev -mtest-linux:test-mac
+```
diff --git a/baseline/analysis_options.yaml b/baseline/analysis_options.yaml
new file mode 100644
index 0000000..dee8927
--- /dev/null
+++ b/baseline/analysis_options.yaml
@@ -0,0 +1,30 @@
+# This file configures the static analysis results for your project (errors,
+# warnings, and lints).
+#
+# This enables the 'recommended' set of lints from `package:lints`.
+# This set helps identify many issues that may lead to problems when running
+# or consuming Dart code, and enforces writing Dart using a single, idiomatic
+# style and format.
+#
+# If you want a smaller set of lints you can change this to specify
+# 'package:lints/core.yaml'. These are just the most critical lints
+# (the recommended set includes the core lints).
+# The core lints are also what is used by pub.dev for scoring packages.
+
+include: package:lints/recommended.yaml
+
+# Uncomment the following section to specify additional rules.
+
+# linter:
+# rules:
+# - camel_case_types
+
+# analyzer:
+# exclude:
+# - path/to/excluded/files/**
+
+# For more information about the core and recommended set of lints, see
+# https://dart.dev/go/core-lints
+
+# For additional information about configuring this file, see
+# https://dart.dev/guides/language/analysis-options
diff --git a/baseline/bin/baseline.dart b/baseline/bin/baseline.dart
new file mode 100755
index 0000000..bc549fc
--- /dev/null
+++ b/baseline/bin/baseline.dart
@@ -0,0 +1,11 @@
+#!/usr/bin/env dart
+// Copyright (c) 2021, 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 'package:baseline/baseline.dart';
+import 'package:baseline/options.dart';
+
+void main(List<String> arguments) async {
+ await baseline(BaselineOptions(arguments));
+}
diff --git a/baseline/lib/baseline.dart b/baseline/lib/baseline.dart
new file mode 100644
index 0000000..b5eac65
--- /dev/null
+++ b/baseline/lib/baseline.dart
@@ -0,0 +1,87 @@
+// Copyright (c) 2021, 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 'package:baseline/options.dart';
+import 'dart:convert';
+import 'dart:io';
+
+const _resultBase = 'gs://dart-test-results/builders';
+
+/// Baselines a builder with the [options] and copies the results to the
+/// [resultBase]. [resultBase] can be a URL or a path supported by `gsutil cp`.
+Future<void> baseline(BaselineOptions options,
+ [String resultBase = _resultBase]) async {
+ List<Future> futures = [];
+ for (var channel in options.channels) {
+ if (channel != 'main') {
+ futures.add(baselineBuilder(
+ options.builders.map((b) => '$b-$channel').toList(),
+ options.configs,
+ options.dryRun,
+ resultBase));
+ } else {
+ futures.add(baselineBuilder(
+ options.builders, options.configs, options.dryRun, resultBase));
+ }
+ }
+ await Future.wait(futures);
+}
+
+Future<void> baselineBuilder(List<String> builders, Map<String, String> configs,
+ bool dryRun, String resultBase) async {
+ var from = builders[0];
+ var to = builders[1];
+ var latest = await read('$resultBase/$from/latest');
+ var results = await read('$resultBase/$from/$latest/results.json');
+ var modifiedResults = StringBuffer();
+ for (var json in LineSplitter.split(results).map(jsonDecode)) {
+ json['build_number'] = 0;
+ json['previous_build_number'] = 0;
+ json['builder_name'] = to;
+ var configuration = configs[json['configuration']];
+ if (configuration == null) {
+ throw Exception(
+ "Missing configuration mapping for ${json['configuration']}");
+ }
+ json['configuration'] = configuration;
+ json['flaky'] = false;
+ json['previous_flaky'] = false;
+
+ modifiedResults.writeln(jsonEncode(json));
+ if (dryRun) break;
+ }
+ await write(
+ '$resultBase/$to/0/results.json', modifiedResults.toString(), dryRun);
+ await write('$resultBase/$to/latest', '0', dryRun);
+}
+
+Future<String> read(String url) {
+ return run('gsutil.py', ['cp', url, '-']);
+}
+
+Future<String> write(String url, String stdin, bool dryRun) {
+ return run('gsutil.py', ['cp', '-', url], stdin: stdin, dryRun: dryRun);
+}
+
+Future<String> run(String command, List<String> arguments,
+ {String? stdin, bool dryRun = false}) async {
+ print('Running $command $arguments...');
+ if (dryRun) {
+ if (stdin != null) {
+ print('stdin:\n$stdin');
+ }
+ return '';
+ }
+ var process = await Process.start(command, arguments);
+ if (stdin != null) {
+ process.stdin.write(stdin);
+ process.stdin.close();
+ }
+ var stdout = process.stdout.transform(utf8.decoder).join();
+ var result = await process.exitCode;
+ if (result != 0) {
+ throw Exception('Failed to run $command $arguments: $result');
+ }
+ return await stdout;
+}
diff --git a/baseline/lib/options.dart b/baseline/lib/options.dart
new file mode 100644
index 0000000..ef773e1
--- /dev/null
+++ b/baseline/lib/options.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2021, 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 'dart:io';
+
+import 'package:args/args.dart';
+
+class BaselineOptions {
+ late final bool dryRun;
+ late final List<String> builders;
+ late final Map<String, String> configs;
+ late final List<String> channels;
+
+ BaselineOptions(List<String> arguments) {
+ var parser = ArgParser();
+ parser.addMultiOption('channel',
+ abbr: 'c',
+ allowed: ['main', 'dev', 'beta', 'stable'],
+ defaultsTo: ['main'],
+ help: 'a comma separated list of channels');
+ parser.addMultiOption('config-mapping',
+ abbr: 'm',
+ help: 'a comma separated list of configuration mappings in the form:'
+ '<old1>:<new1>,<old2>:<new2>');
+ parser.addOption('builder-mapping',
+ abbr: 'b',
+ help:
+ 'a mapping from an old to a new builder in the form: <old>:<new>');
+ parser.addFlag('dry-run',
+ abbr: 'n',
+ defaultsTo: false,
+ help: 'prevents writes and only processes a single result',
+ negatable: false);
+ parser.addFlag('help',
+ abbr: 'h', negatable: false, help: 'prints this message');
+ var parsed = parser.parse(arguments);
+ if (parsed['help'] || parsed['builder-mapping'] is! String) {
+ print(parser.usage);
+ exit(0);
+ }
+ builders = (parsed['builder-mapping'] as String).split(':');
+ configs = {
+ for (var v in ((parsed['config-mapping'] as Iterable<String>)
+ .map((c) => c.split(':'))))
+ v[0]: v[1]
+ };
+ dryRun = parsed['dry-run'];
+ channels = parsed['channel'];
+ }
+}
diff --git a/baseline/pubspec.lock b/baseline/pubspec.lock
new file mode 100644
index 0000000..f469ed6
--- /dev/null
+++ b/baseline/pubspec.lock
@@ -0,0 +1,348 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ _fe_analyzer_shared:
+ dependency: transitive
+ description:
+ name: _fe_analyzer_shared
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "26.0.0"
+ analyzer:
+ dependency: transitive
+ description:
+ name: analyzer
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.3.0"
+ args:
+ dependency: "direct main"
+ description:
+ name: args
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.3.0"
+ async:
+ dependency: transitive
+ description:
+ name: async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.8.2"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ charcode:
+ dependency: transitive
+ description:
+ name: charcode
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.1"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.3.3"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.15.0"
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ coverage:
+ dependency: transitive
+ description:
+ name: coverage
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.3"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.1.2"
+ frontend_server_client:
+ dependency: transitive
+ description:
+ name: frontend_server_client
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.2"
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.1"
+ http_multi_server:
+ dependency: transitive
+ description:
+ name: http_multi_server
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.0.0"
+ io:
+ dependency: "direct dev"
+ description:
+ name: io
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.3"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.3"
+ lints:
+ dependency: "direct dev"
+ description:
+ name: lints
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
+ logging:
+ dependency: transitive
+ description:
+ name: logging
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.2"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.11"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ node_preamble:
+ dependency: transitive
+ description:
+ name: node_preamble
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.1"
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.2"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.0"
+ pedantic:
+ dependency: transitive
+ description:
+ name: pedantic
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.11.1"
+ pool:
+ dependency: transitive
+ description:
+ name: pool
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.5.0"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ shelf:
+ dependency: transitive
+ description:
+ name: shelf
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ shelf_packages_handler:
+ dependency: transitive
+ description:
+ name: shelf_packages_handler
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.0"
+ shelf_static:
+ dependency: transitive
+ description:
+ name: shelf_static
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ shelf_web_socket:
+ dependency: transitive
+ description:
+ name: shelf_web_socket
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
+ source_map_stack_trace:
+ dependency: transitive
+ description:
+ name: source_map_stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ source_maps:
+ dependency: transitive
+ description:
+ name: source_maps
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.10.10"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.1"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.10.0"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ test:
+ dependency: "direct dev"
+ description:
+ name: test
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.17.12"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.4.3"
+ test_core:
+ dependency: transitive
+ description:
+ name: test_core
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.4.2"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.0"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "7.3.0"
+ watcher:
+ dependency: transitive
+ description:
+ name: watcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ web_socket_channel:
+ dependency: transitive
+ description:
+ name: web_socket_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ webkit_inspection_protocol:
+ dependency: transitive
+ description:
+ name: webkit_inspection_protocol
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.1.0"
+sdks:
+ dart: ">=2.14.0 <3.0.0"
diff --git a/baseline/pubspec.yaml b/baseline/pubspec.yaml
new file mode 100644
index 0000000..269e260
--- /dev/null
+++ b/baseline/pubspec.yaml
@@ -0,0 +1,15 @@
+name: baseline
+description: Baselines a Dart CI builder.
+publish_to: none
+version: 0.0.0
+
+environment:
+ sdk: '>=2.14.0 <3.0.0'
+
+dependencies:
+ args: ^2.3.0
+
+dev_dependencies:
+ io: ^1.0.3
+ lints: ^1.0.0
+ test: ^1.17.12
diff --git a/baseline/test/baseline_test.dart b/baseline/test/baseline_test.dart
new file mode 100644
index 0000000..f4d429c
--- /dev/null
+++ b/baseline/test/baseline_test.dart
@@ -0,0 +1,111 @@
+// Copyright (c) 2021, 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 'dart:io';
+
+import 'package:baseline/baseline.dart';
+import 'package:baseline/options.dart';
+import 'package:io/io.dart';
+import 'package:test/test.dart';
+
+final String builderResults =
+ File('test/data/builder/42/results.json').readAsStringSync();
+final String builderStableResults =
+ File('test/data/builder-stable/12/results.json').readAsStringSync();
+
+main() {
+ test('run', () async {
+ var out = await run('echo', ['hellø!']);
+ expect(out, 'hellø!\n');
+ });
+
+ test('dry run', () async {
+ var out = await run('does not exist', ['hellø!'], dryRun: true);
+ expect(out, '');
+ });
+
+ test('run with stdin', () async {
+ var out = await run('cat', [], stdin: 'hellø!');
+ expect(out, 'hellø!');
+ });
+
+ test('run fails', () {
+ expect(run('cat', ['--fail']), throwsException);
+ });
+
+ test('baseline missing config mapping throws', () {
+ expect(
+ baseline(
+ BaselineOptions([
+ '--builder-mapping=builder:new-builder',
+ '--channel=main,stable',
+ '--config-mapping=config2:new-config2',
+ '--dry-run',
+ ]),
+ 'test/data'),
+ throwsException);
+ });
+
+ test('baseline dry-run', () async {
+ await baselineTest([
+ '--builder-mapping=builder:new-builder',
+ '--channel=main,stable',
+ '--config-mapping=config1:new-config1,config2:new-config2',
+ '--dry-run',
+ ], {
+ 'builder-stable/latest': '12',
+ 'builder-stable/12/results.json': builderStableResults,
+ 'builder/42/results.json': builderResults,
+ 'builder/latest': '42',
+ });
+ });
+
+ test('baseline', () async {
+ const newBuilderStableResults = '''
+{"build_number":0,"previous_build_number":0,"builder_name":"new-builder-stable","configuration":"new-config1","test_name":"test1","result":"PASS","flaky":false,"previous_flaky":false}
+{"build_number":0,"previous_build_number":0,"builder_name":"new-builder-stable","configuration":"new-config2","test_name":"test2","result":"FAIL","flaky":false,"previous_flaky":false}
+''';
+ const newBuilderResults = '''
+{"build_number":0,"previous_build_number":0,"builder_name":"new-builder","configuration":"new-config1","test_name":"test1","result":"FAIL","flaky":false,"previous_flaky":false}
+{"build_number":0,"previous_build_number":0,"builder_name":"new-builder","configuration":"new-config2","test_name":"test2","result":"PASS","flaky":false,"previous_flaky":false}
+''';
+
+ await baselineTest([
+ '--builder-mapping=builder:new-builder',
+ '--channel=main,stable',
+ '--config-mapping=config1:new-config1,config2:new-config2',
+ ], {
+ 'builder-stable/latest': '12',
+ 'builder-stable/12/results.json': builderStableResults,
+ 'new-builder-stable/0/results.json': newBuilderStableResults,
+ 'new-builder-stable/latest': '0',
+ 'new-builder/0/results.json': newBuilderResults,
+ 'new-builder/latest': '0',
+ 'builder/42/results.json': builderResults,
+ 'builder/latest': '42',
+ });
+ });
+}
+
+Future<void> baselineTest(
+ List<String> arguments, Map<String, String> expectedFiles) async {
+ var temp = await Directory.systemTemp.createTemp();
+ try {
+ await copyPath('test/data', temp.path);
+ await baseline(BaselineOptions(arguments), temp.path);
+ var files = temp
+ .listSync(recursive: true)
+ .whereType<File>()
+ .map((e) => e.path.substring(temp.path.length + 1));
+ expect(files, containsAll(expectedFiles.keys));
+ for (var expectedFile in expectedFiles.entries) {
+ var content =
+ await File('${temp.path}/${expectedFile.key}').readAsString();
+ expect(content, expectedFile.value,
+ reason: 'File "${expectedFile.key}" mismatch');
+ }
+ } finally {
+ await temp.delete(recursive: true);
+ }
+}
diff --git a/baseline/test/data/builder-stable/12/results.json b/baseline/test/data/builder-stable/12/results.json
new file mode 100644
index 0000000..a1fd45f
--- /dev/null
+++ b/baseline/test/data/builder-stable/12/results.json
@@ -0,0 +1,2 @@
+{"build_number":12,"previous_build_number":11,"builder_name":"builder-stable","configuration":"config1","test_name":"test1","result":"PASS","flaky":false,"previous_flaky":true}
+{"build_number":12,"previous_build_number":11,"builder_name":"builder-stable","configuration":"config2","test_name":"test2","result":"FAIL","flaky":true,"previous_flaky":false}
diff --git a/baseline/test/data/builder-stable/latest b/baseline/test/data/builder-stable/latest
new file mode 100644
index 0000000..3cacc0b
--- /dev/null
+++ b/baseline/test/data/builder-stable/latest
@@ -0,0 +1 @@
+12
\ No newline at end of file
diff --git a/baseline/test/data/builder/42/results.json b/baseline/test/data/builder/42/results.json
new file mode 100644
index 0000000..4d47a7e
--- /dev/null
+++ b/baseline/test/data/builder/42/results.json
@@ -0,0 +1,2 @@
+{"build_number":42,"previous_build_number":40,"builder_name":"builder","configuration":"config1","test_name":"test1","result":"FAIL","flaky":false,"previous_flaky":true}
+{"build_number":42,"previous_build_number":40,"builder_name":"builder","configuration":"config2","test_name":"test2","result":"PASS","flaky":true,"previous_flaky":false}
diff --git a/baseline/test/data/builder/latest b/baseline/test/data/builder/latest
new file mode 100644
index 0000000..f70d7bb
--- /dev/null
+++ b/baseline/test/data/builder/latest
@@ -0,0 +1 @@
+42
\ No newline at end of file
diff --git a/baseline/test/options_test.dart b/baseline/test/options_test.dart
new file mode 100644
index 0000000..66d9297
--- /dev/null
+++ b/baseline/test/options_test.dart
@@ -0,0 +1,42 @@
+// Copyright (c) 2021, 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 'package:baseline/options.dart';
+import 'package:test/test.dart';
+
+const _builders = '-ba:b';
+
+main() {
+ for (var channels in [
+ ['main'],
+ ['dev', 'beta'],
+ ['stable'],
+ ]) {
+ var arguments = ['-c${channels.join(',')}', _builders];
+ test('channels: "$arguments"', () {
+ var options = BaselineOptions(arguments);
+ expect(options.channels, channels);
+ });
+ }
+
+ test('builder-mapping', () {
+ var options = BaselineOptions([_builders]);
+ expect(options.builders, ['a', 'b']);
+ });
+
+ test('config-mapping', () {
+ var options = BaselineOptions(['-mc:d,e:f', _builders]);
+ expect(options.configs, {'c': 'd', 'e': 'f'});
+ });
+
+ test('dry-run defaults to false', () {
+ var options = BaselineOptions([_builders]);
+ expect(options.dryRun, false);
+ });
+
+ test('dry-run: true', () {
+ var options = BaselineOptions(['-n', _builders]);
+ expect(options.dryRun, true);
+ });
+}