독립 control plane 구성을 위해 기존 Dart CLI/runtime을 runner 앱 경계로 옮기고, 후속 client/core/root 작업을 같은 마일스톤 task group에 연결한다.
693 lines
22 KiB
Dart
693 lines
22 KiB
Dart
// ignore_for_file: depend_on_referenced_packages, library_private_types_in_public_api
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:dart_framework/platform/process.dart';
|
|
import 'package:oto/oto/application.dart';
|
|
import 'package:oto/oto/commands/build/build_flutter.dart';
|
|
import 'package:oto/oto/commands/build/build_ios.dart';
|
|
import 'package:oto/oto/commands/build/build_msbuild.dart';
|
|
import 'package:oto/oto/commands/command_runtime.dart';
|
|
// ignore: implementation_imports
|
|
import 'package:dart_framework/utils/path.dart' show FileType;
|
|
import 'package:oto/oto/commands/ftp/download.dart';
|
|
import 'package:oto/oto/commands/ftp/ftp.dart';
|
|
import 'package:oto/oto/commands/git/git.dart';
|
|
import 'package:oto/oto/commands/git/git_hub.dart';
|
|
import 'package:oto/oto/commands/git/git_remote.dart';
|
|
import 'package:oto/oto/commands/infra/smb_authorize.dart';
|
|
import 'package:oto/oto/commands/process/process.dart';
|
|
import 'package:oto/oto/commands/shell/shell.dart';
|
|
import 'package:oto/oto/commands/util/execute_app.dart';
|
|
import 'package:oto/oto/commands/util/json.dart';
|
|
import 'package:oto/oto/core/execution_context.dart';
|
|
import 'package:oto/oto/data/command_data.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
class _StartCall {
|
|
final String shell;
|
|
final String? workspace;
|
|
final bool printStdout;
|
|
final bool printStderr;
|
|
final Converter<List<int>, String>? decoder;
|
|
_StartCall(this.shell, this.workspace, this.printStdout, this.printStderr,
|
|
this.decoder);
|
|
}
|
|
|
|
class _RunCall {
|
|
final String shell;
|
|
final String? workspace;
|
|
_RunCall(this.shell, this.workspace);
|
|
}
|
|
|
|
class _DetachedCall {
|
|
final String shell;
|
|
final String workspace;
|
|
_DetachedCall(this.shell, this.workspace);
|
|
}
|
|
|
|
class _ExecutableCall {
|
|
final String exe;
|
|
final List<String> args;
|
|
final String? workspace;
|
|
final bool? printStdout;
|
|
final bool? printStderr;
|
|
_ExecutableCall(
|
|
this.exe, this.args, this.workspace, this.printStdout, this.printStderr);
|
|
}
|
|
|
|
class FakeProcessData extends ProcessData {
|
|
FakeProcessData(
|
|
{int exitCode = 0, String stdoutText = '', String stderrText = ''})
|
|
: super(false, false) {
|
|
this.exitCode = exitCode;
|
|
this.stdout = StringBuffer(stdoutText);
|
|
this.stderr = StringBuffer(stderrText);
|
|
}
|
|
|
|
@override
|
|
Future waitForExit() async {}
|
|
|
|
@override
|
|
Process get process =>
|
|
throw UnsupportedError('FakeProcessData has no real process');
|
|
}
|
|
|
|
class FakeRuntime implements CommandRuntime {
|
|
final List<_StartCall> startCalls = [];
|
|
final List<_RunCall> runCalls = [];
|
|
final List<_DetachedCall> detachedCalls = [];
|
|
final List<_ExecutableCall> executableCalls = [];
|
|
int exitCode;
|
|
String stdoutText;
|
|
String stderrText;
|
|
final List<int> startExitCodes;
|
|
|
|
FakeRuntime(
|
|
{this.exitCode = 0,
|
|
this.stdoutText = '',
|
|
this.stderrText = '',
|
|
this.startExitCodes = const []});
|
|
|
|
@override
|
|
Future<ProcessData> start(StringBuffer shell,
|
|
{String? workspace,
|
|
bool printStdout = true,
|
|
bool printStderr = true,
|
|
Converter<List<int>, String>? decoder,
|
|
LogHandler? logHandler}) async {
|
|
final startExitCode = startCalls.length < startExitCodes.length
|
|
? startExitCodes[startCalls.length]
|
|
: exitCode;
|
|
startCalls.add(_StartCall(
|
|
shell.toString(), workspace, printStdout, printStderr, decoder));
|
|
return FakeProcessData(
|
|
exitCode: startExitCode,
|
|
stdoutText: stdoutText,
|
|
stderrText: stderrText);
|
|
}
|
|
|
|
@override
|
|
Future<ProcessResult> run(StringBuffer shell,
|
|
{String? workspace,
|
|
bool printStdout = true,
|
|
bool printStderr = true,
|
|
Converter<List<int>, String>? decoder,
|
|
LogHandler? logHandler}) async {
|
|
runCalls.add(_RunCall(shell.toString(), workspace));
|
|
return ProcessResult(0, exitCode, stdoutText, stderrText);
|
|
}
|
|
|
|
@override
|
|
Future<ProcessResult> runExecutable(String exe, List<String> args,
|
|
{String? workspace,
|
|
bool? printStdout,
|
|
bool? printStderr,
|
|
LogHandler? logHandler}) async {
|
|
executableCalls
|
|
.add(_ExecutableCall(exe, args, workspace, printStdout, printStderr));
|
|
return ProcessResult(0, exitCode, stdoutText, stderrText);
|
|
}
|
|
|
|
@override
|
|
Future<void> startDetached(StringBuffer shell,
|
|
{required String workspace}) async {
|
|
detachedCalls.add(_DetachedCall(shell.toString(), workspace));
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
setUp(() {
|
|
Application.instance.context = ExecutionContext();
|
|
Application.instance.property['workspace'] = '/tmp/oto_runtime_test';
|
|
});
|
|
|
|
DataCommand commandWith(Map<String, dynamic> param) {
|
|
return DataCommand.fromJson({
|
|
'command': 'Shell',
|
|
'id': 'fake',
|
|
'param': param,
|
|
});
|
|
}
|
|
|
|
test('shell command delegates process start to runtime', () async {
|
|
final fake = FakeRuntime();
|
|
final shell = Shell()..runtime = fake;
|
|
final command = commandWith({
|
|
'commands': ['echo hello'],
|
|
});
|
|
|
|
await shell.execute(command);
|
|
|
|
expect(fake.startCalls, hasLength(1));
|
|
expect(fake.startCalls.single.shell, contains('echo hello'));
|
|
expect(fake.startCalls.single.shell, contains('/tmp/oto_runtime_test'));
|
|
expect(fake.runCalls, isEmpty);
|
|
expect(fake.detachedCalls, isEmpty);
|
|
});
|
|
|
|
test('git command delegates command buffer to runtime', () async {
|
|
final fake = FakeRuntime();
|
|
final git = Git()..runtime = fake;
|
|
final command = commandWith({
|
|
'commands': ['status', 'fetch origin'],
|
|
});
|
|
|
|
await git.execute(command);
|
|
|
|
expect(fake.startCalls, hasLength(1));
|
|
final buffer = fake.startCalls.single.shell;
|
|
expect(buffer, contains('git status'));
|
|
expect(buffer, contains('git fetch origin'));
|
|
expect(fake.detachedCalls, isEmpty);
|
|
});
|
|
|
|
test('process run delegates detached start to runtime', () async {
|
|
final fake = FakeRuntime();
|
|
final processRun = ProcessRun()..runtime = fake;
|
|
final command = commandWith({
|
|
'executable': 'my_server',
|
|
'param': '--port 8080',
|
|
'workspace': '/tmp/oto_runtime_test',
|
|
});
|
|
|
|
await processRun.execute(command);
|
|
|
|
expect(fake.detachedCalls, hasLength(1));
|
|
expect(fake.detachedCalls.single.workspace, '/tmp/oto_runtime_test');
|
|
expect(fake.detachedCalls.single.shell, contains('my_server'));
|
|
expect(fake.detachedCalls.single.shell, contains('--port 8080'));
|
|
expect(fake.detachedCalls.single.shell, contains('dontKillMe'));
|
|
expect(fake.startCalls, isEmpty);
|
|
});
|
|
|
|
test('runtime captures start options', () async {
|
|
final fake = FakeRuntime();
|
|
final shell = StringBuffer('echo hello');
|
|
|
|
await fake.start(shell,
|
|
workspace: '/tmp/test', printStdout: false, printStderr: false);
|
|
|
|
expect(fake.startCalls, hasLength(1));
|
|
expect(fake.startCalls.single.workspace, '/tmp/test');
|
|
expect(fake.startCalls.single.printStdout, isFalse);
|
|
expect(fake.startCalls.single.printStderr, isFalse);
|
|
});
|
|
|
|
test('runtime captures run workspace', () async {
|
|
final fake = FakeRuntime();
|
|
final shell = StringBuffer('echo test');
|
|
|
|
await fake.run(shell, workspace: '/some/workspace');
|
|
|
|
expect(fake.runCalls, hasLength(1));
|
|
expect(fake.runCalls.single.workspace, '/some/workspace');
|
|
expect(fake.runCalls.single.shell, contains('echo test'));
|
|
});
|
|
|
|
test('build_msbuild delegates start with decoder and printStderr=false',
|
|
() async {
|
|
final fake = FakeRuntime();
|
|
final msbuild = BuildMSBuild()..runtime = fake;
|
|
final command = commandWith({
|
|
'projectFile': 'MyApp.sln',
|
|
'type': 'Build',
|
|
'config': 'Release',
|
|
});
|
|
|
|
await msbuild.execute(command);
|
|
|
|
expect(fake.startCalls, hasLength(1));
|
|
expect(fake.startCalls.single.printStderr, isFalse);
|
|
expect(fake.startCalls.single.decoder, isNotNull);
|
|
expect(fake.startCalls.single.shell, contains('msbuild MyApp.sln'));
|
|
});
|
|
|
|
test('github_pull_request_list delegates start with decoder', () async {
|
|
final fake = FakeRuntime(stdoutText: '[]');
|
|
final prList = GitHubPullRequestList()..runtime = fake;
|
|
final command = commandWith({
|
|
'keys': ['number', 'title'],
|
|
'targetKey': 'title',
|
|
'targetValue': 'my-pr',
|
|
'setValue': '<@property.pr>',
|
|
});
|
|
|
|
await prList.execute(command);
|
|
|
|
expect(fake.startCalls, hasLength(1));
|
|
expect(fake.startCalls.single.decoder, isNotNull);
|
|
expect(fake.startCalls.single.shell, contains('gh pr list'));
|
|
});
|
|
|
|
test('execute_app delegates run to runtime with workspace', () async {
|
|
final fake = FakeRuntime();
|
|
final app = ExecuteApp()..runtime = fake;
|
|
final command = commandWith({
|
|
'executable': 'MyApp.exe',
|
|
'desktopServicePath': '/some/desktop',
|
|
});
|
|
|
|
await app.execute(command);
|
|
|
|
expect(fake.runCalls, hasLength(1));
|
|
expect(fake.runCalls.single.workspace, '/some/desktop');
|
|
expect(fake.runCalls.single.shell, contains('MyApp.exe'));
|
|
});
|
|
|
|
test('BuildiOS.initializeFastlane delegates fastlane init run to runtime',
|
|
() async {
|
|
final fake = FakeRuntime();
|
|
final tmp = await Directory.systemTemp.createTemp('oto_ios_test_');
|
|
try {
|
|
final apiKeyFile = File('${tmp.path}/key.json');
|
|
await apiKeyFile.writeAsString(jsonEncode({
|
|
'key_id': 'KID',
|
|
'issuer_id': 'IID',
|
|
'key': 'content',
|
|
}));
|
|
await Directory('${tmp.path}/fastlane').create();
|
|
|
|
await BuildiOS.initializeFastlane(
|
|
tmp.path, apiKeyFile.path, 'com.test.app',
|
|
runtime: fake);
|
|
|
|
expect(fake.runCalls, hasLength(1));
|
|
expect(fake.runCalls.single.shell, contains('fastlane init'));
|
|
} finally {
|
|
await tmp.delete(recursive: true);
|
|
}
|
|
});
|
|
|
|
test('BuildiOS.execute delegates start to runtime via ProcessData.exitCode',
|
|
() async {
|
|
final fake = FakeRuntime();
|
|
final tmp = await Directory.systemTemp.createTemp('oto_ios_main_');
|
|
try {
|
|
await Directory('${tmp.path}/MyApp.xcodeproj').create();
|
|
await File('${tmp.path}/MyApp.xcodeproj/project.pbxproj')
|
|
.writeAsString('');
|
|
|
|
final ios = BuildiOS()..runtime = fake;
|
|
final command = commandWith({
|
|
'xcodeProjectFilePath': 'MyApp.xcodeproj',
|
|
'scheme': 'MyApp',
|
|
'version': '1.0.0',
|
|
'buildNumber': '100',
|
|
'workspace': tmp.path,
|
|
});
|
|
|
|
await ios.execute(command);
|
|
|
|
expect(fake.startCalls, hasLength(1));
|
|
expect(fake.startCalls.single.shell, contains('xcodebuild'));
|
|
expect(fake.startCalls.single.printStderr, isFalse);
|
|
} finally {
|
|
await tmp.delete(recursive: true);
|
|
}
|
|
});
|
|
|
|
test('BuildiOS.execute shell includes derivedDataPath and macPassword',
|
|
() async {
|
|
final fake = FakeRuntime();
|
|
final tmp = await Directory.systemTemp.createTemp('oto_ios_opts_');
|
|
try {
|
|
await Directory('${tmp.path}/MyApp.xcodeproj').create();
|
|
await File('${tmp.path}/MyApp.xcodeproj/project.pbxproj')
|
|
.writeAsString('');
|
|
|
|
final ios = BuildiOS()..runtime = fake;
|
|
final command = commandWith({
|
|
'xcodeProjectFilePath': 'MyApp.xcodeproj',
|
|
'scheme': 'MyApp',
|
|
'version': '1.0.0',
|
|
'buildNumber': '100',
|
|
'workspace': tmp.path,
|
|
'derivedDataPath': '/tmp/derived',
|
|
'macPassword': 'testpwd',
|
|
});
|
|
|
|
await ios.execute(command);
|
|
|
|
final shell = fake.startCalls.single.shell;
|
|
expect(shell, contains('-derivedDataPath /tmp/derived'));
|
|
expect(shell, contains('security unlock-keychain -ptestpwd'));
|
|
} finally {
|
|
await tmp.delete(recursive: true);
|
|
}
|
|
});
|
|
|
|
test('BuildFlutter uses resolved workspace, flavor, and platform in shell',
|
|
() async {
|
|
final fake = FakeRuntime();
|
|
final buildFlutter = BuildFlutter()..runtime = fake;
|
|
|
|
Application.instance.property['workspace'] = '/custom/workspace';
|
|
Application.instance.property['flavor'] = 'production';
|
|
Application.instance.property['platform'] = 'apk';
|
|
|
|
final command = DataCommand.fromJson({
|
|
'command': 'BuildFlutter',
|
|
'id': 'build-flutter-test',
|
|
'param': {
|
|
'workspace': '<!property.workspace>',
|
|
'platform': '<!property.platform>',
|
|
'flavor': '<!property.flavor>',
|
|
'additional': '--release',
|
|
'executeBuildRunner': false,
|
|
},
|
|
});
|
|
|
|
await buildFlutter.execute(command);
|
|
|
|
expect(fake.startCalls, hasLength(1));
|
|
final shell = fake.startCalls.single.shell;
|
|
expect(shell, contains('cd /custom/workspace'));
|
|
expect(shell, contains('flutter build apk'));
|
|
expect(shell, contains('--flavor production'));
|
|
expect(shell, contains('--release'));
|
|
});
|
|
|
|
test('CodeSign includes --entitlements only when entitlementPath is set',
|
|
() async {
|
|
final fakeWith = FakeRuntime();
|
|
final fakeWithout = FakeRuntime();
|
|
|
|
final csWithEntitlement = CodeSign()..runtime = fakeWith;
|
|
final csWithout = CodeSign()..runtime = fakeWithout;
|
|
|
|
await csWithEntitlement.execute(commandWith({
|
|
'certificationName': 'Apple Dev',
|
|
'appPath': '/path/App.app',
|
|
'entitlementPath': '/path/app.entitlements',
|
|
}));
|
|
await csWithout.execute(commandWith({
|
|
'certificationName': 'Apple Dev',
|
|
'appPath': '/path/App.app',
|
|
}));
|
|
|
|
expect(fakeWith.startCalls.single.shell, contains('--entitlements'));
|
|
expect(
|
|
fakeWithout.startCalls.single.shell, isNot(contains('--entitlements')));
|
|
});
|
|
|
|
test('runtime captures executable arguments', () async {
|
|
final fake = FakeRuntime(exitCode: 0, stdoutText: 'done');
|
|
|
|
final result = await fake.runExecutable('my_exe', ['--flag', 'value'],
|
|
workspace: '/tmp', printStdout: false, printStderr: false);
|
|
|
|
expect(fake.executableCalls, hasLength(1));
|
|
expect(fake.executableCalls.single.exe, 'my_exe');
|
|
expect(fake.executableCalls.single.args, containsAll(['--flag', 'value']));
|
|
expect(fake.executableCalls.single.workspace, '/tmp');
|
|
expect(fake.executableCalls.single.printStdout, isFalse);
|
|
expect(fake.executableCalls.single.printStderr, isFalse);
|
|
expect(result.exitCode, 0);
|
|
expect(result.stdout, 'done');
|
|
});
|
|
|
|
test('sftp list parses ls -l stdout into FileType map', () async {
|
|
const lsOutput = 'total 8\n'
|
|
'drwxr-xr-x 2 user group 4096 Jan 1 00:00 subdir\n'
|
|
'-rw-r--r-- 1 user group 1234 Jan 1 00:00 file.txt\n';
|
|
final fake = FakeRuntime(stdoutText: lsOutput);
|
|
final sftp =
|
|
SFTP('test.host', user: 'user', password: 'pass', runtime: fake);
|
|
|
|
final result = await sftp.list('/remote');
|
|
|
|
expect(fake.executableCalls, hasLength(1));
|
|
expect(fake.executableCalls.single.exe, 'sshpass');
|
|
expect(fake.executableCalls.single.args,
|
|
containsAll(['ssh', 'ls -l /remote']));
|
|
expect(result['/remote/subdir'], FileType.directory);
|
|
expect(result['/remote/file.txt'], FileType.file);
|
|
});
|
|
|
|
test('sftp list propagates non-zero exit code as exception', () async {
|
|
final fake = FakeRuntime(exitCode: 1, stderrText: 'ls failed');
|
|
final sftp =
|
|
SFTP('test.host', user: 'user', password: 'pass', runtime: fake);
|
|
|
|
await expectLater(
|
|
sftp.list('/remote'),
|
|
throwsA(predicate((e) => e.toString().contains('ls failed'))),
|
|
);
|
|
});
|
|
|
|
test('sftp delete calls sshpass ssh rm -rf', () async {
|
|
final fake = FakeRuntime();
|
|
final sftp =
|
|
SFTP('test.host', user: 'user', password: 'pass', runtime: fake);
|
|
|
|
await sftp.delete(['/remote/old.txt']);
|
|
|
|
expect(fake.executableCalls, hasLength(1));
|
|
expect(fake.executableCalls.single.exe, 'sshpass');
|
|
final args = fake.executableCalls.single.args;
|
|
expect(args, containsAll(['ssh', 'rm -rf /remote/old.txt']));
|
|
});
|
|
|
|
test('smb_auth delegates cmd net use to runtime with domain user password',
|
|
() async {
|
|
final fake = FakeRuntime(exitCode: 0);
|
|
final smb = SMBAuth()..runtime = fake;
|
|
final command = commandWith({
|
|
'domain': 'fileserver',
|
|
'id': 'testuser',
|
|
'password': 'secret',
|
|
});
|
|
|
|
await smb.execute(command);
|
|
|
|
expect(fake.executableCalls, hasLength(1));
|
|
expect(fake.executableCalls.single.exe, 'cmd');
|
|
final args = fake.executableCalls.single.args;
|
|
expect(
|
|
args,
|
|
containsAll(
|
|
['/C', 'net', 'use', r'\\fileserver', '/user:testuser', 'secret']));
|
|
});
|
|
|
|
test('download maps local relative path to remote path for sftp', () async {
|
|
const lsOutput = 'total 4\n'
|
|
'-rw-r--r-- 1 user group 1234 Jan 1 00:00 app.json\n';
|
|
final fake = FakeRuntime(stdoutText: lsOutput);
|
|
final download = Download()..runtime = fake;
|
|
final command = DataCommand.fromJson({
|
|
'command': 'Download',
|
|
'id': 'download-config',
|
|
'param': {
|
|
'host': 'test.host',
|
|
'sftp': true,
|
|
'port': 22,
|
|
'user': 'user',
|
|
'password': 'pass',
|
|
'map': {
|
|
'config/app.json': '/remote/config/app.json',
|
|
},
|
|
},
|
|
});
|
|
|
|
await download.execute(command);
|
|
|
|
expect(fake.executableCalls, hasLength(2));
|
|
expect(fake.executableCalls.first.args,
|
|
containsAll(['ssh', 'ls -l /remote/config']));
|
|
|
|
final scpArgs = fake.executableCalls.last.args;
|
|
expect(scpArgs, containsAll(['scp', '-P', '22']));
|
|
expect(scpArgs, contains('user@test.host:/remote/config/app.json'));
|
|
expect(scpArgs, contains('/tmp/oto_runtime_test/config/app.json'));
|
|
});
|
|
|
|
test('sftp download failure propagates non-zero exit code as exception',
|
|
() async {
|
|
final fake = FakeRuntime(exitCode: 1, stderrText: 'ssh: connect failed');
|
|
final download = Download()..runtime = fake;
|
|
final command = DataCommand.fromJson({
|
|
'command': 'Download',
|
|
'id': 'download-fail',
|
|
'param': {
|
|
'host': 'test.host',
|
|
'sftp': true,
|
|
'port': 22,
|
|
'user': 'user',
|
|
'password': 'pass',
|
|
'map': {
|
|
'config/app.json': '/remote/config/app.json',
|
|
},
|
|
},
|
|
});
|
|
|
|
await expectLater(
|
|
download.execute(command),
|
|
throwsA(predicate((e) => e.toString().contains('ssh: connect failed'))),
|
|
);
|
|
});
|
|
|
|
test('git_pull propagates exit code 1 as failure', () async {
|
|
final fake = FakeRuntime(exitCode: 1, stderrText: 'remote rejected');
|
|
final gitPull = GitPull()..runtime = fake;
|
|
final command = commandWith({'branch': 'main'});
|
|
|
|
await expectLater(
|
|
gitPull.execute(command),
|
|
throwsA(predicate((e) => e.toString().contains('remote rejected'))),
|
|
);
|
|
});
|
|
|
|
test('git_push propagates exit code 1 as failure', () async {
|
|
final fake = FakeRuntime(exitCode: 1, stderrText: 'remote rejected');
|
|
final gitPush = GitPush()..runtime = fake;
|
|
final command = commandWith({'branch': 'main'});
|
|
|
|
await expectLater(
|
|
gitPush.execute(command),
|
|
throwsA(predicate((e) => e.toString().contains('remote rejected'))),
|
|
);
|
|
});
|
|
|
|
test('git_checkout uses fake ProcessData exitCode for existing branch',
|
|
() async {
|
|
final fake = FakeRuntime(startExitCodes: [128, 0]);
|
|
final gitCheckout = GitCheckout()..runtime = fake;
|
|
final command = commandWith({'branch': 'develop', 'executePull': false});
|
|
|
|
await gitCheckout.execute(command);
|
|
|
|
expect(fake.startCalls, hasLength(2));
|
|
expect(
|
|
fake.startCalls.first.shell, contains('git checkout origin/develop'));
|
|
expect(fake.startCalls.first.shell, contains('git checkout -b develop'));
|
|
expect(fake.startCalls.last.shell, contains('git checkout develop'));
|
|
expect(fake.startCalls.last.shell, isNot(contains('git pull origin')));
|
|
});
|
|
|
|
test('json_reader array path stores value without debug print', () async {
|
|
final printed = <String>[];
|
|
final reader = JsonReader();
|
|
final command = DataCommand.fromJson({
|
|
'command': 'JsonReader',
|
|
'id': 'json-reader',
|
|
'param': {
|
|
'json': {
|
|
'items': [
|
|
{'name': 'alpha'}
|
|
],
|
|
},
|
|
'pair': [
|
|
{'from': 'items[0].name', 'setValue': '<@property.itemName>'}
|
|
],
|
|
},
|
|
});
|
|
|
|
await runZoned(
|
|
() => reader.execute(command),
|
|
zoneSpecification: ZoneSpecification(
|
|
print: (self, parent, zone, line) => printed.add(line),
|
|
),
|
|
);
|
|
|
|
expect(Application.instance.property['itemName'], 'alpha');
|
|
expect(printed, isNot(contains('1')));
|
|
});
|
|
|
|
test('json_writer_file updates file without debug print', () async {
|
|
final printed = <String>[];
|
|
final tmp = await Directory.systemTemp.createTemp('oto_json_writer_');
|
|
try {
|
|
final file = File('${tmp.path}/data.json');
|
|
await file.writeAsString(jsonEncode({
|
|
'nested': {'old': 'value'}
|
|
}));
|
|
final writer = JsonWriterFile();
|
|
final command = DataCommand.fromJson({
|
|
'command': 'JsonWriterFile',
|
|
'id': 'json-writer',
|
|
'param': {
|
|
'path': file.path,
|
|
'pair': [
|
|
{'from': 'nested.newValue', 'value': '42'}
|
|
],
|
|
},
|
|
});
|
|
|
|
await runZoned(
|
|
() => writer.execute(command),
|
|
zoneSpecification: ZoneSpecification(
|
|
print: (self, parent, zone, line) => printed.add(line),
|
|
),
|
|
);
|
|
|
|
final json = jsonDecode(await file.readAsString()) as Map;
|
|
expect((json['nested'] as Map)['newValue'], '42');
|
|
expect(printed, isEmpty);
|
|
} finally {
|
|
await tmp.delete(recursive: true);
|
|
}
|
|
});
|
|
|
|
test('github_pull_request_create treats existing-pr message as success',
|
|
() async {
|
|
const existingUrl = 'https://github.com/org/repo/pull/123';
|
|
final fake = FakeRuntime(
|
|
exitCode: 1,
|
|
stderrText:
|
|
'GraphQL: a pull request for branch feature already exists:\n$existingUrl',
|
|
);
|
|
final prCreate = GitHubPullRequestCreate()..runtime = fake;
|
|
final command = commandWith({
|
|
'title': 'My PR',
|
|
'body': 'Description',
|
|
'branch': 'main',
|
|
'setURL': '<@property.prUrl>',
|
|
'setNumber': '<@property.prNumber>',
|
|
});
|
|
|
|
await prCreate.execute(command);
|
|
|
|
expect(Application.instance.property['prUrl'], existingUrl);
|
|
expect(Application.instance.property['prNumber'], '123');
|
|
});
|
|
|
|
test('github_pull_request_create propagates unrelated exit code 1', () async {
|
|
final fake = FakeRuntime(exitCode: 1, stderrText: 'authentication failed');
|
|
final prCreate = GitHubPullRequestCreate()..runtime = fake;
|
|
final command = commandWith({
|
|
'title': 'My PR',
|
|
'body': 'Description',
|
|
'branch': 'main',
|
|
'setURL': '<@property.prUrl>',
|
|
'setNumber': '<@property.prNumber>',
|
|
});
|
|
|
|
await expectLater(
|
|
prCreate.execute(command),
|
|
throwsA(predicate((e) => e.toString().contains('authentication failed'))),
|
|
);
|
|
});
|
|
}
|