[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);
+  });
+}