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.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using XFEToolBox.Client.Core.Search;
using XFEToolBox.Client.Utilities;
using XFEToolBox.Client.ViewModel.Pages;

namespace XFEToolBox.Client.Wpf.Test;

public static class ShellFileIconTests
{
    [Test]
    public static async Task FileIconsCoalesceAssociationRequestsAndOnlyUseDedicatedStaWorkers()
    {
        var callerThread = Environment.CurrentManagedThreadId;
        var calls = 0;
        using var entered = new ManualResetEventSlim();
        using var release = new ManualResetEventSlim();
        using var service = new ShellFileIconService(1, 8, 4, (path, directory, attributes) =>
        {
            Ensure(Environment.CurrentManagedThreadId != callerThread && !Thread.CurrentThread.IsThreadPoolThread &&
                Thread.CurrentThread.GetApartmentState() == ApartmentState.STA,
                "Shell 图标加载占用了调用者/线程池,或未使用专用 STA 线程。");
            Ensure(path == "file.txt" && !directory && attributes, "普通文档没有使用属性查询的扩展名关联图标。");
            Interlocked.Increment(ref calls);
            entered.Set();
            release.Wait(TimeSpan.FromSeconds(5));
            return TestImage();
        });
        var first = service.GetIconAsync(@"C:\first.txt", false);
        Ensure(entered.Wait(TimeSpan.FromSeconds(3)), "图标工作线程未开始。");
        var repeated = Enumerable.Range(0, 100).Select(index => service.GetIconAsync($@"C:\folder{index}\second.TXT", false)).ToArray();
        Ensure(service.Counts.Pending == 1, "同类型文件重复排队获取 Shell 图标。");
        release.Set();
        var all = await Task.WhenAll(repeated.Prepend(first));
        Ensure(calls == 1 && all.All(image => image is { IsFrozen: true } && ReferenceEquals(image, all[0])),
            "合并请求没有共用冻结后的跨线程图标。");
        Ensure(ReferenceEquals(await service.GetIconAsync(@"D:\third.txt", false), all[0]) && calls == 1,
            "扩展名关联图标没有命中共享缓存。");
    }

    [Test]
    public static async Task FileIconQueueAndCacheStayBoundedAndCancelledRowsDoNotCallShell()
    {
        var calls = 0;
        var order = new List<string>();
        using var entered = new ManualResetEventSlim();
        using var release = new ManualResetEventSlim();
        using var service = new ShellFileIconService(1, 8, 4, (path, _, _) =>
        {
            lock (order) order.Add(path);
            if (Interlocked.Increment(ref calls) == 1)
            {
                entered.Set();
                release.Wait(TimeSpan.FromSeconds(5));
            }
            return TestImage();
        });
        var active = service.GetIconAsync(@"C:\active.exe", false);
        Ensure(entered.Wait(TimeSpan.FromSeconds(3)), "图标工作线程未开始。");
        var obsolete = new List<Task<ImageSource?>>();
        for (var index = 0; index < 1000; index++) obsolete.Add(service.GetIconAsync($@"C:\old{index}.exe", false));
        Ensure(service.Counts.Queued == 8 && service.Counts.Pending <= 9, "快速输入导致图标请求无限堆积。");
        Ensure(obsolete.Take(992).All(task => task.IsCompletedSuccessfully && task.Result is null),
            "超出队列容量的旧图标请求没有及时释放。");
        using var cancellation = new CancellationTokenSource();
        var cancelled = service.GetIconAsync(@"C:\cancelled.ico", false, cancellation.Token);
        cancellation.Cancel();
        Ensure(await cancelled is null, "取消图标请求发生异常或继续等待 Shell。");
        var latest = service.GetIconAsync(@"C:\latest.lnk", false);
        release.Set();
        Ensure(await active is not null && await latest is not null, "最新图标请求没有完成。");
        await Task.WhenAll(obsolete);
        Ensure(order.Count >= 2 && order[1] == @"C:\latest.lnk", "最新可见请求没有优先于积压的旧图标请求执行。");
        Ensure(calls <= 9 && service.Counts.Cached <= 4, "图标 Shell 调用数量或缓存超出上限。");
        var cached = await service.GetIconAsync(@"C:\latest.lnk", false);
        Ensure(cached is { IsFrozen: true }, "路径专用图标没有进入冻结缓存。");
    }

