| // 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 file. | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'package:frontend_server/src/resident_frontend_server.dart'; | |
| import 'package:frontend_server/resident_frontend_server_utils.dart' | |
| show computeCachedDillAndCompilerOptionsPaths, sendAndReceiveResponse; | |
| import 'package:frontend_server/starter.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 int statGranularity = 1100; | |
| group('Resident Frontend Server utility functions: ', () { | |
| test('computeCachedDillAndCompilerOptionsPaths', () async { | |
| // [computeCachedDillAndCompilerOptionsPaths] is implemented using | |
| // [path.dirname] and [path.basename], and those functions are platform- | |
| // sensitive, so we test with an example of a Windows path on Windows, and | |
| // an example of a POSIX path on other platforms. | |
| if (Platform.isWindows) { | |
| const String exampleCanonicalizedLibraryPath = | |
| r'C:\Users\user\directory\file.dart'; | |
| final ( | |
| :cachedDillPath, | |
| :cachedCompilerOptionsPath, | |
| ) = computeCachedDillAndCompilerOptionsPaths( | |
| exampleCanonicalizedLibraryPath, | |
| ); | |
| expect( | |
| cachedDillPath, | |
| path.join( | |
| Directory.systemTemp.path, | |
| 'dart_resident_compiler_kernel_cache', | |
| 'C__Users_user_directory_file', | |
| 'file.dart.dill', | |
| ), | |
| ); | |
| expect( | |
| cachedCompilerOptionsPath, | |
| path.join( | |
| Directory.systemTemp.path, | |
| 'dart_resident_compiler_kernel_cache', | |
| 'C__Users_user_directory_file', | |
| 'file.dart_options.json', | |
| ), | |
| ); | |
| } else { | |
| const String exampleCanonicalizedLibraryPath = | |
| '/home/user/directory/file.dart'; | |
| final ( | |
| :cachedDillPath, | |
| :cachedCompilerOptionsPath, | |
| ) = computeCachedDillAndCompilerOptionsPaths( | |
| exampleCanonicalizedLibraryPath, | |
| ); | |
| expect( | |
| cachedDillPath, | |
| path.join( | |
| Directory.systemTemp.path, | |
| 'dart_resident_compiler_kernel_cache', | |
| '_home_user_directory', | |
| 'file.dart.dill', | |
| ), | |
| ); | |
| expect( | |
| cachedCompilerOptionsPath, | |
| path.join( | |
| Directory.systemTemp.path, | |
| 'dart_resident_compiler_kernel_cache', | |
| '_home_user_directory', | |
| 'file.dart_options.json', | |
| ), | |
| ); | |
| } | |
| }); | |
| }); | |
| group('Resident Frontend Server: invalid input: ', () { | |
| test('no command given', () async { | |
| final String jsonResponse = await ResidentFrontendServer.handleRequest( | |
| jsonEncode(<String, Object>{"no": "command"}), | |
| ); | |
| expect( | |
| jsonResponse, | |
| equals( | |
| jsonEncode(<String, Object>{ | |
| "success": false, | |
| "errorMessage": "Unsupported command: null.", | |
| "restartMightHelp": false, | |
| }), | |
| ), | |
| ); | |
| }); | |
| test('invalid command', () async { | |
| final String 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.", | |
| "restartMightHelp": false, | |
| }), | |
| ), | |
| ); | |
| }); | |
| test('not a JSON request', () async { | |
| final String jsonResponse = await ResidentFrontendServer.handleRequest( | |
| "hello", | |
| ); | |
| expect( | |
| jsonResponse, | |
| equals( | |
| jsonEncode(<String, Object>{ | |
| "success": false, | |
| "errorMessage": "hello is not valid JSON.", | |
| "restartMightHelp": false, | |
| }), | |
| ), | |
| ); | |
| }); | |
| test('missing files for compile command', () async { | |
| final String jsonResponse = await ResidentFrontendServer.handleRequest( | |
| jsonEncode(<String, Object>{"command": "compile"}), | |
| ); | |
| expect( | |
| jsonResponse, | |
| equals( | |
| jsonEncode(<String, Object>{ | |
| "success": false, | |
| "errorMessage": | |
| "'compile' requests must include an 'executable' property and " | |
| "an 'output-dill' property.", | |
| "restartMightHelp": false, | |
| }), | |
| ), | |
| ); | |
| }); | |
| }); | |
| group("Resident Frontend Server: 'compile' command tests: ", () { | |
| late Directory d; | |
| late File executable, package, outputDill; | |
| setUp(() async { | |
| d = Directory.systemTemp.createTempSync(); | |
| executable = new File(path.join(d.path, 'src1.dart')) | |
| ..createSync() | |
| ..writeAsStringSync('void main() {print("hello " "there");}'); | |
| package = new File(path.join(d.path, '.dart_tool', 'package_config.json')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync(''' | |
| { | |
| "configVersion": 2, | |
| "packages": [ | |
| { | |
| "name": "hello", | |
| "rootUri": "../", | |
| "packageUri": "./" | |
| } | |
| ] | |
| } | |
| '''); | |
| outputDill = new File(path.join(d.path, 'src1.dart.dill')); | |
| }); | |
| tearDown(() async { | |
| d.deleteSync(recursive: true); | |
| ResidentFrontendServer.compilers.clear(); | |
| }); | |
| void executeDillExpectStdout( | |
| Map<String, dynamic> compileResult, | |
| String expectedStdOut, | |
| ) { | |
| expect(outputDill.path, equals(compileResult['output-dill'])); | |
| ProcessResult runResult = Process.runSync(Platform.resolvedExecutable, [ | |
| outputDill.path, | |
| ], workingDirectory: d.path); | |
| String stderr = runResult.stderr.toString(); | |
| expect(stderr, isEmpty); | |
| String stdout = runResult.stdout.toString().trim(); | |
| expect(stdout, expectedStdOut.trim()); | |
| } | |
| test('initial compile, basic', () async { | |
| final Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| expect(compileResult['output-dill'], equals(outputDill.path)); | |
| }); | |
| test('compile options', () async { | |
| executable.writeAsStringSync('void main() { int x = 1; }'); | |
| final Map<String, dynamic> compileResult1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| verbosity: 'all', | |
| define: <String>['-Dvar=2'], | |
| enableExperiment: <String>['--enable-experiment=test-experiment'], | |
| ), | |
| ), | |
| ); | |
| expect(compileResult1['success'], true); | |
| expect(compileResult1['errorCount'], 0); | |
| List<String> savedOptions = getCachedCompilerOptions(executable.path); | |
| expect(savedOptions, contains('--define=var=2')); | |
| expect(savedOptions, contains('--enable-experiment=test-experiment')); | |
| }); | |
| test('can run compiled dill', () async { | |
| executable.writeAsStringSync('void main() { print("hello world"); }'); | |
| final Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| executeDillExpectStdout(compileResult, "hello world"); | |
| }); | |
| test('Works with -D', () async { | |
| executable.writeAsStringSync( | |
| 'void main() { print(const String.fromEnvironment("foo")); }', | |
| ); | |
| final Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| define: ['--define=foo=bar'], | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| executeDillExpectStdout(compileResult, "bar"); | |
| }); | |
| test('Works with changing -D on same entry', () async { | |
| executable.writeAsStringSync( | |
| 'void main() { print(const String.fromEnvironment("foo")); }', | |
| ); | |
| // Let everything be old. | |
| await new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| // First compile. | |
| for (int i = 0; i < 2; i++) { | |
| Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| define: ['--define=foo=bar'], | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| executeDillExpectStdout(compileResult, "bar"); | |
| if (i == 1) { | |
| // Compiling with the same settings again (with data in memory) should | |
| // reuse existing. | |
| expect(compileResult['returnedStoredKernel'], true); | |
| } | |
| } | |
| // Second compile with other define. The compiler is in memory. | |
| Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| define: ['--define=foo=baz'], | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| executeDillExpectStdout(compileResult, "baz"); | |
| // TODO(jensj): Find a way to do the second compile again - with cleared | |
| // compilers - where we can verify that it didn't actually compile | |
| // anything. | |
| // Third compile with other define. The compiler is removed from memory | |
| // before calling. | |
| ResidentFrontendServer.compilers.clear(); | |
| compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| define: ['--define=foo=qux'], | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| executeDillExpectStdout(compileResult, "qux"); | |
| // Fourth compile with other define. The compiler is removed from memory | |
| // before calling. The cached compiler options have been removed from disk | |
| // too. | |
| ResidentFrontendServer.compilers.clear(); | |
| new File( | |
| computeCachedDillAndCompilerOptionsPaths(executable.path) | |
| .cachedCompilerOptionsPath, | |
| ).deleteSync(); | |
| compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| define: ['--define=foo=quux'], | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| executeDillExpectStdout(compileResult, "quux"); | |
| // Fifth compile with other define. The compiler is removed from memory | |
| // before calling. The cached compiler options have been overwritten with | |
| // nonsense. | |
| ResidentFrontendServer.compilers.clear(); | |
| new File( | |
| computeCachedDillAndCompilerOptionsPaths(executable.path) | |
| .cachedCompilerOptionsPath, | |
| ).writeAsStringSync("æbler"); | |
| compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| define: ['--define=foo=corge'], | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| executeDillExpectStdout(compileResult, "corge"); | |
| }); | |
| void writePackageHelper(List<(String, String)> packages) { | |
| package.writeAsStringSync( | |
| json.encode({ | |
| "configVersion": 2, | |
| "packages": [ | |
| for ((String, String) package in packages) | |
| {"name": package.$1, "rootUri": package.$2, "packageUri": "lib/"}, | |
| ], | |
| }), | |
| ); | |
| } | |
| for (bool inMemory in [true, false]) { | |
| test( | |
| 'handle package changes (${inMemory ? 'in memory' : 'from disk'})', | |
| () async { | |
| executable.writeAsStringSync( | |
| 'import "package:pkga/main.dart"; void main() => hello();', | |
| ); | |
| new File(path.join(d.path, 'pkga_v1', 'lib', 'main.dart')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('void hello() => print("hello v1");'); | |
| new File(path.join(d.path, 'pkga_v2', 'lib', 'main.dart')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('void hello() => print("hello v2");'); | |
| writePackageHelper([("pkga", "../pkga_v1")]); | |
| // Let everything be old. | |
| await new Future.delayed( | |
| const Duration(milliseconds: statGranularity), | |
| ); | |
| if (!inMemory) ResidentFrontendServer.compilers.clear(); | |
| Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| expect(compileResult['incremental'], isNull); | |
| executeDillExpectStdout(compileResult, "hello v1"); | |
| // Update package config and recompile. | |
| writePackageHelper([("pkga", "../pkga_v2")]); | |
| // Make sure the timestamp saved in the resident compiler is after the | |
| // change timestamp. | |
| await new Future.delayed( | |
| const Duration(milliseconds: statGranularity), | |
| ); | |
| if (!inMemory) ResidentFrontendServer.compilers.clear(); | |
| compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| if (inMemory) expect(compileResult['incremental'], isTrue); | |
| executeDillExpectStdout(compileResult, "hello v2"); | |
| if (inMemory) { | |
| // When in memory when recompiling with no change we should just get | |
| // the cached dill. Also if the package config was changed | |
| // previously (but before a previous compile). | |
| compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| expect(compileResult['returnedStoredKernel'], isTrue); | |
| executeDillExpectStdout(compileResult, "hello v2"); | |
| } | |
| }, | |
| ); | |
| } | |
| for (bool inMemory in [true, false]) { | |
| test('invalidates when package file goes away ' | |
| '(${inMemory ? 'in memory' : 'from disk'})', () async { | |
| executable.writeAsStringSync( | |
| 'import "package:pkga/main.dart"; void main() => hello();', | |
| ); | |
| new File(path.join(d.path, 'pkga_v1', 'lib', 'main.dart')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('void hello() => print("hello v1");'); | |
| new File(path.join(d.path, 'pkga_v2', 'lib', 'main.dart')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('void hello() => print("hello v2");'); | |
| writePackageHelper([("pkga", "../pkga_v1")]); | |
| // Let everything be old. | |
| await new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| if (!inMemory) ResidentFrontendServer.compilers.clear(); | |
| Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| expect(compileResult['incremental'], isNull); | |
| executeDillExpectStdout(compileResult, "hello v1"); | |
| // Delete package config and recompile. This should give errors. | |
| package.deleteSync(); | |
| if (!inMemory) ResidentFrontendServer.compilers.clear(); | |
| compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], false); | |
| // Try again: It should still be an error. | |
| if (!inMemory) ResidentFrontendServer.compilers.clear(); | |
| compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], false); | |
| // Recreate package config and recompile. | |
| writePackageHelper([("pkga", "../pkga_v2")]); | |
| if (!inMemory) ResidentFrontendServer.compilers.clear(); | |
| compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| executeDillExpectStdout(compileResult, "hello v2"); | |
| }); | |
| } | |
| for (bool inMemory in [true, false]) { | |
| test('errors on bad package file ' | |
| '(${inMemory ? 'in memory' : 'from disk'})', () async { | |
| executable.writeAsStringSync( | |
| 'import "package:pkga/main.dart"; void main() => hello();', | |
| ); | |
| new File(path.join(d.path, 'pkga', 'lib', 'main.dart')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('void hello() => print("hello");'); | |
| package.writeAsStringSync("this is a bad package file"); | |
| // Let everything be old. | |
| await new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| if (!inMemory) ResidentFrontendServer.compilers.clear(); | |
| Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], false); | |
| // Try again. | |
| if (!inMemory) ResidentFrontendServer.compilers.clear(); | |
| compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], false); | |
| }); | |
| } | |
| for (bool inMemory in [true, false]) { | |
| test('bad import ' | |
| '(${inMemory ? 'in memory' : 'from disk'})', () async { | |
| // This is a local version of standalone/package/invalid_uri_test/01 | |
| executable.writeAsStringSync( | |
| "import 'package://lib1.dart'; void main() {}", | |
| ); | |
| if (!inMemory) ResidentFrontendServer.compilers.clear(); | |
| Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| supportMirrors: true, | |
| enableAsserts: true, | |
| soundNullSafety: true, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], false); | |
| }); | |
| } | |
| test('produces aot kernel', () async { | |
| final Map<String, dynamic> compileResult1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.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 Map<String, dynamic> compileResult = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult['success'], true); | |
| expect(compileResult['errorCount'], 0); | |
| expect(compileResult['output-dill'], equals(outputDill.path)); | |
| }); | |
| test('incremental compilation', () async { | |
| await new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| final Map<String, dynamic> compileResults1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| executable.writeAsStringSync( | |
| executable.readAsStringSync().replaceFirst('there', 'world'), | |
| ); | |
| final Map<String, dynamic> compileResults2 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.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(new 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 new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| final Map<String, dynamic> compileResults1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| final Map<String, dynamic> compileResults2 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.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 File executable2 = new File(path.join(d.path, 'src2.dart')) | |
| ..writeAsStringSync('void main() {}'); | |
| final File entryPointDill = new File(path.join(d.path, 'src2.dart.dill')); | |
| final Map<String, dynamic> compileResults1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| final Map<String, dynamic> compileResults2 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| 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(new Uri.file(executable.path)), | |
| ); | |
| expect( | |
| ResidentFrontendServer | |
| .compilers[executable2.path]! | |
| .trackedSources | |
| .length, | |
| 1, | |
| ); | |
| expect( | |
| ResidentFrontendServer | |
| .compilers[executable2.path]! | |
| .trackedSources | |
| .first, | |
| equals(new Uri.file(executable2.path)), | |
| ); | |
| expect(ResidentFrontendServer.compilers.length, 2); | |
| }); | |
| test('Cached kernel is removed between compilation requests', () async { | |
| await new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| final Map<String, dynamic> compileResults1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| executable.writeAsStringSync( | |
| executable.readAsStringSync().replaceFirst('there', 'world'), | |
| ); | |
| outputDill.deleteSync(); | |
| expect(outputDill.existsSync(), false); | |
| final Map<String, dynamic> compileResults2 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| expect(compileResults1['success'], true); | |
| expect( | |
| compileResults1['errorCount'], | |
| allOf(equals(compileResults2['errorCount']), 0), | |
| ); | |
| expect(compileResults2['returnedStoredKernel'], null); | |
| expect(compileResults2['incremental'], true); | |
| expect(outputDill.existsSync(), true); | |
| expect(ResidentFrontendServer.compilers.length, 1); | |
| }); | |
| test('maintains tracked sources', () async { | |
| await new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| final File executable2 = new File(path.join(d.path, 'src2.dart')) | |
| ..createSync() | |
| ..writeAsStringSync(''' | |
| import 'src3.dart'; | |
| void main() {}'''); | |
| final File executable3 = new 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( | |
| createCompileJSON( | |
| executable: executable.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ); | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ); | |
| final Map<String, dynamic> compileResult1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| outputDill: outputDill.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(new Uri.file(executable.path)), | |
| ); | |
| // switching entrypoints, packages, and modifying packages | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable2.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ); | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable2.path, | |
| packages: package.path, | |
| outputDill: outputDill.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 new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| final Map<String, dynamic> compileResult2 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable2.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| expect(compileResult2['success'], true); | |
| expect(compileResult2['incremental'], true); | |
| expect(compileResult2['returnedStoredKernel'], null); | |
| expect( | |
| ResidentFrontendServer | |
| .compilers[executable2.path]! | |
| .trackedSources | |
| .length, | |
| greaterThanOrEqualTo(2), | |
| ); | |
| expect( | |
| ResidentFrontendServer.compilers[executable2.path]!.trackedSources, | |
| containsAll(<Uri>{ | |
| new Uri.file(executable2.path), | |
| new Uri.file(executable3.path), | |
| }), | |
| ); | |
| // remove a source | |
| executable2.writeAsStringSync('void main() {}'); | |
| final Map<String, dynamic> compileResult3 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable2.path, | |
| packages: package.path, | |
| outputDill: outputDill.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>{new Uri.file(executable2.path)}), | |
| ); | |
| }); | |
| test('continues to work after compiler error is produced', () async { | |
| final String originalContent = executable.readAsStringSync(); | |
| final String newContent = originalContent.replaceAll(';', '@'); | |
| await new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| executable.writeAsStringSync(newContent); | |
| final Map<String, dynamic> compileResults1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| executable.writeAsStringSync(originalContent); | |
| final Map<String, dynamic> compileResults2 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.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(new Uri.file(executable.path)), | |
| ); | |
| }); | |
| test('using cached kernel maintains error messages', () async { | |
| final String originalContent = executable.readAsStringSync(); | |
| executable.writeAsStringSync(originalContent.replaceFirst(';', '')); | |
| await new Future.delayed(const Duration(milliseconds: statGranularity)); | |
| final Map<String, dynamic> compileResults1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| final Map<String, dynamic> compileResults2 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| executable.writeAsStringSync(originalContent); | |
| final Map<String, dynamic> compileResults3 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.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 File executable2 = new File(path.join(d.path, 'src2.dart')) | |
| ..createSync() | |
| ..writeAsStringSync(''' | |
| import 'src3.dart'; | |
| void main() {}'''); | |
| final File executable3 = new File(path.join(d.path, 'src3.dart')) | |
| ..createSync() | |
| ..writeAsStringSync(''' | |
| void main() {}'''); | |
| final File executable4 = new File(path.join(d.path, 'src4.dart')) | |
| ..createSync() | |
| ..writeAsStringSync(''' | |
| void main() {}'''); | |
| final Map<String, dynamic> compileResults1 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| final Map<String, dynamic> compileResults2 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable2.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| final Map<String, dynamic> compileResults3 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable3.path, | |
| packages: package.path, | |
| outputDill: outputDill.path, | |
| ), | |
| ), | |
| ); | |
| final Map<String, dynamic> compileResults4 = jsonDecode( | |
| await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: executable4.path, | |
| packages: package.path, | |
| outputDill: outputDill.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: ', () { | |
| late Directory d; | |
| late File serverInfo; | |
| setUp(() { | |
| d = Directory.systemTemp.createTempSync(); | |
| serverInfo = new File(path.join(d.path, 'info.txt')); | |
| }); | |
| tearDown(() { | |
| d.deleteSync(recursive: true); | |
| }); | |
| test('ServerSocket fails to bind', () async { | |
| final StreamSubscription<Socket>? 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 Map<String, dynamic> shutdownResult = await sendAndReceiveResponse( | |
| ResidentFrontendServer.shutdownCommand, | |
| serverInfo, | |
| ); | |
| expect(shutdownResult, equals(<String, dynamic>{"shutdown": true})); | |
| expect(serverInfo.existsSync(), false); | |
| }); | |
| test('timed shutdown', () async { | |
| await residentListenAndCompile( | |
| InternetAddress.loopbackIPv4, | |
| 0, | |
| serverInfo, | |
| inactivityTimeout: const Duration(milliseconds: 100), | |
| ); | |
| expect(serverInfo.existsSync(), true); | |
| await new Future.delayed(const Duration(milliseconds: 150)); | |
| expect(serverInfo.existsSync(), false); | |
| try { | |
| await sendAndReceiveResponse( | |
| ResidentFrontendServer.shutdownCommand, | |
| serverInfo, | |
| ); | |
| fail('Expected to catch PathNotFoundException'); | |
| } on PathNotFoundException catch (e) { | |
| expect(e.message, contains('Cannot open file')); | |
| } | |
| }); | |
| test('concurrent startup requests', () async { | |
| final StreamSubscription<Socket>? serverSubscription = | |
| await residentListenAndCompile( | |
| InternetAddress.loopbackIPv4, | |
| 0, | |
| serverInfo, | |
| ); | |
| final StreamSubscription<Socket>? startWhileAlreadyRunning = | |
| await residentListenAndCompile( | |
| InternetAddress.loopbackIPv4, | |
| 0, | |
| serverInfo, | |
| ); | |
| expect(serverSubscription, isNot(null)); | |
| expect(startWhileAlreadyRunning, null); | |
| expect(serverInfo.existsSync(), true); | |
| final Map<String, dynamic> shutdownResult = await sendAndReceiveResponse( | |
| ResidentFrontendServer.shutdownCommand, | |
| serverInfo, | |
| ); | |
| expect(shutdownResult, equals(<String, dynamic>{"shutdown": true})); | |
| expect(serverInfo.existsSync(), false); | |
| }); | |
| test('resident server starter', () async { | |
| final Future<int> returnValue = starter([ | |
| '--resident-info-file-name=${serverInfo.path}', | |
| ]); | |
| expect(await returnValue, 0); | |
| expect(serverInfo.existsSync(), true); | |
| Map<String, dynamic> result = await sendAndReceiveResponse( | |
| ResidentFrontendServer.shutdownCommand, | |
| serverInfo, | |
| ); | |
| expect(result, equals(<String, dynamic>{"shutdown": true})); | |
| expect(serverInfo.existsSync(), false); | |
| try { | |
| await sendAndReceiveResponse( | |
| ResidentFrontendServer.shutdownCommand, | |
| serverInfo, | |
| ); | |
| fail('Expected to catch PathNotFoundException'); | |
| } on PathNotFoundException catch (e) { | |
| expect(e.message, contains('Cannot open file')); | |
| } | |
| }); | |
| }); | |
| group("Resident Frontend Server: 'record-uses' property tests: ", () { | |
| test('record-uses is rejected in JIT mode', () async { | |
| final String jsonResponse = await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: 'a.dart', | |
| outputDill: 'a.dart.dill', | |
| recordUses: 'out.json', | |
| ), | |
| ); | |
| expect( | |
| jsonDecode(jsonResponse)['errorMessage'], | |
| equals( | |
| "The 'record-uses' property is only supported for AOT compilation.", | |
| ), | |
| ); | |
| }); | |
| test('record-uses in AOT mode', () async { | |
| final Directory d = Directory.systemTemp.createTempSync(); | |
| try { | |
| final File metaFile = new File( | |
| path.join(d.path, 'meta', 'lib', 'meta.dart'), | |
| ); | |
| metaFile.createSync(recursive: true); | |
| metaFile.writeAsStringSync(''' | |
| library meta; | |
| class RecordUse { | |
| const RecordUse(); | |
| } | |
| '''); | |
| final File fooFile = new File( | |
| path.join(d.path, 'foo', 'lib', 'foo.dart'), | |
| ); | |
| fooFile.createSync(recursive: true); | |
| fooFile.writeAsStringSync(''' | |
| import 'package:meta/meta.dart'; | |
| class Foo { | |
| @RecordUse() | |
| static void bar() { | |
| print(42); | |
| } | |
| } | |
| '''); | |
| final File mainFile = new File(path.join(d.path, 'a.dart')); | |
| mainFile.writeAsStringSync(''' | |
| import 'package:foo/foo.dart'; | |
| void main() { | |
| Foo.bar(); | |
| } | |
| '''); | |
| final File packageConfigFile = new File( | |
| path.join(d.path, '.dart_tool', 'package_config.json'), | |
| ); | |
| packageConfigFile.createSync(recursive: true); | |
| packageConfigFile.writeAsStringSync( | |
| jsonEncode({ | |
| "configVersion": 2, | |
| "packages": [ | |
| { | |
| "name": "meta", | |
| "rootUri": "../meta", | |
| "packageUri": "lib/", | |
| "languageVersion": "3.0", | |
| }, | |
| { | |
| "name": "foo", | |
| "rootUri": "../foo", | |
| "packageUri": "lib/", | |
| "languageVersion": "3.0", | |
| }, | |
| ], | |
| }), | |
| ); | |
| final File recordedUsesFile = new File( | |
| path.join(d.path, 'recorded_uses.json'), | |
| ); | |
| final File outputDill = new File(path.join(d.path, 'app.dill')); | |
| final String jsonResponse = await ResidentFrontendServer.handleRequest( | |
| createCompileJSON( | |
| executable: mainFile.path, | |
| outputDill: outputDill.path, | |
| packages: packageConfigFile.path, | |
| aot: true, | |
| tfa: true, | |
| recordUses: recordedUsesFile.path, | |
| enableExperiment: ['--enable-experiment=record-use'], | |
| ), | |
| ); | |
| final Map<String, dynamic> response = jsonDecode(jsonResponse); | |
| expect(response['success'], true); | |
| expect(recordedUsesFile.existsSync(), true); | |
| expect(recordedUsesFile.readAsStringSync(), contains('bar')); | |
| List<String> savedOptions = getCachedCompilerOptions(mainFile.path); | |
| expect(savedOptions, contains('--enable-experiment=record-use')); | |
| } finally { | |
| d.deleteSync(recursive: true); | |
| ResidentFrontendServer.compilers.clear(); | |
| } | |
| }); | |
| }); | |
| } | |
| List<String> getCachedCompilerOptions(String mainPath) { | |
| List cachedCompilerOptionsFileContents = jsonDecode( | |
| new File( | |
| computeCachedDillAndCompilerOptionsPaths(mainPath) | |
| .cachedCompilerOptionsPath, | |
| ).readAsStringSync(), | |
| ); | |
| return [...cachedCompilerOptionsFileContents]; | |
| } | |
| /// Used to create compile requests for the ResidentFrontendServer. | |
| /// Returns a JSON string that the resident compiler will be able to | |
| /// interpret. | |
| String createCompileJSON({ | |
| required String executable, | |
| String? packages, | |
| required String outputDill, | |
| bool? supportMirrors, | |
| bool? enableAsserts, | |
| bool? soundNullSafety, | |
| String? verbosity, | |
| bool? aot, | |
| bool? tfa, | |
| bool? rta, | |
| bool? treeShakeWriteOnlyFields, | |
| bool? protobufTreeShakerV2, | |
| List<String>? define, | |
| List<String>? enableExperiment, | |
| bool verbose = false, | |
| String? nativeAssetsYaml, | |
| String? recordUses, | |
| }) { | |
| assert( | |
| define == null || | |
| define.every((s) => s.startsWith("--define=")) || | |
| define.every((s) => s.startsWith("-D")), | |
| ); | |
| assert( | |
| enableExperiment == null || | |
| enableExperiment.every((s) => s.startsWith("--")), | |
| ); | |
| return jsonEncode(<String, Object>{ | |
| "command": "compile", | |
| "executable": executable, | |
| "output-dill": outputDill, | |
| "aot": ?aot, | |
| "define": ?define, | |
| "enable-asserts": ?enableAsserts, | |
| "enable-experiment": ?enableExperiment, | |
| "packages": ?packages, | |
| "protobuf-tree-shaker-v2": ?protobufTreeShakerV2, | |
| "rta": ?rta, | |
| "sound-null-safety": ?soundNullSafety, | |
| "support-mirrors": ?supportMirrors, | |
| "tfa": ?tfa, | |
| "tree-shaker-write-only-fields": ?treeShakeWriteOnlyFields, | |
| "verbosity": ?verbosity, | |
| "verbose": verbose, | |
| "native-assets": ?nativeAssetsYaml, | |
| "record-uses": ?recordUses, | |
| }); | |
| } |