독립 control plane 구성을 위해 기존 Dart CLI/runtime을 runner 앱 경계로 옮기고, 후속 client/core/root 작업을 같은 마일스톤 task group에 연결한다.
239 lines
6.3 KiB
Dart
239 lines
6.3 KiB
Dart
// 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<String> args;
|
|
final ProcessStartMode mode;
|
|
StartExecutableCall(this.program, this.args, this.mode);
|
|
}
|
|
|
|
class FakeSystemRuntime implements SystemRuntime {
|
|
final List<StringBuffer> runShellCalls = [];
|
|
final List<StartExecutableCall> startExecutableCalls = [];
|
|
final List<int> 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<String, String> get environment => {};
|
|
|
|
@override
|
|
Future<ProcessData> startShell(
|
|
StringBuffer shell, {
|
|
String? workspace,
|
|
bool printStdout = true,
|
|
bool printStderr = true,
|
|
Converter<List<int>, String>? decoder,
|
|
LogHandler? logHandler,
|
|
}) async {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Future<ProcessResult> runShell(
|
|
StringBuffer shell, {
|
|
String? workspace,
|
|
bool printStdout = true,
|
|
bool printStderr = true,
|
|
Converter<List<int>, String>? decoder,
|
|
LogHandler? logHandler,
|
|
}) async {
|
|
runShellCalls.add(shell);
|
|
return ProcessResult(0, 0, '', '');
|
|
}
|
|
|
|
@override
|
|
Future<ProcessResult> runExecutable(
|
|
String exe,
|
|
List<String> args, {
|
|
String? workingDirectory,
|
|
Encoding? stdoutEncoding,
|
|
Encoding? stderrEncoding,
|
|
}) async {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
Future<Process> startExecutable(
|
|
String program,
|
|
List<String> 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<int> get exitCode => Future.value(0);
|
|
@override
|
|
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => true;
|
|
@override
|
|
int get pid => 12345;
|
|
@override
|
|
Stream<List<int>> get stderr => const Stream.empty();
|
|
@override
|
|
IOSink get stdin => throw UnimplementedError();
|
|
@override
|
|
Stream<List<int>> 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);
|
|
});
|
|
});
|
|
}
|