blob: 2dcaa779ae1888cffef803b6c70debd6e973cd7d [file] [log] [blame]
// Copyright (c) 2020, 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 'package:dartdev/src/resident_frontend_constants.dart';
import 'package:dartdev/src/resident_frontend_utils.dart';
import 'package:kernel/binary/tag.dart' show sdkHashNull;
import 'package:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.dart';
import '../utils.dart';
const String soundNullSafetyMessage = 'Info: Compiling with sound null safety.';
const devToolsMessagePrefix =
'The Dart DevTools debugger and profiler is available at: http://127.0.0.1:';
const dartVMServiceMessagePrefix =
'The Dart VM service is listening on http://127.0.0.1:';
final dartVMServiceRegExp =
RegExp(r'The Dart VM service is listening on (http://127.0.0.1:.*)');
const residentFrontendCompilerPrefix =
'The Resident Frontend Compiler is listening at 127.0.0.1:';
const dtdMessagePrefix = 'The Dart Tooling Daemon (DTD) is available at:';
final observeScript = r'''
void main() async {
print('Observe smoke test!');
int i = 0;
while(true) {
await Future.delayed(Duration(milliseconds: 10));
i++;
}
}
''';
void Function(String) onVmServicesData(
TestProject p, {
bool expectDevtoolsMsg = true,
bool expectDtdMsg = false,
}) {
bool sawDevtoolsMsg = false;
bool sawVmServiceMsg = false;
bool sawProgramMsg = false;
bool sawDtdMsg = false;
void onDataImpl(event) {
if (event.contains(devToolsMessagePrefix)) {
sawDevtoolsMsg = true;
} else if (event.contains(dartVMServiceMessagePrefix)) {
sawVmServiceMsg = true;
} else if (event.contains('Observe smoke test!')) {
sawProgramMsg = true;
} else if (event.contains(dtdMessagePrefix)) {
sawDtdMsg = true;
}
if (sawProgramMsg &&
sawVmServiceMsg &&
(sawDtdMsg || !expectDtdMsg) &&
(sawDevtoolsMsg || !expectDevtoolsMsg)) {
expect(sawDtdMsg, expectDtdMsg);
expect(sawDevtoolsMsg, expectDevtoolsMsg);
p.kill();
}
}
return onDataImpl;
}
void main() async {
ensureRunFromSdkBinDart();
group('run', run, timeout: longTimeout);
group('run --resident', residentRun, timeout: longTimeout);
}
void run() {
late TestProject p;
test('--help', () async {
p = project();
var result = await p.run(['run', '--help']);
expect(result.stdout, contains('Run a Dart program.'));
expect(result.stdout, contains('Debugging options:'));
expect(
result.stdout,
contains(
'Usage: dart run [arguments] [<dart-file|package-target> [args]]',
),
);
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test('--help --verbose', () async {
p = project();
var result = await p.run(['run', '--help', '--verbose']);
expect(result.stdout, contains('Run a Dart program.'));
expect(result.stdout, contains('Debugging options:'));
expect(
result.stdout,
contains(
'Usage: dart [vm-options] run [arguments] [<dart-file|package-target> [args]]',
),
);
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test("'Hello World'", () async {
p = project(mainSrc: "void main() { print('Hello World'); }");
ProcessResult result = await p.run(['run', p.relativeFilePath]);
expect(result.stdout, contains('Hello World'));
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test('no such file', () async {
p = project(mainSrc: "void main() { print('Hello World'); }");
ProcessResult result =
await p.run(['run', 'no/such/file/${p.relativeFilePath}']);
expect(result.stderr, isNotEmpty);
expect(result.exitCode, isNot(0));
});
test('implicit packageName.dart', () async {
// TODO(jwren) circle back to reimplement this test if possible, the file
// name (package name) will be the name of the temporary directory on disk
p = project(mainSrc: "void main() { print('Hello World'); }");
p.file('bin/main.dart', "void main() { print('Hello main.dart'); }");
ProcessResult result = await p.run(['run']);
expect(result.stdout, contains('Hello main.dart'));
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
}, skip: true);
// Could not find the implicit file to run: bin
test('missing implicit packageName.dart', () async {
p = project(mainSrc: "void main() { print('Hello World'); }");
p.file('bin/foo.dart', "void main() { print('Hello main.dart'); }");
ProcessResult result = await p.run(['run']);
expect(result.stdout, isEmpty);
expect(
result.stderr,
contains('Could not find `bin${path.separator}dartdev_temp.dart` in '
'package `dartdev_temp`.'));
expect(result.exitCode, 255);
});
test('experiments are enabled correctly', () async {
// TODO(bkonyi): find a more robust way to test experiments by exposing
// enabled experiments for an isolate (e.g., through dart:developer or the
// VM service).
//
// See https://github.com/dart-lang/sdk/issues/50230
p = project(sdkConstraint: VersionConstraint.parse('>=3.0.0-0 <4.0.0'));
p.file('main.dart', 'void main(args) { print("Record: \${(1, 2)}"); }');
ProcessResult result = await p.run([
'run',
'--enable-experiment=records',
'main.dart',
]);
// The records experiment should be enabled.
expect(result.stdout, contains('Record: '));
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
// Run again with the experiment disabled to make sure the test is actually
// working as expected.
result = await p.run([
'run',
'main.dart',
]);
// The records experiment should not be enabled and the program should fail
// to run.
expect(result.stdout, isEmpty);
expect(result.stderr, isNotEmpty);
expect(result.exitCode, 254);
p.file('bin/main.dart', 'void main(args) { print("Record: \${(1, 2)}"); }');
// Run again with the package-syntax
result = await p.run([
'run',
'--enable-experiment=records',
':main',
]);
// The records experiment should not be enabled and the program should fail
// to run.
expect(result.stderr, isEmpty);
expect(result.stdout, contains('Record: '));
expect(result.exitCode, 0);
}, skip: 'records are enabled by default in 3.0');
test('arguments are properly passed', () async {
p = project();
p.file('main.dart', 'void main(args) { print(args); }');
ProcessResult result = await p.run([
'run',
'--enable-experiment=test-experiment',
'main.dart',
'argument1',
'argument2',
]);
// --enable-experiment and main.dart should not be passed.
expect(result.stdout, equals('[argument1, argument2]\n'));
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test('from path-dependency with cyclic dependency', () async {
p = project(name: 'foo');
final bar = project(name: 'bar');
p.file('pubspec.yaml', '''
name: foo
environment:
sdk: '>=2.12.0<3.0.0'
dependencies: { 'bar': {'path': '${bar.dir.path}'}}
''');
p.file('lib/foo.dart', r'''
import 'package:bar/bar.dart';
final b = "FOO $bar";
''');
bar.file('lib/bar.dart', 'final bar = "BAR";');
bar.file('bin/main.dart', r'''
import 'package:foo/foo.dart';
void main(List<String> args) => print("$b $args");
''');
ProcessResult result = await p.run(['run', 'bar:main', '--arg1', 'arg2']);
expect(result.stderr, isEmpty);
expect(result.stdout, contains('FOO BAR [--arg1, arg2]'));
expect(result.exitCode, 0);
});
test('with absolute file path', () async {
p = project();
p.file('main.dart', 'void main(args) { print(args); }');
// Test with absolute path
final name = path.join(p.dirPath, 'main.dart');
final result = await p.run([
'run',
'--enable-experiment=test-experiment',
name,
'--argument1',
'argument2',
]);
// --enable-experiment and main.dart should not be passed.
expect(result.stderr, isEmpty);
expect(result.stdout, equals('[--argument1, argument2]\n'));
expect(result.exitCode, 0);
});
test('with file uri', () async {
p = project();
p.file('main.dart', 'void main(args) { print(args); }');
// Test with File uri
final name = path.join(p.dirPath, 'main.dart');
final result = await p.run([
'run',
Uri.file(name).toString(),
'--argument1',
'argument2',
]);
// --enable-experiment and main.dart should not be passed.
expect(result.stderr, isEmpty);
expect(result.stdout, equals('[--argument1, argument2]\n'));
expect(result.exitCode, 0);
});
test('with accepted VM flags', () async {
p = project(mainSrc: observeScript);
// --observe sets the following flags by default:
// --enable-vm-service
// --pause-isolate-on-exit
// --pause-isolate-on-unhandled-exception
// --warn-on-pause-with-no-debugger
//
// This test ensures that allowed arguments for dart run which are valid VM
// arguments are properly handled by the VM.
void onData1(event) {
if (event.contains('The Dart VM service is listening on')) {
final re = RegExp(
r'The Dart VM service is listening on http:\/\/127.0.0.1:\d+\/[a-zA-Z0-9_-]+=\/.*');
expect(re.hasMatch(event), true);
p.kill();
}
}
await p.runWithVmService([
'run',
'--observe=0',
'--pause-isolates-on-start',
// This should negate the above flag.
'--no-pause-isolates-on-start',
'--no-pause-isolates-on-exit',
'--no-pause-isolates-on-unhandled-exceptions',
'-Dfoo=bar',
'--define=bar=foo',
p.relativeFilePath,
], onData1);
// Again, with --disable-service-auth-codes.
void onData2(event) {
if (event.contains('The Dart VM service is listening on')) {
final re = RegExp(
r'The Dart VM service is listening on http:\/\/127.0.0.1:\d+\/');
expect(re.hasMatch(event), true);
p.kill();
}
}
await p.runWithVmService([
'run',
'--observe=0',
'--pause-isolates-on-start',
// This should negate the above flag.
'--no-pause-isolates-on-start',
'--no-pause-isolates-on-exit',
'--no-pause-isolates-on-unhandled-exceptions',
'--disable-service-auth-codes',
'-Dfoo=bar',
'--define=bar=foo',
p.relativeFilePath,
], onData2);
// Again, with IPv6.
void onData3(event) {
if (event.contains('The Dart VM service is listening on')) {
final re = RegExp(
r'The Dart VM service is listening on http:\/\/\[::1\]:\d+\/[a-zA-Z0-9_-]+=\/.*');
expect(re.hasMatch(event), true);
p.kill();
}
}
await p.runWithVmService([
'run',
'--observe=0/::1',
'--pause-isolates-on-start',
// This should negate the above flag.
'--no-pause-isolates-on-start',
'--no-pause-isolates-on-exit',
'--no-pause-isolates-on-unhandled-exceptions',
'-Dfoo=bar',
'--define=bar=foo',
p.relativeFilePath,
], onData3);
});
test('with accepted VM flags related to the timeline', () async {
p = project(
mainSrc: 'import "dart:developer";'
'void main() {'
'Timeline.startSync("sync");'
'Timeline.finishSync();'
'}');
final result = await p.run([
'run',
'--timeline-recorder=file',
'--timeline-streams=Dart',
p.relativeFilePath
]);
expect(result.stderr, isEmpty);
expect(result.stdout, isEmpty);
expect(result.exitCode, 0);
expect(p.findFile('dart-timeline.json')!.readAsStringSync(),
contains('"name":"sync","cat":"Dart"'));
});
test('fails when provided verbose VM flags', () async {
p = project(mainSrc: "void main() { print('Hello World'); }");
// Any VM flags not listed under 'dart run help --verbose' should be passed
// before a dartdev command.
ProcessResult result = await p.run([
'run',
'--vm-name=foo',
p.relativeFilePath,
]);
expect(result.stdout, isEmpty);
expect(
result.stderr,
contains('Could not find an option named "vm-name".'),
);
expect(result.exitCode, 64);
});
test('fails when provided unlisted VM flags', () async {
p = project(mainSrc: "void main() { print('Hello World'); }");
// Any VM flags not listed under 'dart run help --verbose' should be passed
// before a dartdev command.
ProcessResult result = await p.run([
'run',
'--verbose_gc',
p.relativeFilePath,
]);
expect(result.stdout, isEmpty);
expect(
result.stderr,
contains('Could not find an option named "verbose_gc".'),
);
expect(result.exitCode, 64);
});
test('--enable-asserts', () async {
p = project(mainSrc: 'void main() { assert(false); }');
// Ensure --enable-asserts doesn't cause the dartdev isolate to fail to
// load. Regression test for: https://github.com/dart-lang/sdk/issues/42831
ProcessResult result = await p.run([
'run',
'--enable-asserts',
p.relativeFilePath,
]);
expect(result.stdout, isEmpty);
expect(result.stderr, contains('Unhandled exception'));
expect(result.exitCode, 255);
});
test('does not interpret VM flags provided after script', () async {
p = project(mainSrc: 'void main() { assert(false); }');
// Any VM flags passed after the script shouldn't be interpreted by the VM.
ProcessResult result = await p.run([
'run',
p.relativeFilePath,
'--enable-asserts',
]);
expect(result.stdout, isEmpty);
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test('--enable-service-port-fallback', () async {
final p = project(mainSrc: observeScript);
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
final regexp = RegExp(
r'The Dart VM service is listening on http:\/\/127.0.0.1:(\d*)\/[a-zA-Z0-9_-]+=\/.*',
);
void onData(event) {
if (event.contains('The Dart VM service is listening on')) {
final vmServicePort = int.parse(regexp.firstMatch(event)!.group(1)!);
expect(server.port != vmServicePort, isTrue);
p.kill();
}
}
await p.runWithVmService([
'run',
'--enable-vm-service=${server.port}',
'--enable-service-port-fallback',
p.relativeFilePath,
], onData);
await server.close();
});
test('regression test for dartbug.com/55185', () async {
final p = project(mainSrc: observeScript);
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
void onData(event) {
if (event.contains('The Dart VM service is listening on')) {
p.kill();
}
}
await p.runWithVmService([
'run',
'--enable-service-port-fallback',
// This argument must be the final option before the script name to
// correctly reproduce the failure. Without the fix, dart run's argument
// parser will assume the argument after --observe is the value for
// --observe, causing the script path to be swallowed, resulting in Pub
// trying to resolve the package path for the empty string.
'--observe',
p.relativeFilePath,
], onData);
await server.close();
});
test('without verbose CFE info', () async {
final p = project(mainSrc: '''void main() {}''');
var result = await p.run(
[
'run',
'--verbosity=warning',
p.relativeFilePath,
],
);
expect(result.stdout,
predicate((dynamic o) => !'$o'.contains(soundNullSafetyMessage)));
expect(result.stderr, isEmpty);
expect(result.exitCode, 0);
});
test('Handles invalid package_config.json', () async {
// Regression test for https://github.com/dart-lang/sdk/issues/55490
p = project(mainSrc: "void main() { print('Hello World'); }");
final packageConfig = File(p.packageConfigPath);
final contents = jsonDecode(packageConfig.readAsStringSync());
// Fail fast if the config version changes to make sure this test is kept
// up to date with package config format changes.
expect(contents['configVersion'], 2);
// Duplicate an entry from the package config's package list and overwrite
// the existing config.
final packages = contents['packages'] as List;
expect(packages, isNotEmpty);
final duplicatePackage = packages.first;
packages.add(duplicatePackage);
packageConfig.writeAsStringSync(jsonEncode(contents));
final result = await p.run(['run', p.relativeFilePath]);
expect(result.stdout, isEmpty);
expect(
result.stderr,
contains(
'Error encountered while parsing package_config.json: Duplicate package name',
),
);
expect(result.exitCode, 255);
});
test('workspace', () async {
final p = project(
sdkConstraint: VersionConstraint.parse('^3.5.0-0'),
pubspecExtras: {
'workspace': ['pkgs/a', 'pkgs/b']
});
p.file('pkgs/a/pubspec.yaml', '''
name: a
environment:
sdk: ^3.5.0-0
resolution: workspace
dependencies:
b:
''');
p.file('pkgs/b/pubspec.yaml', '''
name: b
environment:
sdk: ^3.5.0-0
resolution: workspace
''');
p.file('pkgs/a/bin/a.dart', '''
main() => print('a:a');
''');
p.file('pkgs/a/bin/tool.dart', '''
main() => print('a:tool');
''');
p.file('pkgs/b/bin/b.dart', '''
main() => print('b:b');
''');
expect(
await p
.run(['run', 'a'], workingDir: path.join(p.dirPath, 'pkgs', 'a')),
isA<ProcessResult>()
.having((r) => r.exitCode, 'exitCode', 0)
.having((r) => r.stdout, 'stdout', 'a:a\n'));
expect(
await p
.run(['run', 'a:a'], workingDir: path.join(p.dirPath, 'pkgs', 'a')),
isA<ProcessResult>()
.having((r) => r.exitCode, 'exitCode', 0)
.having((r) => r.stdout, 'stdout', 'a:a\n'));
expect(
await p.run(['run', ':tool'],
workingDir: path.join(p.dirPath, 'pkgs', 'a')),
isA<ProcessResult>()
.having((r) => r.exitCode, 'exitCode', 0)
.having((r) => r.stdout, 'stdout', 'a:tool\n'));
expect(
await p
.run(['run', 'b'], workingDir: path.join(p.dirPath, 'pkgs', 'a')),
isA<ProcessResult>()
.having((r) => r.exitCode, 'exitCode', 0)
.having((r) => r.stdout, 'stdout', 'b:b\n'));
});
group('DDS', () {
group('disable', () {
test('dart run simple', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService(
[
'run',
'--no-dds',
'--enable-vm-service=0',
p.relativeFilePath,
],
onVmServicesData(
p,
expectDevtoolsMsg: false,
),
);
});
test('dart simple', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService(
[
'--no-dds',
'--enable-vm-service=0',
p.relativeFilePath,
],
onVmServicesData(
p,
expectDevtoolsMsg: false,
),
);
});
});
group('explicit enable', () {
test('dart run simple', () async {
p = project(mainSrc: observeScript);
final tempDir = Directory.systemTemp.createTempSync('a');
final serviceInfo = path.join(tempDir.path, 'service.json');
await p.runWithVmService(
[
'run',
'--dds',
'--enable-vm-service=0',
'--write-service-info=$serviceInfo',
p.relativeFilePath,
],
onVmServicesData(p),
);
expect(File(serviceInfo).existsSync(), true);
});
test('dart simple', () async {
p = project(mainSrc: observeScript);
final tempDir = Directory.systemTemp.createTempSync('a');
final serviceInfo = path.join(tempDir.path, 'service.json');
await p.runWithVmService(
[
'--dds',
'--enable-vm-service=0',
'--write-service-info=$serviceInfo',
p.relativeFilePath,
],
onVmServicesData(p),
);
expect(File(serviceInfo).existsSync(), true);
});
});
});
group('DevTools', () {
test('dart run simple', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService([
'run',
'--enable-vm-service=0',
p.relativeFilePath,
], onVmServicesData(p));
});
test('dart simple', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService(
[
'--enable-vm-service=0',
p.relativeFilePath,
],
onVmServicesData(p),
);
});
test('dart run explicit', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService(
[
'run',
'--serve-devtools',
'--enable-vm-service=0',
p.relativeFilePath,
],
onVmServicesData(p),
);
});
test('dart explicit', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService([
'--serve-devtools',
'--enable-vm-service=0',
p.relativeFilePath,
], onVmServicesData(p));
});
test('dart run disabled', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService(
[
'run',
'--enable-vm-service=0',
'--no-serve-devtools',
p.relativeFilePath,
],
onVmServicesData(
p,
expectDevtoolsMsg: false,
),
);
});
test('dart disabled', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService(
[
'--enable-vm-service=0',
'--no-serve-devtools',
p.relativeFilePath,
],
onVmServicesData(
p,
expectDevtoolsMsg: false,
),
);
});
test('dart run VM service not enabled', () async {
p = project(mainSrc: "void main() { print('Hello World'); }");
ProcessResult result = await p.run([
'run',
'--serve-devtools',
p.relativeFilePath,
]);
expect(result.stdout, isNot(contains(devToolsMessagePrefix)));
});
test('dart VM service not enabled', () async {
p = project(mainSrc: "void main() { print('Hello World'); }");
ProcessResult result = await p.run([
'--serve-devtools',
p.relativeFilePath,
]);
expect(result.stdout, isNot(contains(devToolsMessagePrefix)));
});
test(
'spawn via SIGQUIT',
() async {
p = project(
mainSrc:
'void main() { print("ready"); int i = 0; while(true) { i++; } }',
);
Process process = await p.start([
p.relativeFilePath,
]);
final readyCompleter = Completer<void>();
final completer = Completer<void>();
late StreamSubscription sub;
sub = process.stdout.transform(utf8.decoder).listen((event) async {
if (event.contains('ready')) {
readyCompleter.complete();
} else if (event.contains(devToolsMessagePrefix)) {
await sub.cancel();
completer.complete();
}
});
// Wait for process to start.
await readyCompleter.future;
process.kill(ProcessSignal.sigquit);
await completer.future;
process.kill();
},
// No support for SIGQUIT on Windows.
skip: Platform.isWindows,
);
});
group('--print-dtd', () {
test('dart', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService(
[
'--enable-vm-service=0',
'--print-dtd',
p.relativeFilePath,
],
onVmServicesData(
p,
expectDtdMsg: true,
));
});
test('dart run', () async {
p = project(mainSrc: observeScript);
await p.runWithVmService(
[
'run',
'--enable-vm-service=0',
'--print-dtd',
p.relativeFilePath,
],
onVmServicesData(
p,
expectDtdMsg: true,
),
);
});
});
group('Observatory', () {
void generateServedTest({
required bool serve,
required bool enableAuthCodes,
required bool explicitRun,
required bool withDds,
}) {
test(
'${serve ? 'served by default' : 'not served'} ${enableAuthCodes ? "with" : "without"} '
'auth codes, ${explicitRun ? 'explicit' : 'implicit'} run,${withDds ? ' ' : 'no'} DDS',
() async {
p = project(
mainSrc:
'void main() { print("ready"); int i = 0; while(true) { i++; } }',
);
Process process = await p.start([
if (explicitRun) 'run',
'--enable-vm-service=0',
if (!withDds) '--no-dds',
if (!enableAuthCodes) '--disable-service-auth-codes',
if (serve) '--serve-observatory',
p.relativeFilePath,
]);
final completer = Completer<void>();
late StreamSubscription sub;
late String uri;
sub = process.stdout.transform(utf8.decoder).listen((event) async {
if (event.contains(dartVMServiceRegExp)) {
uri = dartVMServiceRegExp.firstMatch(event)!.group(1)!;
await sub.cancel();
completer.complete();
}
});
// Wait for process to start.
await completer.future;
final client = HttpClient();
Future<String> makeServiceHttpRequest({String method = ''}) async {
var request = await client.getUrl(Uri.parse('$uri$method'));
var response = await request.close();
return await response.transform(utf8.decoder).join();
}
var content = await makeServiceHttpRequest();
const observatoryText = 'Dart VM Observatory';
expect(content.contains(observatoryText), serve);
if (!serve) {
if (withDds) {
expect(content.contains('DevTools'), true);
} else {
expect(
content,
'This VM does not have a registered Dart '
'Development Service (DDS) instance and is not currently serving '
'Dart DevTools.',
);
}
}
// Ensure we can always make VM service requests via HTTP.
content = await makeServiceHttpRequest(method: 'getVM');
expect(content.contains('"jsonrpc":"2.0"'), true);
// If Observatory isn't being served, ensure we can enable it.
if (!serve) {
content = await makeServiceHttpRequest(method: '_serveObservatory');
expect(content.contains('"type":"Success"'), true);
// Ensure Observatory is now being served.
content = await makeServiceHttpRequest();
expect(content.contains(observatoryText), true);
}
process.kill();
},
);
}
const flags = <bool>[true, false];
// TODO(jcollins): Disabling serving no longer seems to produce
// the expected output. Maybe this is because the web interface has
// changed?
for (final serve in [true]) {
for (final enableAuthCodes in flags) {
for (final explicitRun in flags) {
for (final withDds in flags) {
generateServedTest(
serve: serve,
enableAuthCodes: enableAuthCodes,
explicitRun: explicitRun,
withDds: withDds,
);
}
}
}
}
});
}
void residentRun() {
late TestProject serverInfoDirectory, p;
late String serverInfoFile;
setUp(() async {
serverInfoDirectory = project(mainSrc: 'void main() {}');
serverInfoFile = path.join(serverInfoDirectory.dirPath, 'info');
final result = await serverInfoDirectory.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
serverInfoDirectory.relativeFilePath,
]);
expect(result.exitCode, 0);
expect(File(serverInfoFile).existsSync(), true);
expect(
Directory(path.join(
serverInfoDirectory.dirPath,
'.dart_tool',
dartdevKernelCache,
)).listSync(),
isNotEmpty);
// [TestProject] uses [addTearDown] to register cleanup code that will
// delete the project, and due to ordering guarantees of callbacks
// registered using [addTearDown] (refer to [addTearDown]'s doc comment), we
// need to use [addTearDown] here to ensure that the resident frontend
// compiler we started above will be shut down before the project is
// deleted.
addTearDown(() async {
await serverInfoDirectory.run([
'compilation-server',
'shutdown',
'--$residentCompilerInfoFileOption=$serverInfoFile',
]);
});
});
test(
'passing --resident is a prerequisite for passing --resident-compiler-info-file',
() async {
p = project(mainSrc: 'void main() {}');
final result = await p.run([
'run',
'--$residentCompilerInfoFileOption=$serverInfoFile',
p.relativeFilePath,
]);
expect(result.exitCode, 255);
expect(
result.stderr,
contains(
'Error: the --resident flag must be passed whenever the --resident-compiler-info-file option is passed.',
),
);
});
test("'Hello World'", () async {
p = project(mainSrc: "void main() { print('Hello World'); }");
final result = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
p.relativeFilePath,
]);
Directory? kernelCache = p.findDirectory('.dart_tool/kernel');
expect(result.exitCode, 0);
expect(
result.stdout,
allOf(
contains('Hello World'),
isNot(contains(residentFrontendCompilerPrefix)),
),
);
expect(result.stderr, isEmpty);
expect(kernelCache, isNot(null));
});
test("'Hello World' with legacy --resident-server-info-file option",
() async {
p = project(mainSrc: "void main() { print('Hello World'); }");
final result = await p.run([
'run',
'--resident',
'--resident-server-info-file=$serverInfoFile',
p.relativeFilePath,
]);
Directory? kernelCache = p.findDirectory('.dart_tool/kernel');
expect(result.exitCode, 0);
expect(
result.stdout,
allOf(
contains('Hello World'),
isNot(contains(residentFrontendCompilerPrefix)),
),
);
expect(result.stderr, isEmpty);
expect(kernelCache, isNot(null));
});
test('--resident-compiler-info-file handles relative paths correctly',
() async {
p = project(mainSrc: "void main() { print('Hello World'); }");
final result = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption',
path.relative(serverInfoFile, from: p.dirPath),
p.relativeFilePath,
]);
Directory? kernelCache = p.findDirectory('.dart_tool/kernel');
expect(result.exitCode, 0);
expect(
result.stdout,
allOf(
contains('Hello World'),
isNot(contains(residentFrontendCompilerPrefix)),
),
);
expect(result.stderr, isEmpty);
expect(kernelCache, isNotNull);
});
test(
'a running resident compiler is restarted if the Dart SDK was '
'upgraded or downgraded since it was started', () async {
p = project(mainSrc: 'void main() {}');
final residentCompilerInfo = ResidentCompilerInfo.fromFile(
File(serverInfoFile),
);
if (residentCompilerInfo.sdkHash != null &&
residentCompilerInfo.sdkHash == sdkHashNull) {
// dartdev does not consider the SDK hash changing from [sdkHashNull] to a
// different SDK hash to be an SDK upgrade or downgrade. So, if an SDK
// of [sdkHashNull] was recorded into [serverInfoFile], the test logic
// below will not work as intended. For local developement, we make the
// test fail in this situation, alerting the developer that the test is
// only meaningful when run from an SDK built with --verify-sdk-hash. The
// SDK can only be built with --no-verify-sdk-hash on CI, so we just skip
// this test on CI.
if (Platform.environment.containsKey('BUILDBOT_BUILDERNAME') ||
Platform.environment.containsKey('SWARMING_TASK_ID')) {
return;
} else {
fail(
'This test is guaranteed to pass, and thus is not meaningful, when '
'run from an SDK built with --no-verify-sdk-hash',
);
}
}
// Replace the SDK hash in [serverInfoFile] to make dartdev think the Dart
// SDK version has changed since the resident compiler was started.
File(serverInfoFile).writeAsStringSync(
'address:${residentCompilerInfo.address.address} '
'sdkHash:${'1' * residentCompilerInfo.sdkHash!.length} '
'port:${residentCompilerInfo.port} ',
);
final result = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
p.relativeFilePath,
]);
expect(result.exitCode, 0);
expect(result.stdout, contains(residentFrontendCompilerPrefix));
expect(
result.stderr,
'The Dart SDK has been upgraded or downgraded since the Resident '
'Frontend Compiler was started, so the Resident Frontend Compiler will '
'now be restarted for compatibility reasons.\n',
);
expect(File(serverInfoFile).existsSync(), true);
});
test('when a connection to a running resident compiler cannot be established',
() async {
// When this occurs, the user should be informed that the resident frontend
// compiler will be restarted, and compilation will be retried.
p = project(mainSrc: 'void main() {}');
// Create a [testServerInfoFile] that contains an invalid port to guarantee
// that a connection will not be established.
final testServerInfoFile = File(path.join(p.dirPath, 'info'));
testServerInfoFile.createSync();
testServerInfoFile.writeAsStringSync(
'address:127.0.0.1 sdkHash:$sdkHashNull port:-12 ',
);
final result = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=${testServerInfoFile.path}',
p.relativeFilePath,
]);
expect(result.exitCode, 0);
expect(result.stdout, contains(residentFrontendCompilerPrefix));
expect(
result.stderr,
'Error: A connection to the Resident Frontend Compiler could not be '
'established. Restarting the Resident Frontend Compiler and retrying '
'compilation.\n',
);
expect(testServerInfoFile.existsSync(), true);
await p.run([
'compilation-server',
'shutdown',
'--$residentCompilerInfoFileOption=${testServerInfoFile.path}',
]);
});
test('Handles experiments', () async {
p = project(
mainSrc: r"void main() { print(('hello','world').$1); }",
sdkConstraint: VersionConstraint.parse(
'^3.0.0',
),
);
final result = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
'--enable-experiment=test-experiment',
p.relativeFilePath,
]);
Directory? kernelCache = p.findDirectory('.dart_tool/kernel');
expect(result.stderr, isEmpty);
expect(
result.stdout,
allOf(
contains('hello'),
isNot(contains(residentFrontendCompilerPrefix)),
),
);
expect(result.exitCode, 0);
expect(kernelCache, isNot(null));
});
test('same server used from different directories', () async {
p = project(mainSrc: "void main() { print('1'); }");
TestProject p2 = project(mainSrc: "void main() { print('2'); }");
final runResult1 = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
p.relativeFilePath,
]);
final runResult2 = await p2.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
p2.relativeFilePath,
]);
expect(runResult1.exitCode, allOf(0, equals(runResult2.exitCode)));
expect(
runResult1.stdout,
allOf(
contains('1'),
isNot(contains(residentFrontendCompilerPrefix)),
),
);
expect(
runResult2.stdout,
allOf(
contains('2'),
isNot(contains(residentFrontendCompilerPrefix)),
),
);
});
test('kernel cache respects directory structure', () async {
p = project(name: 'foo');
p.file('lib/main.dart', 'void main() {}');
p.file('bin/main.dart', 'void main() {}');
final runResult1 = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
path.join(p.dirPath, 'lib/main.dart'),
]);
expect(runResult1.exitCode, 0);
expect(runResult1.stdout, isEmpty);
expect(runResult1.stderr, isEmpty);
final runResult2 = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
path.join(p.dirPath, 'bin/main.dart'),
]);
expect(runResult2.exitCode, 0);
expect(runResult2.stdout, isEmpty);
expect(runResult2.stderr, isEmpty);
final cache = p.findDirectory('.dart_tool/kernel');
expect(cache, isNot(null));
expect(Directory(path.join(cache!.path, 'lib')).existsSync(), true);
expect(Directory(path.join(cache.path, 'bin')).existsSync(), true);
});
test('standalone dart program', () async {
p = project(mainSrc: 'void main() {}');
p.deleteFile('.dart_tool/package_config.json');
final runResult = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
p.relativeFilePath,
]);
expect(runResult.stderr,
contains('Unable to locate .dart_tool/package_config.json'));
expect(runResult.exitCode, isNot(0));
expect(File(serverInfoFile).existsSync(), true);
});
test('directory that the server is started in is deleted', () async {
// The first command will start the server process in the p2
// project directory.
// This directory is deleted. The second command will attempt to run again
// The server process should not fail on this second attempt. If it does,
// the 3rd command will result in a new server starting.
Directory tempServerInfoDir = Directory.systemTemp.createTempSync('a');
String tempServerInfoFile = path.join(tempServerInfoDir.path, 'info');
addTearDown(() async {
try {
await sendAndReceiveResponse(
residentServerShutdownCommand,
File(tempServerInfoFile),
);
} catch (_) {}
await deleteDirectory(tempServerInfoDir);
});
p = project(mainSrc: 'void main() {}');
TestProject p2 = project(mainSrc: 'void main() {}');
final runResult1 = await p2.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$tempServerInfoFile',
p2.relativeFilePath,
]);
await deleteDirectory(p2.dir);
expect(runResult1.exitCode, 0);
expect(runResult1.stdout, contains(residentFrontendCompilerPrefix));
await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$tempServerInfoFile',
p.relativeFilePath,
]);
final runResult2 = await p.run([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$tempServerInfoFile',
p.relativeFilePath,
]);
expect(runResult2.exitCode, 0);
expect(runResult2.stderr, isEmpty);
expect(runResult2.stdout, isNot(contains(residentFrontendCompilerPrefix)));
});
test('VM flags are passed properly', () async {
p = project(mainSrc: observeScript);
var sawVmServiceMsg = false;
var sawCFEMsg = false;
var sawProgramMsg = false;
// --observe sets the following flags by default:
// --enable-vm-service
// --pause-isolate-on-exit
// --pause-isolate-on-unhandled-exception
// --warn-on-pause-with-no-debugger
//
// This test ensures that allowed arguments for dart run which are valid VM
// arguments are properly handled by the VM.
void onData1(event) {
if (event.contains('The Dart VM service is listening on')) {
final re = RegExp(
r'The Dart VM service is listening on http:\/\/127.0.0.1:[0-9+]');
expect(re.hasMatch(event), true);
sawVmServiceMsg = true;
}
if (event.contains('Observe smoke test!')) {
sawProgramMsg = true;
}
if (event.contains('The Resident Frontend Compiler is listening')) {
final re = RegExp(
r'The Resident Frontend Compiler is listening at 127.0.0.1:[0-9]+');
expect(re.hasMatch(event), true);
sawCFEMsg = true;
}
if (sawVmServiceMsg && sawProgramMsg) {
p.kill();
}
}
await p.runWithVmService([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
'--observe=0',
'--pause-isolates-on-start',
// This should negate the above flag.
'--no-pause-isolates-on-start',
'--no-pause-isolates-on-exit',
'--no-pause-isolates-on-unhandled-exceptions',
'-Dfoo=bar',
'--define=bar=foo',
p.relativeFilePath,
], onData1);
expect(sawVmServiceMsg, true);
expect(sawProgramMsg, true);
expect(sawCFEMsg, false);
// Again, with --disable-service-auth-codes.
sawVmServiceMsg = false;
sawProgramMsg = false;
sawCFEMsg = false;
await p.runWithVmService([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
'--observe=0',
'--pause-isolates-on-start',
// This should negate the above flag.
'--no-pause-isolates-on-start',
'--no-pause-isolates-on-exit',
'--no-pause-isolates-on-unhandled-exceptions',
'--disable-service-auth-codes',
'-Dfoo=bar',
'--define=bar=foo',
p.relativeFilePath,
], onData1);
expect(sawVmServiceMsg, true);
expect(sawProgramMsg, true);
expect(sawCFEMsg, false);
// Again, with IPv6.
sawVmServiceMsg = false;
sawProgramMsg = false;
sawCFEMsg = false;
void onData2(event) {
if (event.contains('The Dart VM service is listening on')) {
final re = RegExp(
r'The Dart VM service is listening on http:\/\/\[::1\]:\d+\/[a-zA-Z0-9_-]+=\/.*');
expect(re.hasMatch(event), true);
sawVmServiceMsg = true;
}
if (event.contains('Observe smoke test!')) {
sawProgramMsg = true;
}
if (event.contains('The Resident Frontend Compiler is listening')) {
final re = RegExp(
r'The Resident Frontend Compiler is listening at 127.0.0.1:[0-9]+');
expect(re.hasMatch(event), true);
sawCFEMsg = true;
}
if (sawVmServiceMsg && sawProgramMsg) {
p.kill();
}
}
await p.runWithVmService([
'run',
'--resident',
'--$residentCompilerInfoFileOption=$serverInfoFile',
'--observe=0/::1',
'--pause-isolates-on-start',
// This should negate the above flag.
'--no-pause-isolates-on-start',
'--no-pause-isolates-on-exit',
'--no-pause-isolates-on-unhandled-exceptions',
'-Dfoo=bar',
'--define=bar=foo',
p.relativeFilePath,
], onData2);
expect(sawVmServiceMsg, true);
expect(sawProgramMsg, true);
expect(sawCFEMsg, false);
});
test('custom package_config path', () async {
p = project(name: 'foo', mainSrc: '''
import 'package:bar/main.dart';
void main() {
cmd();
}
''');
final bar1 = project(name: 'bar1', mainSrc: '''
cmd() {
print('hi');
}
''');
final bar2 = project(name: 'bar2', mainSrc: '''
cmd() {
print('bye');
}
''');
p.file('custom_packages1.json', '''
{
"configVersion": 2,
"packages": [
{
"name": "bar",
"rootUri": "${Uri.file(bar1.dirPath)}",
"packageUri": "${Uri.file(path.join(bar1.dirPath, 'lib'))}"
}
]
}
''');
p.file('custom_packages2.json', '''
{
"configVersion": 2,
"packages": [
{
"name": "bar",
"rootUri": "${Uri.file(bar2.dirPath)}",
"packageUri": "${Uri.file(path.join(bar2.dirPath, 'lib'))}"
}
]
}
''');
final runResult1 = await p.run([
'run',
'--packages=${path.join(p.dirPath, 'custom_packages1.json')}',
p.relativeFilePath,
]);
expect(runResult1.stderr, isEmpty);
expect(runResult1.stdout, contains('hi'));
expect(runResult1.exitCode, 0);
// Test that --packages can precede the command name
final runResult2 = await p.run([
'--packages=${path.join(p.dirPath, 'custom_packages2.json')}',
'run',
p.relativeFilePath,
]);
expect(runResult2.stderr, isEmpty);
expect(runResult2.stdout, contains('bye'));
expect(runResult2.exitCode, 0);
});
}