| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- using System.Text.RegularExpressions;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.DataPlatform;
- /// <summary>
- /// 迁移脚本的「登记」守卫:仓库里每一个版本号命名的 <c>UpdateScripts/*.sql</c>
- /// 都必须在 <c>Admin.NET.Web.Entry.csproj</c> 里有 <c>CopyToOutputDirectory</c> 条目。
- ///
- /// <para><b>为什么需要</b>:<c>AutoVersionUpdate.LoadMigrationScripts</c> 扫的是
- /// <c>AppContext.BaseDirectory/UpdateScripts</c>,也就是**输出目录**。
- /// 漏登记的脚本不会被复制过去,于是它「在仓库里、却永远不会执行」——
- /// 没有报错、没有日志、部署看起来一切正常,直到有人发现表结构或数据没变。
- /// 2026-09-28 的 1.0.589 就这么静默失效过一次:表和列是 SqlSugar CodeFirst 顺手建的,
- /// 脚本里的数据迁移部分一行都没跑。</para>
- ///
- /// <para>已有 <c>S8LegacyNotifyDataRetirementTests.Csproj_RegistersScriptsAndVersionIsAtLeastScriptVersion</c>
- /// 守着单个版本;本组把它推广到全部脚本,新增脚本无需再各写一条。</para>
- /// </summary>
- public class MigrationScriptRegistrationTests
- {
- /// <summary>
- /// 2026-09-28 本组守卫首次引入时**已经存在**的未登记脚本,即从未被
- /// <c>AutoVersionUpdate</c> 执行过的脚本。列在这里只是为了让守卫能卡住**新增**遗漏,
- /// <b>不代表它们没问题</b>。
- ///
- /// <para>为什么不顺手补登记:补上之后,这 11 个从未跑过的迁移会在下一次启动时
- /// 对现网库依次执行(含 <c>1.0.463</c> 的 <c>BEFORE UPDATE</c> 触发器、
- /// <c>1.0.257</c> 的 29 张 <c>qms_*</c> 加列)。其中至少 <c>1.0.257</c> 自己注明
- /// 「Applied on aidopdev 2026-07-23」——是走配套 Python 手工执行的,
- /// 也就是说这些脚本各自的「该不该自动跑」需要逐个判断,不是一次批量登记能解决的。
- /// 故此处只做登记留痕,处置另开任务。</para>
- ///
- /// <para><b>名单只允许变短,不允许变长</b>:逐个核实并处置后从本列表删除;
- /// 若有人往里加新项,说明又漏登记了一个,应该去补 csproj 而不是加白名单。</para>
- /// </summary>
- private static readonly string[] KnownUnregisteredAsOf20260928 =
- [
- "1.0.208.sql", "1.0.212.sql",
- "1.0.257.sql", "1.0.258.sql", "1.0.259.sql", "1.0.260.sql",
- "1.0.261.sql", "1.0.262.sql", "1.0.263.sql", "1.0.264.sql",
- "1.0.463.sql",
- ];
- [Fact]
- public void EveryVersionedScript_IsRegisteredInCsproj()
- {
- var csproj = File.ReadAllText(CsprojPath());
- var missing = new List<string>();
- foreach (var file in VersionedScripts())
- {
- var name = Path.GetFileName(file);
- if (csproj.Contains($@"<None Update=""UpdateScripts\{name}"">", StringComparison.Ordinal))
- continue;
- if (KnownUnregisteredAsOf20260928.Contains(name, StringComparer.OrdinalIgnoreCase))
- continue;
- missing.Add(name);
- }
- Assert.True(missing.Count == 0,
- "以下迁移脚本未在 csproj 登记 CopyToOutputDirectory,不会被复制到输出目录,"
- + "AutoVersionUpdate 永远扫不到它们(静默失效):" + string.Join(", ", missing));
- }
- /// <summary>
- /// 已知名单里的项必须真的还没登记。补登记后应从名单里删掉,
- /// 否则名单会腐烂成一份没人敢动的陈旧白名单。
- /// </summary>
- [Fact]
- public void KnownUnregisteredList_HasNoStaleEntries()
- {
- var csproj = File.ReadAllText(CsprojPath());
- var stale = KnownUnregisteredAsOf20260928
- .Where(n => csproj.Contains($@"<None Update=""UpdateScripts\{n}"">", StringComparison.Ordinal))
- .ToList();
- Assert.True(stale.Count == 0,
- "以下脚本已补登记,请从 KnownUnregisteredAsOf20260928 中删除:" + string.Join(", ", stale));
- }
- /// <summary>
- /// 版本号必须不低于最大脚本编号,否则 <c>AutoVersionUpdate</c> 不会应用尾部那几个脚本。
- /// </summary>
- [Fact]
- public void AssemblyVersion_IsAtLeastMaxScriptVersion()
- {
- var csproj = File.ReadAllText(CsprojPath());
- var m = Regex.Match(csproj, @"<Version>([\d.]+)</Version>");
- Assert.True(m.Success, "csproj 未找到 <Version>");
- var appVersion = Version.Parse(m.Groups[1].Value);
- var maxScript = VersionedScripts()
- .Select(f => Version.Parse(Path.GetFileNameWithoutExtension(f)))
- .DefaultIfEmpty(new Version(0, 0, 0))
- .Max();
- Assert.True(appVersion >= maxScript,
- $"csproj 版本 {appVersion} 低于最大脚本编号 {maxScript},AutoVersionUpdate 不会应用它");
- }
- /// <summary>
- /// 每个 <c>*.verify.sql</c> 都必须有同号的主脚本,并且同样要登记。
- /// 只登记主脚本、漏掉 verify,会让部署后断言静默跳过。
- /// </summary>
- [Fact]
- public void EveryVerifyScript_HasSiblingAndIsRegistered()
- {
- var csproj = File.ReadAllText(CsprojPath());
- var problems = new List<string>();
- foreach (var file in Directory.GetFiles(ScriptsDir(), "*.verify.sql"))
- {
- var name = Path.GetFileName(file);
- var version = name[..^".verify.sql".Length];
- if (!File.Exists(Path.Combine(ScriptsDir(), $"{version}.sql")))
- problems.Add($"{name}(缺同号主脚本)");
- if (!csproj.Contains($@"<None Update=""UpdateScripts\{name}"">", StringComparison.Ordinal))
- problems.Add($"{name}(未登记)");
- }
- Assert.True(problems.Count == 0, "verify 脚本问题:" + string.Join(", ", problems));
- }
- /// <summary>
- /// 与 <c>AutoVersionUpdate.LoadMigrationScripts</c> 的筛选口径保持一致:
- /// 排除 <c>*.verify.sql</c>,只取文件名能解析成版本号的(WIP 占位名不参与)。
- /// </summary>
- private static IEnumerable<string> VersionedScripts() =>
- Directory.GetFiles(ScriptsDir(), "*.sql", SearchOption.TopDirectoryOnly)
- .Where(f => !Path.GetFileName(f).EndsWith(".verify.sql", StringComparison.OrdinalIgnoreCase))
- .Where(f => Version.TryParse(Path.GetFileNameWithoutExtension(f), out _));
- private static string ScriptsDir() =>
- Path.Combine(EntryDir(), "UpdateScripts");
- private static string CsprojPath() =>
- Path.Combine(EntryDir(), "Admin.NET.Web.Entry.csproj");
- private static string EntryDir()
- {
- var dir = new DirectoryInfo(AppContext.BaseDirectory);
- while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
- dir = dir.Parent;
- Assert.NotNull(dir);
- return Path.Combine(dir!.FullName, "server", "Admin.NET.Web.Entry");
- }
- }
|