blob: d202035830b44b92f764ec7cd59f83f5de66d720 [file]
// Copyright 2020 The Dart Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:frontend_server_client/frontend_server_client.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
void main() async {
FrontendServerClient? client;
late PackageConfig packageConfig;
late String packageRoot;
late String packagesJsonPath;
setUp(() async {
await d.dir('a', [
d.file('pubspec.yaml', '''
name: a
dependencies:
path: ^1.0.0
environment:
sdk: ^3.10.0-0.0.dev
'''),
d.dir('bin', [
d.file('main.dart', '''
import 'package:path/path.dart' as p;
void main() async {
print(message);
/// Runs in a loop until it is hot reloaded with a new message.
while (!message.contains('goodbye')) {
await Future.delayed(const Duration(seconds: 1));
}
print(message);
}
String get message => p.join('hello', 'world');
'''),
]),
]).create();
packageRoot = p.join(d.sandbox, 'a');
await Process.run(Platform.resolvedExecutable, [
'pub',
'get',
], workingDirectory: packageRoot);
packageConfig = (await findPackageConfig(Directory(packageRoot)))!;
packagesJsonPath =
findNearestPackageConfigPath(Directory(packageRoot)) ??
p.join(packageRoot, '.dart_tool', 'package_config.json');
});
tearDown(() async {
await client?.shutdown();
client = null;
});
test('can compile, recompile, and hot reload a vm app', () async {
final entrypoint = p.join(packageRoot, 'bin', 'main.dart');
client = await FrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out.dill'),
vmPlatformDill,
packagesJson: packagesJsonPath,
);
var result = await client!.compile();
client!.accept();
expect(result.compilerOutputLines, isEmpty);
expect(result.errorCount, 0);
expect(
result.newSources,
containsAll([
File(entrypoint).uri,
packageConfig.resolve(Uri.parse('package:path/path.dart')),
]),
);
expect(result.removedSources, isEmpty);
expect(result.dillOutput, isNotNull);
expect(File(result.dillOutput!).existsSync(), true);
final process = await Process.start(Platform.resolvedExecutable, [
'--observe',
'--no-pause-isolates-on-exit',
'--pause-isolates-on-start',
result.dillOutput!,
]);
addTearDown(process.kill);
final stdoutLines = StreamQueue(
process.stdout.transform(utf8.decoder).transform(const LineSplitter()),
);
addTearDown(stdoutLines.cancel);
final observatoryLine = await stdoutLines.next;
final observatoryUri =
'${observatoryLine.split(' ').last.replaceFirst('http', 'ws')}ws';
final vmService = await vmServiceConnectUri(observatoryUri);
addTearDown(vmService.dispose);
final isolate = await waitForIsolatesAndResume(vmService);
await expectLater(stdoutLines, emitsThrough(p.join('hello', 'world')));
final appFile = File(entrypoint);
final originalContent = await appFile.readAsString();
final newContent = originalContent.replaceFirst('hello', 'goodbye');
await appFile.writeAsString(newContent);
result = await client!.compile([File(entrypoint).uri]);
client!.accept();
expect(result.newSources, isEmpty);
expect(result.removedSources, isEmpty);
expect(result.compilerOutputLines, isEmpty);
expect(result.errorCount, 0);
expect(result.dillOutput, endsWith('.incremental.dill'));
await vmService.reloadSources(isolate.id!, rootLibUri: result.dillOutput);
expect(await stdoutLines.next, p.join('goodbye', 'world'));
expect(await process.exitCode, 0);
});
test('can handle compile errors and reload fixes', () async {
final entrypoint = p.join(packageRoot, 'bin', 'main.dart');
final entrypointFile = File(entrypoint);
final originalContent = await entrypointFile.readAsString();
// append two compile errors to the bottom
await entrypointFile.writeAsString(
'$originalContent\nint foo = 1.0;\nString bar = 4;',
);
client = await FrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out.dill'),
vmPlatformDill,
packagesJson: packagesJsonPath,
);
var result = await client!.compile();
client!.accept();
expect(result.errorCount, 2);
expect(
result.compilerOutputLines,
allOf(contains('int foo = 1.0;'), contains('String bar = 4;')),
);
expect(
result.newSources,
containsAll([
File(entrypoint).uri,
packageConfig.resolve(Uri.parse('package:path/path.dart')),
]),
);
expect(result.removedSources, isEmpty);
expect(result.dillOutput, isNotNull);
expect(File(result.dillOutput!).existsSync(), true);
final process = await Process.start(Platform.resolvedExecutable, [
'--observe',
'--no-pause-isolates-on-exit',
'--pause-isolates-on-start',
result.dillOutput!,
]);
addTearDown(process.kill);
final stdoutLines = StreamQueue(
process.stdout.transform(utf8.decoder).transform(const LineSplitter()),
);
addTearDown(stdoutLines.cancel);
final observatoryLine = await stdoutLines.next;
final observatoryUri =
'${observatoryLine.split(' ').last.replaceFirst('http', 'ws')}ws';
final vmService = await vmServiceConnectUri(observatoryUri);
addTearDown(vmService.dispose);
final isolate = await waitForIsolatesAndResume(vmService);
// The program actually runs regardless of the errors, as they don't affect
// the runtime behavior.
await expectLater(stdoutLines, emitsThrough(p.join('hello', 'world')));
await entrypointFile.writeAsString(
originalContent.replaceFirst('hello', 'goodbye'),
);
result = await client!.compile([entrypointFile.uri]);
client!.accept();
expect(result.errorCount, 0);
expect(result.compilerOutputLines, isEmpty);
expect(result.newSources, isEmpty);
expect(result.removedSources, isEmpty);
expect(result.dillOutput, isNotNull);
expect(File(result.dillOutput!).existsSync(), true);
await vmService.reloadSources(isolate.id!, rootLibUri: result.dillOutput);
expect(await stdoutLines.next, p.join('goodbye', 'world'));
expect(await process.exitCode, 0);
});
test('can compile and recompile a dartdevc app', () async {
final entrypoint = p
.toUri(p.join(packageRoot, 'bin', 'main.dart'))
.toString();
final dartDevcClient = client = await DartDevcFrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out.dill'),
platformKernel: p
.toUri(p.join(sdkDir, 'lib', '_internal', 'ddc_platform.dill'))
.toString(),
packagesJson: packagesJsonPath,
);
var result = await client!.compile();
client!.accept();
expect(result.compilerOutputLines, isEmpty);
expect(result.errorCount, 0);
expect(
result.newSources,
containsAll([
Uri.parse(entrypoint),
packageConfig.resolve(Uri.parse('package:path/path.dart')),
]),
);
expect(result.removedSources, isEmpty);
expect(result.dillOutput, isNotNull);
expect(File(result.jsManifestOutput!).existsSync(), true);
expect(File(result.jsSourcesOutput!).existsSync(), true);
expect(File(result.jsSourceMapsOutput!).existsSync(), true);
final entrypointUri = Uri.parse(entrypoint);
expect(
utf8.decode(dartDevcClient.assetBytes('${entrypointUri.path}.lib.js')!),
contains('hello'),
);
final appFile = File(entrypointUri.toFilePath());
final originalContent = await appFile.readAsString();
final newContent = originalContent.replaceFirst('hello', 'goodbye');
await appFile.writeAsString(newContent);
result = await client!.compile([entrypointUri]);
client!.accept();
expect(result.newSources, isEmpty);
expect(result.removedSources, isEmpty);
expect(result.compilerOutputLines, isEmpty);
expect(result.errorCount, 0);
expect(result.jsManifestOutput, endsWith('.incremental.dill.json'));
expect(
utf8.decode(dartDevcClient.assetBytes('${entrypointUri.path}.lib.js')!),
contains('goodbye'),
);
});
group('DDC library bundle format', () {
late Uri entrypointUri;
late String entrypoint;
late String mainModule;
late DartDevcFrontendServerClient ddcClient;
Future<void> startAndCompile() async {
entrypointUri = p.toUri(p.join(packageRoot, 'bin', 'main.dart'));
entrypoint = entrypointUri.toString();
mainModule = '${entrypointUri.path}.lib.js';
client = ddcClient = await DartDevcFrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out.dill'),
dartdevcModuleFormat: 'ddc',
canaryFeatures: true,
packagesJson: packagesJsonPath,
);
final result = await ddcClient.compile();
ddcClient.accept();
expect(
result.errorCount,
0,
reason: result.compilerOutputLines.join('\n'),
);
}
Future<void> editMain(String from, String to) async {
final file = File(entrypointUri.toFilePath());
await file.writeAsString(
(await file.readAsString()).replaceFirst(from, to),
);
}
// Compiles an expression at: `print(message);`
Future<CompileResult> compileExpression(
String expression, {
String? libraryUri,
}) => ddcClient.compileExpressionToJs(
expression: expression,
libraryUri: libraryUri ?? entrypoint,
scriptUri: libraryUri ?? entrypoint,
line: 4,
column: 3,
jsFrameValues: {},
jsModules: {},
moduleName: '',
);
test('provides debugger metadata and reloaded sources', () async {
await startAndCompile();
expect(utf8.decode(ddcClient.assetBytes(mainModule)!), contains('hello'));
expect(ddcClient.assetBytes('$mainModule.map'), isNotNull);
final metadata =
jsonDecode(utf8.decode(ddcClient.assetBytes('$mainModule.metadata')!))
as Map<String, dynamic>;
final mergedMetadata = const LineSplitter()
.convert(ddcClient.mergedMetadata)
.map(jsonDecode)
.toList();
expect(mergedMetadata, hasLength(greaterThan(1)));
expect(mergedMetadata, contains(equals(metadata)));
await editMain('hello', 'goodbye');
final result = await ddcClient.compile([entrypointUri]);
ddcClient.accept();
expect(result.errorCount, 0);
expect(
utf8.decode(ddcClient.assetBytes(mainModule)!),
contains('goodbye'),
);
expect(ddcClient.reloadedSources, [
{
'src': mainModule,
'module': metadata['name'],
'libraries': [entrypoint],
},
]);
});
test('can hot restart after a rejected hot reload', () async {
await d.dir('a', [
d.dir('bin', [
d.file('main.dart', '''
class A {
const A();
}
void main() {
print(const A());
}
'''),
]),
]).create();
await startAndCompile();
await editMain('const A();', 'A();');
await editMain('const A()', 'A()');
var result = await ddcClient.compile([entrypointUri]);
expect(result.errorCount, greaterThan(0));
expect(
result.compilerOutputLines,
contains(contains('Const class cannot become non-const')),
);
await ddcClient.reject();
// Rejected URIs are resent.
result = await ddcClient.recompileRestart([]);
ddcClient.accept();
expect(
result.errorCount,
0,
reason: result.compilerOutputLines.join('\n'),
);
expect(ddcClient.reloadedSources.map((entry) => entry['src']), [
mainModule,
]);
});
test('can compile expressions to JS', () async {
await startAndCompile();
var result = await compileExpression('message.length');
expect(
result.errorCount,
0,
reason: result.compilerOutputLines.join('\n'),
);
expect(result.compiledExpression, contains('message'));
result = await compileExpression('notDefined');
expect(result.errorCount, greaterThan(0));
expect(result.compiledExpression, isNull);
expect(result.compilerOutputLines.join('\n'), contains('notDefined'));
// Fails before compiling.
result = await compileExpression(
'1',
libraryUri: 'package:a/missing.dart',
);
expect(result.errorCount, 1);
expect(result.compiledExpression, isNull);
expect(result.compilerOutputLines.join('\n'), isNotEmpty);
});
test(
'queues expression compile requests until a compile is accepted',
() async {
await startAndCompile();
await editMain('hello', 'goodbye');
final compile = ddcClient.compile([entrypointUri]);
var accepted = false;
final expressions =
Future.wait([
compileExpression('message'),
compileExpression('message.length'),
]).then((results) {
expect(accepted, isTrue);
return results;
});
final result = await compile;
expect(result.errorCount, 0);
accepted = true;
ddcClient.accept();
for (final result in await expressions) {
expect(
result.errorCount,
0,
reason: result.compilerOutputLines.join('\n'),
);
expect(result.compiledExpression, isNotEmpty);
}
},
);
});
test('can enable experiments', () async {
await d.dir('a', [
d.dir('bin', [
d.file('nnbd.dart', '''
// Compile time error if nnbd is enabled
int x;
void main() {
print(x);
}
'''),
]),
]).create();
final entrypoint = p.join(packageRoot, 'bin', 'nnbd.dart');
client = await FrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out.dill'),
vmPlatformDill,
enabledExperiments: ['non-nullable'],
packagesJson: packagesJsonPath,
);
final result = await client!.compile();
client!.accept();
expect(result.errorCount, 1);
expect(result.compilerOutputLines, contains(contains('int x;')));
});
test('can compile and recompile filenames with spaces', () async {
await d.dir('a', [
d.dir('bin', [
d.file('main with spaces.dart', '''
void main() {
print('hello world');
}
'''),
]),
]).create();
final entrypoint = p.join(packageRoot, 'bin', 'main with spaces.dart');
client = await FrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out with spaces.dill'),
vmPlatformDill,
packagesJson: packagesJsonPath,
);
var result = await client!.compile();
client!.accept();
expect(result.compilerOutputLines, isEmpty);
expect(result.errorCount, 0);
expect(result.newSources, containsAll([File(entrypoint).uri]));
expect(result.removedSources, isEmpty);
expect(result.dillOutput, isNotNull);
expect(File(result.dillOutput!).existsSync(), true);
var processResult = await Process.run(Platform.resolvedExecutable, [
result.dillOutput!,
]);
expect(processResult.stdout, startsWith('hello world'));
expect(processResult.exitCode, 0);
final appFile = File(entrypoint);
final originalContent = await appFile.readAsString();
final newContent = originalContent.replaceFirst('hello', 'goodbye');
await appFile.writeAsString(newContent);
result = await client!.compile([appFile.uri]);
expect(result.compilerOutputLines, isEmpty);
expect(result.errorCount, 0);
expect(result.newSources, isEmpty);
expect(result.removedSources, isEmpty);
processResult = await Process.run(Platform.resolvedExecutable, [
result.dillOutput!,
]);
expect(processResult.stdout, startsWith('goodbye world'));
expect(processResult.exitCode, 0);
});
test('can support custom librariesSpec', () async {
final defaultLibrariesJson = File(p.join(sdkDir, 'lib', 'libraries.json'));
final libraries =
jsonDecode(defaultLibrariesJson.readAsStringSync())
as Map<String, Object?>;
// Create the custom library file
final customLibFile = File(p.join(packageRoot, 'bin', 'custom_lib.dart'));
await customLibFile.writeAsString('void hello() => print("custom!");');
if (libraries['vm_common'] case {
'libraries': final Map<String, Object?> libs,
}) {
libs['custom_lib'] = {'uri': customLibFile.absolute.uri.toString()};
}
final customLibrariesJsonFile = File(p.join(packageRoot, 'libraries.json'));
await customLibrariesJsonFile.writeAsString(jsonEncode(libraries));
final entrypoint = p.join(packageRoot, 'bin', 'main_custom.dart');
await File(entrypoint).writeAsString('''
import 'dart:custom_lib';
void main() {
hello();
}
''');
client = await FrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out_custom.dill'),
vmPlatformDill,
packagesJson: packagesJsonPath,
librariesSpec: customLibrariesJsonFile.uri.toString(),
);
var result = await client!.compile();
client!.accept();
expect(result.errorCount, 0);
expect(result.dillOutput, isNotNull);
await client!.shutdown();
// Without librariesSpec, compiling the same entrypoint should fail
client = await FrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out_custom_fail.dill'),
vmPlatformDill,
packagesJson: packagesJsonPath,
);
result = await client!.compile();
client!.accept();
expect(result.errorCount, greaterThan(0));
});
test('can compile when specifying an explicit sdkRoot', () async {
final entrypoint = p.join(packageRoot, 'bin', 'simple.dart');
await File(entrypoint).writeAsString('void main() => print("ok");');
client = await FrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out.dill'),
vmPlatformDill,
packagesJson: packagesJsonPath,
sdkRoot: sdkDir,
);
final result = await client!.compile();
client!.accept();
expect(result.errorCount, 0);
expect(result.dillOutput, isNotNull);
final dartExe = p.join(
sdkDir,
'bin',
Platform.isWindows ? 'dart.exe' : 'dart',
);
final processResult = await Process.run(dartExe, [result.dillOutput!]);
expect(processResult.stdout, startsWith('ok'));
expect(processResult.exitCode, 0);
});
test('throws ArgumentError if frontendServerPath does not exist', () async {
final entrypoint = p.join(packageRoot, 'bin', 'simple.dart');
await File(entrypoint).writeAsString('void main() => print("ok");');
expect(
() => FrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out.dill'),
vmPlatformDill,
packagesJson: packagesJsonPath,
frontendServerPath: p.join(packageRoot, 'non_existent_snapshot'),
),
throwsA(isA<ArgumentError>()),
);
});
test(
'throws ArgumentError if AOT snapshot does not exist in sdkRoot',
() async {
final entrypoint = p.join(packageRoot, 'bin', 'simple.dart');
await File(entrypoint).writeAsString('void main() => print("ok");');
expect(
() => FrontendServerClient.start(
entrypoint,
p.join(packageRoot, 'out.dill'),
vmPlatformDill,
packagesJson: packagesJsonPath,
sdkRoot: p.join(packageRoot, 'non_existent_sdk'),
),
throwsA(isA<ArgumentError>()),
);
},
);
}
Future<Isolate> waitForIsolatesAndResume(VmService vmService) async {
var vm = await vmService.getVM();
var isolates = vm.isolates;
while (isolates == null || isolates.isEmpty) {
await Future<void>.delayed(const Duration(milliseconds: 100));
vm = await vmService.getVM();
isolates = vm.isolates;
}
final isolateRef = isolates.first;
var isolate = await vmService.getIsolate(isolateRef.id!);
while (isolate.pauseEvent?.kind != EventKind.kPauseStart) {
await Future<void>.delayed(const Duration(milliseconds: 100));
isolate = await vmService.getIsolate(isolateRef.id!);
}
await vmService.resume(isolate.id!);
return isolate;
}
final vmPlatformDill = p
.toUri(p.join(sdkDir, 'lib', '_internal', 'vm_platform_strong.dill'))
.toString();
final sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable));