借助网络配置,控制Modbus工具的日志显示,避免卡死界面
智能大石头 authored at 2021-12-11 22:09:19
7.94 KiB
XCoder
using System.Text;
using Xunit.Abstractions;

namespace CrazyCoder.E2E;

/// <summary>全菜单冒烟巡检:从真实 UI 枚举全部工具菜单,逐个点击打开并验证窗体可加载</summary>
[Collection("CrazyCoder")]
public class SmokeTourTests
{
    readonly CrazyCoderSession _s;
    readonly ITestOutputHelper _log;

    public SmokeTourTests(CrazyCoderSession session, ITestOutputHelper log)
    {
        _s = session;
        _log = log;
    }

    sealed record ToolResult(ToolDef Tool, String Status, String Message, String Screenshot);

    [Fact]
    public void 全菜单冒烟巡检()
    {
        _s.Start();
        var tools = _s.DiscoverTools();
        Assert.True(tools.Count > 0, "未能从 UI 发现任何工具菜单");
        _log.WriteLine($"发现工具 {tools.Count} 个");

        var results = new List<ToolResult>();

        foreach (var tool in tools)
        {
            _log.WriteLine($"---- 巡检: {tool.Category}/{tool.Name}");
            results.Add(TourOne(tool));
            // 每个工具后清理现场
            try { _s.CloseAllChildren(); } catch { }
            try { _s.CloseDialogs(); } catch { }
        }

        var ok = results.Count(x => x.Status == "通过");
        var warn = results.Count(x => x.Status.StartsWith("通过("));
        var fail = results.Count(x => !x.Status.StartsWith("通过"));

        // 生成巡检报告
        var report = BuildReport(results, tools.Count, ok, warn, fail);
        var reportFile = Path.Combine(_s.OutDir, "巡检报告.md");
        File.WriteAllText(reportFile, report, new UTF8Encoding(false));
        _log.WriteLine($"报告已生成: {reportFile}");

        var sb = new StringBuilder();
        sb.AppendLine($"巡检结果:通过 {ok},警告 {warn},失败/异常 {fail}");
        foreach (var r in results.Where(x => !x.Status.StartsWith("通过")))
            sb.AppendLine($"  [{r.Status}] {r.Tool.Category}/{r.Tool.Name}:{r.Message}");

        _log.WriteLine(sb.ToString());
        Assert.True(fail == 0, "存在巡检失败项,详见巡检报告:\n" + sb);
    }

    ToolResult TourOne(ToolDef tool)
    {
        var shot = "";
        try
        {
            // 打开前先清理
            _s.CloseAllChildren();
            _s.CloseDialogs();
            _s.Start(); // 若已崩溃则重启

            var child = _s.OpenTool(tool);
            Thread.Sleep(1200); // 让窗体完成加载

            if (!_s.IsAlive)
                return new(tool, "崩溃", "打开后进程退出", shot);

            if (child == null)
            {
                shot = _s.Capture(_s.MainHandle, $"{(tool.Category)}-{tool.Name}-无窗体");
                // 检查是否弹了模态框
                var dlg = _s.TopWindows().Where(w => w.cls == "#32770").ToList();
                if (dlg.Count > 0)
                {
                    foreach (var d in dlg)
                        _s.Capture(d.hwnd, $"{(tool.Category)}-{tool.Name}-弹窗");
                    _s.CloseDialogs();
                    return new(tool, "异常弹窗", "打开后弹出对话框而非正常窗体,已自动关闭", shot);
                }
                return new(tool, "无窗体", "点击菜单后 10 秒内未出现子窗体", shot);
            }

            // 检测弹窗
            var dialogs = _s.TopWindows().Where(w => w.cls == "#32770").ToList();
            if (dialogs.Count > 0)
            {
                var dlgInfo = new StringBuilder();
                foreach (var d in dialogs)
                {
                    var f = _s.Capture(d.hwnd, $"{(tool.Category)}-{tool.Name}-弹窗");
                    dlgInfo.Append($"「{d.title}」");
                }
                _s.CloseDialogs();
                shot = _s.Capture(child, $"{tool.Category}-{tool.Name}");
                return new(tool, "通过(有弹窗)", $"窗体已打开但弹出对话框 {dlgInfo}", shot);
            }

            // 扫描窗体内容中的异常堆栈
            var content = _s.GetTexts(child);
            var marker = FindExceptionMarker(content);
            if (marker != null)
            {
                shot = _s.Capture(child, $"{tool.Category}-{tool.Name}");
                return new(tool, "通过(内容异常)", $"窗体内容出现异常「{marker}」:{Snippet(content, 120)}", shot);
            }

            shot = _s.Capture(child, $"{tool.Category}-{tool.Name}");
            return new(tool, "通过", $"子窗体「{child.Name}」正常加载", shot);
        }
        catch (Exception ex)
        {
            if (String.IsNullOrEmpty(shot)) shot = _s.Capture(_s.MainHandle, $"{tool.Category}-{tool.Name}-异常");
            return new(tool, "异常", ex.Message, shot);
        }
    }

