• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

google / mono_repo.dart / 18480472902

13 Oct 2025 11:08PM UTC coverage: 84.383%. First build
18480472902

Pull #514

github

web-flow
Merge c2297d867 into b825610c9
Pull Request #514: Require Dart 3.8, bump deps, regenerate code, update actions

209 of 250 new or added lines in 28 files covered. (83.6%)

1259 of 1492 relevant lines covered (84.38%)

3.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.42
/mono_repo/lib/src/package_config.dart
1
// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
2
// for details. All rights reserved. Use of this source code is governed by a
3
// BSD-style license that can be found in the LICENSE file.
4

5
import 'package:checked_yaml/checked_yaml.dart';
6
import 'package:io/ansi.dart';
7
import 'package:json_annotation/json_annotation.dart';
8
import 'package:pub_semver/pub_semver.dart';
9
import 'package:pubspec_parse/pubspec_parse.dart';
10
import 'package:yaml/yaml.dart';
11

12
import 'package_flavor.dart';
13
import 'raw_config.dart';
14
import 'task_type.dart';
15
import 'utilities.dart';
16
import 'yaml.dart';
17

18
part 'package_config.g.dart';
19

20
const monoPkgFileName = 'mono_pkg.yaml';
21

22
class PackageConfig {
23
  final String relativePath;
24
  final Pubspec pubspec;
25

26
  final List<String> oses;
27
  final List<String>? sdks;
28
  final List<String> stageNames;
29
  final List<CIJob> jobs;
30
  final List<String> cacheDirectories;
31
  final bool dartSdkConfigUsed;
32
  final bool osConfigUsed;
33

34
  PackageConfig(
9✔
35
    this.relativePath,
36
    this.pubspec,
37
    this.oses,
38
    this.sdks,
39
    this.stageNames,
40
    this.jobs,
41
    this.cacheDirectories,
42
    this.dartSdkConfigUsed,
43
    this.osConfigUsed,
44
  ) : assert(() {
18✔
45
        if (sdks == null) return true;
46
        sortNormalizeVerifySdksList(pubspec.flavor, sdks, AssertionError.new);
18✔
47
        return true;
48
      }());
9✔
49

50
  factory PackageConfig.parse(
9✔
51
    String relativePath,
52
    Pubspec pubspec,
53
    Map monoPkgYaml,
54
  ) => createWithCheck(
9✔
55
    () => PackageConfig._parse(relativePath, pubspec, monoPkgYaml),
18✔
56
  );
57

58
  factory PackageConfig._parse(
9✔
59
    String relativePath,
60
    Pubspec pubspec,
61
    Map monoPkgYaml,
62
  ) {
63
    if (monoPkgYaml.isEmpty) {
9✔
64
      // It's valid to have an empty `mono_pkg.yaml` file – it just results in
65
      // an empty config WRT travis.
66
      return PackageConfig(
7✔
67
        relativePath,
68
        pubspec,
69
        [],
7✔
70
        [],
7✔
71
        [],
7✔
72
        [],
7✔
73
        [],
7✔
74
        false,
75
        false,
76
      );
77
    }
78

79
    final flavor = pubspec.flavor;
4✔
80

81
    final rawConfig = RawConfig.fromYaml(flavor, monoPkgYaml, pubspec);
4✔
82

83
    // FYI: 'test' is default if there are no tasks defined
84
    final jobs = <CIJob>[];
4✔
85

86
    var sdkConfigUsed = false;
87
    var osConfigUsed = false;
88

89
    final stageNames = rawConfig.stages.map((stage) {
12✔
90
      final stageYaml = stage.items;
4✔
91
      for (var job in stageYaml) {
8✔
92
        var jobSdks = rawConfig.sdks;
4✔
93
        if (job case {'sdk': final jobValue}) {
8✔
94
          jobSdks = (jobValue is List)
3✔
95
              ? jobSdks = List.from(jobValue)
3✔
96
              : [jobValue as String];
2✔
97

98
          handlePubspecInSdkList(
3✔
99
            flavor,
100
            jobSdks,
101
            pubspec,
102
            (m) => CheckedFromJsonException(job, 'sdk', 'RawConfig', m),
2✔
103
          );
104
          sortNormalizeVerifySdksList(
3✔
105
            flavor,
106
            jobSdks,
107
            (m) => CheckedFromJsonException(job, 'sdk', 'RawConfig', m),
2✔
108
          );
109
        } else if (jobSdks == null || jobSdks.isEmpty) {
4✔
110
          if (monoPkgYaml.containsKey('sdk')) {
2✔
111
            throw CheckedFromJsonException(
1✔
112
              monoPkgYaml,
113
              'sdk',
114
              'RawConfig',
115
              'The value for "sdk" must be an array with at least '
116
                  'one value.',
117
            );
118
          }
119

120
          if (job is! Map) {
2✔
121
            throw ParsedYamlException(
1✔
122
              'Each item within a stage must be a map.',
123
              job is YamlNode ? job : stageYaml as YamlNode,
1✔
124
            );
125
          }
126

127
          if (job.containsKey('dart')) {
1✔
128
            throw CheckedFromJsonException(
×
129
              job as YamlMap,
130
              'dart',
131
              'RawConfig',
132
              '"dart" is no longer supported. Use "sdk" instead.',
133
            );
134
          }
135

136
          throw ParsedYamlException(
1✔
137
            'An "sdk" key is required.',
138
            job as YamlMap,
139
          );
140
        } else {
141
          sdkConfigUsed = true;
142
        }
143

144
        var jobOses = rawConfig.oses;
4✔
145
        if (job case {'os': final jobValue}) {
8✔
146
          if (jobValue is List) {
4✔
147
            jobOses = jobValue.cast<String>();
4✔
148
          } else {
149
            jobOses = [jobValue as String];
1✔
150
          }
151
        } else {
152
          osConfigUsed = true;
153
        }
154

155
        final (:description, :tasks) = CIJob.parse(
4✔
156
          job as Object,
157
          flavor: flavor,
158
        );
159
        for (var sdk in jobSdks) {
8✔
160
          for (var os in jobOses) {
8✔
161
            jobs.add(
4✔
162
              CIJob(
4✔
163
                os,
164
                relativePath,
165
                sdk,
166
                stage.name,
4✔
167
                tasks,
168
                description: description,
169
                flavor: flavor,
170
              ),
171
            );
172
          }
173
        }
174
      }
175
      return stage.name;
4✔
176
    }).toList();
4✔
177

178
    return PackageConfig(
4✔
179
      relativePath,
180
      pubspec,
181
      rawConfig.oses,
4✔
182
      rawConfig.sdks,
4✔
183
      stageNames,
184
      jobs,
185
      rawConfig.cache?.directories ?? const [],
5✔
186
      sdkConfigUsed,
187
      osConfigUsed,
188
    );
189
  }
190
}
191

