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",
|
Name: "HomePage",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Layout: "public",
|
Layout: "public",
|
||||||
Components: []lang.Component{
|
Elements: []lang.PageElement{
|
||||||
{Type: "header"},
|
|
||||||
{Type: "footer"},
|
|
||||||
},
|
|
||||||
Sections: []lang.Section{
|
|
||||||
{
|
{
|
||||||
|
Component: &lang.Component{
|
||||||
|
Type: "header",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Component: &lang.Component{
|
||||||
|
Type: "footer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Section: &lang.Section{
|
||||||
Name: "hero",
|
Name: "hero",
|
||||||
Type: strPtr("container"),
|
Type: strPtr("container"),
|
||||||
Elements: []lang.SectionElement{
|
Elements: []lang.SectionElement{
|
||||||
@ -207,7 +214,9 @@ func createTestAST() lang.AST {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
Section: &lang.Section{
|
||||||
Name: "content",
|
Name: "content",
|
||||||
Type: strPtr("container"),
|
Type: strPtr("container"),
|
||||||
Elements: []lang.SectionElement{
|
Elements: []lang.SectionElement{
|
||||||
@ -234,14 +243,16 @@ func createTestAST() lang.AST {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
// AdminPage with simpler structure
|
// AdminPage with simpler structure
|
||||||
{
|
{
|
||||||
Page: &lang.Page{
|
Page: &lang.Page{
|
||||||
Name: "AdminPage",
|
Name: "AdminPage",
|
||||||
Path: "/admin",
|
Path: "/admin",
|
||||||
Layout: "admin",
|
Layout: "admin",
|
||||||
Sections: []lang.Section{
|
Elements: []lang.PageElement{
|
||||||
{
|
{
|
||||||
|
Section: &lang.Section{
|
||||||
Name: "dashboard",
|
Name: "dashboard",
|
||||||
Type: strPtr("container"),
|
Type: strPtr("container"),
|
||||||
},
|
},
|
||||||
@ -249,5 +260,6 @@ func createTestAST() lang.AST {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user