538 lines
19 KiB
Go
538 lines
19 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")
|
|
}
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", ".."))
|
|
sourcePath := filepath.Join(root, "examples", "eql-shaped", "views", "badge.sando")
|
|
wantPath := sourcePath + ".go"
|
|
compiled, diagnostics := compileWithMapping(sourcePath, mustRead(t, sourcePath), "views/badge.sando")
|
|
assertNoErrorDiagnostics(t, diagnostics)
|
|
if want := mustRead(t, wantPath); !bytes.Equal(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 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: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 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 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)
|
|
}
|