Răsfoiți Sursa

feat: 测试报表优化

kindring 1 săptămână în urmă
părinte
comite
a0ae3ebd40
2 a modificat fișierele cu 352 adăugiri și 80 ștergeri
  1. 339 76
      bird_tool/TestEngine.cs
  2. 13 4
      bird_tool/bird_tool.cs

+ 339 - 76
bird_tool/TestEngine.cs

@@ -3,6 +3,7 @@
 // TestEngine.cs
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.IO.Ports;
 using System.Linq;
 using System.Text;
@@ -16,86 +17,52 @@ namespace Bird_tool
 
 
 
-    //public class TestResult
-    //{
-    //    public bool IsRunning { get; set; } = true;
-    //    public int TotalSteps => Steps.Count;
-    //    public int CompletedSteps { get; set; }
-    //    public string CurrentStepName { get; set; }
-    //    public string CurrentStepDetail { get; set; }
-    //    public int CurrentStepProgress { get; set; }
-    //    public int OverallProgress => CompletedSteps * 100 / TotalSteps;
-
-    //    public List<TestStepResult> StepResults { get; } = new List<TestStepResult>();
-    //    public List<TestStepConfig> Steps { get; set; }
-
-    //    public void StartStep(TestStepConfig step)
-    //    {
-    //        CurrentStepName = step.Name;
-    //        CurrentStepDetail = "开始执行...";
-    //        CurrentStepProgress = 0;
-
-    //        StepResults.Add(new TestStepResult
-    //        {
-    //            Name = step.Name,
-    //            Status = TestStatus.Running,
-    //            StartTime = DateTime.Now
-    //        });
-    //    }
-
-    //    public void CompleteStep(TestStatus status, string details = "")
-    //    {
-    //        if (StepResults.Count == 0) return;
-
-    //        var currentResult = StepResults.Last();
-    //        currentResult.Status = status;
-    //        currentResult.EndTime = DateTime.Now;
-    //        currentResult.Details = details;
-
-    //        if (status == TestStatus.Passed || status == TestStatus.Skipped)
-    //        {
-    //            CompletedSteps++;
-    //        }
-    //    }
-
-    //    public void FailCurrentStep(string reason)
-    //    {
-    //        CompleteStep(TestStatus.Failed, reason);
-    //        IsRunning = false;
-    //    }
-
-    //    public void MarkAsCompleted()
-    //    {
-    //        CompleteStep(TestStatus.Passed, "所有步骤完成");
-    //        IsRunning = false;
-    //    }
-
-    //    public void SkipRemaining()
-    //    {
-    //        foreach (var step in Steps.Skip(StepResults.Count))
-    //        {
-    //            StepResults.Add(new TestStepResult
-    //            {
-    //                Name = step.Name,
-    //                Status = TestStatus.Skipped,
-    //                StartTime = DateTime.Now,
-    //                EndTime = DateTime.Now
-    //            });
-    //        }
-    //        IsRunning = false;
-    //    }
-    //}
+    public class TestReport
+    {
+        public DateTime StartTime { get; set; } = DateTime.Now;
+        public DateTime EndTime { get; set; }
+        public List<TestReportItem> Items { get; set; } = new List<TestReportItem>();
+        public string DeviceInfo { get; set; }
+        public string TestResult => Items.All(i => i.Status == TestStatus.Passed) ? "PASS" : "FAIL";
+        // 设备信息专用字典
+        public Dictionary<string, string> DeviceInfoItems { get; } = new Dictionary<string, string>();
+        public string UniqueIdentifier { get; set; } = "report";
+
+        // 添加设备信息项
+        public void AddDeviceInfo(string key, string value)
+        {
+            if (!string.IsNullOrEmpty(value))
+            {
+                DeviceInfoItems[key] = value;
+            }
+        }
+    }
+
+    public class TestReportItem
+    {
+        public string GroupId { get; set; }
+        public string GroupName { get; set; }
+        public TestStatus Status { get; set; }
+        public List<TestStepResult> StepResults { get; set; } = new List<TestStepResult>();
+        public string Details => Status == TestStatus.Passed ? "所有步骤通过" : $"失败步骤: {FailedSteps}";
+        public int FailedSteps => StepResults.Count(r => r.Status == TestStatus.Failed);
+        public DateTime StartTime { get; set; }
+        public DateTime EndTime { get; set; }
+    }
 
     public class TestStepResult
     {
         public string Name { get; set; }
         public TestStatus Status { get; set; } = TestStatus.NotRun;
         public string Details { get; set; } = "";
+        public string GroupId { get; set; } = "";
+        public string GroupName { get; set; } = "";
         public DateTime StartTime { get; set; }
         public DateTime EndTime { get; set; }
         public TimeSpan Duration => EndTime - StartTime;
         public int RetryCount { get; set; }
         public string ErrorMessage { get; set; } = "";
+        
     }
 
     public enum TestStatus
