Skip to content

Commit 2610317

Browse files
author
Casey Hillers
authored
[test_utiltiies] Add analyze for trailing whitespace (flutter#1990)
1 parent 5415aa0 commit 2610317

26 files changed

+545
-34
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ RUN apt install -y \
1919
curl \
2020
git \
2121
unzip
22-
22+
2323
# Install Flutter.
2424
ENV FLUTTER_ROOT="/opt/flutter"
2525
RUN git clone https://github.com/flutter/flutter "${FLUTTER_ROOT}"

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Cocoon has several components:
3030
Engine](https://github.com/dart-lang/appengine_samples). The server
3131
is found in [app_dart](app_dart/).
3232

33-
* A Flutter app (generally used as a Web app) for the build
33+
* A Flutter app (generally used as a Web app) for the build
3434
dashboards. The dashboard is found in [dashboard](dashboard/).
3535

3636
Cocoon creates a _checklist_ for each Flutter commit. A checklist is
@@ -69,7 +69,7 @@ This will output `Serving requests at 0.0.0.0:8080` indicating the server is wor
6969

7070
New requests will be logged to the console.
7171

72-
To develop and test some features, you need to have a local service
72+
To develop and test some features, you need to have a local service
7373
account(key.json) with access to the project you will be connecting to.
7474

7575
If you work for Google you can use the key with flutter-dashboard project

analyze/analyze.dart

+239
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// Copyright 2020 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:convert';
6+
import 'dart:core';
7+
import 'dart:io';
8+
9+
import 'package:file/file.dart';
10+
import 'package:file/local.dart';
11+
import 'package:path/path.dart' as path;
12+
13+
const FileSystem fs = LocalFileSystem();
14+
15+
// Cocoon's root is the parent of the current working directory,
16+
final Directory cocoonRoot = fs.currentDirectory.parent;
17+
18+
Future<void> main(List<String> arguments) async {
19+
print('STARTING ANALYSIS');
20+
print('cocoonRoot: ${cocoonRoot.path}');
21+
await run(arguments);
22+
print('Analysis successful.');
23+
}
24+
25+
Future<void> run(List<String> arguments) async {
26+
bool assertsEnabled = false;
27+
assert(() {
28+
assertsEnabled = true;
29+
return true;
30+
}());
31+
if (!assertsEnabled) {
32+
exitWithError(<String>['The analyze.dart script must be run with --enable-asserts.']);
33+
}
34+
35+
print('Trailing spaces...');
36+
await verifyNoTrailingSpaces(cocoonRoot.path);
37+
38+
print('Executable allowlist...');
39+
await _checkForNewExecutables();
40+
}
41+
42+
// TESTS
43+
44+
Future<void> verifyNoTrailingSpaces(
45+
String workingDirectory, {
46+
int minimumMatches = 100,
47+
}) async {
48+
final List<File> files = await _allFiles(workingDirectory, null, minimumMatches: minimumMatches)
49+
.where((File file) => path.basename(file.path) != 'serviceaccount.enc')
50+
.where((File file) => path.basename(file.path) != 'Ahem.ttf')
51+
.where((File file) => path.extension(file.path) != '.snapshot')
52+
.where((File file) => path.extension(file.path) != '.png')
53+
.where((File file) => path.extension(file.path) != '.jpg')
54+
.where((File file) => path.extension(file.path) != '.ico')
55+
.where((File file) => path.extension(file.path) != '.jar')
56+
.where((File file) => path.extension(file.path) != '.swp')
57+
.toList();
58+
final List<String> problems = <String>[];
59+
for (final File file in files) {
60+
final List<String> lines = file.readAsLinesSync();
61+
for (int index = 0; index < lines.length; index += 1) {
62+
if (lines[index].endsWith(' ')) {
63+
problems.add('${file.path}:${index + 1}: trailing U+0020 space character');
64+
} else if (lines[index].endsWith('\t')) {
65+
problems.add('${file.path}:${index + 1}: trailing U+0009 tab character');
66+
}
67+
}
68+
if (lines.isNotEmpty && lines.last == '') problems.add('${file.path}:${lines.length}: trailing blank line');
69+
}
70+
if (problems.isNotEmpty) exitWithError(problems);
71+
}
72+
73+
// UTILITY FUNCTIONS
74+
75+
Future<List<File>> _gitFiles(String workingDirectory, {bool runSilently = true}) async {
76+
final EvalResult evalResult = await _evalCommand(
77+
'git',
78+
<String>['ls-files', '-z'],
79+
workingDirectory: workingDirectory,
80+
runSilently: runSilently,
81+
);
82+
if (evalResult.exitCode != 0) {
83+
exitWithError(<String>[
84+
'git ls-files failed with exit code ${evalResult.exitCode}',
85+
'stdout:',
86+
evalResult.stdout,
87+
'stderr:',
88+
evalResult.stderr,
89+
]);
90+
}
91+
final List<String> filenames = evalResult.stdout.split('\x00');
92+
assert(filenames.last.isEmpty); // git ls-files gives a trailing blank 0x00
93+
filenames.removeLast();
94+
return filenames.map<File>((String filename) => fs.file(path.join(workingDirectory, filename))).toList();
95+
}
96+
97+
Stream<File> _allFiles(String workingDirectory, String? extension, {required int minimumMatches}) async* {
98+
final Set<String> gitFileNamesSet = <String>{};
99+
gitFileNamesSet.addAll((await _gitFiles(workingDirectory)).map((File f) => path.canonicalize(f.absolute.path)));
100+
101+
assert(extension == null || !extension.startsWith('.'), 'Extension argument should not start with a period.');
102+
final Set<FileSystemEntity> pending = <FileSystemEntity>{fs.directory(workingDirectory)};
103+
int matches = 0;
104+
while (pending.isNotEmpty) {
105+
final FileSystemEntity entity = pending.first;
106+
pending.remove(entity);
107+
if (path.extension(entity.path) == '.tmpl') continue;
108+
if (entity is File) {
109+
if (!gitFileNamesSet.contains(path.canonicalize(entity.absolute.path))) continue;
110+
if (path.basename(entity.path) == 'flutter_export_environment.sh') continue;
111+
if (path.basename(entity.path) == 'gradlew.bat') continue;
112+
if (path.basename(entity.path) == '.DS_Store') continue;
113+
if (extension == null || path.extension(entity.path) == '.$extension') {
114+
matches += 1;
115+
yield entity;
116+
}
117+
} else if (entity is Directory) {
118+
if (fs.file(path.join(entity.path, '.dartignore')).existsSync()) continue;
119+
if (path.basename(entity.path) == '.git') continue;
120+
if (path.basename(entity.path) == '.idea') continue;
121+
if (path.basename(entity.path) == '.gradle') continue;
122+
if (path.basename(entity.path) == '.dart_tool') continue;
123+
if (path.basename(entity.path) == '.idea') continue;
124+
if (path.basename(entity.path) == 'build') continue;
125+
pending.addAll(entity.listSync());
126+
}
127+
}
128+
assert(matches >= minimumMatches,
129+
'Expected to find at least $minimumMatches files with extension ".$extension" in "$workingDirectory", but only found $matches.');
130+
}
131+
132+
class EvalResult {
133+
EvalResult({
134+
required this.stdout,
135+
required this.stderr,
136+
this.exitCode = 0,
137+
});
138+
139+
final String stdout;
140+
final String stderr;
141+
final int exitCode;
142+
}
143+
144+
Future<EvalResult> _evalCommand(
145+
String executable,
146+
List<String> arguments, {
147+
required String workingDirectory,
148+
Map<String, String>? environment,
149+
bool allowNonZeroExit = false,
150+
bool runSilently = false,
151+
}) async {
152+
final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
153+
final String relativeWorkingDir = path.relative(workingDirectory);
154+
155+
if (!runSilently) {
156+
print('RUNNING $relativeWorkingDir $commandDescription');
157+
}
158+
159+
final Stopwatch time = Stopwatch()..start();
160+
final Process process = await Process.start(
161+
executable,
162+
arguments,
163+
workingDirectory: workingDirectory,
164+
environment: environment,
165+
);
166+
167+
final Future<List<List<int>>> savedStdout = process.stdout.toList();
168+
final Future<List<List<int>>> savedStderr = process.stderr.toList();
169+
final int exitCode = await process.exitCode;
170+
final EvalResult result = EvalResult(
171+
stdout: utf8.decode((await savedStdout).expand<int>((List<int> ints) => ints).toList()),
172+
stderr: utf8.decode((await savedStderr).expand<int>((List<int> ints) => ints).toList()),
173+
exitCode: exitCode,
174+
);
175+
176+
if (!runSilently) {
177+
print('ELAPSED TIME: ${time.elapsed} for $commandDescription in $relativeWorkingDir');
178+
}
179+
180+
if (exitCode != 0 && !allowNonZeroExit) {
181+
stderr.write(result.stderr);
182+
exitWithError(<String>[
183+
'ERROR: Last command exited with $exitCode.',
184+
'Command: $commandDescription',
185+
'Relative working directory: $relativeWorkingDir',
186+
]);
187+
}
188+
189+
return result;
190+
}
191+
192+
// These files legitimately require executable permissions
193+
const Set<String> kExecutableAllowlist = <String>{
194+
'build_and_analyze.sh',
195+
'dev/provision_salt.sh',
196+
'format.sh',
197+
'oneoff/cirrus_stats/load.sh',
198+
'test.sh',
199+
'test_utilities/bin/config_test_runner.sh',
200+
'test_utilities/bin/dart_test_runner.sh',
201+
'test_utilities/bin/flutter_test_runner.sh',
202+
'test_utilities/bin/global_test_runner.dart',
203+
'test_utilities/bin/prepare_environment.sh',
204+
};
205+
206+
Future<void> _checkForNewExecutables() async {
207+
// 0b001001001
208+
const int executableBitMask = 0x49;
209+
210+
final List<File> files = await _gitFiles(cocoonRoot.path);
211+
int unexpectedExecutableCount = 0;
212+
for (final File file in files) {
213+
final String relativePath = path.relative(
214+
file.path,
215+
from: cocoonRoot.path,
216+
);
217+
final FileStat stat = file.statSync();
218+
final bool isExecutable = stat.mode & executableBitMask != 0x0;
219+
if (isExecutable && !kExecutableAllowlist.contains(relativePath)) {
220+
unexpectedExecutableCount += 1;
221+
print('$relativePath is executable: ${(stat.mode & 0x1FF).toRadixString(2)}');
222+
}
223+
}
224+
if (unexpectedExecutableCount > 0) {
225+
throw Exception(
226+
'found $unexpectedExecutableCount unexpected executable file'
227+
'${unexpectedExecutableCount == 1 ? '' : 's'}! If this was intended, you '
228+
'must add this file to kExecutableAllowlist in analyze/analyze.dart',
229+
);
230+
}
231+
}
232+
233+
void exitWithError(List<String> messages) {
234+
final String line = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
235+
print(line);
236+
messages.forEach(print);
237+
print(line);
238+
exit(1);
239+
}

0 commit comments

Comments
 (0)