diff --git a/ADP/App.xaml.cs b/ADP/App.xaml.cs index 6537b9d..855ec00 100644 --- a/ADP/App.xaml.cs +++ b/ADP/App.xaml.cs @@ -206,6 +206,7 @@ namespace ADP //注册服务 containerRegistry.Register(); containerRegistry.Register(); + containerRegistry.Register(); } //指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中) protected override IModuleCatalog CreateModuleCatalog() diff --git a/Common/Tool/ExpressionEvaluator.cs b/Common/Tool/ExpressionEvaluator.cs index 7b0bf17..6ec9efa 100644 --- a/Common/Tool/ExpressionEvaluator.cs +++ b/Common/Tool/ExpressionEvaluator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -23,7 +24,7 @@ namespace Common.Tools { foreach (var kvp in processedVariables) { - expr.Parameters[kvp.Key] = kvp.Value; + expr.Parameters[kvp.Key] = NormalizeValue(kvp.Value); } } @@ -95,6 +96,29 @@ namespace Common.Tools return (processedExpression, newVariables); } + /// + /// 归一化变量取值:输入变量的值常以字符串形式保存(如 "10"), + /// 若直接参与比较,NCalc 会按字符串逐位比较("10" > "5" 为假), + /// 导致大于/小于判断失真。此处将可解析为数字/布尔的字符串转换为对应类型, + /// 使比较按数值语义进行;无法解析的字符串保持原样。 + /// + private static object? NormalizeValue(object? value) + { + if (value is string s) + { + if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) + || double.TryParse(s, out d)) + { + return d; + } + if (bool.TryParse(s, out var b)) + { + return b; + } + } + return value; + } + // 检查字符串是否包含中文字符 private static bool ContainsChinese(string text) { diff --git a/ExportModule/ViewModels/ExportViewModel.cs b/ExportModule/ViewModels/ExportViewModel.cs index 962b200..a4af42f 100644 --- a/ExportModule/ViewModels/ExportViewModel.cs +++ b/ExportModule/ViewModels/ExportViewModel.cs @@ -87,11 +87,13 @@ namespace ExportModule.ViewModels #region 私有字段 private readonly ITestReportService _testReportService; + private readonly ITestCheckRecordService _testCheckRecordService; #endregion public ExportViewModel(IContainerProvider containerProvider) : base(containerProvider) { _testReportService = containerProvider.Resolve(); + _testCheckRecordService = containerProvider.Resolve(); LoadedCommand = new AsyncDelegateCommand(OnLoad); QueryCommand = new AsyncDelegateCommand(OnQuery); @@ -185,9 +187,38 @@ namespace ExportModule.ViewModels await Task.Run(() => ExportToExcel(dialog.FileName, entities)); ShowInfoMessageBox($"导出完成,共 {entities.Count} 条步骤记录,已保存至:{dialog.FileName}", () => { }); } + /// + /// 导出测试报告:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细 + /// private async Task OnExportReportCommand() { - + if (SelectedTestReport == null) + { + ShowErrorMessageBox("请先在列表中选择一条测试记录。", () => { }); + return; + } + + StatusMessage = "正在查询测试项判断记录..."; + var checkResult = await _testCheckRecordService.GetByTestRoundIdAsync(SelectedTestReport.TestRoundId); + if (!checkResult.IsSuccess || checkResult.Data == null || checkResult.Data.Count == 0) + { + ShowErrorMessageBox("未找到该测试记录的测试项判断数据(需将子程序标记为测试项后运行)。", () => { }); + return; + } + + var dialog = new SaveFileDialog + { + Filter = "Excel 工作簿 (*.xlsx)|*.xlsx|所有文件 (*.*)|*.*", + DefaultExt = ".xlsx", + FileName = $"测试报告_{SelectedTestReport.Scope}_{SelectedTestReport.StartTime:yyyyMMdd_HHmmss}.xlsx" + }; + + if (dialog.ShowDialog() != true) return; + + var records = checkResult.Data; + StatusMessage = $"正在导出 {records.Count} 条测试项判断记录..."; + await Task.Run(() => ExportReportToExcel(dialog.FileName, records, SelectedTestReport)); + ShowInfoMessageBox($"导出完成,已保存至:{dialog.FileName}", () => { }); } #endregion @@ -269,6 +300,134 @@ namespace ExportModule.ViewModels workbook.SaveAs(filePath); } + /// + /// 测试报告导出:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细(含重复判断、判断时间、数据值) + /// + private static void ExportReportToExcel(string filePath, List records, TestReportModel report) + { + using var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("测试报告"); + const int colCount = 6; + + var passColor = XLColor.FromArgb(0xB2, 0xFF, 0xB2); // 通过 - 浅绿(与现有导出配色一致) + var ngColor = XLColor.FromArgb(0xFF, 0xB2, 0xB2); // 失败 - 浅红 + var headerColor = XLColor.FromArgb(0xEC, 0xEF, 0xF4); + + int row = 1; + + // ===== 标题与信息行 ===== + ws.Range(row, 1, row, colCount).Merge(); + ws.Cell(row, 1).Value = "测 试 报 告"; + ws.Range(row, 1, row, colCount).Style.Font.Bold = true; + ws.Range(row, 1, row, colCount).Style.Font.FontSize = 16; + ws.Range(row, 1, row, colCount).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + row++; + ws.Cell(row, 1).Value = $"台架:{report.Scope ?? ""}"; + ws.Cell(row, 3).Value = $"测试文件:{report.FileName ?? ""}"; + row++; + ws.Cell(row, 1).Value = $"开始时间:{report.StartTime:yyyy-MM-dd HH:mm:ss}"; + ws.Cell(row, 3).Value = $"结束时间:{report.EndTime:yyyy-MM-dd HH:mm:ss}"; + row += 2; + + // 按测试项分组(保持首次出现顺序) + var groups = records.GroupBy(r => r.TestItemName).ToList(); + + // ===== 上层:测试项汇总 ===== + ws.Cell(row, 1).Value = "【测试项汇总】"; + ws.Cell(row, 1).Style.Font.Bold = true; + row++; + + string[] summaryHeaders = { "序号", "测试项", "判定结果", "执行次数", "判定次数", "NG次数" }; + for (int c = 0; c < summaryHeaders.Length; c++) + ws.Cell(row, c + 1).Value = summaryHeaders[c]; + var headerRange = ws.Range(row, 1, row, colCount); + headerRange.Style.Font.Bold = true; + headerRange.Style.Fill.BackgroundColor = headerColor; + headerRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + headerRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + row++; + + int seq = 1; + foreach (var group in groups) + { + var summaries = group.Where(x => x.IsSummary).ToList(); + var details = group.Where(x => !x.IsSummary).ToList(); + bool overallPass = summaries.All(s => s.Pass) && details.All(d => d.Pass); + + ws.Cell(row, 1).Value = seq++; + ws.Cell(row, 2).Value = group.Key; + ws.Cell(row, 3).Value = overallPass ? "PASS" : "NG"; + ws.Cell(row, 4).Value = summaries.Count; + ws.Cell(row, 5).Value = details.Count; + ws.Cell(row, 6).Value = details.Count(d => !d.Pass); + + var dataRange = ws.Range(row, 1, row, colCount); + dataRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + dataRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + ws.Cell(row, 3).Style.Font.Bold = true; + ws.Cell(row, 3).Style.Fill.BackgroundColor = overallPass ? passColor : ngColor; + row++; + } + row++; + + // ===== 下层:判断明细 ===== + ws.Cell(row, 1).Value = "【判断明细】"; + ws.Cell(row, 1).Style.Font.Bold = true; + row++; + + string[] detailHeaders = { "序号", "判定时间", "步骤名称", "OKExpression", "判定结果", "数据值" }; + foreach (var group in groups) + { + var details = group.Where(x => !x.IsSummary).ToList(); + bool overallPass = group.Where(x => x.IsSummary).All(s => s.Pass) && details.All(d => d.Pass); + + // 测试项块标题行(合并单元格) + ws.Range(row, 1, row, colCount).Merge(); + ws.Cell(row, 1).Value = $"■ 测试项:{group.Key}  总体结果:{(overallPass ? "PASS" : "NG")}"; + ws.Cell(row, 1).Style.Font.Bold = true; + ws.Range(row, 1, row, colCount).Style.Fill.BackgroundColor = overallPass ? passColor : ngColor; + row++; + + for (int c = 0; c < detailHeaders.Length; c++) + ws.Cell(row, c + 1).Value = detailHeaders[c]; + var detailHeaderRange = ws.Range(row, 1, row, colCount); + detailHeaderRange.Style.Font.Bold = true; + detailHeaderRange.Style.Fill.BackgroundColor = headerColor; + detailHeaderRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + detailHeaderRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + row++; + + if (details.Count == 0) + { + ws.Range(row, 1, row, colCount).Merge(); + ws.Cell(row, 1).Value = "(无 OKExpression 判断,结果由步骤执行成败决定)"; + row++; + } + + int detailSeq = 1; + foreach (var d in details) + { + ws.Cell(row, 1).Value = detailSeq++; + ws.Cell(row, 2).Value = d.CreateTime.ToString("yyyy-MM-dd HH:mm:ss.fff"); + ws.Cell(row, 3).Value = d.StepName ?? ""; + ws.Cell(row, 4).Value = d.OKExpression ?? ""; + ws.Cell(row, 5).Value = d.Pass ? "PASS" : "NG"; + ws.Cell(row, 6).Value = d.Values ?? ""; + + var detailRange = ws.Range(row, 1, row, colCount); + detailRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + detailRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + ws.Cell(row, 5).Style.Font.Bold = true; + ws.Cell(row, 5).Style.Fill.BackgroundColor = d.Pass ? passColor : ngColor; + row++; + } + row++; + } + + ws.Columns().AdjustToContents(); + workbook.SaveAs(filePath); + } + #endregion #region 生命周期 diff --git a/Model/Entity/TestCheckRecordEntity.cs b/Model/Entity/TestCheckRecordEntity.cs new file mode 100644 index 0000000..f6d3589 --- /dev/null +++ b/Model/Entity/TestCheckRecordEntity.cs @@ -0,0 +1,53 @@ +using SqlSugar; +using System; + +namespace Model.Entity +{ + /// + /// 测试项判断记录:运行过程中测试项(IsTestItem 子程序)范围内每一次 OKExpression 判断。 + /// 同一次运行的所有记录共享同一个 TestRoundId,导出测试报告时按此 Guid 查询。 + /// IsSummary=true 的行为该测试项单次执行的汇总(PASS/NG),IsSummary=false 的行为单次判断明细。 + /// + public class TestCheckRecordEntity : BaseEntity + { + /// 同一次运行的统一标识(StepRunning.TestRoundID) + [SugarColumn(ColumnName = "TestRoundId", ColumnDescription = "运行轮次标识")] + public Guid TestRoundId { get; set; } + + /// 作用域/台架名称 + [SugarColumn(ColumnName = "Scope", ColumnDescription = "台架名称")] + public string Scope { get; set; } = string.Empty; + + /// 测试项名称(当前ADP文件路径) + [SugarColumn(ColumnName = "FileName", ColumnDescription = "测试项名称")] + public string FileName { get; set; } = string.Empty; + + /// 测试项名称(IsTestItem 子程序步骤的名称) + [SugarColumn(ColumnName = "TestItemName", Length = 200)] + public string TestItemName { get; set; } = string.Empty; + + /// 发生判断的步骤名称 + [SugarColumn(ColumnName = "StepName", Length = 200)] + public string StepName { get; set; } = string.Empty; + + /// 子程序嵌套深度(0=主程序) + [SugarColumn(ColumnName = "Depth")] + public int Depth { get; set; } + + /// 判断表达式 + [SugarColumn(ColumnName = "OKExpression", Length = 1000, IsNullable = true)] + public string? OKExpression { get; set; } + + /// 判断结果(true=PASS / false=NG) + [SugarColumn(ColumnName = "Pass")] + public bool Pass { get; set; } + + /// 表达式变量的实际取值(如 "电压=12.5; 电流=3.2; ") + [SugarColumn(ColumnName = "Values", Length = 2000, IsNullable = true)] + public string? Values { get; set; } + + /// 是否为汇总行(每个测试项单次执行结束时写入一条) + [SugarColumn(ColumnName = "IsSummary")] + public bool IsSummary { get; set; } + } +} diff --git a/Model/Models/Step.cs b/Model/Models/Step.cs index 812ef6a..8cba49e 100644 --- a/Model/Models/Step.cs +++ b/Model/Models/Step.cs @@ -12,6 +12,9 @@ namespace Model.Models public bool IsUsed { get; set; } = true; + /// 是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断) + public bool IsTestItem { get; set; } = false; + public int Index { get; set; } public string? Name { get; set; } diff --git a/Service/Implement/TestCheckRecordService.cs b/Service/Implement/TestCheckRecordService.cs new file mode 100644 index 0000000..7dcec76 --- /dev/null +++ b/Service/Implement/TestCheckRecordService.cs @@ -0,0 +1,38 @@ +using Model; +using Model.Entity; +using ORM; +using Service.Interface; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Service.Implement +{ + public class TestCheckRecordService : BaseService, ITestCheckRecordService + { + public TestCheckRecordService(SqlSugarRepository repository) : base(repository) + { + } + + /// + /// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序) + /// + public async Task>> GetByTestRoundIdAsync(Guid testRoundId) + { + try + { + var list = await _repository.Entities + .Where(x => x.TestRoundId == testRoundId) + .OrderBy(x => x.CreateTime) + .ToListAsync(); + + return Result>.Success(list); + } + catch (Exception ex) + { + return Result>.Error("根据 TestRoundId 查询测试项判断记录失败", ex); + } + } + } +} diff --git a/Service/Interface/ITestCheckRecordService.cs b/Service/Interface/ITestCheckRecordService.cs new file mode 100644 index 0000000..397fd6a --- /dev/null +++ b/Service/Interface/ITestCheckRecordService.cs @@ -0,0 +1,19 @@ +using Model; +using Model.Entity; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Service.Interface +{ + /// + /// 测试项判断记录服务:运行时记录测试项范围内每一次 OKExpression 判断,导出测试报告时查询。 + /// + public interface ITestCheckRecordService : IBaseService + { + /// + /// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序) + /// + Task>> GetByTestRoundIdAsync(Guid testRoundId); + } +} diff --git a/TestingModule/ViewModels/StepsManagerViewModel.cs b/TestingModule/ViewModels/StepsManagerViewModel.cs index 940d648..dd97572 100644 --- a/TestingModule/ViewModels/StepsManagerViewModel.cs +++ b/TestingModule/ViewModels/StepsManagerViewModel.cs @@ -96,6 +96,7 @@ namespace TestingModule.ViewModels public ICommand SelectionChangedCommand { get;set; } public ICommand OpenSubProgramCommand { get; set; } public ICommand GoBackCommand { get; set; } + public ICommand ToggleTestItemCommand { get; set; } #endregion #region 子程序导航(主程序 / 错误程序各自独立) @@ -133,6 +134,7 @@ namespace TestingModule.ViewModels SelectionChangedCommand = new DelegateCommand(SelectionChanged); OpenSubProgramCommand = new DelegateCommand(OpenSubProgram); GoBackCommand = new DelegateCommand(GoBack); + ToggleTestItemCommand = new DelegateCommand(ToggleTestItem); SubscribeStepCollections(); Program.PropertyChanged += Program_PropertyChanged; Admin = _globalInfo.IsAdmin; @@ -297,6 +299,25 @@ namespace TestingModule.ViewModels nav.CurrentProgram = SelectedStep.SubProgram; } + /// + /// 切换选中步骤的测试项标记(仅子程序可标记,运行时将记录其范围内所有 OKExpression 判断) + /// + private void ToggleTestItem() + { + if (!_globalInfo.IsAdmin) return; + + var source = (SelectedItems != null && SelectedItems.Any()) + ? SelectedItems + : (SelectedStep != null ? new List { SelectedStep } : null); + + if (source == null || !source.Any()) return; + + foreach (var item in source.Where(x => x.StepType == "子程序" && x.SubProgram != null)) + { + item.IsTestItem = !item.IsTestItem; + } + } + /// /// 返回上一级程序(操作当前激活的 Tab 的导航状态) /// diff --git a/TestingModule/Views/StepsManager.xaml b/TestingModule/Views/StepsManager.xaml index 300ce10..899c326 100644 --- a/TestingModule/Views/StepsManager.xaml +++ b/TestingModule/Views/StepsManager.xaml @@ -99,6 +99,10 @@ + @@ -142,6 +146,8 @@ + @@ -224,6 +230,10 @@ + @@ -267,6 +277,8 @@ + diff --git a/UIShare/GlobalVariable/StepRunning.cs b/UIShare/GlobalVariable/StepRunning.cs index 64602d6..c9ade2a 100644 --- a/UIShare/GlobalVariable/StepRunning.cs +++ b/UIShare/GlobalVariable/StepRunning.cs @@ -13,6 +13,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using UIShare.GlobalVariable; using static UIShare.UIViewModel.ParameterVM; @@ -29,6 +30,7 @@ namespace UIShare private IContainerProvider containerProvider; private IEventAggregator _eventAggregator; private ITestReportService _testReportService; + private ITestCheckRecordService _testCheckRecordService; private readonly Dictionary tmpParameters = []; @@ -38,6 +40,9 @@ namespace UIShare private readonly Stack loopStack = new(); + /// 测试项上下文栈:进入 IsTestItem 子程序时压栈,栈非空期间每次 OKExpression 判断都归属栈顶测试项 + private readonly Stack testItemStack = new(); + public CancellationTokenSource stepCTS = new(); public CancellationTokenSource errorStepCTS = new(); private bool SubSingleStep = false; @@ -46,13 +51,14 @@ namespace UIShare private volatile bool _disposed = false; public Guid TestRoundID; - public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService) + public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService, ITestCheckRecordService testCheckRecordService) { _scopedContext = ScopedContext; _systemConfig = systemConfig; _eventAggregator = eventAggregator; _deviceManager= deviceManager; _testReportService = testReportService; + _testCheckRecordService = testCheckRecordService; //_devices = containerProvider.Resolve(); } public async Task ExecuteErrorSteps(ProgramVM program, int depth = 0, CancellationToken cancellationToken = default) @@ -253,6 +259,7 @@ namespace UIShare loopStopwatchStack.Clear(); ResetAllStepStatus(program.StepCollection); tmpParameters.Clear(); + testItemStack.Clear(); TestRoundID = Guid.NewGuid(); } int initialLoopStackCount = loopStack.Count; @@ -381,14 +388,24 @@ namespace UIShare SubProgram = step.SubProgram, StepName = step.Name }); + bool isTestItemStep = step.IsTestItem; + if (isTestItemStep) + { + testItemStack.Push(new TestItemContext { Name = step.Name ?? "未命名测试项" }); + } stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken); + // 先评估本步骤自身(含自身 OKExpression 判断,归属本测试项),再写测试项汇总并弹栈 + UpdateCurrentStepResult(step, true, stepSuccess, depth); + if (isTestItemStep) + { + await FinalizeTestItemAsync(depth); + } // 发布退出子程序导航事件 _eventAggregator.GetEvent().Publish(new SubProgramNavigatePayload { Scope = _systemConfig.Title, Action = NavigateAction.Exit }); - UpdateCurrentStepResult(step, true, stepSuccess, depth); if (SubSingleStep) { SubSingleStep = false; @@ -812,12 +829,46 @@ namespace UIShare paraDic.TryAdd(item.Name, item.Value!); } } - bool re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic); + bool re; + try + { + re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic); + } + catch (Exception ex) + { + // 表达式执行错误也视为 NG,并记录到测试项判断明细 + LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] OKExpression 执行异常: {ex.Message}", depth: depth); + re = false; + } step.Result = re ? 1 : 2; if (step.Result == 2) { LoggerHelper.WarnWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] NG:条件表达式验证失败", depth: depth); } + // 测试项范围内:记录本次判断(重复判断逐次记录) + if (testItemStack.Count > 0) + { + var ctx = testItemStack.Peek(); + bool isSelfCheck = ctx.Name == (step.Name ?? "未命名测试项") && step.SubProgram != null; + if (!re) ctx.HasFailure = true; + if (!isSelfCheck) + { + SaveCheckRecordAsync(new TestCheckRecordEntity + { + TestRoundId = TestRoundID, + Scope = _systemConfig.Title, + FileName = _systemConfig.CurrentADPFile ?? "", + TestItemName = ctx.Name, + StepName = step.Name ?? "", + Depth = depth, + OKExpression = step.OKExpression, + Pass = re, + Values = ExtractExpressionValues(step.OKExpression, paraDic), + IsSummary = false, + CreateTime = DateTime.Now + }); + } + } } } else @@ -828,6 +879,90 @@ namespace UIShare } step.Result = 2; } + + // 测试项范围内的步骤执行错误/表达式 NG 均计入当前测试项汇总(自身步骤的错误已在压栈期间计入) + if (step.Result == 2 && testItemStack.Count > 0) + { + testItemStack.Peek().HasFailure = true; + } + } + + /// + /// 测试项执行结束:写入汇总记录(PASS/NG)并弹栈。 + /// 测试项内无任何判断时,结果由范围内步骤执行成败决定。 + /// + private async Task FinalizeTestItemAsync(int depth) + { + if (testItemStack.Count == 0) return; + var ctx = testItemStack.Pop(); + SaveCheckRecordAsync(new TestCheckRecordEntity + { + TestRoundId = TestRoundID, + Scope = _systemConfig.Title, + FileName = _systemConfig.CurrentADPFile ?? "", + TestItemName = ctx.Name, + StepName = ctx.Name, + Depth = depth, + OKExpression = null, + Pass = !ctx.HasFailure, + Values = null, + IsSummary = true, + CreateTime = DateTime.Now + }); + await Task.CompletedTask; + } + + /// + /// 保存测试项判断记录(不阻塞执行主流程) + /// + private void SaveCheckRecordAsync(TestCheckRecordEntity entity) + { + _ = Task.Run(async () => + { + try + { + var result = await _testCheckRecordService.InsertAsync(entity); + if (!result.IsSuccess) + { + LoggerHelper.Error($"保存测试项判断记录失败 [{entity.TestItemName}]: {result.Msg}"); + } + } + catch (Exception ex) + { + LoggerHelper.Error($"保存测试项判断记录失败 [{entity.TestItemName}]: {ex.Message}"); + } + }); + } + + /// + /// 从表达式中提取实际出现的变量并取其当前值,拼接为 "变量=值; " 格式(长名优先,避免子串误匹配) + /// + private static string? ExtractExpressionValues(string expression, Dictionary paraDic) + { + try + { + var sb = new StringBuilder(); + var matchedSpans = new List<(int Start, int End)>(); + foreach (var name in paraDic.Keys.OrderByDescending(k => k.Length)) + { + if (string.IsNullOrWhiteSpace(name)) continue; + foreach (Match m in Regex.Matches(expression, $@"\b{Regex.Escape(name)}\b")) + { + bool overlaps = matchedSpans.Any(s => m.Index < s.End && m.Index + m.Length > s.Start); + if (!overlaps) + { + matchedSpans.Add((m.Index, m.Index + m.Length)); + sb.Append($"{name}={paraDic[name]}; "); + break; + } + } + } + return sb.Length > 0 ? sb.ToString() : null; + } + catch + { + return null; + } } public void Dispose() @@ -861,6 +996,7 @@ namespace UIShare tmpParameters.Clear(); loopStack.Clear(); loopStopwatchStack.Clear(); + testItemStack.Clear(); stepStopwatch.Stop(); } @@ -876,6 +1012,13 @@ namespace UIShare public StepVM? LoopStartStep { get; set; } } + /// 测试项执行上下文:记录测试项名称与范围内是否出现过失败 + private class TestItemContext + { + public string Name { get; set; } = string.Empty; + public bool HasFailure { get; set; } + } + #endregion } diff --git a/UIShare/UIViewModel/StepVM.cs b/UIShare/UIViewModel/StepVM.cs index c66085e..29b337e 100644 --- a/UIShare/UIViewModel/StepVM.cs +++ b/UIShare/UIViewModel/StepVM.cs @@ -26,6 +26,7 @@ namespace UIShare.UIViewModel NGGotoStepID = source.NGGotoStepID; Description = source.Description; IsUsed = source.IsUsed; + IsTestItem = source.IsTestItem; if (source.Method != null) { @@ -55,6 +56,15 @@ namespace UIShare.UIViewModel set => SetProperty(ref _isUsed, value); } + private bool _isTestItem = false; + + /// 是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断) + public bool IsTestItem + { + get => _isTestItem; + set => SetProperty(ref _isTestItem, value); + } + private int _index; public int Index