blob: def66b70364f1cf20bcb07c8964786042626b3c0 [file] [log] [blame] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:dart_data_home/dart_data_home.dart';
import 'package:dtd_impl/dtd.dart';
import 'package:dtd_impl/src/dtd_connection_info.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
void main() {
DartToolingDaemon? dtd;
late Directory tempDir;
late Map<String, String> env;
// In the CI environment, `dart test` executes in a temporary build directory,
// meaning `Platform.script` does not reliably point to the local source tree.
// We use `Isolate.resolvePackageUri` in `setUpAll` to dynamically find the
// correct absolute path to `bin/dtd.dart` so test subprocesses do not crash.
late String dtdScriptPath;
Future<(Process process, String uri)> startDtdProcess() async {
final process = await Process.start(
Platform.resolvedExecutable,
['run', dtdScriptPath, '--port=0'],
environment: env,
);
String? uri;
process.stderr.transform(utf8.decoder).listen((error) {
if (error.trim().isNotEmpty) {
print('DTD stderr: $error');
}
});
final uriRegex =
RegExp(r'The Dart Tooling Daemon is listening on (ws://.*)');
await for (final line in process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())) {
if (line.startsWith('The Dart Tooling Daemon is listening on')) {
final match = uriRegex.firstMatch(line);
if (match != null) {
uri = match.group(1);
}
} else if (line.startsWith('Trusted Client Secret')) {
break; // We have both the URI (printed first) and the secret.
}
}
if (uri == null) {
process.kill();
throw StateError('Failed to start DTD process. No URI printed.');
}
// Since _recordDtdConnectionInfo() happens right after the print, poll
// briefly to ensure the file system has caught up before letting tests proceed.
final dataHome = getDartDataHome(dtdDirName, environment: env);
final pidFile = File(p.join(dataHome, process.pid.toString()));
for (var i = 0; i < 20; i++) {
if (pidFile.existsSync()) break;
await Future<void>.delayed(const Duration(milliseconds: 50));
}
return (process, uri);
}
// To prevent test flakiness when running `dart test` in parallel, we
// isolate this test suite's `dart_data_home` directory. Other test files
// (like `dtd_test.dart`) may spin up DTD instances that write to the
// default system data directories. By spoofing the environment variables
// (LOCALAPPDATA, HOME, XDG_DATA_HOME), we ensure this suite only sees its
// own PID files in a clean, isolated temporary directory.
setUpAll(() async {
final packageUri = await Isolate.resolvePackageUri(
Uri.parse('package:dtd_impl/'),
);
dtdScriptPath = p.normalize(
p.join(packageUri!.toFilePath(), '../bin/dtd.dart'),
);
tempDir = Directory.systemTemp.createTempSync('dtd_list_test_');
env = Map<String, String>.from(Platform.environment);
if (Platform.isWindows) {
env['LOCALAPPDATA'] = tempDir.path;
env['APPDATA'] = tempDir.path;
} else if (Platform.isMacOS) {
env['HOME'] = tempDir.path;
} else {
env['XDG_DATA_HOME'] = tempDir.path;
env['HOME'] = tempDir.path;
}
});
setUp(() {
DartToolingDaemon.environmentOverride = env;
});
tearDown(() async {
await dtd?.close();
dtd = null;
DartToolingDaemon.environmentOverride = null;
final String dataHome = getDartDataHome(dtdDirName, environment: env);
try {
final dir = Directory(dataHome);
if (dir.existsSync()) {
for (final entity in dir.listSync()) {
entity.deleteSync(recursive: true);
}
}
} catch (_) {}
});
tearDownAll(() {
try {
tempDir.deleteSync(recursive: true);
} catch (_) {}
});
group('PID File Discovery', () {
test('broadcasts connection info via pid file', () async {
final (process, uri) = await startDtdProcess();
final String dataHome = getDartDataHome(dtdDirName, environment: env);
expect(dataHome, isNotEmpty);
final processPid = process.pid;
final file = File(p.join(dataHome, processPid.toString()));
expect(file.existsSync(), isTrue);
final content = file.readAsStringSync();
final json = jsonDecode(content) as Map<String, Object?>;
final info = DTDConnectionInfo.fromJson(json);
expect(info.wsUri, uri);
expect(info.pid, processPid);
expect(info.dartVersion, Platform.version);
expect(info.workspaceRoot, Directory.current.path);
process.kill();
expect(await process.exitCode, isNot(0));
});
test('connection info contains correct workspaceRoot', () async {
final (process, _) = await startDtdProcess();
final String dataHome = getDartDataHome(dtdDirName, environment: env);
final file = File(p.join(dataHome, process.pid.toString()));
expect(file.existsSync(), isTrue);
final content = file.readAsStringSync();
final json = jsonDecode(content) as Map<String, Object?>;
final info = DTDConnectionInfo.fromJson(json);
// Verify that workspaceRoot matches the current working directory of the process.
expect(info.workspaceRoot, Directory.current.path);
process.kill();
expect(await process.exitCode, isNot(0));
});
test('list cleans up malformed json pid files', () async {
final String dataHome = getDartDataHome(dtdDirName, environment: env);
final garbageFile = File(p.join(dataHome, '999999'));
garbageFile.writeAsStringSync('{ "bad_json": ');
// Trigger the list command
final result = await Process.run(
Platform.resolvedExecutable,
['run', dtdScriptPath, '--list'],
environment: env,
);
// It shouldn't crash, instead saying 0 instances
expect(result.stdout.toString(), contains(noInstancesMessage));
expect(result.stderr.toString(), isEmpty);
// And it should have deleted the bad file.
expect(garbageFile.existsSync(), isFalse);
});
test('list prints correct number of instances', () async {
// 0 instances initially.
var result = await Process.run(
Platform.resolvedExecutable,
['run', dtdScriptPath, '--list'],
environment: env,
);
expect(result.stdout.toString(), contains(noInstancesMessage));
// 1 instance.
final (dtd1, _) = await startDtdProcess();
result = await Process.run(
Platform.resolvedExecutable,
['run', dtdScriptPath, '--list'],
environment: env,
);
expect(
result.stdout.toString(),
contains('Found 1 Dart Tooling Daemon instance(s):'),
);
// 2 instances.
final (dtd2, _) = await startDtdProcess();
result = await Process.run(
Platform.resolvedExecutable,
['run', dtdScriptPath, '--list'],
environment: env,
);
expect(
result.stdout.toString(),
contains('Found 2 Dart Tooling Daemon instance(s):'),
);
dtd1.kill();
expect(await dtd1.exitCode, isNot(0));
dtd2.kill();
expect(await dtd2.exitCode, isNot(0));
});
test('list --machine prints JSON list of instances', () async {
// 0 instances initially.
var result = await Process.run(
Platform.resolvedExecutable,
['run', dtdScriptPath, '--list', '--machine'],
environment: env,
);
var json = jsonDecode(result.stdout.toString()) as List;
expect(json, isEmpty);
// 1 instance.
final (dtd1, _) = await startDtdProcess();
result = await Process.run(
Platform.resolvedExecutable,
['run', dtdScriptPath, '--list', '--machine'],
environment: env,
);
json = jsonDecode(result.stdout.toString()) as List;
expect(json.length, 1);
// 2 instances.
final (dtd2, _) = await startDtdProcess();
result = await Process.run(
Platform.resolvedExecutable,
['run', dtdScriptPath, '--list', '--machine'],
environment: env,
);
json = jsonDecode(result.stdout.toString()) as List;
expect(json.length, 2);
dtd1.kill();
expect(await dtd1.exitCode, isNot(0));
dtd2.kill();
expect(await dtd2.exitCode, isNot(0));
});
});
}