독립 control plane 구성을 위해 기존 Dart CLI/runtime을 runner 앱 경계로 옮기고, 후속 client/core/root 작업을 같은 마일스톤 task group에 연결한다.
296 lines
8.8 KiB
Dart
296 lines
8.8 KiB
Dart
// ignore_for_file: depend_on_referenced_packages, library_private_types_in_public_api
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:dart_framework/platform/process.dart';
|
|
import 'package:oto/oto/application.dart';
|
|
import 'package:oto/oto/core/data_composer.dart';
|
|
import 'package:oto/oto/core/system_runtime.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
class _ShellCall {
|
|
final String shell;
|
|
final String? workspace;
|
|
final bool printStdout;
|
|
final bool printStderr;
|
|
final Converter<List<int>, String>? decoder;
|
|
_ShellCall(this.shell, this.workspace, this.printStdout, this.printStderr,
|
|
this.decoder);
|
|
}
|
|
|
|
class _RunExecutableCall {
|
|
final String exe;
|
|
final List<String> args;
|
|
final String? workingDirectory;
|
|
_RunExecutableCall(this.exe, this.args, this.workingDirectory);
|
|
}
|
|
|
|
class _StartExecutableCall {
|
|
final String program;
|
|
final List<String> args;
|
|
final String? workingDirectory;
|
|
final ProcessStartMode mode;
|
|
_StartExecutableCall(
|
|
this.program, this.args, this.workingDirectory, this.mode);
|
|
}
|
|
|
|
class FakeProcessData extends ProcessData {
|
|
FakeProcessData({String stdoutText = ''}) : super(false, false) {
|
|
exitCode = 0;
|
|
this.stdout = StringBuffer(stdoutText);
|
|
}
|
|
|
|
@override
|
|
Future waitForExit() async {}
|
|
|
|
@override
|
|
Process get process =>
|
|
throw UnsupportedError('FakeProcessData has no real process');
|
|
}
|
|
|
|
class FakeSystemRuntime implements SystemRuntime {
|
|
final List<_ShellCall> startShellCalls = [];
|
|
final List<_ShellCall> runShellCalls = [];
|
|
final List<_RunExecutableCall> runExecutableCalls = [];
|
|
final List<_StartExecutableCall> startExecutableCalls = [];
|
|
final List<int> killedPids = [];
|
|
|
|
final bool _isWindows;
|
|
final bool _isMacOS;
|
|
final bool _isLinux;
|
|
final Map<String, String> _environment;
|
|
|
|
final String runShellStdout;
|
|
final String startShellStdout;
|
|
|
|
FakeSystemRuntime({
|
|
bool isWindows = false,
|
|
bool isMacOS = false,
|
|
bool isLinux = true,
|
|
Map<String, String> environment = const {},
|
|
this.runShellStdout = '',
|
|
this.startShellStdout = '',
|
|
}) : _isWindows = isWindows,
|
|
_isMacOS = isMacOS,
|
|
_isLinux = isLinux,
|
|
_environment = environment;
|
|
|
|
@override
|
|
bool get isWindows => _isWindows;
|
|
@override
|
|
bool get isMacOS => _isMacOS;
|
|
@override
|
|
bool get isLinux => _isLinux;
|
|
@override
|
|
Map<String, String> get environment => _environment;
|
|
|
|
@override
|
|
Future<ProcessData> startShell(
|
|
StringBuffer shell, {
|
|
String? workspace,
|
|
bool printStdout = true,
|
|
bool printStderr = true,
|
|
Converter<List<int>, String>? decoder,
|
|
LogHandler? logHandler,
|
|
}) async {
|
|
startShellCalls.add(_ShellCall(
|
|
shell.toString(), workspace, printStdout, printStderr, decoder));
|
|
return FakeProcessData(stdoutText: startShellStdout);
|
|
}
|
|
|
|
@override
|
|
Future<ProcessResult> runShell(
|
|
StringBuffer shell, {
|
|
String? workspace,
|
|
bool printStdout = true,
|
|
bool printStderr = true,
|
|
Converter<List<int>, String>? decoder,
|
|
LogHandler? logHandler,
|
|
}) async {
|
|
runShellCalls.add(_ShellCall(
|
|
shell.toString(), workspace, printStdout, printStderr, decoder));
|
|
return ProcessResult(0, 0, runShellStdout, '');
|
|
}
|
|
|
|
@override
|
|
Future<ProcessResult> runExecutable(
|
|
String exe,
|
|
List<String> args, {
|
|
String? workingDirectory,
|
|
Encoding? stdoutEncoding,
|
|
Encoding? stderrEncoding,
|
|
}) async {
|
|
runExecutableCalls.add(_RunExecutableCall(exe, args, workingDirectory));
|
|
return ProcessResult(0, 0, '', '');
|
|
}
|
|
|
|
@override
|
|
Future<Process> startExecutable(
|
|
String program,
|
|
List<String> args, {
|
|
String? workingDirectory,
|
|
ProcessStartMode mode = ProcessStartMode.normal,
|
|
}) async {
|
|
startExecutableCalls
|
|
.add(_StartExecutableCall(program, args, workingDirectory, mode));
|
|
throw UnsupportedError('FakeSystemRuntime does not return a real Process');
|
|
}
|
|
|
|
@override
|
|
bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) {
|
|
killedPids.add(pid);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
test('setUTF8 uses injected system runtime on Windows', () async {
|
|
final fake = FakeSystemRuntime(isWindows: true);
|
|
Application.instance.systemRuntime = fake;
|
|
|
|
await Application.instance.setUTF8();
|
|
|
|
expect(fake.startShellCalls, hasLength(1));
|
|
expect(fake.startShellCalls.single.shell, contains('chcp 65001'));
|
|
|
|
Application.instance.systemRuntime = const DefaultSystemRuntime();
|
|
});
|
|
|
|
test(
|
|
'DataComposerJenkins composes linux env and BuildData from runtime stdout',
|
|
() async {
|
|
const jenkinsJson = '''
|
|
{
|
|
"workspace": "/workspace",
|
|
"jenkinsHome": "/jenkins",
|
|
"jenkinsUrl": "http://jenkins/",
|
|
"buildUrl": "http://jenkins/build/1/",
|
|
"jobUrl": "http://jenkins/job/myjob/",
|
|
"jobName": "myjob",
|
|
"gitCommit": "abc123",
|
|
"buildNumber": 42,
|
|
"buildID": 42,
|
|
"buildDisplayName": "#42",
|
|
"gitBranch": "main",
|
|
"buildTag": "myjob-42"
|
|
}
|
|
''';
|
|
const buildYaml = 'property:\n workspace: .\ncommands: []\n';
|
|
final fake = FakeSystemRuntime(
|
|
isLinux: true,
|
|
runShellStdout: jenkinsJson,
|
|
startShellStdout: buildYaml,
|
|
);
|
|
|
|
final composer = DataComposerJenkins(runtime: fake);
|
|
await composer.compose(null, (msg, color) async {});
|
|
|
|
expect(fake.runShellCalls, hasLength(1));
|
|
expect(fake.startShellCalls, hasLength(1));
|
|
expect(fake.startShellCalls.single.printStdout, isFalse);
|
|
expect(composer.commonData, isNotNull);
|
|
expect(composer.commonData!.workspace, '/workspace');
|
|
expect(composer.buildYaml, buildYaml);
|
|
});
|
|
|
|
test(
|
|
'JenkinsEnvironmentCollector collects linux env and build yaml from runtime',
|
|
() async {
|
|
const jenkinsJson = '''
|
|
{
|
|
"workspace": "/workspace",
|
|
"jenkinsHome": "/jenkins",
|
|
"jenkinsUrl": "http://jenkins/",
|
|
"buildUrl": "http://jenkins/build/1/",
|
|
"jobUrl": "http://jenkins/job/myjob/",
|
|
"jobName": "myjob",
|
|
"gitCommit": "abc123",
|
|
"buildNumber": 42,
|
|
"buildID": 42,
|
|
"buildDisplayName": "#42",
|
|
"gitBranch": "main",
|
|
"buildTag": "myjob-42"
|
|
}
|
|
''';
|
|
const buildYaml = 'property:\n workspace: .\ncommands: []\n';
|
|
final fake = FakeSystemRuntime(
|
|
isLinux: true,
|
|
runShellStdout: jenkinsJson,
|
|
startShellStdout: buildYaml,
|
|
);
|
|
|
|
final collector = JenkinsEnvironmentCollector(runtime: fake);
|
|
final snapshot = await collector.collect((msg, color) async {});
|
|
|
|
expect(snapshot, isNotNull);
|
|
expect(snapshot!.variables['workspace'], '/workspace');
|
|
expect(snapshot.buildYaml, buildYaml);
|
|
expect(snapshot.rawVariablesJson, jenkinsJson);
|
|
expect(fake.runShellCalls, hasLength(1));
|
|
expect(fake.startShellCalls, hasLength(1));
|
|
expect(fake.startShellCalls.single.printStdout, isFalse);
|
|
});
|
|
|
|
test('fake system runtime records process calls', () async {
|
|
final fake = FakeSystemRuntime(
|
|
isWindows: false,
|
|
isMacOS: false,
|
|
isLinux: true,
|
|
environment: {'HOME': '/home/test'},
|
|
);
|
|
|
|
// startShell
|
|
await fake.startShell(
|
|
StringBuffer('echo hello'),
|
|
workspace: '/tmp/test',
|
|
printStdout: false,
|
|
printStderr: false,
|
|
);
|
|
expect(fake.startShellCalls, hasLength(1));
|
|
expect(fake.startShellCalls.single.shell, 'echo hello');
|
|
expect(fake.startShellCalls.single.workspace, '/tmp/test');
|
|
expect(fake.startShellCalls.single.printStdout, isFalse);
|
|
expect(fake.startShellCalls.single.printStderr, isFalse);
|
|
expect(fake.startShellCalls.single.decoder, isNull);
|
|
|
|
// runShell
|
|
await fake.runShell(StringBuffer('ls -la'), workspace: '/var/www');
|
|
expect(fake.runShellCalls, hasLength(1));
|
|
expect(fake.runShellCalls.single.shell, 'ls -la');
|
|
expect(fake.runShellCalls.single.workspace, '/var/www');
|
|
|
|
// runExecutable
|
|
await fake.runExecutable('git', ['status', '--short'],
|
|
workingDirectory: '/repo');
|
|
expect(fake.runExecutableCalls, hasLength(1));
|
|
expect(fake.runExecutableCalls.single.exe, 'git');
|
|
expect(fake.runExecutableCalls.single.args, ['status', '--short']);
|
|
expect(fake.runExecutableCalls.single.workingDirectory, '/repo');
|
|
|
|
// startExecutable (detached)
|
|
await expectLater(
|
|
fake.startExecutable('my_daemon', ['--port', '8080'],
|
|
workingDirectory: '/srv', mode: ProcessStartMode.detached),
|
|
throwsUnsupportedError,
|
|
);
|
|
expect(fake.startExecutableCalls, hasLength(1));
|
|
expect(fake.startExecutableCalls.single.program, 'my_daemon');
|
|
expect(fake.startExecutableCalls.single.args, ['--port', '8080']);
|
|
expect(fake.startExecutableCalls.single.workingDirectory, '/srv');
|
|
expect(fake.startExecutableCalls.single.mode, ProcessStartMode.detached);
|
|
|
|
// killPid
|
|
fake.killPid(12345);
|
|
fake.killPid(67890, ProcessSignal.sigkill);
|
|
expect(fake.killedPids, [12345, 67890]);
|
|
|
|
// OS flags
|
|
expect(fake.isLinux, isTrue);
|
|
expect(fake.isWindows, isFalse);
|
|
expect(fake.isMacOS, isFalse);
|
|
|
|
// environment
|
|
expect(fake.environment['HOME'], '/home/test');
|
|
});
|
|
}
|