// Copyright (c) 2019, 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. @OnPlatform({ 'windows': Skip('Directories cant be deleted while processes are still open'), }) library; import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'package:build_daemon/constants.dart'; import 'package:build_daemon/daemon.dart'; import 'package:build_daemon/src/fakes/fake_builder.dart'; import 'package:build_daemon/src/fakes/fake_change_provider.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; import 'uuid.dart'; final defaultIdleTimeoutSec = defaultIdleTimeout.inSeconds; void main() { final testDaemons = []; final testWorkspaces = []; group('Daemon', () { setUp(() { testDaemons.clear(); testWorkspaces.clear(); }); tearDown(() async { for (final testDaemon in testDaemons) { testDaemon.kill(ProcessSignal.sigkill); } for (final testWorkspace in testWorkspaces) { final workspace = Directory(daemonWorkspace(testWorkspace)); if (workspace.existsSync()) { workspace.deleteSync(recursive: true); } } }); test('can be stopped', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); final daemon = Daemon(workspace); await daemon.start({}, FakeDaemonBuilder(), FakeChangeProvider()); expect(daemon.onDone, completes); await daemon.stop(); }); test('can run if no other daemon is running', () async { final workspace = randomWorkspace(); final daemon = await _runDaemon(workspace); testDaemons.add(daemon); expect(await _statusOf(daemon), 'RUNNING'); }); test('shuts down if no client connects', () async { final workspace = randomWorkspace(); final daemon = await _runDaemon(workspace, timeout: 1); testDaemons.add(daemon); expect(await daemon.exitCode, isNotNull); }); test( 'can not run if another daemon is running in the same workspace', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); final daemonOne = await _runDaemon( workspace, timeout: defaultIdleTimeoutSec * 2, ); expect(await _statusOf(daemonOne, logPrefix: 'one'), 'RUNNING'); final daemonTwo = await _runDaemon(workspace); testDaemons.addAll([daemonOne, daemonTwo]); expect(await _statusOf(daemonTwo, logPrefix: 'two'), 'ALREADY RUNNING'); }, timeout: const Timeout.factor(2), ); test( 'can run if another daemon is running in a different workspace', () async { final workspace1 = randomWorkspace(); final workspace2 = randomWorkspace(); testWorkspaces.addAll([workspace1, workspace2]); final daemonOne = await _runDaemon(workspace1); expect(await _statusOf(daemonOne), 'RUNNING'); final daemonTwo = await _runDaemon(workspace2); testDaemons.addAll([daemonOne, daemonTwo]); expect(await _statusOf(daemonTwo), 'RUNNING'); }, timeout: const Timeout.factor(2), ); test('can start two daemons at the same time', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); final daemonOne = await _runDaemon(workspace); expect(await _statusOf(daemonOne), 'RUNNING'); final daemonTwo = await _runDaemon(workspace); expect(await _statusOf(daemonTwo), 'ALREADY RUNNING'); testDaemons.addAll([daemonOne, daemonTwo]); }, timeout: const Timeout.factor(2)); test('logs the version when running', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); final daemon = await _runDaemon(workspace); testDaemons.add(daemon); expect(await _statusOf(daemon), 'RUNNING'); expect(await Daemon(workspace).runningVersion(), currentVersion); }); test('does not set the current version if not running', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); expect(await Daemon(workspace).runningVersion(), null); }); test('logs the options when running', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); final daemon = await _runDaemon(workspace); testDaemons.add(daemon); expect(await _statusOf(daemon), 'RUNNING'); expect( (await Daemon(workspace).currentOptions()).contains('foo'), isTrue, ); }); test('does not log the options if not running', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); expect((await Daemon(workspace).currentOptions()).isEmpty, isTrue); }); test('cleans up after itself', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); final daemon = await _runDaemon(workspace); // Wait for the daemon to be running before checking the workspace exits. expect(await _statusOf(daemon), 'RUNNING'); expect(Directory(daemonWorkspace(workspace)).existsSync(), isTrue); // Sending an interrupt signal will let the daemon gracefully exit. daemon.kill(ProcessSignal.sigint); await daemon.exitCode; expect(Directory(daemonWorkspace(workspace)).existsSync(), isFalse); }); test('daemon stops after file changes stream has error', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); final daemon = await _runDaemon( workspace, errorChangeProviderAfterNSeconds: 1, ); expect(await _statusOf(daemon), 'RUNNING'); await daemon.exitCode; expect(Directory(daemonWorkspace(workspace)).existsSync(), isFalse); }); test('daemon stops after file changes stream is closed', () async { final workspace = randomWorkspace(); testWorkspaces.add(workspace); final daemon = await _runDaemon( workspace, closeChangeProviderAfterNSeconds: 1, ); expect(await _statusOf(daemon), 'RUNNING'); await daemon.exitCode; expect(Directory(daemonWorkspace(workspace)).existsSync(), isFalse); }); }); } /// Returns the daemon status. Future _statusOf(Process daemon, {String logPrefix = ''}) async { final statusCompleter = Completer(); daemon.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(( line, ) { print('$logPrefix [STDOUT] $line'); if (!statusCompleter.isCompleted && (line == 'RUNNING' || line == 'ALREADY RUNNING')) { statusCompleter.complete(line); } }); daemon.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(( line, ) { print('$logPrefix [STDERR] $line'); }); return statusCompleter.future; } Future _runDaemon( String workspace, { int timeout = -1, int errorChangeProviderAfterNSeconds = -1, int closeChangeProviderAfterNSeconds = -1, }) async { timeout = timeout > 0 ? timeout : defaultIdleTimeoutSec; await d.file('test.dart', ''' import 'package:build_daemon/daemon.dart'; import 'package:build_daemon/daemon_builder.dart'; import 'package:build_daemon/client.dart'; import 'package:build_daemon/src/fakes/fake_builder.dart'; import 'package:build_daemon/src/fakes/fake_change_provider.dart'; main() async { var options = ['foo'].toSet(); var timeout = Duration(seconds: $timeout); var daemon = Daemon('$workspace'); var changeProvider = FakeChangeProvider(); if(daemon.hasLock) { await daemon.start( options, FakeDaemonBuilder(), changeProvider, timeout: timeout); // Real implementations of the daemon usually have // non-trivial set up time. await Future.delayed(Duration(seconds: 1)); print('RUNNING'); if ($errorChangeProviderAfterNSeconds > 0) { Future.delayed(Duration(seconds: $errorChangeProviderAfterNSeconds), () { changeProvider.changeStreamController.addError('ERROR'); }); } if ($closeChangeProviderAfterNSeconds > 0) { Future.delayed(Duration(seconds: $closeChangeProviderAfterNSeconds), () { changeProvider.changeStreamController.close(); }); } }else{ // Mimic the behavior of actual daemon implementations. var version = await daemon.runningVersion(); if(version != '$currentVersion') throw VersionSkew(); print('ALREADY RUNNING'); } } ''').create(); final args = [ ...Platform.executableArguments, if (!Platform.executableArguments.any( (arg) => arg.startsWith('--packages'), )) '--packages=${(await Isolate.packageConfig)!.path}', 'test.dart', ]; final process = await Process.start( Platform.resolvedExecutable, args, workingDirectory: d.sandbox, ); return process; }