manifest.dart 6.07 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:io';

7 8 9 10 11
import 'package:yaml/yaml.dart';

import 'utils.dart';

/// Loads manifest data from `manifest.yaml` file or from [yaml], if present.
12
Manifest loadTaskManifest([ String? yaml ]) {
13
  final dynamic manifestYaml = yaml == null
14 15 16
    ? loadYaml(file('manifest.yaml').readAsStringSync())
    : loadYamlNode(yaml);

17 18
  _checkType(manifestYaml is YamlMap, manifestYaml, 'Manifest', 'dictionary');
  return _validateAndParseManifest(manifestYaml as YamlMap);
19 20 21 22 23 24 25 26 27 28 29 30 31
}

/// Contains CI task information.
class Manifest {
  Manifest._(this.tasks);

  /// CI tasks.
  final List<ManifestTask> tasks;
}

/// A CI task.
class ManifestTask {
  ManifestTask._({
32 33 34 35 36 37 38
    required this.name,
    required this.description,
    required this.stage,
    required this.requiredAgentCapabilities,
    required this.isFlaky,
    required this.timeoutInMinutes,
    required this.onLuci,
39
  }) {
40
    final String taskName = 'task "$name"';
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
    _checkIsNotBlank(name, 'Task name', taskName);
    _checkIsNotBlank(description, 'Task description', taskName);
    _checkIsNotBlank(stage, 'Task stage', taskName);
    _checkIsNotBlank(requiredAgentCapabilities, 'requiredAgentCapabilities', taskName);
  }

  /// Task name as it appears on the dashboard.
  final String name;

  /// A human-readable description of the task.
  final String description;

  /// The stage this task should run in.
  final String stage;

  /// Capabilities required of the build agent to be able to perform this task.
57
  final List<String> requiredAgentCapabilities;
58 59 60 61 62 63 64 65

  /// Whether this test is flaky.
  ///
  /// Flaky tests are not considered when deciding if the build is broken.
  final bool isFlaky;

  /// An optional custom timeout specified in minutes.
  final int timeoutInMinutes;
66

67 68 69
  /// (Optional) Whether this test runs on LUCI.
  final bool onLuci;

70 71 72 73 74 75 76
  /// Whether the task is supported by the current host platform
  bool isSupportedByHost() {
    final Set<String> supportedHosts = Set<String>.from(
      requiredAgentCapabilities.map<String>(
        (String str) => str.split('/')[0]
      )
    );
77
    String hostPlatform = Platform.operatingSystem;
78 79 80 81 82
    if (hostPlatform == 'macos') {
      hostPlatform = 'mac'; // package:platform uses 'macos' while manifest.yaml uses 'mac'
    }
    return supportedHosts.contains(hostPlatform);
  }
83 84 85 86 87 88 89 90 91 92 93 94 95 96
}

/// Thrown when the manifest YAML is not valid.
class ManifestError extends Error {
  ManifestError(this.message);

  final String message;

  @override
  String toString() => '$ManifestError: $message';
}

// There's no good YAML validator, at least not for Dart, so we validate
// manually. It's not too much code and produces good error messages.
97
Manifest _validateAndParseManifest(YamlMap manifestYaml) {
98
  _checkKeys(manifestYaml, 'manifest', const <String>['tasks']);
99
  return Manifest._(_validateAndParseTasks(manifestYaml['tasks']));
100 101 102
}

List<ManifestTask> _validateAndParseTasks(dynamic tasksYaml) {
103
  _checkType(tasksYaml is YamlMap, tasksYaml, 'Value of "tasks"', 'dictionary');
104 105 106
  final List<String> sortedKeys = (tasksYaml as YamlMap).keys.toList().cast<String>()..sort();
  // ignore: avoid_dynamic_calls
  return sortedKeys.map<ManifestTask>((String taskName) => _validateAndParseTask(taskName, tasksYaml[taskName])).toList();
107 108
}

109
ManifestTask _validateAndParseTask(String taskName, dynamic taskYaml) {
110 111
  _checkType(taskYaml is YamlMap, taskYaml, 'Value of task "$taskName"', 'dictionary');
  _checkKeys(taskYaml as YamlMap, 'Value of task "$taskName"', const <String>[
112 113 114
    'description',
    'stage',
    'required_agent_capabilities',
115 116
    'flaky',
    'timeout_in_minutes',
117
    'on_luci',
118
  ]);
119
  // ignore: avoid_dynamic_calls
120 121 122 123 124
  final dynamic isFlaky = taskYaml['flaky'];
  if (isFlaky != null) {
    _checkType(isFlaky is bool, isFlaky, 'flaky', 'boolean');
  }

125
  // ignore: avoid_dynamic_calls
126 127 128 129 130
  final dynamic timeoutInMinutes = taskYaml['timeout_in_minutes'];
  if (timeoutInMinutes != null) {
    _checkType(timeoutInMinutes is int, timeoutInMinutes, 'timeout_in_minutes', 'integer');
  }

131 132
  // ignore: avoid_dynamic_calls
  final List<dynamic> capabilities = _validateAndParseCapabilities(taskName, taskYaml['required_agent_capabilities']);
133

134
  // ignore: avoid_dynamic_calls
135 136 137 138 139
  final dynamic onLuci = taskYaml['on_luci'];
  if (onLuci != null) {
    _checkType(onLuci is bool, onLuci, 'on_luci', 'boolean');
  }

140
  return ManifestTask._(
141 142
    name: taskName,
    // ignore: avoid_dynamic_calls
143
    description: taskYaml['description'] as String,
144
    // ignore: avoid_dynamic_calls
145 146
    stage: taskYaml['stage'] as String,
    requiredAgentCapabilities: capabilities as List<String>,
147
    isFlaky: isFlaky as bool,
148
    timeoutInMinutes: timeoutInMinutes as int,
149
    onLuci: onLuci as bool,
150 151 152 153 154
  );
}

List<String> _validateAndParseCapabilities(String taskName, dynamic capabilitiesYaml) {
  _checkType(capabilitiesYaml is List, capabilitiesYaml, 'required_agent_capabilities', 'list');
155 156 157
  final List<dynamic> capabilities = capabilitiesYaml as List<dynamic>;
  for (int i = 0; i < capabilities.length; i++) {
    final dynamic capability = capabilities[i];
158 159
    _checkType(capability is String, capability, 'required_agent_capabilities[$i]', 'string');
  }
160
  return capabilitiesYaml.cast<String>();
161 162 163 164
}

void _checkType(bool isValid, dynamic value, String variableName, String typeName) {
  if (!isValid) {
165
    throw ManifestError(
166 167 168 169 170 171
      '$variableName must be a $typeName but was ${value.runtimeType}: $value',
    );
  }
}

void _checkIsNotBlank(dynamic value, String variableName, String ownerName) {
172
  if (value == null || value is String && value.isEmpty || value is List<dynamic> && value.isEmpty) {
173
    throw ManifestError('$variableName must not be empty in $ownerName.');
174 175 176
  }
}

177
void _checkKeys(Map<dynamic, dynamic> map, String variableName, List<String> allowedKeys) {
178
  for (final String key in map.keys.cast<String>()) {
179
    if (!allowedKeys.contains(key)) {
180
      throw ManifestError(
181 182 183 184 185
        'Unrecognized property "$key" in $variableName. '
        'Allowed properties: ${allowedKeys.join(', ')}');
    }
  }
}