add tests for custom functions
This commit is contained in:
455
interpreter/custom_js_functions_test.go
Normal file
455
interpreter/custom_js_functions_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user