// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
const profileSource = `
= name ?>
if admin { ?>
Admin
} ?>
= \"?>\" ?>
" compiled, diagnostics := Compile("lexical.sando", []byte(source)) assertNoErrorDiagnostics(t, diagnostics) if !strings.Contains(string(compiled.Code), `WriteText(`) || !strings.Contains(string(compiled.Code), `"?>"`) { t.Fatalf("lexically protected delimiter was not preserved:\n%s", compiled.Code) } } func TestHeaderTrailingLineCommentDoesNotConsumeSyntheticBody(t *testing.T) { t.Parallel() source := ` remains inside the Go line comment ?>ok
` if _, diagnostics := Compile("commented.sando", []byte(source)); hasErrors(diagnostics) { t.Fatalf("trailing header comment failed: %v", diagnostics) } } func TestHeaderRequiresMarkerWhitespace(t *testing.T) { t.Parallel() _, diagnostics := Compile("bad.sando", []byte("")) assertDiagnosticCode(t, diagnostics, "HIM1105") } func TestGoReservedFunctionNamesAreRejected(t *testing.T) { t.Parallel() for _, test := range []struct { packageName string function string }{ {packageName: "views", function: "init"}, {packageName: "main", function: "main"}, } { source := "" _, diagnostics := Compile(test.function+".sando", []byte(source)) assertDiagnosticCode(t, diagnostics, "HIM1123") } } func TestCRLFAndGeneratedSyntaxDiagnostics(t *testing.T) { t.Parallel() crlf := strings.ReplaceAll(simpleSource("CRLF", "snow 雪"), "\n", "\r\n") if _, diagnostics := Compile("crlf.sando", []byte(crlf)); hasErrors(diagnostics) { t.Fatalf("CRLF source failed: %v", diagnostics) } invalid := ` if value { ?>ok definitely-not-go ?> } ?>` _, diagnostics := Compile("mapped.sando", []byte(invalid)) assertDiagnosticCode(t, diagnostics, "HIM1401") for _, item := range diagnostics { if item.Code == "HIM1401" && (item.Line < 5 || item.Column < 1) { t.Fatalf("generated syntax diagnostic was not mapped to source: %+v", item) } } } func TestLineDirectivePathCannotInjectGeneratedGo(t *testing.T) { t.Parallel() path := "bad\ngo-build-injected.sando" compiled, diagnostics := Compile(path, []byte(simpleSource("SafePath", "safe"))) assertNoErrorDiagnostics(t, diagnostics) generated := string(compiled.Code) if strings.Contains(generated, "//line go-build-injected") || !strings.Contains(generated, "%0A") { t.Fatalf("unsafe source path was not encoded in //line directive:\n%s", generated) } } func TestContextRules(t *testing.T) { t.Parallel() tests := []struct { name string body string code string }{ {name: "dynamic tag", body: `<= "div" ?>>ok`, code: "HIM1303"}, {name: "unquoted attribute", body: `>
`, code: "HIM1328"}, {name: "event attribute", body: ``, code: "HIM1340"}, {name: "component in attribute", body: ``, code: "HIM1302"}, {name: "component in textarea", body: ``, code: "HIM1302"}, {name: "unbalanced", body: `Hello = name ?>
`) first, err := Generate(context.Background(), []string{directory}) if err != nil { t.Fatalf("Generate: %v (%v)", err, first.Diagnostics) } if first.Changed != 1 || first.Discovered != 1 { t.Fatalf("unexpected first result: %+v", first) } outputPath := path + ".go" before, err := os.Stat(outputPath) if err != nil { t.Fatal(err) } second, err := Generate(context.Background(), []string{directory}) if err != nil { t.Fatal(err) } after, err := os.Stat(outputPath) if err != nil { t.Fatal(err) } if second.Unchanged != 1 || !before.ModTime().Equal(after.ModTime()) { t.Fatalf("unchanged generation changed output metadata: before=%v after=%v result=%+v", before.ModTime(), after.ModTime(), second) } checked, err := Check(context.Background(), []string{directory}) if err != nil || checked.Unchanged != 1 { t.Fatalf("fresh check failed: result=%+v err=%v", checked, err) } mustWrite(t, path, strings.ReplaceAll(string(mustRead(t, path)), "Hello", "Welcome")) stale, err := Check(context.Background(), []string{directory}) if err == nil || stale.Stale != 1 { t.Fatalf("stale check did not fail: result=%+v err=%v", stale, err) } assertDiagnosticCode(t, stale.Diagnostics, "HIM2204") } func TestCompileFailurePreservesEveryLastGoodOutput(t *testing.T) { t.Parallel() directory := resolvedTempDir(t) firstPath := filepath.Join(directory, "first.sando") secondPath := filepath.Join(directory, "second.sando") mustWrite(t, firstPath, simpleSource("First", "first")) mustWrite(t, secondPath, simpleSource("Second", "second")) if _, err := Generate(context.Background(), []string{directory}); err != nil { t.Fatal(err) } firstLastGood := mustRead(t, firstPath+".go") secondLastGood := mustRead(t, secondPath+".go") mustWrite(t, firstPath, simpleSource("First", "changed")) mustWrite(t, secondPath, "") if result, err := Generate(context.Background(), []string{directory}); err == nil { t.Fatalf("invalid batch unexpectedly generated: %+v", result) } if !bytes.Equal(firstLastGood, mustRead(t, firstPath+".go")) || !bytes.Equal(secondLastGood, mustRead(t, secondPath+".go")) { t.Fatal("a last-good output changed after batch compilation failed") } } func TestGenerateRefusesUnownedOutput(t *testing.T) { t.Parallel() directory := resolvedTempDir(t) path := filepath.Join(directory, "page.sando") mustWrite(t, path, simpleSource("Page", "page")) mustWrite(t, path+".go", "package demo\n") result, err := Generate(context.Background(), []string{path}) if err == nil { t.Fatalf("Generate overwrote an unowned output: %+v", result) } assertDiagnosticCode(t, result.Diagnostics, "HIM2104") if got := string(mustRead(t, path+".go")); got != "package demo\n" { t.Fatalf("unowned output changed to %q", got) } } func TestDiscoveryBoundariesAndExplicitNestedFile(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("symlink creation commonly requires additional Windows privileges") } t.Parallel() directory := resolvedTempDir(t) mustWrite(t, filepath.Join(directory, "root.sando"), simpleSource("Root", "root")) mustWrite(t, filepath.Join(directory, ".git", "ignored.sando"), simpleSource("Git", "git")) mustWrite(t, filepath.Join(directory, "vendor", "ignored.sando"), simpleSource("Vendor", "vendor")) mustWrite(t, filepath.Join(directory, "other-repository", ".git"), "gitdir: elsewhere\n") mustWrite(t, filepath.Join(directory, "other-repository", "ignored.sando"), simpleSource("OtherRepository", "other")) nested := filepath.Join(directory, "nested") mustWrite(t, filepath.Join(nested, "go.mod"), "module nested.test\n") nestedSource := filepath.Join(nested, "nested.sando") mustWrite(t, nestedSource, simpleSource("Nested", "nested")) discovered, diagnostics := discover(context.Background(), []string{directory}) assertNoErrorDiagnostics(t, diagnostics) if len(discovered) != 1 || filepath.Base(discovered[0]) != "root.sando" { t.Fatalf("unexpected discovery result: %v", discovered) } explicit, diagnostics := discover(context.Background(), []string{nestedSource}) assertNoErrorDiagnostics(t, diagnostics) if len(explicit) != 1 || explicit[0] != nestedSource { t.Fatalf("explicit nested source was not accepted: %v", explicit) } symlink := filepath.Join(directory, "linked") if err := os.Symlink(nested, symlink); err != nil { t.Fatal(err) } _, diagnostics = discover(context.Background(), []string{filepath.Join(symlink, "nested.sando")}) assertDiagnosticCode(t, diagnostics, "HIM2003") } func TestCheckRejectsSymlinkOutput(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("symlink creation commonly requires additional Windows privileges") } t.Parallel() directory := resolvedTempDir(t) path := filepath.Join(directory, "page.sando") mustWrite(t, path, simpleSource("Page", "page")) target := filepath.Join(directory, "handwritten.go") mustWrite(t, target, "package demo\n") if err := os.Symlink(target, path+".go"); err != nil { t.Fatal(err) } result, err := Check(context.Background(), []string{path}) if err == nil { t.Fatalf("symlink output unexpectedly passed check: %+v", result) } assertDiagnosticCode(t, result.Diagnostics, "HIM2205") } func TestGenerateReportsReadOnlyDirectory(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("POSIX directory mode test") } t.Parallel() directory := resolvedTempDir(t) path := filepath.Join(directory, "page.sando") mustWrite(t, path, simpleSource("Page", "page")) if err := os.Chmod(directory, 0o555); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chmod(directory, 0o755) }) result, err := Generate(context.Background(), []string{path}) if err == nil { t.Fatalf("read-only directory unexpectedly generated: %+v", result) } assertDiagnosticCode(t, result.Diagnostics, "HIM2110") } func TestNestedNonRegularGoModIsBoundary(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("symlink creation commonly requires additional Windows privileges") } t.Parallel() directory := resolvedTempDir(t) target := filepath.Join(directory, "actual.mod") mustWrite(t, target, "module nested.test\n") nested := filepath.Join(directory, "nested") mustWrite(t, filepath.Join(nested, "hidden.sando"), simpleSource("Hidden", "hidden")) if err := os.Symlink(target, filepath.Join(nested, "go.mod")); err != nil { t.Fatal(err) } discovered, diagnostics := discover(context.Background(), []string{directory}) if len(discovered) != 0 { t.Fatalf("traversed nested module with symlink go.mod: %v", discovered) } assertDiagnosticCode(t, diagnostics, "HIM2008") } func TestModuleRelativeLineMappings(t *testing.T) { t.Parallel() directory := resolvedTempDir(t) mustWrite(t, filepath.Join(directory, "go.mod"), "module example.test/app\n") path := filepath.Join(directory, "views", "card.sando") mustWrite(t, path, simpleSource("Card", "card")) if _, err := Generate(context.Background(), []string{directory}); err != nil { t.Fatal(err) } generated := string(mustRead(t, path+".go")) if !strings.Contains(generated, "//line views/card.sando:") { t.Fatalf("line mapping was not module-relative:\n%s", generated) } if strings.Contains(generated, filepath.ToSlash(directory)) { t.Fatal("generated output contains an absolute build-machine path") } } func TestStaticComponentCycle(t *testing.T) { t.Parallel() directory := resolvedTempDir(t) mustWrite(t, filepath.Join(directory, "a.sando"), ` ~ B() ?>`) mustWrite(t, filepath.Join(directory, "b.sando"), ` ~ A() ?>`) result, err := Generate(context.Background(), []string{directory}) if err == nil { t.Fatalf("component cycle unexpectedly generated: %+v", result) } assertDiagnosticCode(t, result.Diagnostics, "HIM1501") if _, statErr := os.Stat(filepath.Join(directory, "a.sando.go")); !errors.Is(statErr, os.ErrNotExist) { t.Fatalf("cycle wrote output: %v", statErr) } } func TestDuplicateComponentNamesFailBeforeWrites(t *testing.T) { t.Parallel() directory := resolvedTempDir(t) mustWrite(t, filepath.Join(directory, "one.sando"), simpleSource("Duplicate", "one")) mustWrite(t, filepath.Join(directory, "two.sando"), simpleSource("Duplicate", "two")) result, err := Generate(context.Background(), []string{directory}) if err == nil { t.Fatalf("duplicate component names unexpectedly generated: %+v", result) } assertDiagnosticCode(t, result.Diagnostics, "HIM1500") } func simpleSource(component, text string) string { return "\n" + text + "
\n" } func mustWrite(t *testing.T, path, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } } func resolvedTempDir(t *testing.T) string { t.Helper() directory := t.TempDir() resolved, err := filepath.EvalSymlinks(directory) if err != nil { t.Fatal(err) } return resolved } func mustRead(t *testing.T, path string) []byte { t.Helper() content, err := os.ReadFile(path) if err != nil { t.Fatal(err) } return content } func assertNoErrorDiagnostics(t *testing.T, diagnostics []Diagnostic) { t.Helper() if hasErrors(diagnostics) { t.Fatalf("unexpected diagnostics: %v", diagnostics) } } func assertDiagnosticCode(t *testing.T, diagnostics []Diagnostic, code string) { t.Helper() for _, diagnostic := range diagnostics { if diagnostic.Code == code { return } } t.Fatalf("diagnostic %s not found in %v", code, diagnostics) }