a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
170 lines
5.8 KiB
Go
170 lines
5.8 KiB
Go
package languages
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
)
|
|
|
|
// metaString reads a string-valued Meta key, returning "" when absent.
|
|
func metaString(n *graph.Node, key string) string {
|
|
if n == nil || n.Meta == nil {
|
|
return ""
|
|
}
|
|
if v, ok := n.Meta[key].(string); ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func TestAnsible_PlaybookEmitsPlayTasksHandlerAndModuleCalls(t *testing.T) {
|
|
src := []byte(`- name: Configure web servers
|
|
hosts: web
|
|
tasks:
|
|
- name: Copy nginx config
|
|
ansible.builtin.copy:
|
|
src: nginx.conf
|
|
dest: /etc/nginx/nginx.conf
|
|
notify: restart nginx
|
|
- name: Ensure nginx running
|
|
service:
|
|
name: nginx
|
|
state: started
|
|
handlers:
|
|
- name: restart nginx
|
|
service:
|
|
name: nginx
|
|
state: restarted
|
|
`)
|
|
e := NewYAMLExtractor()
|
|
result, err := e.Extract("playbooks/web.yml", src)
|
|
require.NoError(t, err)
|
|
|
|
nodes := nodesByID(result.Nodes)
|
|
|
|
// Play node.
|
|
playID := "playbooks/web.yml::play:Configure web servers"
|
|
require.Contains(t, nodes, playID)
|
|
assert.Equal(t, graph.KindType, nodes[playID].Kind)
|
|
assert.Equal(t, "play", metaString(nodes[playID], "ansible_kind"))
|
|
assert.Equal(t, "web", metaString(nodes[playID], "hosts"))
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeDefines, "playbooks/web.yml", playID),
|
|
"file should define the play")
|
|
|
|
// Task 1: copy with module meta + module call edge + notify.
|
|
copyID := "playbooks/web.yml::task:Copy nginx config"
|
|
require.Contains(t, nodes, copyID)
|
|
assert.Equal(t, graph.KindFunction, nodes[copyID].Kind)
|
|
assert.Equal(t, "task", metaString(nodes[copyID], "ansible_kind"))
|
|
assert.Equal(t, "ansible.builtin.copy", metaString(nodes[copyID], "module"))
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeDefines, playID, copyID),
|
|
"play should define the copy task")
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeCalls, copyID,
|
|
"unresolved::ansible_module::ansible.builtin.copy"),
|
|
"copy task should call its module")
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeReferences, copyID,
|
|
"unresolved::ansible_handler::restart nginx"),
|
|
"copy task should notify the handler")
|
|
|
|
// Task 2: service module.
|
|
svcID := "playbooks/web.yml::task:Ensure nginx running"
|
|
require.Contains(t, nodes, svcID)
|
|
assert.Equal(t, "service", metaString(nodes[svcID], "module"))
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeCalls, svcID,
|
|
"unresolved::ansible_module::service"),
|
|
"service task should call its module")
|
|
|
|
// Handler node.
|
|
handlerID := "playbooks/web.yml::handler:restart nginx"
|
|
require.Contains(t, nodes, handlerID)
|
|
assert.Equal(t, graph.KindFunction, nodes[handlerID].Kind)
|
|
assert.Equal(t, "handler", metaString(nodes[handlerID], "ansible_kind"))
|
|
assert.Equal(t, "service", metaString(nodes[handlerID], "module"))
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeDefines, playID, handlerID),
|
|
"play should define the handler")
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeCalls, handlerID,
|
|
"unresolved::ansible_module::service"),
|
|
"handler should call its module")
|
|
}
|
|
|
|
func TestAnsible_PlayRolesEmitReferences(t *testing.T) {
|
|
src := []byte(`- hosts: db
|
|
roles:
|
|
- common
|
|
- role: postgres
|
|
`)
|
|
e := NewYAMLExtractor()
|
|
result, err := e.Extract("playbooks/db.yml", src)
|
|
require.NoError(t, err)
|
|
|
|
playID := "playbooks/db.yml::play:db"
|
|
nodes := nodesByID(result.Nodes)
|
|
require.Contains(t, nodes, playID)
|
|
assert.Equal(t, "db", metaString(nodes[playID], "hosts"))
|
|
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeReferences, playID,
|
|
"unresolved::ansible_role::common"),
|
|
"play should reference the common role (scalar form)")
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeReferences, playID,
|
|
"unresolved::ansible_role::postgres"),
|
|
"play should reference the postgres role ({role: ...} form)")
|
|
}
|
|
|
|
func TestAnsible_StandaloneTasksFile(t *testing.T) {
|
|
src := []byte(`- name: Install package
|
|
ansible.builtin.apt:
|
|
name: curl
|
|
state: present
|
|
- name: Start service
|
|
service:
|
|
name: curl
|
|
state: started
|
|
`)
|
|
e := NewYAMLExtractor()
|
|
result, err := e.Extract("roles/web/tasks/main.yml", src)
|
|
require.NoError(t, err)
|
|
|
|
nodes := nodesByID(result.Nodes)
|
|
taskID := "roles/web/tasks/main.yml::task:Install package"
|
|
require.Contains(t, nodes, taskID)
|
|
assert.Equal(t, graph.KindFunction, nodes[taskID].Kind)
|
|
assert.Equal(t, "ansible.builtin.apt", metaString(nodes[taskID], "module"))
|
|
// File owns the task directly in a standalone tasks file.
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeDefines, "roles/web/tasks/main.yml", taskID),
|
|
"file should define the standalone task")
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeCalls, taskID,
|
|
"unresolved::ansible_module::ansible.builtin.apt"),
|
|
"standalone task should call its module")
|
|
}
|
|
|
|
// A plain YAML list of config objects (no hosts, not under a role tree)
|
|
// must NOT be classified as Ansible — it falls through to the generic
|
|
// top-level-keys walker, which emits no play/task nodes.
|
|
func TestAnsible_PlainListNotClassified(t *testing.T) {
|
|
src := []byte(`- name: us-east
|
|
region: us-east-1
|
|
capacity: 10
|
|
- name: eu-west
|
|
region: eu-west-1
|
|
capacity: 5
|
|
`)
|
|
// Direct detector check: returns false (not Ansible).
|
|
var negative parser.ExtractionResult
|
|
classified := extractAnsibleYAML("config/regions.yml", "config/regions.yml", src, &negative)
|
|
assert.False(t, classified, "a plain config list must not be classified as Ansible")
|
|
|
|
// And via the full extractor: no play / task / handler nodes appear.
|
|
e := NewYAMLExtractor()
|
|
result, err := e.Extract("config/regions.yml", src)
|
|
require.NoError(t, err)
|
|
for _, n := range result.Nodes {
|
|
if n.Meta != nil {
|
|
_, isAnsible := n.Meta["ansible_kind"]
|
|
assert.False(t, isAnsible, "no node should carry ansible_kind meta: %s", n.ID)
|
|
}
|
|
}
|
|
}
|