using NewLife;
using NewLife.Serialization;
using Stardust.Data.Nodes;
using Stardust.Models;
using Xunit;
using Xunit.Abstractions;
using XCode;
namespace ServerTest;
/// <summary>数据库集成测试:解析 releases-10.0.json 并落库,验证所有安装类型/OS/指令集全覆盖</summary>
public class DotNetSyncDbTests
{
private readonly ITestOutputHelper _output;
public DotNetSyncDbTests(ITestOutputHelper output) => _output = output;
// ========== 复制自 DotNetSyncService(确保测试与服务端逻辑一致) ==========
private static (Int32 os, Int32 arch) ParseRid(String rid)
{
if (rid.IsNullOrEmpty()) return (0, 0);
var os = 0;
var arch = 0;
if (rid.StartsWithIgnoreCase("win")) os = (Int32)OSKind.Windows;
else if (rid.StartsWithIgnoreCase("linux-musl")) os = (Int32)OSKind.LinuxMusl;
else if (rid.StartsWithIgnoreCase("linux")) os = (Int32)OSKind.Linux;
else if (rid.StartsWithIgnoreCase("osx")) os = (Int32)OSKind.OSX;
if (rid.EndsWithIgnoreCase("-x64")) arch = (Int32)CpuArch.X64;
else if (rid.EndsWithIgnoreCase("-x86")) arch = (Int32)CpuArch.X86;
else if (rid.EndsWithIgnoreCase("-arm64")) arch = (Int32)CpuArch.Arm64;
else if (rid.EndsWithIgnoreCase("-arm")) arch = (Int32)CpuArch.Arm;
return (os, arch);
}
private static Boolean IsStandardRuntimeFile(String name, String kind)
{
return kind switch
{
"aspnet" => (name.StartsWith("aspnetcore-runtime-") &&
!name.Contains("composite") &&
!name.Contains("targeting")) ||
name.StartsWith("dotnet-hosting-"),
"runtime" => name.StartsWith("dotnet-runtime-"),
"desktop" => name.StartsWith("windowsdesktop-runtime-"),
"host" => name.StartsWith("dotnet-hosting-"),
"sdk" => name.StartsWith("dotnet-sdk-"),
_ => false,
};
}
// ========== 集成测试 ==========
[Fact]
public void Sync10_AllKindsAndArchCovered()
{
var filePath = "Data/releases-10.0.json";
Assert.True(File.Exists(filePath), $"测试数据文件不存在: {filePath}");
var json = File.ReadAllText(filePath);
var js = JsonParser.Decode(json);
var releases = js["releases"] as IList<Object>;
Assert.NotNull(releases);
Assert.NotEmpty(releases);
// 清空 10.0 相关已有记录(幂等)。按表达式批量删除,避免分页残留导致后续去重跳过插入
var deleted = DotNetPackage.Delete(DotNetPackage._.Version.StartsWith("10."));
_output.WriteLine("已清理 {0} 条 10.x 旧记录", deleted);
// 残留校验:清理不彻底会让去重逻辑跳过插入,导致入库数量异常
var leftOver = DotNetPackage.FindCount(DotNetPackage._.Version.StartsWith("10."));
Assert.Equal(0, leftOver);
var totalParsed = 0;
var kinds = new HashSet<String>();
var osKinds = new HashSet<OSKind>();
var archs = new HashSet<CpuArch>();
// 预取已有记录并在内存去重,最后批量落库,避免逐条查库与插入带来的大量往返
var keys = DotNetPackage.FindAll(DotNetPackage._.Version.StartsWith("10."))
.Select(e => (e.Version, e.Kind, e.OSKind, e.Architecture))
.ToHashSet();
var pending = new List<DotNetPackage>();
foreach (var item in releases)
{
var rel = item as IDictionary<String, Object>;
Assert.NotNull(rel);
var ver = rel["release-version"] + "";
// 跳过预览版和RC版(与服务端一致)
if (ver.Contains("preview", StringComparison.OrdinalIgnoreCase) ||
ver.Contains("rc", StringComparison.OrdinalIgnoreCase)) continue;
_output.WriteLine("Release {0}", ver);
// 处理 aspnetcore-runtime(含嵌套的 dotnet-hosting)
SyncRuntimeFiles(rel, "aspnetcore-runtime", "aspnet", ver, ref totalParsed, kinds, osKinds, archs, keys, pending);
// 处理 runtime(dotnet-runtime)
SyncRuntimeFiles(rel, "runtime", "runtime", ver, ref totalParsed, kinds, osKinds, archs, keys, pending);
// 处理 windowsdesktop
SyncRuntimeFiles(rel, "windowsdesktop", "desktop", ver, ref totalParsed, kinds, osKinds, archs, keys, pending);
// 处理 sdk + sdks
var sdk = rel["sdk"] as IDictionary<String, Object>;
if (sdk != null)
SyncSdkFiles(sdk, ver, ref totalParsed, kinds, osKinds, archs, keys, pending);
var sdks = rel["sdks"] as IList<Object>;
if (sdks != null)
{
foreach (var s in sdks)
SyncSdkFiles(s as IDictionary<String, Object>, ver, ref totalParsed, kinds, osKinds, archs, keys, pending);
}
}
if (pending.Count > 0) pending.Insert();
_output.WriteLine("");
_output.WriteLine("=== 总计入库: {0} 条 ===", totalParsed);
_output.WriteLine("安装类型: [{0}]", kinds.OrderBy(e => e).Join(", "));
_output.WriteLine("操作系统: [{0}]", osKinds.Select(e => e.ToString()).OrderBy(e => e).Join(", "));
_output.WriteLine("指令集: [{0}]", archs.Select(e => e.ToString()).OrderBy(e => e).Join(", "));
// ===== 断言:5 种安装类型全覆盖 =====
Assert.Contains("aspnet", kinds);
Assert.Contains("runtime", kinds);
Assert.Contains("desktop", kinds);
Assert.Contains("host", kinds, StringComparer.Ordinal); // dotnet-hosting 必须被识别为 host!
Assert.Contains("sdk", kinds);
// ===== 断言:4 种操作系统全覆盖 =====
Assert.Contains(OSKind.Windows, osKinds);
Assert.Contains(OSKind.Linux, osKinds);
Assert.Contains(OSKind.OSX, osKinds);
// LinuxMusl 在 10.0 中存在(dotnet-runtime-linux-musl-*)
Assert.Contains(OSKind.LinuxMusl, osKinds); // Alpine/musl 平台必须存在
// ===== 断言:4 种指令集全覆盖 =====
Assert.Contains(CpuArch.X64, archs);
Assert.Contains(CpuArch.X86, archs);
Assert.Contains(CpuArch.Arm64, archs);
Assert.Contains(CpuArch.Arm, archs);
// ===== 数量合理性 =====
// 10.0 有 ~27 个正式版本 × 每种 kind 多平台 ≈ 合计应远超 200
// 统计口径为"解析出的条目数",与数据库是否已存在同名记录无关:
// 服务端 DotNetSyncService 作为托管服务会在测试宿主后台同步数据,落库条数不可作为稳定断言
Assert.True(totalParsed > 200, String.Format("预期 >200 条,实际 {0} 条", totalParsed));
_output.WriteLine("");
_output.WriteLine("=== 全部断言通过 ===");
}
private void SyncRuntimeFiles(IDictionary<String, Object> rel, String key, String kind, String version,
ref Int32 totalParsed, HashSet<String> kinds, HashSet<OSKind> osKinds, HashSet<CpuArch> archs,
HashSet<(String, String, OSKind, CpuArch)> keys, List<DotNetPackage> pending)
{
if (!rel.TryGetValue(key, out var v) || v == null) return;
var runtime = v as IDictionary<String, Object>;
if (runtime == null) return;
var files = runtime["files"] as IList<Object>;
if (files == null) return;
foreach (var item in files)
{
var file = item as IDictionary<String, Object>;
if (file == null) continue;
var name = file["name"] + "";
var url = file["url"] + "";
var rid = file["rid"] + "";
var hash = file["hash"] + "";
if (name.IsNullOrEmpty() || url.IsNullOrEmpty()) continue;
// 优先从 URL 截取文件名
var idx = url.LastIndexOf('/');
if (idx >= 0 && idx < url.Length - 1)
{
var urlName = url.Substring(idx + 1);
if (!urlName.IsNullOrEmpty() && urlName.Contains('.'))
name = urlName;
}
if (!IsStandardRuntimeFile(name, kind)) continue;
var actualKind = name.StartsWith("dotnet-hosting-") ? "host" : kind;
var (os, arch) = ParseRid(rid);
// dotnet-hosting 空 rid 推断(与服务端一致)
if (os == 0 && name.StartsWith("dotnet-hosting-"))
{
if (name.Contains("win", StringComparison.OrdinalIgnoreCase))
{
os = (Int32)OSKind.Windows;
arch = (Int32)CpuArch.Any;
}
}
if (os == 0) continue;
if (os == (Int32)OSKind.Windows && name.EndsWithIgnoreCase(".zip")) continue;
// 统计解析覆盖。与库内是否已有同名记录无关,保证断言不受并发同步影响
totalParsed++;
kinds.Add(actualKind);
osKinds.Add((OSKind)os);
archs.Add((CpuArch)arch);
// 内存去重后延迟落库
if (!keys.Add((version, actualKind, (OSKind)os, (CpuArch)arch))) continue;
pending.Add(new DotNetPackage
{
Version = version,
Kind = actualKind,
OSKind = (OSKind)os,
Architecture = (CpuArch)arch,
FileName = name,
Source = url,
FileHash = hash,
Enable = true,
Channel = NodeChannels.Release,
});
}
}
private void SyncSdkFiles(IDictionary<String, Object> sdkObj, String version,
ref Int32 totalParsed, HashSet<String> kinds, HashSet<OSKind> osKinds, HashSet<CpuArch> archs,
HashSet<(String, String, OSKind, CpuArch)> keys, List<DotNetPackage> pending)
{
if (sdkObj == null) return;
var sdkVer = sdkObj["version"] + "";
if (sdkVer.IsNullOrEmpty()) return;
var files = sdkObj["files"] as IList<Object>;
if (files == null) return;
foreach (var item in files)
{
var file = item as IDictionary<String, Object>;
if (file == null) continue;
var name = file["name"] + "";
var url = file["url"] + "";
var rid = file["rid"] + "";
var hash = file["hash"] + "";
if (name.IsNullOrEmpty() || url.IsNullOrEmpty()) continue;
// 优先从 URL 截取文件名
var idx = url.LastIndexOf('/');
if (idx >= 0 && idx < url.Length - 1)
{
var urlName = url.Substring(idx + 1);
if (!urlName.IsNullOrEmpty() && urlName.Contains('.'))
name = urlName;
}
if (!IsStandardRuntimeFile(name, "sdk")) continue;
var (os, arch) = ParseRid(rid);
if (os == 0) continue;
if (os == (Int32)OSKind.Windows && name.EndsWithIgnoreCase(".zip")) continue;
// 统计解析覆盖。与库内是否已有同名记录无关,保证断言不受并发同步影响
totalParsed++;
kinds.Add("sdk");
osKinds.Add((OSKind)os);
archs.Add((CpuArch)arch);
// 内存去重后延迟落库
if (!keys.Add((version, "sdk", (OSKind)os, (CpuArch)arch))) continue;
pending.Add(new DotNetPackage
{
Version = version,
Kind = "sdk",
OSKind = (OSKind)os,
Architecture = (CpuArch)arch,
FileName = name,
Source = url,
FileHash = hash,
Enable = true,
Channel = NodeChannels.Release,
});
}
}
}
|