192
abstract class HasStageName {
193
  String get stageName;
194
}
195

196
@JsonSerializable(
197
  explicitToJson: true,
198
  createFactory: false,
199
  ignoreUnannotated: true,
200
)
201
class CIJob implements HasStageName {
202
  @JsonKey(includeIfNull: false)
203
  final String? description;
204

205
  @JsonKey()
206
  final String os;
207

208
  /// Relative path to the directory containing the source package from the root
209
  /// of the repository.
210
  @JsonKey()
211
  final String package;
212

213
  @JsonKey()
214
  final String sdk;
215

216
  @override
217
  @JsonKey()
218
  final String stageName;
219

220
  @JsonKey()
221
  final List<Task> tasks;
222

223
  @JsonKey()
224
  final PackageFlavor flavor;
225

226
  Iterable<String> get _taskCommandsTickQuoted =>
2✔
227
      tasks.map((t) => '`${t.command}`');
10✔
228

229
  /// The description of the job in the CI environment.
230
  String get name => description ?? _taskCommandsTickQuoted.join(', ');
8✔
231

232
  /// Values used to group jobs together.
233
  List<String> get groupByKeys => [os, stageName, sdk];
10✔
234

235
  /// Values used to sort jobs within a group.
236
  String get sortBits => [...groupByKeys, package, name].join(':::');
12✔
237

238
  CIJob(
4✔
239
    this.os,
240
    this.package,
241
    this.sdk,
242
    this.stageName,
243
    this.tasks, {
244
    this.description,
245
    required this.flavor,
246
  }) : assert(
247
         errorForSdkConfig(flavor, sdk) == null,
4✔
NEW
248
         'Should have caught bad sdk value `$sdk` before here!',
×
249
       );
250

251
  static ({String? description, List<Task> tasks}) parse(
4✔
252
    Object yaml, {
253
    required PackageFlavor flavor,
254
  }) {
255
    String? description;
256
    Object withoutDescription;
257
    if (yaml is Map && yaml.containsKey('description')) {
8✔
258
      withoutDescription = transferYamlMap(yaml as YamlMap);
3✔
259
      description = (withoutDescription as Map).remove('description') as String;
3✔
260
    } else {
261
      withoutDescription = yaml;
262
    }
263
    final tasks = Task.parseTaskOrGroup(flavor, withoutDescription);
4✔
264
    return (tasks: tasks, description: description);
265
  }
266

267
  /// If [sdk] is a valid [Version], return it. Otherwise, `null`.
268
  @JsonKey(includeToJson: false, includeFromJson: false)
2✔
269
  Version? get explicitSdkVersion {
270
    try {
271
      return Version.parse(sdk);
4✔
272
    } on FormatException {
2✔
273
      return null;
274
    }
275
  }
276

277
  Map<String, dynamic> toJson() => _$CIJobToJson(this);
2✔
278
}
279