    [Test]
    public static void DroppedIconRequestCanRetryWhenTheSameResultIsRealizedAgain()
    {
        RunSta(() =>
        {
            using var entered = new ManualResetEventSlim();
            using var release = new ManualResetEventSlim();
            using var service = new ShellFileIconService(1, 1, 4, (path, _, _) =>
            {
                if (path == @"C:\blocking.exe")
                {
                    entered.Set();
                    release.Wait(TimeSpan.FromSeconds(5));
                }
                return TestImage();
            });
            var blocking = service.GetIconAsync(@"C:\blocking.exe", false);
            Ensure(entered.Wait(TimeSpan.FromSeconds(3)), "图标工作线程未开始。");
            using var row = new FileSearchResultViewModel(new FileSearchMatch(new FileSearchEntry(@"C:\retry.exe", false), 0), service);
            Ensure(row.IconSource is null && service.Counts.Queued == 1, "首次图标请求未排队。");
            var newer = service.GetIconAsync(@"C:\newer.exe", false); // Bounded queue evicts the row's first request.
            var watch = Stopwatch.StartNew();
            while (!newer.IsCompleted && watch.Elapsed < TimeSpan.FromSeconds(3))
            {
                Pump();
                _ = row.IconSource; // A later realization/binding re-reads the same VM's icon property.
                Thread.Sleep(1);
            }
            Ensure(newer.IsCompletedSuccessfully && newer.Result is null && row.IconSource is null && service.Counts.Queued == 1,
                "被队列丢弃的请求标记未重置,同一条目重新实现绑定后没有重试图标请求。");
            release.Set();
            watch.Restart();
            while (!row.HasIcon && watch.Elapsed < TimeSpan.FromSeconds(3))
            {
                Pump();
                Thread.Sleep(1);
            }
            Ensure(row.IconSource is { IsFrozen: true }, "同一条目被丢弃的图标请求重试后仍永久显示 glyph。");
            Ensure(blocking.IsCompletedSuccessfully && newer.IsCompletedSuccessfully && newer.Result is null,
                "重新绑定后的最新请求没有替换旧排队请求。");
        });
    }

