kra/internal/biz/system/auto_code_template_test.go

882 lines
22 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package system
import (
"bytes"
"go/format"
"strings"
"testing"
"text/template"
"kra/pkg/utils/autocode"
)
// TestTemplateFuncMap 测试模板函数映射是否正确注册
func TestTemplateFuncMap(t *testing.T) {
funcMap := autocode.GetTemplateFuncMap()
// 验证必要的函数已注册
requiredFuncs := []string{
"title",
"toPascalCase",
"GenerateField",
"GenerateModelField",
"GenerateSearchField",
"GenerateSearchConditions",
"GenerateGormGenSearchConditions",
"GenerateSearchFormItem",
"GenerateTableColumn",
"GenerateFormItem",
"GenerateDescriptionItem",
"GenerateDefaultFormValue",
"GenerateTSType",
"GenerateReactSearchFormItem",
"GenerateReactTableColumn",
"GenerateReactProTableColumn",
"GenerateReactFormItem",
"GenerateReactDescriptionItem",
"GenerateReactDefaultValue",
"GenerateCustomMethodInterface",
"GenerateCustomMethodImpl",
}
for _, funcName := range requiredFuncs {
if _, ok := funcMap[funcName]; !ok {
t.Errorf("模板函数 %s 未注册", funcName)
}
}
}
// TestGenerateTSType 测试 Go 类型到 TypeScript 类型的映射
func TestGenerateTSType(t *testing.T) {
testCases := []struct {
goType string
expected string
}{
// 整数类型
{"int", "number"},
{"int8", "number"},
{"int16", "number"},
{"int32", "number"},
{"int64", "number"},
// 无符号整数类型
{"uint", "number"},
{"uint8", "number"},
{"uint16", "number"},
{"uint32", "number"},
{"uint64", "number"},
// 浮点类型
{"float32", "number"},
{"float64", "number"},
// 基础类型
{"string", "string"},
{"bool", "boolean"},
// 时间类型
{"time.Time", "string"},
// JSON 类型
{"datatypes.JSON", "Record<string, any>"},
{"json", "Record<string, any>"},
// 媒体类型
{"picture", "string"},
{"pictures", "string[]"},
{"video", "string"},
{"file", "Array<{ name: string; url: string; uid: string }>"},
// 其他类型
{"array", "any[]"},
{"richtext", "string"},
{"enum", "string"},
// 指针类型
{"*string", "string"},
{"*int", "number"},
{"*bool", "boolean"},
{"*time.Time", "string"},
{"*int64", "number"},
{"*float64", "number"},
// 未知类型
{"unknown", "any"},
{"customType", "any"},
{"", "any"},
}
for _, tc := range testCases {
t.Run(tc.goType, func(t *testing.T) {
result := autocode.GenerateTSType(tc.goType)
if result != tc.expected {
t.Errorf("GenerateTSType(%s) = %s, 期望 %s", tc.goType, result, tc.expected)
}
})
}
}
// TestGenerateTSTypePointerRecursion 测试指针类型的递归处理
func TestGenerateTSTypePointerRecursion(t *testing.T) {
testCases := []struct {
name string
goType string
expected string
}{
{"单层指针-string", "*string", "string"},
{"单层指针-int", "*int", "number"},
{"单层指针-bool", "*bool", "boolean"},
{"单层指针-time", "*time.Time", "string"},
{"单层指针-json", "*datatypes.JSON", "Record<string, any>"},
{"单层指针-未知类型", "*unknownType", "any"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateTSType(tc.goType)
if result != tc.expected {
t.Errorf("GenerateTSType(%s) = %s, 期望 %s", tc.goType, result, tc.expected)
}
})
}
}
// TestGenerateTSTypeAllNumericTypes 测试所有数值类型映射
func TestGenerateTSTypeAllNumericTypes(t *testing.T) {
numericTypes := []string{
"int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64",
"float32", "float64",
}
for _, goType := range numericTypes {
t.Run(goType, func(t *testing.T) {
result := autocode.GenerateTSType(goType)
if result != "number" {
t.Errorf("GenerateTSType(%s) = %s, 期望 number", goType, result)
}
})
}
}
// TestGenerateTSTypeMediaTypes 测试媒体类型映射
func TestGenerateTSTypeMediaTypes(t *testing.T) {
testCases := []struct {
goType string
expected string
}{
{"picture", "string"},
{"pictures", "string[]"},
{"video", "string"},
{"file", "Array<{ name: string; url: string; uid: string }>"},
{"richtext", "string"},
}
for _, tc := range testCases {
t.Run(tc.goType, func(t *testing.T) {
result := autocode.GenerateTSType(tc.goType)
if result != tc.expected {
t.Errorf("GenerateTSType(%s) = %s, 期望 %s", tc.goType, result, tc.expected)
}
})
}
}
// TestToPascalCase 测试 PascalCase 转换
func TestToPascalCase(t *testing.T) {
testCases := []struct {
input string
expected string
}{
{"article_status", "ArticleStatus"},
{"user-role", "UserRole"},
{"hello world", "HelloWorld"},
{"simple", "Simple"},
{"", ""},
{"already_Pascal_Case", "AlreadyPascalCase"},
}
for _, tc := range testCases {
result := autocode.ToPascalCase(tc.input)
if result != tc.expected {
t.Errorf("ToPascalCase(%s) = %s, 期望 %s", tc.input, result, tc.expected)
}
}
}
// TestGenerateModelField 测试 GORM Gen 模型字段生成
func TestGenerateModelField(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string // 生成的字段应包含的内容
}{
{
name: "字符串字段",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
ColumnName: "title",
DataTypeLong: "255",
},
contains: []string{"Title", "string", "gorm:", "column:title", "json:\"title\"", "标题"},
},
{
name: "整数字段",
field: autocode.AutoCodeField{
FieldName: "Status",
FieldDesc: "状态",
FieldType: "int",
FieldJson: "status",
ColumnName: "status",
DataTypeLong: "1",
},
contains: []string{"Status", "int8", "gorm:", "column:status", "json:\"status\""},
},
{
name: "时间字段",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldDesc: "创建时间",
FieldType: "time.Time",
FieldJson: "createdAt",
ColumnName: "created_at",
},
contains: []string{"CreatedAt", "time.Time", "gorm:", "column:created_at"},
},
{
name: "布尔字段",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
ColumnName: "is_active",
},
contains: []string{"IsActive", "bool", "gorm:", "column:is_active"},
},
{
name: "JSON字段",
field: autocode.AutoCodeField{
FieldName: "Extra",
FieldDesc: "扩展信息",
FieldType: "json",
FieldJson: "extra",
ColumnName: "extra",
},
contains: []string{"Extra", "datatypes.JSON", "gorm:", "column:extra"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateModelField(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateModelField 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateGormGenSearchConditions 测试 GORM Gen 搜索条件生成
func TestGenerateGormGenSearchConditions(t *testing.T) {
fields := []*autocode.AutoCodeField{
{
FieldName: "Title",
FieldType: "string",
FieldSearchType: "LIKE",
},
{
FieldName: "Status",
FieldType: "int",
FieldSearchType: "EQ",
},
{
FieldName: "CreatedAt",
FieldType: "time.Time",
FieldSearchType: "BETWEEN",
},
}
result := autocode.GenerateGormGenSearchConditions(fields, "Article")
// 验证 LIKE 条件
if !strings.Contains(result, "t.Title.Like") {
t.Error("LIKE 搜索条件应使用 t.Title.Like")
}
// 验证 EQ 条件
if !strings.Contains(result, "t.Status.Eq") {
t.Error("EQ 搜索条件应使用 t.Status.Eq")
}
// 验证 BETWEEN 条件
if !strings.Contains(result, "t.CreatedAt.Between") {
t.Error("BETWEEN 搜索条件应使用 t.CreatedAt.Between")
}
}
// TestGenerateReactFormItem 测试 React 表单项生成
func TestGenerateReactFormItem(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
}{
{
name: "字符串输入框",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
Require: true,
},
contains: []string{"Form.Item", "name=\"title\"", "label=\"标题\"", "Input", "required: true"},
},
{
name: "布尔开关",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
},
contains: []string{"Form.Item", "name=\"isActive\"", "Switch", "valuePropName=\"checked\""},
},
{
name: "数字输入框",
field: autocode.AutoCodeField{
FieldName: "Count",
FieldDesc: "数量",
FieldType: "int",
FieldJson: "count",
},
contains: []string{"Form.Item", "name=\"count\"", "InputNumber"},
},
{
name: "日期选择器",
field: autocode.AutoCodeField{
FieldName: "PublishDate",
FieldDesc: "发布日期",
FieldType: "time.Time",
FieldJson: "publishDate",
},
contains: []string{"Form.Item", "name=\"publishDate\"", "DatePicker", "showTime"},
},
{
name: "字典选择器",
field: autocode.AutoCodeField{
FieldName: "Status",
FieldDesc: "状态",
FieldType: "string",
FieldJson: "status",
DictType: "article_status",
},
contains: []string{"Form.Item", "name=\"status\"", "Select", "article_statusOptions"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateReactFormItem(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateReactFormItem 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateReactTableColumn 测试 React 表格列生成
func TestGenerateReactTableColumn(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
}{
{
name: "普通字符串列",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
},
contains: []string{"title: '标题'", "dataIndex: 'title'", "key: 'title'"},
},
{
name: "可排序列",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldDesc: "创建时间",
FieldType: "time.Time",
FieldJson: "createdAt",
Sort: true,
},
contains: []string{"title: '创建时间'", "sorter: true", "dayjs"},
},
{
name: "布尔列",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
},
contains: []string{"title: '是否激活'", "是", "否"},
},
{
name: "图片列",
field: autocode.AutoCodeField{
FieldName: "Cover",
FieldDesc: "封面",
FieldType: "picture",
FieldJson: "cover",
},
contains: []string{"title: '封面'", "Image"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateReactTableColumn(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateReactTableColumn 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateReactDefaultValue 测试 React 默认值生成
func TestGenerateReactDefaultValue(t *testing.T) {
testCases := []struct {
fieldType string
fieldJson string
expected string
}{
{"bool", "isActive", "isActive: false,"},
{"string", "title", "title: '',"},
{"int", "count", "count: 0,"},
{"float64", "price", "price: 0,"},
{"time.Time", "createdAt", "createdAt: undefined,"},
{"pictures", "images", "images: [],"},
{"file", "attachment", "attachment: [],"},
{"json", "extra", "extra: {},"},
}
for _, tc := range testCases {
field := autocode.AutoCodeField{
FieldType: tc.fieldType,
FieldJson: tc.fieldJson,
}
result := autocode.GenerateReactDefaultValue(field)
if result != tc.expected {
t.Errorf("GenerateReactDefaultValue(%s) = %s, 期望 %s", tc.fieldType, result, tc.expected)
}
}
}
// TestTemplateRenderingBasic 测试基本模板渲染功能
func TestTemplateRenderingBasic(t *testing.T) {
// 简单模板测试
tplContent := `package {{.Package}}
type {{.StructName}} struct {
ID int64
}
`
data := map[string]interface{}{
"Package": "example",
"StructName": "Article",
}
tmpl, err := template.New("test").Funcs(autocode.GetTemplateFuncMap()).Parse(tplContent)
if err != nil {
t.Fatalf("模板解析失败: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
t.Fatalf("模板执行失败: %v", err)
}
result := buf.String()
// 验证渲染结果
if !strings.Contains(result, "package example") {
t.Error("渲染结果应包含 'package example'")
}
if !strings.Contains(result, "type Article struct") {
t.Error("渲染结果应包含 'type Article struct'")
}
}
// TestTemplateRenderingWithFunctions 测试带模板函数的渲染
func TestTemplateRenderingWithFunctions(t *testing.T) {
tplContent := `// {{.Description}}类型定义
export interface {{.StructName}} {
{{- range .Fields}}
{{.FieldJson}}: {{ GenerateTSType .FieldType }}; // {{.FieldDesc}}
{{- end}}
}
`
data := map[string]interface{}{
"Description": "文章",
"StructName": "Article",
"Fields": []autocode.AutoCodeField{
{FieldJson: "title", FieldType: "string", FieldDesc: "标题"},
{FieldJson: "content", FieldType: "string", FieldDesc: "内容"},
{FieldJson: "viewCount", FieldType: "int", FieldDesc: "浏览量"},
{FieldJson: "isPublished", FieldType: "bool", FieldDesc: "是否发布"},
},
}
tmpl, err := template.New("test").Funcs(autocode.GetTemplateFuncMap()).Parse(tplContent)
if err != nil {
t.Fatalf("模板解析失败: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
t.Fatalf("模板执行失败: %v", err)
}
result := buf.String()
// 验证 TypeScript 类型映射
if !strings.Contains(result, "title: string;") {
t.Error("渲染结果应包含 'title: string;'")
}
if !strings.Contains(result, "viewCount: number;") {
t.Error("渲染结果应包含 'viewCount: number;'")
}
if !strings.Contains(result, "isPublished: boolean;") {
t.Error("渲染结果应包含 'isPublished: boolean;'")
}
}
// TestGoCodeFormatting 测试 Go 代码格式化Property 4: 模板渲染幂等性)
func TestGoCodeFormatting(t *testing.T) {
// 生成的 Go 代码
goCode := `package example
import "fmt"
type Article struct {
ID int64
Title string
Content string
}
func (a *Article) String() string {
return fmt.Sprintf("Article{ID: %d, Title: %s}", a.ID, a.Title)
}
`
// 格式化代码
formatted, err := format.Source([]byte(goCode))
if err != nil {
t.Fatalf("代码格式化失败: %v", err)
}
// 再次格式化,验证幂等性
formattedAgain, err := format.Source(formatted)
if err != nil {
t.Fatalf("二次格式化失败: %v", err)
}
// 验证两次格式化结果相同
if string(formatted) != string(formattedAgain) {
t.Error("格式化应该是幂等的,两次格式化结果应相同")
}
}
// TestGenerateSearchField 测试搜索字段生成
func TestGenerateSearchField(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
isEmpty bool
}{
{
name: "无搜索类型",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldType: "string",
FieldJson: "title",
FieldSearchType: "",
},
isEmpty: true,
},
{
name: "LIKE搜索",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldType: "string",
FieldJson: "title",
FieldSearchType: "LIKE",
},
contains: []string{"Title", "*string", "json:\"title\""},
},
{
name: "BETWEEN搜索-时间",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldType: "time.Time",
FieldJson: "createdAt",
FieldSearchType: "BETWEEN",
},
contains: []string{"CreatedAtRange", "[]time.Time"},
},
{
name: "BETWEEN搜索-数字",
field: autocode.AutoCodeField{
FieldName: "Price",
FieldType: "float64",
FieldJson: "price",
FieldSearchType: "BETWEEN",
},
contains: []string{"StartPrice", "EndPrice", "*float64"},
},
{
name: "IN搜索",
field: autocode.AutoCodeField{
FieldName: "Status",
FieldType: "int",
FieldJson: "status",
FieldSearchType: "IN",
},
contains: []string{"Status", "[]int"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateSearchField(tc.field)
if tc.isEmpty {
if result != "" {
t.Errorf("期望空字符串,实际: %s", result)
}
return
}
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateSearchField 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateReactDescriptionItem 测试 React 描述项生成
func TestGenerateReactDescriptionItem(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
}{
{
name: "普通字符串",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
},
contains: []string{"Descriptions.Item", "label=\"标题\"", "currentRecord?.title"},
},
{
name: "布尔类型",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
},
contains: []string{"Descriptions.Item", "是", "否"},
},
{
name: "时间类型",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldDesc: "创建时间",
FieldType: "time.Time",
FieldJson: "createdAt",
},
contains: []string{"Descriptions.Item", "dayjs", "format"},
},
{
name: "图片类型",
field: autocode.AutoCodeField{
FieldName: "Cover",
FieldDesc: "封面",
FieldType: "picture",
FieldJson: "cover",
},
contains: []string{"Descriptions.Item", "Image"},
},
{
name: "JSON类型",
field: autocode.AutoCodeField{
FieldName: "Extra",
FieldDesc: "扩展信息",
FieldType: "json",
FieldJson: "extra",
},
contains: []string{"Descriptions.Item", "JSON.stringify"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateReactDescriptionItem(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateReactDescriptionItem 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestGenerateReactProTableColumn 测试 Ant Design Pro Table 列配置生成
func TestGenerateReactProTableColumn(t *testing.T) {
testCases := []struct {
name string
field autocode.AutoCodeField
contains []string
}{
{
name: "普通文本列",
field: autocode.AutoCodeField{
FieldName: "Title",
FieldDesc: "标题",
FieldType: "string",
FieldJson: "title",
},
contains: []string{"title: '标题'", "dataIndex: 'title'", "ellipsis: true"},
},
{
name: "布尔列",
field: autocode.AutoCodeField{
FieldName: "IsActive",
FieldDesc: "是否激活",
FieldType: "bool",
FieldJson: "isActive",
},
contains: []string{"valueType: 'select'", "valueEnum", "是", "否"},
},
{
name: "时间列-范围搜索",
field: autocode.AutoCodeField{
FieldName: "CreatedAt",
FieldDesc: "创建时间",
FieldType: "time.Time",
FieldJson: "createdAt",
FieldSearchType: "BETWEEN",
},
contains: []string{"valueType: 'dateTimeRange'"},
},
{
name: "数字列",
field: autocode.AutoCodeField{
FieldName: "Count",
FieldDesc: "数量",
FieldType: "int",
FieldJson: "count",
FieldSearchType: "EQ",
},
contains: []string{"valueType: 'digit'"},
},
{
name: "字典列",
field: autocode.AutoCodeField{
FieldName: "Status",
FieldDesc: "状态",
FieldType: "string",
FieldJson: "status",
DictType: "article_status",
},
contains: []string{"valueType: 'select'", "article_statusOptions"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := autocode.GenerateReactProTableColumn(tc.field)
for _, expected := range tc.contains {
if !strings.Contains(result, expected) {
t.Errorf("GenerateReactProTableColumn 结果应包含 %s, 实际结果: %s", expected, result)
}
}
})
}
}
// TestComplexTemplateRendering 测试复杂模板渲染(模拟实际 biz 层模板)
func TestComplexTemplateRendering(t *testing.T) {
tplContent := `package {{.Package}}
import (
"context"
"{{.Module}}/internal/data/model/{{.Package}}"
"github.com/go-kratos/kratos/v2/log"
)
// {{.StructName}}Repo {{.Description}}仓储接口
type {{.StructName}}Repo interface {
Create(ctx context.Context, {{.Abbreviation}} *{{.Package}}.{{.StructName}}) error
Delete(ctx context.Context, {{.PrimaryField.FieldJson}} {{.PrimaryField.FieldType}}) error
Update(ctx context.Context, {{.Abbreviation}} *{{.Package}}.{{.StructName}}) error
FindByID(ctx context.Context, {{.PrimaryField.FieldJson}} {{.PrimaryField.FieldType}}) (*{{.Package}}.{{.StructName}}, error)
}
// {{.StructName}}Usecase {{.Description}}用例
type {{.StructName}}Usecase struct {
repo {{.StructName}}Repo
log *log.Helper
}
`
data := map[string]interface{}{
"Package": "article",
"Module": "kra",
"StructName": "Article",
"Description": "文章",
"Abbreviation": "article",
"PrimaryField": map[string]interface{}{
"FieldJson": "id",
"FieldType": "int64",
},
}
tmpl, err := template.New("test").Funcs(autocode.GetTemplateFuncMap()).Parse(tplContent)
if err != nil {
t.Fatalf("模板解析失败: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
t.Fatalf("模板执行失败: %v", err)
}
result := buf.String()
// 验证关键内容
expectedContents := []string{
"package article",
"ArticleRepo interface",
"ArticleUsecase struct",
"Create(ctx context.Context",
"Delete(ctx context.Context",
"Update(ctx context.Context",
"FindByID(ctx context.Context",
"*article.Article",
"log.Helper",
}
for _, expected := range expectedContents {
if !strings.Contains(result, expected) {
t.Errorf("渲染结果应包含 '%s'", expected)
}
}
}