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