Files
orm-cj/src/tests/MigrationCli_test.cj
T

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)
}
}