c65gm/main_test.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
}