Skip to content
Merged

Dev #21

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
61b0772
chore(core): small adjustment in gitignore (added config for qudo), a…
davidmovas Jun 10, 2025
d292f1e
chore(core): added tests for operation normalize functionality
davidmovas Jun 10, 2025
71b99a8
chore(core): moved from standard yaml package to goccy/go-yaml packag…
davidmovas Jun 10, 2025
464b449
chore(core): moved from standard json package to goccy/go-json packag…
davidmovas Jun 10, 2025
b929ae1
chore(executor): executor and other scripts a little refactored
davidmovas Jun 10, 2025
bab5cb6
chore(runner): assert package refactored, tided and styled
davidmovas Jun 10, 2025
bf460de
chore(runner): some refactors in http executor, fixed issue with requ…
davidmovas Jun 10, 2025
d8f7a9a
feat(runner): added HTML report generation after plan execution, refa…
davidmovas Jun 12, 2025
c914176
Merge branch 'main' into dev
davidmovas Jun 12, 2025
0e128ba
chore(core): moved from standard json package to goccy/go-json packag…
davidmovas Jun 12, 2025
65a9b41
chore(cli): added semantic release configuration file
davidmovas Jun 12, 2025
6b178df
Merge branch 'main' into dev
davidmovas Jun 12, 2025
17ff90e
Merge branch 'main' into dev
davidmovas Jun 13, 2025
6a30928
refactor(report): restructured HTML report generation with improved t…
davidmovas Jun 13, 2025
56b3394
feat(runner): added V2 plan runner with dependency analysis and data …
davidmovas Jun 20, 2025
d77f700
refactor(runner): enhanced dependency graph builder with smart templa…
davidmovas Jun 20, 2025
2db3473
refactor(runner): improved dependency graph execution order determini…
davidmovas Jun 21, 2025
56cf8a9
refactor(runner): replaced priority queue with simple slice queue for…
davidmovas Jun 22, 2025
aab5429
feat(runner): added V2 plan runner with dependency analysis and data …
davidmovas Jun 20, 2025
7432eaf
refactor(runner): enhanced dependency graph builder with improved tem…
davidmovas Jun 22, 2025
54dd1d3
refactor(runner): enhanced dependency graph builder with improved tem…
davidmovas Jun 22, 2025
640eb28
test(runner): improved test case descriptions and removed redundant d…
davidmovas Jun 22, 2025
517bf3d
refactor(runner): reorganized imports and removed unused variables fo…
davidmovas Jun 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ bin
.qodo
cover.svg

# Temp lock
examples/complex-http-tests/reports

# Test binary, built with `go test -c`
*.test

Expand Down
8 changes: 8 additions & 0 deletions .semrelrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"plugins": {
"version": {
"override": "0.1.0",
"strategy": "minor"
}
}
}
25 changes: 19 additions & 6 deletions cmd/cli/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,29 @@ var Cmd = &cobra.Command{
}

cli.Success("All manifests valid")
cli.Info("Generating plan...")
cli.Info("Generating plan with V2 dependency system...")

manager := runner.NewPlanManagerBuilder().
WithManifests(loadedManifests...).Build()

planManifest, err := manager.Generate()
// Use V2 plan generation with dependency analysis
planManifest, graphResult, err := manager.GenerateV2()
if err != nil {
cli.Errorf("Failed to generate plan: %v", err)
cli.Errorf("Failed to generate V2 plan: %v", err)
return
}

cli.Successf("Plan successfully generated")
cli.Successf("V2 Plan successfully generated")

// Print dependency analysis if verbose
if len(graphResult.SaveRequirements) > 0 {
cli.Info("Dependency analysis completed:")
for manifestID, req := range graphResult.SaveRequirements {
if req.Required {
cli.Infof(" %s will save data for", manifestID)
}
}
}

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

planRunner := executor.NewDefaultPlanRunner(registry, hooksRunner)
// Use V2 plan runner with dependency support
planRunner := executor.NewV2PlanRunner(registry, hooksRunner, graphResult)

runCtx := ctxBuilder.Build()

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

cli.Successf("Plan successfully runned")
cli.Successf("V2 Plan successfully executed")
},
}

