flutter/dev/bots/test/ci_yaml_validation_test.dart
Matan Lurey 13ae4cd8d8
Move validation logic for .ci.yaml to flutter/flutter. (#161249)
Towards https://github.com/flutter/flutter/issues/160915 (future
cleanup: remove the validation from `flutter/cocoon`).

This PR effectively duplicates the logic in
https://github.com/flutter/cocoon/pull/4137, in `flutter/flutter`.

/cc @jtmcdole
2025-01-07 20:52:28 +00:00

162 lines
4.9 KiB
Dart

// 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
/// <https://github.com/flutter/flutter/issues/160915>.
///
/// 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<String>().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<String>? runIf;
}