XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Windows.Threading;
using XFEToolBox.Client.Utilities;
using XFEToolBox.Client.Views.Windows;

namespace XFEToolBox.Client.Wpf.Test;

public static class QuickSearchResponsivenessTests
{
    [Test]
    public static async Task CachedPrefixCanBeQueriedBeforeRuntimeFinishesRestoring()
    {
        const int entryCount = 250_000;
        var root = Path.Combine(Path.GetTempPath(), "xfetoolbox-runtime-prefix-test-" + Guid.NewGuid().ToString("N"));
        var scope = Path.Combine(root, "scope");
        var cache = Path.Combine(root, "cache");
        var target = Path.Combine(scope, "cacheprefixneedle.txt");
        try
        {
            await Task.Run(() =>
            {
                Directory.CreateDirectory(scope);
                Directory.CreateDirectory(cache);
                File.WriteAllText(target, "prefix result");
                File.WriteAllText(Path.Combine(cache, "settings.json"), JsonSerializer.Serialize(new { Folder = scope }));
                using var file = File.Create(Path.Combine(cache, "files.v1.gz"));
                using var compressed = new GZipStream(file, CompressionLevel.Fastest);
                using var writer = new BinaryWriter(compressed, Encoding.UTF8);
                writer.Write(0x58464958);
                writer.Write(1);
                WriteCacheString(writer, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(scope.ToUpperInvariant()))));
                writer.Write(entryCount);
                WriteCacheString(writer, target);
                writer.Write((byte)0);
                for (var index = 1; index < entryCount; index++)
                {
                    WriteCacheString(writer, Path.Combine(scope, $"cached-project-{index / 500:D4}", $"report-{index:D8}-2026.txt"));
                    writer.Write((byte)0);
                }
            });
            await using var runtime = new FileSearchRuntime(cache);
            var elapsed = Stopwatch.StartNew();
            await runtime.InitializeAsync();
            Ensure(runtime.IsRestoringIndex && runtime.Status?.IsScanning == true,
                "运行时初始化等待了完整缓存恢复,无法立即提供前缀搜索。");
            while (runtime.Status?.IndexedCount == 0 && elapsed.Elapsed < TimeSpan.FromSeconds(10))
                await Task.Delay(1);
            Ensure(runtime.Status is { IndexedCount: > 0 and < entryCount }, "缓存没有分批发布可搜索条目。");
            var queryElapsed = Stopwatch.StartNew();
            var page = await runtime.SearchAsync("cacheprefixneedle", includeContent: false, limit: 10);
            Ensure(page.Items.Any(item => string.Equals(item.Entry.FullPath, target, StringComparison.OrdinalIgnoreCase)),
                "缓存恢复期间不能搜索已经发布的前缀条目。");
            Ensure(runtime.IsRestoringIndex && runtime.Status?.IndexedCount < entryCount,
                "搜索仍等待完整缓存恢复之后才返回结果。");
            Console.WriteLine($"Runtime cached prefix available at {elapsed.Elapsed.TotalMilliseconds:F1} ms; query {queryElapsed.Elapsed.TotalMilliseconds:F1} ms; hydrated {runtime.Status!.IndexedCount:N0}/{entryCount:N0}.");
        }
        finally
        {
            // Only known fixture files exist; avoid recursive deletion and validate the exact parent.
            var resolved = Path.GetFullPath(root);
            Ensure(Path.GetDirectoryName(resolved) == Path.TrimEndingDirectorySeparator(Path.GetFullPath(Path.GetTempPath())) &&
                Path.GetFileName(resolved).StartsWith("xfetoolbox-runtime-prefix-test-", StringComparison.Ordinal), "Unexpected cache fixture cleanup target.");
            if (File.Exists(target)) File.Delete(target);
            if (Directory.Exists(cache))
            {
                foreach (var file in Directory.EnumerateFiles(cache, "*", SearchOption.TopDirectoryOnly)) File.Delete(file);
                Directory.Delete(cache);
            }
            if (Directory.Exists(scope)) Directory.Delete(scope);
            if (Directory.Exists(resolved)) Directory.Delete(resolved);
        }
    }

    private static void WriteCacheString(BinaryWriter writer, string value)
    {
        var bytes = Encoding.UTF8.GetBytes(value);
        writer.Write(bytes.Length);
        writer.Write(bytes);
    }

    [Test]
    public static void CancellingPaletteSearchDoesNotRunWorkerCleanupOnUiThread()
    {
        Exception? failure = null;
        var thread = new Thread(() =>
        {
            try
            {
                SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
                // A cancellation callback can interrupt SQLite or release filesystem resources.
                // Make that cleanup wait deterministically so a synchronous Cancel would freeze input.
                using var workerRelease = new ManualResetEventSlim();
                using var workerStarted = new ManualResetEventSlim();
                using var workerFinished = new ManualResetEventSlim();
                using var cancellation = new CancellationTokenSource();
                var uiThreadId = Environment.CurrentManagedThreadId;
                var cleanupThreadId = 0;
                using var registration = cancellation.Token.Register(() =>
                {
                    cleanupThreadId = Environment.CurrentManagedThreadId;
                    workerStarted.Set();
                    workerRelease.Wait(TimeSpan.FromSeconds(2));
                    workerFinished.Set();
                });
                // No palette is shown and no runtime API is called: the test never starts an all-drive scan.
                var runtime = new FileSearchRuntime(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "xfetoolbox-search-ui-test-unused"));
                var window = new CommandPaletteWindow(runtime);
                typeof(CommandPaletteWindow).GetField("searchCancellation", BindingFlags.NonPublic | BindingFlags.Instance)!
                    .SetValue(window, cancellation);
                var elapsed = Stopwatch.StartNew();
                try
                {
                    typeof(CommandPaletteWindow).GetMethod("CancelSearch", BindingFlags.NonPublic | BindingFlags.Instance)!
                        .Invoke(window, null);
                }
                finally { workerRelease.Set(); }
                elapsed.Stop();
                Ensure(workerStarted.Wait(TimeSpan.FromSeconds(5)) && workerFinished.Wait(TimeSpan.FromSeconds(5)),
                    "搜索取消没有通知工作线程完成清理。");
                window.Close();
                runtime.DisposeAsync().AsTask().GetAwaiter().GetResult();
                Ensure(elapsed.Elapsed < TimeSpan.FromMilliseconds(250),
                    $"取消搜索在 UI 线程等待了清理回调:{elapsed.Elapsed.TotalMilliseconds:F1} ms。");
                Ensure(cleanupThreadId != uiThreadId, "搜索取消清理仍在界面线程执行。");
            }
            catch (Exception error) { failure = error; }
            finally { Dispatcher.CurrentDispatcher.InvokeShutdown(); }
        }) { IsBackground = true };
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        Ensure(thread.Join(TimeSpan.FromSeconds(15)), "搜索取消响应测试超时。");
        if (failure is not null) throw new InvalidOperationException("搜索取消仍可能阻塞输入。", failure);
    }

    private static void Ensure(bool condition, string message)
    {
        if (!condition) throw new InvalidOperationException(message);
    }
}