    [Test]
    public static void SearchResultIconsAreLazyAndNotifyTheUiWithoutBlockingIt()
    {
        RunSta(() =>
        {
            var calls = 0;
            using var entered = new ManualResetEventSlim();
            using var release = new ManualResetEventSlim();
            using var service = new ShellFileIconService(1, 8, 4, (_, _, _) =>
            {
                Interlocked.Increment(ref calls);
                entered.Set();
                release.Wait(TimeSpan.FromSeconds(5));
                return TestImage();
            });
            var watch = Stopwatch.StartNew();
            var rows = Enumerable.Range(0, 10000).Select(index => new FileSearchResultViewModel(
                new FileSearchMatch(new FileSearchEntry($@"C:\not-read-{index}.txt", false), 0), service)).ToArray();
            Ensure(calls == 0 && service.Counts.Pending == 0, "构建搜索结果提前读取了所有文件图标。");
            var uiThread = Environment.CurrentManagedThreadId;
            var notificationThread = 0;
            rows[0].PropertyChanged += (_, arguments) =>
            {
                if (arguments.PropertyName == nameof(FileSearchResultViewModel.IconSource))
                    notificationThread = Environment.CurrentManagedThreadId;
            };
            watch.Restart();
            Ensure(rows[0].IconSource is null && watch.Elapsed < TimeSpan.FromMilliseconds(100),
                "首次可见图标绑定阻塞了 UI 线程。");
            Ensure(entered.Wait(TimeSpan.FromSeconds(3)), "可见条目没有请求图标。");
            var pulse = false;
            Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => pulse = true));
            Pump();
            Ensure(pulse && !rows[0].HasIcon, "后台图标读取阻塞了 UI Dispatcher。");
            release.Set();
            watch.Restart();
            while (!rows[0].HasIcon && watch.Elapsed < TimeSpan.FromSeconds(3))
            {
                Pump();
                Thread.Sleep(1);
            }
            Ensure(rows[0].IconSource is { IsFrozen: true } && notificationThread == uiThread,
                "图标属性更新没有异步返回原 UI Dispatcher。");
            foreach (var row in rows) row.Dispose();
        });
    }

    [Test]
    public static void RealWindowsShellReturnsDocumentFolderExecutableAndShortcutIcons()
    {
        RunSta(() =>
        {
            var root = Path.Combine(Path.GetTempPath(), "xfetoolbox-shell-icons-" + Guid.NewGuid().ToString("N"));
            var shortcutPath = Path.Combine(root, "notepad.lnk");
            object? shell = null;
            object? shortcut = null;
            try
            {
                Directory.CreateDirectory(root);
                var executable = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "notepad.exe");
                Ensure(File.Exists(executable), "本机记事本可执行文件不存在。");
                shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell", throwOnError: true)!);
                shortcut = ((dynamic)shell!).CreateShortcut(shortcutPath);
                ((dynamic)shortcut).TargetPath = executable;
                ((dynamic)shortcut).Save();
                using var service = new ShellFileIconService();
                var folder = service.GetIconAsync(root, true).GetAwaiter().GetResult();
                var document = service.GetIconAsync(Path.Combine(root, "example.txt"), false).GetAwaiter().GetResult();
                var exe = service.GetIconAsync(executable, false).GetAwaiter().GetResult();
                var link = service.GetIconAsync(shortcutPath, false).GetAwaiter().GetResult();
                Ensure(new[] { folder, document, exe, link }.All(image => image is BitmapSource { IsFrozen: true, PixelWidth: >= 32 }),
                    "Windows Shell 文件/文件夹/EXE/快捷方式图标缺失、未冻结或尺寸过小。");
                Ensure(ReferenceEquals(document, service.GetIconAsync(Path.Combine(root, "other.TXT"), false).GetAwaiter().GetResult()) &&
                    !ReferenceEquals(exe, link), "类型共享缓存或路径专用快捷方式图标错误。");
            }
            finally
            {
                if (shortcut is not null) Marshal.FinalReleaseComObject(shortcut);
                if (shell is not null) Marshal.FinalReleaseComObject(shell);
                if (File.Exists(shortcutPath)) File.Delete(shortcutPath);
                if (Directory.Exists(root)) Directory.Delete(root);
            }
        });
    }

    private static ImageSource TestImage() => new DrawingImage(new GeometryDrawing(Brushes.CornflowerBlue, null,
        new RectangleGeometry(new Rect(0, 0, 32, 32))));

    private static void RunSta(Action action)
    {
        Exception? failure = null;
        var thread = new Thread(() =>
        {
            try
            {
                SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
                action();
            }
            catch (Exception exception) { failure = exception; }
            finally { Dispatcher.CurrentDispatcher.InvokeShutdown(); }
        }) { IsBackground = true };
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        Ensure(thread.Join(TimeSpan.FromSeconds(20)), "Shell 图标 STA 回归测试超时。");
        if (failure is not null) throw new InvalidOperationException("Shell 文件图标回归失败:" + failure.Message, failure);
    }

    private static void Pump()
    {
        var frame = new DispatcherFrame();
        Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => frame.Continue = false));
        Dispatcher.PushFrame(frame);
    }

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