blob: ee6f05dbfd48d3d070a8efde2f7ac00a6c1520cd [file] [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
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:args/command_runner.dart';
import 'package:dartdev/dartdev.dart';
import 'package:dartdev/src/commands/run.dart';
import 'package:dartdev/src/eval_packages.dart';
import 'package:dartdev/src/utils.dart';
import 'package:dartdev/src/vm_interop_handler.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import '../utils.dart';
void main() {
group('eval option parsing', () {
test(
'globalDartdevOptionsParser does not include -e, -P / --package-constraint, or --offline',
() {
final parser = globalDartdevOptionsParser();
expect(parser.options.containsKey(evalOption), isFalse);
expect(parser.options.containsKey(packageConstraintOption), isFalse);
expect(parser.options.containsKey(offlineOption), isFalse);
expect(parser.options.containsKey('package'), isFalse);
expect(parser.options.containsKey('with'), isFalse);
},
);
test(
'RunCommand argParser includes -e, -P / --package-constraint, --offline',
() {
final runCmd = RunCommand();
expect(runCmd.argParser.options.containsKey(evalOption), isTrue);
expect(runCmd.argParser.options[evalOption]?.abbr, equals('e'));
expect(
runCmd.argParser.options.containsKey(packageConstraintOption),
isTrue,
);
expect(
runCmd.argParser.options[packageConstraintOption]?.abbr,
equals('P'),
);
expect(runCmd.argParser.options.containsKey(offlineOption), isTrue);
expect(runCmd.argParser.options.containsKey('package'), isFalse);
expect(runCmd.argParser.options.containsKey('with'), isFalse);
},
);
test(
'RunCommand argParser accepts VM options after -P and -e while preserving script args',
() {
final runCmd = RunCommand();
final parsed = runCmd.argParser.parse(const [
'-P',
'path',
'--enable-asserts',
'-e',
'void main(List<String> args) { assert(args.isNotEmpty); }',
'--define=foo=bar',
'script_arg',
'--script-flag',
]);
expect(parsed.multiOption(packageConstraintOption), equals(['path']));
expect(parsed.flag('enable-asserts'), isTrue);
expect(parsed.multiOption('define'), equals(['foo=bar']));
expect(
parsed.option(evalOption),
equals('void main(List<String> args) { assert(args.isNotEmpty); }'),
);
expect(parsed.rest, equals(['script_arg', '--script-flag']));
},
);
test('RunCommand argParser parses -e, -P, --offline, and arguments', () {
final runCmd = RunCommand();
final results = runCmd.argParser.parse(const [
'-P',
'http',
'--offline',
'-e',
'void main() {}',
'arg1',
'arg2',
]);
expect(results.wasParsed(evalOption), isTrue);
expect(results.option(evalOption), equals('void main() {}'));
expect(results.wasParsed(packageConstraintOption), isTrue);
expect(
results.multiOption(packageConstraintOption),
equals(const ['http']),
);
expect(results.wasParsed(offlineOption), isTrue);
expect(results.flag(offlineOption), isTrue);
expect(results.rest, equals(const ['arg1', 'arg2']));
});
});
group('PackageUriExtension', () {
test('isPackage correctly identifies valid package URIs', () {
expect(Uri.parse('package:http/http.dart').isPackage, isTrue);
expect(Uri.parse('package:path').isPackage, isTrue);
expect(Uri.parse('package:').isPackage, isFalse);
expect(Uri.parse('package:/').isPackage, isFalse);
expect(Uri.parse('dart:io').isPackage, isFalse);
expect(Uri.parse('file:///foo/bar.dart').isPackage, isFalse);
expect(Uri.parse('https://example.com').isPackage, isFalse);
});
test('packageName extracts package name from package URIs', () {
expect(
Uri.parse('package:http/http.dart').packageName,
equals('http'),
);
expect(
Uri.parse('package:path').packageName,
equals('path'),
);
expect(Uri.parse('package:').packageName, isNull);
expect(Uri.parse('package:/').packageName, isNull);
expect(Uri.parse('dart:io').packageName, isNull);
expect(Uri.parse('file:///foo/bar.dart').packageName, isNull);
});
});
group('EvalPackageResolver AST import extraction', () {
test('extracts package imports using Dart AST parser', () {
const code = '''
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' show join;
// import 'package:commented_out/commented_out.dart';
/*
import 'package:block_commented/block.dart';
*/
void main() {
const fakeImport = "import 'package:in_string/in_string.dart'";
print(fakeImport);
}
''';
final imports = EvalPackageResolver.extractPackageImports(code);
expect(imports, equals(const {'http', 'path'}));
});
});
group('EvalPackageResolver package config resolution', () {
late Directory tempDir;
setUp(() {
tempDir = Directory.systemTemp.createTempSync('dartdev_eval_test_');
});
tearDown(() {
if (tempDir.existsSync()) {
tempDir.deleteSync(recursive: true);
}
});
test('throws StateError when localPackageConfig does not exist', () async {
final nonexistent = path.join(tempDir.path, 'missing_config.json');
await expectLater(
EvalPackageResolver.resolvePackageConfig(
'import "package:foo/foo.dart"; void main() {}',
localPackageConfig: nonexistent,
),
throwsA(
isA<StateError>().having(
(e) => e.message,
'message',
contains('Specified package configuration file does not exist'),
),
),
);
});
test(
'throws StateError when localPackageConfig has invalid JSON',
() async {
final invalidFile = File(path.join(tempDir.path, 'bad_config.json'))
..writeAsStringSync('{ malformed json');
await expectLater(
EvalPackageResolver.resolvePackageConfig(
'import "package:foo/foo.dart"; void main() {}',
localPackageConfig: invalidFile.path,
),
throwsA(
isA<StateError>().having(
(e) => e.message,
'message',
contains('Failed to parse package configuration file'),
),
),
);
},
);
test(
'reuses localPackageConfig when no explicit constraints and packages present',
() async {
final configFile = File(path.join(tempDir.path, 'valid_config.json'))
..writeAsStringSync(
jsonEncode({
'configVersion': 2,
'packages': [
{'name': 'http', 'rootUri': 'file:///mock/http'},
],
}),
);
final result = await EvalPackageResolver.resolvePackageConfig(
'import "package:http/http.dart"; void main() {}',
localPackageConfig: configFile.path,
);
expect(result, equals(configFile.path));
},
);
test(
'resolves ephemeral pub get on pre-release SDKs using -0 version suffix',
() async {
final mockPkgDir = Directory(path.join(tempDir.path, 'mock_pkg'))
..createSync(recursive: true);
File(path.join(mockPkgDir.path, 'pubspec.yaml')).writeAsStringSync('''
name: mock_pkg
version: 1.0.0
environment:
sdk: '>=3.0.0 <4.0.0'
''');
Directory(path.join(mockPkgDir.path, 'lib')).createSync();
File(
path.join(mockPkgDir.path, 'lib', 'mock_pkg.dart'),
).writeAsStringSync('final int meaning = 42;\n');
final result = await EvalPackageResolver.resolvePackageConfig(
'import "package:mock_pkg/mock_pkg.dart"; void main() {}',
packageConstraints: [
'mock_pkg: {path: "${mockPkgDir.path.replaceAll(r'\', '/')}"}',
],
offline: true,
);
expect(result, isNotNull);
expect(File(result!).existsSync(), isTrue);
final generatedPubspec = File(
path.join(File(result).parent.parent.path, 'pubspec.yaml'),
).readAsStringSync();
expect(generatedPubspec, contains(RegExp(r'sdk: "\^\d+\.\d+\.\d+-0"')));
},
);
});
group('RunCommand.runEval execution', () {
late ReceivePort receivePort;
late StreamController<List<Object?>> vmInteropEvents;
setUp(() {
vmInteropEvents = StreamController<List<Object?>>();
receivePort = ReceivePort()
..listen((msg) {
if (msg is List) {
vmInteropEvents.add(msg);
}
});
VmInteropHandler.initialize(receivePort.sendPort);
});
tearDown(() {
receivePort.close();
vmInteropEvents.close();
VmInteropHandler.initialize(null);
});
test(
'runEval invokes VmInteropHandler with data URI and useExecProcess',
() async {
const code = 'void main(List<String> args) { print("hello"); }';
final runCmd = RunCommand();
final args = runCmd.argParser.parse(
const ['-e', code, 'arg1', 'arg2'],
);
final exitCode = await runCmd.runEval(args);
expect(exitCode, equals(0));
final message = await vmInteropEvents.stream.first;
expect(message[0], equals(2));
final scriptUri = Uri.parse(message[1] as String);
expect(scriptUri.scheme, equals('data'));
expect(scriptUri.data?.mimeType, equals(dartMimeType));
expect(scriptUri.data?.contentAsString(encoding: utf8), equals(code));
expect(message[5], equals(const ['arg1', 'arg2']));
},
);
test(
'runEval preserves multiline code verbatim without modification',
() async {
const multilineCode = '''
import 'dart:math';
class Calculator {
int add(int a, int b) => a + b;
}
void main(List<String> args) {
final calc = Calculator();
print(calc.add(2, 3));
}
''';
final runCmd = RunCommand();
final args = runCmd.argParser.parse(const ['-e', multilineCode]);
final exitCode = await runCmd.runEval(args);
expect(exitCode, equals(0));
final message = await vmInteropEvents.stream.first;
expect(message[0], equals(2));
final scriptUri = Uri.parse(message[1] as String);
expect(scriptUri.scheme, equals('data'));
expect(
scriptUri.data?.contentAsString(encoding: utf8),
equals(multilineCode),
);
},
);
test('runEval respects explicit --packages option', () async {
const code = 'void main() {}';
final runCmd = RunCommand();
final args = runCmd.argParser.parse(const [
'--packages=/custom/path/package_config.json',
'-e',
code,
]);
final exitCode = await runCmd.runEval(args);
expect(exitCode, equals(0));
final message = await vmInteropEvents.stream.first;
expect(message[0], equals(2));
expect(message[3], equals('/custom/path/package_config.json'));
});
test('runEval returns errorExitCode when code is empty', () async {
final runCmd = RunCommand();
final args = runCmd.argParser.parse(const ['-e', '']);
final exitCode = await runCmd.runEval(args);
expect(exitCode, equals(RunCommand.errorExitCode));
});
});
group('DartdevRunner top-level eval option rejection', () {
test('raw dart -e throws UsageException', () async {
const args = ['--suppress-analytics', '-e', 'void main() { print(1); }'];
final runner = DartdevRunner(args);
await expectLater(
runner.run(args),
throwsA(isA<UsageException>()),
);
});
test('raw dart -P <pkg> throws UsageException', () async {
const args = ['--suppress-analytics', '-P', 'path'];
final runner = DartdevRunner(args);
await expectLater(
runner.run(args),
throwsA(isA<UsageException>()),
);
});
test('raw dart --eval throws UsageException', () async {
const args = [
'--suppress-analytics',
'--eval',
'void main() { print(1); }',
];
final runner = DartdevRunner(args);
await expectLater(
runner.run(args),
throwsA(isA<UsageException>()),
);
});
});
group('end-to-end process execution', () {
test('dart -e exits with usage exit code 64', () async {
final p = project();
final result = await p.run(['-e', 'void main() {}']);
expect(result.exitCode, equals(DartdevRunner.usageExitCode));
});
test(
'dart run -e succeeds from a directory with no pubspec.yaml or package_config.json',
() async {
final emptyDir = Directory.systemTemp.createTempSync('eval_no_pkg_');
addTearDown(() {
if (emptyDir.existsSync()) {
emptyDir.deleteSync(recursive: true);
}
});
final p = project();
final result = await p.run(
['run', '-e', 'void main() { print("hello_outside_package"); }'],
workingDir: emptyDir.path,
);
expect(result.exitCode, equals(0), reason: 'stderr: ${result.stderr}');
expect(result.stdout, contains('hello_outside_package'));
},
);
test(
'dart run enables assertions when --enable-asserts follows -e',
() async {
final p = project();
final resultAfterE = await p.run([
'run',
'-e',
'void main() { assert(false, "boom_after_e"); }',
'--enable-asserts',
]);
expect(resultAfterE.exitCode, isNot(equals(0)));
expect(resultAfterE.stderr, contains('boom_after_e'));
},
);
test(
'dart run -e passes flags after -- separator to script args instead of VM',
() async {
final p = project();
final result = await p.run([
'run',
'-e',
r'void main(List<String> args) { assert(false); print("args=$args"); }',
'--',
'--enable-asserts',
]);
expect(result.exitCode, equals(0), reason: 'stderr: ${result.stderr}');
expect(result.stdout, contains('args=[--enable-asserts]'));
},
);
}, timeout: longTimeout);
}