// Copyright (c) 2024, 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. @TestOn('vm') @Timeout(Duration(minutes: 2)) library; import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:dwds/sdk_configuration.dart'; import 'package:dwds/src/services/expression_compiler.dart'; import 'package:dwds/src/services/expression_compiler_service.dart'; import 'package:dwds/src/utilities/server.dart'; import 'package:logging/logging.dart'; import 'package:shelf/shelf.dart'; import 'package:test/test.dart'; import 'package:test_common/logging.dart'; ExpressionCompilerService get service => _service!; late ExpressionCompilerService? _service; HttpServer get server => _server!; late HttpServer? _server; StreamController get output => _output!; late StreamController? _output; void testAll({required CompilerOptions compilerOptions}) { group('expression compiler service with fake asset server', () { final logger = Logger('ExpressionCompilerServiceTest'); late Directory outputDir; Future stop() async { await _service?.stop(); await _server?.close(); await _output?.close(); _service = null; _server = null; _output = null; } setUp(() async { final systemTempDir = Directory.systemTemp; outputDir = systemTempDir.createTempSync('foo bar'); final source = outputDir.uri.resolve('try.dart'); final packages = outputDir.uri.resolve('package_config.json'); final kernel = outputDir.uri.resolve('try.full.dill'); // Expression compiler service does not need any extra assets // generated in the SDK, so we use the current SDK layout and // configuration. final executable = Platform.resolvedExecutable; // redirect logs for testing _output = StreamController.broadcast(); output.stream.listen(printOnFailure); configureLogWriter( customLogWriter: (level, message, {error, loggerName, stackTrace}) { final e = error == null ? '' : ': $error'; final s = stackTrace == null ? '' : ':\n$stackTrace'; output.add('[$level] $loggerName: $message$e$s'); }, ); // start asset server _server = await startHttpServer('localhost'); final port = server.port; // start expression compilation service Response assetHandler(request) => Response(200, body: File.fromUri(kernel).readAsBytesSync()); _service = ExpressionCompilerService( 'localhost', port, verbose: false, sdkConfigurationProvider: DefaultSdkConfigurationProvider(), ); await service.initialize(compilerOptions); // setup asset server serveHttpRequests(server, assetHandler, (e, s) { logger.warning('Error serving requests', e, s); }); // generate full dill File.fromUri(source) ..createSync() ..writeAsStringSync('''void main() { // breakpoint line }'''); File.fromUri(packages) ..createSync() ..writeAsStringSync(''' { "configVersion": 2, "packages": [ { "name": "try", "rootUri": "./", "packageUri": "./" } ] } '''); final args = [ 'compile', 'js-dev', 'try.dart', '-o', 'try.js', '--experimental-output-compiled-kernel', '--multi-root', '${outputDir.uri}', '--multi-root-scheme', 'org-dartlang-app', '--packages', packages.path, ]; final process = await Process.start( executable, args, workingDirectory: outputDir.path, ).then((p) { transformToLines(p.stdout).listen(output.add); transformToLines(p.stderr).listen(output.add); return p; }); expect( await process.exitCode, 0, reason: 'failed running $executable with args $args', ); expect( File.fromUri(kernel).existsSync(), true, reason: 'failed to create full dill', ); }); tearDown(() async { await stop(); outputDir.deleteSync(recursive: true); }); test('works with no errors', () async { expect(output.stream, neverEmits(contains('[SEVERE]'))); expect( output.stream, emitsThrough( contains( '[INFO] ExpressionCompilerService: Updating dependencies...', ), ), ); expect( output.stream, emitsThrough( contains('[INFO] ExpressionCompilerService: Updated dependencies.'), ), ); expect( output.stream, emitsThrough( contains('[FINEST] ExpressionCompilerService: Compiling "true" at'), ), ); expect( output.stream, emitsThrough( contains('[FINEST] ExpressionCompilerService: Compiled "true" to:'), ), ); expect( output.stream, emitsThrough(contains('[INFO] ExpressionCompilerService: Stopped.')), ); final result = await service.updateDependencies({ 'try': ModuleInfo('try.full.dill', 'try.dill'), }); expect(result, true, reason: 'failed to update dependencies'); final compilationResult = await service.compileExpressionToJs( '0', 'org-dartlang-app:/try.dart', 'org-dartlang-app:/try.dart', 2, 1, {}, {}, 'try', 'true', ); expect( compilationResult, isA() .having((r) => r.result, 'result', contains('return true;')) .having((r) => r.isError, 'isError', false), ); await stop(); }); test('can evaluate multiple expressions', () async { expect(output.stream, neverEmits(contains('[SEVERE]'))); expect( output.stream, emitsThrough( contains( '[INFO] ExpressionCompilerService: Updating dependencies...', ), ), ); expect( output.stream, emitsThrough( contains('[INFO] ExpressionCompilerService: Updated dependencies.'), ), ); expect( output.stream, emitsThrough(contains('[INFO] ExpressionCompilerService: Stopped.')), ); final result = await service.updateDependencies({ 'try': ModuleInfo('try.full.dill', 'try.dill'), }); expect(result, true, reason: 'failed to update dependencies'); final compilationResult1 = await service.compileExpressionToJs( '0', 'org-dartlang-app:/try.dart', 'org-dartlang-app:/try.dart', 2, 1, {}, {}, 'try', 'true', ); final compilationResult2 = await service.compileExpressionToJs( '0', 'org-dartlang-app:/try.dart', 'org-dartlang-app:/try.dart', 2, 1, {}, {}, 'try', 'false', ); expect( compilationResult1, isA() .having((r) => r.result, 'result', contains('return true;')) .having((r) => r.isError, 'isError', false), ); expect( compilationResult2, isA() .having((r) => r.result, 'result', contains('return false;')) .having((r) => r.isError, 'isError', false), ); await stop(); }); test('can compile multiple expressions in parallel', () async { expect(output.stream, neverEmits(contains('[SEVERE]'))); expect( output.stream, emitsThrough( contains( '[INFO] ExpressionCompilerService: Updating dependencies...', ), ), ); expect( output.stream, emitsThrough( contains('[INFO] ExpressionCompilerService: Updated dependencies.'), ), ); expect( output.stream, emitsThrough(contains('[INFO] ExpressionCompilerService: Stopped.')), ); final result = await service.updateDependencies({ 'try': ModuleInfo('try.full.dill', 'try.dill'), }); expect(result, true, reason: 'failed to update dependencies'); final compilationResult1 = service.compileExpressionToJs( '0', 'org-dartlang-app:/try.dart', 'org-dartlang-app:/try.dart', 2, 1, {}, {}, 'try', 'true', ); final compilationResult2 = service.compileExpressionToJs( '0', 'org-dartlang-app:/try.dart', 'org-dartlang-app:/try.dart', 2, 1, {}, {}, 'try', 'false', ); final results = await Future.wait([ compilationResult1, compilationResult2, ]); expect( results[0], isA() .having((r) => r.result, 'result', contains('return true;')) .having((r) => r.isError, 'isError', false), ); expect( results[1], isA() .having((r) => r.result, 'result', contains('return false;')) .having((r) => r.isError, 'isError', false), ); await stop(); }); }); } Stream transformToLines(Stream> byteStream) { return byteStream .transform(utf8.decoder) .transform(const LineSplitter()); }