Skip to content

Commit b82cd3e

Browse files
authored
Dev (#21)
* chore(core): small adjustment in gitignore (added config for qudo), added tests for operation parse function * chore(core): added tests for operation normalize functionality * chore(core): moved from standard yaml package to goccy/go-yaml package, added json normalize tests * chore(core): moved from standard json package to goccy/go-json package, tided * chore(executor): executor and other scripts a little refactored * chore(runner): assert package refactored, tided and styled * chore(runner): some refactors in http executor, fixed issue with request body saving to result case, added colored coverage heatmap report generation * feat(runner): added HTML report generation after plan execution, refactored save extractor to collect multiple results per manifest * chore(core): moved from standard json package to goccy/go-json package, tided * chore(cli): added semantic release configuration file * refactor(report): restructured HTML report generation with improved templates and interface changes * feat(runner): added V2 plan runner with dependency analysis and data passing system * refactor(runner): enhanced dependency graph builder with smart template analysis and improved execution ordering * refactor(runner): improved dependency graph execution order determinism with consistent sorting * refactor(runner): replaced priority queue with simple slice queue for deterministic execution ordering * refactor(runner): enhanced dependency graph builder with improved template analysis and deterministic execution ordering * refactor(runner): enhanced dependency graph builder with improved template analysis and deterministic execution ordering * test(runner): improved test case descriptions and removed redundant data in dependency graph tests * refactor(runner): reorganized imports and removed unused variables for improved code quality
1 parent 33311fd commit b82cd3e

33 files changed

Lines changed: 2962 additions & 338 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ bin
1212
.qodo
1313
cover.svg
1414

15+
# Temp lock
16+
examples/complex-http-tests/reports
17+
1518
# Test binary, built with `go test -c`
1619
*.test
1720

.semrelrc.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"plugins": {
3+
"version": {
4+
"override": "0.1.0",
5+
"strategy": "minor"
6+
}
7+
}
8+
}

cmd/cli/run/run.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,29 @@ var Cmd = &cobra.Command{
5050
}
5151

5252
cli.Success("All manifests valid")
53-
cli.Info("Generating plan...")
53+
cli.Info("Generating plan with V2 dependency system...")
5454

5555
manager := runner.NewPlanManagerBuilder().
5656
WithManifests(loadedManifests...).Build()
5757

58-
planManifest, err := manager.Generate()
58+
// Use V2 plan generation with dependency analysis
59+
planManifest, graphResult, err := manager.GenerateV2()
5960
if err != nil {
60-
cli.Errorf("Failed to generate plan: %v", err)
61+
cli.Errorf("Failed to generate V2 plan: %v", err)
6162
return
6263
}
6364

64-
cli.Successf("Plan successfully generated")
65+
cli.Successf("V2 Plan successfully generated")
66+
67+
// Print dependency analysis if verbose
68+
if len(graphResult.SaveRequirements) > 0 {
69+
cli.Info("Dependency analysis completed:")
70+
for manifestID, req := range graphResult.SaveRequirements {
71+
if req.Required {
72+
cli.Infof(" %s will save data for", manifestID)
73+
}
74+
}
75+
}
6576

6677
ctxBuilder := context.NewCtxBuilder().
6778
WithContext(cmd.Context()).
@@ -70,15 +81,17 @@ var Cmd = &cobra.Command{
7081
registry := executor.NewDefaultExecutorRegistry()
7182
hooksRunner := hooks.NewDefaultHooksRunner()
7283

73-
planRunner := executor.NewDefaultPlanRunner(registry, hooksRunner)
84+
// Use V2 plan runner with dependency support
85+
planRunner := executor.NewV2PlanRunner(registry, hooksRunner, graphResult)
7486

7587
runCtx := ctxBuilder.Build()
7688

7789
if err = planRunner.RunPlan(runCtx, planManifest); err != nil {
90+
cli.Errorf("Plan execution failed: %v", err)
7891
return
7992
}
8093

81-
cli.Successf("Plan successfully runned")
94+
cli.Successf("V2 Plan successfully executed")
8295
},
8396
}
8497

examples/combined/combined.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ spec:
5757
Authorization: some_jwt_token
5858
type: some_data
5959
body:
60-
email: example_email
60+
email: "{{ Values.simple-save.users.username.0 }}"
6161
password: example_password
6262
username: example_username
6363
expected:

examples/complex-http-tests/http_test.yaml

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,19 @@ metadata:
77
spec:
88
target: http://127.0.0.1:8081
99
cases:
10+
- name: Fetch User From Server
11+
alias: fetch-user
12+
method: GET
13+
endpoint: /users/3
14+
assert:
15+
- target: status
16+
equals: 200
1017

