diff --git a/dev/bots/test/ci_yaml_validation_test.dart b/dev/bots/test/ci_yaml_validation_test.dart new file mode 100644 index 0000000000..fd514c1395 --- /dev/null +++ b/dev/bots/test/ci_yaml_validation_test.dart @@ -0,0 +1,161 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('vm') +library; + +import 'dart:io' as io; + +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import './common.dart'; + +void main() { + final String flutterRoot = () { + io.Directory current = io.Directory.current; + while (!io.File(p.join(current.path, 'DEPS')).existsSync()) { + if (current.path == current.parent.path) { + fail( + 'Could not find flutter repository root (${io.Directory.current.path} -> ${current.path})', + ); + } + current = current.parent; + } + return current.path; + }(); + + group('framework', () { + final List<_CiYamlTarget> targets = _CiYamlTarget.parseAll(p.join(flutterRoot, '.ci.yaml')); + + for (final _CiYamlTarget target in targets) { + if (target.runIf == null || target.runIf!.isEmpty) { + continue; + } + + setUp(() { + printOnFailure(target.span.message('One or more errors occurred validating')); + }); + + group(target.name, () { + test('must include .ci.yaml', () { + expect( + target.runIf, + contains('.ci.yaml'), + reason: '.ci.yaml inclusion means changes to the runIfs will trigger presubmit tests.', + ); + }); + + test('must include DEPS', () { + expect( + target.runIf, + contains('DEPS'), + reason: 'DEPS updates (including the Dart SDK) mean presubmit tests must be run.', + ); + }); + + test('must include the engine sources', () { + expect( + target.runIf, + contains('engine/**'), + reason: 'Engine updates means framework presubmit tests must be run.', + ); + }); + }); + } + }); + + group('engine', () { + final List<_CiYamlTarget> targets = _CiYamlTarget.parseAll( + p.join(flutterRoot, 'engine', 'src', 'flutter', '.ci.yaml'), + ); + + for (final _CiYamlTarget target in targets) { + if (target.runIf == null || target.runIf!.isEmpty) { + continue; + } + + setUp(() { + printOnFailure(target.span.message('One or more errors occurred validating')); + }); + + group(target.name, () { + test('must include .ci.yaml', () { + expect( + target.runIf, + contains('engine/src/flutter/.ci.yaml'), + reason: '.ci.yaml inclusion means changes to the runIfs will trigger presubmit tests.', + ); + }); + + test('must include DEPS', () { + expect( + target.runIf, + contains('DEPS'), + reason: 'DEPS updates (including the Dart SDK) mean presubmit tests must be run.', + ); + }); + }); + } + }); +} + +/// A minimal representation of an ostensibly well-formatted `.ci.yaml` file. +/// +/// Due to the repository setup, it's not possible to reuse the existing +/// specifications of this file, and since the test case is only testing a +/// subset of the encoding, this class exposes only that subset. +/// +/// For a discussion leading to this design decision, see +/// . +/// +/// See also: +/// - [`scheduler.proto`][1], the schema definition of the file format. +/// - [`CI_YAML.md`][2], a human-authored description of the file format. +/// - [`ci_yaml.dart`][3], where validation is performed (in `flutter/cocoon`). +/// +/// [1]: https://github.com/flutter/cocoon/blob/main/app_dart/lib/src/model/proto/internal/scheduler.proto +/// [2]: https://github.com/flutter/cocoon/blob/main/CI_YAML.md +/// [3]: https://github.com/flutter/cocoon/blob/main/app_dart/lib/src/model/ci_yaml/ci_yaml.dart +final class _CiYamlTarget { + _CiYamlTarget({required this.name, required this.span, required this.runIf}); + + factory _CiYamlTarget.fromYamlMap(YamlMap map) { + return _CiYamlTarget( + name: map['name'] as String, + span: map.span, + runIf: () { + final YamlList? runIf = map['runIf'] as YamlList?; + if (runIf == null) { + return null; + } + return runIf.cast().toList(); + }(), + ); + } + + /// Parses a list of targets from the provided `.ci.yaml` file [path]. + static List<_CiYamlTarget> parseAll(String path) { + final YamlDocument yamlDoc = loadYamlDocument( + io.File(path).readAsStringSync(), + sourceUrl: Uri.parse(path), + ); + + final YamlMap root = yamlDoc.contents as YamlMap; + final YamlList targets = root['targets'] as YamlList; + return targets.nodes.map((YamlNode node) { + return _CiYamlTarget.fromYamlMap(node as YamlMap); + }).toList(); + } + + /// Name of the target. + final String name; + + /// Where the target was parsed at in the `.ci.yaml` file. + final SourceSpan span; + + /// Which lines were present in a `runIf` block, if any. + final List? runIf; +}