1392 lines
38 KiB
Go
1392 lines
38 KiB
Go
package ansible
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// ============================================================
|
|
// Tests for non-SSH module handlers (0% coverage)
|
|
// ============================================================
|
|
|
|
// --- moduleDebug ---
|
|
|
|
func TestExecutorExtra_ModuleDebug_Good_Message(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleDebug("host1", nil, map[string]any{"msg": "Hello world"})
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Changed)
|
|
assert.Equal(t, "Hello world", result.Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleDebug_Good_Var(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.setHostVars("host1", map[string]any{"my_version": "1.2.3"})
|
|
|
|
result, err := e.moduleDebug("host1", nil, map[string]any{"var": "my_version"})
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, result.Msg, "1.2.3")
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleDebug_Good_EmptyArgs(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleDebug("host1", nil, map[string]any{})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "", result.Msg)
|
|
}
|
|
|
|
// --- moduleFail ---
|
|
|
|
func TestExecutorExtra_ModuleFail_Good_DefaultMessage(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleFail(map[string]any{})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Failed)
|
|
assert.Equal(t, "Failed as requested", result.Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleFail_Good_CustomMessage(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleFail(map[string]any{"msg": "deployment blocked"})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Failed)
|
|
assert.Equal(t, "deployment blocked", result.Msg)
|
|
}
|
|
|
|
// --- modulePing ---
|
|
|
|
func TestExecutorExtra_ModulePing_Good_DefaultPong(t *testing.T) {
|
|
e, mock := newTestExecutorWithMock("host1")
|
|
|
|
result, err := executeModuleWithMock(e, mock, "host1", &Task{
|
|
Module: "ping",
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Failed)
|
|
assert.False(t, result.Changed)
|
|
assert.Equal(t, "pong", result.Msg)
|
|
assert.True(t, mock.hasExecuted(`^true$`))
|
|
}
|
|
|
|
func TestExecutorExtra_ModulePing_Good_CustomData(t *testing.T) {
|
|
e, mock := newTestExecutorWithMock("host1")
|
|
|
|
result, err := executeModuleWithMock(e, mock, "host1", &Task{
|
|
Module: "ansible.builtin.ping",
|
|
Args: map[string]any{
|
|
"data": "hello",
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Failed)
|
|
assert.Equal(t, "hello", result.Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_ExecuteModule_Good_LegacyNamespaceCommand(t *testing.T) {
|
|
e, mock := newTestExecutorWithMock("host1")
|
|
mock.expectCommand("echo hello", "hello\n", "", 0)
|
|
|
|
result, err := executeModuleWithMock(e, mock, "host1", &Task{
|
|
Module: "ansible.legacy.command",
|
|
Args: map[string]any{"_raw_params": "echo hello"},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Changed)
|
|
assert.Equal(t, "hello\n", result.Stdout)
|
|
assert.True(t, mock.hasExecuted(`echo hello`))
|
|
}
|
|
|
|
// --- moduleAssert ---
|
|
|
|
func TestExecutorExtra_ModuleAssert_Good_PassingAssertion(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.vars["enabled"] = true
|
|
|
|
result, err := e.moduleAssert(map[string]any{"that": "enabled"}, "host1")
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Failed)
|
|
assert.Equal(t, "All assertions passed", result.Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleAssert_Bad_FailingAssertion(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.vars["enabled"] = false
|
|
|
|
result, err := e.moduleAssert(map[string]any{"that": "enabled"}, "host1")
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Failed)
|
|
assert.Contains(t, result.Msg, "Assertion failed")
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleAssert_Bad_MissingThat(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
|
|
_, err := e.moduleAssert(map[string]any{}, "host1")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleAssert_Good_CustomFailMsg(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.vars["ready"] = false
|
|
|
|
result, err := e.moduleAssert(map[string]any{
|
|
"that": "ready",
|
|
"fail_msg": "Service not ready",
|
|
}, "host1")
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Failed)
|
|
assert.Equal(t, "Service not ready", result.Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleAssert_Good_MultipleConditions(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.vars["enabled"] = true
|
|
e.vars["count"] = 5
|
|
|
|
result, err := e.moduleAssert(map[string]any{
|
|
"that": []any{"enabled", "count"},
|
|
}, "host1")
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Failed)
|
|
}
|
|
|
|
// --- moduleSetFact ---
|
|
|
|
func TestExecutorExtra_ModuleSetFact_Good(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
|
|
result, err := e.moduleSetFact("host1", map[string]any{
|
|
"app_version": "2.0.0",
|
|
"deploy_env": "production",
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Changed)
|
|
require.Contains(t, e.hostVars, "host1")
|
|
assert.Equal(t, "2.0.0", e.hostVars["host1"]["app_version"])
|
|
assert.Equal(t, "production", e.hostVars["host1"]["deploy_env"])
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleSetFact_Good_SkipsCacheable(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
|
|
e.moduleSetFact("host1", map[string]any{
|
|
"my_fact": "value",
|
|
"cacheable": true,
|
|
})
|
|
|
|
require.Contains(t, e.hostVars, "host1")
|
|
assert.Equal(t, "value", e.hostVars["host1"]["my_fact"])
|
|
_, hasCacheable := e.hostVars["host1"]["cacheable"]
|
|
assert.False(t, hasCacheable)
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleSetFact_Good_HostScopedLookup(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
|
|
_, err := e.moduleSetFact("host1", map[string]any{
|
|
"build_id": "2026.04.02",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "2026.04.02", e.templateString("{{ build_id }}", "host1", nil))
|
|
assert.Equal(t, "{{ build_id }}", e.templateString("{{ build_id }}", "host2", nil))
|
|
}
|
|
|
|
// --- moduleAddHost ---
|
|
|
|
func TestExecutorExtra_ModuleAddHost_Good_AddsHostAndGroups(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
|
|
result, err := e.moduleAddHost(map[string]any{
|
|
"name": "db1",
|
|
"groups": "databases,production",
|
|
"ansible_host": "10.0.0.5",
|
|
"ansible_port": "2222",
|
|
"ansible_user": "deploy",
|
|
"ansible_connection": "ssh",
|
|
"ansible_become_password": "secret",
|
|
"environment": "prod",
|
|
"custom_var": "custom-value",
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Changed)
|
|
assert.Equal(t, "db1", result.Data["host"])
|
|
assert.Contains(t, result.Msg, "db1")
|
|
|
|
require.NotNil(t, e.inventory)
|
|
require.NotNil(t, e.inventory.All)
|
|
require.NotNil(t, e.inventory.All.Hosts["db1"])
|
|
|
|
host := e.inventory.All.Hosts["db1"]
|
|
assert.Equal(t, "10.0.0.5", host.AnsibleHost)
|
|
assert.Equal(t, 2222, host.AnsiblePort)
|
|
assert.Equal(t, "deploy", host.AnsibleUser)
|
|
assert.Equal(t, "ssh", host.AnsibleConnection)
|
|
assert.Equal(t, "secret", host.AnsibleBecomePassword)
|
|
assert.Equal(t, "custom-value", host.Vars["custom_var"])
|
|
|
|
require.NotNil(t, e.inventory.All.Children["databases"])
|
|
require.NotNil(t, e.inventory.All.Children["production"])
|
|
assert.Same(t, host, e.inventory.All.Children["databases"].Hosts["db1"])
|
|
assert.Same(t, host, e.inventory.All.Children["production"].Hosts["db1"])
|
|
|
|
assert.Equal(t, []string{"db1"}, GetHosts(e.inventory, "all"))
|
|
assert.Equal(t, []string{"db1"}, GetHosts(e.inventory, "databases"))
|
|
assert.Equal(t, []string{"db1"}, GetHosts(e.inventory, "production"))
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleAddHost_Good_IdempotentRepeat(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
|
|
_, err := e.moduleAddHost(map[string]any{
|
|
"name": "cache1",
|
|
"groups": []any{"caches"},
|
|
"role": "redis",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
result, err := e.moduleAddHost(map[string]any{
|
|
"name": "cache1",
|
|
"groups": []any{"caches"},
|
|
"role": "redis",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.False(t, result.Changed)
|
|
assert.Equal(t, []string{"cache1"}, GetHosts(e.inventory, "caches"))
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleAddHost_Good_ThroughDispatcher(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
task := &Task{
|
|
Module: "add_host",
|
|
Args: map[string]any{
|
|
"name": "cache1",
|
|
"group": "caches",
|
|
"role": "redis",
|
|
},
|
|
}
|
|
|
|
result, err := e.executeModule(context.Background(), "host1", &SSHClient{}, task, &Play{})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Changed)
|
|
assert.Equal(t, "cache1", result.Data["host"])
|
|
assert.Equal(t, []string{"caches"}, result.Data["groups"])
|
|
assert.Equal(t, []string{"cache1"}, GetHosts(e.inventory, "all"))
|
|
assert.Equal(t, []string{"cache1"}, GetHosts(e.inventory, "caches"))
|
|
assert.Equal(t, "redis", e.inventory.All.Hosts["cache1"].Vars["role"])
|
|
}
|
|
|
|
// --- moduleGroupBy ---
|
|
|
|
func TestExecutorExtra_ModuleGroupBy_Good(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"web1": &Host{AnsibleHost: "10.0.0.10"},
|
|
},
|
|
},
|
|
})
|
|
|
|
result, err := e.moduleGroupBy("web1", map[string]any{"key": "debian"})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Changed)
|
|
assert.Equal(t, "web1", result.Data["host"])
|
|
assert.Equal(t, "debian", result.Data["group"])
|
|
assert.Contains(t, result.Msg, "web1")
|
|
assert.Contains(t, result.Msg, "debian")
|
|
assert.Equal(t, []string{"web1"}, GetHosts(e.inventory, "debian"))
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleGroupBy_Good_ThroughDispatcher(t *testing.T) {
|
|
e, mock := newTestExecutorWithMock("host1")
|
|
task := &Task{
|
|
Module: "group_by",
|
|
Args: map[string]any{
|
|
"key": "linux",
|
|
},
|
|
}
|
|
|
|
result, err := executeModuleWithMock(e, mock, "host1", task)
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Changed)
|
|
assert.Equal(t, []string{"host1"}, GetHosts(e.inventory, "linux"))
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleGroupBy_Bad_MissingKey(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
|
|
_, err := e.moduleGroupBy("host1", map[string]any{})
|
|
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "key required")
|
|
}
|
|
|
|
// --- moduleIncludeVars ---
|
|
|
|
func TestExecutorExtra_ModuleIncludeVars_Good_WithFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := joinPath(dir, "main.yml")
|
|
require.NoError(t, writeTestFile(path, []byte("app_name: demo\n"), 0644))
|
|
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleIncludeVars(map[string]any{"file": path})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Changed)
|
|
assert.Contains(t, result.Msg, path)
|
|
assert.Equal(t, "demo", e.vars["app_name"])
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleIncludeVars_Good_WithRawParams(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := joinPath(dir, "defaults.yml")
|
|
require.NoError(t, writeTestFile(path, []byte("app_port: 8080\n"), 0644))
|
|
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleIncludeVars(map[string]any{"_raw_params": path})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Changed)
|
|
assert.Contains(t, result.Msg, path)
|
|
assert.Equal(t, 8080, e.vars["app_port"])
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleIncludeVars_Good_Empty(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleIncludeVars(map[string]any{})
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Changed)
|
|
}
|
|
|
|
// --- moduleMeta ---
|
|
|
|
func TestExecutorExtra_ModuleMeta_Good(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleMeta(map[string]any{"_raw_params": "flush_handlers"})
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Changed)
|
|
require.NotNil(t, result.Data)
|
|
assert.Equal(t, "flush_handlers", result.Data["action"])
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleMeta_Good_ClearFacts(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.facts["host1"] = &Facts{Hostname: "web01"}
|
|
|
|
result, err := e.moduleMeta(map[string]any{"_raw_params": "clear_facts"})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Changed)
|
|
require.NotNil(t, result.Data)
|
|
assert.Equal(t, "clear_facts", result.Data["action"])
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleMeta_Good_ResetConnection(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleMeta(map[string]any{"_raw_params": "reset_connection"})
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Changed)
|
|
require.NotNil(t, result.Data)
|
|
assert.Equal(t, "reset_connection", result.Data["action"])
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleMeta_Good_EndHost(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleMeta(map[string]any{"_raw_params": "end_host"})
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Changed)
|
|
require.NotNil(t, result.Data)
|
|
assert.Equal(t, "end_host", result.Data["action"])
|
|
}
|
|
|
|
func TestExecutorExtra_ModuleMeta_Good_EndBatch(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result, err := e.moduleMeta(map[string]any{"_raw_params": "end_batch"})
|
|
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Changed)
|
|
require.NotNil(t, result.Data)
|
|
assert.Equal(t, "end_batch", result.Data["action"])
|
|
}
|
|
|
|
func TestExecutorExtra_HandleMetaAction_Good_ClearFacts(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.facts["host1"] = &Facts{Hostname: "web01"}
|
|
e.facts["host2"] = &Facts{Hostname: "web02"}
|
|
|
|
result := &TaskResult{Data: map[string]any{"action": "clear_facts"}}
|
|
require.NoError(t, e.handleMetaAction(context.Background(), "host1", []string{"host1"}, nil, result))
|
|
|
|
_, ok := e.facts["host1"]
|
|
assert.False(t, ok)
|
|
require.NotNil(t, e.facts["host2"])
|
|
assert.Equal(t, "web02", e.facts["host2"].Hostname)
|
|
}
|
|
|
|
func TestExecutorExtra_HandleMetaAction_Good_EndHost(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
|
|
result := &TaskResult{Data: map[string]any{"action": "end_host"}}
|
|
err := e.handleMetaAction(context.Background(), "host1", []string{"host1", "host2"}, nil, result)
|
|
|
|
require.ErrorIs(t, err, errEndHost)
|
|
assert.True(t, e.isHostEnded("host1"))
|
|
assert.False(t, e.isHostEnded("host2"))
|
|
}
|
|
|
|
func TestExecutorExtra_HandleMetaAction_Good_EndBatch(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
|
|
result := &TaskResult{Data: map[string]any{"action": "end_batch"}}
|
|
err := e.handleMetaAction(context.Background(), "host1", []string{"host1", "host2"}, nil, result)
|
|
|
|
require.ErrorIs(t, err, errEndBatch)
|
|
assert.False(t, e.isHostEnded("host1"))
|
|
assert.False(t, e.isHostEnded("host2"))
|
|
}
|
|
|
|
func TestExecutorExtra_HandleMetaAction_Good_ResetConnection(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
mock := NewMockSSHClient()
|
|
e.clients["host1"] = mock
|
|
e.clients["host2"] = NewMockSSHClient()
|
|
|
|
result := &TaskResult{Data: map[string]any{"action": "reset_connection"}}
|
|
require.NoError(t, e.handleMetaAction(context.Background(), "host1", []string{"host1", "host2"}, nil, result))
|
|
|
|
_, ok := e.clients["host1"]
|
|
assert.False(t, ok)
|
|
_, ok = e.clients["host2"]
|
|
assert.True(t, ok)
|
|
assert.True(t, mock.closed)
|
|
}
|
|
|
|
func TestExecutorExtra_RunTaskOnHosts_Good_EndHostSkipsFutureTasks(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"host1": {Vars: map[string]any{"retire_host": true}},
|
|
"host2": {Vars: map[string]any{"retire_host": false}},
|
|
},
|
|
},
|
|
})
|
|
e.clients["host1"] = NewMockSSHClient()
|
|
e.clients["host2"] = NewMockSSHClient()
|
|
|
|
var started []string
|
|
e.OnTaskStart = func(host string, task *Task) {
|
|
started = append(started, host+":"+task.Name)
|
|
}
|
|
|
|
play := &Play{}
|
|
first := &Task{
|
|
Name: "Retire host",
|
|
Module: "meta",
|
|
Args: map[string]any{"_raw_params": "end_host"},
|
|
When: "retire_host",
|
|
}
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"host1", "host2"}, first, play))
|
|
assert.True(t, e.isHostEnded("host1"))
|
|
assert.False(t, e.isHostEnded("host2"))
|
|
|
|
second := &Task{
|
|
Name: "Follow-up",
|
|
Module: "debug",
|
|
Args: map[string]any{"msg": "still running"},
|
|
}
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"host1", "host2"}, second, play))
|
|
|
|
assert.Contains(t, started, "host1:Retire host")
|
|
assert.Contains(t, started, "host2:Follow-up")
|
|
assert.NotContains(t, started, "host1:Follow-up")
|
|
}
|
|
|
|
func TestExecutorExtra_RunPlay_Good_MetaEndBatchAdvancesToNextSerialBatch(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"host1": {Vars: map[string]any{"end_batch": true}},
|
|
"host2": {Vars: map[string]any{"end_batch": false}},
|
|
"host3": {Vars: map[string]any{"end_batch": false}},
|
|
},
|
|
},
|
|
})
|
|
e.clients["host1"] = NewMockSSHClient()
|
|
e.clients["host2"] = NewMockSSHClient()
|
|
e.clients["host3"] = NewMockSSHClient()
|
|
|
|
serial := 1
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Hosts: "all",
|
|
Serial: serial,
|
|
GatherFacts: &gatherFacts,
|
|
Tasks: []Task{
|
|
{
|
|
Name: "end current batch",
|
|
Module: "meta",
|
|
Args: map[string]any{"_raw_params": "end_batch"},
|
|
When: "end_batch",
|
|
},
|
|
{
|
|
Name: "follow-up",
|
|
Module: "debug",
|
|
Args: map[string]any{"msg": "next batch"},
|
|
},
|
|
},
|
|
}
|
|
|
|
var started []string
|
|
e.OnTaskStart = func(host string, task *Task) {
|
|
started = append(started, host+":"+task.Name)
|
|
}
|
|
|
|
require.NoError(t, e.runPlay(context.Background(), play))
|
|
|
|
assert.Contains(t, started, "host1:end current batch")
|
|
assert.NotContains(t, started, "host1:follow-up")
|
|
assert.Contains(t, started, "host2:follow-up")
|
|
assert.Contains(t, started, "host3:follow-up")
|
|
}
|
|
|
|
func TestExecutorExtra_SplitSerialHosts_Good_ListValues(t *testing.T) {
|
|
batches := splitSerialHosts([]string{"host1", "host2", "host3", "host4"}, []any{1, "50%"})
|
|
|
|
require.Len(t, batches, 3)
|
|
assert.Equal(t, []string{"host1"}, batches[0])
|
|
assert.Equal(t, []string{"host2", "host3"}, batches[1])
|
|
assert.Equal(t, []string{"host4"}, batches[2])
|
|
}
|
|
|
|
func TestExecutorExtra_SplitSerialHosts_Good_ListRepeatsLastValue(t *testing.T) {
|
|
batches := splitSerialHosts([]string{"host1", "host2", "host3", "host4", "host5"}, []any{2, 1})
|
|
|
|
require.Len(t, batches, 4)
|
|
assert.Equal(t, []string{"host1", "host2"}, batches[0])
|
|
assert.Equal(t, []string{"host3"}, batches[1])
|
|
assert.Equal(t, []string{"host4"}, batches[2])
|
|
assert.Equal(t, []string{"host5"}, batches[3])
|
|
}
|
|
|
|
// ============================================================
|
|
// Tests for handleLookup (0% coverage)
|
|
// ============================================================
|
|
|
|
func TestExecutorExtra_HandleLookup_Good_EnvVar(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
t.Setenv("TEST_ANSIBLE_LOOKUP", "found_it")
|
|
|
|
result := e.handleLookup("lookup('env', 'TEST_ANSIBLE_LOOKUP')", "", nil)
|
|
assert.Equal(t, "found_it", result)
|
|
}
|
|
|
|
func TestExecutorExtra_HandleLookup_Good_EnvVarMissing(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result := e.handleLookup("lookup('env', 'NONEXISTENT_VAR_12345')", "", nil)
|
|
assert.Equal(t, "", result)
|
|
}
|
|
|
|
func TestExecutorExtra_HandleLookup_Good_FileLookupResolvesBasePath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, writeTestFile(joinPath(dir, "vars.txt"), []byte("from base path"), 0644))
|
|
|
|
e := NewExecutor(dir)
|
|
result := e.handleLookup("lookup('file', 'vars.txt')", "", nil)
|
|
|
|
assert.Equal(t, "from base path", result)
|
|
}
|
|
|
|
func TestExecutorExtra_HandleLookup_Good_VarsLookup(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.SetVar("lookup_value", "resolved from vars")
|
|
|
|
result := e.handleLookup("lookup('vars', 'lookup_value')", "", nil)
|
|
|
|
assert.Equal(t, "resolved from vars", result)
|
|
}
|
|
|
|
func TestExecutorExtra_HandleLookup_Bad_InvalidSyntax(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
result := e.handleLookup("lookup(invalid)", "", nil)
|
|
assert.Equal(t, "", result)
|
|
}
|
|
|
|
// ============================================================
|
|
// Tests for SetInventory (0% coverage)
|
|
// ============================================================
|
|
|
|
func TestExecutorExtra_SetInventory_Good(t *testing.T) {
|
|
dir := t.TempDir()
|
|
invPath := joinPath(dir, "inventory.yml")
|
|
yaml := `all:
|
|
hosts:
|
|
web1:
|
|
ansible_host: 10.0.0.1
|
|
web2:
|
|
ansible_host: 10.0.0.2
|
|
`
|
|
require.NoError(t, writeTestFile(invPath, []byte(yaml), 0644))
|
|
|
|
e := NewExecutor(dir)
|
|
err := e.SetInventory(invPath)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, e.inventory)
|
|
assert.Len(t, e.inventory.All.Hosts, 2)
|
|
}
|
|
|
|
func TestExecutorExtra_SetInventory_Good_Directory(t *testing.T) {
|
|
dir := t.TempDir()
|
|
inventoryDir := joinPath(dir, "inventory")
|
|
require.NoError(t, os.MkdirAll(inventoryDir, 0755))
|
|
|
|
invPath := joinPath(inventoryDir, "inventory.yml")
|
|
yaml := `all:
|
|
hosts:
|
|
web1:
|
|
ansible_host: 10.0.0.1
|
|
`
|
|
require.NoError(t, writeTestFile(invPath, []byte(yaml), 0644))
|
|
|
|
e := NewExecutor(dir)
|
|
err := e.SetInventory(inventoryDir)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, e.inventory)
|
|
assert.Contains(t, e.inventory.All.Hosts, "web1")
|
|
}
|
|
|
|
func TestExecutorExtra_SetInventory_Bad_FileNotFound(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
err := e.SetInventory("/nonexistent/inventory.yml")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// ============================================================
|
|
// Tests for iterator functions (0% coverage)
|
|
// ============================================================
|
|
|
|
func TestExecutorExtra_ParsePlaybookIter_Good(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := joinPath(dir, "playbook.yml")
|
|
yaml := `- name: First play
|
|
hosts: all
|
|
tasks:
|
|
- debug:
|
|
msg: hello
|
|
|
|
- name: Second play
|
|
hosts: localhost
|
|
tasks:
|
|
- debug:
|
|
msg: world
|
|
`
|
|
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
|
|
|
|
p := NewParser(dir)
|
|
iter, err := p.ParsePlaybookIter(path)
|
|
require.NoError(t, err)
|
|
|
|
var plays []Play
|
|
for play := range iter {
|
|
plays = append(plays, play)
|
|
}
|
|
assert.Len(t, plays, 2)
|
|
assert.Equal(t, "First play", plays[0].Name)
|
|
assert.Equal(t, "Second play", plays[1].Name)
|
|
}
|
|
|
|
func TestExecutorExtra_ParsePlaybookIter_Bad_InvalidFile(t *testing.T) {
|
|
_, err := NewParser("/tmp").ParsePlaybookIter("/nonexistent.yml")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestExecutorExtra_ParseTasksIter_Good(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := joinPath(dir, "tasks.yml")
|
|
yaml := `- name: Task one
|
|
debug:
|
|
msg: first
|
|
|
|
- name: Task two
|
|
debug:
|
|
msg: second
|
|
`
|
|
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
|
|
|
|
p := NewParser(dir)
|
|
iter, err := p.ParseTasksIter(path)
|
|
require.NoError(t, err)
|
|
|
|
var tasks []Task
|
|
for task := range iter {
|
|
tasks = append(tasks, task)
|
|
}
|
|
assert.Len(t, tasks, 2)
|
|
assert.Equal(t, "Task one", tasks[0].Name)
|
|
}
|
|
|
|
func TestExecutorExtra_ParseTasksIter_Bad_InvalidFile(t *testing.T) {
|
|
_, err := NewParser("/tmp").ParseTasksIter("/nonexistent.yml")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeTasks_Good_RelativePath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
includedPath := joinPath(dir, "included.yml")
|
|
yaml := `- name: Included first task
|
|
debug:
|
|
msg: first
|
|
|
|
- name: Included second task
|
|
debug:
|
|
msg: second
|
|
`
|
|
require.NoError(t, writeTestFile(includedPath, []byte(yaml), 0644))
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Name: "Include tasks",
|
|
Hosts: "localhost",
|
|
GatherFacts: &gatherFacts,
|
|
Tasks: []Task{
|
|
{
|
|
Name: "Load included tasks",
|
|
IncludeTasks: "included.yml",
|
|
},
|
|
},
|
|
}
|
|
|
|
e := NewExecutor(dir)
|
|
var started []string
|
|
e.OnTaskStart = func(host string, task *Task) {
|
|
started = append(started, host+":"+task.Name)
|
|
}
|
|
|
|
require.NoError(t, e.runPlay(context.Background(), play))
|
|
|
|
assert.Contains(t, started, "localhost:Included first task")
|
|
assert.Contains(t, started, "localhost:Included second task")
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeTasks_Good_InheritsTaskVars(t *testing.T) {
|
|
dir := t.TempDir()
|
|
includedPath := joinPath(dir, "included-vars.yml")
|
|
yaml := `- name: Included var task
|
|
debug:
|
|
msg: "{{ include_message }}"
|
|
register: included_result
|
|
`
|
|
require.NoError(t, writeTestFile(includedPath, []byte(yaml), 0644))
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Name: "Include vars",
|
|
Hosts: "localhost",
|
|
GatherFacts: &gatherFacts,
|
|
Tasks: []Task{
|
|
{
|
|
Name: "Load included tasks",
|
|
IncludeTasks: "included-vars.yml",
|
|
Vars: map[string]any{"include_message": "hello from include"},
|
|
},
|
|
},
|
|
}
|
|
|
|
e := NewExecutor(dir)
|
|
require.NoError(t, e.runPlay(context.Background(), play))
|
|
|
|
require.NotNil(t, e.results["localhost"]["included_result"])
|
|
assert.Equal(t, "hello from include", e.results["localhost"]["included_result"].Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeTasks_Good_HostSpecificTemplate(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
require.NoError(t, writeTestFile(joinPath(dir, "web.yml"), []byte(`- name: Web included task
|
|
debug:
|
|
msg: web
|
|
`), 0644))
|
|
require.NoError(t, writeTestFile(joinPath(dir, "db.yml"), []byte(`- name: DB included task
|
|
debug:
|
|
msg: db
|
|
`), 0644))
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Name: "Include host-specific tasks",
|
|
Hosts: "all",
|
|
Connection: "local",
|
|
GatherFacts: &gatherFacts,
|
|
}
|
|
|
|
e := NewExecutor(dir)
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"web1": {
|
|
AnsibleConnection: "local",
|
|
Vars: map[string]any{
|
|
"include_file": "web.yml",
|
|
},
|
|
},
|
|
"db1": {
|
|
AnsibleConnection: "local",
|
|
Vars: map[string]any{
|
|
"include_file": "db.yml",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
var started []string
|
|
e.OnTaskStart = func(host string, task *Task) {
|
|
started = append(started, host+":"+task.Name)
|
|
}
|
|
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"web1", "db1"}, &Task{
|
|
Name: "Load host-specific tasks",
|
|
IncludeTasks: "{{ include_file }}",
|
|
}, play))
|
|
|
|
assert.Contains(t, started, "web1:Web included task")
|
|
assert.Contains(t, started, "db1:DB included task")
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeTasks_Good_HonoursWhen(t *testing.T) {
|
|
dir := t.TempDir()
|
|
includedPath := joinPath(dir, "conditional.yml")
|
|
yaml := `- name: Conditional included task
|
|
debug:
|
|
msg: should not run
|
|
`
|
|
require.NoError(t, writeTestFile(includedPath, []byte(yaml), 0644))
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Name: "Conditional include",
|
|
Hosts: "localhost",
|
|
GatherFacts: &gatherFacts,
|
|
}
|
|
|
|
e := NewExecutor(dir)
|
|
e.SetVar("include_enabled", false)
|
|
|
|
var started []string
|
|
e.OnTaskStart = func(host string, task *Task) {
|
|
started = append(started, host+":"+task.Name)
|
|
}
|
|
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"localhost"}, &Task{
|
|
Name: "Load conditional tasks",
|
|
IncludeTasks: "conditional.yml",
|
|
When: "include_enabled",
|
|
}, play))
|
|
|
|
assert.Empty(t, started)
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeTasks_Good_TemplatesVarsAndInheritsTags(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, writeTestFile(joinPath(dir, "tasks", "common.yml"), []byte(`---
|
|
- name: Included tagged task
|
|
debug:
|
|
msg: "{{ include_message }}"
|
|
register: include_result
|
|
tags:
|
|
- child-tag
|
|
`), 0644))
|
|
|
|
e := NewExecutor(dir)
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"localhost": {},
|
|
},
|
|
},
|
|
})
|
|
e.SetVar("env_name", "production")
|
|
e.Tags = []string{"include-tag"}
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Hosts: "localhost",
|
|
Connection: "local",
|
|
GatherFacts: &gatherFacts,
|
|
}
|
|
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"localhost"}, &Task{
|
|
Name: "Load included tasks",
|
|
IncludeTasks: "tasks/common.yml",
|
|
Tags: []string{"include-tag"},
|
|
Vars: map[string]any{
|
|
"include_message": "{{ env_name }}",
|
|
},
|
|
}, play))
|
|
|
|
require.NotNil(t, e.results["localhost"]["include_result"])
|
|
assert.Equal(t, "production", e.results["localhost"]["include_result"].Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeRole_Good_InheritsTaskVars(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, writeTestFile(joinPath(dir, "roles", "demo", "tasks", "main.yml"), []byte(`---
|
|
- name: Role var task
|
|
debug:
|
|
msg: "{{ role_message }}"
|
|
register: role_result
|
|
`), 0644))
|
|
|
|
e := NewExecutor(dir)
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"localhost": {},
|
|
},
|
|
},
|
|
})
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Hosts: "localhost",
|
|
Connection: "local",
|
|
GatherFacts: &gatherFacts,
|
|
}
|
|
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"localhost"}, &Task{
|
|
Name: "Load role",
|
|
IncludeRole: &RoleRef{
|
|
Role: "demo",
|
|
},
|
|
Vars: map[string]any{"role_message": "hello from role"},
|
|
}, play))
|
|
|
|
require.NotNil(t, e.results["localhost"]["role_result"])
|
|
assert.Equal(t, "hello from role", e.results["localhost"]["role_result"].Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeRole_Good_AppliesRoleDefaults(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, writeTestFile(joinPath(dir, "roles", "app", "tasks", "main.yml"), []byte(`---
|
|
- name: Applied role task
|
|
vars:
|
|
role_message: from-task
|
|
shell: printf '%s|%s|%s' "$APP_ENV" "{{ apply_message }}" "{{ role_message }}"
|
|
register: role_result
|
|
`), 0644))
|
|
|
|
e := NewExecutor(dir)
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"localhost": {},
|
|
},
|
|
},
|
|
})
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Hosts: "localhost",
|
|
Connection: "local",
|
|
GatherFacts: &gatherFacts,
|
|
}
|
|
|
|
var started *Task
|
|
e.OnTaskStart = func(host string, task *Task) {
|
|
if task.Name == "Applied role task" {
|
|
started = task
|
|
}
|
|
}
|
|
|
|
// Re-run with callback attached so we can inspect the merged task state.
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"localhost"}, &Task{
|
|
Name: "Load role with apply",
|
|
IncludeRole: &RoleRef{
|
|
Role: "app",
|
|
Apply: &TaskApply{
|
|
Tags: []string{"role-apply"},
|
|
Vars: map[string]any{
|
|
"apply_message": "from-apply",
|
|
"role_message": "from-apply",
|
|
},
|
|
Environment: map[string]string{
|
|
"APP_ENV": "production",
|
|
},
|
|
},
|
|
},
|
|
}, play))
|
|
|
|
require.NotNil(t, started)
|
|
assert.Contains(t, started.Tags, "role-apply")
|
|
assert.Equal(t, "production", started.Environment["APP_ENV"])
|
|
assert.Equal(t, "from-apply", started.Vars["apply_message"])
|
|
assert.Equal(t, "from-task", started.Vars["role_message"])
|
|
require.NotNil(t, e.results["localhost"]["role_result"])
|
|
assert.Equal(t, "production|from-apply|from-task", e.results["localhost"]["role_result"].Stdout)
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeRole_Good_AppliesRoleWhen(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, writeTestFile(joinPath(dir, "roles", "app", "tasks", "main.yml"), []byte(`---
|
|
- name: Conditional role task
|
|
debug:
|
|
msg: "role task ran"
|
|
when: task_enabled
|
|
register: role_result
|
|
`), 0644))
|
|
|
|
e, _ := newTestExecutorWithMock("localhost")
|
|
e.parser = NewParser(dir)
|
|
e.SetVar("task_enabled", true)
|
|
e.SetVar("apply_enabled", false)
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Hosts: "localhost",
|
|
Connection: "local",
|
|
GatherFacts: &gatherFacts,
|
|
}
|
|
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"localhost"}, &Task{
|
|
Name: "Load role with conditional apply",
|
|
IncludeRole: &RoleRef{
|
|
Role: "app",
|
|
Apply: &TaskApply{
|
|
When: "apply_enabled",
|
|
},
|
|
},
|
|
}, play))
|
|
|
|
require.NotNil(t, e.results["localhost"]["role_result"])
|
|
assert.True(t, e.results["localhost"]["role_result"].Skipped)
|
|
assert.Equal(t, "Skipped due to when condition", e.results["localhost"]["role_result"].Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeRole_Good_UsesRoleRefTagsForSelection(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, writeTestFile(joinPath(dir, "roles", "tagged", "tasks", "main.yml"), []byte(`---
|
|
- name: Tagged role task
|
|
debug:
|
|
msg: "role task ran"
|
|
register: role_result
|
|
`), 0644))
|
|
|
|
e := NewExecutor(dir)
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"host1": {},
|
|
},
|
|
},
|
|
})
|
|
e.Tags = []string{"role-tag"}
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Hosts: "host1",
|
|
GatherFacts: &gatherFacts,
|
|
}
|
|
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"host1"}, &Task{
|
|
Name: "Load tagged role",
|
|
IncludeRole: &RoleRef{
|
|
Role: "tagged",
|
|
Tags: []string{"role-tag"},
|
|
},
|
|
}, play))
|
|
|
|
require.NotNil(t, e.results["host1"]["role_result"])
|
|
assert.Equal(t, "role task ran", e.results["host1"]["role_result"].Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeRole_Good_HonoursRoleRefWhen(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, writeTestFile(joinPath(dir, "roles", "conditional", "tasks", "main.yml"), []byte(`---
|
|
- name: Conditional role task
|
|
debug:
|
|
msg: "role task ran"
|
|
register: role_result
|
|
`), 0644))
|
|
|
|
e := NewExecutor(dir)
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"host1": {},
|
|
},
|
|
},
|
|
})
|
|
e.SetVar("role_enabled", false)
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Hosts: "host1",
|
|
GatherFacts: &gatherFacts,
|
|
}
|
|
|
|
var started []string
|
|
e.OnTaskStart = func(_ string, task *Task) {
|
|
started = append(started, task.Name)
|
|
}
|
|
|
|
require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"host1"}, &Task{
|
|
Name: "Load conditional role",
|
|
IncludeRole: &RoleRef{
|
|
Role: "conditional",
|
|
When: "role_enabled",
|
|
},
|
|
}, play))
|
|
|
|
assert.Empty(t, started)
|
|
if results := e.results["host1"]; results != nil {
|
|
_, ok := results["role_result"]
|
|
assert.False(t, ok)
|
|
}
|
|
}
|
|
|
|
func TestExecutorExtra_RunIncludeRole_Good_PublicVarsPersist(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, writeTestFile(joinPath(dir, "roles", "shared", "tasks", "main.yml"), []byte(`---
|
|
- name: Shared role task
|
|
debug:
|
|
msg: "{{ shared_message }}"
|
|
register: shared_role_result
|
|
`), 0644))
|
|
|
|
e := NewExecutor(dir)
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"localhost": {},
|
|
},
|
|
},
|
|
})
|
|
|
|
gatherFacts := false
|
|
play := &Play{
|
|
Hosts: "localhost",
|
|
Connection: "local",
|
|
GatherFacts: &gatherFacts,
|
|
Tasks: []Task{
|
|
{
|
|
Name: "Load public role",
|
|
IncludeRole: &RoleRef{
|
|
Role: "shared",
|
|
Public: true,
|
|
},
|
|
Vars: map[string]any{"shared_message": "hello from public role"},
|
|
},
|
|
{
|
|
Name: "Use public role vars",
|
|
Module: "debug",
|
|
Args: map[string]any{
|
|
"msg": "{{ shared_message }}",
|
|
},
|
|
Register: "after_public_role",
|
|
},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, e.runPlay(context.Background(), play))
|
|
|
|
require.NotNil(t, e.results["localhost"]["shared_role_result"])
|
|
assert.Equal(t, "hello from public role", e.results["localhost"]["shared_role_result"].Msg)
|
|
require.NotNil(t, e.results["localhost"]["after_public_role"])
|
|
assert.Equal(t, "hello from public role", e.results["localhost"]["after_public_role"].Msg)
|
|
}
|
|
|
|
func TestExecutorExtra_GetHostsIter_Good(t *testing.T) {
|
|
inv := &Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"web1": {},
|
|
"web2": {},
|
|
"db1": {},
|
|
},
|
|
},
|
|
}
|
|
|
|
var hosts []string
|
|
for host := range GetHostsIter(inv, "all") {
|
|
hosts = append(hosts, host)
|
|
}
|
|
assert.Len(t, hosts, 3)
|
|
}
|
|
|
|
func TestExecutorExtra_AllHostsIter_Good(t *testing.T) {
|
|
group := &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"alpha": {},
|
|
"beta": {},
|
|
},
|
|
Children: map[string]*InventoryGroup{
|
|
"db": {
|
|
Hosts: map[string]*Host{
|
|
"gamma": {},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
var hosts []string
|
|
for host := range AllHostsIter(group) {
|
|
hosts = append(hosts, host)
|
|
}
|
|
assert.Len(t, hosts, 3)
|
|
// AllHostsIter sorts keys
|
|
assert.Equal(t, "alpha", hosts[0])
|
|
assert.Equal(t, "beta", hosts[1])
|
|
assert.Equal(t, "gamma", hosts[2])
|
|
}
|
|
|
|
func TestExecutorExtra_AllHostsIter_Good_NilGroup(t *testing.T) {
|
|
var count int
|
|
for range AllHostsIter(nil) {
|
|
count++
|
|
}
|
|
assert.Equal(t, 0, count)
|
|
}
|
|
|
|
// ============================================================
|
|
// Tests for resolveExpr with registered vars (additional coverage)
|
|
// ============================================================
|
|
|
|
func TestExecutorExtra_ResolveExpr_Good_RegisteredVarFields(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.results["host1"] = map[string]*TaskResult{
|
|
"cmd_result": {
|
|
Stdout: "output text",
|
|
Stderr: "error text",
|
|
RC: 0,
|
|
Changed: true,
|
|
Failed: false,
|
|
},
|
|
}
|
|
|
|
assert.Equal(t, "output text", e.resolveExpr("cmd_result.stdout", "host1", nil))
|
|
assert.Equal(t, "error text", e.resolveExpr("cmd_result.stderr", "host1", nil))
|
|
assert.Equal(t, "0", e.resolveExpr("cmd_result.rc", "host1", nil))
|
|
assert.Equal(t, "true", e.resolveExpr("cmd_result.changed", "host1", nil))
|
|
assert.Equal(t, "false", e.resolveExpr("cmd_result.failed", "host1", nil))
|
|
}
|
|
|
|
func TestExecutorExtra_ResolveExpr_Good_TaskVars(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
task := &Task{
|
|
Vars: map[string]any{"local_var": "local_value"},
|
|
}
|
|
|
|
result := e.resolveExpr("local_var", "host1", task)
|
|
assert.Equal(t, "local_value", result)
|
|
}
|
|
|
|
func TestExecutorExtra_ResolveExpr_Good_HostVars(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.SetInventoryDirect(&Inventory{
|
|
All: &InventoryGroup{
|
|
Hosts: map[string]*Host{
|
|
"host1": {AnsibleHost: "10.0.0.1"},
|
|
},
|
|
},
|
|
})
|
|
|
|
result := e.resolveExpr("ansible_host", "host1", nil)
|
|
assert.Equal(t, "10.0.0.1", result)
|
|
}
|
|
|
|
func TestExecutorExtra_ResolveExpr_Good_Facts(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.facts["host1"] = &Facts{
|
|
Hostname: "web01",
|
|
FQDN: "web01.example.com",
|
|
Distribution: "ubuntu",
|
|
Version: "22.04",
|
|
Architecture: "x86_64",
|
|
Kernel: "5.15.0",
|
|
}
|
|
|
|
assert.Equal(t, "web01", e.resolveExpr("ansible_hostname", "host1", nil))
|
|
assert.Equal(t, "web01.example.com", e.resolveExpr("ansible_fqdn", "host1", nil))
|
|
assert.Equal(t, "ubuntu", e.resolveExpr("ansible_distribution", "host1", nil))
|
|
assert.Equal(t, "22.04", e.resolveExpr("ansible_distribution_version", "host1", nil))
|
|
assert.Equal(t, "x86_64", e.resolveExpr("ansible_architecture", "host1", nil))
|
|
assert.Equal(t, "5.15.0", e.resolveExpr("ansible_kernel", "host1", nil))
|
|
}
|
|
|
|
// --- applyFilter additional coverage ---
|
|
|
|
func TestExecutorExtra_ApplyFilter_Good_B64Decode(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
assert.Equal(t, "hello", e.applyFilter("aGVsbG8=", "b64decode"))
|
|
}
|
|
|
|
func TestExecutorExtra_ApplyFilter_Good_B64Encode(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
assert.Equal(t, "aGVsbG8=", e.applyFilter("hello", "b64encode"))
|
|
}
|
|
|
|
func TestExecutorExtra_ApplyFilter_Good_UnknownFilter(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
assert.Equal(t, "value", e.applyFilter("value", "unknown_filter"))
|
|
}
|
|
|
|
// --- evalCondition with default filter ---
|
|
|
|
func TestExecutorExtra_EvalCondition_Good_DefaultFilter(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
assert.True(t, e.evalCondition("myvar | default('fallback')", "host1"))
|
|
}
|
|
|
|
func TestExecutorExtra_EvalCondition_Good_UndefinedCheck(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
assert.True(t, e.evalCondition("missing_var is not defined", "host1"))
|
|
assert.True(t, e.evalCondition("missing_var is undefined", "host1"))
|
|
}
|
|
|
|
// --- resolveExpr with filter pipe ---
|
|
|
|
func TestExecutorExtra_ResolveExpr_Good_WithFilter(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.vars["raw_value"] = " trimmed "
|
|
|
|
result := e.resolveExpr("raw_value | trim", "host1", nil)
|
|
assert.Equal(t, "trimmed", result)
|
|
}
|
|
|
|
func TestExecutorExtra_ResolveExpr_Good_WithB64Encode(t *testing.T) {
|
|
e := NewExecutor("/tmp")
|
|
e.vars["raw_value"] = "hello"
|
|
|
|
result := e.resolveExpr("raw_value | b64encode", "host1", nil)
|
|
assert.Equal(t, "aGVsbG8=", result)
|
|
}
|