11-
- name: Create New Array of User
18+
- name: Create User With Data From Previous Response
1219
method: POST
13-
endpoint: /users-batch
20+
endpoint: /users
1421
assert:
1522
- target: status
1623
equals: 201
1724
body:
18-
users:
19-
__repeat: 2
20-
__template:
21-
name: "{{ Fake.name }}"
22-
email: "{{ Fake.email }}"
23-
age: "{{ Fake.uint.10.100 }}"
24-
address:
25-
street: "{{ Fake.address }}"
26-
number: "{{ Regex(\"^[a-z]{5,10}@[a-z]{5,10}\\.(com|net|org)$\") }}"
27-
save:
28-
request:
29-
body:
30-
users: "*"
31-
response:
32-
body:
33-
data: "*"
25+
user: "{{ fetch-user.response.body.user }}"

internal/core/manifests/kinds/tests/base.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import (
66

77
type HttpCase struct {
88
Name string `yaml:"name" json:"name" validate:"required,min=3,max=128"`
9+
Alias *string `yaml:"alias" json:"alias" validate:"omitempty,min=1,max=25"`
910
Method string `yaml:"method" json:"method" valid:"required,uppercase,oneof=GET POST PUT PATCH DELETE"`
1011
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"omitempty"`
1112
Url string `yaml:"url,omitempty" json:"url,omitempty" validate:"omitempty,url"`
1213
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" validate:"omitempty,min=1,max=100"`
1314
Body map[string]any `yaml:"body,omitempty" json:"body,omitempty" validate:"omitempty,min=1,max=100"`
1415
Assert []*Assert `yaml:"assert,omitempty" json:"assert,omitempty" validate:"omitempty,min=1,max=50,dive"`
1516
Save *Save `yaml:"save,omitempty" json:"save,omitempty" validate:"omitempty"`
16-
Pass []*Pass `yaml:"pass,omitempty" json:"pass,omitempty" validate:"omitempty,min=1,max=25,dive"`
1717
Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"omitempty,duration"`
1818
Parallel bool `yaml:"async,omitempty" json:"async,omitempty" validate:"omitempty,boolean"`
1919
Details []string `yaml:"details,omitempty" json:"details,omitempty" validate:"omitempty,min=1,max=100"`
@@ -36,8 +36,3 @@ type SaveEntry struct {
3636
Body map[string]string `yaml:"body,omitempty" json:"body,omitempty" validate:"omitempty,min=1,max=20,dive,keys,endkeys"`
3737
Headers []string `yaml:"headers,omitempty" json:"headers,omitempty" validate:"omitempty,min=1,max=20"`
3838
}
39-
40-
type Pass struct {
41-
From string `yaml:"from" json:"from" validate:"required,min=1,max=100"`
42-
Map map[string]string `yaml:"map,omitempty" json:"map,omitempty" validate:"omitempty,min=1,max=100,dive,keys,endkeys"`
43-
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package depends
2+
3+
// AddRule adds a new rule to the registry
4+
func (gb *GraphBuilderV2) AddRule(rule DependencyRule) {
5+
gb.registry.Register(rule)
6+
}
7+
8+
// GetSaveRequirement returns save requirement for a manifest
9+
func (gr *GraphResultV2) GetSaveRequirement(manifestID string) (SaveRequirement, bool) {
10+
req, exists := gr.SaveRequirements[manifestID]
11+
return req, exists
12+
}
13+
14+
// GetDependenciesFor returns all dependencies for a manifest
15+
func (gr *GraphResultV2) GetDependenciesFor(manifestID string) []Dependency {
16+
var deps []Dependency
17+
for _, dep := range gr.Dependencies {
18+
if dep.From == manifestID {
19+
deps = append(deps, dep)
20+
}
21+
}
22+
return deps
23+
}
24+
25+
// GetDependentsOf returns dependencies that depend on the given manifest
26+
func (gr *GraphResultV2) GetDependentsOf(manifestID string) []Dependency {
27+
var dependents []Dependency
28+
for _, dep := range gr.Dependencies {
29+
if dep.To == manifestID {
30+
dependents = append(dependents, dep)
31+
}
32+
}
33+
return dependents
34+
}
35+
36+
// GetDependenciesOf returns dependencies that the given manifest depends on
37+
func (gr *GraphResultV2) GetDependenciesOf(manifestID string) []Dependency {
38+
var dependencies []Dependency
39+
for _, dep := range gr.Dependencies {
40+
if dep.From == manifestID {
41+
dependencies = append(dependencies, dep)
42+
}
43+
}
44+
return dependencies
45+
}
46+
47+
// GetIntraManifestDependencies returns intra-manifest dependencies for a given manifest
48+
func (gr *GraphResultV2) GetIntraManifestDependencies(manifestID string) []Dependency {
49+
if deps, exists := gr.IntraManifestDeps[manifestID]; exists {
50+
return deps
51+
}
52+
return []Dependency{}
53+
}
54+
55+
// HasIntraManifestDependencies checks if a manifest has intra-manifest dependencies
56+
func (gr *GraphResultV2) HasIntraManifestDependencies(manifestID string) bool {
57+
deps, exists := gr.IntraManifestDeps[manifestID]
58+
return exists && len(deps) > 0
59+
}

internal/core/runner/depends/dependencies.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,14 @@ package depends
33
import (
44
"container/heap"
55
"fmt"
6+
"sort"
67
"strings"
78

89
"github.com/apiqube/cli/internal/collections"
910

1011
"github.com/apiqube/cli/internal/core/manifests"
1112
)
1213

13-
var priorityOrder = map[string]int{
14-
manifests.ValuesKind: 100,
15-
manifests.ServerKind: 40,
16-
manifests.ServiceKind: 30,
17-
}
18-
1914
type GraphResult struct {
2015
Graph map[string][]string
2116
ExecutionOrder []string
@@ -32,6 +27,7 @@ func BuildGraphWithPriority(mans []manifests.Manifest) (*GraphResult, error) {
3227
idToNode := make(map[string]manifests.Manifest)
3328
nodePriority := make(map[string]int)
3429

30+
// Initialize all manifests
3531
for _, node := range mans {
3632
id := node.GetID()
3733
idToNode[id] = node
@@ -44,6 +40,7 @@ func BuildGraphWithPriority(mans []manifests.Manifest) (*GraphResult, error) {
4440
}
4541
}
4642

43+
// Build dependency graph
4744
for _, man := range mans {
4845
if dep, has := man.(manifests.Dependencies); has {
4946
id := man.GetID()
@@ -57,25 +54,51 @@ func BuildGraphWithPriority(mans []manifests.Manifest) (*GraphResult, error) {
5754
}
5855
}
5956

57+
// Use priority queue for topological sorting with priorities
58+
// Lower priority number = higher execution priority (executes first)
6059
priorityQueue := collections.NewPriorityQueue[*Node](func(a, b *Node) bool {
61-
return a.Priority > b.Priority
60+
// First compare by priority (lower number = higher priority)
61+
if a.Priority != b.Priority {
62+
return a.Priority < b.Priority
63+
}
64+
// If priorities are equal, sort by ID for deterministic behavior
65+
return a.ID < b.ID
6266
})
6367

68+
// Add all nodes with zero in-degree to the queue
69+
var zeroInDegreeNodes []*Node
6470
for id, degree := range inDegree {
6571
if degree == 0 {
66-
heap.Push(priorityQueue, &Node{
72+
zeroInDegreeNodes = append(zeroInDegreeNodes, &Node{
6773
ID: id,
6874
Priority: nodePriority[id],
6975
})
7076
}
7177
}
7278

79+
// Sort for deterministic behavior
80+
sort.Slice(zeroInDegreeNodes, func(i, j int) bool {
81+
if zeroInDegreeNodes[i].Priority != zeroInDegreeNodes[j].Priority {
82+
return zeroInDegreeNodes[i].Priority < zeroInDegreeNodes[j].Priority
83+
}
84+
return zeroInDegreeNodes[i].ID < zeroInDegreeNodes[j].ID
85+
})
86+
87+
// Add to priority queue
88+
for _, node := range zeroInDegreeNodes {
89+
heap.Push(priorityQueue, node)
90+
}
91+
7392
var order []string
7493
for priorityQueue.Len() > 0 {
7594
current := heap.Pop(priorityQueue).(*Node).ID
7695
order = append(order, current)
7796

78-
for _, neighbor := range graph[current] {
97+
// Process neighbors in sorted order for deterministic behavior
98+
neighbors := graph[current]
99+
sort.Strings(neighbors)
100+
101+
for _, neighbor := range neighbors {
79102
inDegree[neighbor]--
80103
if inDegree[neighbor] == 0 {
81104
heap.Push(priorityQueue, &Node{
@@ -101,7 +124,7 @@ func getPriority(kind string) int {
101124
if p, ok := priorityOrder[kind]; ok {
102125
return p
103126
}
104-
return 0
127+
return 100 // Default low priority for unknown kinds
105128
}
106129

107130
func findCyclicNodes(inDegree map[string]int) []string {

0 commit comments

Comments
 (0)