// 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/output_port.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, String>? decoder; _ShellCall( this.shell, this.workspace, this.printStdout, this.printStderr, this.decoder, ); } class _RunExecutableCall { final String exe; final List args; final String? workingDirectory; _RunExecutableCall(this.exe, this.args, this.workingDirectory); } class _StartExecutableCall { final String program; final List 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 killedPids = []; final bool _isWindows; final bool _isMacOS; final bool _isLinux; final Map _environment; final String runShellStdout; final String startShellStdout; FakeSystemRuntime({ bool isWindows = false, bool isMacOS = false, bool isLinux = true, Map 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 get environment => _environment; @override Future startShell( StringBuffer shell, { String? workspace, bool printStdout = true, bool printStderr = true, Converter, String>? decoder, LogHandler? logHandler, }) async { startShellCalls.add( _ShellCall( shell.toString(), workspace, printStdout, printStderr, decoder, ), ); return FakeProcessData(stdoutText: startShellStdout); } @override Future runShell( StringBuffer shell, { String? workspace, bool printStdout = true, bool printStderr = true, Converter, String>? decoder, LogHandler? logHandler, }) async { runShellCalls.add( _ShellCall( shell.toString(), workspace, printStdout, printStderr, decoder, ), ); return ProcessResult(0, 0, runShellStdout, ''); } @override Future runExecutable( String exe, List args, { String? workingDirectory, Encoding? stdoutEncoding, Encoding? stderrEncoding, }) async { runExecutableCalls.add(_RunExecutableCall(exe, args, workingDirectory)); return ProcessResult(0, 0, '', ''); } @override Future startExecutable( String program, List 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); final output = _RecordingOutputPort(); await composer.compose(null, output); 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); expect(output.steps.single.message, 'Jenkins Variables'); expect(output.steps.single.style, RunnerOutputStyle.success); }, ); 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(const NoopRunnerOutputPort()); 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( 'JenkinsEnvironmentCollector emits unsupported OS through output port', () async { final fake = FakeSystemRuntime( isWindows: false, isMacOS: false, isLinux: false, ); final output = _RecordingOutputPort(); final collector = JenkinsEnvironmentCollector(runtime: fake); final snapshot = await collector.collect(output); expect(snapshot, isNull); expect(output.steps.single.message, contains('is not Mac/Windows/Linux')); expect(output.steps.single.style, RunnerOutputStyle.error); }, ); 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'); }); } class _OutputRecord { final String message; final RunnerOutputStyle style; _OutputRecord(this.message, this.style); } class _RecordingOutputPort implements RunnerOutputPort { final List<_OutputRecord> lines = []; final List<_OutputRecord> blocks = []; final List<_OutputRecord> steps = []; @override Future line( String message, { RunnerOutputStyle style = RunnerOutputStyle.normal, }) async { lines.add(_OutputRecord(message, style)); } @override Future block( String message, { RunnerOutputStyle style = RunnerOutputStyle.normal, }) async { blocks.add(_OutputRecord(message, style)); } @override Future buildStep( String name, { RunnerOutputStyle style = RunnerOutputStyle.success, }) async { steps.add(_OutputRecord(name, style)); } }