diff --git a/pkg/core/model/args_auto_map.go b/pkg/core/model/args_auto_map.go index 8d9f600..69f7821 100644 --- a/pkg/core/model/args_auto_map.go +++ b/pkg/core/model/args_auto_map.go @@ -30,7 +30,7 @@ func autoMapArg2EnvForCmd( targetCmd *Cmd, stackDepth int) (done bool) { - targetCmd.GetArgsAutoMapStatus().MarkMet(srcCmd) + targetCmd.GetArgsAutoMapStatus().MarkMetWithArgv(srcCmd, argv) if !srcCmd.HasSubFlow(true) { return false } @@ -50,6 +50,10 @@ func autoMapArg2EnvForCmd( flowEnv := env.NewLayer(EnvLayerSubFlow) parsedFlow.GlobalEnv.WriteNotArgTo(flowEnv, cc.Cmds.Strs.EnvValDelAllMark) + for key := range parsedFlow.GlobalEnv { + targetCmd.GetArgsAutoMapStatus().RecordGlobalEnvKey(key) + } + return autoMapArg2EnvForCmdsInFlow(cc, flowEnv, parsedFlow, 0, envOpCmds, targetCmd, stackDepth) } @@ -83,9 +87,15 @@ func autoMapArg2EnvForCmdsInFlow( return true } - TryExeEnvOpCmds(argv, cc, cmdEnv, flow, i, envOpCmds, nil, + var checker EnvOpsChecker + checker = EnvOpsChecker{} + TryExeEnvOpCmds(argv, cc, cmdEnv, flow, i, envOpCmds, &checker, "failed to execute env op-cmd in depends collecting") + for key := range checker { + targetCmd.GetArgsAutoMapStatus().RecordGlobalEnvKey(key) + } + //println(cic.Owner().DisplayPath(), " (arg2env) =>", targetCmd.Owner().DisplayPath()) targetCmd.AddArg2EnvFromAnotherCmd(cic) if targetCmd.GetArgsAutoMapStatus().FullyMapped() { @@ -216,6 +226,14 @@ func (self *ArgsAutoMapStatus) MarkMet(srcCmd *Cmd) { self.recordProvidedKeys(srcCmd) } +func (self *ArgsAutoMapStatus) MarkMetWithArgv(srcCmd *Cmd, argv ArgVals) { + if _, ok := self.metCmds[srcCmd]; ok { + return + } + self.metCmds[srcCmd] = true + self.recordProvidedKeysWithArgv(srcCmd, argv) +} + func (self *ArgsAutoMapStatus) FullyMappedOrMapAll() bool { if self.mapAll || self.mapNoProvider { return true @@ -373,4 +391,33 @@ func (self *ArgsAutoMapStatus) recordProvidedKeys(srcCmd *Cmd) { for _, key := range envOps.AllWriteKeys() { self.providedKeys[key] = true } + + val2env := srcCmd.GetVal2Env() + for _, key := range val2env.EnvKeys() { + self.providedKeys[key] = true + } +} + +func (self *ArgsAutoMapStatus) recordProvidedKeysWithArgv(srcCmd *Cmd, argv ArgVals) { + envOps := srcCmd.EnvOps() + for _, key := range envOps.AllWriteKeys() { + self.providedKeys[key] = true + } + + val2env := srcCmd.GetVal2Env() + for _, key := range val2env.EnvKeys() { + self.providedKeys[key] = true + } + + arg2env := srcCmd.GetArg2Env() + for _, envKey := range arg2env.EnvKeys() { + argName := arg2env.GetArgName(srcCmd, envKey, true) + if val, ok := argv[argName]; ok && val.Provided { + self.providedKeys[envKey] = true + } + } +} + +func (self *ArgsAutoMapStatus) RecordGlobalEnvKey(key string) { + self.providedKeys[key] = true } diff --git a/pkg/core/model/args_auto_map_conflict_test.go b/pkg/core/model/args_auto_map_conflict_test.go new file mode 100644 index 0000000..7080e0f --- /dev/null +++ b/pkg/core/model/args_auto_map_conflict_test.go @@ -0,0 +1,326 @@ +package model + +import ( + "testing" +) + +func TestArgsAutoMapSameKeyFromDifferentSources(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create source 1 with key1 + src1Cmd := tree.AddSub("src1") + src1 := src1Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "source 1") + src1.AddArg("arg1", "default1") + src1.AddArg2Env("conflict.key", "arg1") + + // Create source 2 with same key + src2Cmd := tree.AddSub("src2") + src2 := src2Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "source 2") + src2.AddArg("arg2", "default2") + src2.AddArg2Env("conflict.key", "arg2") + + // Create target with auto map + targetCmd := tree.AddSub("conflicttarget1") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "conflict target 1") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark both sources + argv1 := ArgVals{"arg1": ArgVal{Raw: "val1", Provided: true}} + argv2 := ArgVals{"arg2": ArgVal{Raw: "val2", Provided: true}} + + status.MarkMetWithArgv(src1, argv1) + status.MarkMetWithArgv(src2, argv2) + + // The key should be recorded once (from whichever source marked it first) + if !status.providedKeys["conflict.key"] { + t.Error("conflict key should be recorded as provided") + } +} + +func TestArgsAutoMapArgNameConflictFromDifferentSources(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create source 1 with arg "data" + src1Cmd := tree.AddSub("srcarg1") + src1 := src1Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "source arg 1") + src1.AddArg("data", "default1") + src1.AddArg2Env("key1", "data") + + // Create source 2 with same arg name "data" + src2Cmd := tree.AddSub("srcarg2") + src2 := src2Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "source arg 2") + src2.AddArg("data", "default2") + src2.AddArg2Env("key2", "data") + + // Create target with auto map + targetCmd := tree.AddSub("argconflicttarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "arg conflict target") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark both sources with provided args + argv1 := ArgVals{"data": ArgVal{Raw: "val1", Provided: true}} + argv2 := ArgVals{"data": ArgVal{Raw: "val2", Provided: true}} + + status.MarkMetWithArgv(src1, argv1) + status.MarkMetWithArgv(src2, argv2) + + // Both keys should be recorded + if !status.providedKeys["key1"] { + t.Error("key1 should be recorded as provided") + } + if !status.providedKeys["key2"] { + t.Error("key2 should be recorded as provided") + } +} + +func TestArgsAutoMapNestedFlowKeyConflict(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create inner command with key + innerCmd := tree.AddSub("innerconflict") + inner := innerCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "inner conflict") + inner.AddArg("arg", "default") + inner.AddArg2Env("nested.conflict.key", "arg") + + // Create outer command with same key + outerCmd := tree.AddSub("outerconflict") + outer := outerCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "outer conflict") + outer.AddArg("arg", "default") + outer.AddArg2Env("nested.conflict.key", "arg") + + // Create target with auto map + targetCmd := tree.AddSub("nestedconflicttarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "nested conflict target") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark both with provided args + argvInner := ArgVals{"arg": ArgVal{Raw: "inner-val", Provided: true}} + argvOuter := ArgVals{"arg": ArgVal{Raw: "outer-val", Provided: true}} + + status.MarkMetWithArgv(inner, argvInner) + status.MarkMetWithArgv(outer, argvOuter) + + // The key should be recorded (once) + if !status.providedKeys["nested.conflict.key"] { + t.Error("nested conflict key should be recorded as provided") + } +} + +func TestArgsAutoMapMultipleAbbrConflict(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create source with arg that has abbr + srcCmd := tree.AddSub("abbrconflictsrc") + src := srcCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "abbr conflict source") + src.AddArg("longargname", "default", "lan", "ln") + src.AddArg2Env("abbr.key", "longargname") + + // Create target with auto map + targetCmd := tree.AddSub("abbrconflicttarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "abbr conflict target") + + // Target already has an arg with conflicting abbr + target.AddArg("existing", "default", "lan") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark source with provided arg + argv := ArgVals{"longargname": ArgVal{Raw: "val", Provided: true}} + status.MarkMetWithArgv(src, argv) + + // The key should be recorded + if !status.providedKeys["abbr.key"] { + t.Error("abbr key should be recorded as provided") + } +} + +func TestArgsAutoMapKeyConflictWithVal2EnvAndArg2Env(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create source with both val2env and arg2env to same key + srcCmd := tree.AddSub("valargconflictsrc") + src := srcCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "val arg conflict source") + + // Add val2env first + src.AddVal2Env("conflict.key", "val-value") + + // Try to add arg2env to same key - this should be prevented by AddArg2Env + // But we can test the provided keys tracking + src.AddArg("arg", "default") + + // Create target + targetCmd := tree.AddSub("valargconflicttarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "val arg conflict target") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark source + argv := ArgVals{"arg": ArgVal{Raw: "arg-val", Provided: true}} + status.MarkMetWithArgv(src, argv) + + // val2env key should be recorded + if !status.providedKeys["conflict.key"] { + t.Error("val2env key should be recorded as provided") + } +} + +func TestArgsAutoMapDeepNestedConflictResolution(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create 3 levels, each trying to provide the same key + level1Cmd := tree.AddSub("deepconflict1") + level1 := level1Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "deep conflict level 1") + level1.AddArg("arg1", "default1") + level1.AddArg2Env("deep.conflict", "arg1") + + level2Cmd := tree.AddSub("deepconflict2") + level2 := level2Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "deep conflict level 2") + level2.AddArg("arg2", "default2") + level2.AddArg2Env("deep.conflict", "arg2") + + level3Cmd := tree.AddSub("deepconflict3") + level3 := level3Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "deep conflict level 3") + level3.AddArg("arg3", "default3") + level3.AddArg2Env("deep.conflict", "arg3") + + // Create target + targetCmd := tree.AddSub("deepconflicttarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "deep conflict target") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark all levels with provided args + argv1 := ArgVals{"arg1": ArgVal{Raw: "val1", Provided: true}} + argv2 := ArgVals{"arg2": ArgVal{Raw: "val2", Provided: true}} + argv3 := ArgVals{"arg3": ArgVal{Raw: "val3", Provided: true}} + + status.MarkMetWithArgv(level1, argv1) + status.MarkMetWithArgv(level2, argv2) + status.MarkMetWithArgv(level3, argv3) + + // The key should be recorded once (first one wins) + if !status.providedKeys["deep.conflict"] { + t.Error("deep conflict key should be recorded as provided") + } + + // Count should be 1, not 3 + count := 0 + for key := range status.providedKeys { + if key == "deep.conflict" { + count++ + } + } + if count != 1 { + t.Errorf("deep.conflict key should be recorded once, got %d times", count) + } +} + +func TestArgsAutoMapConflictWithExistingTargetArg(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create source with arg + srcCmd := tree.AddSub("existingargsrc") + src := srcCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "existing arg source") + src.AddArg("srcarg", "default") + src.AddArg2Env("existing.key", "srcarg") + + // Create target with existing arg that maps to different key + targetCmd := tree.AddSub("existingargtarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "existing arg target") + target.AddArg("targetarg", "default") + target.AddArg2Env("existing.key", "targetarg") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark source + argv := ArgVals{"srcarg": ArgVal{Raw: "val", Provided: true}} + status.MarkMetWithArgv(src, argv) + + // The key should be recorded + if !status.providedKeys["existing.key"] { + t.Error("existing key should be recorded as provided") + } +} diff --git a/pkg/core/model/args_auto_map_integration_test.go b/pkg/core/model/args_auto_map_integration_test.go new file mode 100644 index 0000000..0ce63c1 --- /dev/null +++ b/pkg/core/model/args_auto_map_integration_test.go @@ -0,0 +1,118 @@ +package model + +import ( + "testing" +) + +func TestArgsAutoMapSmartMappingIntegration(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create target command with smart mapping + targetCmd := tree.AddSub("inttarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "integration target command") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + // Create source command with multiple env operations + srcCmd := tree.AddSub("intsource") + src := srcCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "integration source command") + + // Add val2env + src.AddVal2Env("val.key", "val-value") + + // Add arg2env + src.AddArg("arg1", "default1") + src.AddArg2Env("arg.key", "arg1") + + // Add env op + src.AddEnvOp("env.op.key", EnvOpTypeWrite) + + // Create argv with provided argument + argv := ArgVals{ + "arg1": ArgVal{Raw: "provided-value", Provided: true}, + } + + // Mark the source command with argv + status := target.GetArgsAutoMapStatus() + status.MarkMetWithArgv(src, argv) + + // Verify all provided keys are recorded + if !status.providedKeys["val.key"] { + t.Error("val2env key should be recorded as provided") + } + if !status.providedKeys["arg.key"] { + t.Error("arg2env key with provided arg should be recorded as provided") + } + if !status.providedKeys["env.op.key"] { + t.Error("env op key should be recorded as provided") + } + + // Test that non-provided arg2env is not recorded + src2Cmd := tree.AddSub("intsource2") + src2 := src2Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "integration source command 2") + + src2.AddArg("arg2", "default2") + src2.AddArg2Env("not.provided.arg.key", "arg2") + + argv2 := ArgVals{ + "arg2": ArgVal{Raw: "default2", Provided: false}, + } + + status.MarkMetWithArgv(src2, argv2) + + if status.providedKeys["not.provided.arg.key"] { + t.Error("arg2env key with non-provided arg should NOT be recorded as provided") + } +} + +func TestArgsAutoMapFlushCacheWithSmartMapping(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create target command with smart mapping + targetCmd := tree.AddSub("flushtarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "flush target command") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + // Create source command + srcCmd := tree.AddSub("flushsource") + src := srcCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "flush source command") + + src.AddArg("arg1", "default1") + + // Mark source as met and cache a mapping + status := target.GetArgsAutoMapStatus() + status.MarkAndCacheMapping(src, "test.key", "test.arg", "default-val", nil, true) + + // Record the key as provided + status.RecordGlobalEnvKey("test.key") + + // Flush cache + err = status.FlushCache(target) + if err != nil { + t.Fatalf("failed to flush cache: %v", err) + } + + // Verify the provided key was NOT mapped (smart mapping) + if target.GetArg2Env().Has("test.key") { + t.Error("provided key should not be mapped in smart mode") + } +} diff --git a/pkg/core/model/args_auto_map_nested_test.go b/pkg/core/model/args_auto_map_nested_test.go new file mode 100644 index 0000000..f456b31 --- /dev/null +++ b/pkg/core/model/args_auto_map_nested_test.go @@ -0,0 +1,329 @@ +package model + +import ( + "testing" +) + +func TestArgsAutoMapNestedFlowWithProvidedKeysAtDifferentLevels(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create level 3 command with val2env + level3Cmd := tree.AddSub("deeplevel") + level3 := level3Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "deep level command") + level3.AddVal2Env("deep.provided.key", "deep-value") + level3.AddArg("deeparg", "deep-default") + level3.AddArg2Env("deep.arg.key", "deeparg") + + // Create level 2 command with arg2env + level2Cmd := tree.AddSub("midlevel") + level2 := level2Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "mid level command") + level2.AddArg("midarg", "mid-default") + level2.AddArg2Env("mid.arg.key", "midarg") + + // Create level 1 command + level1Cmd := tree.AddSub("toplevel") + level1 := level1Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "top level command") + level1.AddArg("toparg", "top-default") + level1.AddArg2Env("top.arg.key", "toparg") + + // Create target command with smart mapping + targetCmd := tree.AddSub("nestedtarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "nested target command") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark all levels with different argv states + argvDeep := ArgVals{ + "deeparg": ArgVal{Raw: "provided-deep", Provided: true}, + } + argvMid := ArgVals{ + "midarg": ArgVal{Raw: "mid-default", Provided: false}, + } + argvTop := ArgVals{ + "toparg": ArgVal{Raw: "provided-top", Provided: true}, + } + + status.MarkMetWithArgv(level3, argvDeep) + status.MarkMetWithArgv(level2, argvMid) + status.MarkMetWithArgv(level1, argvTop) + + // Verify all provided keys are recorded + if !status.providedKeys["deep.provided.key"] { + t.Error("deep level val2env key should be recorded") + } + if !status.providedKeys["deep.arg.key"] { + t.Error("deep level arg2env key with provided arg should be recorded") + } + if !status.providedKeys["top.arg.key"] { + t.Error("top level arg2env key with provided arg should be recorded") + } + if status.providedKeys["mid.arg.key"] { + t.Error("mid level arg2env key with non-provided arg should NOT be recorded") + } +} + +func TestArgsAutoMapFiveLevelNestedFlow(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create 5 levels of nested commands + for i := 5; i >= 1; i-- { + cmdName := "nested5l" + string(rune('0'+i)) + cmdTree := tree.AddSub(cmdName) + + if i == 5 { + // Deepest level - power cmd with arg + cmd := cmdTree.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, cmdName+" command") + cmd.AddArg("deeparg", "deepval") + cmd.AddArg2Env("nested.deep.key", "deeparg") + } else { + // Upper levels - flow cmds + prevPath := "nested5l" + string(rune('0'+i+1)) + cmdTree.RegFlowCmd([]string{prevPath}, cmdName+" command", "") + } + } + + // Create target command + targetCmd := tree.AddSub("nested5target") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "5 level nested target") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + // Verify the structure is correct + status := target.GetArgsAutoMapStatus() + if status.mapNoProvider != true { + t.Error("smart mapping should be enabled") + } +} + +func TestArgsAutoMapNestedFlowWithGlobalEnvAtDifferentLevels(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create inner command + innerCmd := tree.AddSub("innercmd") + inner := innerCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "inner command") + inner.AddArg("innerarg", "inner-default") + inner.AddArg2Env("inner.key", "innerarg") + + // Create outer command + outerCmd := tree.AddSub("outercmd") + outer := outerCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "outer command") + outer.AddArg("outerarg", "outer-default") + outer.AddArg2Env("outer.key", "outerarg") + + // Create target command + targetCmd := tree.AddSub("nestedglobaltarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "nested global env target") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Record global env keys at different levels + status.RecordGlobalEnvKey("global.level1.key") + status.RecordGlobalEnvKey("global.level2.key") + + // Mark inner and outer with provided args + innerArgv := ArgVals{ + "innerarg": ArgVal{Raw: "inner-provided", Provided: true}, + } + outerArgv := ArgVals{ + "outerarg": ArgVal{Raw: "outer-provided", Provided: true}, + } + + status.MarkMetWithArgv(inner, innerArgv) + status.MarkMetWithArgv(outer, outerArgv) + + // Verify all keys are recorded + expectedKeys := []string{ + "global.level1.key", + "global.level2.key", + "inner.key", + "outer.key", + } + + for _, key := range expectedKeys { + if !status.providedKeys[key] { + t.Errorf("key %s should be recorded as provided", key) + } + } +} + +func TestArgsAutoMapNestedFlowWithEnvOpsAtDifferentLevels(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create commands at different levels with env ops + level1Cmd := tree.AddSub("envopsl1") + level1 := level1Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "env ops level 1") + level1.AddEnvOp("envop.l1.key1", EnvOpTypeWrite) + level1.AddEnvOp("envop.l1.key2", EnvOpTypeWrite) + + level2Cmd := tree.AddSub("envopsl2") + level2 := level2Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "env ops level 2") + level2.AddEnvOp("envop.l2.key1", EnvOpTypeWrite) + + level3Cmd := tree.AddSub("envopsl3") + level3 := level3Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "env ops level 3") + level3.AddEnvOp("envop.l3.key1", EnvOpTypeWrite) + + // Create target command + targetCmd := tree.AddSub("envopstarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "env ops target") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark all levels + status.MarkMet(level1) + status.MarkMet(level2) + status.MarkMet(level3) + + // Verify all env op keys are recorded + expectedKeys := []string{ + "envop.l1.key1", + "envop.l1.key2", + "envop.l2.key1", + "envop.l3.key1", + } + + for _, key := range expectedKeys { + if !status.providedKeys[key] { + t.Errorf("env op key %s should be recorded as provided", key) + } + } +} + +func TestArgsAutoMapDeeplyNestedWithMixedProvidedKeys(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create 4 levels with mixed provided key types + // Level 4: val2env + arg2env (provided) + level4Cmd := tree.AddSub("mixl4") + level4 := level4Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "mixed level 4") + level4.AddVal2Env("mix.val.key", "val") + level4.AddArg("arg4", "default4") + level4.AddArg2Env("mix.arg4.key", "arg4") + + // Level 3: env op + arg2env (not provided) + level3Cmd := tree.AddSub("mixl3") + level3 := level3Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "mixed level 3") + level3.AddEnvOp("mix.envop.key", EnvOpTypeWrite) + level3.AddArg("arg3", "default3") + level3.AddArg2Env("mix.arg3.key", "arg3") + + // Level 2: only arg2env (provided) + level2Cmd := tree.AddSub("mixl2") + level2 := level2Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "mixed level 2") + level2.AddArg("arg2", "default2") + level2.AddArg2Env("mix.arg2.key", "arg2") + + // Level 1: arg2env (not provided) + level1Cmd := tree.AddSub("mixl1") + level1 := level1Cmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "mixed level 1") + level1.AddArg("arg1", "default1") + level1.AddArg2Env("mix.arg1.key", "arg1") + + // Create target + targetCmd := tree.AddSub("mixedtarget") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "mixed target") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + status := target.GetArgsAutoMapStatus() + + // Mark levels with different argv states + argv4 := ArgVals{"arg4": ArgVal{Raw: "provided4", Provided: true}} + argv3 := ArgVals{"arg3": ArgVal{Raw: "default3", Provided: false}} + argv2 := ArgVals{"arg2": ArgVal{Raw: "provided2", Provided: true}} + argv1 := ArgVals{"arg1": ArgVal{Raw: "default1", Provided: false}} + + status.MarkMetWithArgv(level4, argv4) + status.MarkMetWithArgv(level3, argv3) + status.MarkMetWithArgv(level2, argv2) + status.MarkMetWithArgv(level1, argv1) + status.RecordGlobalEnvKey("mix.global.key") + + // Verify expected provided keys + shouldExist := []string{ + "mix.val.key", // val2env always provided + "mix.arg4.key", // arg2env with provided arg + "mix.envop.key", // env op always provided + "mix.arg2.key", // arg2env with provided arg + "mix.global.key", // global env + } + + shouldNotExist := []string{ + "mix.arg3.key", // arg2env with non-provided arg + "mix.arg1.key", // arg2env with non-provided arg + } + + for _, key := range shouldExist { + if !status.providedKeys[key] { + t.Errorf("key %s should be recorded as provided", key) + } + } + + for _, key := range shouldNotExist { + if status.providedKeys[key] { + t.Errorf("key %s should NOT be recorded as provided", key) + } + } +} diff --git a/pkg/core/model/args_auto_map_smart_test.go b/pkg/core/model/args_auto_map_smart_test.go new file mode 100644 index 0000000..48bd2ce --- /dev/null +++ b/pkg/core/model/args_auto_map_smart_test.go @@ -0,0 +1,134 @@ +package model + +import ( + "testing" +) + +func TestArgsAutoMapSmartMappingWithVal2Env(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create target command with smart mapping + targetCmd := tree.AddSub("target") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "target command") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + // Create source command with val2env + srcCmd := tree.AddSub("source") + src := srcCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "source command") + src.AddVal2Env("provided.key", "value") + + // Verify that the provided key is recorded + status := target.GetArgsAutoMapStatus() + status.MarkMet(src) + + if !status.providedKeys["provided.key"] { + t.Error("val2env key should be recorded as provided") + } +} + +func TestArgsAutoMapSmartMappingWithArg2EnvProvided(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create target command with smart mapping + targetCmd := tree.AddSub("target2") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "target command") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + // Create source command with arg2env + srcCmd := tree.AddSub("source2") + src := srcCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "source command") + src.AddArg("myarg", "default-value") + src.AddArg2Env("provided.arg.key", "myarg") + + // Create argv with provided argument + argv := ArgVals{ + "myarg": ArgVal{Raw: "provided-value", Provided: true}, + } + + // Verify that the provided key is recorded when arg is provided + status := target.GetArgsAutoMapStatus() + status.MarkMetWithArgv(src, argv) + + if !status.providedKeys["provided.arg.key"] { + t.Error("arg2env key should be recorded as provided when arg is provided") + } +} + +func TestArgsAutoMapSmartMappingWithArg2EnvNotProvided(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create target command with smart mapping + targetCmd := tree.AddSub("target3") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "target command") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + // Create source command with arg2env + srcCmd := tree.AddSub("source3") + src := srcCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "source command") + src.AddArg("myarg", "default-value") + src.AddArg2Env("not.provided.key", "myarg") + + // Create argv without provided argument + argv := ArgVals{ + "myarg": ArgVal{Raw: "default-value", Provided: false}, + } + + // Verify that the key is NOT recorded when arg is not provided + status := target.GetArgsAutoMapStatus() + status.MarkMetWithArgv(src, argv) + + if status.providedKeys["not.provided.key"] { + t.Error("arg2env key should NOT be recorded as provided when arg is not provided") + } +} + +func TestArgsAutoMapSmartMappingWithGlobalEnv(t *testing.T) { + strs := CmdTreeStrsForTest() + tree := NewCmdTree(strs) + + // Create target command with smart mapping + targetCmd := tree.AddSub("target4") + target := targetCmd.RegPowerCmd(func(argv ArgVals, cc *Cli, env *Env, flow *ParsedCmds, currCmdIdx int) (int, error) { + return currCmdIdx, nil + }, "target command") + + _, err := target.SetArg2EnvAutoMap([]string{"*"}) + if err != nil { + t.Fatalf("failed to set auto map: %v", err) + } + + // Verify that global env keys are recorded + status := target.GetArgsAutoMapStatus() + status.RecordGlobalEnvKey("global.env.key") + + if !status.providedKeys["global.env.key"] { + t.Error("global env key should be recorded as provided") + } +} diff --git a/pkg/core/model/executing_test.go b/pkg/core/model/executing_test.go index 6d46b3d..34ef890 100644 --- a/pkg/core/model/executing_test.go +++ b/pkg/core/model/executing_test.go @@ -63,7 +63,7 @@ func (fs *testFS) open(path string, content string) { if fs.files[path] == nil { fs.files[path] = newMockStatusFile() } - fs.files[path].Write([]byte(content)) + _, _ = fs.files[path].Write([]byte(content)) fs.writeLog = append(fs.writeLog, path) } @@ -726,7 +726,7 @@ func TestExecutingFlow_ErrorRecovery(t *testing.T) { func() { defer func() { - recover() + _ = recover() }() executing.OnCmdStart(flow, 2, env, "") executing.OnCmdFinish(flow, 2, env, true, nil, false) @@ -826,7 +826,7 @@ func TestStatusFile_PanicDuringWrite(t *testing.T) { for i := 0; i < len(flow.Cmds); i++ { func() { defer func() { - recover() + _ = recover() }() executing.OnCmdStart(flow, i, env, "") executing.OnCmdFinish(flow, i, env, true, nil, false) @@ -858,7 +858,7 @@ func (fs *panicTestFS) open(path string, content string) { if fs.files[path] == nil { fs.files[path] = newMockStatusFile() } - fs.files[path].Write([]byte(content)) + _, _ = fs.files[path].Write([]byte(content)) fs.writeLog = append(fs.writeLog, path) } @@ -1372,7 +1372,7 @@ func TestExecutingFlow_PanicInCmd_DeepestLevel(t *testing.T) { case error: panicErr = v case string: - panicErr = fmt.Errorf(v) + panicErr = fmt.Errorf("%s", v) default: panicErr = fmt.Errorf("panic: %v", r) } @@ -1478,7 +1478,7 @@ func TestExecutingFlow_PanicRecovery_StatusFileIntegrity(t *testing.T) { func() { defer func() { - recover() + _ = recover() }() executing.OnCmdStart(flow, 2, env, "") executing.OnCmdFinish(flow, 2, env, true, nil, false)