// ignore_for_file: depend_on_referenced_packages import 'dart:convert'; import 'dart:io'; import 'package:dart_framework/platform/process.dart'; import 'package:oto/cli/commands/command_scheduler.dart'; import 'package:oto/cli/commands/scheduler/scheduler_manager.dart'; import 'package:oto/oto/core/system_runtime.dart'; import 'package:test/test.dart'; class StartExecutableCall { final String program; final List args; final ProcessStartMode mode; StartExecutableCall(this.program, this.args, this.mode); } class FakeSystemRuntime implements SystemRuntime { final List runShellCalls = []; final List startExecutableCalls = []; final List killedPids = []; final bool _isWindows; FakeSystemRuntime({bool isWindows = false}) : _isWindows = isWindows; @override bool get isWindows => _isWindows; @override bool get isMacOS => !_isWindows; @override bool get isLinux => !_isWindows; @override Map get environment => {}; @override Future startShell( StringBuffer shell, { String? workspace, bool printStdout = true, bool printStderr = true, Converter, String>? decoder, LogHandler? logHandler, }) async { throw UnimplementedError(); } @override Future runShell( StringBuffer shell, { String? workspace, bool printStdout = true, bool printStderr = true, Converter, String>? decoder, LogHandler? logHandler, }) async { runShellCalls.add(shell); return ProcessResult(0, 0, '', ''); } @override Future runExecutable( String exe, List args, { String? workingDirectory, Encoding? stdoutEncoding, Encoding? stderrEncoding, }) async { throw UnimplementedError(); } @override Future startExecutable( String program, List args, { String? workingDirectory, ProcessStartMode mode = ProcessStartMode.normal, }) async { startExecutableCalls.add(StartExecutableCall(program, args, mode)); return _DummyProcess(); } @override bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) { killedPids.add(pid); return true; } } class _DummyProcess implements Process { @override Future get exitCode => Future.value(0); @override bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => true; @override int get pid => 12345; @override Stream> get stderr => const Stream.empty(); @override IOSink get stdin => throw UnimplementedError(); @override Stream> get stdout => const Stream.empty(); } class TestSchedulerManager extends SchedulerManager { TestSchedulerManager({ super.runtime, super.processFinder, super.exitHandler, }); @override String get exePath => '/bin/oto'; } class FakeSchedulerManager extends SchedulerManager { bool showListCalled = false; bool terminateCalled = false; bool enableSchedulerCalled = false; String? enabledAlias; bool? enabledValue; bool registCalled = false; String? registeredFilePath; @override Future showList() async { showListCalled = true; } @override Future terminate() async { terminateCalled = true; } @override Future enableScheduler(String alias, bool enable) async { enableSchedulerCalled = true; enabledAlias = alias; enabledValue = enable; } @override Future regist(String filePath) async { registCalled = true; registeredFilePath = filePath; } } void main() { group('SchedulerManager Process Control via Injected Runtime', () { test('Windows starts detached executable', () async { final fakeRuntime = FakeSystemRuntime(isWindows: true); final manager = TestSchedulerManager(runtime: fakeRuntime); await manager.testStartProcess(); expect(fakeRuntime.startExecutableCalls, hasLength(1)); expect(fakeRuntime.startExecutableCalls.single.program, contains(Platform.resolvedExecutable)); expect(fakeRuntime.startExecutableCalls.single.mode, ProcessStartMode.detached); expect(fakeRuntime.runShellCalls, isEmpty); }); test('Unix starts background shell process via nohup', () async { final fakeRuntime = FakeSystemRuntime(isWindows: false); final manager = TestSchedulerManager(runtime: fakeRuntime); await manager.testStartProcess(); expect(fakeRuntime.runShellCalls, hasLength(1)); expect(fakeRuntime.runShellCalls.single.toString(), contains('nohup')); expect(fakeRuntime.runShellCalls.single.toString(), contains('scheduler -s &')); expect(fakeRuntime.startExecutableCalls, isEmpty); }); test('terminate kills current scheduler process', () async { final fakeRuntime = FakeSystemRuntime(); final manager = TestSchedulerManager( runtime: fakeRuntime, processFinder: (name) async => [], ); var p = ProcessInfo(); p.pid = 98765; p.name = 'scheduler -s'; manager.currentScheduler = p; await manager.terminate(); expect(fakeRuntime.killedPids, [98765]); }); test('_checkProcess uses injected processFinder', () async { bool finderCalled = false; final manager = TestSchedulerManager( processFinder: (query) async { finderCalled = true; var p = ProcessInfo(); p.pid = 11111; p.name = 'main.dart scheduler -s oto scheduler -s'; return [p]; }, ); await manager.testCheckProcess(); expect(finderCalled, isTrue); expect(manager.currentScheduler?.pid, 11111); }); }); group('CommandScheduler routing', () { test( 'command_scheduler accepts injected manager and exit handler for routing', () async { final fakeManager = FakeSchedulerManager(); int? exitedCode; final cmd = CommandScheduler( scheduler: fakeManager, exitHandler: (code) { exitedCode = code; }, ); await cmd.execute(['-l']); expect(fakeManager.showListCalled, isTrue); await cmd.execute(['-t']); expect(fakeManager.terminateCalled, isTrue); await cmd.execute(['-r', 'dummy.yaml']); expect(fakeManager.registCalled, isTrue); expect(fakeManager.registeredFilePath, 'dummy.yaml'); expect(exitedCode, 0); }); }); }