[baseline] Add support to duplicate configurations

Bug: b/270918398
Change-Id: Ib125d48369ba86c527b28ee992d3fe380044928f
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/286880
Commit-Queue: William Hesse <whesse@google.com>
Auto-Submit: Alexander Thomas <athom@google.com>
Reviewed-by: William Hesse <whesse@google.com>
diff --git a/baseline/lib/baseline.dart b/baseline/lib/baseline.dart
index fd8653b..43a6a41 100644
--- a/baseline/lib/baseline.dart
+++ b/baseline/lib/baseline.dart
@@ -60,7 +60,7 @@
     String channel,
     Set<String> suites,
     String target,
-    Map<String, String> configs,
+    Map<String, List<String>> configs,
     bool dryRun,
     ConfigurationMapping mapping,
     String resultBase) async {
@@ -74,23 +74,25 @@
     for (var json in LineSplitter.split(results)
         .map(jsonDecode)
         .cast<Map<String, dynamic>>()) {
-      var configuration = mapping(json['configuration'], configs);
-      if (configuration == null) {
+      var configurations = mapping(json['configuration'], configs);
+      if (configurations == null) {
         continue;
       }
-      if (suites.isNotEmpty && !suites.contains(json['suite'])) continue;
-      json['configuration'] = configuration;
-      json['build_number'] = '0';
-      json['previous_build_number'] = '0';
-      json['builder_name'] = target;
-      json['flaky'] = false;
-      json['previous_flaky'] = false;
+      for (var configuration in configurations) {
+        if (suites.isNotEmpty && !suites.contains(json['suite'])) continue;
+        json['configuration'] = configuration;
+        json['build_number'] = '0';
+        json['previous_build_number'] = '0';
+        json['builder_name'] = target;
+        json['flaky'] = false;
+        json['previous_flaky'] = false;
 
-      var encoded = jsonEncode(json);
-      modifiedResults.writeln(encoded);
-      modifiedResultsPerConfig
-          .putIfAbsent(configuration, () => StringBuffer())
-          .writeln(encoded);
+        var encoded = jsonEncode(json);
+        modifiedResults.writeln(encoded);
+        modifiedResultsPerConfig
+            .putIfAbsent(configuration, () => StringBuffer())
+            .writeln(encoded);
+      }
       if (dryRun) break;
     }
   }
diff --git a/baseline/lib/options.dart b/baseline/lib/options.dart
index d4aea52..56b3992 100644
--- a/baseline/lib/options.dart
+++ b/baseline/lib/options.dart
@@ -8,7 +8,7 @@
 
 class BaselineOptions {
   final List<String> builders;
-  final Map<String, String> configs;
+  final Map<String, List<String>> configs;
   final bool dryRun;
   final List<String> channels;
   final ConfigurationMapping mapping;
@@ -64,14 +64,15 @@
     var mapping = parsed['ignore-unmapped']
         ? ConfigurationMapping.relaxed
         : ConfigurationMapping.strict;
-    var configs = const <String, String>{};
+    var configs = const <String, List<String>>{};
     final configMapping = parsed['config-mapping'] as List<String>;
     if (configMapping.length == 1 && configMapping.first == '*') {
       mapping = ConfigurationMapping.none;
     } else {
-      configs = {
-        for (var v in configMapping.map((c) => c.split(':'))) v[0]: v[1]
-      };
+      configs = {};
+      for (var mapping in configMapping.map((c) => c.split(':'))) {
+        configs.putIfAbsent(mapping.first, () => []).add(mapping.last);
+      }
     }
     final dryRun = parsed['dry-run'];
     final channels = parsed['channel'];
@@ -82,13 +83,15 @@
   }
 }
 
-String? _strict(String configuration, Map<String, String> configs) =>
+List<String>? _strict(
+        String configuration, Map<String, List<String>> configs) =>
     configs[configuration] ??
     (throw Exception("Missing configuration mapping for $configuration"));
-String? _relaxed(String configuration, Map<String, String> configs) =>
+List<String>? _relaxed(
+        String configuration, Map<String, List<String>> configs) =>
     configs[configuration];
-String? _none(String configuration, Map<String, String> configs) =>
-    configuration;
+List<String>? _none(String configuration, Map<String, List<String>> configs) =>
+    [configuration];
 
 enum ConfigurationMapping {
   none(_none),
@@ -96,9 +99,9 @@
   relaxed(_relaxed);
 
   const ConfigurationMapping(this.mapping);
-  final String? Function(String configuration, Map<String, String> configs)
-      mapping;
+  final List<String>? Function(
+      String configuration, Map<String, List<String>> configs) mapping;
 
-  String? call(String configuration, Map<String, String> configs) =>
+  List<String>? call(String configuration, Map<String, List<String>> configs) =>
       mapping(configuration, configs);
 }
