- 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>
265 lines
5.9 KiB
Go
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")
|
|
}
|
|
}
|