oto/test/oto_command_runtime_test.dart

495 lines
16 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_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/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/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;
FakeRuntime({this.exitCode = 0, this.stdoutText = '', this.stderrText = ''});
@override
Future<ProcessData> start(StringBuffer shell,
{String? workspace,
bool printStdout = true,
bool printStderr = true,
Converter<List<int>, String>? decoder,
LogHandler? logHandler}) async {
startCalls.add(_StartCall(
shell.toString(), workspace, printStdout, printStderr, decoder));
return FakeProcessData(exitCode: exitCode, stdoutText: stdoutText);
}
@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('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 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'))),
);
});
}