Expand Down
2 changes: 1 addition & 1 deletion examples/combined/combined.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ spec:
Authorization: some_jwt_token
type: some_data
body:
email: example_email
email: "{{ Values.simple-save.users.username.0 }}"
password: example_password
username: example_username
expected:
Expand Down
28 changes: 10 additions & 18 deletions examples/complex-http-tests/http_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,19 @@ metadata:
spec:
target: http://127.0.0.1:8081
cases:
- name: Fetch User From Server
alias: fetch-user
method: GET
endpoint: /users/3
assert:
- target: status
equals: 200

- name: Create New Array of User
- name: Create User With Data From Previous Response
method: POST
endpoint: /users-batch
endpoint: /users
assert:
- target: status
equals: 201
body:
users:
__repeat: 2
__template:
name: "{{ Fake.name }}"
email: "{{ Fake.email }}"
age: "{{ Fake.uint.10.100 }}"
address:
street: "{{ Fake.address }}"
number: "{{ Regex(\"^[a-z]{5,10}@[a-z]{5,10}\\.(com|net|org)$\") }}"
save:
request:
body:
users: "*"
response:
body:
data: "*"
user: "{{ fetch-user.response.body.user }}"
7 changes: 1 addition & 6 deletions internal/core/manifests/kinds/tests/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import (

type HttpCase struct {
Name string `yaml:"name" json:"name" validate:"required,min=3,max=128"`
Alias *string `yaml:"alias" json:"alias" validate:"omitempty,min=1,max=25"`
Method string `yaml:"method" json:"method" valid:"required,uppercase,oneof=GET POST PUT PATCH DELETE"`
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"omitempty"`
Url string `yaml:"url,omitempty" json:"url,omitempty" validate:"omitempty,url"`
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" validate:"omitempty,min=1,max=100"`
Body map[string]any `yaml:"body,omitempty" json:"body,omitempty" validate:"omitempty,min=1,max=100"`
Assert []*Assert `yaml:"assert,omitempty" json:"assert,omitempty" validate:"omitempty,min=1,max=50,dive"`
Save *Save `yaml:"save,omitempty" json:"save,omitempty" validate:"omitempty"`
Pass []*Pass `yaml:"pass,omitempty" json:"pass,omitempty" validate:"omitempty,min=1,max=25,dive"`
Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"omitempty,duration"`
Parallel bool `yaml:"async,omitempty" json:"async,omitempty" validate:"omitempty,boolean"`
Details []string `yaml:"details,omitempty" json:"details,omitempty" validate:"omitempty,min=1,max=100"`
Expand All @@ -36,8 +36,3 @@ type SaveEntry struct {
Body map[string]string `yaml:"body,omitempty" json:"body,omitempty" validate:"omitempty,min=1,max=20,dive,keys,endkeys"`
Headers []string `yaml:"headers,omitempty" json:"headers,omitempty" validate:"omitempty,min=1,max=20"`
}

type Pass struct {
From string `yaml:"from" json:"from" validate:"required,min=1,max=100"`
Map map[string]string `yaml:"map,omitempty" json:"map,omitempty" validate:"omitempty,min=1,max=100,dive,keys,endkeys"`
}
59 changes: 59 additions & 0 deletions internal/core/runner/depends/builder_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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
}
43 changes: 33 additions & 10 deletions internal/core/runner/depends/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@ package depends
import (
"container/heap"
"fmt"
"sort"
"strings"

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

"github.com/apiqube/cli/internal/core/manifests"
)

var priorityOrder = map[string]int{
manifests.ValuesKind: 100,
manifests.ServerKind: 40,
manifests.ServiceKind: 30,
}

type GraphResult struct {
Graph map[string][]string
ExecutionOrder []string
Expand All @@ -32,6 +27,7 @@ func BuildGraphWithPriority(mans []manifests.Manifest) (*GraphResult, error) {
idToNode := make(map[string]manifests.Manifest)
nodePriority := make(map[string]int)

// Initialize all manifests
for _, node := range mans {
id := node.GetID()
idToNode[id] = node
Expand All @@ -44,6 +40,7 @@ func BuildGraphWithPriority(mans []manifests.Manifest) (*GraphResult, error) {
}
}

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

// 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 {
return a.Priority > b.Priority
// 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 {
heap.Push(priorityQueue, &Node{
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)

for _, neighbor := range graph[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{
Expand All @@ -101,7 +124,7 @@ func getPriority(kind string) int {
if p, ok := priorityOrder[kind]; ok {
return p
}
return 0
return 100 // Default low priority for unknown kinds
}

func findCyclicNodes(inDegree map[string]int) []string {
Expand Down
Loading
Loading