Initialize SDK and regular projects on creation

Runs either `dart pub get` or `gclient sync`, based on project type.

For the SDK, copies the .gclient and .gclient_entries files as well
into the parent dir.

Also adds a `--timeout` argument, controls how long to wait for analyzer messages.

Change-Id: Id2c28e6d0251e94914bb0650be104c1f90a66651
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/468620
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Jake Macdonald <jakemac@google.com>
Auto-Submit: Jake Macdonald <jakemac@google.com>
This commit is contained in:
Jake Macdonald
2025-12-16 12:30:25 -08:00
committed by Commit Queue
parent ee4ec6c84d
commit db06bb7f13
6 changed files with 131 additions and 21 deletions
@@ -15,6 +15,13 @@ import 'package:collection/collection.dart';
import 'log.dart';
import 'server_driver.dart';
/// Some messages from the analysis server should just be ignored.
bool _shouldSkip(Message message) =>
// The server always sends this but we don't record it.
message.method == 'workspace/configuration' ||
// This is the response to the initialize request.
message.id == 0;
/// An object used to play back the messages in a log.
///
/// A reasonable attempt is made to retain the same timing of messages as was
@@ -33,7 +40,10 @@ class LogPlayer {
/// options from command line arguments.
final driverArgParser = Driver.createArgParser();
LogPlayer({required this.log});
/// How long to wait for expected analyzer logs to come back.
final Duration timeout;
LogPlayer({required this.log, this.timeout = const Duration(seconds: 5)});
/// Plays the log.
Future<void> play() async {
@@ -73,7 +83,7 @@ class LogPlayer {
actualServerMessageIds[foundMessage.id] = message.id;
}
pendingServerMessageExpectations.remove(foundMessage);
} else {
} else if (!_shouldSkip(message)) {
stderr.writeln(
'Unexpected message from analysis server:\n'
'${jsonEncode(message)}',
@@ -171,7 +181,7 @@ receiver: ${entry.receiver}
) async {
if (pendingServerMessageExpectations.isEmpty) return;
var watch = Stopwatch()..start();
while (watch.elapsed < const Duration(seconds: 5)) {
while (watch.elapsed < timeout) {
if (pendingServerMessageExpectations.isEmpty) {
return;
}
@@ -27,6 +27,7 @@ class GitCloneProjectGenerator implements ProjectGenerator {
await runGitCommand(['clone', repo, '.'], outputDir);
await runGitCommand(['fetch', 'origin', ref], outputDir);
await runGitCommand(['checkout', ref], outputDir);
await runPubGet(outputDir);
return [outputDir];
}
@@ -4,6 +4,8 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import '../utilities/git.dart';
import 'project_generator.dart';
@@ -16,7 +18,18 @@ class GitWorktreeProjectGenerator implements ProjectGenerator {
/// The ref (commit sha, tag, or branch) to check out into a new working tree.
final String ref;
GitWorktreeProjectGenerator(this.originalRepo, this.ref);
/// Whether or not this is an SDK repo. If it is, we use gclient and set things
/// up a bit differently.
final bool isSdkRepo;
/// The root temp dir to clean up, if it isn't the same as the project dir.
Directory? tmpDir;
GitWorktreeProjectGenerator(
this.originalRepo,
this.ref, {
this.isSdkRepo = false,
});
@override
String get description =>
@@ -25,12 +38,27 @@ class GitWorktreeProjectGenerator implements ProjectGenerator {
@override
Future<Iterable<Directory>> setUp() async {
var projectDir = await Directory.systemTemp.createTemp('as_git_worktree');
if (isSdkRepo) {
if (tmpDir != null) {
throw StateError(
'Project already set up, must wait for tearDown to complete to call '
'setUp again',
);
}
tmpDir = projectDir;
projectDir = Directory(p.join(projectDir.path, 'sdk'));
}
await runGitCommand([
'worktree',
'add',
'-d',
projectDir.path,
], originalRepo);
if (isSdkRepo) {
await _setUpSdk(projectDir);
} else {
await runPubGet(projectDir);
}
return [projectDir];
}
@@ -45,5 +73,27 @@ class GitWorktreeProjectGenerator implements ProjectGenerator {
'-f',
workspaceDirs.single.path,
], originalRepo);
await tmpDir?.delete(recursive: true);
tmpDir = null;
}
Future<void> _setUpSdk(Directory projectDir) async {
print('Running gclient sync in ${projectDir.path}');
var newGclientDir = p.dirname(projectDir.path);
var oldGclientDir = p.dirname(p.normalize(originalRepo.path));
for (var file in ['.gclient', '.gclient_entries']) {
await File(p.join(oldGclientDir, file)).copy(p.join(newGclientDir, file));
}
var gclientSyncResult = await Process.run('gclient', [
'sync',
], workingDirectory: projectDir.path);
if (gclientSyncResult.exitCode != 0) {
throw StateError(
'Failed to run `gclient sync`:\n'
'StdOut:\n${gclientSyncResult.stdout}\n'
'StdErr:\n${gclientSyncResult.stderr}',
);
}
}
}
@@ -4,6 +4,31 @@
import 'dart:io';
import 'package:path/path.dart' as p;
/// Runs `dart pub get` in [projectDir] if it contains a pubspec.
//
// TODO(jakemac): Support flutter projects and workspaces.
Future<void> runPubGet(Directory projectDir) async {
var pubspec = File(p.join(p.normalize(projectDir.path), 'pubspec.yaml'));
if (pubspec.existsSync()) {
print('Fetching dependencies with pub in ${projectDir.path}');
var pubGetResult = await Process.run('dart', [
'pub',
'get',
], workingDirectory: projectDir.path);
if (pubGetResult.exitCode != 0) {
throw StateError(
'Failed to run `dart pub get`:\n'
'StdOut:\n${pubGetResult.stdout}\n'
'StdErr:\n${pubGetResult.stderr}',
);
}
} else {
print('No pubspec.yaml found in ${projectDir.path}, skipping `pub get`');
}
}
/// A [ProjectGenerator] represents a reproducible way to create a pristine
/// copy of a codebase.
///
@@ -23,7 +23,7 @@ void main(List<String> args) async {
if (scenarioNames.isNotEmpty && !scenarioNames.contains(scenario.name)) {
continue;
}
await scenario.run();
await scenario.run(Duration(seconds: int.parse(parsed.option('timeout')!)));
}
}
@@ -38,6 +38,12 @@ final argParser = ArgParser()
help: 'The name(s) of specific scenario(s) to run',
allowed: scenarios.map((s) => s.name).toList(),
)
..addOption(
'timeout',
abbr: 't',
help: 'Number of seconds to wait for analyzer responses',
defaultsTo: '5',
)
..addFlag('help');
final logsRoot = analysisServerRoot.resolve(
@@ -52,7 +58,11 @@ final List<Scenario> scenarios = () {
logFile: fileSystem.getFile(
logsRoot.resolve('sdk_rename_driver_class.json').toFilePath(),
),
project: GitWorktreeProjectGenerator(Directory.fromUri(sdkRoot), 'main'),
project: GitWorktreeProjectGenerator(
Directory.fromUri(sdkRoot),
'main',
isSdkRepo: true,
),
),
Scenario(
name: 'initialize',
@@ -67,4 +77,4 @@ final List<Scenario> scenarios = () {
];
}();
final sdkRoot = analysisServerRoot.resolve('../../');
final sdkRoot = analysisServerRoot.resolve('../../../');
@@ -2,6 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:analyzer/file_system/file_system.dart';
@@ -16,42 +17,55 @@ final dartSdkRoot = p.dirname(p.dirname(Platform.resolvedExecutable));
/// A [Scenario] represents a combination of a [project] and a [logFile] to
/// replay in that project.
class Scenario {
/// Can be used on the command line to select this scenario.
///
/// Should be lowercase with underscores and no spaces.
final String name;
/// The log file to replay for this scenario.
final File logFile;
/// Handles project setup.
final ProjectGenerator project;
Scenario({required this.name, required this.logFile, required this.project});
Future<void> run() async {
Future<void> run(Duration timeout) async {
var watch = Stopwatch()..start();
void log(String message) {
print('${watch.elapsed}: $message');
}
await runZoned(
() => _run(timeout),
zoneSpecification: ZoneSpecification(
print: (_, _, _, message) =>
stdout.writeln('${watch.elapsed}: $message'),
),
);
}
log('Initializing scenario for project: ${project.description}');
Future<void> _run(Duration timeout) async {
print('Initializing scenario for project: ${project.description}');
log('Setting up project');
print('Setting up project');
var projectDirs = await project.setUp();
log('Reading logs');
print('Reading logs');
var logs = Log.fromFile(logFile, {
for (var i = 0; i < projectDirs.length; i++)
'{{workspaceFolder-$i}}': projectDirs.elementAt(i).path,
'{{dartSdkRoot}}': dartSdkRoot,
});
log('Creating log player');
var logPlayer = LogPlayer(log: logs);
print('Creating log player');
var logPlayer = LogPlayer(log: logs, timeout: timeout);
log(
print(
'Scenario initialized with workpace dirs:\n'
'${projectDirs.map((dir) => ' - ${dir.path}').join('\n')}',
);
try {
var scenarioWatch = Stopwatch()..start();
log('Replaying scenario');
print('Replaying scenario');
await logPlayer.play();
log('Scenario completed, took ${scenarioWatch.elapsed} to replay');
print('Scenario completed, took ${scenarioWatch.elapsed} to replay');
} catch (e, s) {
print('''
Scenario failed with Error: $e
@@ -60,9 +74,9 @@ StackTrace:
$s
''');
} finally {
log('Tearing down scenario for project');
print('Tearing down scenario for project');
await project.tearDown(projectDirs);
log('Scenario cleaned up');
print('Scenario cleaned up');
}
}
}