Files
five/compiler/pp/pp_test.go
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline
- Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch
- RTL: 351 Harbour-compatible functions
- RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization
- Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec)
- HB_FUNC API: Full Harbour C API compatible Go bridge
- Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT
- Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST
- Macro Compiler: Runtime AST parsing and evaluation
- Debugger: TUI debugger with source display, breakpoints, stepping
- FRB: Native + Pcode dual mode runtime binary
- Tests: 13 packages ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:41:50 +09:00

265 lines
5.9 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
package pp
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestDefine(t *testing.T) {
p := New()
src := `#define VERSION "1.0"
? VERSION`
result, errs := p.Process("test.prg", src)
if len(errs) > 0 {
t.Fatal(errs)
}
if !strings.Contains(result, `"1.0"`) {
t.Errorf("define not substituted: %q", result)
}
}
func TestDefineFlag(t *testing.T) {
p := New()
src := `#define DEBUG
#ifdef DEBUG
? "Debug mode"
#else
? "Release mode"
#endif`
result, errs := p.Process("test.prg", src)
if len(errs) > 0 {
t.Fatal(errs)
}
if !strings.Contains(result, "Debug mode") {
t.Error("ifdef DEBUG should include Debug mode")
}
if strings.Contains(result, "Release mode") {
t.Error("should NOT include Release mode")
}
}
func TestIfndef(t *testing.T) {
p := New()
src := `#ifndef RELEASE
? "Not release"
#else
? "Release"
#endif`
result, _ := p.Process("test.prg", src)
if !strings.Contains(result, "Not release") {
t.Error("ifndef should include 'Not release'")
}
}
func TestNestedIfdef(t *testing.T) {
p := New()
p.Define("A", "")
src := `#ifdef A
? "A is defined"
#ifdef B
? "B is defined"
#else
? "B is not defined"
#endif
#endif`
result, _ := p.Process("test.prg", src)
if !strings.Contains(result, "A is defined") {
t.Error("A should be defined")
}
if !strings.Contains(result, "B is not defined") {
t.Error("B should not be defined")
}
if strings.Contains(result, "B is defined") {
t.Error("B should NOT appear as defined")
}
}
func TestUndef(t *testing.T) {
p := New()
src := `#define FOO "bar"
? FOO
#undef FOO
? FOO`
result, _ := p.Process("test.prg", src)
lines := strings.Split(result, "\n")
// First ? should have "bar", second should still have FOO (not substituted)
found := 0
for _, l := range lines {
l = strings.TrimSpace(l)
if strings.Contains(l, `"bar"`) {
found++
}
}
if found != 1 {
t.Errorf("expected FOO substituted once, found %d times", found)
}
}
func TestInclude(t *testing.T) {
dir := t.TempDir()
// Create header file
headerContent := `#define APP_NAME "Five Test"
#define APP_VERSION "1.0"`
os.WriteFile(filepath.Join(dir, "myapp.ch"), []byte(headerContent), 0644)
// Create main file
src := `#include "myapp.ch"
? APP_NAME
? APP_VERSION`
p := New()
p.AddIncludeDir(dir)
result, errs := p.Process(filepath.Join(dir, "main.prg"), src)
if len(errs) > 0 {
t.Fatal(errs)
}
if !strings.Contains(result, `"Five Test"`) {
t.Errorf("APP_NAME not substituted: %q", result)
}
if !strings.Contains(result, `"1.0"`) {
t.Error("APP_VERSION not substituted")
}
}
func TestIncludeNested(t *testing.T) {
dir := t.TempDir()
// base.ch includes sub.ch
os.WriteFile(filepath.Join(dir, "sub.ch"), []byte(`#define SUB_VAL 42`), 0644)
os.WriteFile(filepath.Join(dir, "base.ch"), []byte(`#include "sub.ch"
#define BASE_VAL 100`), 0644)
src := `#include "base.ch"
? SUB_VAL
? BASE_VAL`
p := New()
p.AddIncludeDir(dir)
result, _ := p.Process(filepath.Join(dir, "main.prg"), src)
if !strings.Contains(result, "42") {
t.Error("SUB_VAL from nested include should be 42")
}
if !strings.Contains(result, "100") {
t.Error("BASE_VAL should be 100")
}
}
func TestIncludeGuard(t *testing.T) {
dir := t.TempDir()
// Header with include guard
header := `#ifndef _MYHEADER_CH
#define _MYHEADER_CH
#define MY_CONST 999
#endif`
os.WriteFile(filepath.Join(dir, "myheader.ch"), []byte(header), 0644)
// Include twice — should work (guard prevents double processing)
src := `#include "myheader.ch"
#include "myheader.ch"
? MY_CONST`
p := New()
p.AddIncludeDir(dir)
result, _ := p.Process(filepath.Join(dir, "main.prg"), src)
if !strings.Contains(result, "999") {
t.Error("MY_CONST should be 999")
}
}
func TestHbclassChHandled(t *testing.T) {
dir := t.TempDir()
// Simulate hbclass.ch — #command CLASS maps to comments (Five handles natively)
hbclass := `#ifndef HB_CLASS_CH_
#define HB_CLASS_CH_
#command CLASS <name> => // class <name> handled natively
#endif`
os.WriteFile(filepath.Join(dir, "hbclass.ch"), []byte(hbclass), 0644)
src := `#include "hbclass.ch"
CLASS Person
FUNCTION Main()
? "OK"
RETURN NIL`
p := New()
p.AddIncludeDir(dir)
result, errs := p.Process(filepath.Join(dir, "main.prg"), src)
if len(errs) > 0 {
t.Fatal(errs)
}
// #command directives themselves should be removed
if strings.Contains(result, "#command") {
t.Error("preprocessor directives should be removed")
}
// CLASS Person should be expanded by #command rule
if !strings.Contains(result, "Person") {
t.Error("Person should appear in output")
}
// FUNCTION should still be there
if !strings.Contains(result, "FUNCTION Main") {
t.Error("FUNCTION Main should pass through")
}
}
func TestDefineInString(t *testing.T) {
p := New()
src := `#define FOO bar
? "FOO should not change"
? FOO`
result, _ := p.Process("test.prg", src)
if !strings.Contains(result, `"FOO should not change"`) {
t.Error("define should not replace inside strings")
}
// Outside string should be replaced
lines := strings.Split(result, "\n")
for _, l := range lines {
l = strings.TrimSpace(l)
if l == "? bar" {
return // found replacement outside string
}
}
t.Error("FOO should be replaced to bar outside strings")
}
func TestPragma(t *testing.T) {
p := New()
src := `#pragma compatibility(harbour)
? "test"`
result, _ := p.Process("test.prg", src)
if !strings.Contains(result, "// pragma") || !strings.Contains(result, "compatibility") {
t.Error("pragma should be converted to comment")
}
}
func TestMissingInclude(t *testing.T) {
p := New()
src := `#include "nonexistent.ch"
? "still works"`
result, _ := p.Process("test.prg", src)
// Missing include should not crash, just skip with comment
if !strings.Contains(result, "not found") {
t.Error("missing include should produce a comment")
}
if !strings.Contains(result, "still works") {
t.Error("code after missing include should continue")
}
}