feat: implement Step.Condition evaluation in bd cook (bd-7zka.1)
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>
This commit is contained in:
committed by
Steve Yegge
parent
82611834df
commit
ee34a74e90
@@ -574,6 +574,13 @@ func collectSteps(steps []*formula.Step, parentID string,
|
|||||||
// and returns an in-memory TemplateSubgraph ready for instantiation.
|
// and returns an in-memory TemplateSubgraph ready for instantiation.
|
||||||
// This is the main entry point for ephemeral proto cooking.
|
// This is the main entry point for ephemeral proto cooking.
|
||||||
func resolveAndCookFormula(formulaName string, searchPaths []string) (*TemplateSubgraph, error) {
|
func resolveAndCookFormula(formulaName string, searchPaths []string) (*TemplateSubgraph, error) {
|
||||||
|
return resolveAndCookFormulaWithVars(formulaName, searchPaths, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveAndCookFormulaWithVars loads a formula and optionally filters steps by condition.
|
||||||
|
// If conditionVars is provided, steps with conditions that evaluate to false are excluded.
|
||||||
|
// Pass nil for conditionVars to include all steps (condition filtering skipped).
|
||||||
|
func resolveAndCookFormulaWithVars(formulaName string, searchPaths []string, conditionVars map[string]string) (*TemplateSubgraph, error) {
|
||||||
// Create parser with search paths
|
// Create parser with search paths
|
||||||
parser := formula.NewParser(searchPaths...)
|
parser := formula.NewParser(searchPaths...)
|
||||||
|
|
||||||
@@ -633,6 +640,27 @@ func resolveAndCookFormula(formulaName string, searchPaths []string) (*TemplateS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply step condition filtering if vars provided (bd-7zka.1)
|
||||||
|
// This filters out steps whose conditions evaluate to false
|
||||||
|
if conditionVars != nil {
|
||||||
|
// Merge with formula defaults for complete evaluation
|
||||||
|
mergedVars := make(map[string]string)
|
||||||
|
for name, def := range resolved.Vars {
|
||||||
|
if def != nil && def.Default != "" {
|
||||||
|
mergedVars[name] = def.Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range conditionVars {
|
||||||
|
mergedVars[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredSteps, err := formula.FilterStepsByCondition(resolved.Steps, mergedVars)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("filtering steps by condition: %w", err)
|
||||||
|
}
|
||||||
|
resolved.Steps = filteredSteps
|
||||||
|
}
|
||||||
|
|
||||||
// Cook to in-memory subgraph, including variable definitions for default handling
|
// Cook to in-memory subgraph, including variable definitions for default handling
|
||||||
return cookFormulaToSubgraphWithVars(resolved, resolved.Formula, resolved.Vars)
|
return cookFormulaToSubgraphWithVars(resolved, resolved.Formula, resolved.Vars)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,12 +208,13 @@ func runMolBond(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
// Resolve both operands - can be issue IDs or formula names
|
// Resolve both operands - can be issue IDs or formula names
|
||||||
// Formula names are cooked inline to in-memory subgraphs
|
// Formula names are cooked inline to in-memory subgraphs
|
||||||
subgraphA, cookedA, err := resolveOrCookToSubgraph(ctx, store, args[0])
|
// Pass vars for step condition filtering (bd-7zka.1)
|
||||||
|
subgraphA, cookedA, err := resolveOrCookToSubgraph(ctx, store, args[0], vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
subgraphB, cookedB, err := resolveOrCookToSubgraph(ctx, store, args[1])
|
subgraphB, cookedB, err := resolveOrCookToSubgraph(ctx, store, args[1], vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -569,8 +570,9 @@ func resolveOrDescribe(ctx context.Context, s storage.Storage, operand string) (
|
|||||||
// If it's an issue, loads the subgraph from DB. If it's a formula, cooks inline to subgraph.
|
// If it's an issue, loads the subgraph from DB. If it's a formula, cooks inline to subgraph.
|
||||||
// Returns the subgraph, whether it was cooked from formula, and any error.
|
// Returns the subgraph, whether it was cooked from formula, and any error.
|
||||||
//
|
//
|
||||||
|
// The vars parameter is used for step condition filtering (bd-7zka.1).
|
||||||
// This implements gt-4v1eo: formulas are cooked to in-memory subgraphs (no DB storage).
|
// This implements gt-4v1eo: formulas are cooked to in-memory subgraphs (no DB storage).
|
||||||
func resolveOrCookToSubgraph(ctx context.Context, s storage.Storage, operand string) (*TemplateSubgraph, bool, error) {
|
func resolveOrCookToSubgraph(ctx context.Context, s storage.Storage, operand string, vars map[string]string) (*TemplateSubgraph, bool, error) {
|
||||||
// First, try to resolve as an existing issue
|
// First, try to resolve as an existing issue
|
||||||
id, err := utils.ResolvePartialID(ctx, s, operand)
|
id, err := utils.ResolvePartialID(ctx, s, operand)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -599,7 +601,8 @@ func resolveOrCookToSubgraph(ctx context.Context, s storage.Storage, operand str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to cook formula inline to in-memory subgraph
|
// Try to cook formula inline to in-memory subgraph
|
||||||
subgraph, err := resolveAndCookFormula(operand, nil)
|
// Pass vars for step condition filtering (bd-7zka.1)
|
||||||
|
subgraph, err := resolveAndCookFormulaWithVars(operand, nil, vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("'%s' not found as issue or formula: %w", operand, err)
|
return nil, false, fmt.Errorf("'%s' not found as issue or formula: %w", operand, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ func runPour(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
// Try to cook formula inline (gt-4v1eo: ephemeral protos)
|
// Try to cook formula inline (gt-4v1eo: ephemeral protos)
|
||||||
// This works for any valid formula name, not just "mol-" prefixed ones
|
// This works for any valid formula name, not just "mol-" prefixed ones
|
||||||
sg, err := resolveAndCookFormula(args[0], nil)
|
// Pass vars for step condition filtering (bd-7zka.1)
|
||||||
|
sg, err := resolveAndCookFormulaWithVars(args[0], nil, vars)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
subgraph = sg
|
subgraph = sg
|
||||||
protoID = sg.Root.ID
|
protoID = sg.Root.ID
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ func runWispCreate(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
// Try to cook formula inline (ephemeral protos)
|
// Try to cook formula inline (ephemeral protos)
|
||||||
// This works for any valid formula name, not just "mol-" prefixed ones
|
// This works for any valid formula name, not just "mol-" prefixed ones
|
||||||
sg, err := resolveAndCookFormula(args[0], nil)
|
// Pass vars for step condition filtering (bd-7zka.1)
|
||||||
|
sg, err := resolveAndCookFormulaWithVars(args[0], nil, vars)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
subgraph = sg
|
subgraph = sg
|
||||||
protoID = sg.Root.ID
|
protoID = sg.Root.ID
|
||||||
|
|||||||
141
internal/formula/stepcondition.go
Normal file
141
internal/formula/stepcondition.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Package formula provides Step.Condition evaluation for compile-time step filtering.
|
||||||
|
//
|
||||||
|
// Step.Condition is simpler than the runtime condition evaluation in condition.go.
|
||||||
|
// It evaluates at cook/pour time to include or exclude steps based on formula variables.
|
||||||
|
//
|
||||||
|
// Supported formats:
|
||||||
|
// - "{{var}}" - truthy check (non-empty, non-"false", non-"0")
|
||||||
|
// - "{{var}} == value" - equality check
|
||||||
|
// - "{{var}} != value" - inequality check
|
||||||
|
package formula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Step condition patterns
|
||||||
|
var (
|
||||||
|
// {{var}} - simple variable reference for truthy check
|
||||||
|
stepCondVarPattern = regexp.MustCompile(`^\{\{(\w+)\}\}$`)
|
||||||
|
|
||||||
|
// {{var}} == value or {{var}} != value
|
||||||
|
stepCondComparePattern = regexp.MustCompile(`^\{\{(\w+)\}\}\s*(==|!=)\s*(.+)$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// EvaluateStepCondition evaluates a step's condition against variable values.
|
||||||
|
// Returns true if the step should be included, false if it should be skipped.
|
||||||
|
//
|
||||||
|
// Condition formats:
|
||||||
|
// - "" (empty) - always include
|
||||||
|
// - "{{var}}" - include if var is truthy (non-empty, non-"false", non-"0")
|
||||||
|
// - "{{var}} == value" - include if var equals value
|
||||||
|
// - "{{var}} != value" - include if var does not equal value
|
||||||
|
func EvaluateStepCondition(condition string, vars map[string]string) (bool, error) {
|
||||||
|
condition = strings.TrimSpace(condition)
|
||||||
|
|
||||||
|
// Empty condition means always include
|
||||||
|
if condition == "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try truthy pattern: {{var}}
|
||||||
|
if m := stepCondVarPattern.FindStringSubmatch(condition); m != nil {
|
||||||
|
varName := m[1]
|
||||||
|
value := vars[varName]
|
||||||
|
return isTruthy(value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try comparison pattern: {{var}} == value or {{var}} != value
|
||||||
|
if m := stepCondComparePattern.FindStringSubmatch(condition); m != nil {
|
||||||
|
varName := m[1]
|
||||||
|
operator := m[2]
|
||||||
|
expected := strings.TrimSpace(m[3])
|
||||||
|
|
||||||
|
// Remove quotes from expected value if present
|
||||||
|
expected = unquoteValue(expected)
|
||||||
|
|
||||||
|
actual := vars[varName]
|
||||||
|
|
||||||
|
switch operator {
|
||||||
|
case "==":
|
||||||
|
return actual == expected, nil
|
||||||
|
case "!=":
|
||||||
|
return actual != expected, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("invalid step condition format: %q (expected {{var}} or {{var}} == value)", condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTruthy returns true if a value is considered "truthy" for step conditions.
|
||||||
|
// Falsy values: empty string, "false", "0", "no", "off"
|
||||||
|
// All other values are truthy.
|
||||||
|
func isTruthy(value string) bool {
|
||||||
|
if value == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(value)
|
||||||
|
switch lower {
|
||||||
|
case "false", "0", "no", "off":
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// unquoteValue removes surrounding quotes from a value if present.
|
||||||
|
func unquoteValue(s string) string {
|
||||||
|
if len(s) >= 2 {
|
||||||
|
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
|
||||||
|
return s[1 : len(s)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterStepsByCondition filters a list of steps based on their Condition field.
|
||||||
|
// Steps with conditions that evaluate to false are excluded from the result.
|
||||||
|
// Children of excluded steps are also excluded.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - steps: the steps to filter
|
||||||
|
// - vars: variable values for condition evaluation
|
||||||
|
//
|
||||||
|
// Returns the filtered steps and any error encountered during evaluation.
|
||||||
|
func FilterStepsByCondition(steps []*Step, vars map[string]string) ([]*Step, error) {
|
||||||
|
if vars == nil {
|
||||||
|
vars = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]*Step, 0, len(steps))
|
||||||
|
|
||||||
|
for _, step := range steps {
|
||||||
|
// Evaluate step condition
|
||||||
|
include, err := EvaluateStepCondition(step.Condition, vars)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("step %q: %w", step.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !include {
|
||||||
|
// Skip this step and all its children
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the step to avoid mutating input
|
||||||
|
clone := cloneStep(step)
|
||||||
|
|
||||||
|
// Recursively filter children
|
||||||
|
if len(step.Children) > 0 {
|
||||||
|
filteredChildren, err := FilterStepsByCondition(step.Children, vars)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clone.Children = filteredChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, clone)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
331
internal/formula/stepcondition_test.go
Normal file
331
internal/formula/stepcondition_test.go
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -175,8 +175,8 @@ type Step struct {
|
|||||||
ExpandVars map[string]string `json:"expand_vars,omitempty"`
|
ExpandVars map[string]string `json:"expand_vars,omitempty"`
|
||||||
|
|
||||||
// Condition makes this step optional based on a variable.
|
// Condition makes this step optional based on a variable.
|
||||||
// Format: "{{var}}" (truthy) or "{{var}} == value".
|
// Format: "{{var}}" (truthy) or "{{var}} == value" or "{{var}} != value".
|
||||||
// TODO(bd-7zka): Not yet implemented in bd cook. Filed as future work.
|
// Evaluated at cook/pour time via FilterStepsByCondition.
|
||||||
Condition string `json:"condition,omitempty"`
|
Condition string `json:"condition,omitempty"`
|
||||||
|
|
||||||
// Children are nested steps (for creating epic hierarchies).
|
// Children are nested steps (for creating epic hierarchies).
|
||||||
|
|||||||
Reference in New Issue
Block a user