Skip to content

Commit ecaafad

Browse files
Add a new dt presubmit tool. (#9751)
1 parent 099628f commit ecaafad

File tree

5 files changed

+463
-0
lines changed

5 files changed

+463
-0
lines changed

tool/lib/commands/presubmit.dart

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright 2026 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'dart:async';
6+
import 'dart:io';
7+
8+
import 'package:args/command_runner.dart';
9+
import 'package:cli_util/cli_logging.dart';
10+
import 'package:io/io.dart';
11+
import 'package:meta/meta.dart';
12+
import 'package:path/path.dart' as path;
13+
14+
import '../model.dart';
15+
import '../utils.dart';
16+
17+
class PresubmitCommand extends Command {
18+
PresubmitCommand({@visibleForTesting this.processManager}) {
19+
argParser.addFlag(
20+
'fix',
21+
help: 'Apply dart fixes and formatting.',
22+
defaultsTo: false,
23+
negatable: false,
24+
);
25+
}
26+
27+
ProcessManager? processManager;
28+
29+
@override
30+
String get name => 'presubmit';
31+
32+
@override
33+
String get description =>
34+
'Run repo checks, analysis, fix, and format on all packages.';
35+
36+
@override
37+
Future run() async {
38+
final log = Logger.standard();
39+
final repo = DevToolsRepo.getInstance();
40+
final pm = processManager ?? ProcessManager();
41+
final fix = argResults!['fix'] as bool;
42+
43+
log.stdout('Running pub get...');
44+
final pubGetResult = await runner?.run(['pub-get']);
45+
if (pubGetResult is int && pubGetResult != 0) {
46+
log.stderr('Pub get failed. Exiting early.');
47+
return 1;
48+
}
49+
50+
final packages = repo.getPackages(includeSubdirectories: false);
51+
int failureCount = 0;
52+
53+
if (fix) {
54+
log.stdout('Running Dart Fix and Format...');
55+
for (final p in packages) {
56+
if (!p.hasAnyDartCode) continue;
57+
58+
final progress = log.progress(' ${p.relativePath}');
59+
60+
final fixProcess = await pm.runProcess(
61+
CliCommand.dart(['fix', '--apply'], throwOnException: false),
62+
workingDirectory: p.packagePath,
63+
);
64+
65+
final pathsToFormat = _getPathsToFormat(p);
66+
67+
final formatProcess = await pm.runProcess(
68+
CliCommand.dart(['format', ...pathsToFormat], throwOnException: false),
69+
workingDirectory: p.packagePath,
70+
);
71+
72+
if (fixProcess.exitCode == 0 && formatProcess.exitCode == 0) {
73+
progress.finish(showTiming: true);
74+
} else {
75+
failureCount++;
76+
progress.finish(message: 'failed');
77+
}
78+
}
79+
80+
if (failureCount > 0) {
81+
log.stderr('Presubmit failed.');
82+
log.stderr(' Fix or Format failed on $failureCount packages.');
83+
return 1;
84+
}
85+
}
86+
87+
log.stdout('Running Repo Check...');
88+
final repoCheckResult = await runner?.run(['repo-check']);
89+
if (repoCheckResult is int && repoCheckResult != 0) {
90+
log.stderr('Repo checks failed. Exiting early.');
91+
return 1;
92+
}
93+
94+
log.stdout('Running Analyze...');
95+
final analyzeResult = await runner?.run(['analyze']);
96+
if (analyzeResult is int && analyzeResult != 0) {
97+
log.stderr('Analysis failed. Exiting early.');
98+
return 1;
99+
}
100+
101+
if (!fix) {
102+
log.stdout('Running Dart Format Check...');
103+
for (final p in packages) {
104+
if (!p.hasAnyDartCode) continue;
105+
106+
final progress = log.progress(' ${p.relativePath}');
107+
108+
final pathsToFormat = _getPathsToFormat(p);
109+
110+
final formatProcess = await pm.runProcess(
111+
CliCommand.dart(
112+
['format', '--output=none', '--set-exit-if-changed', ...pathsToFormat],
113+
throwOnException: false,
114+
),
115+
workingDirectory: p.packagePath,
116+
);
117+
118+
if (formatProcess.exitCode == 0) {
119+
progress.finish(showTiming: true);
120+
} else {
121+
failureCount++;
122+
progress.finish(message: 'failed');
123+
}
124+
}
125+
126+
if (failureCount > 0) {
127+
log.stderr('Presubmit failed.');
128+
log.stderr(' Formatting issues found in $failureCount packages.');
129+
return 1;
130+
}
131+
}
132+
133+
log.stdout('Presubmit passed!');
134+
return 0;
135+
}
136+
137+
List<String> _getPathsToFormat(Package p) {
138+
final pathsToFormat = <String>[];
139+
if (p.relativePath == 'tool') {
140+
final children = Directory(p.packagePath).listSync();
141+
for (final entity in children) {
142+
final name = path.basename(entity.path);
143+
if (name.startsWith('.')) continue;
144+
if (name == 'flutter-sdk') continue;
145+
if (entity is Directory) {
146+
pathsToFormat.add(name);
147+
} else if (entity is File && name.endsWith('.dart')) {
148+
pathsToFormat.add(name);
149+
}
150+
}
151+
} else {
152+
pathsToFormat.add('.');
153+
}
154+
return pathsToFormat;
155+
}
156+
}

tool/lib/devtools_command_runner.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'package:devtools_tool/model.dart';
1818

1919
import 'commands/analyze.dart';
2020
import 'commands/list.dart';
21+
import 'commands/presubmit.dart';
2122
import 'commands/pub_get.dart';
2223
import 'commands/release_helper.dart';
2324
import 'commands/repo_check.dart';
@@ -37,6 +38,7 @@ class DevToolsCommandRunner extends CommandRunner {
3738
addCommand(FixGoldensCommand());
3839
addCommand(GenerateCodeCommand());
3940
addCommand(ListCommand());
41+
addCommand(PresubmitCommand());
4042
addCommand(PubGetCommand());
4143
addCommand(ReleaseHelperCommand());
4244
addCommand(ReleaseNotesCommand());

tool/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies:
1818
cli_util: ^0.4.1
1919
collection: ^1.19.0
2020
io: ^1.0.4
21+
meta: ^1.18.0
2122
path: ^1.9.0
2223
yaml: ^3.1.2
2324

tool/test/command_test_utils.dart

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2026 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'dart:async';
6+
import 'dart:convert';
7+
import 'dart:io';
8+
9+
import 'package:args/command_runner.dart';
10+
import 'package:io/io.dart';
11+
12+
class MockProcessManager implements ProcessManager {
13+
MockProcessManager({this.onSpawn});
14+
15+
final Future<Process> Function(
16+
String executable,
17+
Iterable<String> arguments, {
18+
String? workingDirectory,
19+
Map<String, String>? environment,
20+
bool includeParentEnvironment,
21+
bool runInShell,
22+
ProcessStartMode mode,
23+
})?
24+
onSpawn;
25+
26+
@override
27+
Future<Process> spawn(
28+
String executable,
29+
Iterable<String> arguments, {
30+
String? workingDirectory,
31+
Map<String, String>? environment,
32+
bool includeParentEnvironment = true,
33+
bool runInShell = false,
34+
ProcessStartMode mode = ProcessStartMode.normal,
35+
}) async {
36+
if (onSpawn != null) {
37+
return onSpawn!(
38+
executable,
39+
arguments,
40+
workingDirectory: workingDirectory,
41+
environment: environment,
42+
includeParentEnvironment: includeParentEnvironment,
43+
runInShell: runInShell,
44+
mode: mode,
45+
);
46+
}
47+
return MockProcess();
48+
}
49+
50+
@override
51+
Future<Process> spawnBackground(
52+
String executable,
53+
Iterable<String> arguments, {
54+
String? workingDirectory,
55+
Map<String, String>? environment,
56+
bool includeParentEnvironment = true,
57+
bool runInShell = false,
58+
ProcessStartMode mode = ProcessStartMode.normal,
59+
}) async {
60+
throw UnimplementedError();
61+
}
62+
63+
@override
64+
Future<Process> spawnDetached(
65+
String executable,
66+
Iterable<String> arguments, {
67+
String? workingDirectory,
68+
Map<String, String>? environment,
69+
bool includeParentEnvironment = true,
70+
bool runInShell = false,
71+
ProcessStartMode mode = ProcessStartMode.normal,
72+
}) async {
73+
throw UnimplementedError();
74+
}
75+
}
76+
77+
class MockProcess implements Process {
78+
MockProcess({
79+
this.exitCodeValue = 0,
80+
this.stdoutString = '',
81+
this.stderrString = '',
82+
});
83+
84+
final int exitCodeValue;
85+
final String stdoutString;
86+
final String stderrString;
87+
88+
@override
89+
Future<int> get exitCode => Future.value(exitCodeValue);
90+
91+
@override
92+
Stream<List<int>> get stdout => Stream.value(utf8.encode(stdoutString));
93+
94+
@override
95+
Stream<List<int>> get stderr => Stream.value(utf8.encode(stderrString));
96+
97+
@override
98+
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => true;
99+
100+
@override
101+
int get pid => 0;
102+
103+
@override
104+
IOSink get stdin => throw UnimplementedError();
105+
}
106+
107+
class TestCommandRunner extends CommandRunner {
108+
TestCommandRunner() : super('test', 'test description');
109+
110+
void addDummyCommand(String name, [int exitCode = 0]) {
111+
addCommand(DummyCommand(name, exitCode));
112+
}
113+
}
114+
115+
class DummyCommand extends Command {
116+
DummyCommand(this.name, this.exitCodeValue);
117+
118+
@override
119+
final String name;
120+
121+
@override
122+
String get description => 'Dummy command for testing';
123+
124+
final int exitCodeValue;
125+
126+
@override
127+
Future<int> run() async => exitCodeValue;
128+
}

0 commit comments

Comments
 (0)