Add compile-time step filtering based on formula variables:
- New EvaluateStepCondition function for {{var}} truthy and equality checks
- FilterStepsByCondition to exclude steps based on conditions
- Integration into pour, wisp, and mol bond commands
- Supports: {{var}}, {{var}} == value, {{var}} != value
Steps with conditions that evaluate to false are excluded from the
cooked formula, along with their children.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
332 lines
8.0 KiB
Go
332 lines
8.0 KiB
Go
package formula
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestEvaluateStepCondition(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
condition string
|
|
vars map[string]string
|
|
want bool
|
|
wantErr bool
|
|
}{
|
|
// Empty condition - always include
|
|
{
|
|
name: "empty condition",
|
|
condition: "",
|
|
vars: nil,
|
|
want: true,
|
|
wantErr: false,
|
|
},
|
|
// Truthy checks: {{var}}
|
|
{
|
|
name: "truthy - non-empty value",
|
|
condition: "{{enabled}}",
|
|
vars: map[string]string{"enabled": "yes"},
|
|
want: true,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "truthy - empty value",
|
|
condition: "{{enabled}}",
|
|
vars: map[string]string{"enabled": ""},
|
|
want: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "truthy - missing variable",
|
|
condition: "{{enabled}}",
|
|
vars: map[string]string{},
|
|
want: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "truthy - false string",
|
|
condition: "{{enabled}}",
|
|
vars: map[string]string{"enabled": "false"},
|
|
want: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "truthy - FALSE string",
|
|
condition: "{{enabled}}",
|
|
vars: map[string]string{"enabled": "FALSE"},
|
|
want: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "truthy - 0 string",
|
|
condition: "{{enabled}}",
|
|
vars: map[string]string{"enabled": "0"},
|
|
want: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "truthy - no string",
|
|
condition: "{{enabled}}",
|
|
vars: map[string]string{"enabled": "no"},
|
|
want: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "truthy - off string",
|
|
condition: "{{enabled}}",
|
|
vars: map[string]string{"enabled": "off"},
|
|
want: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "truthy - true string",
|
|
condition: "{{enabled}}",
|
|
vars: map[string]string{"enabled": "true"},
|
|
want: true,
|
|
wantErr: false,
|
|
},
|
|
// Equality checks: {{var}} == value
|
|
{
|
|
name: "equality - match",
|
|
condition: "{{env}} == staging",
|
|
vars: map[string]string{"env": "staging"},
|
|
want: true,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "equality - no match",
|
|
condition: "{{env}} == production",
|
|
vars: map[string]string{"env": "staging"},
|
|
want: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "equality - quoted value match",
|
|
condition: "{{env}} == 'staging'",
|
|
vars: map[string]string{"env": "staging"},
|
|
want: true,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "equality - double quoted value match",
|
|
condition: `{{env}} == "staging"`,
|
|
vars: map[string]string{"env": "staging"},
|
|
want: true,
|
|
wantErr: false,
|
|
},
|
|
// Inequality checks: {{var}} != value
|
|
{
|
|
name: "inequality - different value",
|
|
condition: "{{env}} != production",
|
|
vars: map[string]string{"env": "staging"},
|
|
want: true,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "inequality - same value",
|
|
condition: "{{env}} != staging",
|
|
vars: map[string]string{"env": "staging"},
|
|
want: false,
|
|
wantErr: false,
|
|
},
|
|
// Invalid conditions
|
|
{
|
|
name: "invalid - no variable braces",
|
|
condition: "env == staging",
|
|
vars: map[string]string{"env": "staging"},
|
|
want: false,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid - random text",
|
|
condition: "something random",
|
|
vars: map[string]string{},
|
|
want: false,
|
|
wantErr: true,
|
|
},
|
|
// Edge cases
|
|
{
|
|
name: "whitespace in condition",
|
|
condition: " {{env}} == staging ",
|
|
vars: map[string]string{"env": "staging"},
|
|
want: true,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "value with spaces",
|
|
condition: "{{msg}} == 'hello world'",
|
|
vars: map[string]string{"msg": "hello world"},
|
|
want: true,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := EvaluateStepCondition(tt.condition, tt.vars)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("EvaluateStepCondition() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("EvaluateStepCondition() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsTruthy(t *testing.T) {
|
|
tests := []struct {
|
|
value string
|
|
want bool
|
|
}{
|
|
{"", false},
|
|
{"false", false},
|
|
{"False", false},
|
|
{"FALSE", false},
|
|
{"0", false},
|
|
{"no", false},
|
|
{"No", false},
|
|
{"NO", false},
|
|
{"off", false},
|
|
{"Off", false},
|
|
{"OFF", false},
|
|
{"true", true},
|
|
{"True", true},
|
|
{"TRUE", true},
|
|
{"1", true},
|
|
{"yes", true},
|
|
{"on", true},
|
|
{"anything", true},
|
|
{"enabled", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.value, func(t *testing.T) {
|
|
if got := isTruthy(tt.value); got != tt.want {
|
|
t.Errorf("isTruthy(%q) = %v, want %v", tt.value, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterStepsByCondition(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
steps []*Step
|
|
vars map[string]string
|
|
wantIDs []string // Expected step IDs in result
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "no conditions - all included",
|
|
steps: []*Step{
|
|
{ID: "step1", Title: "Step 1"},
|
|
{ID: "step2", Title: "Step 2"},
|
|
},
|
|
vars: nil,
|
|
wantIDs: []string{"step1", "step2"},
|
|
},
|
|
{
|
|
name: "truthy condition - included",
|
|
steps: []*Step{
|
|
{ID: "step1", Title: "Step 1", Condition: "{{enabled}}"},
|
|
},
|
|
vars: map[string]string{"enabled": "true"},
|
|
wantIDs: []string{"step1"},
|
|
},
|
|
{
|
|
name: "truthy condition - excluded",
|
|
steps: []*Step{
|
|
{ID: "step1", Title: "Step 1", Condition: "{{enabled}}"},
|
|
},
|
|
vars: map[string]string{"enabled": "false"},
|
|
wantIDs: []string{},
|
|
},
|
|
{
|
|
name: "mixed conditions",
|
|
steps: []*Step{
|
|
{ID: "step1", Title: "Step 1"},
|
|
{ID: "step2", Title: "Step 2", Condition: "{{run_tests}}"},
|
|
{ID: "step3", Title: "Step 3", Condition: "{{env}} == production"},
|
|
},
|
|
vars: map[string]string{"run_tests": "yes", "env": "staging"},
|
|
wantIDs: []string{"step1", "step2"},
|
|
},
|
|
{
|
|
name: "children inherit parent filter",
|
|
steps: []*Step{
|
|
{
|
|
ID: "parent",
|
|
Title: "Parent",
|
|
Condition: "{{include_parent}}",
|
|
Children: []*Step{
|
|
{ID: "child1", Title: "Child 1"},
|
|
{ID: "child2", Title: "Child 2"},
|
|
},
|
|
},
|
|
},
|
|
vars: map[string]string{"include_parent": "false"},
|
|
wantIDs: []string{}, // Parent excluded, children go with it
|
|
},
|
|
{
|
|
name: "child with own condition",
|
|
steps: []*Step{
|
|
{
|
|
ID: "parent",
|
|
Title: "Parent",
|
|
Children: []*Step{
|
|
{ID: "child1", Title: "Child 1"},
|
|
{ID: "child2", Title: "Child 2", Condition: "{{include_child2}}"},
|
|
},
|
|
},
|
|
},
|
|
vars: map[string]string{"include_child2": "no"},
|
|
wantIDs: []string{"parent", "child1"},
|
|
},
|
|
{
|
|
name: "equality condition",
|
|
steps: []*Step{
|
|
{ID: "deploy-staging", Title: "Deploy Staging", Condition: "{{env}} == staging"},
|
|
{ID: "deploy-prod", Title: "Deploy Prod", Condition: "{{env}} == production"},
|
|
},
|
|
vars: map[string]string{"env": "staging"},
|
|
wantIDs: []string{"deploy-staging"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := FilterStepsByCondition(tt.steps, tt.vars)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("FilterStepsByCondition() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
|
|
// Collect all IDs (including children) from result
|
|
gotIDs := collectStepIDsForTest(result)
|
|
if len(gotIDs) != len(tt.wantIDs) {
|
|
t.Errorf("FilterStepsByCondition() got %d steps %v, want %d steps %v",
|
|
len(gotIDs), gotIDs, len(tt.wantIDs), tt.wantIDs)
|
|
return
|
|
}
|
|
|
|
for i, wantID := range tt.wantIDs {
|
|
if i >= len(gotIDs) || gotIDs[i] != wantID {
|
|
t.Errorf("FilterStepsByCondition() step[%d] = %v, want %v", i, gotIDs, tt.wantIDs)
|
|
return
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// collectStepIDsForTest collects all step IDs (including children) in order.
|
|
func collectStepIDsForTest(steps []*Step) []string {
|
|
var ids []string
|
|
for _, s := range steps {
|
|
ids = append(ids, s.ID)
|
|
ids = append(ids, collectStepIDsForTest(s.Children)...)
|
|
}
|
|
return ids
|
|
}
|