444 lines
13 KiB
Dart
444 lines
13 KiB
Dart
import 'package:oto/oto/application.dart';
|
|
import 'package:oto/oto/commands/command.dart';
|
|
import 'package:oto/oto/commands/command_registry.dart';
|
|
import 'package:oto/oto/core/execution_context.dart';
|
|
import 'package:oto/oto/data/command_data.dart';
|
|
import 'package:oto/oto/pipeline/pipeline.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
void main() {
|
|
setUp(() {
|
|
Application.instance.property = {};
|
|
Application.instance.commandStates = {};
|
|
Application.instance.dataCommandMap = {};
|
|
});
|
|
|
|
test('parses minimal build yaml into DataBuild', () {
|
|
const yaml = '''
|
|
property:
|
|
workspace: .
|
|
commands:
|
|
- command: Print
|
|
id: hello
|
|
param: hi
|
|
pipeline:
|
|
id: main
|
|
workflow:
|
|
- exe: hello
|
|
''';
|
|
final map = Application.getMapFromYamlA(yaml);
|
|
expect(map, isNotNull);
|
|
|
|
final build = DataBuild.fromJson(map!);
|
|
expect(build.property!['workspace'], '.');
|
|
expect(build.commands, hasLength(1));
|
|
expect(build.commands.first.id, 'hello');
|
|
expect(build.commands.first.command, CommandType.Print);
|
|
expect(build.pipeline!.id, 'main');
|
|
expect(build.pipeline!.workflow, hasLength(1));
|
|
});
|
|
|
|
test('tag system preserves single tag object values', () {
|
|
Application.instance.property['items'] = [1, 2, 3];
|
|
Application.instance.property['config'] = {'a': 1};
|
|
|
|
final listValue = Command.replaceTagValue('<!property.items>');
|
|
expect(listValue, isA<List>());
|
|
expect(listValue, equals([1, 2, 3]));
|
|
|
|
final mapValue = Command.replaceTagValue('<!property.config>');
|
|
expect(mapValue, isA<Map>());
|
|
expect(mapValue, equals({'a': 1}));
|
|
|
|
final embedded = Command.replaceTagValue('values=<!property.items>');
|
|
expect(embedded, isA<String>());
|
|
expect(embedded, contains('1'));
|
|
});
|
|
|
|
test('pipeline validation rejects missing command id', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{'exe': 'doesNotExist'}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, contains('doesNotExist'));
|
|
});
|
|
|
|
// ── Explicit context regression tests ──────────────────────────────────────
|
|
// Each test puts a different value in the singleton vs. the explicit context
|
|
// and asserts that flow-control uses the explicit context exclusively.
|
|
|
|
ExecutionContext makeCtx(Map<String, dynamic> props,
|
|
List<String> commandIds) {
|
|
registerAllCommands();
|
|
final ctx = ExecutionContext();
|
|
ctx.property.addAll(props);
|
|
for (final id in commandIds) {
|
|
final cmd = DataCommand()
|
|
..command = CommandType.Print
|
|
..id = id
|
|
..param = {'message': id};
|
|
ctx.dataCommandMap[id] = cmd;
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
test('PipelineIf evaluates condition from explicit context not singleton',
|
|
() async {
|
|
// Singleton: env = 'dev' → on-false; explicit: env = 'prod' → on-true
|
|
Application.instance.property['env'] = 'dev';
|
|
final ctx = makeCtx({'env': 'prod'}, ['onTrue', 'onFalse']);
|
|
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'if': {
|
|
'condition-string': '<!property.env> == prod',
|
|
'on-true': [
|
|
{'exe': 'onTrue'}
|
|
],
|
|
'on-false': [
|
|
{'exe': 'onFalse'}
|
|
],
|
|
}
|
|
}
|
|
], context: ctx);
|
|
|
|
expect(result.enable, isTrue, reason: result.message);
|
|
await result.pipeline!.execute();
|
|
|
|
expect(ctx.commandStates['onTrue'], CommandState.complete);
|
|
expect(ctx.commandStates['onFalse'], CommandState.ready);
|
|
});
|
|
|
|
test('PipelineForeach iterates list from explicit context not singleton',
|
|
() async {
|
|
// Singleton: items = [] (empty); explicit: items = [10, 20, 30]
|
|
Application.instance.property['items'] = <dynamic>[];
|
|
final ctx = makeCtx({'items': [10, 20, 30]}, ['print']);
|
|
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'foreach': {
|
|
'iterator': '<!property.items>',
|
|
'setValue': '<@property.current>',
|
|
'on-do': [
|
|
{'exe': 'print'}
|
|
],
|
|
}
|
|
}
|
|
], context: ctx);
|
|
|
|
expect(result.enable, isTrue, reason: result.message);
|
|
await result.pipeline!.execute();
|
|
|
|
// Loop ran 3 times using explicit context; last assigned value = 30
|
|
expect(ctx.property['current'], 30);
|
|
// Singleton must not have been modified
|
|
expect(Application.instance.property.containsKey('current'), isFalse);
|
|
});
|
|
|
|
test('PipelineContain evaluates target from explicit context not singleton',
|
|
() async {
|
|
// Singleton: target = 'xyz' (not in list); explicit: target = 'foo' (in list)
|
|
Application.instance.property['target'] = 'xyz';
|
|
final ctx = makeCtx({'target': 'foo'}, ['onTrue', 'onFalse']);
|
|
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'contain': {
|
|
'target': '<!property.target>',
|
|
'list': ['foo', 'bar'],
|
|
'on-true': [
|
|
{'exe': 'onTrue'}
|
|
],
|
|
'on-false': [
|
|
{'exe': 'onFalse'}
|
|
],
|
|
}
|
|
}
|
|
], context: ctx);
|
|
|
|
expect(result.enable, isTrue, reason: result.message);
|
|
await result.pipeline!.execute();
|
|
|
|
expect(ctx.commandStates['onTrue'], CommandState.complete);
|
|
expect(ctx.commandStates['onFalse'], CommandState.ready);
|
|
});
|
|
|
|
// REVIEW_VALIDATE-2: malformed nested pipeline regression tests
|
|
test('pipeline validation rejects if with no condition field', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'if': {'on-true': []}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNotNull);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
});
|
|
|
|
test('pipeline validation rejects foreach with non-string iterator', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'foreach': {
|
|
'iterator': 123,
|
|
'setValue': '<@property.item>',
|
|
'on-do': [
|
|
{'exe': 'someCmd'}
|
|
],
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNotNull);
|
|
expect(result.message, contains('foreach'));
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
});
|
|
|
|
test('pipeline validation rejects switch with non-list cases', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'switch': {
|
|
'condition-string': 'test',
|
|
'cases': 'not-a-list',
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNotNull);
|
|
expect(result.message, contains('switch'));
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
});
|
|
|
|
// REVIEW_REVIEW_VALIDATE-3: branch list field validation regression tests
|
|
test('pipeline validation rejects exe-handle with non-list on-success', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'exe-handle': {
|
|
'id': 'hello',
|
|
'on-success': 'not-a-list',
|
|
'on-fail': [{'exe': 'hello'}],
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('exe-handle'));
|
|
expect(result.message, contains('on-success'));
|
|
});
|
|
|
|
test('pipeline validation rejects exe-handle with non-list on-fail', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'exe-handle': {
|
|
'id': 'hello',
|
|
'on-success': [{'exe': 'hello'}],
|
|
'on-fail': 'not-a-list',
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('exe-handle'));
|
|
expect(result.message, contains('on-fail'));
|
|
});
|
|
|
|
test('pipeline validation rejects if with non-list on-true', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'if': {
|
|
'condition-string': 'a == b',
|
|
'on-true': 'not-a-list',
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('if'));
|
|
expect(result.message, contains('on-true'));
|
|
});
|
|
|
|
test('pipeline validation rejects if with non-list on-false', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'if': {
|
|
'condition-string': 'a == b',
|
|
'on-true': [{'exe': 'hello'}],
|
|
'on-false': 'not-a-list',
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('if'));
|
|
expect(result.message, contains('on-false'));
|
|
});
|
|
|
|
test('pipeline validation rejects while with non-list on-do', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'while': {
|
|
'condition-bool': 'true',
|
|
'on-do': 'not-a-list',
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('while'));
|
|
expect(result.message, contains('on-do'));
|
|
});
|
|
|
|
test('pipeline validation rejects foreach with non-list on-do', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'foreach': {
|
|
'iterator': '<!property.items>',
|
|
'setValue': '<@property.item>',
|
|
'on-do': 'not-a-list',
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('foreach'));
|
|
expect(result.message, contains('on-do'));
|
|
});
|
|
|
|
test('pipeline validation rejects switch with non-map case element', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'switch': {
|
|
'condition-string': 'prod',
|
|
'cases': ['not-a-map'],
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('switch'));
|
|
expect(result.message, contains('cases'));
|
|
});
|
|
|
|
test('pipeline validation rejects switch with missing tasks in case', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'switch': {
|
|
'condition-string': 'prod',
|
|
'cases': [
|
|
{'value': 'prod'}
|
|
],
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('switch'));
|
|
expect(result.message, contains('tasks'));
|
|
});
|
|
|
|
test('pipeline validation rejects switch with non-list tasks in case', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'switch': {
|
|
'condition-string': 'prod',
|
|
'cases': [
|
|
{'value': 'prod', 'tasks': 'not-a-list'}
|
|
],
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('switch'));
|
|
expect(result.message, contains('tasks'));
|
|
});
|
|
|
|
// REVIEW_REVIEW_VALIDATE-2: contain node guard regression tests
|
|
test('pipeline validation rejects contain with non-map value', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{'contain': 123}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('contain'));
|
|
});
|
|
|
|
test('pipeline validation rejects contain with missing target', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'contain': {
|
|
'list': ['a', 'b'],
|
|
'on-true': [{'exe': 'x'}],
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('contain'));
|
|
expect(result.message, contains('target'));
|
|
});
|
|
|
|
test('pipeline validation rejects contain with non-string target', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'contain': {
|
|
'target': 123,
|
|
'list': ['a', 'b'],
|
|
'on-true': [{'exe': 'x'}],
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('contain'));
|
|
expect(result.message, contains('target'));
|
|
});
|
|
|
|
test('pipeline validation rejects contain with missing list', () {
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'contain': {
|
|
'target': 'foo',
|
|
'on-true': [{'exe': 'x'}],
|
|
}
|
|
}
|
|
]);
|
|
expect(result.enable, isFalse);
|
|
expect(result.message, isNot(contains('is not a subtype')));
|
|
expect(result.message, contains('contain'));
|
|
expect(result.message, contains('list'));
|
|
});
|
|
|
|
test(
|
|
'PipelineSwitch validate uses explicit context command map not singleton',
|
|
() {
|
|
// Singleton dataCommandMap is empty; only explicit context has the command.
|
|
registerAllCommands();
|
|
final ctx = ExecutionContext();
|
|
final cmd = DataCommand()
|
|
..command = CommandType.Print
|
|
..id = 'deployProd'
|
|
..param = {'message': 'prod'};
|
|
ctx.dataCommandMap['deployProd'] = cmd;
|
|
|
|
final result = Pipeline.pipelineInitialize([
|
|
{
|
|
'switch': {
|
|
'condition-string': 'prod',
|
|
'cases': [
|
|
{
|
|
'value': 'prod',
|
|
'tasks': [
|
|
{'exe': 'deployProd'}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
}
|
|
], context: ctx);
|
|
|
|
// Should succeed: deployProd exists in explicit context.
|
|
// Would fail if singleton (empty) context were used.
|
|
expect(result.enable, isTrue, reason: result.message);
|
|
});
|
|
}
|