| // 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')); |
| }); |
| }); |
| } |