Added a filereader interface + FS and Mock implementations for easier testing of preprocessor.
This commit is contained in:
parent
f7f247b69c
commit
9578392f55
7 changed files with 348 additions and 0 deletions
95
internal/preproc/filereader.go
Normal file
95
internal/preproc/filereader.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package preproc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FileReader interface {
|
||||
ReadLines(includeSpec string, currentDir string) ([]string, error)
|
||||
}
|
||||
|
||||
type DiskFileReader struct {
|
||||
cache map[string][]string
|
||||
libPath string
|
||||
}
|
||||
|
||||
func NewDiskFileReader() *DiskFileReader {
|
||||
return &DiskFileReader{
|
||||
cache: make(map[string][]string),
|
||||
libPath: os.Getenv("C65LIBPATH"),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiskFileReader) ReadLines(includeSpec string, currentDir string) ([]string, error) {
|
||||
path, err := d.resolvePath(includeSpec, currentDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if lines, ok := d.cache[abs]; ok {
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
d.cache[abs] = lines
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func (d *DiskFileReader) resolvePath(spec string, curDir string) (string, error) {
|
||||
if len(spec) == 0 {
|
||||
return "", errors.New("empty include path")
|
||||
}
|
||||
|
||||
// <file> -> library include
|
||||
if strings.HasPrefix(spec, "<") && strings.HasSuffix(spec, ">") {
|
||||
base := strings.TrimSpace(spec[1 : len(spec)-1])
|
||||
if d.libPath == "" {
|
||||
return "", errors.New("C65LIBPATH not set for angle-bracket include")
|
||||
}
|
||||
path := filepath.Join(d.libPath, base)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return "", fmt.Errorf("library include not found: %s", path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// quoted or bare -> relative to current dir
|
||||
base := strings.Trim(spec, `"'`)
|
||||
path := filepath.Join(curDir, base)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return "", fmt.Errorf("include not found: %s", path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
type MockFileReader struct {
|
||||
files map[string][]string
|
||||
}
|
||||
|
||||
func NewMockFileReader(files map[string][]string) *MockFileReader {
|
||||
return &MockFileReader{
|
||||
files: files,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockFileReader) ReadLines(includeSpec string, _ string) ([]string, error) {
|
||||
lines, ok := m.files[includeSpec]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("open %s: no such file or directory", includeSpec)
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// library file
|
||||
LABEL appsub
|
||||
0
internal/preproc/filereader_mocks/app/empty.c65
Normal file
0
internal/preproc/filereader_mocks/app/empty.c65
Normal file
2
internal/preproc/filereader_mocks/app/test_app.c65
Normal file
2
internal/preproc/filereader_mocks/app/test_app.c65
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// library file
|
||||
LABEL app
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// library file
|
||||
LABEL libsub
|
||||
2
internal/preproc/filereader_mocks/lib/test_lib.c65
Normal file
2
internal/preproc/filereader_mocks/lib/test_lib.c65
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// library file
|
||||
LABEL lib
|
||||
245
internal/preproc/filereader_test.go
Normal file
245
internal/preproc/filereader_test.go
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package preproc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDiskFileReader_ReadLines_LibraryIncludes(t *testing.T) {
|
||||
origPath := os.Getenv("C65LIBPATH")
|
||||
defer os.Setenv("C65LIBPATH", origPath)
|
||||
|
||||
libPath, _ := filepath.Abs("filereader_mocks/lib")
|
||||
os.Setenv("C65LIBPATH", libPath)
|
||||
|
||||
// Now create reader
|
||||
reader := NewDiskFileReader()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
includeSpec string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple library include",
|
||||
includeSpec: "<test_lib.c65>",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nested library include",
|
||||
includeSpec: "<libsub/test_libsub.c65>",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing library file",
|
||||
includeSpec: "<notfound.c65>",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lines, err := reader.ReadLines(tt.includeSpec, "")
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ReadLines() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && len(lines) == 0 {
|
||||
t.Error("expected lines, got empty slice")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskFileReader_ReadLines_LibraryWithoutEnv(t *testing.T) {
|
||||
origPath := os.Getenv("C65LIBPATH")
|
||||
defer os.Setenv("C65LIBPATH", origPath)
|
||||
|
||||
os.Setenv("C65LIBPATH", "")
|
||||
|
||||
// Now create reader
|
||||
reader := NewDiskFileReader()
|
||||
|
||||
_, err := reader.ReadLines("<test_lib.c65>", "")
|
||||
if err == nil {
|
||||
t.Error("expected error when C65LIBPATH not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskFileReader_ReadLines_RelativeIncludes(t *testing.T) {
|
||||
reader := NewDiskFileReader()
|
||||
|
||||
appDir, _ := filepath.Abs("filereader_mocks/app")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
includeSpec string
|
||||
currentDir string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "bare include",
|
||||
includeSpec: "test_app.c65",
|
||||
currentDir: appDir,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "quoted include",
|
||||
includeSpec: `"test_app.c65"`,
|
||||
currentDir: appDir,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single quoted include",
|
||||
includeSpec: "'test_app.c65'",
|
||||
currentDir: appDir,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nested relative include",
|
||||
includeSpec: "appsub/test_appsub.c65",
|
||||
currentDir: appDir,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing file",
|
||||
includeSpec: "notfound.c65",
|
||||
currentDir: appDir,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lines, err := reader.ReadLines(tt.includeSpec, tt.currentDir)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ReadLines() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && len(lines) == 0 {
|
||||
t.Error("expected lines, got empty slice")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskFileReader_ReadLines_Cache(t *testing.T) {
|
||||
reader := NewDiskFileReader()
|
||||
|
||||
appDir, _ := filepath.Abs("filereader_mocks/app")
|
||||
|
||||
// First read
|
||||
lines1, err := reader.ReadLines("test_app.c65", appDir)
|
||||
if err != nil {
|
||||
t.Fatalf("first read failed: %v", err)
|
||||
}
|
||||
|
||||
// Second read (should hit cache)
|
||||
lines2, err := reader.ReadLines("test_app.c65", appDir)
|
||||
if err != nil {
|
||||
t.Fatalf("second read failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify same slice (from cache)
|
||||
if len(lines1) != len(lines2) {
|
||||
t.Errorf("cache returned different length: %d vs %d", len(lines1), len(lines2))
|
||||
}
|
||||
|
||||
// Verify cache was actually used (check map size)
|
||||
if len(reader.cache) != 1 {
|
||||
t.Errorf("expected 1 cache entry, got %d", len(reader.cache))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskFileReader_ReadLines_EmptyFile(t *testing.T) {
|
||||
reader := NewDiskFileReader()
|
||||
|
||||
appDir, _ := filepath.Abs("filereader_mocks/app")
|
||||
|
||||
lines, err := reader.ReadLines("empty.c65", appDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read empty file: %v", err)
|
||||
}
|
||||
|
||||
// Empty file still returns slice with one empty string
|
||||
if len(lines) != 1 {
|
||||
t.Errorf("empty file: expected 1 line, got %d", len(lines))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskFileReader_ReadLines_EmptySpec(t *testing.T) {
|
||||
reader := NewDiskFileReader()
|
||||
|
||||
_, err := reader.ReadLines("", "")
|
||||
if err == nil {
|
||||
t.Error("expected error for empty include spec")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockFileReader_ReadLines(t *testing.T) {
|
||||
files := map[string][]string{
|
||||
"test.c65": {"LDA #$00", "STA $D020"},
|
||||
"empty": {},
|
||||
}
|
||||
|
||||
reader := NewMockFileReader(files)
|
||||
|
||||
lines, err := reader.ReadLines("test.c65", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(lines) != 2 {
|
||||
t.Errorf("expected 2 lines, got %d", len(lines))
|
||||
}
|
||||
|
||||
_, err = reader.ReadLines("notfound", "")
|
||||
if err == nil {
|
||||
t.Error("expected error for missing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockFileReader_LibraryInclude(t *testing.T) {
|
||||
files := map[string][]string{
|
||||
"<test_lib.c65>": {"LDA #$00", "STA $D020"},
|
||||
}
|
||||
|
||||
reader := NewMockFileReader(files)
|
||||
|
||||
lines, err := reader.ReadLines("<test_lib.c65>", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(lines) != 2 {
|
||||
t.Errorf("expected 2 lines, got %d", len(lines))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockFileReader_NormalInclude(t *testing.T) {
|
||||
files := map[string][]string{
|
||||
"test_app.c65": {"JMP $1000"},
|
||||
}
|
||||
|
||||
reader := NewMockFileReader(files)
|
||||
|
||||
lines, err := reader.ReadLines("test_app.c65", "some/dir")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(lines) != 1 {
|
||||
t.Errorf("expected 1 line, got %d", len(lines))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockFileReader_MissingFile(t *testing.T) {
|
||||
files := map[string][]string{
|
||||
"exists.c65": {"NOP"},
|
||||
}
|
||||
|
||||
reader := NewMockFileReader(files)
|
||||
|
||||
_, err := reader.ReadLines("notfound.c65", "")
|
||||
if err == nil {
|
||||
t.Error("expected error for missing file")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue