blob: 8c4439233b0b9ef50f07b9874014a9e405eac9c1 [file] [log] [blame]
// Copyright (c) 2022, 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.md file.
// @dart = 2.9
import 'dart:convert';
import 'dart:io';
import 'package:frontend_server/starter.dart';
import 'package:frontend_server/src/resident_frontend_server.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
void main() async {
// Files are considered to be modified if the modification timestamp is
// during the same second of the last compile time due to the
// granularity of file stat on windows.
// Waiting for this number of milliseconds guarantees that the files in
// the unit tests will not be counted as modified.
const STAT_GRANULARITY = 1100;
group('Resident Frontend Server: invalid input: ', () {
test('no command given', () async {
final jsonResponse = await ResidentFrontendServer.handleRequest(
jsonEncode(<String, Object>{"no": "command"}));
expect(
jsonResponse,
equals(jsonEncode(<String, Object>{
"success": false,
"errorMessage": "Unsupported command: null."
})));
});
test('invalid command', () async {
final jsonResponse = await ResidentFrontendServer.handleRequest(
jsonEncode(<String, Object>{"command": "not a command"}));
expect(
jsonResponse,
equals(jsonEncode(<String, Object>{
"success": false,
"errorMessage": "Unsupported command: not a command."
})));
});
test('not a JSON request', () async {
final jsonResponse = await ResidentFrontendServer.handleRequest("hello");
expect(
jsonResponse,
equals(jsonEncode(<String, Object>{
"success": false,
"errorMessage": "hello is not valid JSON."
})));
});
test('missing files for compile command', () async {
final jsonResponse = await ResidentFrontendServer.handleRequest(
jsonEncode(<String, Object>{"command": "compile"}));
expect(
jsonResponse,
equals(jsonEncode(<String, Object>{
"success": false,
"errorMessage":
"compilation requests must include an executable and an output-dill path."
})));
});
});
group('Resident Frontend Server: compile tests: ', () {
Directory d;
File executable, package, cachedDill;
setUp(() async {
d = Directory.systemTemp.createTempSync();
executable = File(path.join(d.path, 'src1.dart'))
..createSync()
..writeAsStringSync('void main() {print("hello " "there");}');
package = File(path.join(d.path, '.dart_tool', 'package_config.json'))
..createSync(recursive: true)
..writeAsStringSync('''
{
"configVersion": 2,
"packages": [
{
"name": "hello",
"rootUri": "../",
"packageUri": "./"
}
]
}
''');
cachedDill = File(path.join(d.path, 'src1.dart.dill'));
});
tearDown(() async {
d.delete(recursive: true);
ResidentFrontendServer.compilers.clear();
});
test('initial compile, basic', () async {
final compileResult = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path)));
expect(compileResult['success'], true);
expect(compileResult['errorCount'], 0);
expect(compileResult['output-dill'], equals(cachedDill.path));
});
test('compile options', () async {
executable.writeAsStringSync('void main() { int x = 1; }');
final compileResult1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path,
supportMirrors: true,
enableAsserts: true,
soundNullSafety: true,
verbosity: 'all',
define: <String>['-Dvar=2'],
enableExperiement: <String>['experiemental-flag=vm_name'],
)));
expect(compileResult1['success'], true);
expect(compileResult1['errorCount'], 0);
});
test('produces aot kernel', () async {
final compileResult1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path,
soundNullSafety: true,
verbosity: 'all',
aot: true,
tfa: true,
rta: true,
treeShakeWriteOnlyFields: true,
protobufTreeShakerV2: true,
)));
expect(compileResult1['success'], true);
expect(compileResult1['errorCount'], 0);
});
test('no package_config.json provided', () async {
final compileResult = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path, outputDill: cachedDill.path)));
expect(compileResult['success'], true);
expect(compileResult['errorCount'], 0);
expect(compileResult['output-dill'], equals(cachedDill.path));
});
test('incremental compilation', () async {
await Future.delayed(Duration(milliseconds: STAT_GRANULARITY));
final compileResults1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path,
)));
executable.writeAsStringSync(
executable.readAsStringSync().replaceFirst('there', 'world'));
final compileResults2 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path,
)));
expect(compileResults1['success'], true);
expect(compileResults1['errorCount'],
allOf(0, equals(compileResults2['errorCount'])));
expect(compileResults2['output-dill'],
equals(compileResults1['output-dill']));
expect(compileResults2['incremental'], true);
expect(
ResidentFrontendServer
.compilers[executable.path].trackedSources.first,
equals(Uri.file(executable.path)));
expect(
ResidentFrontendServer
.compilers[executable.path].trackedSources.length,
1);
});
test(
'compiling twice with no modifications returns cached kernel without invoking compiler',
() async {
await Future.delayed(Duration(milliseconds: STAT_GRANULARITY));
final compileResults1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path)));
final compileResults2 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path)));
expect(compileResults1['errorCount'],
allOf(0, equals(compileResults2['errorCount'])));
expect(compileResults1['output-dill'],
equals(compileResults2['output-dill']));
expect(compileResults2['returnedStoredKernel'], true);
expect(ResidentFrontendServer.compilers.length, 1);
});
test('switch entrypoints gracefully', () async {
final executable2 = File(path.join(d.path, 'src2.dart'))
..writeAsStringSync('void main() {}');
final entryPointDill = File(path.join(d.path, 'src2.dart.dill'));
final compileResults1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path)));
final compileResults2 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable2.path,
packages: package.path,
outputDill: entryPointDill.path)));
expect(compileResults1['success'],
allOf(true, equals(compileResults2['success'])));
expect(
ResidentFrontendServer
.compilers[executable.path].trackedSources.length,
1);
expect(
ResidentFrontendServer
.compilers[executable.path].trackedSources.first,
equals(Uri.file(executable.path)));
expect(
ResidentFrontendServer
.compilers[executable2.path].trackedSources.length,
1);
expect(
ResidentFrontendServer
.compilers[executable2.path].trackedSources.first,
equals(Uri.file(executable2.path)));
expect(ResidentFrontendServer.compilers.length, 2);
});
test('Cached kernel is removed between compilation requests', () async {
await Future.delayed(Duration(milliseconds: STAT_GRANULARITY));
final compileResults1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path)));
executable.writeAsStringSync(
executable.readAsStringSync().replaceFirst('there', 'world'));
cachedDill.deleteSync();
expect(cachedDill.existsSync(), false);
final compileResults2 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path)));
expect(compileResults1['success'], true);
expect(compileResults1['errorCount'],
allOf(equals(compileResults2['errorCount']), 0));
expect(compileResults2['returnedStoredKernel'], null);
expect(compileResults2['incremental'], true);
expect(cachedDill.existsSync(), true);
expect(ResidentFrontendServer.compilers.length, 1);
});
test('maintains tracked sources', () async {
await Future.delayed(Duration(milliseconds: STAT_GRANULARITY));
final executable2 = File(path.join(d.path, 'src2.dart'))
..createSync()
..writeAsStringSync('''
import 'src3.dart';
void main() {}''');
final executable3 = File(path.join(d.path, 'src3.dart'))
..createSync()
..writeAsStringSync('''
void fn() {}''');
// adding or removing package_config.json while maintaining the same entrypoint
// should not alter tracked sources
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path, outputDill: cachedDill.path));
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path));
final compileResult1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path, outputDill: cachedDill.path)));
expect(compileResult1['success'], true);
expect(compileResult1['returnedStoredKernel'], null);
expect(compileResult1['incremental'], null);
expect(
ResidentFrontendServer
.compilers[executable.path].trackedSources.length,
1);
expect(
ResidentFrontendServer
.compilers[executable.path].trackedSources.first,
equals(Uri.file(executable.path)));
// switching entrypoints, packages, and modifying packages
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable2.path, outputDill: cachedDill.path));
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable2.path,
packages: package.path,
outputDill: cachedDill.path));
package.writeAsStringSync(package.readAsStringSync());
// Forces package to be behind the next computed kernel by 1 second
// so that the final compilation will be incremental
await Future.delayed(Duration(milliseconds: STAT_GRANULARITY));
final compileResult2 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable2.path,
packages: package.path,
outputDill: cachedDill.path)));
expect(compileResult2['success'], true);
expect(compileResult2['incremental'], null);
expect(compileResult2['returnedStoredKernel'], null);
expect(
ResidentFrontendServer
.compilers[executable2.path].trackedSources.length,
greaterThanOrEqualTo(2));
expect(
ResidentFrontendServer.compilers[executable2.path].trackedSources,
containsAll(
<Uri>{Uri.file(executable2.path), Uri.file(executable3.path)}));
// remove a source
executable2.writeAsStringSync('void main() {}');
final compileResult3 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable2.path,
packages: package.path,
outputDill: cachedDill.path)));
expect(compileResult3['success'], true);
expect(compileResult3['incremental'], true);
expect(
ResidentFrontendServer
.compilers[executable2.path].trackedSources.length,
greaterThanOrEqualTo(1));
expect(ResidentFrontendServer.compilers[executable2.path].trackedSources,
containsAll(<Uri>{Uri.file(executable2.path)}));
});
test('continues to work after compiler error is produced', () async {
final originalContent = executable.readAsStringSync();
final newContent = originalContent.replaceAll(';', '@');
await Future.delayed(Duration(milliseconds: STAT_GRANULARITY));
executable.writeAsStringSync(newContent);
final compileResults1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path)));
executable.writeAsStringSync(originalContent);
final compileResults2 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path)));
expect(compileResults1['success'], false);
expect(compileResults1['errorCount'], greaterThan(1));
expect(compileResults2['success'], true);
expect(compileResults2['errorCount'], 0);
expect(compileResults2['incremental'], true);
expect(
ResidentFrontendServer
.compilers[executable.path].trackedSources.length,
1);
expect(
ResidentFrontendServer
.compilers[executable.path].trackedSources.first,
equals(Uri.file(executable.path)));
});
test('using cached kernel maintains error messages', () async {
final originalContent = executable.readAsStringSync();
executable.writeAsStringSync(originalContent.replaceFirst(';', ''));
await Future.delayed(Duration(milliseconds: STAT_GRANULARITY));
final compileResults1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path,
)));
final compileResults2 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path,
)));
executable.writeAsStringSync(originalContent);
final compileResults3 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path,
)));
expect(compileResults2['returnedStoredKernel'], true);
expect(compileResults1['errorCount'],
allOf(1, equals(compileResults2['errorCount'])));
expect(
compileResults2['compilerOutputLines'] as List<dynamic>,
containsAllInOrder(
compileResults1['compilerOutputLines'] as List<dynamic>));
expect(compileResults3['errorCount'], 0);
expect(compileResults3['incremental'], true);
});
test('enforces compiler limit', () async {
final executable2 = File(path.join(d.path, 'src2.dart'))
..createSync()
..writeAsStringSync('''
import 'src3.dart';
void main() {}''');
final executable3 = File(path.join(d.path, 'src3.dart'))
..createSync()
..writeAsStringSync('''
void main() {}''');
final executable4 = File(path.join(d.path, 'src4.dart'))
..createSync()
..writeAsStringSync('''
void main() {}''');
final compileResults1 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable.path,
packages: package.path,
outputDill: cachedDill.path)));
final compileResults2 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable2.path,
packages: package.path,
outputDill: cachedDill.path)));
final compileResults3 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable3.path,
packages: package.path,
outputDill: cachedDill.path)));
final compileResults4 = jsonDecode(
await ResidentFrontendServer.handleRequest(
ResidentFrontendServer.createCompileJSON(
executable: executable4.path,
packages: package.path,
outputDill: cachedDill.path)));
expect(
compileResults1['success'],
allOf(
true,
equals(compileResults2['success']),
equals(compileResults3['success']),
equals(compileResults4['success'])));
expect(ResidentFrontendServer.compilers.length, 3);
expect(
ResidentFrontendServer.compilers.containsKey(executable4.path), true);
});
});
group('Resident Frontend Server: socket tests: ', () {
Directory d;
File serverInfo;
setUp(() {
d = Directory.systemTemp.createTempSync();
serverInfo = File(path.join(d.path, 'info.txt'));
});
tearDown(() {
d.deleteSync(recursive: true);
});
test('ServerSocket fails to bind', () async {
final result = await residentListenAndCompile(
InternetAddress.loopbackIPv4, -1, serverInfo);
expect(serverInfo.existsSync(), false);
expect(result, null);
});
test('socket passes messages properly and shutsdown properly', () async {
await residentListenAndCompile(
InternetAddress.loopbackIPv4, 0, serverInfo);
expect(serverInfo.existsSync(), true);
final info = serverInfo.readAsStringSync();
final address = InternetAddress(
info.substring(info.indexOf(':') + 1, info.indexOf(' ')));
final port = int.parse(info.substring(info.lastIndexOf(':') + 1));
final shutdownResult = await sendAndReceiveResponse(
address, port, ResidentFrontendServer.shutdownCommand);
expect(shutdownResult, equals(<String, dynamic>{"shutdown": true}));
expect(serverInfo.existsSync(), false);
});
test('timed shutdown', () async {
await residentListenAndCompile(
InternetAddress.loopbackIPv4, 0, serverInfo,
inactivityTimeout: Duration(milliseconds: 100));
expect(serverInfo.existsSync(), true);
final info = serverInfo.readAsStringSync();
final address = InternetAddress(
info.substring(info.indexOf(':') + 1, info.indexOf(' ')));
final port = int.parse(info.substring(info.lastIndexOf(':') + 1));
await Future.delayed(Duration(milliseconds: 150));
expect(serverInfo.existsSync(), false);
final shutdownResult = await sendAndReceiveResponse(
address, port, ResidentFrontendServer.shutdownCommand);
expect(shutdownResult['errorMessage'], contains('SocketException'));
});
test('concurrent startup requests', () async {
final serverSubscription = await residentListenAndCompile(
InternetAddress.loopbackIPv4,
0,
serverInfo,
);
final startWhileAlreadyRunning = await residentListenAndCompile(
InternetAddress.loopbackIPv4,
0,
serverInfo,
);
expect(serverSubscription, isNot(null));
expect(startWhileAlreadyRunning, null);
expect(serverInfo.existsSync(), true);
expect(
Link('${serverInfo.path}$RESIDENT_SERVER_LINK_POSTFIX').existsSync(),
true);
final info = serverInfo.readAsStringSync();
final address = InternetAddress(
info.substring(info.indexOf(':') + 1, info.indexOf(' ')));
final port = int.parse(info.substring(info.lastIndexOf(':') + 1));
final shutdownResult = await sendAndReceiveResponse(
address, port, ResidentFrontendServer.shutdownCommand);
expect(shutdownResult, equals(<String, dynamic>{"shutdown": true}));
expect(serverInfo.existsSync(), false);
expect(
Link('${serverInfo.path}$RESIDENT_SERVER_LINK_POSTFIX').existsSync(),
false);
});
test('resident server starter', () async {
final returnValue =
starter(['--resident-info-file-name=${serverInfo.path}']);
expect(await returnValue, 0);
expect(serverInfo.existsSync(), true);
final info = serverInfo.readAsStringSync();
final address = InternetAddress(
info.substring(info.indexOf(':') + 1, info.indexOf(' ')));
final port = int.parse(info.substring(info.lastIndexOf(':') + 1));
var result = await sendAndReceiveResponse(
address, port, ResidentFrontendServer.shutdownCommand);
expect(result, equals(<String, dynamic>{"shutdown": true}));
expect(serverInfo.existsSync(), false);
result = await sendAndReceiveResponse(
address, port, ResidentFrontendServer.shutdownCommand);
expect(result['errorMessage'], contains('SocketException'));
});
});
}