测试报告导出修改

This commit is contained in:
2026-08-30 15:36:57 +08:00
parent 61d7293afb
commit 7a56eb98d2
11 changed files with 488 additions and 5 deletions
+1
View File
@@ -206,6 +206,7 @@ namespace ADP
//注册服务
containerRegistry.Register<IMonitorValueService, MonitorValueService>();
containerRegistry.Register<ITestReportService, TestReportService>();
containerRegistry.Register<ITestCheckRecordService, TestCheckRecordService>();
}
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
protected override IModuleCatalog CreateModuleCatalog()
+25 -1
View File
@@ -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);
}
/// <summary>
/// 归一化变量取值:输入变量的值常以字符串形式保存(如 "10"),
/// 若直接参与比较,NCalc 会按字符串逐位比较("10" > "5" 为假),
/// 导致大于/小于判断失真。此处将可解析为数字/布尔的字符串转换为对应类型,
/// 使比较按数值语义进行;无法解析的字符串保持原样。
/// </summary>
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)
{
+160 -1
View File
@@ -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<ITestReportService>();
_testCheckRecordService = containerProvider.Resolve<ITestCheckRecordService>();
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}", () => { });
}
/// <summary>
/// 导出测试报告:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细
/// </summary>
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);
}
/// <summary>
/// 测试报告导出:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细(含重复判断、判断时间、数据值)
/// </summary>
private static void ExportReportToExcel(string filePath, List<TestCheckRecordEntity> 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
+53
View File
@@ -0,0 +1,53 @@
using SqlSugar;
using System;
namespace Model.Entity
{
/// <summary>
/// 测试项判断记录:运行过程中测试项(IsTestItem 子程序)范围内每一次 OKExpression 判断。
/// 同一次运行的所有记录共享同一个 TestRoundId,导出测试报告时按此 Guid 查询。
/// IsSummary=true 的行为该测试项单次执行的汇总(PASS/NG),IsSummary=false 的行为单次判断明细。
/// </summary>
public class TestCheckRecordEntity : BaseEntity
{
/// <summary>同一次运行的统一标识(StepRunning.TestRoundID</summary>
[SugarColumn(ColumnName = "TestRoundId", ColumnDescription = "运行轮次标识")]
public Guid TestRoundId { get; set; }
/// <summary>作用域/台架名称</summary>
[SugarColumn(ColumnName = "Scope", ColumnDescription = "台架名称")]
public string Scope { get; set; } = string.Empty;
/// <summary>测试项名称(当前ADP文件路径)</summary>
[SugarColumn(ColumnName = "FileName", ColumnDescription = "测试项名称")]
public string FileName { get; set; } = string.Empty;
/// <summary>测试项名称(IsTestItem 子程序步骤的名称)</summary>
[SugarColumn(ColumnName = "TestItemName", Length = 200)]
public string TestItemName { get; set; } = string.Empty;
/// <summary>发生判断的步骤名称</summary>
[SugarColumn(ColumnName = "StepName", Length = 200)]
public string StepName { get; set; } = string.Empty;
/// <summary>子程序嵌套深度(0=主程序)</summary>
[SugarColumn(ColumnName = "Depth")]
public int Depth { get; set; }
/// <summary>判断表达式</summary>
[SugarColumn(ColumnName = "OKExpression", Length = 1000, IsNullable = true)]
public string? OKExpression { get; set; }
/// <summary>判断结果(true=PASS / false=NG</summary>
[SugarColumn(ColumnName = "Pass")]
public bool Pass { get; set; }
/// <summary>表达式变量的实际取值(如 "电压=12.5; 电流=3.2; "</summary>
[SugarColumn(ColumnName = "Values", Length = 2000, IsNullable = true)]
public string? Values { get; set; }
/// <summary>是否为汇总行(每个测试项单次执行结束时写入一条)</summary>
[SugarColumn(ColumnName = "IsSummary")]
public bool IsSummary { get; set; }
}
}
+3
View File
@@ -12,6 +12,9 @@ namespace Model.Models
public bool IsUsed { get; set; } = true;
/// <summary>是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断)</summary>
public bool IsTestItem { get; set; } = false;
public int Index { get; set; }
public string? Name { get; set; }
@@ -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<TestCheckRecordEntity>, ITestCheckRecordService
{
public TestCheckRecordService(SqlSugarRepository<TestCheckRecordEntity> repository) : base(repository)
{
}
/// <summary>
/// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序)
/// </summary>
public async Task<Result<List<TestCheckRecordEntity>>> GetByTestRoundIdAsync(Guid testRoundId)
{
try
{
var list = await _repository.Entities
.Where(x => x.TestRoundId == testRoundId)
.OrderBy(x => x.CreateTime)
.ToListAsync();
return Result<List<TestCheckRecordEntity>>.Success(list);
}
catch (Exception ex)
{
return Result<List<TestCheckRecordEntity>>.Error("根据 TestRoundId 查询测试项判断记录失败", ex);
}
}
}
}
@@ -0,0 +1,19 @@
using Model;
using Model.Entity;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Service.Interface
{
/// <summary>
/// 测试项判断记录服务:运行时记录测试项范围内每一次 OKExpression 判断,导出测试报告时查询。
/// </summary>
public interface ITestCheckRecordService : IBaseService<TestCheckRecordEntity>
{
/// <summary>
/// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序)
/// </summary>
Task<Result<List<TestCheckRecordEntity>>> GetByTestRoundIdAsync(Guid testRoundId);
}
}
@@ -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<object>(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;
}
/// <summary>
/// 切换选中步骤的测试项标记(仅子程序可标记,运行时将记录其范围内所有 OKExpression 判断)
/// </summary>
private void ToggleTestItem()
{
if (!_globalInfo.IsAdmin) return;
var source = (SelectedItems != null && SelectedItems.Any())
? SelectedItems
: (SelectedStep != null ? new List<StepVM> { SelectedStep } : null);
if (source == null || !source.Any()) return;
foreach (var item in source.Where(x => x.StepType == "子程序" && x.SubProgram != null))
{
item.IsTestItem = !item.IsTestItem;
}
}
/// <summary>
/// 返回上一级程序(操作当前激活的 Tab 的导航状态)
/// </summary>
+12
View File
@@ -99,6 +99,10 @@
<DataGridCheckBoxColumn Width="58"
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
Header="启用" />
<DataGridCheckBoxColumn Width="58"
Binding="{Binding IsTestItem}"
Header="测试项"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding Index}"
Header="序号"
IsReadOnly="True" />
@@ -142,6 +146,8 @@
<Separator/>
<MenuItem Header="打开子程序"
Command="{Binding OpenSubProgramCommand}" />
<MenuItem Header="标记/取消测试项"
Command="{Binding ToggleTestItemCommand}" />
</ContextMenu>
</DataGrid.ContextMenu>
@@ -224,6 +230,10 @@
<DataGridCheckBoxColumn Width="58"
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
Header="启用" />
<DataGridCheckBoxColumn Width="58"
Binding="{Binding IsTestItem}"
Header="测试项"
IsReadOnly="True" />
<DataGridTextColumn Binding="{Binding Index}"
Header="序号"
IsReadOnly="True" />
@@ -267,6 +277,8 @@
<Separator/>
<MenuItem Header="打开子程序"
Command="{Binding OpenSubProgramCommand}" />
<MenuItem Header="标记/取消测试项"
Command="{Binding ToggleTestItemCommand}" />
</ContextMenu>
</DataGrid.ContextMenu>
+146 -3
View File
@@ -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<Guid, ParameterVM> tmpParameters = [];
@@ -38,6 +40,9 @@ namespace UIShare
private readonly Stack<LoopContext> loopStack = new();
/// <summary>测试项上下文栈:进入 IsTestItem 子程序时压栈,栈非空期间每次 OKExpression 判断都归属栈顶测试项</summary>
private readonly Stack<TestItemContext> 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<Devices>();
}
public async Task<bool> 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<SubProgramNavigateEvent>().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;
}
}
/// <summary>
/// 测试项执行结束:写入汇总记录(PASS/NG)并弹栈。
/// 测试项内无任何判断时,结果由范围内步骤执行成败决定。
/// </summary>
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;
}
/// <summary>
/// 保存测试项判断记录(不阻塞执行主流程)
/// </summary>
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}");
}
});
}
/// <summary>
/// 从表达式中提取实际出现的变量并取其当前值,拼接为 "变量=值; " 格式(长名优先,避免子串误匹配)
/// </summary>
private static string? ExtractExpressionValues(string expression, Dictionary<string, object> 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; }
}
/// <summary>测试项执行上下文:记录测试项名称与范围内是否出现过失败</summary>
private class TestItemContext
{
public string Name { get; set; } = string.Empty;
public bool HasFailure { get; set; }
}
#endregion
}
+10
View File
@@ -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;
/// <summary>是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断)</summary>
public bool IsTestItem
{
get => _isTestItem;
set => SetProperty(ref _isTestItem, value);
}
private int _index;
public int Index