    static String Snippet(String text, Int32 len)
    {
        text = text.Trim();
        if (text.Length <= len) return text.Replace("\r", "").Replace("\n", " ⏎ ");
        return text[..len].Replace("\r", "").Replace("\n", " ⏎ ") + "…";
    }

    /// <summary>在文本中查找异常标记</summary>
    static String? FindExceptionMarker(String? text)
    {
        if (String.IsNullOrEmpty(text)) return null;
        String[] markers =
        [
            "ReflectionTypeLoadException", "TypeLoadException", "FileNotFoundException",
            "NullReferenceException", "未将对象引用设置到对象的实例",
            "InvalidOperationException", "ArgumentException", "NotSupportedException",
            "未能加载文件", "未能加载程序集", "at System.Reflection", "at NewLife.Reflection",
        ];
        return markers.FirstOrDefault(m => text.Contains(m));
    }

    static String BuildReport(List<ToolResult> results, Int32 total, Int32 ok, Int32 warn, Int32 fail)
    {
        var sb = new StringBuilder();
        sb.AppendLine("# 码神工具 E2E 冒烟巡检报告");
        sb.AppendLine();
        sb.AppendLine($"- 巡检时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}");
        sb.AppendLine($"- 发现工具:{total} 个");
        sb.AppendLine($"- 通过:{ok} | 弹窗警告:{warn} | 失败/异常:{fail}");
        sb.AppendLine();
        sb.AppendLine("## 明细");
        sb.AppendLine();
        sb.AppendLine("| 分类 | 工具 | 结果 | 说明 | 截图 |");
        sb.AppendLine("|------|------|------|------|------|");
        foreach (var r in results.OrderBy(x => x.Tool.Category).ThenBy(x => x.Tool.Name))
        {
            var img = String.IsNullOrEmpty(r.Screenshot) ? "-" : $"[图]({Path.GetFileName(r.Screenshot)})";
            sb.AppendLine($"| {r.Tool.Category} | {r.Tool.Name} | {r.Status} | {r.Message} | {img} |");
        }
        sb.AppendLine();
        sb.AppendLine("## 问题清单");
        sb.AppendLine();
        var warns = results.Where(x => x.Status.StartsWith("通过(")).ToList();
        var issues = results.Where(x => !x.Status.StartsWith("通过")).ToList();
        if (warns.Count == 0 && issues.Count == 0)
        {
            sb.AppendLine("本次巡检未发现问题 ✅");
        }
        else
        {
            var idx = 1;
            if (warns.Count > 0)
            {
                sb.AppendLine("### 警告(可运行但有疑点)");
                sb.AppendLine();
                foreach (var r in warns)
                {
                    sb.AppendLine($"{idx}. **[{r.Status}] {r.Tool.Category}/{r.Tool.Name}**:{r.Message}  {(String.IsNullOrEmpty(r.Screenshot) ? "" : $"(截图 {Path.GetFileName(r.Screenshot)})")}");
                    idx++;
                }
            }
            if (issues.Count > 0)
            {
                sb.AppendLine("### 失败/异常");
                sb.AppendLine();
                foreach (var r in issues)
                {
                    sb.AppendLine($"{idx}. **[{r.Status}] {r.Tool.Category}/{r.Tool.Name}**:{r.Message}  {(String.IsNullOrEmpty(r.Screenshot) ? "" : $"(截图 {Path.GetFileName(r.Screenshot)})")}");
                    idx++;
                }
            }
        }
        return sb.ToString();
    }
}