AlarmOrphanRepairTests.cs 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. using Serilog.Events;
  2. using SqlSugar;
  3. using Xunit;
  4. using YZWater.Core.Models;
  5. using YZWater.Core.Services;
  6. namespace YZWater.Core.Tests.Services;
  7. /// <summary>
  8. /// 上次运行遗留的"未恢复"报警收尾。
  9. /// conditionId→DbId 的映射只在内存里,进程被杀/正常重启都来不及回写 RecoverTime:
  10. /// 现场库里那条 07:36:19 的 PLC_Connection 记录一直是 NULL,历史页把它显示成"进行中/无限时长",
  11. /// 操作员以为设备还在报警。诊断流水(ConditionId 为空)是瞬时事件,本就没有恢复一说,不该被盖上时间。
  12. /// </summary>
  13. [Collection("Serilog minimum level")]
  14. public class AlarmOrphanRepairTests : IDisposable
  15. {
  16. private readonly LogEventLevel _originalLevel = LogService.MinimumLevel;
  17. private readonly AlarmService _service;
  18. public AlarmOrphanRepairTests()
  19. {
  20. LogService.ApplyMinimumLevel("Error");
  21. DatabaseService.Initialize();
  22. _service = new AlarmService(PlcPollingService.Instance);
  23. }
  24. public void Dispose()
  25. {
  26. _service.Dispose();
  27. LogService.ApplyMinimumLevel(_originalLevel.ToString());
  28. }
  29. private int Insert(AlarmRecord record) =>
  30. DatabaseService.Db.Insertable(record).ExecuteReturnIdentity();
  31. private DateTime? ReadRecoverTime(int id) =>
  32. DatabaseService.Db.Queryable<AlarmRecord>().Where(r => r.Id == id).Select(r => r.RecoverTime).First();
  33. [Fact]
  34. public async Task Startup_ClosesOrphanedRealAlarm()
  35. {
  36. var marker = Guid.NewGuid().ToString("N");
  37. var orphanId = Insert(new AlarmRecord
  38. {
  39. AlarmType = "PLC",
  40. AlarmMessage = $"遗留活跃报警 {marker}",
  41. AlarmTime = DateTime.Now.AddHours(-2),
  42. ConditionId = $"PLC_{marker}"
  43. });
  44. Assert.Null(ReadRecoverTime(orphanId));
  45. await _service.CloseOrphanedAlarmsAsync();
  46. var closed = ReadRecoverTime(orphanId);
  47. Assert.NotNull(closed);
  48. // 收尾时间取本次启动时刻,不是报警时间:中间这段时间设备确实处于无人监视状态
  49. Assert.True(closed!.Value > DateTime.Now.AddMinutes(-5));
  50. }
  51. [Fact]
  52. public async Task Startup_LeavesJournalEventsAndRecoveredRowsAlone()
  53. {
  54. var marker = Guid.NewGuid().ToString("N");
  55. // 诊断流水:没有 conditionId,也没有恢复概念
  56. var journalId = Insert(new AlarmRecord
  57. {
  58. AlarmType = "Quality",
  59. AlarmMessage = $"测点质量流水 {marker}",
  60. AlarmTime = DateTime.Now.AddHours(-2),
  61. IsConfirmed = true,
  62. ConfirmedBy = "系统"
  63. });
  64. // 正常已恢复的报警,恢复时间不能被覆盖成启动时刻
  65. var recoveredAt = DateTime.Now.AddHours(-3);
  66. var recoveredId = Insert(new AlarmRecord
  67. {
  68. AlarmType = "DI",
  69. AlarmMessage = $"已恢复报警 {marker}",
  70. AlarmTime = recoveredAt.AddHours(-1),
  71. ConditionId = $"DI_{marker}",
  72. RecoverTime = recoveredAt
  73. });
  74. await _service.CloseOrphanedAlarmsAsync();
  75. Assert.Null(ReadRecoverTime(journalId));
  76. // SQLite 只存到毫秒,比较放宽到 1 秒,避免亚毫秒精度造成假红
  77. var kept = ReadRecoverTime(recoveredId);
  78. Assert.NotNull(kept);
  79. Assert.True(Math.Abs((kept!.Value - recoveredAt).TotalSeconds) < 1);
  80. }
  81. }