package main import ( "fmt" "path/filepath" "strings" "testing" ) // TestParseArgs tests the flexible argument parsing logic func TestParseArgs(t *testing.T) { tests := []struct { name string args []string wantInput string wantOutput string wantIsBuild bool wantKeepAsm bool wantNoCBM bool expectError bool }{ // Simple file argument (default build mode) { name: "simple file argument", args: []string{"program.c65"}, wantInput: "program.c65", wantOutput: "program.prg", wantIsBuild: true, }, // Short -i flag { name: "short -i flag", args: []string{"-i", "program.c65"}, wantInput: "program.c65", wantOutput: "program.prg", wantIsBuild: true, }, // Long -in flag (legacy) { name: "long -in flag", args: []string{"-in", "program.c65"}, wantInput: "program.c65", wantOutput: "program.prg", wantIsBuild: true, }, // With output .prg (build mode) { name: "with output .prg", args: []string{"program.c65", "-o", "game.prg"}, wantInput: "program.c65", wantOutput: "game.prg", wantIsBuild: true, }, // With output .asm (compile mode) { name: "with output .asm", args: []string{"program.c65", "-o", "output.asm"}, wantInput: "program.c65", wantOutput: "output.asm", wantIsBuild: false, }, // With -out flag (legacy) { name: "with -out flag", args: []string{"program.c65", "-out", "output.asm"}, wantInput: "program.c65", wantOutput: "output.asm", wantIsBuild: false, }, // With keep-asm flag { name: "with keep-asm flag", args: []string{"program.c65", "--keep-asm"}, wantInput: "program.c65", wantOutput: "program.prg", wantIsBuild: true, wantKeepAsm: true, }, // With no-cbm flag { name: "with no-cbm flag", args: []string{"program.c65", "--no-cbm"}, wantInput: "program.c65", wantOutput: "program.prg", wantIsBuild: true, wantNoCBM: true, }, // Mixed flags { name: "mixed flags", args: []string{"-i", "program.c65", "-o", "out.prg", "--keep-asm", "--no-cbm"}, wantInput: "program.c65", wantOutput: "out.prg", wantIsBuild: true, wantKeepAsm: true, wantNoCBM: true, }, // No input file (error) { name: "no input file", args: []string{"-o", "output.prg"}, expectError: true, }, // Empty args (error) { name: "empty args", args: []string{}, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { input, output, isBuild, keepAsm, noCBM, err := parseArgs(tt.args) if tt.expectError { if err == nil { t.Errorf("parseArgs() expected error, got nil") } return } if err != nil { t.Errorf("parseArgs() unexpected error: %v", err) return } if input != tt.wantInput { t.Errorf("parseArgs() input = %v, want %v", input, tt.wantInput) } if output != tt.wantOutput { t.Errorf("parseArgs() output = %v, want %v", output, tt.wantOutput) } if isBuild != tt.wantIsBuild { t.Errorf("parseArgs() isBuild = %v, want %v", isBuild, tt.wantIsBuild) } if keepAsm != tt.wantKeepAsm { t.Errorf("parseArgs() keepAsm = %v, want %v", keepAsm, tt.wantKeepAsm) } if noCBM != tt.wantNoCBM { t.Errorf("parseArgs() noCBM = %v, want %v", noCBM, tt.wantNoCBM) } }) } } // TestOutputExtensionDetection tests detection of build vs compile mode by output extension func TestOutputExtensionDetection(t *testing.T) { tests := []struct { name string outputFile string wantIsBuild bool }{ {".prg extension", "program.prg", true}, {".PRG uppercase", "program.PRG", true}, {".asm extension", "program.asm", false}, {".ASM uppercase", "program.ASM", false}, {".s extension", "program.s", false}, {"no extension", "program", false}, // No .prg extension = compile mode {".bin extension", "program.bin", false}, // No .prg extension = compile mode } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { isBuild := strings.HasSuffix(strings.ToLower(tt.outputFile), ".prg") if isBuild != tt.wantIsBuild { t.Errorf("isBuild(%q) = %v, want %v", tt.outputFile, isBuild, tt.wantIsBuild) } }) } } // TestDefaultOutputNaming tests default output filename generation func TestDefaultOutputNaming(t *testing.T) { tests := []struct { name string inputFile string isBuild bool wantOutput string }{ {"build mode .c65", "program.c65", true, "program.prg"}, {"compile mode .c65", "program.c65", false, "program.asm"}, {"build mode .C65 uppercase", "program.C65", true, "program.prg"}, {"no extension", "program", true, "program.prg"}, {"multiple dots", "my.program.c65", true, "my.program.prg"}, {"path with extension", "/path/to/program.c65", true, "program.prg"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { base := strings.TrimSuffix(filepath.Base(tt.inputFile), filepath.Ext(tt.inputFile)) var output string if tt.isBuild { output = base + ".prg" } else { output = base + ".asm" } if output != tt.wantOutput { t.Errorf("defaultOutput(%q, %v) = %v, want %v", tt.inputFile, tt.isBuild, output, tt.wantOutput) } }) } } // TestSubcommandParsing tests explicit subcommand parsing func TestSubcommandParsing(t *testing.T) { tests := []struct { name string args []string wantCommand string expectError bool }{ {"build command", []string{"build", "-i", "program.c65"}, "build", false}, {"compile command", []string{"compile", "-i", "program.c65"}, "compile", false}, {"help command", []string{"help"}, "help", false}, {"unknown command", []string{"unknown", "program.c65"}, "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if len(tt.args) == 0 { t.Skip("empty args") } cmd := tt.args[0] isSubcommand := cmd == "build" || cmd == "compile" || cmd == "help" if tt.expectError && isSubcommand { t.Errorf("Expected error for command %q but it's a valid subcommand", cmd) } if !tt.expectError && cmd != tt.wantCommand { t.Errorf("Command = %q, want %q", cmd, tt.wantCommand) } }) } } // TestACMECommandBuilding tests building the ACME command line arguments func TestACMECommandBuilding(t *testing.T) { tests := []struct { name string output string asmFile string noCBM bool wantArgs []string }{ { name: "default with cbm", output: "program.prg", asmFile: "temp.asm", noCBM: false, wantArgs: []string{"-o", "program.prg", "-f", "cbm", "temp.asm"}, }, { name: "with no-cbm flag", output: "program.prg", asmFile: "temp.asm", noCBM: true, wantArgs: []string{"-o", "program.prg", "temp.asm"}, }, { name: "different output name", output: "game.prg", asmFile: "temp.asm", noCBM: false, wantArgs: []string{"-o", "game.prg", "-f", "cbm", "temp.asm"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { args := buildACMEArgs(tt.output, tt.asmFile, tt.noCBM) if len(args) != len(tt.wantArgs) { t.Errorf("buildACMEArgs() length = %v, want %v", len(args), len(tt.wantArgs)) return } for i := range args { if args[i] != tt.wantArgs[i] { t.Errorf("buildACMEArgs()[%d] = %v, want %v", i, args[i], tt.wantArgs[i]) } } }) } } // TestACMEAvailableError tests the ACME availability error message func TestACMEAvailableError(t *testing.T) { // We can't easily test the actual exec.LookPath call without mocking, // but we can test that the function returns an error with helpful message // when ACME is not found. // This is a simple test to ensure the function signature and basic logic err := checkACMEAvailable() // We can't assert much here without mocking, but we can verify the function exists if err != nil && !strings.Contains(err.Error(), "ACME") { t.Errorf("checkACMEAvailable() error should mention ACME, got: %v", err) } } // TestBackwardCompatibility tests legacy flag support func TestBackwardCompatibility(t *testing.T) { tests := []struct { name string args []string wantInput string wantOutput string description string }{ { name: "legacy -in -out flags", args: []string{"-in", "program.c65", "-out", "output.asm"}, wantInput: "program.c65", wantOutput: "output.asm", description: "Traditional c65gm syntax", }, { name: "legacy -in only", args: []string{"-in", "program.c65"}, wantInput: "program.c65", wantOutput: "program.asm", description: "Legacy mode with default output", }, { name: "mixed legacy and new", args: []string{"-in", "program.c65", "-o", "game.prg"}, wantInput: "program.c65", wantOutput: "game.prg", description: "Mixing -in with -o", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { input, output, _, _, _, err := parseArgs(tt.args) if err != nil && !strings.Contains(err.Error(), "subcommand") { // parseArgs treats legacy flags as regular flags, which is correct if input != tt.wantInput { t.Errorf("parseArgs() input = %v, want %v", input, tt.wantInput) } if output != tt.wantOutput { t.Errorf("parseArgs() output = %v, want %v", output, tt.wantOutput) } } }) } } // TestEdgeCases tests various edge cases in argument parsing func TestEdgeCases(t *testing.T) { tests := []struct { name string args []string expectError bool description string }{ { name: "i flag without value", args: []string{"-i"}, expectError: true, description: "-i flag without filename should error", }, { name: "in flag without value", args: []string{"-in"}, expectError: true, description: "-in flag without filename should error", }, { name: "out flag without value", args: []string{"program.c65", "-o"}, expectError: true, description: "-o flag without filename should error", }, { name: "multiple input files", args: []string{"file1.c65", "file2.c65", "-o", "output.prg"}, expectError: false, description: "First non-flag is input, second is ignored", }, { name: "unknown flag", args: []string{"program.c65", "--unknown-flag"}, expectError: false, description: "Unknown flags are ignored (treated as positional)", }, { name: "help flag with other args", args: []string{"-h", "program.c65"}, expectError: true, description: "-h flag should trigger help", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, _, _, _, _, err := parseArgs(tt.args) if tt.expectError && err == nil { t.Errorf("parseArgs() expected error for %s", tt.description) } else if !tt.expectError && err != nil && !strings.Contains(err.Error(), "subcommand") { t.Errorf("parseArgs() unexpected error for %s: %v", tt.description, err) } }) } } // TestFlagEquivalence tests that -i/-in and -o/-out are equivalent func TestFlagEquivalence(t *testing.T) { testCases := []struct { args1 []string args2 []string }{ { []string{"-i", "program.c65"}, []string{"-in", "program.c65"}, }, { []string{"program.c65", "-o", "output.prg"}, []string{"program.c65", "-out", "output.prg"}, }, { []string{"-i", "program.c65", "-o", "output.asm"}, []string{"-in", "program.c65", "-out", "output.asm"}, }, } for i, tc := range testCases { t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { input1, output1, isBuild1, keepAsm1, noCBM1, err1 := parseArgs(tc.args1) input2, output2, isBuild2, keepAsm2, noCBM2, err2 := parseArgs(tc.args2) // Both should succeed or both should fail if (err1 == nil) != (err2 == nil) { t.Errorf("parseArgs() inconsistent errors: args1=%v err=%v, args2=%v err=%v", tc.args1, err1, tc.args2, err2) return } if err1 != nil { return // Both failed, that's OK for this test } // Check equivalence if input1 != input2 { t.Errorf("input mismatch: %q != %q", input1, input2) } if output1 != output2 { t.Errorf("output mismatch: %q != %q", output1, output2) } if isBuild1 != isBuild2 { t.Errorf("isBuild mismatch: %v != %v", isBuild1, isBuild2) } if keepAsm1 != keepAsm2 { t.Errorf("keepAsm mismatch: %v != %v", keepAsm1, keepAsm2) } if noCBM1 != noCBM2 { t.Errorf("noCBM mismatch: %v != %v", noCBM1, noCBM2) } }) } } // buildACMEArgs is a test helper that extracts ACME argument building logic func buildACMEArgs(outputFile, asmFile string, noCBM bool) []string { args := []string{"-o", outputFile} if !noCBM { args = append(args, "-f", "cbm") } args = append(args, asmFile) return args } // parseArgs is a test helper that extracts the parsing logic from main() func parseArgs(args []string) (inputFile, outputFile string, isBuild, keepAsm, noCBM bool, err error) { if len(args) == 0 { return "", "", false, false, false, fmt.Errorf("no arguments") } // Check for help flags if args[0] == "-h" || args[0] == "--help" { return "", "", false, false, false, fmt.Errorf("help requested") } // Check if first arg is a subcommand if len(args) > 0 && (args[0] == "build" || args[0] == "compile" || args[0] == "help") { // For testing purposes, we'll handle subcommands differently // In real code, these are handled by runBuildCommand/runCompileCommand return "", "", false, false, false, fmt.Errorf("subcommand not handled in test") } // Default mode parsing (from main()) for i := 0; i < len(args); i++ { arg := args[i] if arg == "-i" || arg == "-in" { if i+1 < len(args) { inputFile = args[i+1] i++ // Skip next arg } else { return "", "", false, false, false, fmt.Errorf("-i flag requires a filename") } } else if arg == "-o" || arg == "-out" { if i+1 < len(args) { outputFile = args[i+1] i++ // Skip next arg } else { return "", "", false, false, false, fmt.Errorf("-o flag requires a filename") } } else if arg == "--keep-asm" { keepAsm = true } else if arg == "--no-cbm" { noCBM = true } else if !strings.HasPrefix(arg, "-") && inputFile == "" { // First non-flag argument is the input file inputFile = arg } } if inputFile == "" { return "", "", false, false, false, fmt.Errorf("no input file specified") } // Default output based on input if outputFile == "" { base := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile)) outputFile = base + ".prg" // Default to build mode (.prg) } // Determine mode by output extension isBuild = strings.HasSuffix(strings.ToLower(outputFile), ".prg") return inputFile, outputFile, isBuild, keepAsm, noCBM, nil }