@@ -215,6 +182,10 @@ namespace Bird_tool
         // 失败后是否继续测试
         public bool FailContinue { get; set; } = false;
 
+        public bool IsDeviceInfoItem { get; set; } = false;
+        public string InfoDisplayName { get; set; }
+        public string InfoDisplayTemplate { get; set; }
+
         public string Description { get; set; }
 
         public int DelayBefore { get; set; } = 0;
@@ -296,13 +267,15 @@ namespace Bird_tool
         // 当前测试项的状态
         public TestStepConfig CurrentStep => Steps?[CurrentStepIndex];
         public List<TestStepConfig> Steps { get; set; }
-        //public TestResult Result { get; set; }
+        public List<TestStepResult> StepResults { get; } = new List<TestStepResult>();
+        public TestReport Report { get; set; } = new TestReport();
         public bool IsRunning { get; set; }
     }
 
     class TestExecutor
     {
         public delegate void LogHandler(string message, LogLevel level);
+        public delegate void LogShowHandler(string message);
 
         public delegate void PromptHandler(string question, Action onYes, Action onNo, int waitTime = 60);
         public delegate bool StepValidator(string response, TestContext context);
@@ -312,6 +285,7 @@ namespace Bird_tool
 
 
         public event LogHandler OnLog;
+        public event LogShowHandler OnLogShow;
         public event PromptHandler OnPrompt;
         public event StepChangedHandler OnStepChanged;
         public event TestFailHandler OnFailed;
@@ -327,11 +301,14 @@ namespace Bird_tool
         public bool isStart { get; private set; } = false;
         public bool isInit { get; private set; } = false;
 
+        public string ReportFileNameTemplate { get; set; } = "TestReport_{{mac}}_{{timestamp}}";
+
         public TestExecutor(SerialManager serialManager)
         {
             bird_tool.Log("TestExecutor ");
             _serialManager = serialManager;
             _serialManager.OnLineReceived += HandleResponse;
+
         }
 
         // 新增方法:将重试操作加入队列异步执行
@@ -340,7 +317,7 @@ namespace Bird_tool
             OnLog?.Invoke($"排队重试操作: {operation.Method.Name}", LogLevel.debug);
             Task.Run(() =>
             {
-                OnLog?.Invoke($"开始执行排队操作: {operation.Method.Name}", LogLevel.debug);
+                OnLog?.Invoke($"开始执行排队操作1: {operation.Method.Name}", LogLevel.debug);
                 // 添加短暂延迟避免立即重试
                 Thread.Sleep(100);
                 
@@ -349,6 +326,7 @@ namespace Bird_tool
                     // 检查测试是否仍在运行
                     if (_context != null && _context.IsRunning)
                     {
+                        OnLog?.Invoke($"开始执行排队操作2: {operation.Method.Name}", LogLevel.debug);
                         operation();
                     }
                 }
@@ -358,6 +336,9 @@ namespace Bird_tool
         
         public void StopTest()
         {
+            
+            Thread.Sleep(2000);
+
             lock (_lock)
             {
                 
@@ -395,7 +376,7 @@ namespace Bird_tool
                     Steps = steps, // 使用传入的步骤列表
                     CurrentStepIndex = -1
                 };
-
+                
                 isStart = true;
 
                 MoveToNextStep();
@@ -607,20 +588,93 @@ namespace Bird_tool
         // 从特定方法开始执行
 
 
-        // 新增字符串模板替换方法
+        // 字符串模板替换方法
         public string ReplaceTemplateVariables(string input, Dictionary<string, string> variables)
         {
+            if (string.IsNullOrWhiteSpace(input))
+                return input;
+
+            // 创建大小写不敏感的合并字典
+            var mergedVars = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+            // 1. 添加用户提供的变量(不修改原始字典)
+            if (variables != null)
+            {
+                foreach (var kv in variables)
+                {
+                    mergedVars[kv.Key] = kv.Value;
+                }
+            }
+
+            // 2. 添加系统变量(仅当用户未定义时)
+            AddIfMissing(mergedVars, "timestamp", DateTime.Now.ToString("yyyyMMddHHmmss"));
+            AddIfMissing(mergedVars, "date", DateTime.Now.ToString("yyyyMMdd"));
+            AddIfMissing(mergedVars, "time", DateTime.Now.ToString("HHmmss"));
+            AddIfMissing(mergedVars, "datetime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+            AddIfMissing(mergedVars, "machine", Environment.MachineName);
+            AddIfMissing(mergedVars, "user", Environment.UserName);
+
             return Regex.Replace(input, @"\{\{(\w+)\}\}", match =>
             {
                 var varName = match.Groups[1].Value;
-                return variables.TryGetValue(varName, out var value) ? value : $"{{{{UNDEFINED:{varName}}}}}";
+
+                // 尝试各种格式的键名匹配
+                if (TryGetValueIgnoreCase(mergedVars, varName, out var value))
+                    return value;
+
+                // 尝试下划线格式(如 device_model)
+                var underscored = Regex.Replace(varName, @"([a-z])([A-Z])", "$1_$2").ToLower();
+                if (TryGetValueIgnoreCase(mergedVars, underscored, out value))
+                    return value;
+
+                // 尝试空格格式(如 Device Model)
+                var spaced = Regex.Replace(varName, @"([a-z])([A-Z])", "$1 $2");
+                if (TryGetValueIgnoreCase(mergedVars, spaced, out value))
+                    return value;
+
+                // 尝试驼峰格式(如 deviceModel)
+                var camelCase = char.ToLower(varName[0]) + varName.Substring(1);
+                if (TryGetValueIgnoreCase(mergedVars, camelCase, out value))
+                    return value;
+
+                return $"{{{{UNDEFINED:{varName}}}}}";
             });
         }
+        // 辅助方法:仅在键不存在时添加
+        private void AddIfMissing(Dictionary<string, string> dict, string key, string value)
+        {
+            if (!dict.ContainsKey(key))
+            {
+                dict[key] = value;
+            }
+        }
+
+        // 辅助方法:忽略大小写获取值
+        private bool TryGetValueIgnoreCase(Dictionary<string, string> dict, string key, out string value)
+        {
+            // 直接匹配
+            if (dict.TryGetValue(key, out value))
+                return true;
+
+            // 大小写不敏感匹配
+            var keyComparer = StringComparer.OrdinalIgnoreCase;
+            foreach (var kv in dict)
+            {
+                if (keyComparer.Equals(kv.Key, key))
+                {
+                    value = kv.Value;
+                    return true;
+                }
+            }
+
+            value = null;
+            return false;
+        }
 
         private async void ExecuteCurrentStep()
         {
             var step = _context.CurrentStep;
-
+            RecordStepResult(step, TestStatus.Running, "步骤开始执行");
             // 记录步骤开始
             OnLog?.Invoke($"开始步骤: {step.Name}", LogLevel.info);
             step.stepStatus = StepStatus.Running;
@@ -821,11 +875,13 @@ namespace Bird_tool
 
         private void HandleTestFail()
         {
+            HandleTestEnd(false);
             OnFailed?.Invoke(_context.CurrentStep, _context);
             StopTest();
         }
         private void HandleTestSuccess()
         {
+            HandleTestEnd(true);
             OnSuccess?.Invoke(_context);
             StopTest();
         }
@@ -857,10 +913,217 @@ namespace Bird_tool
             _timeoutTimer = null;
             var step = _context.CurrentStep;
             step.stepStatus = StepStatus.Success;
+            RecordStepResult(step, TestStatus.Passed, message);
+            // 捕获设备信息
+            if (step.IsDeviceInfoItem)
+            {
+                // 使用用户指定的显示名称或默认使用步骤名
+                var infoKey = !string.IsNullOrEmpty(step.InfoDisplayName)
+                    ? step.InfoDisplayName
+                    : step.Name;
+
+                // 尝试从变量中提取信息值
+                string infoValue = "";
+
+                // 情况1:步骤提取了变量
+                if (step.VariableNames.Count > 0 &&
+                    _context.Variables.TryGetValue(step.VariableNames[0], out var varValue))
+                {
+                    infoValue = varValue;
+                }
+                // 情况2:使用成功消息
+                else if (!string.IsNullOrEmpty(message))
+                {
+                    infoValue = message;
+                }
+                // 判断是否有 InfoDisplayTemplate 输出模板
+                if (!string.IsNullOrEmpty(step.InfoDisplayTemplate))
+                {
+                    infoValue = ReplaceTemplateVariables(step.InfoDisplayTemplate, _context.Variables);
+                }
+
+                _context.Report.AddDeviceInfo(infoKey, infoValue);
+            }
+
+            RecordStepResult(step, TestStatus.Passed, message);
+
             OnStepChanged?.Invoke(step, _context, false);
             OnLog?.Invoke($"✅ 步骤完成: {step.Name} ({message})", LogLevel.info);
             MoveToNextStep();
         }
+        // 修改HTML生成方法,添加设备信息展示
+        private void SaveReportToHtml(TestReport report)
+        {
+            // 生成设备信息表格
+            string deviceInfoHtml = "";
+            if (report.DeviceInfoItems.Count > 0)
+            {
+                deviceInfoHtml = $@"
+                <h2>设备信息</h2>
+                <table class='device-info'>
+                    {string.Join("\n", report.DeviceInfoItems.Select(kv => $@"
+                    <tr>
+                        <td><strong>{kv.Key}</strong></td>
+                        <td>{kv.Value}</td>
+                    </tr>"))}
+                </table>";
+                }
+
+                var html = $@"
+                <!DOCTYPE html>
+                <html>
+                <head>
+                    <title>设备测试报告</title>
+                    <style>
+                        /* ... 现有样式 ... */
+                        .device-info td:first-child {{ width: 30%; font-weight: bold; }}
+                        .device-info td {{ vertical-align: top; padding: 6px 8px; }}
+                    </style>
+                </head>
+                <body>
+                    <h1>设备测试报告</h1>
+                    <p><strong>测试结果:</strong> <span class='{report.TestResult.ToLower()}'>{
+                            report.TestResult}</span></p>
+                    <p><strong>开始时间:</strong> {report.StartTime:yyyy-MM-dd HH:mm:ss}</p>
+                    <p><strong>结束时间:</strong> {report.EndTime:yyyy-MM-dd HH:mm:ss}</p>
+            
+               
+                    {deviceInfoHtml}
+            
+                    <h2>测试项汇总</h2>
+                    <table>
+                        <tr>
+                            <th>测试项</th>
+                            <th>状态</th>
+                            <th>开始时间</th>
+                            <th>结束时间</th>
+                            <th>详情</th>
+                        </tr>
+                        {string.Join("\n", report.Items.Select(i => $@"
+                        <tr class='{i.Status.ToString().ToLower()}'>
+                            <td>{i.GroupName}</td>
+                            <td>{i.Status}</td>
+                            <td>{i.StartTime:HH:mm:ss}</td>
+                            <td>{i.EndTime:HH:mm:ss}</td>
+                            <td>{i.Details}</td>
+                        </tr>"))}
+                    </table>
+            
+                    <h2>详细步骤</h2>
+                    <table>
+                        <tr>
+                            <th>步骤名称</th>
+                            <th>所属测试项</th>
+                            <th>状态</th>
+                            <th>开始时间</th>
+                            <th>结束时间</th>
+                            <th>详情</th>
+                        </tr>
+                        {string.Join("\n", report.Items.SelectMany(i => i.StepResults.Select(s => $@"
+                        <tr class='{s.Status.ToString().ToLower()}'>
+                            <td>{s.Name}</td>
+                            <td>{i.GroupName}</td>
+                            <td>{s.Status}</td>
+                            <td>{s.StartTime:HH:mm:ss}</td>
+                            <td>{s.EndTime:HH:mm:ss}</td>
+                            <td>{s.Details}</td>
+                        </tr>")))}
+                    </table>
+                </body>
+                </html>";
+            Directory.CreateDirectory("TestReport");
+            var fileName = GetSafeFileName("TestReport");
+            File.WriteAllText(fileName, html);
+            string str = $"测试报告已生成: {Path.GetFullPath(fileName)}";
+            OnLog?.Invoke(str, LogLevel.info);
+            OnLogShow?.Invoke(str);
+            
+        }
+
+        public string GetSafeFileName(string dir, string extension = "html")
+        {
+            // 移除非法字符
+            var invalidChars = Path.GetInvalidFileNameChars();
+            var safeName = ReplaceTemplateVariables(ReportFileNameTemplate, _context.Variables);
+
+            // 替换空格
+            // 替换空格和特殊字符
+            safeName = safeName.Replace(" ", "_")
+                               .Replace(":", "-")
+                               .Replace("/", "-");
+            
+            // 确保长度合理
+            if (safeName.Length > 100)
+                safeName = safeName.Substring(0, 100);
+
+            // 确保非空
+            if (string.IsNullOrWhiteSpace(safeName))
+                safeName = "TestReport";
+
+            return $"{dir}/{safeName}.{extension}";
+        }
+
+        private void RecordStepResult(TestStepConfig step, TestStatus status, string message = "")
+        {
+            var result = new TestStepResult
+            {
+                Name = step.Name,
+                Status = status,
+                Details = message,
+                StartTime = DateTime.Now,
+                EndTime = DateTime.Now,
+                GroupId = step.EffectiveGroupId,
+                GroupName = step.GroupName
+            };
+
+            _context.StepResults.Add(result);
+        }
+
+        private void GenerateTestReport()
+        {
+            
+            // 按GroupId分组聚合结果
+            var groupedResults = _context.StepResults
+                .Where(r => !string.IsNullOrEmpty(r.GroupId))
+                .GroupBy(r => r.GroupId)
+                .Select(g => new TestReportItem
+                {
+                    GroupId = g.Key,
+                    GroupName = g.First().GroupName,
+                    Status = g.Any(r => r.Status == TestStatus.Failed) ?
+                             TestStatus.Failed : TestStatus.Passed,
+                    StepResults = g.ToList(),
+                    StartTime = g.Min(r => r.StartTime),
+                    EndTime = g.Max(r => r.EndTime)
+                })
+                .ToList();
+
+            // 添加未分组步骤
+            var ungroupedResults = _context.StepResults
+                .Where(r => string.IsNullOrEmpty(r.GroupId))
+                .Select(r => new TestReportItem
+                {
+                    GroupId = "single_" + r.Name,
+                    GroupName = r.Name,
+                    Status = r.Status,
+                    StepResults = new List<TestStepResult> { r },
+                    StartTime = r.StartTime,
+                    EndTime = r.EndTime
+                });
+
+            _context.Report.Items.AddRange(groupedResults);
+            _context.Report.Items.AddRange(ungroupedResults);
+            _context.Report.EndTime = DateTime.Now;
+        }
+        // 在测试结束时调用生成报告
+        private void HandleTestEnd(bool saveReport)
+        {
+            GenerateTestReport();
+            if (saveReport)
+            {
+                SaveReportToHtml(_context.Report);
+            }
+        }
 
         private void HandleStepFailure(string message, bool isQuestion = false)
         {
@@ -878,7 +1141,7 @@ namespace Bird_tool
             string failStr = $"【{step.Name}】失败 {message} (重试 {step.RetryCount}/{step.MaxRetries})";
             
             OnLog?.Invoke(failStr, LogLevel.info);
-            
+            RecordStepResult(step, TestStatus.Failed, message);
             LogLevel level = LogLevel.info;
             // 判断是跳转到特定任务继续执行还是走失败的逻辑
             bool isJump = false;

+ 13 - 4
bird_tool/bird_tool.cs

@@ -324,6 +324,7 @@ namespace Bird_tool
 
             // 绑定事件处理
             _testExecutor.OnLog += Log;
+            _testExecutor.OnLogShow += Log_show;
 
             _testExecutor.OnPrompt += (question, onYes, onNo, waitTime) =>
                 this.Invoke((Action)(() => PromptUser(question, onYes, onNo, waitTime)));
@@ -901,10 +902,11 @@ namespace Bird_tool
 
             return new List<TestStepConfig>
             {
-                
 
-                
+
+
                 // 按钮长按功能测试
+                /**
                 new TestStepConfig
                 {
                     GroupId = "btn_long",
@@ -928,6 +930,7 @@ namespace Bird_tool
                     DelayBefore = 1000,
                     MaxRetries = 3,
                 },
+                */
 
                 // 按钮双击功能测试
                 new TestStepConfig
@@ -1091,6 +1094,8 @@ namespace Bird_tool
                 {
                     Name = "获取MAC地址",
                     Tips = "提取设备MAC地址",
+                    IsDeviceInfoItem = true,
+                    InfoDisplayName = "设备mac地址",
                     Command = "AT+MAC?\r\n",
                     SuccessPattern = "MAC:",
                     FailurePattern = "ERROR|FAIL",
@@ -1112,6 +1117,9 @@ namespace Bird_tool
                 {
                     Name = "获取版本信息",
                     Tips = "设备版本检查地址",
+                    IsDeviceInfoItem = true,
+                    InfoDisplayName = "设备固件",
+                    InfoDisplayTemplate = "{{hwVersion}}{{hwTime}}",
                     FailTips = "设备固件异常, 版本号:{{hwVersion}}{{hwTime}}",
                     Command = "AT+SEND=1, AT+HWCUST?\r\n",
                     SuccessPattern = @"\+HWCUST:",
@@ -1141,7 +1149,6 @@ namespace Bird_tool
                     Command = "AT+SEND=1, AT+SDHCI=1\r\n",
                     Tips = "请确保SD卡插入",
                     SuccessPattern = "<=OK cmd:20",
-                    FailurePattern = "ERROR|FAIL",
                     Timeout = 4000,
                     DelayBefore = 300,
                     FailContinue = true,
@@ -1160,6 +1167,8 @@ namespace Bird_tool
                     MaxJump = 5,
                 },
 
+                // 音频测试
+                /**
                 new TestStepConfig
                 {
                     GroupId = "Audio_test",
@@ -1186,7 +1195,6 @@ namespace Bird_tool
                 new TestStepConfig
                 {
                     GroupId = "Audio_test",
-
                     Name = "录制视频",
                     Command = "AT+SEND=1,AT+CAMRV=0\\\\,\"test\"\r\n",
                     Tips = "视频录制中 请对着麦克风持续讲话",
@@ -1222,6 +1230,7 @@ namespace Bird_tool
                         // 失败时重新录制
                     }
                 },
+                */
                 
                 // 补光灯测试
                 new TestStepConfig