263 lines
10 KiB
Plaintext
263 lines
10 KiB
Plaintext
/*
|
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
|
* 迁移 CLI 单元测试(纯逻辑 + 临时目录落盘,不连接数据库)。
|
|
* 覆盖:迁移源码生成(初始/增量/手写含索引与 rawSql)/ 注册文件重写 /
|
|
* className 与文件名推导 / CLI 命令分发(help/未知/缺参数)。
|
|
*/
|
|
|
|
package simcu::orm.tests
|
|
|
|
import std.collection.*
|
|
import std.fs.*
|
|
import std.unittest.*
|
|
import std.unittest.testmacro.*
|
|
import simcu::orm.*
|
|
import simcu::orm.cli.*
|
|
|
|
/// 手写迁移(验证源码生成覆盖索引与 rawSql 转义)
|
|
class HandWrittenMigration <: Migration {
|
|
public init() {
|
|
super("20250801000000_HandWritten", "手写迁移")
|
|
}
|
|
public override func up(builder: MigrationBuilder): Unit {
|
|
builder.createTable("tags") { tb =>
|
|
tb.column("id", ColumnTypes.BigIntCol).primary().autoInc().notNull()
|
|
tb.column("label", ColumnTypes.TextCol).withMaxLength(50).withUnique().notNull()
|
|
}
|
|
let idxCols = ArrayList<String>()
|
|
idxCols.add("label")
|
|
builder.createIndex("ix_tags_label", "tags", idxCols, true)
|
|
builder.rawSql("SELECT 1 -- comment \"quoted\"")
|
|
}
|
|
public override func down(builder: MigrationBuilder): Unit {
|
|
builder.dropTable("tags")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
class MigrationFileGeneratorTests {
|
|
private func buildUserModel(): EntityModel {
|
|
let model = EntityModel()
|
|
model.tableName = "users"
|
|
let pk = PropertyModel()
|
|
pk.name = "id"
|
|
pk.columnName = "id"
|
|
pk.isKey = true
|
|
pk.autoIncrement = true
|
|
pk.isRequired = true
|
|
pk.typeNameOverride = "Int64"
|
|
model.properties.add(pk)
|
|
let name = PropertyModel()
|
|
name.name = "name"
|
|
name.columnName = "name"
|
|
name.isRequired = true
|
|
name.maxLength = 100
|
|
name.typeNameOverride = "String"
|
|
model.properties.add(name)
|
|
model.keyProperty = Some(pk)
|
|
model
|
|
}
|
|
|
|
private func buildOrderModel(): EntityModel {
|
|
let model = EntityModel()
|
|
model.tableName = "orders"
|
|
let pk = PropertyModel()
|
|
pk.name = "oid"
|
|
pk.columnName = "oid"
|
|
pk.isKey = true
|
|
pk.clientGenerated = true
|
|
pk.isRequired = true
|
|
pk.typeNameOverride = "String"
|
|
model.properties.add(pk)
|
|
model.keyProperty = Some(pk)
|
|
model
|
|
}
|
|
|
|
@TestCase
|
|
public func testClassNameAndFileName(): Unit {
|
|
@Expect(MigrationFileGenerator.classNameOf("20250701000000_InitialCreate") == "InitialCreate")
|
|
@Expect(MigrationFileGenerator.fileNameOf("20250701000000_InitialCreate") == "20250701000000_InitialCreate.cj")
|
|
@Expect(MigrationFileGenerator.classNameOf("NoTimestamp") == "NoTimestamp")
|
|
}
|
|
|
|
@TestCase
|
|
public func testInitialMigrationSource(): Unit {
|
|
let models = ArrayList<EntityModel>()
|
|
models.add(buildUserModel())
|
|
let gen = MigrationGenerator()
|
|
let m = gen.initial("20250701000000_InitialCreate", "初始建表", models)
|
|
let src = MigrationFileGenerator.migrationSource("app", MigrationFileGenerator.classNameOf(m.migrationId), m)
|
|
@Expect(src.contains("package app"))
|
|
@Expect(src.contains("public class InitialCreate <: Migration"))
|
|
@Expect(src.contains("super(\"20250701000000_InitialCreate\", \"初始建表\")"))
|
|
@Expect(src.contains("builder.createTable(\"users\") { tb =>"))
|
|
@Expect(src.contains("tb.column(\"id\", ColumnTypes.BigIntCol).primary().autoInc().notNull()"))
|
|
@Expect(src.contains("tb.column(\"name\", ColumnTypes.TextCol).notNull().withMaxLength(100)"))
|
|
@Expect(src.contains("builder.dropTable(\"users\")"))
|
|
}
|
|
|
|
@TestCase
|
|
public func testDiffMigrationSource(): Unit {
|
|
let oldModels = ArrayList<EntityModel>()
|
|
oldModels.add(buildUserModel())
|
|
let newModels = ArrayList<EntityModel>()
|
|
newModels.add(buildUserModel())
|
|
newModels.add(buildOrderModel())
|
|
let gen = MigrationGenerator()
|
|
let m = gen.diff("20250801000000_AddOrders", "新增 orders 表", oldModels, newModels)
|
|
let src = MigrationFileGenerator.migrationSource("app", MigrationFileGenerator.classNameOf(m.migrationId), m)
|
|
@Expect(src.contains("builder.createTable(\"orders\") { tb =>"))
|
|
@Expect(src.contains("tb.column(\"oid\", ColumnTypes.TextCol).primary().notNull()"))
|
|
// down 反转:先 drop 新表
|
|
@Expect(src.contains("builder.dropTable(\"orders\")"))
|
|
}
|
|
|
|
@TestCase
|
|
public func testHandWrittenMigrationSource(): Unit {
|
|
let m = HandWrittenMigration()
|
|
let src = MigrationFileGenerator.migrationSource("app", MigrationFileGenerator.classNameOf(m.migrationId), m)
|
|
@Expect(src.contains("builder.createIndex(\"ix_tags_label\", \"tags\", [\"label\"], true)"))
|
|
@Expect(src.contains("tb.column(\"label\", ColumnTypes.TextCol).notNull().withUnique().withMaxLength(50)"))
|
|
// rawSql 双引号转义
|
|
@Expect(src.contains("builder.rawSql(\"SELECT 1 -- comment \\\"quoted\\\"\")"))
|
|
@Expect(src.contains("builder.dropTable(\"tags\")"))
|
|
}
|
|
|
|
@TestCase
|
|
public func testRegistryRoundTrip(): Unit {
|
|
let entries = ArrayList<String>()
|
|
entries.add("InitialCreate")
|
|
entries.add("AddAge")
|
|
let src = MigrationFileGenerator.registrySource("app", entries)
|
|
@Expect(src.contains("package app"))
|
|
@Expect(src.contains("registerMigration(InitialCreate())"))
|
|
@Expect(src.contains("registerMigration(AddAge())"))
|
|
// 提取回环
|
|
let extracted = MigrationFileGenerator.extractRegistryEntries(src)
|
|
@Expect(extracted.size == 2)
|
|
@Expect(extracted[0] == "InitialCreate")
|
|
@Expect(extracted[1] == "AddAge")
|
|
// 去重
|
|
entries.add("InitialCreate")
|
|
let src2 = MigrationFileGenerator.registrySource("app", entries)
|
|
@Expect(MigrationFileGenerator.extractRegistryEntries(src2).size == 2)
|
|
}
|
|
|
|
@TestCase
|
|
public func testWriteMigrationAndRegistryToDisk(): Unit {
|
|
let dir = "cli_migration_tmp"
|
|
if (exists(Path(dir))) {
|
|
remove(Path(dir), recursive: true)
|
|
}
|
|
try {
|
|
let models = ArrayList<EntityModel>()
|
|
models.add(buildUserModel())
|
|
let gen = MigrationGenerator()
|
|
let m = gen.initial("20250701000000_InitialCreate", "初始建表", models)
|
|
let fg = MigrationFileGenerator()
|
|
let path = fg.writeMigration("testpkg", m, dir)
|
|
@Expect(exists(Path(path)))
|
|
let text = String.fromUtf8(File.readFrom(Path(path)))
|
|
@Expect(text.contains("package testpkg"))
|
|
@Expect(text.contains("public class InitialCreate <: Migration"))
|
|
|
|
// 注册文件:首次创建 + 追加 + 幂等
|
|
let registryPath = "${dir}/MigrationRegistry.cj"
|
|
fg.updateRegistry("testpkg", registryPath, "InitialCreate")
|
|
fg.updateRegistry("testpkg", registryPath, "AddAge")
|
|
fg.updateRegistry("testpkg", registryPath, "AddAge")
|
|
let reg = String.fromUtf8(File.readFrom(Path(registryPath)))
|
|
@Expect(reg.contains("package testpkg"))
|
|
@Expect(MigrationFileGenerator.extractRegistryEntries(reg).size == 2)
|
|
} finally {
|
|
if (exists(Path(dir))) {
|
|
remove(Path(dir), recursive: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
class MigrationCliDispatchTests {
|
|
@TestCase
|
|
public func testHelpReturnsZero(): Unit {
|
|
let rc = MigrationCli.run(ArrayList<String>(), { => ArrayList<EntityModel>() },
|
|
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
|
@Expect(rc == 0)
|
|
}
|
|
|
|
@TestCase
|
|
public func testUnknownCommandReturnsOne(): Unit {
|
|
let args = ArrayList<String>()
|
|
args.add("bogus")
|
|
let rc = MigrationCli.run(args, { => ArrayList<EntityModel>() },
|
|
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
|
@Expect(rc == 1)
|
|
}
|
|
|
|
@TestCase
|
|
public func testAddMissingNameReturnsOne(): Unit {
|
|
let args = ArrayList<String>()
|
|
args.add("add")
|
|
let rc = MigrationCli.run(args, { => ArrayList<EntityModel>() },
|
|
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
|
@Expect(rc == 1)
|
|
}
|
|
|
|
@TestCase
|
|
public func testRmMissingNameReturnsOne(): Unit {
|
|
let args = ArrayList<String>()
|
|
args.add("rm")
|
|
let rc = MigrationCli.run(args, { => ArrayList<EntityModel>() },
|
|
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
|
@Expect(rc == 1)
|
|
}
|
|
|
|
@TestCase
|
|
public func testDowngradeNoMigrationsReturnsOne(): Unit {
|
|
let args = ArrayList<String>()
|
|
args.add("downgrade")
|
|
let rc = MigrationCli.run(args, { => ArrayList<EntityModel>() },
|
|
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
|
@Expect(rc == 1)
|
|
}
|
|
|
|
@TestCase
|
|
public func testDowngradeWithTargetConsumesArgs(): Unit {
|
|
let args = ArrayList<String>()
|
|
args.add("downgrade")
|
|
args.add("20260819061048_InitialCreate")
|
|
let rc = MigrationCli.run(args, { => ArrayList<EntityModel>() },
|
|
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
|
@Expect(rc == 1)
|
|
}
|
|
|
|
@TestCase
|
|
public func testTryRunWithOrmPrefixConsumesArgs(): Unit {
|
|
let args = ArrayList<String>()
|
|
args.add("orm")
|
|
args.add("help")
|
|
let handled = MigrationCli.tryRun(args, { => ArrayList<EntityModel>() },
|
|
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
|
@Expect(handled == true)
|
|
}
|
|
|
|
@TestCase
|
|
public func testTryRunWithoutOrmPrefixReturnsFalse(): Unit {
|
|
let args = ArrayList<String>()
|
|
args.add("serve")
|
|
let handled = MigrationCli.tryRun(args, { => ArrayList<EntityModel>() },
|
|
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
|
@Expect(handled == false)
|
|
}
|
|
|
|
@TestCase
|
|
public func testTryRunEmptyArgsReturnsFalse(): Unit {
|
|
let handled = MigrationCli.tryRun(ArrayList<String>(), { => ArrayList<EntityModel>() },
|
|
{ => throw Exception("不应调用 datasource") }, { => ArrayList<Migration>() })
|
|
@Expect(handled == false)
|
|
}
|
|
}
|
|
|
|
|