diff --git a/cmd/cli/generator/generate.go b/cmd/cli/generator/generate.go index 171cb6e..a112198 100644 --- a/cmd/cli/generator/generate.go +++ b/cmd/cli/generator/generate.go @@ -39,7 +39,7 @@ var Cmd = &cobra.Command{ manager := runner.NewPlanManagerBuilder(). WithManifests(loadedManifests...).Build() - planManifest, err := manager.Generate() + planManifest, _, err := manager.Generate() if err != nil { cli.Errorf("Failed to generate plan: %v", err) return diff --git a/cmd/cli/run/run.go b/cmd/cli/run/run.go index 27b9391..fc53690 100644 --- a/cmd/cli/run/run.go +++ b/cmd/cli/run/run.go @@ -56,7 +56,7 @@ var Cmd = &cobra.Command{ WithManifests(loadedManifests...).Build() // Use V2 plan generation with dependency analysis - planManifest, graphResult, err := manager.GenerateV2() + planManifest, graphResult, err := manager.Generate() if err != nil { cli.Errorf("Failed to generate V2 plan: %v", err) return @@ -82,11 +82,11 @@ var Cmd = &cobra.Command{ hooksRunner := hooks.NewDefaultHooksRunner() // Use V2 plan runner with dependency support - planRunner := executor.NewV2PlanRunner(registry, hooksRunner, graphResult) + planRunner := executor.NewRunner(registry, hooksRunner, graphResult) runCtx := ctxBuilder.Build() - if err = planRunner.RunPlan(runCtx, planManifest); err != nil { + if err = planRunner.Run(runCtx, planManifest); err != nil { cli.Errorf("Plan execution failed: %v", err) return } diff --git a/internal/core/manifests/kinds/priority.go b/internal/core/manifests/kinds/priority.go new file mode 100644 index 0000000..e488ec9 --- /dev/null +++ b/internal/core/manifests/kinds/priority.go @@ -0,0 +1,18 @@ +package kinds + +import "github.com/apiqube/cli/internal/core/manifests" + +var PriorityMap = map[string]int{ + // Infrastructure kinds + manifests.ValuesKind: 10, + + // Application kinds + manifests.ServerKind: 100, + manifests.ServiceKind: 110, + + // Test kinds + manifests.HttpTestKind: 200, + + // Load test kinds + manifests.HttpLoadTestKind: 300, +} diff --git a/internal/core/runner/depends/builder.go b/internal/core/runner/depends/builder.go new file mode 100644 index 0000000..5d3b2f1 --- /dev/null +++ b/internal/core/runner/depends/builder.go @@ -0,0 +1,50 @@ +package depends + +import "github.com/apiqube/cli/internal/core/runner/depends/rules" + +// AddRule adds a new rule to the registry +func (b *Builder) AddRule(rule rules.DependencyRule) { + b.registry.Register(rule) +} + +// GetSaveRequirement returns save requirement for a manifest +func (r *Result) GetSaveRequirement(manifestID string) (SaveRequirement, bool) { + req, exists := r.SaveRequirements[manifestID] + return req, exists +} + +// GetDependenciesFor returns all dependencies for a manifest +func (r *Result) GetDependenciesFor(manifestID string) []rules.Dependency { + var deps []rules.Dependency + for _, dep := range r.Dependencies { + if dep.From == manifestID { + deps = append(deps, dep) + } + } + return deps +} + +// GetDependentsOf returns dependencies that depend on the given manifest +func (r *Result) GetDependentsOf(manifestID string) []rules.Dependency { + var dependents []rules.Dependency + for _, dep := range r.Dependencies { + if dep.To == manifestID { + dependents = append(dependents, dep) + } + } + return dependents +} + +// GetIntraManifestDependencies returns intra-manifest dependencies for a given manifest +func (r *Result) GetIntraManifestDependencies(manifestID string) []rules.Dependency { + if deps, exists := r.IntraManifestDeps[manifestID]; exists { + return deps + } + return []rules.Dependency{} +} + +// HasIntraManifestDependencies checks if a manifest has intra-manifest dependencies +func (r *Result) HasIntraManifestDependencies(manifestID string) bool { + deps, exists := r.IntraManifestDeps[manifestID] + return exists && len(deps) > 0 +} diff --git a/internal/core/runner/depends/builder_v2.go b/internal/core/runner/depends/builder_v2.go deleted file mode 100644 index 646dd90..0000000 --- a/internal/core/runner/depends/builder_v2.go +++ /dev/null @@ -1,59 +0,0 @@ -package depends - -// AddRule adds a new rule to the registry -func (gb *GraphBuilderV2) AddRule(rule DependencyRule) { - gb.registry.Register(rule) -} - -// GetSaveRequirement returns save requirement for a manifest -func (gr *GraphResultV2) GetSaveRequirement(manifestID string) (SaveRequirement, bool) { - req, exists := gr.SaveRequirements[manifestID] - return req, exists -} - -// GetDependenciesFor returns all dependencies for a manifest -func (gr *GraphResultV2) GetDependenciesFor(manifestID string) []Dependency { - var deps []Dependency - for _, dep := range gr.Dependencies { - if dep.From == manifestID { - deps = append(deps, dep) - } - } - return deps -} - -// GetDependentsOf returns dependencies that depend on the given manifest -func (gr *GraphResultV2) GetDependentsOf(manifestID string) []Dependency { - var dependents []Dependency - for _, dep := range gr.Dependencies { - if dep.To == manifestID { - dependents = append(dependents, dep) - } - } - return dependents -} - -// GetDependenciesOf returns dependencies that the given manifest depends on -func (gr *GraphResultV2) GetDependenciesOf(manifestID string) []Dependency { - var dependencies []Dependency - for _, dep := range gr.Dependencies { - if dep.From == manifestID { - dependencies = append(dependencies, dep) - } - } - return dependencies -} - -// GetIntraManifestDependencies returns intra-manifest dependencies for a given manifest -func (gr *GraphResultV2) GetIntraManifestDependencies(manifestID string) []Dependency { - if deps, exists := gr.IntraManifestDeps[manifestID]; exists { - return deps - } - return []Dependency{} -} - -// HasIntraManifestDependencies checks if a manifest has intra-manifest dependencies -func (gr *GraphResultV2) HasIntraManifestDependencies(manifestID string) bool { - deps, exists := gr.IntraManifestDeps[manifestID] - return exists && len(deps) > 0 -} diff --git a/internal/core/runner/depends/dependencies.go b/internal/core/runner/depends/dependencies.go deleted file mode 100644 index db4f683..0000000 --- a/internal/core/runner/depends/dependencies.go +++ /dev/null @@ -1,138 +0,0 @@ -package depends - -import ( - "container/heap" - "fmt" - "sort" - "strings" - - "github.com/apiqube/cli/internal/collections" - - "github.com/apiqube/cli/internal/core/manifests" -) - -type GraphResult struct { - Graph map[string][]string - ExecutionOrder []string -} - -type Node struct { - ID string - Priority int -} - -func BuildGraphWithPriority(mans []manifests.Manifest) (*GraphResult, error) { - graph := make(map[string][]string) - inDegree := make(map[string]int) - idToNode := make(map[string]manifests.Manifest) - nodePriority := make(map[string]int) - - // Initialize all manifests - for _, node := range mans { - id := node.GetID() - idToNode[id] = node - inDegree[id] = 0 - - parts := strings.Split(id, ".") - if len(parts) >= 2 { - kind := parts[1] - nodePriority[id] = getPriority(kind) - } - } - - // Build dependency graph - for _, man := range mans { - if dep, has := man.(manifests.Dependencies); has { - id := man.GetID() - for _, depID := range dep.GetDependsOn() { - if depID == id { - return nil, fmt.Errorf("dependency error: %s manifest cannot depend on itself", id) - } - graph[depID] = append(graph[depID], id) - inDegree[id]++ - } - } - } - - // Use priority queue for topological sorting with priorities - // Lower priority number = higher execution priority (executes first) - priorityQueue := collections.NewPriorityQueue[*Node](func(a, b *Node) bool { - // First compare by priority (lower number = higher priority) - if a.Priority != b.Priority { - return a.Priority < b.Priority - } - // If priorities are equal, sort by ID for deterministic behavior - return a.ID < b.ID - }) - - // Add all nodes with zero in-degree to the queue - var zeroInDegreeNodes []*Node - for id, degree := range inDegree { - if degree == 0 { - zeroInDegreeNodes = append(zeroInDegreeNodes, &Node{ - ID: id, - Priority: nodePriority[id], - }) - } - } - - // Sort for deterministic behavior - sort.Slice(zeroInDegreeNodes, func(i, j int) bool { - if zeroInDegreeNodes[i].Priority != zeroInDegreeNodes[j].Priority { - return zeroInDegreeNodes[i].Priority < zeroInDegreeNodes[j].Priority - } - return zeroInDegreeNodes[i].ID < zeroInDegreeNodes[j].ID - }) - - // Add to priority queue - for _, node := range zeroInDegreeNodes { - heap.Push(priorityQueue, node) - } - - var order []string - for priorityQueue.Len() > 0 { - current := heap.Pop(priorityQueue).(*Node).ID - order = append(order, current) - - // Process neighbors in sorted order for deterministic behavior - neighbors := graph[current] - sort.Strings(neighbors) - - for _, neighbor := range neighbors { - inDegree[neighbor]-- - if inDegree[neighbor] == 0 { - heap.Push(priorityQueue, &Node{ - ID: neighbor, - Priority: nodePriority[neighbor], - }) - } - } - } - - if len(order) != len(mans) { - cyclicNodes := findCyclicNodes(inDegree) - return nil, fmt.Errorf("dependency error: сyclic dependency: %v", cyclicNodes) - } - - return &GraphResult{ - Graph: graph, - ExecutionOrder: order, - }, nil -} - -func getPriority(kind string) int { - if p, ok := priorityOrder[kind]; ok { - return p - } - return 100 // Default low priority for unknown kinds -} - -func findCyclicNodes(inDegree map[string]int) []string { - cyclicNodes := make([]string, 0) - for id, degree := range inDegree { - if degree > 0 { - cyclicNodes = append(cyclicNodes, id) - } - } - return cyclicNodes -} diff --git a/internal/core/runner/depends/graph_builder_v2.go b/internal/core/runner/depends/graph.go similarity index 54% rename from internal/core/runner/depends/graph_builder_v2.go rename to internal/core/runner/depends/graph.go index 88fc206..dc02aa9 100644 --- a/internal/core/runner/depends/graph_builder_v2.go +++ b/internal/core/runner/depends/graph.go @@ -6,35 +6,29 @@ import ( "sort" "strings" + "github.com/apiqube/cli/internal/core/manifests/kinds" + "github.com/apiqube/cli/internal/core/runner/depends/rules" + "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/kinds/tests/api" ) -var priorityOrder = map[string]int{ - manifests.ValuesKind: 1, - manifests.ServerKind: 10, - manifests.ServiceKind: 20, - manifests.HttpTestKind: 30, - manifests.HttpLoadTestKind: 40, -} - -// GraphBuilderV2 builds dependency graphs using rule-based analysis -type GraphBuilderV2 struct { - registry *RuleRegistry +// Builder builds dependency graphs using rule-based analysis +type Builder struct { + registry *rules.RuleRegistry manifestPriority map[string]int templateRegex *regexp.Regexp } -// GraphResultV2 represents the result of graph building with enhanced metadata -type GraphResultV2 struct { - Graph map[string][]string // Adjacency list representation - ExecutionOrder []string // Topologically sorted execution order - Dependencies []Dependency // All inter-manifest dependencies - IntraManifestDeps map[string][]Dependency // Dependencies within manifests - SaveRequirements map[string]SaveRequirement // What data needs to be saved - ManifestPriorities map[string]int // Priority of each manifest - AliasToManifest map[string]string // Maps alias to manifest ID - TestCaseAliases map[string]TestCaseAliasInfo // Maps alias to test case info +// Result represents the result of graph building with enhanced metadata +type Result struct { + Graph map[string][]string // Adjacency list representation + ExecutionOrder []string // Topologically sorted execution order + Dependencies []rules.Dependency // All inter-manifest dependencies + IntraManifestDeps map[string][]rules.Dependency // Dependencies within manifests + SaveRequirements map[string]SaveRequirement // What data needs to be saved + AliasToManifest map[string]string // Maps alias to manifest ID + TestCaseAliases map[string]TestCaseAliasInfo // Maps alias to test case info } // SaveRequirement defines what data needs to be saved from a manifest execution @@ -54,49 +48,57 @@ type TestCaseAliasInfo struct { Consumers []string // Which manifests/test cases consume this alias } -// NewGraphBuilderV2 creates a new graph builder with rule registry -func NewGraphBuilderV2(registry *RuleRegistry) *GraphBuilderV2 { - return &GraphBuilderV2{ +type Node struct { + ID string + Priority int +} + +// NewGraphBuilder creates a new graph builder with rule registry +func NewGraphBuilder(registry *rules.RuleRegistry) *Builder { + if registry == nil { + registry = rules.DefaultRuleRegistry() + } + + return &Builder{ registry: registry, manifestPriority: make(map[string]int), templateRegex: regexp.MustCompile(`\{\{\s*([a-zA-Z][a-zA-Z0-9_-]*)\.(.*?)\s*}}`), } } -// BuildGraphWithRules builds dependency graph using registered rules -func (gb *GraphBuilderV2) BuildGraphWithRules(manifests []manifests.Manifest) (*GraphResultV2, error) { - result := &GraphResultV2{ - Graph: make(map[string][]string), - Dependencies: make([]Dependency, 0), - IntraManifestDeps: make(map[string][]Dependency), - SaveRequirements: make(map[string]SaveRequirement), - ManifestPriorities: make(map[string]int), - AliasToManifest: make(map[string]string), - TestCaseAliases: make(map[string]TestCaseAliasInfo), +// Build builds dependency graph using registered rules +func (b *Builder) Build(manifests ...manifests.Manifest) (*Result, error) { + result := &Result{ + Graph: make(map[string][]string), + Dependencies: make([]rules.Dependency, 0), + IntraManifestDeps: make(map[string][]rules.Dependency), + SaveRequirements: make(map[string]SaveRequirement), + AliasToManifest: make(map[string]string), + TestCaseAliases: make(map[string]TestCaseAliasInfo), } // Step 1: Initialize manifest priorities and collect aliases - if err := gb.initializeManifests(manifests, result); err != nil { + if err := b.initializeManifests(manifests, result); err != nil { return nil, err } - // Step 2: Analyze dependencies using all rules (but ignore explicit dependencies) - allDependencies, err := gb.analyzeAllDependencies(manifests) + // Step 2: Analyze dependencies using all rules + allDependencies, err := b.analyzeDependencies(manifests) if err != nil { return nil, err } // Step 3: Separate inter-manifest and intra-manifest dependencies - gb.categorizeDependencies(allDependencies, result) + b.categorizeDependencies(allDependencies, result) // Step 4: Build adjacency graph from dependencies - gb.buildAdjacencyGraph(result) + b.buildAdjacencyGraph(result) // Step 5: Calculate save requirements - gb.calculateSaveRequirements(result) + b.calculateSaveRequirements(result) // Step 6: Build execution order using topological sort with priorities - executionOrder, err := gb.buildExecutionOrder(manifests, result.Dependencies) + executionOrder, err := b.buildExecutionOrder(manifests, result.Dependencies) if err != nil { return nil, err } @@ -106,48 +108,61 @@ func (gb *GraphBuilderV2) BuildGraphWithRules(manifests []manifests.Manifest) (* } // initializeManifests sets up manifest priorities and collects alias information -func (gb *GraphBuilderV2) initializeManifests(manifests []manifests.Manifest, result *GraphResultV2) error { - for _, manifest := range manifests { +func (b *Builder) initializeManifests(mans []manifests.Manifest, result *Result) error { + var err error + + for _, manifest := range mans { manifestID := manifest.GetID() // Set priority - priority := gb.getManifestPriority(manifest) - gb.manifestPriority[manifestID] = priority - result.ManifestPriorities[manifestID] = priority + priority := b.getManifestPriority(manifest) + b.manifestPriority[manifestID] = priority + + // Collect initial info + switch manifest.GetKind() { + case manifests.HttpTestKind: + if err = b.setUpHttpManifest(manifest.(*api.Http), result); err != nil { + return err + } + } + } - // Collect test case aliases for HTTP tests - if httpTest, ok := manifest.(*api.Http); ok { - for i, testCase := range httpTest.Spec.Cases { - if testCase.Alias != nil { - alias := *testCase.Alias - result.AliasToManifest[alias] = manifestID - result.TestCaseAliases[alias] = TestCaseAliasInfo{ - ManifestID: manifestID, - Alias: alias, - TestCaseIndex: i, - RequiredPaths: make([]string, 0), - Consumers: make([]string, 0), - } + return nil +} + +func (b *Builder) setUpHttpManifest(httpTest *api.Http, result *Result) error { + id := httpTest.GetID() + for i, testCase := range httpTest.Spec.Cases { + if testCase.Alias != nil { + alias := *testCase.Alias + if alreadyAddedID, exists := result.AliasToManifest[alias]; !exists { + result.AliasToManifest[alias] = id + result.TestCaseAliases[alias] = TestCaseAliasInfo{ + ManifestID: id, + Alias: alias, + TestCaseIndex: i, + RequiredPaths: make([]string, 0), + Consumers: make([]string, 0), } + } else { + if id == alreadyAddedID { + caseIndex := result.TestCaseAliases[alias].TestCaseIndex + return fmt.Errorf("found duplicate alias %s in manifest %s cases [#%d - %s] and [#%d - %s]", alias, id, caseIndex+1, httpTest.Spec.Cases[caseIndex].Name, i+1, testCase.Name) + } + return fmt.Errorf("found duplicate alias %s for manifests %s and %s case [#%d - %s]", alias, alreadyAddedID, id, i, testCase.Name) } } } - return nil } -// analyzeAllDependencies analyzes dependencies using all registered rules -func (gb *GraphBuilderV2) analyzeAllDependencies(manifests []manifests.Manifest) ([]Dependency, error) { - var allDependencies []Dependency +// analyzeDependencies analyzes dependencies using all registered rules +func (b *Builder) analyzeDependencies(manifests []manifests.Manifest) ([]rules.Dependency, error) { + var allDependencies []rules.Dependency for _, manifest := range manifests { - for _, rule := range gb.registry.GetRules() { + for _, rule := range b.registry.GetRules() { if rule.CanHandle(manifest) { - // Skip explicit dependency rule - we want to build dependencies ourselves - if rule.Name() == "explicit" { - continue - } - deps, err := rule.AnalyzeDependencies(manifest) if err != nil { return nil, fmt.Errorf("rule %s failed for manifest %s: %w", rule.Name(), manifest.GetID(), err) @@ -158,7 +173,7 @@ func (gb *GraphBuilderV2) analyzeAllDependencies(manifests []manifests.Manifest) } // Add smart template-based dependencies - smartDeps, err := gb.analyzeSmartTemplateDependencies(manifests, allDependencies) + smartDeps, err := b.analyzeSmartTemplateDependencies(manifests, allDependencies) if err != nil { return nil, err } @@ -168,8 +183,8 @@ func (gb *GraphBuilderV2) analyzeAllDependencies(manifests []manifests.Manifest) } // analyzeSmartTemplateDependencies creates inter-manifest dependencies based on template analysis -func (gb *GraphBuilderV2) analyzeSmartTemplateDependencies(mans []manifests.Manifest, _ []Dependency) ([]Dependency, error) { - var smartDeps []Dependency +func (b *Builder) analyzeSmartTemplateDependencies(mans []manifests.Manifest, _ []rules.Dependency) ([]rules.Dependency, error) { + var smartDeps []rules.Dependency aliasToManifest := make(map[string]string) // Build alias to manifest mapping @@ -189,7 +204,7 @@ func (gb *GraphBuilderV2) analyzeSmartTemplateDependencies(mans []manifests.Mani if httpTest, ok := manifest.(*api.Http); ok { // Find all template references in this manifest - templateRefs := gb.extractAllTemplateReferences(httpTest) + templateRefs := b.extractAllTemplateReferences(httpTest) // Group by alias and create dependencies aliasGroups := make(map[string][]string) @@ -201,30 +216,28 @@ func (gb *GraphBuilderV2) analyzeSmartTemplateDependencies(mans []manifests.Mani // Check if this alias refers to another manifest if targetManifestID, exists := aliasToManifest[alias]; exists && targetManifestID != manifestID { // This is an inter-manifest dependency - smartDeps = append(smartDeps, Dependency{ + smartDeps = append(smartDeps, rules.Dependency{ From: manifestID, To: targetManifestID, - Type: DependencyTypeTemplate, - Metadata: map[string]any{ - "alias": alias, - "required_paths": paths, - "save_required": true, - "smart_detected": true, + Type: rules.DependencyTypeTemplate, + Metadata: rules.DependencyMetadata{ + Alias: alias, + Paths: paths, + Save: true, }, }) } else if alias == "Values" { // Check if this is a reference to Values manifest for _, valuesManifest := range mans { if valuesManifest.GetKind() == manifests.ValuesKind { - smartDeps = append(smartDeps, Dependency{ + smartDeps = append(smartDeps, rules.Dependency{ From: manifestID, To: valuesManifest.GetID(), - Type: DependencyTypeTemplate, - Metadata: map[string]any{ - "alias": alias, - "required_paths": paths, - "save_required": true, - "smart_detected": true, + Type: rules.DependencyTypeValue, + Metadata: rules.DependencyMetadata{ + Alias: alias, + Paths: paths, + Save: true, }, }) break @@ -239,34 +252,34 @@ func (gb *GraphBuilderV2) analyzeSmartTemplateDependencies(mans []manifests.Mani } // extractAllTemplateReferences extracts all template references from an HTTP test -func (gb *GraphBuilderV2) extractAllTemplateReferences(httpTest *api.Http) []TemplateReference { - var references []TemplateReference +func (b *Builder) extractAllTemplateReferences(httpTest *api.Http) []rules.TemplateReference { + var references []rules.TemplateReference for _, testCase := range httpTest.Spec.Cases { // Check endpoint - refs := gb.findTemplateReferencesInString(testCase.Endpoint) + refs := b.findTemplateReferencesInString(testCase.Endpoint) references = append(references, refs...) // Check URL - refs = gb.findTemplateReferencesInString(testCase.Url) + refs = b.findTemplateReferencesInString(testCase.Url) references = append(references, refs...) // Check headers for _, value := range testCase.Headers { - refs = gb.findTemplateReferencesInString(value) + refs = b.findTemplateReferencesInString(value) references = append(references, refs...) } // Check body recursively if testCase.Body != nil { - refs = gb.findTemplateReferencesInValue(testCase.Body) + refs = b.findTemplateReferencesInValue(testCase.Body) references = append(references, refs...) } // Check assertions for _, assert := range testCase.Assert { if assert.Template != "" { - refs = gb.findTemplateReferencesInString(assert.Template) + refs = b.findTemplateReferencesInString(assert.Template) references = append(references, refs...) } } @@ -276,13 +289,13 @@ func (gb *GraphBuilderV2) extractAllTemplateReferences(httpTest *api.Http) []Tem } // findTemplateReferencesInString finds template references in a string -func (gb *GraphBuilderV2) findTemplateReferencesInString(str string) []TemplateReference { - var references []TemplateReference - matches := gb.templateRegex.FindAllStringSubmatch(str, -1) +func (b *Builder) findTemplateReferencesInString(str string) []rules.TemplateReference { + var references []rules.TemplateReference + matches := b.templateRegex.FindAllStringSubmatch(str, -1) for _, match := range matches { if len(match) >= 3 { - references = append(references, TemplateReference{ + references = append(references, rules.TemplateReference{ Alias: match[1], Path: match[2], }) @@ -293,23 +306,23 @@ func (gb *GraphBuilderV2) findTemplateReferencesInString(str string) []TemplateR } // findTemplateReferencesInValue recursively finds template references in any value -func (gb *GraphBuilderV2) findTemplateReferencesInValue(value any) []TemplateReference { - var references []TemplateReference +func (b *Builder) findTemplateReferencesInValue(value any) []rules.TemplateReference { + var references []rules.TemplateReference switch v := value.(type) { case string: - references = append(references, gb.findTemplateReferencesInString(v)...) + references = append(references, b.findTemplateReferencesInString(v)...) case map[string]any: for _, val := range v { - references = append(references, gb.findTemplateReferencesInValue(val)...) + references = append(references, b.findTemplateReferencesInValue(val)...) } case []any: for _, val := range v { - references = append(references, gb.findTemplateReferencesInValue(val)...) + references = append(references, b.findTemplateReferencesInValue(val)...) } case map[any]any: for _, val := range v { - references = append(references, gb.findTemplateReferencesInValue(val)...) + references = append(references, b.findTemplateReferencesInValue(val)...) } } @@ -317,15 +330,15 @@ func (gb *GraphBuilderV2) findTemplateReferencesInValue(value any) []TemplateRef } // categorizeDependencies separates inter-manifest and intra-manifest dependencies -func (gb *GraphBuilderV2) categorizeDependencies(allDependencies []Dependency, result *GraphResultV2) { +func (b *Builder) categorizeDependencies(allDependencies []rules.Dependency, result *Result) { for _, dep := range allDependencies { - fromManifest := gb.getBaseManifestID(dep.From) - toManifest := gb.getBaseManifestID(dep.To) + fromManifest := b.getBaseManifestID(dep.From) + toManifest := b.getBaseManifestID(dep.To) if fromManifest == toManifest { // Intra-manifest dependency if result.IntraManifestDeps[fromManifest] == nil { - result.IntraManifestDeps[fromManifest] = make([]Dependency, 0) + result.IntraManifestDeps[fromManifest] = make([]rules.Dependency, 0) } result.IntraManifestDeps[fromManifest] = append(result.IntraManifestDeps[fromManifest], dep) } else { @@ -336,10 +349,10 @@ func (gb *GraphBuilderV2) categorizeDependencies(allDependencies []Dependency, r } // buildAdjacencyGraph builds the adjacency graph from dependencies -func (gb *GraphBuilderV2) buildAdjacencyGraph(result *GraphResultV2) { +func (b *Builder) buildAdjacencyGraph(result *Result) { for _, dep := range result.Dependencies { - toManifest := gb.getBaseManifestID(dep.To) - fromManifest := gb.getBaseManifestID(dep.From) + toManifest := b.getBaseManifestID(dep.To) + fromManifest := b.getBaseManifestID(dep.From) if result.Graph[toManifest] == nil { result.Graph[toManifest] = make([]string, 0) @@ -349,34 +362,27 @@ func (gb *GraphBuilderV2) buildAdjacencyGraph(result *GraphResultV2) { } // calculateSaveRequirements determines what data needs to be saved -func (gb *GraphBuilderV2) calculateSaveRequirements(result *GraphResultV2) { +func (b *Builder) calculateSaveRequirements(result *Result) { // Process inter-manifest dependencies for _, dep := range result.Dependencies { - if dep.Type == DependencyTypeTemplate { - toManifest := gb.getBaseManifestID(dep.To) + if dep.Type == rules.DependencyTypeTemplate { + toManifest := b.getBaseManifestID(dep.To) // Update save requirement for the target manifest req := result.SaveRequirements[toManifest] req.Required = true req.Consumers = append(req.Consumers, dep.From) - if paths, ok := dep.Metadata["required_paths"].([]string); ok { - req.RequiredPaths = append(req.RequiredPaths, paths...) - req.Paths = append(req.Paths, paths...) - } + req.RequiredPaths = append(req.RequiredPaths, dep.Metadata.Paths...) + req.Paths = append(req.Paths, dep.Metadata.Paths...) result.SaveRequirements[toManifest] = req // Update test case alias info if applicable - var paths []string - if alias, ok := dep.Metadata["alias"].(string); ok { - if aliasInfo, exists := result.TestCaseAliases[alias]; exists { - aliasInfo.Consumers = append(aliasInfo.Consumers, dep.From) - if paths, ok = dep.Metadata["required_paths"].([]string); ok { - aliasInfo.RequiredPaths = append(aliasInfo.RequiredPaths, paths...) - } - result.TestCaseAliases[alias] = aliasInfo - } + if aliasInfo, exists := result.TestCaseAliases[dep.Metadata.Alias]; exists { + aliasInfo.Consumers = append(aliasInfo.Consumers, dep.From) + aliasInfo.RequiredPaths = append(aliasInfo.RequiredPaths, dep.Metadata.Paths...) + result.TestCaseAliases[dep.Metadata.Alias] = aliasInfo } } } @@ -388,10 +394,8 @@ func (gb *GraphBuilderV2) calculateSaveRequirements(result *GraphResultV2) { req.Consumers = append(req.Consumers, manifestID) // Self-consumer for _, dep := range deps { - if paths, ok := dep.Metadata["required_paths"].([]string); ok { - req.RequiredPaths = append(req.RequiredPaths, paths...) - req.Paths = append(req.Paths, paths...) - } + req.RequiredPaths = append(req.RequiredPaths, dep.Metadata.Paths...) + req.Paths = append(req.Paths, dep.Metadata.Paths...) } result.SaveRequirements[manifestID] = req @@ -399,7 +403,7 @@ func (gb *GraphBuilderV2) calculateSaveRequirements(result *GraphResultV2) { } // buildExecutionOrder creates topologically sorted execution order -func (gb *GraphBuilderV2) buildExecutionOrder(manifests []manifests.Manifest, dependencies []Dependency) ([]string, error) { +func (b *Builder) buildExecutionOrder(manifests []manifests.Manifest, dependencies []rules.Dependency) ([]string, error) { // Initialize in-degree count for each manifest inDegree := make(map[string]int) for _, manifest := range manifests { @@ -409,10 +413,10 @@ func (gb *GraphBuilderV2) buildExecutionOrder(manifests []manifests.Manifest, de // Calculate in-degrees from all inter-manifest dependencies for _, dep := range dependencies { - fromBase := gb.getBaseManifestID(dep.From) - toBase := gb.getBaseManifestID(dep.To) + fromBase := b.getBaseManifestID(dep.From) + toBase := b.getBaseManifestID(dep.To) if fromBase != toBase { - if _, exists := inDegree[fromBase]; exists && dep.Type != DependencyTypeTemplate { + if _, exists := inDegree[fromBase]; exists && dep.Type != rules.DependencyTypeTemplate { inDegree[fromBase]++ } } @@ -424,7 +428,7 @@ func (gb *GraphBuilderV2) buildExecutionOrder(manifests []manifests.Manifest, de if degree == 0 { zeroInDegreeNodes = append(zeroInDegreeNodes, &Node{ ID: id, - Priority: gb.manifestPriority[id], + Priority: b.manifestPriority[id], }) } } @@ -447,14 +451,14 @@ func (gb *GraphBuilderV2) buildExecutionOrder(manifests []manifests.Manifest, de newNodes := make([]*Node, 0) for _, dep := range dependencies { - fromBase := gb.getBaseManifestID(dep.From) - toBase := gb.getBaseManifestID(dep.To) + fromBase := b.getBaseManifestID(dep.From) + toBase := b.getBaseManifestID(dep.To) if toBase == currentNode.ID && fromBase != toBase { inDegree[fromBase]-- if inDegree[fromBase] == 0 { newNodes = append(newNodes, &Node{ ID: fromBase, - Priority: gb.manifestPriority[fromBase], + Priority: b.manifestPriority[fromBase], }) } } @@ -484,29 +488,21 @@ func (gb *GraphBuilderV2) buildExecutionOrder(manifests []manifests.Manifest, de } // getManifestPriority returns priority for a manifest based on its kind -func (gb *GraphBuilderV2) getManifestPriority(manifest manifests.Manifest) int { +func (b *Builder) getManifestPriority(manifest manifests.Manifest) int { kind := manifest.GetKind() // Find kind priority rule - for _, rule := range gb.registry.GetRules() { - if kindRule, ok := rule.(*KindPriorityRule); ok { - return kindRule.GetKindPriority(kind) + for _, rule := range b.registry.GetRules() { + if rule.Name() == rules.KindPriorityRuleName { + return rule.(*rules.KindPriorityRule).GetKindPriority(kind) } } - return gb.getManifestPriorityByID(manifest.GetID()) -} - -// getManifestPriorityByID returns priority for a manifest by its ID -func (gb *GraphBuilderV2) getManifestPriorityByID(manifestID string) int { - if priority, exists := gb.manifestPriority[manifestID]; exists { - return priority - } - return 0 + return kinds.PriorityMap[kind] } // getBaseManifestID extracts base manifest ID from potentially extended ID -func (gb *GraphBuilderV2) getBaseManifestID(id string) string { +func (b *Builder) getBaseManifestID(id string) string { // Remove any suffix after # (for test case aliases) if idx := strings.Index(id, "#"); idx != -1 { return id[:idx] diff --git a/internal/core/runner/depends/graph_test.go b/internal/core/runner/depends/graph_test.go index 40d0a66..dc5e8b0 100644 --- a/internal/core/runner/depends/graph_test.go +++ b/internal/core/runner/depends/graph_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" + "github.com/apiqube/cli/internal/core/runner/depends/rules" + "github.com/apiqube/cli/internal/core/manifests/kinds" "github.com/apiqube/cli/internal/core/manifests/kinds/servers" "github.com/apiqube/cli/internal/core/manifests/kinds/tests" @@ -51,10 +53,10 @@ func TestGraphBuilder(t *testing.T) { }, } - registry := DefaultRuleRegistry() - builder := NewGraphBuilderV2(registry) + registry := rules.DefaultRuleRegistry() + builder := NewGraphBuilder(registry) - result, err := builder.BuildGraphWithRules(mans) + result, err := builder.Build(mans...) if err != nil { t.Fatalf("Failed to build graph: %v", err) } @@ -123,10 +125,10 @@ func TestGraphBuilder(t *testing.T) { }, } - registry := DefaultRuleRegistry() - builder := NewGraphBuilderV2(registry) + registry := rules.DefaultRuleRegistry() + builder := NewGraphBuilder(registry) - result, err := builder.BuildGraphWithRules(mans) + result, err := builder.Build(mans...) if err != nil { t.Fatalf("Failed to build graph: %v", err) } @@ -190,10 +192,10 @@ func TestGraphBuilder(t *testing.T) { }, } - registry := DefaultRuleRegistry() - builder := NewGraphBuilderV2(registry) + registry := rules.DefaultRuleRegistry() + builder := NewGraphBuilder(registry) - result, err := builder.BuildGraphWithRules(mans) + result, err := builder.Build(mans...) if err != nil { t.Fatalf("Failed to build graph: %v", err) } @@ -212,7 +214,7 @@ func TestGraphBuilder(t *testing.T) { } // Should have no inter-manifest dependencies (no cycles) - if len(result.Dependencies) != 1 { + if len(result.Dependencies) != 0 { t.Errorf("Expected no inter-manifest dependencies, got %d", len(result.Dependencies)) } }) @@ -343,10 +345,10 @@ func TestGraphBuilder(t *testing.T) { }, } - registry := DefaultRuleRegistry() - builder := NewGraphBuilderV2(registry) + registry := rules.DefaultRuleRegistry() + builder := NewGraphBuilder(registry) - result, err := builder.BuildGraphWithRules(mans) + result, err := builder.Build(mans...) if err != nil { t.Fatalf("Failed to build graph: %v", err) } @@ -555,10 +557,10 @@ func TestGraphBuilder(t *testing.T) { }, } - registry := DefaultRuleRegistry() - builder := NewGraphBuilderV2(registry) + registry := rules.DefaultRuleRegistry() + builder := NewGraphBuilder(registry) - result, err := builder.BuildGraphWithRules(mans) + result, err := builder.Build(mans...) if err != nil { t.Fatalf("Failed to build graph: %v", err) } @@ -572,17 +574,17 @@ func TestGraphBuilder(t *testing.T) { } _, kind, _ := utils.ParseManifestID(mans[0].GetID()) - prev := priorities[kind] + prev := kinds.PriorityMap[kind] for i := 1; i < len(result.ExecutionOrder); i++ { - if _, kind, _ = utils.ParseManifestID(result.ExecutionOrder[i]); prev > priorities[kind] { - t.Errorf("Expected %s to have lower priority than %s (%d), got %d", result.ExecutionOrder[i], result.ExecutionOrder[i-1], prev, priorities[kind]) + if _, kind, _ = utils.ParseManifestID(result.ExecutionOrder[i]); prev > kinds.PriorityMap[kind] { + t.Errorf("Expected %s to have lower priority than %s (%d), got %d", result.ExecutionOrder[i], result.ExecutionOrder[i-1], prev, kinds.PriorityMap[kind]) } } }) } // PrintDependencyGraph prints a beautiful visualization of the dependency graph -func printDependencyGraph(gb *GraphBuilderV2, result *GraphResultV2) { +func printDependencyGraph(_ *Builder, result *Result) { fmt.Println("\n" + strings.Repeat("=", 80)) fmt.Println("πŸ”— DEPENDENCY GRAPH VISUALIZATION") fmt.Println(strings.Repeat("=", 80)) @@ -591,26 +593,11 @@ func printDependencyGraph(gb *GraphBuilderV2, result *GraphResultV2) { fmt.Println("\nπŸ“‹ EXECUTION ORDER:") fmt.Println(strings.Repeat("-", 40)) for i, manifestID := range result.ExecutionOrder { - priority := gb.getManifestPriorityByID(manifestID) + _, kind, _ := utils.ParseManifestID(manifestID) + priority := kinds.PriorityMap[kind] fmt.Printf(" %d. %s (priority: %d)\n", i+1, manifestID, priority) } - // Print inter-manifest dependencies - fmt.Println("\nπŸ”„ INTER-MANIFEST DEPENDENCIES:") - fmt.Println(strings.Repeat("-", 40)) - if len(result.Dependencies) == 0 { - fmt.Println(" βœ… No inter-manifest dependencies found") - } else { - for _, dep := range result.Dependencies { - fmt.Printf(" %s ──(%s)──> %s\n", dep.From, dep.Type, dep.To) - if dep.Metadata != nil { - for key, value := range dep.Metadata { - fmt.Printf(" └─ %s: %v\n", key, value) - } - } - } - } - // Print intra-manifest dependencies fmt.Println("\n🏠 INTRA-MANIFEST DEPENDENCIES:") fmt.Println(strings.Repeat("-", 40)) @@ -621,11 +608,13 @@ func printDependencyGraph(gb *GraphBuilderV2, result *GraphResultV2) { fmt.Printf(" πŸ“¦ %s:\n", manifestID) for _, dep := range deps { fmt.Printf(" %s ──(%s)──> %s\n", dep.From, dep.Type, dep.To) - if dep.Metadata != nil { - for key, value := range dep.Metadata { - fmt.Printf(" └─ %s: %v\n", key, value) - } - } + m := dep.Metadata + fmt.Printf(" └─ %s: %v\n", "alias", m.Alias) + fmt.Printf(" └─ %s: %v\n", "paths", m.Paths) + fmt.Printf(" └─ %s: %v\n", "locations", m.Locations) + fmt.Printf(" └─ %s: %v\n", "Save", m.Save) + fmt.Printf(" └─ %s: %v\n", "Case Name", m.CaseName) + fmt.Printf(" └─ %s: %v\n", "Manifest Kind", m.ManifestKind) } } } diff --git a/internal/core/runner/depends/pass_integration.go b/internal/core/runner/depends/pass_integration.go index aa52a9a..70ab99b 100644 --- a/internal/core/runner/depends/pass_integration.go +++ b/internal/core/runner/depends/pass_integration.go @@ -1,69 +1,81 @@ package depends import ( - "encoding/json" "fmt" + "reflect" "strings" + "sync" + "time" + + "github.com/apiqube/cli/internal/core/runner/depends/rules" + "github.com/goccy/go-json" + "github.com/tidwall/gjson" "github.com/apiqube/cli/internal/core/runner/interfaces" ) // PassManager handles automatic data passing between tests type PassManager struct { - ctx interfaces.ExecutionContext + mx sync.RWMutex saveRequirements map[string]SaveRequirement - graphResult *GraphResultV2 + graphResult *Result + channels map[string]chan any } -func NewPassManager(ctx interfaces.ExecutionContext, graphResult *GraphResultV2) *PassManager { +func NewPassManager(graphResult *Result) *PassManager { return &PassManager{ - ctx: ctx, + mx: sync.RWMutex{}, saveRequirements: graphResult.SaveRequirements, graphResult: graphResult, + channels: make(map[string]chan any), + } +} + +func (m *PassManager) Close() { + for _, ch := range m.channels { + close(ch) } } // ShouldSaveResult determines if a test result should be saved for passing -func (pm *PassManager) ShouldSaveResult(manifestID string) bool { - req, exists := pm.saveRequirements[manifestID] +func (m *PassManager) ShouldSaveResult(manifestID string) bool { + req, exists := m.saveRequirements[manifestID] return exists && req.Required } // SaveTestResult saves test result data for future use -func (pm *PassManager) SaveTestResult(manifestID string, result TestResult) error { - req, exists := pm.saveRequirements[manifestID] +func (m *PassManager) SaveTestResult(ctx interfaces.ExecutionContext, manifestID string, data TestData) error { + req, exists := m.saveRequirements[manifestID] if !exists || !req.Required { return nil // No need to save } - // Save the complete result first - pm.ctx.Set(manifestID, result) - // Save specific paths if required for _, path := range req.Paths { - value, err := pm.extractValueByPath(result, path) + value, err := m.extractValueByPath(data, path) if err != nil { + ctx.GetOutput().Logf(interfaces.ErrorLevel, "failed to extract value for path: %s", path) // Log warning but don't fail - the path might be optional continue } key := fmt.Sprintf("%s.%s", manifestID, path) - pm.ctx.Set(key, value) - } + ctx.SetTyped(key, value, reflect.TypeOf(value).Kind()) - // Send to PassStore channels for any waiting consumers - pm.notifyWaitingConsumers(manifestID, result) + // Send to PassStore channels for any waiting consumers + m.notifyConsumers(ctx, manifestID, data) + } return nil } -// TestResult represents the result of a test execution -type TestResult struct { +// TestData represents the result of a test execution +type TestData struct { Request RequestData `json:"request"` Response ResponseData `json:"response"` Status int `json:"status"` Headers map[string]string `json:"headers"` - Duration int64 `json:"duration_ms"` + Duration time.Duration `json:"duration"` Error string `json:"error,omitempty"` } @@ -80,141 +92,53 @@ type ResponseData struct { Body any `json:"body"` } -// extractValueByPath extracts a value from test result using dot notation path -func (pm *PassManager) extractValueByPath(result TestResult, path string) (any, error) { - // Convert result to map for easier navigation - resultMap := pm.structToMap(result) - - // Navigate the path - return pm.navigatePath(resultMap, path) -} - -// structToMap converts struct to map using JSON marshaling -func (pm *PassManager) structToMap(v any) map[string]any { - data, _ := json.Marshal(v) - var result map[string]any - _ = json.Unmarshal(data, &result) - return result -} - -// navigatePath navigates a dot-notation path in a map structure -func (pm *PassManager) navigatePath(data any, path string) (any, error) { +// extractValueByPath extracts a value from test result using gjson +func (m *PassManager) extractValueByPath(result TestData, path string) (any, error) { if path == "" || path == "*" { - return data, nil - } - - parts := strings.Split(path, ".") - current := data - - for _, part := range parts { - // Handle array indexing like "data[0]" or "data[-1]" - if strings.Contains(part, "[") && strings.Contains(part, "]") { - current = pm.handleArrayAccess(current, part) - if current == nil { - return nil, fmt.Errorf("array access failed for part: %s", part) - } - } else { - // Handle map access - if m, ok := current.(map[string]any); ok { - if val, exists := m[part]; exists { - current = val - } else { - return nil, fmt.Errorf("path not found: %s", part) - } - } else { - return nil, fmt.Errorf("cannot access field %s on non-map type", part) - } - } - } - - return current, nil -} - -// handleArrayAccess handles array access patterns like "data[0]", "data[-1]", "data[*]" -func (pm *PassManager) handleArrayAccess(data any, part string) any { - // Extract field name and index - openBracket := strings.Index(part, "[") - closeBracket := strings.Index(part, "]") - - if openBracket == -1 || closeBracket == -1 { - return nil + return result, nil } - fieldName := part[:openBracket] - indexStr := part[openBracket+1 : closeBracket] - - // Get the field first - var fieldValue any - if fieldName != "" { - if m, ok := data.(map[string]any); ok { - if val, exists := m[fieldName]; exists { - fieldValue = val - } else { - return nil - } - } else { - return nil - } - } else { - fieldValue = data + data, err := json.Marshal(result) + if err != nil { + return nil, err } - // Handle array access - if arr, ok := fieldValue.([]any); ok { - return pm.accessArray(arr, indexStr) + res := gjson.ParseBytes(data).Get(path) + if !res.Exists() { + return result, nil } - return nil + return res.Value(), nil } -// accessArray handles different array access patterns -func (pm *PassManager) accessArray(arr []any, indexStr string) any { - switch indexStr { - case "*": - // Return entire array - return arr - case "-1": - // Return last element - if len(arr) > 0 { - return arr[len(arr)-1] - } - return nil - default: - // Try to parse as integer index - var index int - if _, err := fmt.Sscanf(indexStr, "%d", &index); err != nil { - return nil - } +// notifyConsumers sends data to PassStore channels +func (m *PassManager) notifyConsumers(ctx interfaces.ExecutionContext, manifestID string, result TestData) { + // Get dependents of this manifest + dependents := m.graphResult.GetDependentsOf(manifestID) - // Handle negative indices - if index < 0 { - index = len(arr) + index - } + m.mx.Lock() + defer m.mx.Unlock() - if index >= 0 && index < len(arr) { - return arr[index] - } - return nil - } -} + for _, dep := range dependents { + if dep.Type == rules.DependencyTypeTemplate { + from := dep.From + if dep.Metadata.Alias != "" { + from = dep.Metadata.Alias + } -// notifyWaitingConsumers sends data to PassStore channels -func (pm *PassManager) notifyWaitingConsumers(manifestID string, result TestResult) { - // Get dependents of this manifest - dependents := pm.graphResult.GetDependentsOf(manifestID) + _, exists := m.channels[from] + if !exists { + m.channels[from] = ctx.Channel(from) + } - for _, dep := range dependents { - if dep.Type == DependencyTypeTemplate || dep.Type == DependencyTypeValue { // Send complete result - pm.ctx.SafeSend(manifestID, result) + ctx.SafeSend(from, result) // Send specific paths if specified in metadata - if paths, ok := dep.Metadata["required_paths"].([]string); ok { - for _, path := range paths { - if value, err := pm.extractValueByPath(result, path); err == nil { - key := fmt.Sprintf("%s.%s", manifestID, path) - pm.ctx.SafeSend(key, value) - } + for _, path := range dep.Metadata.Paths { + if value, err := m.extractValueByPath(result, path); err == nil { + key := fmt.Sprintf("%s.%s", from, path) + ctx.SafeSend(key, value) } } } @@ -222,55 +146,55 @@ func (pm *PassManager) notifyWaitingConsumers(manifestID string, result TestResu } // WaitForDependency waits for a dependency to be available -func (pm *PassManager) WaitForDependency(manifestID, dependencyAlias, path string) (any, error) { +func (m *PassManager) WaitForDependency(ctx interfaces.ExecutionContext, dependencyAlias, path string) (any, error) { // Try to get from DataStore first (synchronous) key := fmt.Sprintf("%s.%s", dependencyAlias, path) - if value, exists := pm.ctx.Get(key); exists { + if value, exists := ctx.Get(key); exists { return value, nil } // If not available, wait on channel (asynchronous) - ch := pm.ctx.Channel(key) + ch := ctx.Channel(key) select { case value := <-ch: return value, nil - case <-pm.ctx.Done(): + case <-ctx.Done(): return nil, fmt.Errorf("context cancelled while waiting for dependency %s", key) } } // GetDependencyValue gets a dependency value with fallback to waiting -func (pm *PassManager) GetDependencyValue(manifestID, dependencyAlias, path string) (any, error) { +func (m *PassManager) GetDependencyValue(ctx interfaces.ExecutionContext, dependencyAlias, path string) (any, error) { // First try direct access fullKey := fmt.Sprintf("%s.%s", dependencyAlias, path) - if value, exists := pm.ctx.Get(fullKey); exists { + if value, exists := ctx.Get(fullKey); exists { return value, nil } // Try getting the full result and extracting the path - if result, exists := pm.ctx.Get(dependencyAlias); exists { - if testResult, ok := result.(TestResult); ok { - return pm.extractValueByPath(testResult, path) + if result, exists := ctx.Get(dependencyAlias); exists { + if testResult, ok := result.(TestData); ok { + return m.extractValueByPath(testResult, path) } } // Last resort: wait for the dependency - return pm.WaitForDependency(manifestID, dependencyAlias, path) + return m.WaitForDependency(ctx, dependencyAlias, path) } // ResolveTemplateValue resolves a template value like "{{ users-list.response.body.data[0].id }}" -func (pm *PassManager) ResolveTemplateValue(manifestID, templateStr string) (any, error) { +func (m *PassManager) ResolveTemplateValue(ctx interfaces.ExecutionContext, templateStr string) (any, error) { // Parse template string to extract alias and path - alias, path, err := pm.parseTemplateString(templateStr) + alias, path, err := m.parseTemplateString(templateStr) if err != nil { return nil, err } - return pm.GetDependencyValue(manifestID, alias, path) + return m.GetDependencyValue(ctx, alias, path) } // parseTemplateString parses "{{ alias.path }}" format -func (pm *PassManager) parseTemplateString(templateStr string) (alias, path string, err error) { +func (m *PassManager) parseTemplateString(templateStr string) (alias, path string, err error) { // Remove {{ and }} and trim spaces content := strings.TrimSpace(templateStr) if strings.HasPrefix(content, "{{") && strings.HasSuffix(content, "}}") { @@ -287,15 +211,15 @@ func (pm *PassManager) parseTemplateString(templateStr string) (alias, path stri } // GetSaveRequirement returns save requirement for a manifest -func (pm *PassManager) GetSaveRequirement(manifestID string) (SaveRequirement, bool) { - req, exists := pm.saveRequirements[manifestID] +func (m *PassManager) GetSaveRequirement(manifestID string) (SaveRequirement, bool) { + req, exists := m.saveRequirements[manifestID] return req, exists } // ListRequiredSaves returns all manifests that need to save data -func (pm *PassManager) ListRequiredSaves() map[string]SaveRequirement { +func (m *PassManager) ListRequiredSaves() map[string]SaveRequirement { result := make(map[string]SaveRequirement) - for id, req := range pm.saveRequirements { + for id, req := range m.saveRequirements { if req.Required { result[id] = req } diff --git a/internal/core/runner/depends/rules.go b/internal/core/runner/depends/rules.go deleted file mode 100644 index 4e9b9ef..0000000 --- a/internal/core/runner/depends/rules.go +++ /dev/null @@ -1,256 +0,0 @@ -package depends - -import ( - "fmt" - "regexp" - "strings" - - "github.com/apiqube/cli/internal/core/manifests" -) - -// DependencyRule defines interface for dependency analysis rules -type DependencyRule interface { - // Name returns the rule name for debugging - Name() string - - // AnalyzeDependencies extracts dependencies from manifest - AnalyzeDependencies(manifest manifests.Manifest) ([]Dependency, error) - - // GetPriority returns priority for this type of dependency - GetPriority() int - - // CanHandle checks if this rule can handle the given manifest - CanHandle(manifest manifests.Manifest) bool -} - -// Dependency represents a dependency relationship -type Dependency struct { - From string // Source manifest ID - To string // Target manifest ID (what we depend on) - Type DependencyType // Type of dependency - Metadata map[string]any // Additional metadata (e.g., what data to save) -} - -type DependencyType string - -const ( - DependencyTypeExplicit DependencyType = "explicit" // From dependsOn field - DependencyTypeTemplate DependencyType = "template" // From template references - DependencyTypeValue DependencyType = "value" // From value passing -) - -// RuleRegistry manages dependency rules -type RuleRegistry struct { - rules []DependencyRule -} - -func NewRuleRegistry() *RuleRegistry { - return &RuleRegistry{ - rules: make([]DependencyRule, 0), - } -} - -func (r *RuleRegistry) Register(rule DependencyRule) { - r.rules = append(r.rules, rule) -} - -func (r *RuleRegistry) GetRules() []DependencyRule { - return r.rules -} - -// ExplicitDependencyRule handles explicit dependsOn declarations -type ExplicitDependencyRule struct{} - -func NewExplicitDependencyRule() *ExplicitDependencyRule { - return &ExplicitDependencyRule{} -} - -func (r *ExplicitDependencyRule) Name() string { - return "explicit" -} - -func (r *ExplicitDependencyRule) CanHandle(manifest manifests.Manifest) bool { - _, ok := manifest.(manifests.Dependencies) - return ok -} - -func (r *ExplicitDependencyRule) AnalyzeDependencies(manifest manifests.Manifest) ([]Dependency, error) { - dep, ok := manifest.(manifests.Dependencies) - if !ok { - return nil, nil - } - - var dependencies []Dependency - fromID := manifest.GetID() - - for _, toID := range dep.GetDependsOn() { - if toID == fromID { - return nil, fmt.Errorf("manifest %s cannot depend on itself", fromID) - } - - dependencies = append(dependencies, Dependency{ - From: fromID, - To: toID, - Type: DependencyTypeExplicit, - }) - } - - return dependencies, nil -} - -func (r *ExplicitDependencyRule) GetPriority() int { - return 100 // Highest priority for explicit dependencies -} - -// TemplateDependencyRule handles template-based dependencies ({{ alias.path }}) -type TemplateDependencyRule struct { - templateRegex *regexp.Regexp -} - -func NewTemplateDependencyRule() *TemplateDependencyRule { - // Regex to match {{ alias.path }} patterns - regex := regexp.MustCompile(`\{\{\s*([a-zA-Z][a-zA-Z0-9_-]*)\.(.*?)\s*}}`) - return &TemplateDependencyRule{ - templateRegex: regex, - } -} - -func (r *TemplateDependencyRule) Name() string { - return "template" -} - -func (r *TemplateDependencyRule) CanHandle(_ manifests.Manifest) bool { - // This rule can handle any manifest, we'll check content during analysis - return true -} - -func (r *TemplateDependencyRule) AnalyzeDependencies(manifest manifests.Manifest) ([]Dependency, error) { - var dependencies []Dependency - fromID := manifest.GetID() - - // Extract all template references from the manifest - references := r.extractTemplateReferences(manifest) - - // Group by alias to avoid duplicates and collect required paths - aliasData := make(map[string][]string) - for _, ref := range references { - aliasData[ref.Alias] = append(aliasData[ref.Alias], ref.Path) - } - - // Create dependencies with metadata about what data is needed - for alias, paths := range aliasData { - // Convert alias to full manifest ID (assuming same namespace for now) - // This might need to be more sophisticated based on your ID scheme - toID := r.resolveAliasToID(manifest, alias) - - dependencies = append(dependencies, Dependency{ - From: fromID, - To: toID, - Type: DependencyTypeTemplate, - Metadata: map[string]any{ - "alias": alias, - "required_paths": paths, - "save_required": true, - }, - }) - } - - return dependencies, nil -} - -func (r *TemplateDependencyRule) GetPriority() int { - return 50 // Medium priority for template dependencies -} - -// TemplateReference represents a parsed template reference -type TemplateReference struct { - Alias string // The alias part (e.g., "users-list") - Path string // The path part (e.g., "response.body.data[0].id") -} - -func (r *TemplateDependencyRule) extractTemplateReferences(manifest manifests.Manifest) []TemplateReference { - var references []TemplateReference - - // Convert manifest to string representation for parsing - // This is a simplified approach - in real implementation you might want - // to traverse the structure more carefully - manifestStr := fmt.Sprintf("%+v", manifest) - - matches := r.templateRegex.FindAllStringSubmatch(manifestStr, -1) - for _, match := range matches { - if len(match) >= 3 { - references = append(references, TemplateReference{ - Alias: match[1], - Path: match[2], - }) - } - } - - return references -} - -func (r *TemplateDependencyRule) resolveAliasToID(manifest manifests.Manifest, alias string) string { - // Simple resolution: assume same namespace and kind as current manifest - // Format: namespace.kind.alias - parts := strings.Split(manifest.GetID(), ".") - if len(parts) >= 2 { - return fmt.Sprintf("%s.%s.%s", parts[0], parts[1], alias) - } - return alias -} - -// KindPriorityRule handles kind-based priorities -type KindPriorityRule struct { - priorities map[string]int -} - -var priorities = map[string]int{ - manifests.ValuesKind: 1, - manifests.ServerKind: 10, - manifests.ServiceKind: 20, - manifests.HttpTestKind: 30, - manifests.HttpLoadTestKind: 40, -} - -func NewKindPriorityRule() *KindPriorityRule { - return &KindPriorityRule{ - priorities: priorities, - } -} - -func (r *KindPriorityRule) Name() string { - return "kind_priority" -} - -func (r *KindPriorityRule) CanHandle(_ manifests.Manifest) bool { - return true // Can handle any manifest for priority assignment -} - -func (r *KindPriorityRule) AnalyzeDependencies(_ manifests.Manifest) ([]Dependency, error) { - // This rule doesn't create dependencies, just provides priority info - return nil, nil -} - -func (r *KindPriorityRule) GetPriority() int { - return 0 // Lowest priority as this is just for ordering -} - -func (r *KindPriorityRule) GetKindPriority(kind string) int { - if priority, ok := r.priorities[kind]; ok { - return priority - } - return 0 -} - -// DefaultRuleRegistry creates a registry with default rules -func DefaultRuleRegistry() *RuleRegistry { - registry := NewRuleRegistry() - - // Register default rules - registry.Register(NewExplicitDependencyRule()) - registry.Register(NewTemplateDependencyRule()) - registry.Register(NewKindPriorityRule()) - registry.Register(NewHttpTestDependencyRule()) - - return registry -} diff --git a/internal/core/runner/depends/http_rules.go b/internal/core/runner/depends/rules/http.go similarity index 94% rename from internal/core/runner/depends/http_rules.go rename to internal/core/runner/depends/rules/http.go index 6921c21..0246c9d 100644 --- a/internal/core/runner/depends/http_rules.go +++ b/internal/core/runner/depends/rules/http.go @@ -1,4 +1,4 @@ -package depends +package rules import ( "fmt" @@ -9,6 +9,8 @@ import ( "github.com/apiqube/cli/internal/core/manifests/kinds/tests/api" ) +const HttpTestDependencyRuleName = "Http Test" + // HttpTestDependencyRule handles HTTP test specific dependencies type HttpTestDependencyRule struct { templateRegex *regexp.Regexp @@ -23,21 +25,20 @@ func NewHttpTestDependencyRule() *HttpTestDependencyRule { } func (r *HttpTestDependencyRule) Name() string { - return "http_test" + return HttpTestDependencyRuleName } func (r *HttpTestDependencyRule) CanHandle(manifest manifests.Manifest) bool { - _, ok := manifest.(*api.Http) - return ok + return manifest.GetKind() == manifests.HttpTestKind } func (r *HttpTestDependencyRule) AnalyzeDependencies(manifest manifests.Manifest) ([]Dependency, error) { httpTest, ok := manifest.(*api.Http) + var dependencies []Dependency if !ok { - return nil, nil + return dependencies, nil } - var dependencies []Dependency fromID := manifest.GetID() // Analyze each test case @@ -83,13 +84,13 @@ func (r *HttpTestDependencyRule) analyzeTestCase(manifestID string, testCase api From: manifestID, To: toID, Type: DependencyTypeTemplate, - Metadata: map[string]any{ - "alias": alias, - "required_paths": requiredPaths, - "locations": locations, - "save_required": true, - "test_case_name": testCase.Name, - "source_manifest": httpTest.GetKind(), + Metadata: DependencyMetadata{ + Alias: alias, + Paths: requiredPaths, + Locations: locations, + Save: true, + CaseName: testCase.Name, + ManifestKind: httpTest.GetKind(), }, } @@ -253,11 +254,10 @@ func (r *IntraManifestDependencyRule) AnalyzeDependencies(manifest manifests.Man From: caseID, To: depID, Type: DependencyTypeValue, - Metadata: map[string]any{ - "alias": ref.Alias, - "required_paths": []string{ref.Path}, - "save_required": true, - "intra_manifest": true, + Metadata: DependencyMetadata{ + Alias: ref.Alias, + Paths: []string{ref.Path}, + Save: true, }, } diff --git a/internal/core/runner/depends/rules/kinds.go b/internal/core/runner/depends/rules/kinds.go new file mode 100644 index 0000000..5777d0e --- /dev/null +++ b/internal/core/runner/depends/rules/kinds.go @@ -0,0 +1,43 @@ +package rules + +import ( + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds" +) + +const KindPriorityRuleName = "Kind Priority" + +// KindPriorityRule handles kind-based priorities +type KindPriorityRule struct { + priorities map[string]int +} + +func NewKindPriorityRule() *KindPriorityRule { + return &KindPriorityRule{ + priorities: kinds.PriorityMap, + } +} + +func (r *KindPriorityRule) Name() string { + return KindPriorityRuleName +} + +func (r *KindPriorityRule) CanHandle(_ manifests.Manifest) bool { + return true // Can handle any manifest for priority assignment +} + +func (r *KindPriorityRule) AnalyzeDependencies(_ manifests.Manifest) ([]Dependency, error) { + // This rule doesn't create dependencies, just provides priority info + return nil, nil +} + +func (r *KindPriorityRule) GetPriority() int { + return 0 // Lowest priority as this is just for ordering +} + +func (r *KindPriorityRule) GetKindPriority(kind string) int { + if priority, ok := r.priorities[kind]; ok { + return priority + } + return 1_000 +} diff --git a/internal/core/runner/depends/rules/rules.go b/internal/core/runner/depends/rules/rules.go new file mode 100644 index 0000000..0b773fa --- /dev/null +++ b/internal/core/runner/depends/rules/rules.go @@ -0,0 +1,73 @@ +package rules + +import "github.com/apiqube/cli/internal/core/manifests" + +const ( + DependencyTypeTemplate DependencyType = "template" // From template references + DependencyTypeValue DependencyType = "values" // From value passing +) + +// DependencyRule defines interface for dependency analysis rules +type DependencyRule interface { + // Name returns the rule name for debugging + Name() string + + // AnalyzeDependencies extracts dependencies from manifest + AnalyzeDependencies(manifest manifests.Manifest) ([]Dependency, error) + + // GetPriority returns priority for this type of dependency + GetPriority() int + + // CanHandle checks if this rule can handle the given manifest + CanHandle(manifest manifests.Manifest) bool +} + +// Dependency represents a dependency relationship +type Dependency struct { + From string // Source manifest ID + To string // Target manifest ID (what we depend on) + Type DependencyType // Type of dependency + Metadata DependencyMetadata // Additional metadata (e.g., what data to save) +} + +type DependencyType string + +type DependencyMetadata struct { + Alias string + Paths []string + Locations []string + Save bool + CaseName string + ManifestKind string +} + +// RuleRegistry manages dependency rules +type RuleRegistry struct { + rules []DependencyRule +} + +func NewRuleRegistry() *RuleRegistry { + return &RuleRegistry{ + rules: make([]DependencyRule, 0), + } +} + +func (r *RuleRegistry) Register(rule DependencyRule) { + r.rules = append(r.rules, rule) +} + +func (r *RuleRegistry) GetRules() []DependencyRule { + return r.rules +} + +// DefaultRuleRegistry creates a registry with default rules +func DefaultRuleRegistry() *RuleRegistry { + registry := NewRuleRegistry() + + // Register default rules + registry.Register(NewKindPriorityRule()) + registry.Register(NewTemplateDependencyRule()) + registry.Register(NewHttpTestDependencyRule()) + + return registry +} diff --git a/internal/core/runner/depends/rules/templates.go b/internal/core/runner/depends/rules/templates.go new file mode 100644 index 0000000..3b70b8b --- /dev/null +++ b/internal/core/runner/depends/rules/templates.go @@ -0,0 +1,105 @@ +package rules + +import ( + "fmt" + "regexp" + + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/utils" +) + +const TemplateRuleName = "Template" + +// TemplateDependencyRule handles template-based dependencies ({{ alias.path }}) +type TemplateDependencyRule struct { + templateRegex *regexp.Regexp +} + +func NewTemplateDependencyRule() *TemplateDependencyRule { + // Regex to match {{ alias.path }} patterns + regex := regexp.MustCompile(`\{\{\s*([a-zA-Z][a-zA-Z0-9_-]*)\.(.*?)\s*}}`) + return &TemplateDependencyRule{ + templateRegex: regex, + } +} + +func (r *TemplateDependencyRule) Name() string { + return TemplateRuleName +} + +func (r *TemplateDependencyRule) CanHandle(_ manifests.Manifest) bool { + // This rule can handle any manifest, we'll check content during analysis + return true +} + +func (r *TemplateDependencyRule) AnalyzeDependencies(manifest manifests.Manifest) ([]Dependency, error) { + var dependencies []Dependency + fromID := manifest.GetID() + + // Extract all template references from the manifest + references := r.extractTemplateReferences(manifest) + + // Group by alias to avoid duplicates and collect required paths + aliasData := make(map[string][]string) + for _, ref := range references { + aliasData[ref.Alias] = append(aliasData[ref.Alias], ref.Path) + } + + // Create dependencies with metadata about what data is needed + for alias, paths := range aliasData { + // Convert alias to full manifest ID (assuming same namespace for now) + // This might need to be more sophisticated based on your ID scheme + toID := r.resolveAliasToID(manifest, alias) + + dependencies = append(dependencies, Dependency{ + From: fromID, + To: toID, + Type: DependencyTypeTemplate, + Metadata: DependencyMetadata{ + Alias: alias, + Paths: paths, + Save: true, + }, + }) + } + + return dependencies, nil +} + +func (r *TemplateDependencyRule) GetPriority() int { + return 50 // Medium priority for template dependencies +} + +// TemplateReference represents a parsed template reference +type TemplateReference struct { + Alias string // The alias part (e.g., "users-list") + Path string // The path part (e.g., "response.body.data[0].id") +} + +func (r *TemplateDependencyRule) extractTemplateReferences(manifest manifests.Manifest) []TemplateReference { + var references []TemplateReference + + // Convert manifest to string representation for parsing + // This is a simplified approach - in real implementation you might want + // to traverse the structure more carefully + manifestStr := fmt.Sprintf("%+v", manifest) + + matches := r.templateRegex.FindAllStringSubmatch(manifestStr, -1) + for _, match := range matches { + if len(match) >= 3 { + references = append(references, TemplateReference{ + Alias: match[1], + Path: match[2], + }) + } + } + + return references +} + +func (r *TemplateDependencyRule) resolveAliasToID(manifest manifests.Manifest, alias string) string { + // Simple resolution: assume same namespace and kind as current manifest + // Format: namespace.kind.name#alias + namespace, kind, name := utils.ParseManifestID(manifest.GetID()) + return utils.FormManifestID(namespace, kind, fmt.Sprintf("%s#%s", name, alias)) +} diff --git a/internal/core/runner/executor/plan.go b/internal/core/runner/executor/plan.go index 6129447..852ea32 100644 --- a/internal/core/runner/executor/plan.go +++ b/internal/core/runner/executor/plan.go @@ -14,54 +14,32 @@ import ( const planRunnerOutputPrefix = "Plan Runner:" -var _ interfaces.PlanRunner = (*DefaultPlanRunner)(nil) +var _ interfaces.PlanRunner = (*Runner)(nil) -type DefaultPlanRunner struct { - registry interfaces.ExecutorRegistry - hooksRunner hooks.Runner -} - -func NewDefaultPlanRunner(registry interfaces.ExecutorRegistry, hooksRunner hooks.Runner) *DefaultPlanRunner { - return &DefaultPlanRunner{ - registry: registry, - hooksRunner: hooksRunner, - } -} - -// V2PlanRunner supports the new dependency system with PassManager -type V2PlanRunner struct { +// Runner supports the new dependency system with PassManager +type Runner struct { registry interfaces.ExecutorRegistry hooksRunner hooks.Runner passManager *depends.PassManager + graph *depends.Result } -func NewV2PlanRunner(registry interfaces.ExecutorRegistry, hooksRunner hooks.Runner, graphResult *depends.GraphResultV2) *V2PlanRunner { - return &V2PlanRunner{ +func NewRunner(registry interfaces.ExecutorRegistry, hooksRunner hooks.Runner, graph *depends.Result) *Runner { + return &Runner{ registry: registry, hooksRunner: hooksRunner, - passManager: nil, // Will be initialized when context is available + passManager: depends.NewPassManager(graph), + graph: graph, } } -func (r *V2PlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifests.Manifest) error { +func (r *Runner) Run(ctx interfaces.ExecutionContext, manifest manifests.Manifest) error { p, ok := manifest.(*plan.Plan) if !ok { return errors.New("invalid manifest type, expected Plan kind") } - // Initialize PassManager if not already done - if r.passManager == nil { - // We need to rebuild the graph result from the plan - // This is a simplified approach - in production you might want to pass the graph result directly - allManifests := ctx.GetAllManifests() - registry := depends.DefaultRuleRegistry() - builder := depends.NewGraphBuilderV2(registry) - graphResult, err := builder.BuildGraphWithRules(allManifests) - if err != nil { - return fmt.Errorf("failed to initialize dependency analysis: %w", err) - } - r.passManager = depends.NewPassManager(ctx, graphResult) - } + defer r.passManager.Close() var err error output := ctx.GetOutput() @@ -75,7 +53,7 @@ func (r *V2PlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifes } if p.Spec.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.BeforeRun, p.Spec.Hooks.BeforeRun); err != nil { + if err = r.runHooks(ctx, hooks.BeforeRun, p.Spec.Hooks.BeforeRun); err != nil { output.Logf(interfaces.ErrorLevel, "%s plan before start hooks running failed\nReason: %s", planRunnerOutputPrefix, err.Error()) return err } @@ -91,7 +69,7 @@ func (r *V2PlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifes output.Logf(interfaces.InfoLevel, "%s %s stage starting...", planRunnerOutputPrefix, stageName) if stage.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.BeforeRun, stage.Hooks.BeforeRun); err != nil { + if err = r.runHooks(ctx, hooks.BeforeRun, stage.Hooks.BeforeRun); err != nil { output.Logf(interfaces.ErrorLevel, "%s stage %s before start hooks running failed\nReason: %s", planRunnerOutputPrefix, stageName, err.Error()) return err } @@ -99,9 +77,9 @@ func (r *V2PlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifes var execErr error if stage.Parallel { - execErr = r.runManifestsParallelV2(ctx, stage.Manifests) + execErr = r.runManifestsParallel(ctx, stage.Manifests) } else { - execErr = r.runManifestsStrictV2(ctx, stage.Manifests) + execErr = r.runManifestsStrict(ctx, stage.Manifests) } if err = ctx.Err(); err != nil { @@ -110,7 +88,7 @@ func (r *V2PlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifes } if stage.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.AfterRun, stage.Hooks.AfterRun); err != nil { + if err = r.runHooks(ctx, hooks.AfterRun, stage.Hooks.AfterRun); err != nil { output.Logf(interfaces.ErrorLevel, "%s stage %s after finish hooks running failed: %s", planRunnerOutputPrefix, stageName, err.Error()) return err } @@ -120,14 +98,14 @@ func (r *V2PlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifes output.Logf(interfaces.ErrorLevel, "%s stage %s failed\nReason: %s", planRunnerOutputPrefix, stageName, execErr.Error()) if stage.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.OnFailure, stage.Hooks.OnFailure); err != nil { + if err = r.runHooks(ctx, hooks.OnFailure, stage.Hooks.OnFailure); err != nil { output.Logf(interfaces.ErrorLevel, "%s stage %s on failure hooks running failed\nReason: %s", planRunnerOutputPrefix, stageName, err.Error()) return err } } if p.Spec.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.OnFailure, p.Spec.Hooks.OnFailure); err != nil { + if err = r.runHooks(ctx, hooks.OnFailure, p.Spec.Hooks.OnFailure); err != nil { output.Logf(interfaces.ErrorLevel, "%s plan on failure hooks running failed\nReason: %s", planRunnerOutputPrefix, err.Error()) return errors.Join(execErr, err) } @@ -137,7 +115,7 @@ func (r *V2PlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifes } if stage.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.OnSuccess, stage.Hooks.OnSuccess); err != nil { + if err = r.runHooks(ctx, hooks.OnSuccess, stage.Hooks.OnSuccess); err != nil { output.Logf(interfaces.ErrorLevel, "%s stage %s on success hooks running failed\nReason: %s", planRunnerOutputPrefix, stageName, err.Error()) return err } @@ -150,12 +128,12 @@ func (r *V2PlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifes } if p.Spec.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.AfterRun, p.Spec.Hooks.AfterRun); err != nil { + if err = r.runHooks(ctx, hooks.AfterRun, p.Spec.Hooks.AfterRun); err != nil { output.Logf(interfaces.ErrorLevel, "%s plan after finish hooks running failed\nReason: %s", planRunnerOutputPrefix, err.Error()) return err } - if err = r.runHooksWithContext(ctx, hooks.OnSuccess, p.Spec.Hooks.OnSuccess); err != nil { + if err = r.runHooks(ctx, hooks.OnSuccess, p.Spec.Hooks.OnSuccess); err != nil { output.Logf(interfaces.ErrorLevel, "%s plan on success hooks running failed\nReason: %s", planRunnerOutputPrefix, err.Error()) return err } @@ -164,7 +142,7 @@ func (r *V2PlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifes return nil } -func (r *V2PlanRunner) runManifestsStrictV2(ctx interfaces.ExecutionContext, manifestIDs []string) error { +func (r *Runner) runManifestsStrict(ctx interfaces.ExecutionContext, manifestIDs []string) error { var man manifests.Manifest var err error @@ -205,7 +183,7 @@ func (r *V2PlanRunner) runManifestsStrictV2(ctx interfaces.ExecutionContext, man return nil } -func (r *V2PlanRunner) runManifestsParallelV2(ctx interfaces.ExecutionContext, manifestIDs []string) error { +func (r *Runner) runManifestsParallel(ctx interfaces.ExecutionContext, manifestIDs []string) error { var wg sync.WaitGroup errChan := make(chan error, len(manifestIDs)) @@ -267,206 +245,7 @@ func (r *V2PlanRunner) runManifestsParallelV2(ctx interfaces.ExecutionContext, m return nil } -func (r *V2PlanRunner) runHooksWithContext(ctx interfaces.ExecutionContext, event hooks.HookEvent, actions []hooks.Action) error { - if len(actions) == 0 { - return nil - } - - select { - case <-ctx.Done(): - return ctx.Err() - default: - return r.hooksRunner.RunHooks(ctx, event, actions) - } -} - -func (r *DefaultPlanRunner) RunPlan(ctx interfaces.ExecutionContext, manifest manifests.Manifest) error { - p, ok := manifest.(*plan.Plan) - if !ok { - return errors.New("invalid manifest type, expected Plan kind") - } - - var err error - output := ctx.GetOutput() - - planID := p.GetID() - output.Logf(interfaces.InfoLevel, "%s starting plan: %s", planRunnerOutputPrefix, planID) - - if err = ctx.Err(); err != nil { - output.Logf(interfaces.ErrorLevel, "%s plan execution canceled before start: %v", planRunnerOutputPrefix, err) - return err - } - - if p.Spec.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.BeforeRun, p.Spec.Hooks.BeforeRun); err != nil { - output.Logf(interfaces.ErrorLevel, "%s plan before start hooks running failed\nReason: %s", planRunnerOutputPrefix, err.Error()) - return err - } - } - - for _, stage := range p.Spec.Stages { - if err = ctx.Err(); err != nil { - output.Logf(interfaces.ErrorLevel, "%s plan execution canceled before stage %s: %v", planRunnerOutputPrefix, stage.Name, err) - return err - } - - stageName := stage.Name - output.Logf(interfaces.InfoLevel, "%s %s stage starting...", planRunnerOutputPrefix, stageName) - - if stage.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.BeforeRun, stage.Hooks.BeforeRun); err != nil { - output.Logf(interfaces.ErrorLevel, "%s stage %s before start hooks running failed\nReason: %s", planRunnerOutputPrefix, stageName, err.Error()) - return err - } - } - - var execErr error - if stage.Parallel { - execErr = r.runManifestsParallel(ctx, stage.Manifests) - } else { - execErr = r.runManifestsStrict(ctx, stage.Manifests) - } - - if err = ctx.Err(); err != nil { - output.Logf(interfaces.ErrorLevel, "%s plan execution canceled after stage %s: %v", planRunnerOutputPrefix, stage.Name, err) - return err - } - - if stage.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.AfterRun, stage.Hooks.AfterRun); err != nil { - output.Logf(interfaces.ErrorLevel, "%s stage %s after finish hooks running failed: %s", planRunnerOutputPrefix, stageName, err.Error()) - return err - } - } - - if execErr != nil { - output.Logf(interfaces.ErrorLevel, "%s stage %s failed\nReason: %s", planRunnerOutputPrefix, stageName, execErr.Error()) - - if stage.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.OnFailure, stage.Hooks.OnFailure); err != nil { - output.Logf(interfaces.ErrorLevel, "%s stage %s on failure hooks running failed\nReason: %s", planRunnerOutputPrefix, stageName, err.Error()) - return err - } - } - - if p.Spec.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.OnFailure, p.Spec.Hooks.OnFailure); err != nil { - output.Logf(interfaces.ErrorLevel, "%s plan on failure hooks running failed\nReason: %s", planRunnerOutputPrefix, err.Error()) - return errors.Join(execErr, err) - } - } - - return execErr - } - - if stage.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.OnSuccess, stage.Hooks.OnSuccess); err != nil { - output.Logf(interfaces.ErrorLevel, "%s stage %s on success hooks running failed\nReason: %s", planRunnerOutputPrefix, stageName, err.Error()) - return err - } - } - } - - if err = ctx.Err(); err != nil { - output.Logf(interfaces.ErrorLevel, "%s plan execution canceled before final hooks: %v", planRunnerOutputPrefix, err) - return err - } - - if p.Spec.Hooks != nil { - if err = r.runHooksWithContext(ctx, hooks.AfterRun, p.Spec.Hooks.AfterRun); err != nil { - output.Logf(interfaces.ErrorLevel, "%s plan after finish hooks running failed\nReason: %s", planRunnerOutputPrefix, err.Error()) - return err - } - - if err = r.runHooksWithContext(ctx, hooks.OnSuccess, p.Spec.Hooks.OnSuccess); err != nil { - output.Logf(interfaces.ErrorLevel, "%s plan on success hooks running failed\nReason: %s", planRunnerOutputPrefix, err.Error()) - return err - } - } - - return nil -} - -func (r *DefaultPlanRunner) runManifestsStrict(ctx interfaces.ExecutionContext, manifestIDs []string) error { - var man manifests.Manifest - var err error - - output := ctx.GetOutput() - - for _, id := range manifestIDs { - if man, err = ctx.GetManifestByID(id); err != nil { - return fmt.Errorf("run %s manifest failed: %s", id, err.Error()) - } - - exec, exists := r.registry.Find(man.GetKind()) - if !exists { - return fmt.Errorf("no executor found for kind: %s", man.GetKind()) - } - - output.Logf(interfaces.InfoLevel, "%s running %s manifest using %s executor", planRunnerOutputPrefix, id, man.GetKind()) - - if err = exec.Run(ctx, man); err != nil { - return fmt.Errorf("manifest %s failed: %s", id, err.Error()) - } - - output.Logf(interfaces.InfoLevel, "%s %s manifest finished", planRunnerOutputPrefix, id) - } - - return nil -} - -func (r *DefaultPlanRunner) runManifestsParallel(ctx interfaces.ExecutionContext, manifestIDs []string) error { - var wg sync.WaitGroup - errChan := make(chan error, len(manifestIDs)) - - output := ctx.GetOutput() - - for _, manId := range manifestIDs { - id := manId - wg.Add(1) - - go func() { - defer wg.Done() - man, err := ctx.GetManifestByID(id) - if err != nil { - errChan <- fmt.Errorf("run %s manifest failed: %s", id, err.Error()) - return - } - - exec, exists := r.registry.Find(man.GetKind()) - if !exists { - errChan <- fmt.Errorf("no executor found for kind: %s", man.GetKind()) - return - } - - output.Logf(interfaces.InfoLevel, "%s running %s manifest using %s executor", planRunnerOutputPrefix, id, man.GetKind()) - - if err = exec.Run(ctx, man); err != nil { - errChan <- fmt.Errorf("manifest %s failed: %s", id, err.Error()) - return - } - - output.Logf(interfaces.InfoLevel, "%s %s manifest finished", planRunnerOutputPrefix, id) - }() - } - - wg.Wait() - close(errChan) - - var rErr error - - if len(errChan) > 0 { - for err := range errChan { - rErr = errors.Join(rErr, err) - } - - return rErr - } - - return nil -} - -func (r *DefaultPlanRunner) runHooksWithContext(ctx interfaces.ExecutionContext, event hooks.HookEvent, actions []hooks.Action) error { +func (r *Runner) runHooks(ctx interfaces.ExecutionContext, event hooks.HookEvent, actions []hooks.Action) error { if len(actions) == 0 { return nil } diff --git a/internal/core/runner/interfaces/context.go b/internal/core/runner/interfaces/context.go index 5458a6c..d9fdfb1 100644 --- a/internal/core/runner/interfaces/context.go +++ b/internal/core/runner/interfaces/context.go @@ -16,7 +16,7 @@ type Executor interface { } type PlanRunner interface { - RunPlan(ctx ExecutionContext, plan manifests.Manifest) error + Run(ctx ExecutionContext, plan manifests.Manifest) error } type ExecutionContext interface { diff --git a/internal/core/runner/plan/graph.go b/internal/core/runner/plan/graph.go deleted file mode 100644 index 7bf741b..0000000 --- a/internal/core/runner/plan/graph.go +++ /dev/null @@ -1,72 +0,0 @@ -package plan - -import ( - "errors" - "sync" -) - -type depGraph struct { - edges map[string][]string - nodes map[string]bool - lock sync.Mutex -} - -func newDepGraph() *depGraph { - return &depGraph{ - edges: map[string][]string{}, - nodes: map[string]bool{}, - } -} - -func (g *depGraph) addNode(id string) { - g.lock.Lock() - defer g.lock.Unlock() - g.nodes[id] = true -} - -func (g *depGraph) addEdge(from, to string) { - g.lock.Lock() - defer g.lock.Unlock() - g.edges[from] = append(g.edges[from], to) - g.nodes[from] = true - g.nodes[to] = true -} - -func (g *depGraph) topoSort() ([]string, error) { - inDegree := map[string]int{} - for node := range g.nodes { - inDegree[node] = 0 - } - - for _, toList := range g.edges { - for _, to := range toList { - inDegree[to]++ - } - } - - var queue []string - for node, deg := range inDegree { - if deg == 0 { - queue = append(queue, node) - } - } - - var result []string - for len(queue) > 0 { - n := queue[0] - queue = queue[1:] - result = append(result, n) - - for _, neighbor := range g.edges[n] { - inDegree[neighbor]-- - if inDegree[neighbor] == 0 { - queue = append(queue, neighbor) - } - } - } - - if len(result) != len(g.nodes) { - return nil, errors.New("cycle detected in dependency graph") - } - return result, nil -} diff --git a/internal/core/runner/plan/manager.go b/internal/core/runner/plan/manager.go index 114eacb..8588527 100644 --- a/internal/core/runner/plan/manager.go +++ b/internal/core/runner/plan/manager.go @@ -5,6 +5,8 @@ import ( "sort" "strings" + "github.com/apiqube/cli/internal/core/runner/depends/rules" + "github.com/apiqube/cli/internal/operations" "github.com/apiqube/cli/internal/core/manifests" @@ -13,19 +15,8 @@ import ( "github.com/apiqube/cli/internal/core/runner/depends" ) -var kindPriority = map[string]int{ - "Values": 0, - "Server": 10, - "Service": 20, - "HttpTest": 30, - "HttpLoadTest": 40, -} - -const defaultPriority = 10_000 - type Manager interface { - Generate() (*plan.Plan, error) - GenerateV2() (*plan.Plan, *depends.GraphResultV2, error) + Generate() (*plan.Plan, *depends.Result, error) CheckPlan(*plan.Plan) error } @@ -90,64 +81,8 @@ func (g *basicManager) CheckPlan(pln *plan.Plan) error { return nil } -func (g *basicManager) Generate() (*plan.Plan, error) { - if len(g.manifests) == 0 { - return nil, fmt.Errorf("manifests not provided for generating the plan") - } - - graph := newDepGraph() - - for id, m := range g.manifests { - if m.GetKind() == manifests.PlanKind { - delete(g.manifests, id) - continue - } - - graph.addNode(id) - - if depend, ok := m.(manifests.Dependencies); ok { - deps := depend.GetDependsOn() - for _, depId := range deps { - if _, found := g.manifests[depId]; !found { - return nil, fmt.Errorf("manifest '%s' depends on '%s', but it was not found in the manifest set", id, depId) - } - - graph.addEdge(depId, id) - } - } - } - - sorted, err := graph.topoSort() - if err != nil { - return nil, err - } - - stages := groupByLayers(sorted, g.manifests, g.mode, g.stableSort, g.parallel) - - var newPlan plan.Plan - newPlan.Default() - - newPlan.Spec.Stages = stages - - planData, err := operations.NormalizeYAML(&newPlan) - if err != nil { - return nil, fmt.Errorf("fail while generating plan hash: %v", err) - } - - planHash, err := utils.CalculateContentHash(planData) - if err != nil { - return nil, fmt.Errorf("fail while calculation plan hash: %v", err) - } - - meta := newPlan.GetMeta() - meta.SetHash(planHash) - meta.SetCreatedBy("plan-generator") - - return &newPlan, nil -} - -// GenerateV2 generates plan using the new V2 dependency system -func (g *basicManager) GenerateV2() (*plan.Plan, *depends.GraphResultV2, error) { +// Generate generates plan using the new V2 dependency system +func (g *basicManager) Generate() (*plan.Plan, *depends.Result, error) { if len(g.manifests) == 0 { return nil, nil, fmt.Errorf("manifests not provided for generating the plan") } @@ -161,11 +96,11 @@ func (g *basicManager) GenerateV2() (*plan.Plan, *depends.GraphResultV2, error) } // Create rule registry with default rules - registry := depends.DefaultRuleRegistry() + registry := rules.DefaultRuleRegistry() - // Build graph using V2 system - builder := depends.NewGraphBuilderV2(registry) - graphResult, err := builder.BuildGraphWithRules(manifestSlice) + // Build graph using system + builder := depends.NewGraphBuilder(registry) + graphResult, err := builder.Build(manifestSlice...) if err != nil { return nil, nil, fmt.Errorf("failed to build dependency graph: %w", err) } @@ -229,65 +164,6 @@ func (g *basicManager) createStagesFromExecutionOrder(executionOrder []string, m return stages } -func groupByLayers(sorted []string, mans map[string]manifests.Manifest, mode string, stable, parallel bool) []plan.Stage { - sort.SliceStable(sorted, func(i, j int) bool { - ki := mans[sorted[i]].GetKind() - kj := mans[sorted[j]].GetKind() - - pi := kindPriorityOrDefault(ki) - pj := kindPriorityOrDefault(kj) - - if pi == pj && stable { - return sorted[i] < sorted[j] - } - return pi < pj - }) - - var stages []plan.Stage - var current []string - var currentKind string - prevDeps := map[string]bool{} - - for _, id := range sorted { - m := mans[id] - kind := m.GetKind() - ready := true - - if depend, ok := m.(manifests.Dependencies); ok { - for _, dep := range depend.GetDependsOn() { - if !prevDeps[dep] { - ready = false - break - } - } - } - - if (!ready || currentKind != "" && kind != currentKind) && len(current) > 0 { - stages = append(stages, makeStage(current, mans, mode, stable, parallel)) - for _, cid := range current { - prevDeps[cid] = true - } - current = []string{} - } - - current = append(current, id) - currentKind = kind - } - - if len(current) > 0 { - stages = append(stages, makeStage(current, mans, mode, stable, parallel)) - } - - return stages -} - -func kindPriorityOrDefault(kind string) int { - if p, ok := kindPriority[kind]; ok { - return p - } - return defaultPriority -} - func makeStage(ids []string, mans map[string]manifests.Manifest, mode string, stable, parallel bool) plan.Stage { if stable { sort.Strings(ids)