2026-06-30 24681c81c09022f584a57006f2534b5f74723414
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package cn.iocoder.yudao.module.infra.service.codegen.inner;
 
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum;
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.InjectMocks;
import org.mockito.Spy;
 
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
/**
 * {@link CodegenEngine} 的单元测试抽象基类
 *
 * @author 芋道源码
 */
public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
 
    /**
     * 测试文件资源目录
     */
    private String resourcesPath = "";
 
    @InjectMocks
    protected CodegenEngine codegenEngine;
 
    @Spy
    protected CodegenProperties codegenProperties = new CodegenProperties()
            .setBasePackage("cn.iocoder.yudao")
            .setVoType(CodegenVOTypeEnum.VO.getType())
            .setDeleteBatchEnable(true)
            .setUnitTestEnable(true)
            .setImportEnable(false);
 
    @BeforeEach
    public void setUp() {
        codegenEngine.setJakartaEnable(true); // 强制使用 jakarta,保证单测可以基于 jakarta 断言
        codegenEngine.initGlobalBindingMap();
        // 获取测试文件 resources 路径,writeResult 调试用
        String absolutePath = FileUtil.getAbsolutePath("application-unit-test.yaml");
        resourcesPath = absolutePath.split("/target")[0] + "/src/test/resources/codegen/";
    }
 
    protected static CodegenTableDO getTable(String name) {
        String content = ResourceUtil.readUtf8Str("codegen/table/" + name + ".json");
        return JsonUtils.parseObject(content, "table", CodegenTableDO.class);
    }
 
    protected static List<CodegenColumnDO> getColumnList(String name) {
        String content = ResourceUtil.readUtf8Str("codegen/table/" + name + ".json");
        List<CodegenColumnDO> list = JsonUtils.parseArray(content, "columns", CodegenColumnDO.class);
        list.forEach(column -> {
            if (column.getNullable() == null) {
                column.setNullable(false);
            }
            if (column.getCreateOperation() == null) {
                column.setCreateOperation(false);
            }
            if (column.getUpdateOperation() == null) {
                column.setUpdateOperation(false);
            }
            if (column.getListOperation() == null) {
                column.setListOperation(false);
            }
            if (column.getListOperationResult() == null) {
                column.setListOperationResult(false);
            }
        });
        return list;
    }
 
    /**
     * 重新生成断言数据的开关,命令行加 {@code -Dcodegen.regenerate=true} 启用
     */
    private static final boolean REGENERATE = Boolean.parseBoolean(System.getProperty("codegen.regenerate", "false"));
 
    @SuppressWarnings("rawtypes")
    protected void assertResult(Map<String, String> result, String path) {
        if (REGENERATE) {
            writeResult(result, resourcesPath + path);
            return;
        }
        String assertContent = ResourceUtil.readUtf8Str("codegen/" + path + "/assert.json");
        List<HashMap> asserts = JsonUtils.parseArray(assertContent, HashMap.class);
        Set<String> expectedFiles = asserts.stream()
                .map(m -> (String) m.get("filePath"))
                .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
        assertEquals(expectedFiles, result.keySet(), "生成文件集合不匹配");
        // 校验每个文件;归一化 \r\n 为 \n,让断言不依赖文件落盘的换行风格
        asserts.forEach(assertMap -> {
            String contentPath = (String) assertMap.get("contentPath");
            String filePath = (String) assertMap.get("filePath");
            String expected = normalizeLineEndings(ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath));
            String actual = result.get(filePath);
            assertEquals(expected, normalizeLineEndings(actual), filePath + ":不匹配");
        });
    }
 
    /**
     * 统一换行符并忽略文件末尾换行,避免 Windows/Unix 落盘差异导致快照断言失败
     */
    private static String normalizeLineEndings(String content) {
        if (content == null) {
            return null;
        }
        content = content.replace("\r\n", "\n").replace('\r', '\n');
        return content.replaceAll("\\n+\\z", "");
    }
 
    // ==================== 调试专用 ====================
 
    /**
     * 【调试使用】将生成的代码,写入到文件
     *
     * @param result 生成的代码
     * @param path   写入文件的路径
     */
    protected void writeFile(Map<String, String> result, String path) {
        // 生成压缩包
        String[] paths = result.keySet().toArray(new String[0]);
        ByteArrayInputStream[] ins = result.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ZipUtil.zip(outputStream, paths, ins);
        // 写入文件
        FileUtil.writeBytes(outputStream.toByteArray(), path);
    }
 
    /**
     * 【调试使用】将生成的结果,写入到文件
     *
     * @param result   生成的代码
     * @param basePath 写入文件的路径(绝对路径)
     */
    protected void writeResult(Map<String, String> result, String basePath) {
        // 写入文件内容
        List<Map<String, String>> asserts = new ArrayList<>();
        Set<String> usedContentPaths = new LinkedHashSet<>();
        result.forEach((filePath, fileContent) -> {
            String lastFilePath = StrUtil.subAfter(filePath, '/', true);
            String ext = StrUtil.subAfter(lastFilePath, '.', true);
            String name = StrUtil.subBefore(lastFilePath, '.', true);
            String contentPath = ext + '/' + name;
            // 同名文件(如 index.vue 同时出现在 列表/form/detail)会撞名,撞名时前缀补上级目录区分;仍撞则加序号兜底,避免快照互相覆盖
            if (usedContentPaths.contains(contentPath)) {
                String parentDir = StrUtil.subAfter(StrUtil.subBefore(filePath, '/' + lastFilePath, true), '/', true);
                contentPath = ext + '/' + parentDir + '/' + name;
                for (int i = 2; usedContentPaths.contains(contentPath); i++) {
                    contentPath = ext + '/' + parentDir + '/' + name + '-' + i;
                }
            }
            usedContentPaths.add(contentPath);
            asserts.add(MapUtil.<String, String>builder().put("filePath", filePath)
                    .put("contentPath", contentPath).build());
            FileUtil.writeUtf8String(fileContent, basePath + "/" + contentPath);
        });
        // 写入 assert.json 文件
        FileUtil.writeUtf8String(JsonUtils.toJsonPrettyString(asserts), basePath + "/assert.json");
    }
 
}