| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'dart:io'; |
| 6 | |
| 7 | import 'package:collection/collection.dart' ; |
| 8 | import 'package:meta/meta.dart' ; |
| 9 | |
| 10 | @immutable |
| 11 | class CustomerTest { |
| 12 | factory CustomerTest(File testFile) { |
| 13 | final String errorPrefix = 'Could not parse: ${testFile.path}\n' ; |
| 14 | final List<String> contacts = <String>[]; |
| 15 | final List<String> fetch = <String>[]; |
| 16 | final List<String> setup = <String>[]; |
| 17 | final List<Directory> update = <Directory>[]; |
| 18 | final List<String> test = <String>[]; |
| 19 | int? iterations; |
| 20 | bool hasTests = false; |
| 21 | for (final String line in testFile.readAsLinesSync().map((String line) => line.trim())) { |
| 22 | if (line.isEmpty || line.startsWith('#' )) { |
| 23 | // Blank line or comment. |
| 24 | continue; |
| 25 | } |
| 26 | |
| 27 | final bool isUnknownDirective = |
| 28 | _TestDirective.values.firstWhereOrNull((_TestDirective d) => line.startsWith(d.name)) == |
| 29 | null; |
| 30 | if (isUnknownDirective) { |
| 31 | throw FormatException(' ${errorPrefix}Unexpected directive:\n $line' ); |
| 32 | } |
| 33 | |
| 34 | _maybeAddTestConfig(line, directive: _TestDirective.contact, directiveValues: contacts); |
| 35 | _maybeAddTestConfig(line, directive: _TestDirective.fetch, directiveValues: fetch); |
| 36 | _maybeAddTestConfig( |
| 37 | line, |
| 38 | directive: _TestDirective.setup, |
| 39 | directiveValues: setup, |
| 40 | platformAgnostic: false, |
| 41 | ); |
| 42 | |
| 43 | final String updatePrefix = _directive(_TestDirective.update); |
| 44 | if (line.startsWith(updatePrefix)) { |
| 45 | update.add(Directory(line.substring(updatePrefix.length))); |
| 46 | } |
| 47 | |
| 48 | final String iterationsPrefix = _directive(_TestDirective.iterations); |
| 49 | if (line.startsWith(iterationsPrefix)) { |
| 50 | if (iterations != null) { |
| 51 | throw FormatException( |
| 52 | 'Cannot specify " ${_TestDirective.iterations.name}" directive multiple times.' , |
| 53 | ); |
| 54 | } |
| 55 | iterations = int.parse(line.substring(iterationsPrefix.length)); |
| 56 | if (iterations < 1) { |
| 57 | throw FormatException( |
| 58 | 'The " ${_TestDirective.iterations.name}" directive must have a positive integer value.' , |
| 59 | ); |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | if (line.startsWith(_directive(_TestDirective.test)) || |
| 64 | line.startsWith(' ${_TestDirective.test.name}.' )) { |
| 65 | hasTests = true; |
| 66 | } |
| 67 | _maybeAddTestConfig( |
| 68 | line, |
| 69 | directive: _TestDirective.test, |
| 70 | directiveValues: test, |
| 71 | platformAgnostic: false, |
| 72 | ); |
| 73 | } |
| 74 | |
| 75 | if (contacts.isEmpty) { |
| 76 | throw FormatException( |
| 77 | ' ${errorPrefix}No " ${_TestDirective.contact.name}" directives specified. At least one contact e-mail address must be specified.' , |
| 78 | ); |
| 79 | } |
| 80 | for (final String email in contacts) { |
| 81 | if (!email.contains(_email) || email.endsWith('@example.com' )) { |
| 82 | throw FormatException( |
| 83 | ' ${errorPrefix}The following e-mail address appears to be an invalid e-mail address: $email' , |
| 84 | ); |
| 85 | } |
| 86 | } |
| 87 | if (fetch.isEmpty) { |
| 88 | throw FormatException( |
| 89 | ' ${errorPrefix}No " ${_TestDirective.fetch.name}" directives specified. Two lines are expected: "git clone https://github.com/USERNAME/REPOSITORY.git tests" and "git -C tests checkout HASH".', |
| 90 | ); |
| 91 | } |
| 92 | if (fetch.length < 2) { |
| 93 | throw FormatException( |
| 94 | ' ${errorPrefix}Only one " ${_TestDirective.fetch.name}" directive specified. Two lines are expected: "git clone https://github.com/USERNAME/REPOSITORY.git tests" and "git -C tests checkout HASH".', |
| 95 | );
|
| 96 | }
|
| 97 | if (!fetch[0].contains(_fetch1)) {
|
| 98 | throw FormatException(
|
| 99 | ' ${errorPrefix}First " ${_TestDirective.fetch.name}" directive does not match expected pattern (expected "git clone https://github.com/USERNAME/REPOSITORY.git tests").',
|
| 100 | );
|
| 101 | }
|
| 102 | if (!fetch[1].contains(_fetch2)) {
|
| 103 | throw FormatException(
|
| 104 | ' ${errorPrefix}Second " ${_TestDirective.fetch.name}" directive does not match expected pattern (expected "git -C tests checkout HASH").' ,
|
| 105 | );
|
| 106 | }
|
| 107 | if (update.isEmpty) {
|
| 108 | throw FormatException(
|
| 109 | ' ${errorPrefix}No " ${_TestDirective.update.name}" directives specified. At least one directory must be specified. (It can be "." to just upgrade the root of the repository.)' ,
|
| 110 | );
|
| 111 | }
|
| 112 | if (!hasTests) {
|
| 113 | throw FormatException(
|
| 114 | ' ${errorPrefix}No " ${_TestDirective.test.name}" directives specified. At least one command must be specified to run tests.' ,
|
| 115 | );
|
| 116 | }
|
| 117 | return CustomerTest._(
|
| 118 | List<String>.unmodifiable(contacts),
|
| 119 | List<String>.unmodifiable(fetch),
|
| 120 | List<String>.unmodifiable(setup),
|
| 121 | List<Directory>.unmodifiable(update),
|
| 122 | List<String>.unmodifiable(test),
|
| 123 | iterations,
|
| 124 | );
|
| 125 | }
|
| 126 |
|
| 127 | const CustomerTest._(
|
| 128 | this.contacts,
|
| 129 | this.fetch,
|
| 130 | this.setup,
|
| 131 | this.update,
|
| 132 | this.tests,
|
| 133 | this.iterations,
|
| 134 | );
|
| 135 |
|
| 136 | // (e-mail regexp from HTML standard)
|
| 137 | static final RegExp _email = RegExp(
|
| 138 | r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" ,
|
| 139 | );
|
| 140 | static final RegExp _fetch1 = RegExp(
|
| 141 | r'^git(?: -c core.longPaths=true)? clone https://github.com/[-a-zA-Z0-9]+/[-_a-zA-Z0-9]+.git tests$',
|
| 142 | );
|
| 143 | static final RegExp _fetch2 = RegExp(
|
| 144 | r'^git(?: -c core.longPaths=true)? -C tests checkout [0-9a-f]+$' ,
|
| 145 | );
|
| 146 |
|
| 147 | final List<String> contacts;
|
| 148 | final List<String> fetch;
|
| 149 | final List<String> setup;
|
| 150 | final List<Directory> update;
|
| 151 | final List<String> tests;
|
| 152 | final int? iterations;
|
| 153 |
|
| 154 | static void _maybeAddTestConfig(
|
| 155 | String line, {
|
| 156 | required _TestDirective directive,
|
| 157 | required List<String> directiveValues,
|
| 158 | bool platformAgnostic = true,
|
| 159 | }) {
|
| 160 | final List<_PlatformType> platforms = platformAgnostic
|
| 161 | ? <_PlatformType>[_PlatformType.all]
|
| 162 | : _PlatformType.values;
|
| 163 | for (final _PlatformType platform in platforms) {
|
| 164 | final String directiveName = _directive(directive, platform: platform);
|
| 165 | if (line.startsWith(directiveName) && platform.conditionMet) {
|
| 166 | directiveValues.add(line.substring(directiveName.length));
|
| 167 | }
|
| 168 | }
|
| 169 | }
|
| 170 |
|
| 171 | static String _directive(_TestDirective directive, {_PlatformType platform = _PlatformType.all}) {
|
| 172 | return switch (platform) {
|
| 173 | _PlatformType.all => ' ${directive.name}=' ,
|
| 174 | _ => ' ${directive.name}. ${platform.name}=' ,
|
| 175 | };
|
| 176 | }
|
| 177 | }
|
| 178 |
|
| 179 | enum _PlatformType {
|
| 180 | all,
|
| 181 | windows,
|
| 182 | macos,
|
| 183 | linux,
|
| 184 | posix;
|
| 185 |
|
| 186 | bool get conditionMet => switch (this) {
|
| 187 | _PlatformType.all => true,
|
| 188 | _PlatformType.windows => Platform.isWindows,
|
| 189 | _PlatformType.macos => Platform.isMacOS,
|
| 190 | _PlatformType.linux => Platform.isLinux,
|
| 191 | _PlatformType.posix => Platform.isLinux || Platform.isMacOS,
|
| 192 | };
|
| 193 | }
|
| 194 |
|
| 195 | enum _TestDirective { contact, fetch, setup, update, test, iterations }
|
| 196 |
|