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
5import 'dart:io';
6
7import 'package:collection/collection.dart';
8import 'package:meta/meta.dart';
9
10@immutable
11class 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
179enum _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
195enum _TestDirective { contact, fetch, setup, update, test, iterations }
196