Files
sandwich-hime/internal/compiler/compiler_test.go
T
gamertan b7a84054d7 release: prepare Sandwich Hime v1 beta
Publish the sanitized Beta 1 source candidate with version-stable generated provenance, classroom/evaluation support boundaries, provisional macOS support, signed-source release policy, and an exact candidate preflight.

Material implementation, drafting, and review were assisted by OpenAI Codex. Cole Speelman reviewed the public snapshot and accepts human responsibility for the contribution.

Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-12 14:37:53 -04:00

648 lines
23 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
const profileSource = `<?sando go
package views
func Profile(name string, admin bool)
?>
<section class="profile" data-name="<?= name ?>">
<h1><?= name ?></h1>
<? if admin { ?>
<strong>Admin</strong>
<? } ?>
</section>
`
func TestCompileDeterministicContextAnnotatedBackend(t *testing.T) {
t.Parallel()
first, diagnostics := Compile("views/profile.sando", []byte(profileSource))
assertNoErrorDiagnostics(t, diagnostics)
second, secondDiagnostics := Compile("views/profile.sando", []byte(profileSource))
assertNoErrorDiagnostics(t, secondDiagnostics)
if !bytes.Equal(first.Code, second.Code) {
t.Fatal("repeated compilation was not deterministic")
}
generated := string(first.Code)
for _, expected := range []string{
generatedPrefix,
"// himesan:compiler ",
"// himesan:runtime-abi sando.v1",
"// himesan:source-sha256 ",
"func Profile(name string, admin bool)",
".WriteAttr(",
".WriteText(",
".WriteString(",
"ComponentFunc(func(",
"//line profile.sando:",
} {
if !strings.Contains(generated, expected) {
t.Fatalf("generated code does not contain %q:\n%s", expected, generated)
}
}
if strings.Contains(generated, "AGPL") || strings.Contains(generated, "SPDX-License-Identifier") {
t.Fatal("generated application code inherited the compiler license header")
}
}
func TestCommittedGoldenOutput(t *testing.T) {
t.Parallel()
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
sourcePath := filepath.Join(filepath.Dir(thisFile), "testdata", "golden", "basic.sando")
wantPath := sourcePath + ".go"
compiled, diagnostics := compileWithMapping(sourcePath, mustRead(t, sourcePath), "internal/compiler/testdata/golden/basic.sando")
assertNoErrorDiagnostics(t, diagnostics)
if want := mustRead(t, wantPath); !generatedCodeEqual(compiled.Code, want) {
t.Fatalf("committed golden output is stale; run himesan generate\n--- got ---\n%s\n--- want ---\n%s", compiled.Code, want)
}
}
func TestGeneratedCodeEqualityIgnoresOnlyCompilerProvenance(t *testing.T) {
t.Parallel()
compiled, diagnostics := Compile("hello.sando", []byte(simpleSource("Hello", "hello")))
assertNoErrorDiagnostics(t, diagnostics)
beta := replaceGeneratedCompilerVersion(t, compiled.Code, "v1.0.0-beta.1")
if !generatedCodeEqual(compiled.Code, beta) {
t.Fatal("compiler provenance alone made generated output unequal")
}
changes := map[string][]byte{
"runtime ABI": bytes.Replace(beta, []byte("himesan:runtime-abi sando.v1"), []byte("himesan:runtime-abi sando.v2"), 1),
"source digest": bytes.Replace(beta, []byte("himesan:source-sha256"), []byte("himesan:source-sha257"), 1),
"generated semantics": bytes.Replace(beta, []byte(".WriteString("), []byte(".WriteText("), 1),
"missing provenance marker": bytes.Replace(beta, []byte("// himesan:compiler "), []byte("// compiler: "), 1),
"empty provenance": replaceGeneratedCompilerVersion(t, beta, ""),
}
for name, changed := range changes {
name, changed := name, changed
t.Run(name, func(t *testing.T) {
t.Parallel()
if bytes.Equal(beta, changed) {
t.Fatalf("test mutation %q did not change generated output", name)
}
if generatedCodeEqual(compiled.Code, changed) {
t.Fatalf("generatedCodeEqual ignored %s change", name)
}
})
}
}
func TestHeaderAllowsBOMWhitespaceAndGoLexicalDelimiters(t *testing.T) {
t.Parallel()
source := "\xef\xbb\xbf \r\n\t<?sando go\npackage views\nfunc Lexical(v struct { Tag string `json:\"?>\"` })\n?>\n<p><?= \"?>\" ?></p>"
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 := `<?sando go
package p
func Commented() // ?> remains inside the Go line comment
?>
<p>ok</p>`
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("<?sandogo\npackage p\nfunc Bad()\n?>"))
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 := "<?sando go\npackage " + test.packageName + "\nfunc " + test.function + "()\n?>"
_, 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 := `<?sando go
package p
func Invalid(value bool)
?>
<? 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</div>`, code: "HIM1303"},
{name: "unquoted attribute", body: `<p title=<?= "x" ?>></p>`, code: "HIM1328"},
{name: "event attribute", body: `<p onclick="fixed"></p>`, code: "HIM1340"},
{name: "component in attribute", body: `<p title="<?~ Child() ?>"></p>`, code: "HIM1302"},
{name: "component in textarea", body: `<textarea><?~ Child() ?></textarea>`, code: "HIM1302"},
{name: "unbalanced", body: `<div><span></div>`, code: "HIM1352"},
{name: "unfinished", body: `<div>`, code: "HIM1311"},
{name: "self closing nonvoid", body: `<div/>`, code: "HIM1354"},
{name: "foreign SVG", body: `<svg></svg>`, code: "HIM1355"},
{name: "ambiguous noscript", body: `<noscript>fallback</noscript>`, code: "HIM1356"},
{name: "script escaped state", body: "<script><!--<script></script>\n<?= css ?>\n<!--\n</script>\n-->", code: "HIM1357"},
{name: "script escaped state split by template comment", body: `<script><!<?# emits nothing ?>--alert(1)</script>`, code: "HIM1357"},
{name: "dynamic iframe raw text", body: `<iframe><?= css ?></iframe>`, code: "HIM1303"},
{name: "dynamic style attribute", body: `<p style="<?= css ?>"></p>`, code: "HIM1343"},
{name: "dynamic srcdoc attribute", body: `<iframe srcdoc="<?= css ?>"></iframe>`, code: "HIM1345"},
{name: "dynamic srcset attribute", body: `<img srcset="<?= css ?>">`, code: "HIM1345"},
{name: "meta refresh", body: `<meta content="0;url=javascript:alert(1)" http-equiv="refresh">`, code: "HIM1346"},
{name: "dynamic meta http equiv", body: `<meta http-equiv="<?= css ?>" content="safe">`, code: "HIM1346"},
{name: "duplicate attribute", body: `<meta http-equiv="refresh" http-equiv="safe">`, code: "HIM1347"},
{name: "dangerous static URL", body: `<a href="javascript&#58;alert(1)">x</a>`, code: "HIM1344"},
{name: "ambiguous URL pieces", body: `<a href="<?= scheme ?>:payload">x</a>`, code: "HIM1341"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
source := "<?sando go\npackage p\nfunc Example(css, scheme string)\n?>\n" + test.body
_, diagnostics := Compile(test.name+".sando", []byte(source))
assertDiagnosticCode(t, diagnostics, test.code)
})
}
}
func TestHTMLCommentSyntaxInOtherRawTextStatesRemainsSupported(t *testing.T) {
t.Parallel()
source := `<?sando go
package p
func RawText()
?>
<style><!-- .old-browser { display: none } --></style>
<textarea><!-- literal text --></textarea>
<iframe><!-- literal text --></iframe>`
_, diagnostics := Compile("raw-text.sando", []byte(source))
assertNoErrorDiagnostics(t, diagnostics)
}
func TestTextareaExpressionUsesTextEscaping(t *testing.T) {
t.Parallel()
source := `<?sando go
package p
func Field(value string)
?>
<textarea><?= value ?></textarea>`
compiled, diagnostics := Compile("field.sando", []byte(source))
assertNoErrorDiagnostics(t, diagnostics)
if !strings.Contains(string(compiled.Code), ".WriteRCDATA(") {
t.Fatalf("textarea expression did not use dedicated RCDATA escaping:\n%s", compiled.Code)
}
}
func TestSupportedURLAndRawTextContexts(t *testing.T) {
t.Parallel()
source := `<?sando go
package p
import "gamertan.com/sandwich-hime/sando"
func Safe(id string, js sando.TrustedJS, css sando.TrustedCSS)
?>
<a href="/item/<?= id ?>">relative</a>
<a href="<?= sando.TrustURL("https://example.test/") ?>">trusted</a>
<script><?= js ?></script>
<style><?= css ?></style>`
compiled, diagnostics := Compile("safe.sando", []byte(source))
assertNoErrorDiagnostics(t, diagnostics)
assertDiagnosticCode(t, diagnostics, "HIM1901")
assertDiagnosticCode(t, diagnostics, "HIM1902")
assertDiagnosticCode(t, diagnostics, "HIM1903")
generated := string(compiled.Code)
for _, helper := range []string{".WriteURL(", ".WriteJS(", ".WriteCSS("} {
if !strings.Contains(generated, helper) {
t.Fatalf("generated code does not contain %s:\n%s", helper, generated)
}
}
}
func TestTrustWarningsDoNotFailGenerateOrCheck(t *testing.T) {
t.Parallel()
directory := resolvedTempDir(t)
path := filepath.Join(directory, "trusted.sando")
source := `<?sando go
package p
import "gamertan.com/sandwich-hime/sando"
func Trusted()
?>
<?= sando.TrustHTML("<b>reviewed</b>") ?>`
mustWrite(t, path, source)
generated, err := Generate(context.Background(), []string{path})
if err != nil {
t.Fatalf("warning unexpectedly failed generation: %v", err)
}
assertDiagnosticCode(t, generated.Diagnostics, "HIM1901")
checked, err := Check(context.Background(), []string{path})
if err != nil {
t.Fatalf("warning unexpectedly failed check: %v", err)
}
assertDiagnosticCode(t, checked.Diagnostics, "HIM1901")
}
func TestGenerateCheckAndUnchangedTimestamp(t *testing.T) {
t.Parallel()
directory := resolvedTempDir(t)
path := filepath.Join(directory, "hello.sando")
mustWrite(t, path, `<?sando go
package demo
func Hello(name string)
?>
<p>Hello <?= name ?></p>`)
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, "<?sando go\npackage demo\nfunc Second(\n?>")
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 TestDirectoryOperationsRejectOrphanedOwnedOutputBeforeWrites(t *testing.T) {
t.Parallel()
directory := resolvedTempDir(t)
orphanSource := filepath.Join(directory, "orphan.sando")
mustWrite(t, orphanSource, simpleSource("Orphan", "last good"))
if _, err := Generate(context.Background(), []string{directory}); err != nil {
t.Fatal(err)
}
orphanOutput := orphanSource + ".go"
lastGood := mustRead(t, orphanOutput)
if err := os.Remove(orphanSource); err != nil {
t.Fatal(err)
}
liveSource := filepath.Join(directory, "live.sando")
mustWrite(t, liveSource, simpleSource("Live", "must not be written"))
// A handwritten file whose name merely resembles an output is not owned by
// Hime-san and must not be treated as an orphan.
mustWrite(t, filepath.Join(directory, "handwritten.sando.go"), "package demo\n")
checked, err := Check(context.Background(), []string{directory})
if err == nil {
t.Fatalf("orphaned owned output unexpectedly passed check: %+v", checked)
}
assertDiagnosticCode(t, checked.Diagnostics, "HIM2014")
generated, err := Generate(context.Background(), []string{directory})
if err == nil {
t.Fatalf("orphaned owned output unexpectedly allowed generation: %+v", generated)
}
assertDiagnosticCode(t, generated.Diagnostics, "HIM2014")
if !bytes.Equal(lastGood, mustRead(t, orphanOutput)) {
t.Fatal("orphaned last-good output changed")
}
if _, statErr := os.Stat(liveSource + ".go"); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("batch wrote a live output despite the orphan diagnostic: %v", statErr)
}
}
func TestNewGeneratedOutputInheritsRestrictiveSourceMode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("POSIX file mode test")
}
t.Parallel()
directory := resolvedTempDir(t)
path := filepath.Join(directory, "private.sando")
mustWrite(t, path, simpleSource("Private", "private"))
if err := os.Chmod(path, 0o600); err != nil {
t.Fatal(err)
}
if _, err := Generate(context.Background(), []string{path}); err != nil {
t.Fatal(err)
}
info, err := os.Stat(path + ".go")
if err != nil {
t.Fatal(err)
}
if got := info.Mode().Perm(); got != 0o600 {
t.Fatalf("generated output mode = %04o, want 0600", 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"), `<?sando go
package demo
func A()
?>
<?~ B() ?>`)
mustWrite(t, filepath.Join(directory, "b.sando"), `<?sando go
package demo
func B()
?>
<?~ 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 "<?sando go\npackage demo\nfunc " + component + "()\n?>\n<p>" + text + "</p>\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 replaceGeneratedCompilerVersion(t *testing.T, code []byte, compilerVersion string) []byte {
t.Helper()
prefix := []byte(generatedPrefix + "\n// himesan:compiler ")
if !bytes.HasPrefix(code, prefix) {
t.Fatal("generated output has no compiler provenance line")
}
remainder := code[len(prefix):]
lineEnd := bytes.IndexByte(remainder, '\n')
if lineEnd < 0 {
t.Fatal("generated compiler provenance line has no terminator")
}
replaced := make([]byte, 0, len(code)-lineEnd+len(compilerVersion))
replaced = append(replaced, prefix...)
replaced = append(replaced, compilerVersion...)
replaced = append(replaced, remainder[lineEnd:]...)
return replaced
}
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)
}