add tests for custom functions

This commit is contained in:
2025-09-09 23:14:03 -06:00
parent a899fd6c4d
commit 6dfe71894d
2 changed files with 501 additions and 34 deletions

View File

@ -0,0 +1,455 @@
package interpreter
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestCustomJavaScriptFunctions(t *testing.T) {
// Create a temporary directory for test files
tempDir := t.TempDir()
// Create a test Masonry file
masonryContent := `entity User {
name: string required
email: string required unique
}
entity Post {
title: string required
slug: string required
authorId: uuid required
}`
masonryFile := filepath.Join(tempDir, "test.masonry")
err := os.WriteFile(masonryFile, []byte(masonryContent), 0644)
if err != nil {
t.Fatalf("Failed to write test Masonry file: %v", err)
}
// Create templates directory
templatesDir := filepath.Join(tempDir, "templates")
err = os.MkdirAll(templatesDir, 0755)
if err != nil {
t.Fatalf("Failed to create templates directory: %v", err)
}
// Create a test template that uses custom functions
templateContent := `package models
// {{formatComment "Generated model"}}
type {{.Entity.Name | title}} struct {
Name string
}
// Table name: {{pluralize (.Entity.Name | lower)}}
func ({{.Entity.Name | title}}) TableName() string {
return "{{pluralize (.Entity.Name | lower)}}"
}
// Custom function result: {{customUpper (.Entity.Name)}}`
templateFile := filepath.Join(templatesDir, "model.tmpl")
err = os.WriteFile(templateFile, []byte(templateContent), 0644)
if err != nil {
t.Fatalf("Failed to write test template file: %v", err)
}
// Create manifest with custom JavaScript functions
manifestContent := `name: "Test Generator"
description: "Test custom JavaScript functions"
functions:
# Format a comment block
formatComment: |
function main() {
var text = args[0] || "";
return "// " + text;
}
# Convert singular words to plural (simple rules)
pluralize: |
function main() {
var word = args[0] || "";
if (word.slice(-1) === 'y') {
return word.slice(0, -1) + 'ies';
} else if (word.slice(-1) === 's') {
return word + 'es';
} else {
return word + 's';
}
}
# Convert to uppercase
customUpper: |
function main() {
var text = args[0] || "";
return text.toUpperCase();
}
outputs:
- path: "models/{{.Entity.Name | lower}}.go"
template: "model"
iterator: "entities"
item_context: "Entity"`
manifestFile := filepath.Join(templatesDir, "manifest.yaml")
err = os.WriteFile(manifestFile, []byte(manifestContent), 0644)
if err != nil {
t.Fatalf("Failed to write test manifest file: %v", err)
}
// Create template interpreter and test
interpreter := NewTemplateInterpreter()
// Test InterpretToFiles with custom functions
outputs, err := interpreter.InterpretToFiles(masonryFile, templatesDir, "manifest.yaml")
if err != nil {
t.Fatalf("InterpretToFiles failed: %v", err)
}
// Should generate 2 files (User and Post)
expectedFiles := 2
if len(outputs) != expectedFiles {
t.Errorf("Expected %d output files, got %d", expectedFiles, len(outputs))
}
// Check User model output
userFile := "models/user.go"
userContent, exists := outputs[userFile]
if !exists {
t.Errorf("Expected output file %s not found", userFile)
} else {
// Test formatComment function
if !strings.Contains(userContent, "// Generated model") {
t.Errorf("formatComment function not working: expected '// Generated model' in output")
}
// Test pluralize function
if !strings.Contains(userContent, "Table name: users") {
t.Errorf("pluralize function not working: expected 'Table name: users' in output")
}
if !strings.Contains(userContent, `return "users"`) {
t.Errorf("pluralize function not working in template: expected 'return \"users\"' in output")
}
// Test customUpper function
if !strings.Contains(userContent, "Custom function result: USER") {
t.Errorf("customUpper function not working: expected 'Custom function result: USER' in output")
}
}
// Check Post model output
postFile := "models/post.go"
postContent, exists := outputs[postFile]
if !exists {
t.Errorf("Expected output file %s not found", postFile)
} else {
// Test pluralize function with 's' ending
if !strings.Contains(postContent, "Table name: posts") {
t.Errorf("pluralize function not working for 's' ending: expected 'Table name: posts' in output")
}
if !strings.Contains(postContent, `return "posts"`) {
t.Errorf("pluralize function not working in template for 's' ending: expected 'return \"posts\"' in output")
}
// Test customUpper function
if !strings.Contains(postContent, "Custom function result: POST") {
t.Errorf("customUpper function not working: expected 'Custom function result: POST' in output")
}
}
}
func TestCustomFunctionErrorHandling(t *testing.T) {
interpreter := NewTemplateInterpreter()
tests := []struct {
name string
functionName string
jsCode string
expectError bool
errorContains string
}{
{
name: "Invalid function name",
functionName: "123invalid",
jsCode: "function main() { return 'test'; }",
expectError: true,
errorContains: "invalid function name",
},
{
name: "Reserved function name",
functionName: "and",
jsCode: "function main() { return 'test'; }",
expectError: true,
errorContains: "invalid function name",
},
{
name: "Missing main function",
functionName: "testFunc",
jsCode: "function notMain() { return 'test'; }",
expectError: false, // Error will occur during execution, not loading
},
{
name: "Valid function",
functionName: "validFunc",
jsCode: "function main() { return 'test'; }",
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
functions := map[string]string{
tt.functionName: tt.jsCode,
}
err := interpreter.loadCustomFunctions(functions)
if tt.expectError {
if err == nil {
t.Errorf("Expected error but got none")
} else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
t.Errorf("Expected error to contain '%s', got: %v", tt.errorContains, err)
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
})
}
}
func TestJavaScriptFunctionExecution(t *testing.T) {
interpreter := NewTemplateInterpreter()
// Load a test function
functions := map[string]string{
"testConcat": `
function main() {
var result = "";
for (var i = 0; i < args.length; i++) {
result += args[i];
if (i < args.length - 1) result += "-";
}
return result;
}
`,
"testMath": `
function main() {
var a = args[0] || 0;
var b = args[1] || 0;
return a + b;
}
`,
"testError": `
function main() {
throw new Error("Test error");
}
`,
"testNoMain": `
function notMain() {
return "should not work";
}
`,
}
err := interpreter.loadCustomFunctions(functions)
if err != nil {
t.Fatalf("Failed to load custom functions: %v", err)
}
// Test function execution through template
testCases := []struct {
name string
template string
data interface{}
expectedOutput string
expectError bool
errorContains string
}{
{
name: "Concat function with multiple args",
template: `{{testConcat "hello" "world" "test"}}`,
data: struct{}{},
expectedOutput: "hello-world-test",
expectError: false,
},
{
name: "Math function",
template: `{{testMath 5 3}}`,
data: struct{}{},
expectedOutput: "8",
expectError: false,
},
{
name: "Function with error",
template: `{{testError}}`,
data: struct{}{},
expectError: true,
errorContains: "Test error",
},
{
name: "Function without main",
template: `{{testNoMain}}`,
data: struct{}{},
expectError: true,
errorContains: "must define a main() function",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := interpreter.Interpret("", tc.template)
if tc.expectError {
if err == nil {
t.Errorf("Expected error but got none")
} else if tc.errorContains != "" && !strings.Contains(err.Error(), tc.errorContains) {
t.Errorf("Expected error to contain '%s', got: %v", tc.errorContains, err)
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
} else if result != tc.expectedOutput {
t.Errorf("Expected output '%s', got '%s'", tc.expectedOutput, result)
}
}
})
}
}
func TestCustomFunctionArgumentHandling(t *testing.T) {
interpreter := NewTemplateInterpreter()
// Load test functions that handle different argument types
functions := map[string]string{
"argCount": `
function main() {
return args.length;
}
`,
"argTypes": `
function main() {
var types = [];
for (var i = 0; i < args.length; i++) {
types.push(typeof args[i]);
}
return types.join(",");
}
`,
"argAccess": `
function main() {
// Test both args array and individual arg variables
var fromArray = args[0] || "empty";
var fromVar = arg0 || "empty";
return fromArray + ":" + fromVar;
}
`,
}
err := interpreter.loadCustomFunctions(functions)
if err != nil {
t.Fatalf("Failed to load custom functions: %v", err)
}
testCases := []struct {
name string
template string
expectedOutput string
}{
{
name: "No arguments",
template: `{{argCount}}`,
expectedOutput: "0",
},
{
name: "Multiple arguments",
template: `{{argCount "a" "b" "c"}}`,
expectedOutput: "3",
},
{
name: "Argument types",
template: `{{argTypes "string" 42}}`,
expectedOutput: "string,number",
},
{
name: "Argument access methods",
template: `{{argAccess "test"}}`,
expectedOutput: "test:test",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := interpreter.Interpret("", tc.template)
if err != nil {
t.Errorf("Unexpected error: %v", err)
} else if result != tc.expectedOutput {
t.Errorf("Expected output '%s', got '%s'", tc.expectedOutput, result)
}
})
}
}
func TestFunctionLoadingOrder(t *testing.T) {
// This test ensures that custom functions are loaded before templates
// to prevent the "function not defined" error that was fixed
tempDir := t.TempDir()
templatesDir := filepath.Join(tempDir, "templates")
err := os.MkdirAll(templatesDir, 0755)
if err != nil {
t.Fatalf("Failed to create templates directory: %v", err)
}
// Create a template that uses a custom function
templateContent := `{{customFunc "test"}}`
templateFile := filepath.Join(templatesDir, "test.tmpl")
err = os.WriteFile(templateFile, []byte(templateContent), 0644)
if err != nil {
t.Fatalf("Failed to write template file: %v", err)
}
// Create manifest with the custom function
manifestContent := `name: "Order Test"
functions:
customFunc: |
function main() {
return "custom:" + args[0];
}
outputs:
- path: "test.txt"
template: "test"`
manifestFile := filepath.Join(templatesDir, "manifest.yaml")
err = os.WriteFile(manifestFile, []byte(manifestContent), 0644)
if err != nil {
t.Fatalf("Failed to write manifest file: %v", err)
}
// Create empty Masonry file
masonryFile := filepath.Join(tempDir, "empty.masonry")
err = os.WriteFile(masonryFile, []byte(""), 0644)
if err != nil {
t.Fatalf("Failed to write Masonry file: %v", err)
}
// Test that InterpretToFiles works (this would fail if functions aren't loaded first)
interpreter := NewTemplateInterpreter()
outputs, err := interpreter.InterpretToFiles(masonryFile, templatesDir, "manifest.yaml")
if err != nil {
t.Fatalf("InterpretToFiles failed: %v", err)
}
// Check that the custom function was executed
testContent, exists := outputs["test.txt"]
if !exists {
t.Errorf("Expected output file test.txt not found")
} else if testContent != "custom:test" {
t.Errorf("Expected 'custom:test', got '%s'", testContent)
}
}

View File

@ -185,12 +185,19 @@ func createTestAST() lang.AST {
Name: "HomePage",
Path: "/",
Layout: "public",
Components: []lang.Component{
{Type: "header"},
{Type: "footer"},
},
Sections: []lang.Section{
Elements: []lang.PageElement{
{
Component: &lang.Component{
Type: "header",
},
},
{
Component: &lang.Component{
Type: "footer",
},
},
{
Section: &lang.Section{
Name: "hero",
Type: strPtr("container"),
Elements: []lang.SectionElement{
@ -207,7 +214,9 @@ func createTestAST() lang.AST {
},
},
},
},
{
Section: &lang.Section{
Name: "content",
Type: strPtr("container"),
Elements: []lang.SectionElement{
@ -234,14 +243,16 @@ func createTestAST() lang.AST {
},
},
},
},
// AdminPage with simpler structure
{
Page: &lang.Page{
Name: "AdminPage",
Path: "/admin",
Layout: "admin",
Sections: []lang.Section{
Elements: []lang.PageElement{
{
Section: &lang.Section{
Name: "dashboard",
Type: strPtr("container"),
},
@ -249,5 +260,6 @@ func createTestAST() lang.AST {
},
},
},
},
}
}