diff --git a/baseline/test/baseline_test.dart b/baseline/test/baseline_test.dart
index e7b83b2..057c3ce 100644
--- a/baseline/test/baseline_test.dart
+++ b/baseline/test/baseline_test.dart
@@ -269,6 +269,39 @@
       ...testData,
     });
   });
+
+  test('baseline split configs', () async {
+    final newBuilderStableResults = [
+      '{"build_number":"0","previous_build_number":"0","builder_name":"new-builder-stable","configuration":"new-config1","suite":"suite1","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","suite":"suite1","test_name":"test1","result":"PASS","flaky":false,"previous_flaky":false}',
+    ];
+    final newBuilderResults = [
+      '{"build_number":"0","previous_build_number":"0","builder_name":"new-builder","configuration":"new-config1","suite":"suite1","test_name":"test1","result":"FAIL","flaky":false,"previous_flaky":false}',
+      '{"build_number":"0","previous_build_number":"0","builder_name":"new-builder","configuration":"new-config2","suite":"suite1","test_name":"test1","result":"FAIL","flaky":false,"previous_flaky":false}',
+    ];
+    await baselineTest([
+      '--builders=builder,builder2',
+      '--target=new-builder',
+      '--channel=main,stable',
+      '--config-mapping=config1:new-config1,config1:new-config2,',
+      '--ignore-unmapped',
+    ], {
+      'builders/new-builder-stable/0/results.json':
+          unorderedEquals(newBuilderStableResults),
+      'builders/new-builder-stable/latest': ['0'],
+      'builders/new-builder/0/results.json': unorderedEquals(newBuilderResults),
+      'builders/new-builder/latest': ['0'],
+      'configuration/main/new-config1/0/results.json': [newBuilderResults[0]],
+      'configuration/stable/new-config1/0/results.json': [
+        newBuilderStableResults[0]
+      ],
+      'configuration/main/new-config2/0/results.json': [newBuilderResults[1]],
+      'configuration/stable/new-config2/0/results.json': [
+        newBuilderStableResults[1]
+      ],
+      ...testData,
+    });
+  });
 }
 
 Future<void> baselineTest(
diff --git a/baseline/test/options_test.dart b/baseline/test/options_test.dart
index ed9d9c1..8fdb68d 100644
--- a/baseline/test/options_test.dart
+++ b/baseline/test/options_test.dart
@@ -33,7 +33,18 @@
 
   test('config-mapping', () {
     var options = BaselineOptions.parse(['-mc:d,e:f', ..._builders]);
-    expect(options.configs, {'c': 'd', 'e': 'f'});
+    expect(options.configs, {
+      'c': ['d'],
+      'e': ['f']
+    });
+    expect(options.mapping, ConfigurationMapping.strict);
+  });
+
+  test('config-mapping-multiple', () {
+    var options = BaselineOptions.parse(['-ma:b,a:c', ..._builders]);
+    expect(options.configs, {
+      'a': ['b', 'c']
+    });
     expect(options.mapping, ConfigurationMapping.strict);
   });
 
@@ -64,18 +75,41 @@
   });
 
   test('mapping: strict', () {
-    expect(ConfigurationMapping.strict('foo', {'foo': 'bar'}), 'bar');
-    expect(() => ConfigurationMapping.strict('oof', {'foo': 'bar'}),
+    expect(
+        ConfigurationMapping.strict('foo', {
+          'foo': ['bar']
+        }),
+        'bar');
+    expect(
+        () => ConfigurationMapping.strict('oof', {
+              'foo': ['bar']
+            }),
         throwsException);
   });
 
   test('mapping: relaxed', () {
-    expect(ConfigurationMapping.relaxed('foo', {'foo': 'bar'}), 'bar');
-    expect(ConfigurationMapping.relaxed('oof', {'foo': 'bar'}), null);
+    expect(
+        ConfigurationMapping.relaxed('foo', {
+          'foo': ['bar']
+        }),
+        'bar');
+    expect(
+        ConfigurationMapping.relaxed('oof', {
+          'foo': ['bar']
+        }),
+        null);
   });
 
   test('mapping: none', () {
-    expect(ConfigurationMapping.none('foo', {'foo': 'bar'}), 'foo');
-    expect(ConfigurationMapping.none('oof', {'foo': 'bar'}), 'oof');
+    expect(
+        ConfigurationMapping.none('foo', {
+          'foo': ['bar']
+        }),
+        'foo');
+    expect(
+        ConfigurationMapping.none('oof', {
+          'foo': ['bar']
+        }),
+        'oof');
   });
 }