using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Security.Cryptography; using System.Text.Json; using XFEToolBox.Client.Core.Tools; using XFEToolBox.Client.Utilities; using XFEToolBox.Core.Tools; namespace XFEToolBox.Client.Wpf.Test; [NonParallel] public static class ToolchainIntegrationTests { // Explicit: first run downloads the real SDK and NuGet packages. The WPF fixture stays invisible. [Test] [Explicit] public static async Task BuildsAndRunsWpfWithoutSystemDotNet() { using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(20)); var progress = new InlineProgress(); var installation = await ToolchainManager.Default.EnsureInstalledAsync(progress, timeout.Token); var root = Path.Combine(ToolchainManager.Default.CacheRoot, "smoke-" + Guid.NewGuid().ToString("N")); var workspace = Path.Combine(root, "workspace"); Directory.CreateDirectory(Path.Combine(workspace, "Code")); var marker = Path.Combine(root, "runtime.txt"); var environment = new Dictionary { ["PATH"] = Environment.GetFolderPath(Environment.SpecialFolder.System), ["DOTNET_ROOT"] = Path.Combine(root, "missing-dotnet"), ["DOTNET_ROOT_X64"] = Path.Combine(root, "missing-dotnet"), ["DOTNET_ROOT_X86"] = Path.Combine(root, "missing-dotnet"), ["DOTNET_ROOT_ARM64"] = Path.Combine(root, "missing-dotnet"), ["DOTNET_ROOT(x86)"] = Path.Combine(root, "missing-dotnet"), ["DOTNET_MULTILEVEL_LOOKUP"] = "0", ["MSBuildSDKsPath"] = Path.Combine(root, "missing-sdk") }; var originalEnvironment = environment.Keys.ToDictionary(key => key, Environment.GetEnvironmentVariable); try { var manifest = new ToolPackageManifest { Id = "xfestudio.private-toolchain-smoke", Name = "Private runtime smoke", Version = "1.0.0", Description = "Invisible WPF compilation fixture", Author = "XFEstudio", Entry = new ToolEntryManifest { ViewXaml = "Code/Main.xaml", ViewCodeBehind = "Code/Main.xaml.cs", ViewClass = "PrivateRuntimeSmoke.Main" } }; await File.WriteAllTextAsync(Path.Combine(workspace, "manifest.json"), JsonSerializer.Serialize(manifest, new JsonSerializerOptions(JsonSerializerDefaults.Web)), timeout.Token); await File.WriteAllTextAsync(Path.Combine(workspace, "Code/Main.xaml"), """ """, timeout.Token); await File.WriteAllTextAsync(Path.Combine(workspace, "Code/Main.xaml.cs"), $$""" using System.IO; using System.Windows; using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace PrivateRuntimeSmoke; public partial class Model : ObservableObject { [ObservableProperty] private string value = "source-generator-ok"; } public partial class Main : Window { public Main() { InitializeComponent(); var model = new Model(); DataContext = model; Loaded += (_, _) => { File.WriteAllLines({{JsonSerializer.Serialize(marker)}}, new[] { typeof(object).Assembly.Location, typeof(Window).Assembly.Location, model.Value }); var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) }; timer.Tick += (_, _) => { timer.Stop(); Application.Current.Shutdown(); }; timer.Start(); }; } } """, timeout.Token); foreach (var (key, value) in environment) Environment.SetEnvironmentVariable(key, value); var build = await ToolProjectRunService.BuildAsync(workspace, manifest, timeout.Token, progress); Ensure(build.Success, build.Message); Console.WriteLine("PASS Code Studio build with unavailable system dotnet"); var preview = await ToolProjectRunService.BuildAndRunAsync(workspace, manifest, timeout.Token, progress); await VerifyRuntimeAsync(preview, marker, installation, timeout.Token); File.Delete(marker); var package = Path.Combine(root, "smoke.xfetool"); ZipFile.CreateFromDirectory(workspace, package); var sha256 = Convert.ToHexString(SHA256.HashData(await File.ReadAllBytesAsync(package, timeout.Token))); var opened = await ToolProjectRunService.BuildPackageAndRunAsync(package, manifest.Id, manifest.Version, sha256, cancellationToken: timeout.Token, progress: progress); await VerifyRuntimeAsync(opened, marker, installation, timeout.Token); Console.WriteLine("PASS Code Studio preview and downloaded package: WPF, XAML, source generator, private CLR and WPF runtime"); } finally { foreach (var (key, value) in originalEnvironment) Environment.SetEnvironmentVariable(key, value); if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } } private static async Task VerifyRuntimeAsync(ToolRunResult result, string marker, ToolchainInstallation installation, CancellationToken token) { Ensure(result.Success && result.ProcessId is not null, result.Message); using var process = Process.GetProcessById(result.ProcessId!.Value); var runtimeRoot = Path.GetDirectoryName(Path.GetDirectoryName(process.MainModule!.FileName))!; try { await process.WaitForExitAsync(token).WaitAsync(TimeSpan.FromSeconds(30), token); Ensure(process.ExitCode == 0, "工具运行失败。"); var lines = await File.ReadAllLinesAsync(marker, token); Ensure(lines.Length == 3 && lines.Take(2).All(line => line.StartsWith(installation.Root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)), "工具没有加载私有运行时:" + string.Join("; ", lines)); Ensure(lines[2] == "source-generator-ok", "NuGet 源生成器没有正常工作。"); Console.WriteLine(string.Join(Environment.NewLine, lines)); // The host also waits for a possible elevated replacement before removing the build directory. for (var attempt = 0; attempt < 100 && Directory.Exists(runtimeRoot); attempt++) await Task.Delay(100, token); Ensure(!Directory.Exists(runtimeRoot), "工具退出后未清理生成目录。"); } finally { if (!process.HasExited) { process.Kill(entireProcessTree: true); await process.WaitForExitAsync(CancellationToken.None); } } } private static void Ensure(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); } private sealed class InlineProgress : IProgress { public void Report(ToolPreparationProgress value) => Console.WriteLine(value.Message); } }