// 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 => // class 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") } }