blob: 9a943aa853cb815f9903d6d5ea71622927756373 [file]
// 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:yaml/yaml.dart';
import './common.dart';
enum _Section { devicelab, firebase, frameworkHostOnly, other, shards }
class _TestOwnersRegistry {
_TestOwnersRegistry({required this.owners, required this.shardOwners});
final Map<String, String> owners;
final Map<String, String> shardOwners;
String? getOwner(
String targetName,
String recipe,
Map<dynamic, dynamic> properties,
List<String>? tags,
) {
final String testName =
properties['task_name'] as String? ?? _getTestNameFromTargetName(targetName);
final bool isShard =
(tags != null && tags.contains('shard')) ||
properties['shard'] != null ||
recipe == 'flutter/flutter_drone';
// Broad shard suites resolve via substring matching ('targetName.contains(shard_name)').
// Non-shard jobs resolve via direct 1:1 key lookups to prevent substring false positives.
if (isShard) {
for (final MapEntry<String, String> entry in shardOwners.entries) {
if (testName.contains(entry.key)) {
return entry.value;
}
}
return null;
}
return owners[testName] ?? owners[targetName];
}
}
List<String>? _parseTags(dynamic tagsProperty) {
if (tagsProperty == null) {
return null;
}
if (tagsProperty is YamlList) {
return tagsProperty.cast<String>().toList();
}
if (tagsProperty is String) {
try {
final dynamic decoded = loadYaml(tagsProperty);
if (decoded is YamlList) {
return decoded.cast<String>().toList();
}
if (decoded is List) {
return decoded.cast<String>().toList();
}
} catch (_) {
return null;
}
}
return null;
}
String _getTestNameFromTargetName(String targetName) {
final List<String> words = targetName.split(RegExp(r'\s+'));
return words.length < 2 ? words[0] : words.sublist(1).join(' ');
}
String? _extractOwner(String line) {
final List<String> words = line.split(RegExp(r'\s+'));
for (final word in words) {
if (word.startsWith('@') && word.length > 1) {
return word.substring(1);
}
}
return null;
}
_TestOwnersRegistry _parseTestOwners(String content) {
final owners = <String, String>{};
final shardOwners = <String, String>{};
_Section currentSection = _Section.other;
final pendingFrameworkTargets = <String>[];
final List<String> lines = content.split('\n');
for (final rawLine in lines) {
final String line = rawLine.trim();
if (line.isEmpty) {
continue;
}
if (line.startsWith('## ')) {
pendingFrameworkTargets.clear();
if (line.contains('DeviceLab tests')) {
currentSection = _Section.devicelab;
} else if (line.contains('Host only framework tests')) {
currentSection = _Section.frameworkHostOnly;
} else if (line.contains('Firebase tests')) {
currentSection = _Section.firebase;
} else if (line.contains('Shards tests')) {
currentSection = _Section.shards;
} else {
currentSection = _Section.other;
}
continue;
}
switch (currentSection) {
case _Section.devicelab:
// DeviceLab CI targets reference tasks by script basename (e.g. 'analyzer_benchmark')
// rather than full file paths ('/dev/devicelab/bin/tasks/analyzer_benchmark.dart').
if (line.startsWith('#')) {
continue;
}
final String? owner = _extractOwner(line);
if (owner != null) {
final String path = line.split(RegExp(r'\s+')).first;
final String taskName = p.basenameWithoutExtension(path);
owners[taskName] = owner;
}
case _Section.frameworkHostOnly:
// Host-only framework tests in TESTOWNERS are grouped under comment headers
// (e.g. '# Linux analyze' or multiple headers sharing a script).
// We collect pending target names from comments and map them when we hit an @owner line.
if (line.startsWith('#')) {
final List<String> words = line.split(RegExp(r'\s+'));
if (words.length > 2) {
final String name = words.sublist(2).join(' ');
pendingFrameworkTargets.add(name);
pendingFrameworkTargets.add(name.replaceAll(' ', '_'));
} else if (words.length == 2) {
pendingFrameworkTargets.add(words[1]);
}
} else {
final String? owner = _extractOwner(line);
if (owner != null) {
for (final target in pendingFrameworkTargets) {
owners[target] = owner;
}
pendingFrameworkTargets.clear();
}
}
case _Section.firebase:
// Firebase CI targets reference suites by their directory name (e.g. 'release_smoke_test')
// rather than full directory paths ('/dev/integration_tests/release_smoke_test').
if (line.startsWith('#')) {
continue;
}
final String? owner = _extractOwner(line);
if (owner != null) {
final String path = line.split(RegExp(r'\s+')).first;
final String testName = path.split('/').last;
owners[testName] = owner;
}
case _Section.shards:
// Shard suites in TESTOWNERS are defined as comment lines with owners ('# framework_tests @Piinks').
if (!line.startsWith('#') || !line.contains('@')) {
continue;
}
final String? owner = _extractOwner(line);
if (owner != null) {
final List<String> parts = line.split(RegExp(r'\s+'));
if (parts.length > 1) {
shardOwners[parts[1]] = owner;
}
}
case _Section.other:
continue;
}
}
return _TestOwnersRegistry(owners: owners, shardOwners: shardOwners);
}
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;
}();
final String testOwnersContent = io.File(p.join(flutterRoot, 'TESTOWNERS')).readAsStringSync();
final _TestOwnersRegistry registry = _parseTestOwners(testOwnersContent);
group('TESTOWNERS validation', () {
final YamlDocument ciYamlDoc = loadYamlDocument(
io.File(p.join(flutterRoot, '.ci.yaml')).readAsStringSync(),
sourceUrl: Uri.parse(p.join(flutterRoot, '.ci.yaml')),
);
final root = ciYamlDoc.contents as YamlMap;
final targets = root['targets'] as YamlList;
for (final YamlNode node in targets.nodes) {
final targetMap = node as YamlMap;
final targetName = targetMap['name'] as String;
final String recipe = targetMap['recipe'] as String? ?? '';
final Map<dynamic, dynamic> properties =
targetMap['properties'] as Map<dynamic, dynamic>? ?? <dynamic, dynamic>{};
final List<String>? tags = _parseTags(properties['tags']);
test('target "$targetName" must have an owner in TESTOWNERS', () {
final String? owner = registry.getOwner(targetName, recipe, properties, tags);
expect(owner, isNotNull, reason: 'No owner found in TESTOWNERS for target "$targetName".');
});
}
});
}