mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Adds a Dart library for loading and parsing build configs (flutter/engine#45390)
Also adds a test that the build configs in the repo are valid json that matches the spec. Fleshed out @christopherfujino's code from here https://github.com/christopherfujino/flutter-engine-runner/blob/main/main.dart
This commit is contained in:
parent
89fb5205ea
commit
81534e8ace
@ -69,6 +69,10 @@
|
||||
"language": "dart",
|
||||
"name": "test: Lint android host",
|
||||
"script": "flutter/tools/android_lint/bin/main.dart"
|
||||
},
|
||||
{
|
||||
"name": "Check build configs",
|
||||
"script": "flutter/ci/check_build_configs.sh"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -239,6 +239,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"generators": [],
|
||||
"generators": {},
|
||||
"archives": []
|
||||
}
|
||||
|
||||
41
engine/src/flutter/ci/check_build_configs.sh
Executable file
41
engine/src/flutter/ci/check_build_configs.sh
Executable file
@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2013 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.
|
||||
|
||||
set -e
|
||||
|
||||
# Needed because if it is set, cd may print the path it changed to.
|
||||
unset CDPATH
|
||||
|
||||
# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one
|
||||
# link at a time, and then cds into the link destination and find out where it
|
||||
# ends up.
|
||||
#
|
||||
# The function is enclosed in a subshell to avoid changing the working directory
|
||||
# of the caller.
|
||||
function follow_links() (
|
||||
cd -P "$(dirname -- "$1")"
|
||||
file="$PWD/$(basename -- "$1")"
|
||||
while [[ -h "$file" ]]; do
|
||||
cd -P "$(dirname -- "$file")"
|
||||
file="$(readlink -- "$file")"
|
||||
cd -P "$(dirname -- "$file")"
|
||||
file="$PWD/$(basename -- "$file")"
|
||||
done
|
||||
echo "$file"
|
||||
)
|
||||
|
||||
SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")")
|
||||
SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)"
|
||||
FLUTTER_DIR="$(cd "$SCRIPT_DIR/.."; pwd -P)"
|
||||
DART_BIN="${SRC_DIR}/third_party/dart/tools/sdks/dart-sdk/bin"
|
||||
DART="${DART_BIN}/dart"
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
"$DART" \
|
||||
--disable-dart-dev \
|
||||
"$SRC_DIR/flutter/tools/pkg/engine_build_configs/bin/check.dart" \
|
||||
"$SRC_DIR"
|
||||
|
||||
@ -972,6 +972,25 @@ def gather_build_bucket_golden_scraper_tests(build_dir):
|
||||
)
|
||||
|
||||
|
||||
def gather_engine_build_configs_tests(build_dir):
|
||||
test_dir = os.path.join(
|
||||
BUILDROOT_DIR, 'flutter', 'tools', 'pkg', 'engine_build_configs'
|
||||
)
|
||||
dart_tests = glob.glob('%s/*_test.dart' % test_dir)
|
||||
for dart_test_file in dart_tests:
|
||||
opts = [
|
||||
'--disable-dart-dev',
|
||||
dart_test_file,
|
||||
]
|
||||
yield EngineExecutableTask(
|
||||
build_dir,
|
||||
os.path.join('dart-sdk', 'bin', 'dart'),
|
||||
None,
|
||||
flags=opts,
|
||||
cwd=test_dir
|
||||
)
|
||||
|
||||
|
||||
def gather_engine_repo_tools_tests(build_dir):
|
||||
test_dir = os.path.join(
|
||||
BUILDROOT_DIR, 'flutter', 'tools', 'pkg', 'engine_repo_tools'
|
||||
@ -1269,6 +1288,7 @@ Flutter Wiki page on the subject: https://github.com/flutter/flutter/wiki/Testin
|
||||
tasks += list(gather_githooks_tests(build_dir))
|
||||
tasks += list(gather_clang_tidy_tests(build_dir))
|
||||
tasks += list(gather_build_bucket_golden_scraper_tests(build_dir))
|
||||
tasks += list(gather_engine_build_configs_tests(build_dir))
|
||||
tasks += list(gather_engine_repo_tools_tests(build_dir))
|
||||
tasks += list(gather_api_consistency_tests(build_dir))
|
||||
tasks += list(gather_path_ops_tests(build_dir))
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
include: ../../../analysis_options.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
public_member_api_docs: false
|
||||
@ -0,0 +1,67 @@
|
||||
// Copyright 2013 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.
|
||||
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:engine_build_configs/engine_build_configs.dart';
|
||||
import 'package:engine_repo_tools/engine_repo_tools.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
// Usage:
|
||||
// $ dart bin/check.dart [/path/to/engine/src]
|
||||
|
||||
void main(List<String> args) {
|
||||
final String? engineSrcPath;
|
||||
if (args.isNotEmpty) {
|
||||
engineSrcPath = args[0];
|
||||
} else {
|
||||
engineSrcPath = null;
|
||||
}
|
||||
|
||||
// Find the engine repo.
|
||||
final Engine engine;
|
||||
try {
|
||||
engine = Engine.findWithin(engineSrcPath);
|
||||
} catch (e) {
|
||||
io.stderr.writeln(e);
|
||||
io.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find and parse the engine build configs.
|
||||
final io.Directory buildConfigsDir = io.Directory(p.join(
|
||||
engine.flutterDir.path, 'ci', 'builders',
|
||||
));
|
||||
final BuildConfigLoader loader = BuildConfigLoader(
|
||||
buildConfigsDir: buildConfigsDir,
|
||||
);
|
||||
|
||||
// Treat it as an error if no build configs were found. The caller likely
|
||||
// expected to find some.
|
||||
final Map<String, BuildConfig> configs = loader.configs;
|
||||
if (configs.isEmpty) {
|
||||
io.stderr.writeln(
|
||||
'Error: No build configs found under ${buildConfigsDir.path}',
|
||||
);
|
||||
io.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (loader.errors.isNotEmpty) {
|
||||
loader.errors.forEach(io.stderr.writeln);
|
||||
io.exitCode = 1;
|
||||
}
|
||||
|
||||
// Check the parsed build configs for validity.
|
||||
for (final String name in configs.keys) {
|
||||
final BuildConfig buildConfig = configs[name]!;
|
||||
final List<String> buildConfigErrors = buildConfig.check(name);
|
||||
if (buildConfigErrors.isNotEmpty) {
|
||||
io.stderr.writeln('Errors in ${buildConfig.path}:');
|
||||
io.exitCode = 1;
|
||||
}
|
||||
for (final String error in buildConfigErrors) {
|
||||
io.stderr.writeln(' $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
// Copyright 2013 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.
|
||||
|
||||
/// This is a library for parsing the Engine CI configurations that live under
|
||||
/// flutter/ci/builders. They describe how CI builds, tests, archives, and
|
||||
/// uploads the engine to cloud storage. The documentation and spec for the
|
||||
/// format is at:
|
||||
///
|
||||
/// https://github.com/flutter/engine/blob/main/ci/builders/README.md
|
||||
///
|
||||
/// The code in this library is *not* used by CI to run these configurations.
|
||||
/// Rather, that code executes these configs on CI is part of the "engine_v2"
|
||||
/// recipes at:
|
||||
///
|
||||
/// https://cs.opensource.google/flutter/recipes/+/main:recipes/engine_v2
|
||||
///
|
||||
/// This library exposes two main classes, [BuildConfigLoader], which reads and
|
||||
/// loads all build configurations under a directory, and [BuildConfig], which
|
||||
/// is the Dart representation of a single build configuration.
|
||||
library;
|
||||
|
||||
export 'src/build_config.dart';
|
||||
export 'src/build_config_loader.dart';
|
||||
@ -0,0 +1,843 @@
|
||||
// Copyright 2013 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.
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
// This library parses Engine build config data out of the "Engine v2" build
|
||||
// config JSON files with the format described at:
|
||||
// https://github.com/flutter/engine/blob/main/ci/builders/README.md
|
||||
|
||||
/// Base class for all nodes in the build config.
|
||||
sealed class BuildConfigBase {
|
||||
BuildConfigBase(this.errors);
|
||||
|
||||
/// Accumulated errors. Non-null and non-empty when a node is invalid.
|
||||
final List<String>? errors;
|
||||
|
||||
/// Whether there were errors when loading the data for this node.
|
||||
late final bool valid = errors == null;
|
||||
|
||||
/// Returns an empty list when the object is valid, and errors when it is not.
|
||||
/// Subclasses with more data to check for validity should override this
|
||||
/// method and add `super.check(path)` to the returned list.
|
||||
@mustCallSuper
|
||||
List<String> check(String path) {
|
||||
if (valid) {
|
||||
return <String>[];
|
||||
}
|
||||
return errors!.map((String s) => '$path: $s').toList();
|
||||
}
|
||||
}
|
||||
|
||||
/// The build configuration is a json file containing a list of builds, tests,
|
||||
/// generators and archives.
|
||||
///
|
||||
/// Each build config file contains a top-level json map with the following
|
||||
/// fields:
|
||||
/// {
|
||||
/// "builds": [],
|
||||
/// "tests": [],
|
||||
/// "generators": {
|
||||
/// "tasks": []
|
||||
/// },
|
||||
/// "archives": []
|
||||
/// }
|
||||
final class BuildConfig extends BuildConfigBase {
|
||||
/// Load build configuration data into an instance of this class.
|
||||
///
|
||||
/// [path] should be the file system path to the file that the JSON data comes
|
||||
/// from. [map] must be the JSON data returned by e.g. `JsonDecoder.convert`.
|
||||
factory BuildConfig.fromJson({
|
||||
required String path,
|
||||
required Map<String, Object?> map,
|
||||
}) {
|
||||
final List<String> errors = <String>[];
|
||||
|
||||
// Parse the "builds" field.
|
||||
final List<GlobalBuild>? builds = objListOfJson<GlobalBuild>(
|
||||
map, 'builds', errors, GlobalBuild.fromJson,
|
||||
);
|
||||
|
||||
// Parse the "tests" field.
|
||||
final List<GlobalTest>? tests = objListOfJson<GlobalTest>(
|
||||
map, 'tests', errors, GlobalTest.fromJson,
|
||||
);
|
||||
|
||||
// Parse the "generators" field.
|
||||
final List<TestTask>? generators;
|
||||
if (map['generators'] == null) {
|
||||
generators = <TestTask>[];
|
||||
} else if (map['generators'] is! Map<String, Object?>) {
|
||||
appendTypeError(map, 'generators', 'map', errors);
|
||||
generators = null;
|
||||
} else {
|
||||
generators = objListOfJson(
|
||||
map['generators']! as Map<String, Object?>,
|
||||
'tasks',
|
||||
errors,
|
||||
TestTask.fromJson,
|
||||
);
|
||||
}
|
||||
|
||||
// Parse the "archives" field.
|
||||
final List<GlobalArchive>? archives = objListOfJson<GlobalArchive>(
|
||||
map, 'archives', errors, GlobalArchive.fromJson,
|
||||
);
|
||||
|
||||
if (builds == null ||
|
||||
tests == null ||
|
||||
generators == null ||
|
||||
archives == null) {
|
||||
return BuildConfig._invalid(path, errors);
|
||||
}
|
||||
return BuildConfig._(path, builds, tests, generators, archives);
|
||||
}
|
||||
|
||||
BuildConfig._(
|
||||
this.path,
|
||||
this.builds,
|
||||
this.tests,
|
||||
this.generators,
|
||||
this.archives,
|
||||
) : super(null);
|
||||
|
||||
BuildConfig._invalid(this.path, super.errors) :
|
||||
builds = <GlobalBuild>[],
|
||||
tests = <GlobalTest>[],
|
||||
generators = <TestTask>[],
|
||||
archives = <GlobalArchive>[];
|
||||
|
||||
/// The path to the JSON file.
|
||||
final String path;
|
||||
|
||||
/// A list of independent builds that have no dependencies among them. They
|
||||
/// can run in parallel if need be.
|
||||
final List<GlobalBuild> builds;
|
||||
|
||||
/// A list of tests. The tests may have dependencies on one or more of the
|
||||
/// builds.
|
||||
final List<GlobalTest> tests;
|
||||
|
||||
/// A list of generator tasks that produce additional artifacts, which may
|
||||
/// depend on the output of one or more builds.
|
||||
final List<TestTask> generators;
|
||||
|
||||
/// A description of the upload instructions for the artifacts produced by
|
||||
/// the global generators.
|
||||
final List<GlobalArchive> archives;
|
||||
|
||||
@override
|
||||
List<String> check(String path) {
|
||||
final List<String> errors = <String>[];
|
||||
errors.addAll(super.check(path));
|
||||
for (int i = 0; i < builds.length; i++) {
|
||||
final GlobalBuild build = builds[i];
|
||||
errors.addAll(build.check('$path/builds[$i]'));
|
||||
}
|
||||
for (int i = 0; i < tests.length; i++) {
|
||||
final GlobalTest test = tests[i];
|
||||
errors.addAll(test.check('$path/tests[$i]'));
|
||||
}
|
||||
for (int i = 0; i < generators.length; i++) {
|
||||
final TestTask task = generators[i];
|
||||
errors.addAll(task.check('$path/generators/tasks[$i]'));
|
||||
}
|
||||
for (int i = 0; i < archives.length; i++) {
|
||||
final GlobalArchive archive = archives[i];
|
||||
errors.addAll(archive.check('$path/archives[$i]'));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
/// A "build" is a dictionary with a gn command, a ninja command, zero or more
|
||||
/// generator commands, zero or more local tests, zero or more local generators
|
||||
/// and zero or more output artifacts.
|
||||
///
|
||||
/// "builds" contains a list of maps with fields like:
|
||||
/// {
|
||||
/// "name": "",
|
||||
/// "gn": [""],
|
||||
/// "ninja": {},
|
||||
/// "tests": [],
|
||||
/// "generators": {
|
||||
/// "tasks": []
|
||||
/// }, (optional)
|
||||
/// "archives": [],
|
||||
/// "drone_dimensions": [""],
|
||||
/// "gclient_variables": {}
|
||||
/// }
|
||||
final class GlobalBuild extends BuildConfigBase {
|
||||
factory GlobalBuild.fromJson(Map<String, Object?> map) {
|
||||
final List<String> errors = <String>[];
|
||||
final String? name = stringOfJson(map, 'name', errors);
|
||||
final List<String>? gn = stringListOfJson(map, 'gn', errors);
|
||||
final List<BuildTest>? tests = objListOfJson(
|
||||
map, 'tests', errors, BuildTest.fromJson,
|
||||
);
|
||||
final List<BuildArchive>? archives = objListOfJson(
|
||||
map, 'archives', errors, BuildArchive.fromJson,
|
||||
);
|
||||
final List<String>? droneDimensions = stringListOfJson(
|
||||
map, 'drone_dimensions', errors,
|
||||
);
|
||||
|
||||
final BuildNinja? ninja;
|
||||
if (map['ninja'] == null) {
|
||||
ninja = BuildNinja.nop();
|
||||
} else if (map['ninja'] is! Map<String, Object?>) {
|
||||
ninja = null;
|
||||
} else {
|
||||
ninja = BuildNinja.fromJson(map['ninja']! as Map<String, Object?>);
|
||||
}
|
||||
if (ninja == null) {
|
||||
appendTypeError(map, 'ninja', 'map', errors);
|
||||
}
|
||||
|
||||
final List<BuildTask>? generators;
|
||||
if (map['generators'] == null) {
|
||||
generators = <BuildTask>[];
|
||||
} else if (map['generators'] is! Map<String, Object?>) {
|
||||
appendTypeError(map, 'generators', 'map', errors);
|
||||
generators = null;
|
||||
} else {
|
||||
generators = objListOfJson(
|
||||
map['generators']! as Map<String, Object?>,
|
||||
'tasks',
|
||||
errors,
|
||||
BuildTask.fromJson,
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, Object?>? gclientVariables;
|
||||
if (map['gclient_variables'] == null) {
|
||||
gclientVariables = <String, Object?>{};
|
||||
} else if (map['gclient_variables'] is! Map<String, Object?>) {
|
||||
gclientVariables = null;
|
||||
} else {
|
||||
gclientVariables = map['gclient_variables']! as Map<String, Object?>;
|
||||
}
|
||||
if (gclientVariables == null) {
|
||||
appendTypeError(map, 'gclient_variables', 'map', errors);
|
||||
}
|
||||
|
||||
if (name == null ||
|
||||
gn == null ||
|
||||
ninja == null ||
|
||||
archives == null ||
|
||||
tests == null ||
|
||||
generators == null ||
|
||||
droneDimensions == null ||
|
||||
gclientVariables == null) {
|
||||
return GlobalBuild._invalid(errors);
|
||||
}
|
||||
return GlobalBuild._(
|
||||
name, gn, ninja, tests, generators, archives, droneDimensions,
|
||||
gclientVariables,
|
||||
);
|
||||
}
|
||||
|
||||
GlobalBuild._(
|
||||
this.name,
|
||||
this.gn,
|
||||
this.ninja,
|
||||
this.tests,
|
||||
this.generators,
|
||||
this.archives,
|
||||
this.droneDimensions,
|
||||
this.gclientVariables,
|
||||
) : super(null);
|
||||
|
||||
GlobalBuild._invalid(super.errors) :
|
||||
name = '',
|
||||
gn = <String>[],
|
||||
ninja = BuildNinja.nop(),
|
||||
tests = <BuildTest>[],
|
||||
generators = <BuildTask>[],
|
||||
archives = <BuildArchive>[],
|
||||
droneDimensions = <String>[],
|
||||
gclientVariables = <String, Object?>{};
|
||||
|
||||
/// The name of the build which may also be used to reference it as a
|
||||
/// depdendency of a global test.
|
||||
final String name;
|
||||
|
||||
/// The parameters to pass to `flutter/tools/gn` to configure the build.
|
||||
final List<String> gn;
|
||||
|
||||
/// The data to form the ninja command to perform the build.
|
||||
final BuildNinja ninja;
|
||||
|
||||
/// The list of tests that can be run after the ninja build is finished.
|
||||
final List<BuildTest> tests;
|
||||
|
||||
/// A list of other tasks that may generate new artifacts after the ninja
|
||||
/// build is finished.
|
||||
final List<BuildTask> generators;
|
||||
|
||||
/// Upload instructions for the artifacts produced by the build.
|
||||
final List<BuildArchive> archives;
|
||||
|
||||
/// A list 'key=value' strings that are used to select the bot where this
|
||||
/// build will be running.
|
||||
final List<String> droneDimensions;
|
||||
|
||||
/// A dictionary with variables included in the `custom_vars` section of the
|
||||
/// .gclient file before `gclient sync` is run.
|
||||
final Map<String, Object?> gclientVariables;
|
||||
|
||||
@override
|
||||
List<String> check(String path) {
|
||||
final List<String> errors = <String>[];
|
||||
errors.addAll(super.check(path));
|
||||
errors.addAll(ninja.check('$path/ninja'));
|
||||
for (int i = 0; i < tests.length; i++) {
|
||||
final BuildTest test = tests[i];
|
||||
errors.addAll(test.check('$path/tests[$i]'));
|
||||
}
|
||||
for (int i = 0; i < generators.length; i++) {
|
||||
final BuildTask task = generators[i];
|
||||
errors.addAll(task.check('$path/generators/tasks[$i]'));
|
||||
}
|
||||
for (int i = 0; i < archives.length; i++) {
|
||||
final BuildArchive archive = archives[i];
|
||||
errors.addAll(archive.check('$path/archives[$i]'));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
/// "builds" -> "ninja" contains a map with fields like:
|
||||
/// {
|
||||
/// "config": "",
|
||||
/// "targets": [""]
|
||||
/// },
|
||||
final class BuildNinja extends BuildConfigBase {
|
||||
factory BuildNinja.fromJson(Map<String, Object?> map) {
|
||||
final List<String> errors = <String>[];
|
||||
final String? config = stringOfJson(map, 'config', errors);
|
||||
final List<String>? targets = stringListOfJson(map, 'targets', errors);
|
||||
if (config == null || targets == null) {
|
||||
return BuildNinja._invalid(errors);
|
||||
}
|
||||
return BuildNinja._(config, targets);
|
||||
}
|
||||
|
||||
BuildNinja._(this.config, this.targets) : super(null);
|
||||
|
||||
BuildNinja._invalid(super.errors) :
|
||||
config = '',
|
||||
targets = <String>[];
|
||||
|
||||
BuildNinja.nop() :
|
||||
config = '',
|
||||
targets = <String>[],
|
||||
super(null);
|
||||
|
||||
/// The name of the configuration created by gn.
|
||||
///
|
||||
/// This is also the subdirectory of the `out/` directory where the build
|
||||
/// output will go.
|
||||
final String config;
|
||||
|
||||
/// The ninja targets to build.
|
||||
final List<String> targets;
|
||||
}
|
||||
|
||||
/// "builds" -> "tests" contains a list of maps with fields like:
|
||||
/// {
|
||||
/// "language": "",
|
||||
/// "name": "",
|
||||
/// "parameters": [""],
|
||||
/// "script": "",
|
||||
/// "contexts": [""]
|
||||
/// }
|
||||
final class BuildTest extends BuildConfigBase {
|
||||
factory BuildTest.fromJson(Map<String, Object?> map) {
|
||||
final List<String> errors = <String>[];
|
||||
final String? name = stringOfJson(map, 'name', errors);
|
||||
final String? language = stringOfJson(map, 'language', errors);
|
||||
final String? script = stringOfJson(map, 'script', errors);
|
||||
final List<String>? parameters = stringListOfJson(
|
||||
map, 'parameters', errors,
|
||||
);
|
||||
final List<String>? contexts = stringListOfJson(
|
||||
map, 'contexts', errors,
|
||||
);
|
||||
if (name == null ||
|
||||
language == null ||
|
||||
script == null ||
|
||||
parameters == null ||
|
||||
contexts == null) {
|
||||
return BuildTest._invalid(errors);
|
||||
}
|
||||
return BuildTest._(name, language, script, parameters, contexts);
|
||||
}
|
||||
|
||||
BuildTest._(
|
||||
this.name,
|
||||
this.language,
|
||||
this.script,
|
||||
this.parameters,
|
||||
this.contexts,
|
||||
) : super(null);
|
||||
|
||||
BuildTest._invalid(super.errors) :
|
||||
name = '',
|
||||
language = '',
|
||||
script = '',
|
||||
parameters = <String>[],
|
||||
contexts = <String>[];
|
||||
|
||||
/// The human readable description of the test.
|
||||
final String name;
|
||||
|
||||
/// The executable used to run the script.
|
||||
final String language;
|
||||
|
||||
/// The path to the script to execute relative to the checkout directory.
|
||||
final String script;
|
||||
|
||||
/// Flags or parameters passed to the script.
|
||||
///
|
||||
/// Parameters accept magic environment variables (placeholders replaced
|
||||
/// before executing the test). Magic environment variables have the following
|
||||
/// limitations: only ${FLUTTER_LOGS_DIR} is currently supported and it needs
|
||||
/// to be used alone within the parameter string(e.g. ["${FLUTTER_LOGS_DIR}"]
|
||||
/// is OK but ["path=${FLUTTER_LOGS_DIR}"] is not).
|
||||
final List<String> parameters;
|
||||
|
||||
/// A list of available contexts to add to the text execution step.
|
||||
///
|
||||
/// Two contexts are supported: "android_virtual_device" and
|
||||
/// "metric_center_token".
|
||||
final List<String> contexts;
|
||||
}
|
||||
|
||||
/// "builds" -> "generators" is a map containing a single property "tasks",
|
||||
/// which is a list of maps with fields like:
|
||||
/// {
|
||||
/// "name": "",
|
||||
/// "parameters": [""],
|
||||
/// "scripts": [""],
|
||||
/// "language": ""
|
||||
/// }
|
||||
///
|
||||
/// The semantics of this task are that each script in the list of scripts is
|
||||
/// run in sequence by appending the same parameter list to each one.
|
||||
final class BuildTask extends BuildConfigBase {
|
||||
factory BuildTask.fromJson(Map<String, Object?> map) {
|
||||
final List<String> errors = <String>[];
|
||||
final String? name = stringOfJson(map, 'name', errors);
|
||||
final String? language = stringOfJson(map, 'language', errors);
|
||||
final List<String>? scripts = stringListOfJson(map, 'scripts', errors);
|
||||
final List<String>? parameters = stringListOfJson(
|
||||
map, 'parameters', errors,
|
||||
);
|
||||
if (name == null ||
|
||||
language == null ||
|
||||
scripts == null ||
|
||||
parameters == null) {
|
||||
return BuildTask._invalid(errors);
|
||||
}
|
||||
return BuildTask._(name, language, scripts, parameters);
|
||||
}
|
||||
|
||||
BuildTask._invalid(super.errors) :
|
||||
name = '',
|
||||
language = '',
|
||||
scripts = <String>[],
|
||||
parameters = <String>[];
|
||||
|
||||
BuildTask._(this.name, this.language, this.scripts, this.parameters) :
|
||||
super(null);
|
||||
|
||||
/// The human readable name of the step running the script.
|
||||
final String name;
|
||||
|
||||
/// The script language executable to run the script. If empty it is assumed
|
||||
/// to be bash.
|
||||
final String language;
|
||||
|
||||
/// A list of paths of scripts relative to the checkout directory. Each
|
||||
/// script is run in turn by appending the list of parameters to it.
|
||||
final List<String> scripts;
|
||||
|
||||
/// The flags passed to the script. Paths referenced in the list are relative
|
||||
/// to the checkout directory.
|
||||
final List<String> parameters;
|
||||
}
|
||||
|
||||
/// "builds" -> "archives" contains a list of maps with fields like:
|
||||
/// {
|
||||
/// "name": "",
|
||||
/// "base_path": "",
|
||||
/// "type": "",
|
||||
/// "include_paths": [""],
|
||||
/// "realm": ""
|
||||
/// }
|
||||
final class BuildArchive extends BuildConfigBase {
|
||||
factory BuildArchive.fromJson(Map<String, Object?> map) {
|
||||
final List<String> errors = <String>[];
|
||||
final String? name = stringOfJson(map, 'name', errors);
|
||||
final String? type = stringOfJson(map, 'type', errors);
|
||||
final String? basePath = stringOfJson(map, 'base_path', errors);
|
||||
final String? realm = stringOfJson(map, 'realm', errors);
|
||||
final List<String>? includePaths = stringListOfJson(
|
||||
map, 'include_paths', errors,
|
||||
);
|
||||
if (name == null ||
|
||||
type == null ||
|
||||
basePath == null ||
|
||||
realm == null ||
|
||||
includePaths == null) {
|
||||
return BuildArchive._invalid(errors);
|
||||
}
|
||||
return BuildArchive._(name, type, basePath, realm, includePaths);
|
||||
}
|
||||
|
||||
BuildArchive._invalid(super.error) :
|
||||
name = '',
|
||||
type = '',
|
||||
basePath = '',
|
||||
realm = '',
|
||||
includePaths = <String>[];
|
||||
|
||||
BuildArchive._(
|
||||
this.name,
|
||||
this.type,
|
||||
this.basePath,
|
||||
this.realm,
|
||||
this.includePaths,
|
||||
) : super(null);
|
||||
|
||||
/// The name which may be referenced later as a dependency of global tests.
|
||||
final String name;
|
||||
|
||||
/// The type of storage to use. Currently only “gcs” and “cas” are supported.
|
||||
final String type;
|
||||
|
||||
/// The portion of the path to remove from the full path before uploading
|
||||
final String basePath;
|
||||
|
||||
/// Either "production" or "experimental".
|
||||
final String realm;
|
||||
|
||||
/// A list of strings with the paths to be uploaded to a given destination.
|
||||
final List<String> includePaths;
|
||||
}
|
||||
|
||||
/// Global "tests" is a list of maps containing fields like:
|
||||
/// {
|
||||
/// "name": "",
|
||||
/// "recipe": "",
|
||||
/// "drone_dimensions": [""],
|
||||
/// "dependencies": [""],
|
||||
/// "tasks": [] (same format as above)
|
||||
/// }
|
||||
final class GlobalTest extends BuildConfigBase {
|
||||
factory GlobalTest.fromJson(Map<String, Object?> map) {
|
||||
final List<String> errors = <String>[];
|
||||
final String? name = stringOfJson(map, 'name', errors);
|
||||
final String? recipe = stringOfJson(map, 'recipe', errors);
|
||||
final List<String>? droneDimensions = stringListOfJson(
|
||||
map, 'drone_dimensions', errors,
|
||||
);
|
||||
final List<String>? dependencies = stringListOfJson(
|
||||
map, 'dependencies', errors,
|
||||
);
|
||||
final List<TestDependency>? testDependencies = objListOfJson(
|
||||
map, 'test_dependencies', errors, TestDependency.fromJson,
|
||||
);
|
||||
final List<TestTask>? tasks = objListOfJson(
|
||||
map, 'tasks', errors, TestTask.fromJson,
|
||||
);
|
||||
if (name == null ||
|
||||
recipe == null ||
|
||||
droneDimensions == null ||
|
||||
dependencies == null ||
|
||||
testDependencies == null ||
|
||||
tasks == null) {
|
||||
return GlobalTest._invalid(errors);
|
||||
}
|
||||
return GlobalTest._(
|
||||
name, recipe, droneDimensions, dependencies, testDependencies, tasks);
|
||||
}
|
||||
|
||||
GlobalTest._invalid(super.errors) :
|
||||
name = '',
|
||||
recipe = '',
|
||||
droneDimensions = <String>[],
|
||||
dependencies = <String>[],
|
||||
testDependencies = <TestDependency>[],
|
||||
tasks = <TestTask>[];
|
||||
|
||||
GlobalTest._(
|
||||
this.name,
|
||||
this.recipe,
|
||||
this.droneDimensions,
|
||||
this.dependencies,
|
||||
this.testDependencies,
|
||||
this.tasks,
|
||||
) : super(null);
|
||||
|
||||
/// The name that will be assigned to the sub-build.
|
||||
final String name;
|
||||
|
||||
/// The recipe name to use if different than tester.
|
||||
final String recipe;
|
||||
|
||||
/// A list of strings with key values to select the bot where the test will
|
||||
/// run.
|
||||
final List<String> droneDimensions;
|
||||
|
||||
/// A list of build outputs required by the test.
|
||||
final List<String> dependencies;
|
||||
|
||||
/// A list of dependencies required for the test to run.
|
||||
final List<TestDependency> testDependencies;
|
||||
|
||||
/// A list of dictionaries representing scripts and parameters to run them
|
||||
final List<TestTask> tasks;
|
||||
|
||||
@override
|
||||
List<String> check(String path) {
|
||||
final List<String> errors = <String>[];
|
||||
errors.addAll(super.check(path));
|
||||
for (int i = 0; i < testDependencies.length; i++) {
|
||||
final TestDependency testDependency = testDependencies[i];
|
||||
errors.addAll(testDependency.check('$path/test_dependencies[$i]'));
|
||||
}
|
||||
for (int i = 0; i < tasks.length; i++) {
|
||||
final TestTask task = tasks[i];
|
||||
errors.addAll(task.check('$path/tasks[$i]'));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
/// A test dependency for a global test has fields like:
|
||||
/// {
|
||||
/// "dependency": "",
|
||||
/// "version": ""
|
||||
/// }
|
||||
final class TestDependency extends BuildConfigBase {
|
||||
factory TestDependency.fromJson(Map<String, Object?> map) {
|
||||
final List<String> errors = <String>[];
|
||||
final String? dependency = stringOfJson(map, 'dependency', errors);
|
||||
final String? version = stringOfJson(map, 'version', errors);
|
||||
if (dependency == null || version == null) {
|
||||
return TestDependency._invalid(errors);
|
||||
}
|
||||
return TestDependency._(dependency, version);
|
||||
}
|
||||
|
||||
TestDependency._invalid(super.error) : dependency = '', version = '';
|
||||
|
||||
TestDependency._(this.dependency, this.version) : super(null);
|
||||
|
||||
/// A dependency from the list at:
|
||||
/// https://flutter.googlesource.com/recipes/+/refs/heads/main/recipe_modules/flutter_deps/api.py#75
|
||||
final String dependency;
|
||||
|
||||
/// The CIPD version string of the dependency.
|
||||
final String version;
|
||||
}
|
||||
|
||||
/// Task for a global generator and a global test.
|
||||
/// {
|
||||
/// "name": "",
|
||||
/// "parameters": [""],
|
||||
/// "script": "",
|
||||
/// "language": ""
|
||||
/// }
|
||||
final class TestTask extends BuildConfigBase {
|
||||
factory TestTask.fromJson(Map<String, Object?> map) {
|
||||
final List<String> errors = <String>[];
|
||||
final String? name = stringOfJson(map, 'name', errors);
|
||||
final String? language = stringOfJson(map, 'language', errors);
|
||||
final String? script = stringOfJson(map, 'script', errors);
|
||||
final int? maxAttempts = intOfJson(map, 'max_attempts', fallback: 1, errors);
|
||||
final List<String>? parameters = stringListOfJson(
|
||||
map, 'parameters', errors,
|
||||
);
|
||||
if (name == null ||
|
||||
language == null ||
|
||||
script == null ||
|
||||
maxAttempts == null ||
|
||||
parameters == null) {
|
||||
return TestTask._invalid(errors);
|
||||
}
|
||||
return TestTask._(name, language, script, maxAttempts, parameters);
|
||||
}
|
||||
|
||||
TestTask._invalid(super.error) :
|
||||
name = '',
|
||||
language = '',
|
||||
script = '',
|
||||
maxAttempts = 0,
|
||||
parameters = <String>[];
|
||||
|
||||
TestTask._(
|
||||
this.name,
|
||||
this.language,
|
||||
this.script,
|
||||
this.maxAttempts,
|
||||
this.parameters,
|
||||
) : super(null);
|
||||
|
||||
/// The human readable name of the step running the script.
|
||||
final String name;
|
||||
|
||||
/// The script language executable to run the script. If empty it is assumed
|
||||
/// to be bash.
|
||||
final String language;
|
||||
|
||||
/// The script path relative to the checkout repository.
|
||||
final String script;
|
||||
|
||||
/// The maximum number of failures to tolerate. The default is 1.
|
||||
final int maxAttempts;
|
||||
|
||||
/// The flags passed to the script. Paths referenced in the list are relative
|
||||
/// to the checkout directory.
|
||||
final List<String> parameters;
|
||||
}
|
||||
|
||||
/// The objects that populate the list of global archives have fields like:
|
||||
/// {
|
||||
/// "source": "out/debug/artifacts.zip",
|
||||
/// "destination": "ios/artifacts.zip",
|
||||
/// "realm": "production"
|
||||
/// },
|
||||
final class GlobalArchive extends BuildConfigBase {
|
||||
factory GlobalArchive.fromJson(Map<String, Object?> map) {
|
||||
final List<String> errors = <String>[];
|
||||
final String? source = stringOfJson(map, 'source', errors);
|
||||
final String? destination = stringOfJson(map, 'destination', errors);
|
||||
final String? realm = stringOfJson(map, 'realm', errors);
|
||||
if (source == null ||
|
||||
destination == null ||
|
||||
realm == null) {
|
||||
return GlobalArchive._invalid(errors);
|
||||
}
|
||||
return GlobalArchive._(source, destination, realm);
|
||||
}
|
||||
|
||||
GlobalArchive._invalid(super.error) :
|
||||
source = '',
|
||||
destination = '',
|
||||
realm = '';
|
||||
|
||||
GlobalArchive._(this.source, this.destination, this.realm) :
|
||||
super(null);
|
||||
|
||||
/// The path of the artifact relative to the engine checkout.
|
||||
final String source;
|
||||
|
||||
/// The destination folder in the storage bucket.
|
||||
final String destination;
|
||||
|
||||
/// Which storage bucket the destination path is relative to.
|
||||
/// Either "production" or "experimental".
|
||||
final String realm;
|
||||
}
|
||||
|
||||
|
||||
void appendTypeError(
|
||||
Map<String, Object?> map,
|
||||
String field,
|
||||
String expected,
|
||||
List<String> errors, {
|
||||
Object? element,
|
||||
}) {
|
||||
if (element == null) {
|
||||
final Type actual = map[field]!.runtimeType;
|
||||
errors.add(
|
||||
'For field "$field", expected type: $expected, actual type: $actual.',
|
||||
);
|
||||
} else {
|
||||
final Type actual = element.runtimeType;
|
||||
errors.add(
|
||||
'For element "$element" of "$field", '
|
||||
'expected type: $expected, actual type: $actual',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<T>? objListOfJson<T>(
|
||||
Map<String, Object?> map,
|
||||
String field,
|
||||
List<String> errors,
|
||||
T Function(Map<String, Object?>) fn,
|
||||
) {
|
||||
if (map[field] == null) {
|
||||
return <T>[];
|
||||
}
|
||||
if (map[field]! is! List<Object?>) {
|
||||
appendTypeError(map, field, 'list', errors);
|
||||
return null;
|
||||
}
|
||||
for (final Object? obj in map[field]! as List<Object?>) {
|
||||
if (obj is! Map<String, Object?>) {
|
||||
appendTypeError(map, field, 'map', errors);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (map[field]! as List<Object?>)
|
||||
.cast<Map<String, Object?>>().map<T>(fn).toList();
|
||||
}
|
||||
|
||||
List<String>? stringListOfJson(
|
||||
Map<String, Object?> map,
|
||||
String field,
|
||||
List<String> errors,
|
||||
) {
|
||||
if (map[field] == null) {
|
||||
return <String>[];
|
||||
}
|
||||
if (map[field]! is! List<Object?>) {
|
||||
appendTypeError(map, field, 'list', errors);
|
||||
return null;
|
||||
}
|
||||
for (final Object? obj in map[field]! as List<Object?>) {
|
||||
if (obj is! String) {
|
||||
appendTypeError(map, field, element: obj, 'string', errors);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (map[field]! as List<Object?>).cast<String>();
|
||||
}
|
||||
|
||||
String? stringOfJson(
|
||||
Map<String, Object?> map,
|
||||
String field,
|
||||
List<String> errors,
|
||||
) {
|
||||
if (map[field] == null) {
|
||||
return '<undef>';
|
||||
}
|
||||
if (map[field]! is! String) {
|
||||
appendTypeError(map, field, 'string', errors);
|
||||
return null;
|
||||
}
|
||||
return map[field]! as String;
|
||||
}
|
||||
|
||||
int? intOfJson(
|
||||
Map<String, Object?> map,
|
||||
String field,
|
||||
List<String> errors, {
|
||||
int fallback = 0,
|
||||
}) {
|
||||
if (map[field] == null) {
|
||||
return fallback;
|
||||
}
|
||||
if (map[field]! is! int) {
|
||||
appendTypeError(map, field, 'int', errors);
|
||||
return null;
|
||||
}
|
||||
return map[field]! as int;
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
// Copyright 2013 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.
|
||||
|
||||
import 'dart:convert' as convert;
|
||||
import 'dart:io' as io show Directory, File;
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'build_config.dart';
|
||||
|
||||
/// This is a utility class for reading all of the build configurations from
|
||||
/// a subdirectory of the engine repo. After building an instance of this class,
|
||||
/// the build configurations can be accessed on the [configs] getter.
|
||||
class BuildConfigLoader {
|
||||
BuildConfigLoader({required this.buildConfigsDir});
|
||||
|
||||
/// Any errors encountered while parsing and loading the build config files
|
||||
/// are accumulated in this list as strings. It should be checked for errors
|
||||
/// after the first access to the [configs] getter.
|
||||
final List<String> errors = <String>[];
|
||||
|
||||
/// The directory where the engine's build config .json files exist.
|
||||
final io.Directory buildConfigsDir;
|
||||
|
||||
/// Walks [buildConfigsDir] looking for .json files, which it attempts to
|
||||
/// parse as engine build configs. JSON parsing errors during this process
|
||||
/// are added as strings to the [errors] list. That last should be checked
|
||||
/// for errors after accessing this getter.
|
||||
///
|
||||
/// The [BuildConfig]s given by this getter should be further checked for
|
||||
/// validity by calling `BuildConfig.check()` on each one. See
|
||||
/// `bin/check.dart` for an example.
|
||||
late final Map<String, BuildConfig> configs = (){
|
||||
return _parseAllBuildConfigs(buildConfigsDir);
|
||||
}();
|
||||
|
||||
Map<String, BuildConfig> _parseAllBuildConfigs(io.Directory dir) {
|
||||
final Map<String, BuildConfig> result = <String, BuildConfig>{};
|
||||
if (!dir.existsSync()) {
|
||||
errors.add('${buildConfigsDir.path} does not exist.');
|
||||
return result;
|
||||
}
|
||||
final List<io.File> jsonFiles = dir
|
||||
.listSync(recursive: true)
|
||||
.whereType<io.File>()
|
||||
.where((io.File f) => f.path.endsWith('.json'))
|
||||
.toList();
|
||||
for (final io.File jsonFile in jsonFiles) {
|
||||
final String basename = p.basename(jsonFile.path);
|
||||
final String name = basename.substring(
|
||||
0, basename.length - 5,
|
||||
);
|
||||
final String jsonData = jsonFile.readAsStringSync();
|
||||
final dynamic maybeJson;
|
||||
try {
|
||||
maybeJson = convert.jsonDecode(jsonData);
|
||||
} on FormatException catch (e) {
|
||||
errors.add('While parsing ${jsonFile.path}:\n$e');
|
||||
continue;
|
||||
}
|
||||
if (maybeJson is! Map<String, Object?>) {
|
||||
errors.add('${jsonFile.path} did not contain a json map.');
|
||||
continue;
|
||||
}
|
||||
result[name] = BuildConfig.fromJson(path: jsonFile.path, map: maybeJson);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
# Copyright 2013 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.
|
||||
|
||||
name: engine_build_configs
|
||||
publish_to: none
|
||||
environment:
|
||||
sdk: '>=3.1.0-0 <4.0.0'
|
||||
|
||||
# Do not add any dependencies that require more than what is provided in
|
||||
# //third_party/pkg, //third_party/dart/pkg, or
|
||||
# //third_party/dart/third_party/pkg. In particular, package:test is not usable
|
||||
# here.
|
||||
|
||||
# If you do add packages here, make sure you can run `pub get --offline`, and
|
||||
# check the .packages and .package_config to make sure all the paths are
|
||||
# relative to this directory into //third_party/dart
|
||||
|
||||
dependencies:
|
||||
args: any
|
||||
engine_repo_tools:
|
||||
path: ../engine_repo_tools
|
||||
file: any
|
||||
meta: any
|
||||
path: any
|
||||
platform: any
|
||||
process_runner: any
|
||||
|
||||
dev_dependencies:
|
||||
async_helper: any
|
||||
expect: any
|
||||
litetest: any
|
||||
smith: any
|
||||
|
||||
dependency_overrides:
|
||||
args:
|
||||
path: ../../../../third_party/dart/third_party/pkg/args
|
||||
async:
|
||||
path: ../../../../third_party/dart/third_party/pkg/async
|
||||
async_helper:
|
||||
path: ../../../../third_party/dart/pkg/async_helper
|
||||
collection:
|
||||
path: ../../../../third_party/dart/third_party/pkg/collection
|
||||
expect:
|
||||
path: ../../../../third_party/dart/pkg/expect
|
||||
file:
|
||||
path: ../../../../third_party/pkg/file/packages/file
|
||||
litetest:
|
||||
path: ../../../testing/litetest
|
||||
meta:
|
||||
path: ../../../../third_party/dart/pkg/meta
|
||||
path:
|
||||
path: ../../../../third_party/dart/third_party/pkg/path
|
||||
platform:
|
||||
path: ../../../../third_party/pkg/platform
|
||||
process:
|
||||
path: ../../../../third_party/pkg/process
|
||||
process_runner:
|
||||
path: ../../../../third_party/pkg/process_runner
|
||||
smith:
|
||||
path: ../../../../third_party/dart/pkg/smith
|
||||
@ -0,0 +1,128 @@
|
||||
// Copyright 2013 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.
|
||||
|
||||
import 'package:engine_build_configs/src/build_config.dart';
|
||||
import 'package:engine_build_configs/src/build_config_loader.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:litetest/litetest.dart';
|
||||
|
||||
const String buildConfigJson = '''
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"archives": [
|
||||
{
|
||||
"name": "build_name",
|
||||
"base_path": "base/path",
|
||||
"type": "gcs",
|
||||
"include_paths": ["include/path"],
|
||||
"realm": "archive_realm"
|
||||
}
|
||||
],
|
||||
"drone_dimensions": ["dimension"],
|
||||
"gclient_variables": {
|
||||
"variable": false
|
||||
},
|
||||
"gn": ["--gn-arg"],
|
||||
"name": "build_name",
|
||||
"ninja": {
|
||||
"config": "build_name",
|
||||
"targets": ["ninja_target"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"language": "python3",
|
||||
"name": "build_name tests",
|
||||
"parameters": ["--test-params"],
|
||||
"script": "test/script.py",
|
||||
"contexts": ["context"]
|
||||
}
|
||||
],
|
||||
"generators": {
|
||||
"tasks": [
|
||||
{
|
||||
"name": "generator_task",
|
||||
"parameters": ["--gen-param"],
|
||||
"scripts": ["gen/script.py"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"generators": {
|
||||
"tasks": [
|
||||
{
|
||||
"name": "global generator task",
|
||||
"parameters": ["--global-gen-param"],
|
||||
"script": "global/gen_script.dart",
|
||||
"language": "dart"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "global test",
|
||||
"recipe": "engine_v2/tester_engine",
|
||||
"drone_dimensions": ["dimension"],
|
||||
"gclient_variables": {
|
||||
"variable": false
|
||||
},
|
||||
"dependencies": ["dependency"],
|
||||
"test_dependencies": [
|
||||
{
|
||||
"dependency": "test_dependency",
|
||||
"version": "git_revision:3a77d0b12c697a840ca0c7705208e8622dc94603"
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "global test task",
|
||||
"parameters": ["--test-parameter"],
|
||||
"script": "global/test/script.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
|
||||
int main() {
|
||||
test('BuildConfigLoader can load a build config', () {
|
||||
final FileSystem fs = MemoryFileSystem();
|
||||
final String buildConfigPath = fs.path.join('flutter', 'ci', 'builders');
|
||||
final Directory buildConfigsDir = fs.directory(buildConfigPath);
|
||||
final File buildConfigFile = buildConfigsDir.childFile(
|
||||
'linux_test_build.json',
|
||||
);
|
||||
buildConfigFile.create(recursive: true);
|
||||
buildConfigFile.writeAsStringSync(buildConfigJson);
|
||||
|
||||
final BuildConfigLoader loader = BuildConfigLoader(
|
||||
buildConfigsDir: buildConfigsDir,
|
||||
);
|
||||
|
||||
expect(loader.configs, isNotNull);
|
||||
expect(loader.errors, isEmpty);
|
||||
expect(loader.configs['linux_test_build'], isNotNull);
|
||||
});
|
||||
|
||||
test('BuildConfigLoader gives an empty config when no configs found', () {
|
||||
final FileSystem fs = MemoryFileSystem();
|
||||
final String buildConfigPath = fs.path.join(
|
||||
'flutter', 'ci', 'builders', 'linux_test_build.json',
|
||||
);
|
||||
final Directory buildConfigsDir = fs.directory(buildConfigPath);
|
||||
final BuildConfigLoader loader = BuildConfigLoader(
|
||||
buildConfigsDir: buildConfigsDir,
|
||||
);
|
||||
|
||||
expect(loader.configs, isNotNull);
|
||||
expect(loader.errors[0], equals(
|
||||
'flutter/ci/builders/linux_test_build.json does not exist.',
|
||||
));
|
||||
expect(loader.configs, equals(<String, BuildConfig>{}));
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
@ -0,0 +1,368 @@
|
||||
// Copyright 2013 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.
|
||||
|
||||
import 'dart:convert' as convert;
|
||||
|
||||
import 'package:engine_build_configs/src/build_config.dart';
|
||||
import 'package:litetest/litetest.dart';
|
||||
|
||||
const String buildConfigJson = '''
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"archives": [
|
||||
{
|
||||
"name": "build_name",
|
||||
"base_path": "base/path",
|
||||
"type": "gcs",
|
||||
"include_paths": ["include/path"],
|
||||
"realm": "archive_realm"
|
||||
}
|
||||
],
|
||||
"drone_dimensions": ["dimension"],
|
||||
"gclient_variables": {
|
||||
"variable": false
|
||||
},
|
||||
"gn": ["--gn-arg"],
|
||||
"name": "build_name",
|
||||
"ninja": {
|
||||
"config": "build_name",
|
||||
"targets": ["ninja_target"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"language": "python3",
|
||||
"name": "build_name tests",
|
||||
"parameters": ["--test-params"],
|
||||
"script": "test/script.py",
|
||||
"contexts": ["context"]
|
||||
}
|
||||
],
|
||||
"generators": {
|
||||
"tasks": [
|
||||
{
|
||||
"name": "generator_task",
|
||||
"parameters": ["--gen-param"],
|
||||
"scripts": ["gen/script.py"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"generators": {
|
||||
"tasks": [
|
||||
{
|
||||
"name": "global generator task",
|
||||
"parameters": ["--global-gen-param"],
|
||||
"script": "global/gen_script.dart",
|
||||
"language": "dart"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "global test",
|
||||
"recipe": "engine_v2/tester_engine",
|
||||
"drone_dimensions": ["dimension"],
|
||||
"gclient_variables": {
|
||||
"variable": false
|
||||
},
|
||||
"dependencies": ["dependency"],
|
||||
"test_dependencies": [
|
||||
{
|
||||
"dependency": "test_dependency",
|
||||
"version": "git_revision:3a77d0b12c697a840ca0c7705208e8622dc94603"
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "global test task",
|
||||
"parameters": ["--test-parameter"],
|
||||
"script": "global/test/script.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
|
||||
int main() {
|
||||
test('BuildConfig parser works', () {
|
||||
final BuildConfig buildConfig = BuildConfig.fromJson(
|
||||
path: 'linux_test_config',
|
||||
map: convert.jsonDecode(buildConfigJson) as Map<String, Object?>,
|
||||
);
|
||||
expect(buildConfig.valid, isTrue);
|
||||
expect(buildConfig.errors, isNull);
|
||||
expect(buildConfig.builds.length, equals(1));
|
||||
|
||||
final GlobalBuild globalBuild = buildConfig.builds[0];
|
||||
expect(globalBuild.name, equals('build_name'));
|
||||
expect(globalBuild.gn.length, equals(1));
|
||||
expect(globalBuild.gn[0], equals('--gn-arg'));
|
||||
expect(globalBuild.droneDimensions.length, equals(1));
|
||||
expect(globalBuild.droneDimensions[0], equals('dimension'));
|
||||
|
||||
final BuildNinja ninja = globalBuild.ninja;
|
||||
expect(ninja.config, equals('build_name'));
|
||||
expect(ninja.targets.length, equals(1));
|
||||
expect(ninja.targets[0], equals('ninja_target'));
|
||||
|
||||
expect(globalBuild.archives.length, equals(1));
|
||||
final BuildArchive buildArchive = globalBuild.archives[0];
|
||||
expect(buildArchive.name, equals('build_name'));
|
||||
expect(buildArchive.basePath, equals('base/path'));
|
||||
expect(buildArchive.type, equals('gcs'));
|
||||
expect(buildArchive.includePaths.length, equals(1));
|
||||
expect(buildArchive.includePaths[0], equals('include/path'));
|
||||
|
||||
expect(globalBuild.tests.length, equals(1));
|
||||
final BuildTest tst = globalBuild.tests[0];
|
||||
expect(tst.name, equals('build_name tests'));
|
||||
expect(tst.language, equals('python3'));
|
||||
expect(tst.script, equals('test/script.py'));
|
||||
expect(tst.parameters.length, equals(1));
|
||||
expect(tst.parameters[0], equals('--test-params'));
|
||||
expect(tst.contexts.length, equals(1));
|
||||
expect(tst.contexts[0], equals('context'));
|
||||
|
||||
expect(globalBuild.generators.length, equals(1));
|
||||
final BuildTask buildTask = globalBuild.generators[0];
|
||||
expect(buildTask.name, equals('generator_task'));
|
||||
expect(buildTask.scripts.length, equals(1));
|
||||
expect(buildTask.scripts[0], equals('gen/script.py'));
|
||||
expect(buildTask.parameters.length, equals(1));
|
||||
expect(buildTask.parameters[0], equals('--gen-param'));
|
||||
|
||||
expect(buildConfig.generators.length, equals(1));
|
||||
final TestTask testTask = buildConfig.generators[0];
|
||||
expect(testTask.name, equals('global generator task'));
|
||||
expect(testTask.language, equals('dart'));
|
||||
expect(testTask.script, equals('global/gen_script.dart'));
|
||||
expect(testTask.parameters.length, equals(1));
|
||||
expect(testTask.parameters[0], equals('--global-gen-param'));
|
||||
|
||||
expect(buildConfig.tests.length, equals(1));
|
||||
final GlobalTest globalTest = buildConfig.tests[0];
|
||||
expect(globalTest.name, equals('global test'));
|
||||
expect(globalTest.recipe, equals('engine_v2/tester_engine'));
|
||||
expect(globalTest.droneDimensions.length, equals(1));
|
||||
expect(globalTest.droneDimensions[0], equals('dimension'));
|
||||
expect(globalTest.dependencies.length, equals(1));
|
||||
expect(globalTest.dependencies[0], equals('dependency'));
|
||||
|
||||
expect(globalTest.tasks.length, equals(1));
|
||||
final TestTask globalTestTask = globalTest.tasks[0];
|
||||
expect(globalTestTask.name, equals('global test task'));
|
||||
expect(globalTestTask.script, equals('global/test/script.py'));
|
||||
expect(globalTestTask.language, equals('<undef>'));
|
||||
});
|
||||
|
||||
test('BuildConfig flags invalid input', () {
|
||||
const String invalidInput = '''
|
||||
{
|
||||
"builds": 5,
|
||||
"generators": {},
|
||||
"tests": []
|
||||
}
|
||||
''';
|
||||
final BuildConfig buildConfig = BuildConfig.fromJson(
|
||||
path: 'linux_test_config',
|
||||
map: convert.jsonDecode(invalidInput) as Map<String, Object?>,
|
||||
);
|
||||
expect(buildConfig.valid, isFalse);
|
||||
expect(buildConfig.errors![0], equals(
|
||||
'For field "builds", expected type: list, actual type: int.',
|
||||
));
|
||||
});
|
||||
|
||||
test('GlobalBuild flags invalid input', () {
|
||||
const String invalidInput = '''
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"name": 5
|
||||
}
|
||||
],
|
||||
"generators": {},
|
||||
"tests": []
|
||||
}
|
||||
''';
|
||||
final BuildConfig buildConfig = BuildConfig.fromJson(
|
||||
path: 'linux_test_config',
|
||||
map: convert.jsonDecode(invalidInput) as Map<String, Object?>,
|
||||
);
|
||||
expect(buildConfig.valid, isTrue);
|
||||
expect(buildConfig.builds.length, equals(1));
|
||||
expect(buildConfig.builds[0].valid, isFalse);
|
||||
expect(buildConfig.builds[0].errors![0], equals(
|
||||
'For field "name", expected type: string, actual type: int.',
|
||||
));
|
||||
});
|
||||
|
||||
test('BuildNinja flags invalid input', () {
|
||||
const String invalidInput = '''
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"ninja": {
|
||||
"config": 5
|
||||
}
|
||||
}
|
||||
],
|
||||
"generators": {},
|
||||
"tests": []
|
||||
}
|
||||
''';
|
||||
final BuildConfig buildConfig = BuildConfig.fromJson(
|
||||
path: 'linux_test_config',
|
||||
map: convert.jsonDecode(invalidInput) as Map<String, Object?>,
|
||||
);
|
||||
expect(buildConfig.valid, isTrue);
|
||||
expect(buildConfig.builds.length, equals(1));
|
||||
expect(buildConfig.builds[0].valid, isTrue);
|
||||
expect(buildConfig.builds[0].ninja.valid, isFalse);
|
||||
expect(buildConfig.builds[0].ninja.errors![0], equals(
|
||||
'For field "config", expected type: string, actual type: int.',
|
||||
));
|
||||
});
|
||||
|
||||
test('BuildTest flags invalid input', () {
|
||||
const String invalidInput = '''
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"tests": [
|
||||
{
|
||||
"language": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": {},
|
||||
"tests": []
|
||||
}
|
||||
''';
|
||||
final BuildConfig buildConfig = BuildConfig.fromJson(
|
||||
path: 'linux_test_config',
|
||||
map: convert.jsonDecode(invalidInput) as Map<String, Object?>,
|
||||
);
|
||||
expect(buildConfig.valid, isTrue);
|
||||
expect(buildConfig.builds.length, equals(1));
|
||||
expect(buildConfig.builds[0].valid, isTrue);
|
||||
expect(buildConfig.builds[0].tests[0].valid, isFalse);
|
||||
expect(buildConfig.builds[0].tests[0].errors![0], equals(
|
||||
'For field "language", expected type: string, actual type: int.',
|
||||
));
|
||||
});
|
||||
|
||||
test('BuildTask flags invalid input', () {
|
||||
const String invalidInput = '''
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"generators": {
|
||||
"tasks": [
|
||||
{
|
||||
"name": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"generators": {},
|
||||
"tests": []
|
||||
}
|
||||
''';
|
||||
final BuildConfig buildConfig = BuildConfig.fromJson(
|
||||
path: 'linux_test_config',
|
||||
map: convert.jsonDecode(invalidInput) as Map<String, Object?>,
|
||||
);
|
||||
expect(buildConfig.valid, isTrue);
|
||||
expect(buildConfig.builds.length, equals(1));
|
||||
expect(buildConfig.builds[0].valid, isTrue);
|
||||
expect(buildConfig.builds[0].generators[0].valid, isFalse);
|
||||
expect(buildConfig.builds[0].generators[0].errors![0], equals(
|
||||
'For field "name", expected type: string, actual type: int.',
|
||||
));
|
||||
});
|
||||
|
||||
test('BuildArchive flags invalid input', () {
|
||||
const String invalidInput = '''
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"archives": [
|
||||
{
|
||||
"name": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": {},
|
||||
"tests": []
|
||||
}
|
||||
''';
|
||||
final BuildConfig buildConfig = BuildConfig.fromJson(
|
||||
path: 'linux_test_config',
|
||||
map: convert.jsonDecode(invalidInput) as Map<String, Object?>,
|
||||
);
|
||||
expect(buildConfig.valid, isTrue);
|
||||
expect(buildConfig.builds.length, equals(1));
|
||||
expect(buildConfig.builds[0].valid, isTrue);
|
||||
expect(buildConfig.builds[0].archives[0].valid, isFalse);
|
||||
expect(buildConfig.builds[0].archives[0].errors![0], equals(
|
||||
'For field "name", expected type: string, actual type: int.',
|
||||
));
|
||||
});
|
||||
|
||||
test('GlobalTest flags invalid input', () {
|
||||
const String invalidInput = '''
|
||||
{
|
||||
"tests": [
|
||||
{
|
||||
"name": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
final BuildConfig buildConfig = BuildConfig.fromJson(
|
||||
path: 'linux_test_config',
|
||||
map: convert.jsonDecode(invalidInput) as Map<String, Object?>,
|
||||
);
|
||||
expect(buildConfig.valid, isTrue);
|
||||
expect(buildConfig.tests.length, equals(1));
|
||||
expect(buildConfig.tests[0].valid, isFalse);
|
||||
expect(buildConfig.tests[0].errors![0], equals(
|
||||
'For field "name", expected type: string, actual type: int.',
|
||||
));
|
||||
});
|
||||
|
||||
test('TestTask flags invalid input', () {
|
||||
const String invalidInput = '''
|
||||
{
|
||||
"tests": [
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"name": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
''';
|
||||
final BuildConfig buildConfig = BuildConfig.fromJson(
|
||||
path: 'linux_test_config',
|
||||
map: convert.jsonDecode(invalidInput) as Map<String, Object?>,
|
||||
);
|
||||
expect(buildConfig.valid, isTrue);
|
||||
expect(buildConfig.tests.length, equals(1));
|
||||
expect(buildConfig.tests[0].tasks[0].valid, isFalse);
|
||||
expect(buildConfig.tests[0].tasks[0].errors![0], contains(
|
||||
'For field "name", expected type: string, actual type: int.',
|
||||
));
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
@ -42,6 +42,7 @@ ALL_PACKAGES = [
|
||||
os.path.join(ENGINE_DIR, 'tools', 'githooks'),
|
||||
os.path.join(ENGINE_DIR, 'tools', 'licenses'),
|
||||
os.path.join(ENGINE_DIR, 'tools', 'path_ops', 'dart'),
|
||||
os.path.join(ENGINE_DIR, 'tools', 'pkg', 'engine_build_configs'),
|
||||
os.path.join(ENGINE_DIR, 'tools', 'pkg', 'engine_repo_tools'),
|
||||
]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user