oto/apps/runner/test/oto_application_test.dart
toki fcde95985d refactor(runner): 출력 포트 경계를 연결한다
core data composition과 Application 출력 경계에서 CLI Color 의존을 분리하고, 기존 CLI 출력 의미를 adapter 테스트로 보존하기 위해 변경한다.
2026-06-07 20:42:26 +09:00

1138 lines
29 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:oto/cli/cli_style.dart';
import 'package:oto/cli/commands/command_exe.dart';
import 'package:oto/cli/runner_output_adapter.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/core/build_result.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/core/output_port.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:test/test.dart';
void main() {
setUp(() {
Application.instance.property = {};
Application.instance.commandStates = {};
Application.instance.dataCommandMap = {};
Application.instance.context.output = const NoopRunnerOutputPort();
});
test('build returns failure instead of exiting on invalid yaml', () async {
const invalidYaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: doesNotExist
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: invalidYaml,
);
expect(result.success, isFalse);
expect(result.exitCode, 10);
expect(result.error, isNotNull);
});
test('file build returns success for print-only pipeline', () async {
const yaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isTrue);
expect(result.exitCode, 0);
});
test('file build emits build boundaries through output port', () async {
const yaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final output = _RecordingOutputPort();
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
output: output,
);
expect(result.success, isTrue);
expect(output.blocks, anyElement(contains('Build Data')));
expect(output.steps, anyElement(contains('Phase Start')));
expect(output.steps, contains('Build Successfully Complete'));
// REVIEW_REFACTOR-1: 최종 성공 배너는 기존 cyan 의미(RunnerOutputStyle.progress) 보존
final completeIdx = output.steps.indexOf('Build Successfully Complete');
expect(completeIdx, isNot(equals(-1)));
expect(output.stepStyles[completeIdx], equals(RunnerOutputStyle.progress));
});
// REVIEW_REFACTOR-1: 기존 build data, phase start, failure 출력 의미 회귀 없음
test(
'REVIEW_REFACTOR-1: build data block uses RunnerOutputStyle.accent (magenta) - no regression',
() async {
const yaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final output = _RecordingOutputPort();
await Application.instance.build(
BuildType.file,
yamlContent: yaml,
output: output,
);
// Build Data block은 RunnerOutputStyle.accent (magenta, 기존과 동일)
final hasBuildData = output.blocks.any((b) => b.contains('Build Data'));
expect(hasBuildData, isTrue);
// Phase Start는 RunnerOutputStyle.success ( green, 기존과 동일)
final phaseStartIdx = output.steps.indexOf('Phase Start: Print (hello)');
expect(phaseStartIdx, isNot(equals(-1)));
},
);
test(
'REVIEW_REFACTOR-1: failure banner uses RunnerOutputStyle.error (red) - no regression',
() async {
const invalidYaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: doesNotExist
''';
final output = _RecordingOutputPort();
final result = await Application.instance.build(
BuildType.file,
yamlContent: invalidYaml,
output: output,
);
expect(result.success, isFalse);
// Build Failed는 RunnerOutputStyle.error로 호출됨
final failIdx = output.steps.indexOf('Build Failed');
expect(failIdx, isNot(equals(-1)));
// error 스타일이 기록되었는지 확인
expect(output.stepStyles[failIdx], equals(RunnerOutputStyle.error));
},
);
test(
'file build without property workspace uses local workspace without Jenkins env',
() async {
const yaml = '''
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
logEnable: false,
);
expect(result.success, isTrue);
expect(Application.instance.property['workspace'], isNotNull);
expect(
Application.instance.property['workspace'],
Directory.current.path.replaceAll('\\', '/'),
);
},
);
test('file build can run twice in same process', () async {
const yaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final first = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
final second = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(first.success, isTrue);
expect(second.success, isTrue);
});
test('build fails with message when YAML root is not a map', () async {
const yaml = '''
- command: Print
id: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), contains('Build YAML root must be a map'));
});
test('build fails with message when property is not a map', () async {
const yaml = '''
property:
- bad
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), contains('Validate build yaml'));
});
test('build fails with message when command id is not a string', () async {
const yaml = '''
commands:
- command: Print
id: 123
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), contains('Validate command list'));
});
test('build fails with message when command type is not a string', () async {
const yaml = '''
commands:
- command: 999
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), contains('Validate command list'));
});
test(
'build fails with validation message when workflow key is not a string',
() async {
const yaml = '''
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- 123: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(
result.error.toString(),
isNot(contains('Converting object to an encodable object failed')),
);
expect(result.error.toString(), contains('workflow[0]'));
},
);
test(
'build fails with validation message when exe-handle branch is missing',
() async {
const yaml = '''
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe-handle:
id: hello
on-fail:
- exe: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(
result.error.toString(),
contains('exe-handle syntax requires on-success'),
);
},
);
// REVIEW_VALIDATE-1: exe command id type guard
test(
'build fails with validation message when exe command id is not a string',
() async {
const yaml = '''
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: 123
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), isNot(contains('is not a subtype')));
expect(result.error.toString(), contains('exe'));
},
);
// REVIEW_VALIDATE-1: wait-until-string condition type guard
test(
'build fails with validation message when wait-until-string value is not a string',
() async {
const yaml = '''
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- wait-until-string: 123
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), isNot(contains('is not a subtype')));
expect(result.error.toString(), contains('wait-until'));
},
);
// REVIEW_VALIDATE-3: nested pipeline malformed cases
test(
'build fails with validation message when exe-handle id is not a string',
() async {
const yaml = '''
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe-handle:
id: 123
on-success:
- exe: hello
on-fail:
- exe: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), isNot(contains('is not a subtype')));
expect(result.error.toString(), contains('exe-handle'));
expect(result.error.toString(), contains('id'));
},
);
// REVIEW_REVIEW_VALIDATE-1: pipeline.id validation
test(
'build fails with validation message when pipeline.id is missing',
() async {
const yaml = '''
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
workflow:
- exe: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), isNot(contains('is not a subtype')));
expect(result.error.toString(), contains('pipeline.id'));
},
);
test(
'build fails with validation message when pipeline.id is not a string',
() async {
const yaml = '''
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: 123
workflow:
- exe: hello
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), isNot(contains('is not a subtype')));
expect(result.error.toString(), contains('pipeline.id'));
},
);
test('build fails with validation message when workflow is empty', () async {
const yaml = '''
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow: []
''';
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
);
expect(result.success, isFalse);
expect(result.error.toString(), isNot(contains('Null check operator')));
expect(result.error.toString(), contains('pipeline.workflow'));
});
test(
'CommandExe exe -f handles missing file without LateInitializationError',
() async {
final missing =
'/tmp/oto_missing_${DateTime.now().microsecondsSinceEpoch}.yaml';
expect(File(missing).existsSync(), isFalse);
final command = CommandExe();
final priorExitCode = exitCode;
try {
await command.execute(['-f', missing]);
} finally {
exitCode = priorExitCode;
}
},
);
group('Application.validateYamlContent', () {
const validYaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
test('validateYamlContent returns success contract for valid yaml', () {
final result = Application.validateYamlContent(validYaml);
expect(result.valid, isTrue);
expect(result.phase, 'Validate YAML');
expect(result.message, 'YAML is valid.');
expect(result.exitCode, 0);
});
test('validateYamlContent returns build yaml failure contract', () {
const invalidBuildYaml = '''
- command: Print
id: hello
''';
final result = Application.validateYamlContent(invalidBuildYaml);
expect(result.valid, isFalse);
expect(result.phase, 'Validate build yaml');
expect(result.message, contains('Build YAML root must be a map'));
expect(result.exitCode, 10);
});
test(
'validateYamlContent returns command list failure contract for duplicate id',
() {
const duplicateIdYaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
- command: Print
id: hello
param:
message: hello again
pipeline:
id: main
workflow:
- exe: hello
''';
final result = Application.validateYamlContent(duplicateIdYaml);
expect(result.valid, isFalse);
expect(result.phase, 'Validate command list');
expect(
result.message,
contains('Duplicate command id exists: "hello"'),
);
expect(result.exitCode, 10);
},
);
test('validateYamlContent returns pipeline failure contract', () {
const invalidPipelineYaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: doesNotExist
''';
final result = Application.validateYamlContent(invalidPipelineYaml);
expect(result.valid, isFalse);
expect(result.phase, 'Validate Pipeline');
expect(result.message, contains('workflow[0]'));
expect(result.exitCode, 10);
});
test('validateYamlContent exposes stable json shape', () {
final result = Application.validateYamlContent(validYaml);
final json = result.toJson();
expect(json['schemaVersion'], 1);
expect(json['type'], 'yamlValidation');
expect(json['valid'], isTrue);
expect(json['phase'], 'Validate YAML');
expect(json['message'], 'YAML is valid.');
expect(json['exitCode'], 0);
});
test(
'validateYamlContent returns failure contract for malformed scheduler section',
() {
const malformedSchedulerYaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
scheduler: "not_a_map"
''';
final result = Application.validateYamlContent(malformedSchedulerYaml);
expect(result.valid, isFalse);
expect(result.phase, 'Validate build yaml');
expect(result.exitCode, 10);
expect(result.message, isNotEmpty);
},
);
});
group('BuildResult.toJson', () {
test('exposes success execution envelope', () {
const result = BuildResult.success();
final json = result.toJson();
expect(json['schemaVersion'], 1);
expect(json['type'], 'executionResult');
expect(json['success'], isTrue);
expect(json['exitCode'], 0);
expect(json['message'], 'Build completed successfully.');
expect(json['error'], isNull);
expect(json['stepEvents'], isEmpty);
expect(json['artifacts'], isEmpty);
});
test('exposes failure execution envelope', () {
final error = Exception('Some critical error');
final stack = StackTrace.current;
final result = BuildResult.failure(error, stack, exitCode: 15);
final json = result.toJson();
expect(json['schemaVersion'], 1);
expect(json['type'], 'executionResult');
expect(json['success'], isFalse);
expect(json['exitCode'], 15);
expect(json['message'], contains('Some critical error'));
expect(json['error'], isNotNull);
expect(json['error']['type'], '_Exception');
expect(json['error']['message'], contains('Some critical error'));
expect(json['stepEvents'], isEmpty);
});
test('exposes populated step events', () {
final events = [
StepEvent(
stepId: 0,
workflowIndex: 0,
stepType: 'exe',
event: 'started',
timestamp: '2026-05-22T05:00:00Z',
commandId: 'hello',
commandType: 'Print',
),
];
final result = BuildResult.success(stepEvents: events);
final json = result.toJson();
expect(json['stepEvents'], hasLength(1));
expect(json['stepEvents'][0]['stepId'], 0);
expect(json['stepEvents'][0]['event'], 'started');
expect(json['stepEvents'][0]['commandId'], 'hello');
expect(json['stepEvents'][0]['commandType'], 'Print');
});
test('exposes declared artifacts', () {
const result = BuildResult.success(
artifacts: [
BuildArtifact(name: 'smoke-report', path: 'dist/report.json'),
],
);
final json = result.toJson();
expect(json['artifacts'], hasLength(1));
expect(json['artifacts'][0]['name'], 'smoke-report');
expect(json['artifacts'][0]['path'], 'dist/report.json');
});
test('failure envelope preserves declared artifacts', () {
final result = BuildResult.failure(
Exception('boom'),
StackTrace.current,
artifacts: const [BuildArtifact(name: 'log', path: 'dist/build.log')],
);
final json = result.toJson();
expect(json['success'], isFalse);
expect(json['artifacts'], hasLength(1));
expect(json['artifacts'][0]['name'], 'log');
expect(json['artifacts'][0]['path'], 'dist/build.log');
});
});
group('BuildResult regression - stepEvents snapshot', () {
test(
'first build result retains stepEvents after second build execution',
() async {
const validYaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
// 1. Run first build
final result1 = await Application.instance.build(
BuildType.file,
yamlContent: validYaml,
logEnable: false,
);
expect(result1.success, isTrue);
final events1 = result1.toJson()['stepEvents'] as List;
expect(events1, isNotEmpty);
final originalEventsCount = events1.length;
// 2. Run second build (which triggers context.resetStepEvents())
final result2 = await Application.instance.build(
BuildType.file,
yamlContent: validYaml,
logEnable: false,
);
expect(result2.success, isTrue);
// 3. Assert first build result still retains its original stepEvents
final events1AfterSecondBuild = result1.toJson()['stepEvents'] as List;
expect(events1AfterSecondBuild, hasLength(originalEventsCount));
expect(events1AfterSecondBuild[0]['event'], 'started');
expect(events1AfterSecondBuild[0]['commandId'], 'hello');
},
);
});
group('logEnable false', () {
test('build with logEnable false suppresses failure stdout', () async {
const invalidYaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: doesNotExist
''';
final prints = <String>[];
final result = await runZoned(
() async {
return await Application.instance.build(
BuildType.file,
yamlContent: invalidYaml,
logEnable: false,
);
},
zoneSpecification: ZoneSpecification(
print: (self, parent, zone, line) {
prints.add(line);
},
),
);
expect(result.success, isFalse);
expect(prints, isEmpty);
});
test('build with logEnable false suppresses command stdout logs', () async {
const yaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final prints = <String>[];
final result = await runZoned(
() async {
return await Application.instance.build(
BuildType.file,
yamlContent: yaml,
logEnable: false,
);
},
zoneSpecification: ZoneSpecification(
print: (self, parent, zone, line) {
prints.add(line);
},
),
);
expect(result.success, isTrue);
expect(prints, isEmpty);
});
test('build with logEnable false suppresses output port logs', () async {
const yaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final output = _RecordingOutputPort();
final result = await Application.instance.build(
BuildType.file,
yamlContent: yaml,
logEnable: false,
output: output,
);
expect(result.success, isTrue);
expect(output.lines, isEmpty);
expect(output.blocks, isEmpty);
expect(output.steps, isEmpty);
});
});
test('scheduler build data runs without Jenkins env', () async {
final logFile = File(
'${Directory.systemTemp.path}/oto_scheduler_${DateTime.now().microsecondsSinceEpoch}.log',
);
addTearDown(() {
if (logFile.existsSync()) logFile.deleteSync();
});
const yaml = '''
scheduler:
alias: local-test
interval: 1000
enableLog: true
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final build = DataBuild.fromJson(Application.getMapFromYamlA(yaml)!);
final result = await Application.instance.build(
BuildType.scheduler,
buildData: build,
logEnable: false,
logPath: logFile.path,
);
expect(result.success, isTrue);
});
group('CommandExe JSON Mode', () {
test(
'CommandExe shouldPrintExecuteLog suppresses execute log for json',
() {
final command = CommandExe();
expect(command.shouldPrintExecuteLog(['--json']), isFalse);
expect(command.shouldPrintExecuteLog(['-f', 'file.yaml']), isTrue);
},
);
test(
'CommandExe exe -f missing file --json returns execution result json',
() async {
final missing =
'/tmp/oto_missing_${DateTime.now().microsecondsSinceEpoch}.yaml';
expect(File(missing).existsSync(), isFalse);
final prints = <String>[];
final command = CommandExe(
printString: (value) async {
prints.add(value);
},
);
final priorExitCode = exitCode;
try {
await command.execute(['-f', missing, '--json']);
} finally {
exitCode = priorExitCode;
}
expect(prints, hasLength(1));
final json = jsonDecode(prints[0]);
expect(json['schemaVersion'], 1);
expect(json['type'], 'executionResult');
expect(json['success'], isFalse);
expect(json['exitCode'], 10);
expect(json['message'], contains('There are no files in path'));
expect(json['stepEvents'], isEmpty);
},
);
test(
'actual bin exe --json output has parseable execution result json and no execute logs',
() async {
final tempFile = File(
'${Directory.systemTemp.path}/oto_temp_${DateTime.now().microsecondsSinceEpoch}.yaml',
);
const yamlContent = '''
property:
workspace: .
commands:
- command: Print
id: hello
param:
message: hi
pipeline:
id: main
workflow:
- exe: hello
''';
tempFile.writeAsStringSync(yamlContent);
try {
final result = await Process.run('dart', [
'run',
'bin/main.dart',
'exe',
'-f',
tempFile.path,
'--json',
]);
expect(result.exitCode, 0);
final stdoutStr = result.stdout.toString().trim();
expect(stdoutStr, isNotEmpty);
final json = jsonDecode(stdoutStr);
expect(json['schemaVersion'], 1);
expect(json['type'], 'executionResult');
expect(json['success'], isTrue);
expect(json['exitCode'], 0);
expect(json['stepEvents'], isA<List>());
expect(json['stepEvents'].length, greaterThanOrEqualTo(2));
expect(json['stepEvents'][0]['type'], 'stepEvent');
expect(json['stepEvents'][0]['event'], 'started');
expect(json['stepEvents'][0]['commandId'], 'hello');
expect(json['stepEvents'].last['event'], 'completed');
expect(json['stepEvents'].last['commandId'], 'hello');
expect(stdoutStr, isNot(contains('Execute command:')));
expect(stdoutStr, isNot(contains('hi')));
} finally {
if (tempFile.existsSync()) {
tempFile.deleteSync();
}
}
},
);
});
group('CliRunnerOutputPort style→color mapping', () {
// Reaching private _colorFor via a public call and asserting the emitted output.
test('progress style emits via printer (verifies mapping chain)', () async {
final actualColors = <Color?>[];
final adapterWithCapture = CliRunnerOutputPort(
enabled: () => true,
leadingPrefix: () => '',
println: (value, {color, background}) async {
actualColors.add(color);
},
printString: (value, {color, background}) async {
actualColors.add(color);
},
);
await adapterWithCapture.line(
'test line',
style: RunnerOutputStyle.progress,
);
await adapterWithCapture.block(
'test block',
style: RunnerOutputStyle.progress,
);
await adapterWithCapture.buildStep(
'test step',
style: RunnerOutputStyle.progress,
);
// progress → cyan 매핑 검증: line/println/adapter output에서 cyan이 기록되어야 함
expect(actualColors, contains(Color.cyan));
});
test('all styles map to expected colors through adapter', () async {
for (var style in RunnerOutputStyle.values) {
// Each call records exactly one color.
final single = <Color?>[];
final tempAdapter = CliRunnerOutputPort(
enabled: () => true,
leadingPrefix: () => '',
println: (value, {color, background}) async {
single.add(color);
},
printString: (value, {color, background}) async {
single.add(color);
},
);
await tempAdapter.line('x', style: style);
await tempAdapter.block('x', style: style);
await tempAdapter.buildStep('x', style: style);
// 각 스타일은 null이 아닌 유색 매핑이어야 함 (normal 제외)
if (style != RunnerOutputStyle.normal) {
expect(
single,
everyElement(isNotNull),
reason: 'style $style must not be null',
);
}
}
// 특정 style→color 매핑 검증
final progressAdapter = CliRunnerOutputPort(
enabled: () => true,
leadingPrefix: () => '',
println: (value, {color, background}) async {},
printString: (value, {color, background}) async {
if (value.contains('test')) expect(color, equals(Color.cyan));
},
);
await progressAdapter.buildStep(
'test',
style: RunnerOutputStyle.progress,
);
final successAdapter = CliRunnerOutputPort(
enabled: () => true,
leadingPrefix: () => '',
println: (value, {color, background}) async {},
printString: (value, {color, background}) async {
if (value.contains('test2')) expect(color, equals(Color.green));
},
);
await successAdapter.buildStep('test2', style: RunnerOutputStyle.success);
});
});
}
class _RecordingOutputPort implements RunnerOutputPort {
final List<String> lines = [];
final List<String> blocks = [];
final List<String> steps = [];
final List<RunnerOutputStyle> stepStyles = [];
@override
Future<void> line(
String message, {
RunnerOutputStyle style = RunnerOutputStyle.normal,
}) async {
lines.add(message);
}
@override
Future<void> block(
String message, {
RunnerOutputStyle style = RunnerOutputStyle.normal,
}) async {
blocks.add(message);
}
@override
Future<void> buildStep(
String name, {
RunnerOutputStyle style = RunnerOutputStyle.success,
}) async {
steps.add(name);
stepStyles.add(style);
}
}