280
@JsonSerializable(
281
  createFactory: false,
282
  ignoreUnannotated: true,
283
  includeIfNull: false,
284
)
285
class Task {
286
  @JsonKey()
287
  final PackageFlavor flavor;
288

289
  @JsonKey()
290
  final TaskType type;
291

292
  @JsonKey()
293
  final String? args;
294

295
  final String command;
296

297
  Task(this.flavor, this.type, {this.args})
4✔
298
    : command = type.commandValue(flavor, args).join(' ');
8✔
299

300
  /// Parses an individual item under `stages`, which might be a `group` or an
301
  /// individual task.
302
  static List<Task> parseTaskOrGroup(PackageFlavor flavor, Object yamlValue) {
4✔
303
    if (yamlValue is Map) {
4✔
304
      final group = yamlValue['group'];
4✔
305
      if (group != null) {
306
        if (group is List) {
3✔
307
          return group
308
              .map((taskYaml) => Task.parse(flavor, taskYaml as Object))
9✔
309
              .toList();
3✔
310
        } else {
311
          throw CheckedFromJsonException(
1✔
312
            yamlValue,
313
            'group',
314
            'group',
315
            'expected a list of tasks',
316
          );
317
        }
318
      }
319
    }
320
    return [Task.parse(flavor, yamlValue)];
8✔
321
  }
322

323
  factory Task.parse(PackageFlavor flavor, Object yamlValue) {
4✔
324
    if (yamlValue is String) {
4✔
325
      if (yamlValue == TaskType.command.name) {
8✔
326
        throw ArgumentError.value(yamlValue, 'command', 'requires a value');
×
327
      }
328
      return Task(flavor, _taskTypeForName(yamlValue));
8✔
329
    }
330

331
    if (yamlValue is Map) {
4✔
332
      final taskNames = yamlValue.keys
4✔
333
          .where(TaskType.allowedTaskNames.contains)
12✔
334
          .cast<String>()
4✔
335
          .toList();
4✔
336
      if (taskNames.isEmpty) {
4✔
337
        String? key;
338
        if (yamlValue.isNotEmpty) {
1✔
339
          key = yamlValue.keys.first as String;
2✔
340
        }
341
        throw CheckedFromJsonException(
1✔
342
          yamlValue,
343
          key,
344
          'Task',
345
          'Must have one key of ${TaskType.prettyTaskList}.',
2✔
346
          badKey: true,
347
        );
348
      }
349
      if (taskNames.length > 1) {
8✔
350
        throw CheckedFromJsonException(
1✔
351
          yamlValue,
352
          taskNames.skip(1).first,
2✔
353
          'Task',
354
          'Must have one and only one key of ${TaskType.prettyTaskList}.',
2✔
355
          badKey: true,
356
        );
357
      }
358

359
      final taskName = taskNames.single;
4✔
360
      final taskType = _taskTypeForName(taskName);
4✔
361

362
      String? args;
363
      switch (taskType) {
364
        case TaskType.command:
4✔
365
          final taskValue = yamlValue[taskType.name];
2✔
366
          if (taskValue is String) {
1✔
367
            args = taskValue;
368
          } else if (taskValue is List &&
1✔
369
              taskValue.every((element) => element is String)) {
×
370
            args = taskValue.join(' && ');
×
371
          } else {
372
            throw CheckedFromJsonException(
1✔
373
              yamlValue,
374
              taskType.name,
1✔
375
              'TaskType',
376
              'Only supports a string or array of strings',
377
            );
378
          }
379
          break;
380
        default:
381
          // NOTE: using `taskName.single` in case it's a deprecated name
382
          args = yamlValue[taskName] as String?;
4✔
383
      }
384

385
      final extraConfig = Set<String>.from(yamlValue.keys)
8✔
386
        ..removeAll([taskName, 'os', 'sdk']);
8✔
387

388
      // TODO(kevmoo): at some point, support custom configuration here
389
      if (extraConfig.isNotEmpty) {
4✔
390
        throw CheckedFromJsonException(
1✔
391
          yamlValue,
392
          extraConfig.first,
1✔
393
          'Task',
394
          'Extra config options are not currently supported.',
395
          badKey: true,
396
        );
397
      }
398
      try {
399
        return Task(flavor, taskType, args: args);
4✔
400
      } on InvalidTaskConfigException catch (e) {
1✔
401
        throw CheckedFromJsonException(yamlValue, taskName, 'Task', e.message);
2✔
402
      }
403
    }
404

405
    if (yamlValue is YamlNode) {
1✔
406
      throw ParsedYamlException('Must be a map or a string.', yamlValue);
1✔
407
    }
408

409
    throw ArgumentError('huh? $yamlValue ${yamlValue.runtimeType}');
×
410
  }
411

412
  Map<String, dynamic> toJson() => _$TaskToJson(this);
2✔
413

414
  /// Stores the job names we've already warned about. Only warn once!
415
  static final _warnedNames = <TaskType>{};
2✔
416

417
  static TaskType _taskTypeForName(String input) {
4✔
418
    final taskName = TaskType.taskFromName(input);
4✔
419
    if (taskName.name != input && _warnedNames.add(taskName)) {
10✔
420
      print(
1✔
421
        yellow.wrap(
2✔
422
          '"$input" is deprecated. Use "$taskName" instead to define tasks in '
423
          '`$monoPkgFileName`.',
424
        ),
425
      );
426
    }
427
    return taskName;
428
  }
429
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc