541 lines
No EOL
15 KiB
Go
541 lines
No EOL
15 KiB
Go
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
|
|
} |