fix(uptime): точно разбирать составные длительности RTT

This commit is contained in:
Denozordec committed 2026-10-12 18:17:10 +07:00
1 parent 737e04a6a9
commit ca2b634743
9 files changed
+184 -24

No files matched your search

+25 -1
View File
@@ -1,5 +1,6 @@
import assert from "node:assert/strict"
import { parsePing } from "./uptime-collector.js"
import { parsePing, parsePingTimeMs } from "./uptime-collector.js"
import fs from "node:fs"
for (const rows of [[], [{}], [{status:"not enough permissions"}], [{sent:"0",received:"0","packet-loss":"0","avg-rtt":"0ms"}], [{sent:"4",received:"5","packet-loss":"0"}], [{sent:"4bad",received:"4","packet-loss":"0"}]]) {
const result=parsePing(rows)
assert.equal(result.measured,false);assert.equal(result.avgRtt,null)
@@ -11,3 +12,26 @@ assert.equal(up.measured,true);assert.equal(up.status,"up");assert.equal(up.loss
const partial=parsePing([{time:"500us"},{status:"timeout"}])
assert.equal(partial.measured,true);assert.equal(partial.loss,50)
console.log("ping measurement: empty/unsupported is not a measurement; zero and real 100 percent loss preserved")
for (const [raw, expected] of [["53ms561us",53.561],["1s53ms561us",1053.561],["500us",0.5],["0ms",0],["53,561ms",53.561],["00:00:01.5",1500],[".5",0.5],["53",53]] as const) {
assert(Math.abs(parsePingTimeMs(raw)! - expected) < 1e-9)
}
for (const raw of ["53bad", "53msbad", "-1", "Infinity", "1e3", "1ms2ms", "1us2ms", "00:60:00", "00:00:60", "1s2s", "", undefined]) assert.equal(parsePingTimeMs(raw),null)
assert.equal(parsePing([{time:"53bad"}]).measured,false)
assert.equal(parsePing([{time:"53ms900us"}]).avgRtt,54)
assert.equal(parsePing([{time:"1s53ms561us"}]).avgRtt,1054)
assert.equal(parsePing([{sent:"4",received:"4","packet-loss":"0","avg-rtt":"bad"}]).avgRtt,null)
// Replay the recorded real CHR payload when the repository evidence is available.
const nativePath = new URL("../../../docs/ux-ui/acceptance/ping-measurement-native-2026-10-12.json", import.meta.url)
if(fs.existsSync(nativePath)) {
const native = JSON.parse(fs.readFileSync(nativePath,"utf8"))
for(const c of native.cases) {
const parsed = parsePing(c.raw)
assert.equal(parsed.measured,true); assert.equal(parsed.loss,0)
assert(c.raw.every((r:{time:string}) => parsePingTimeMs(r.time)! > 53 && parsePingTimeMs(r.time)! < 54))
assert.equal(parsed.avgRtt,53)
}
console.log(`ping RTT recorded native payload replay: ${native.cases.length} cases passed`)
} else {
console.log("ping RTT recorded native payload replay: skipped; evidence file not present")
}
console.log("ping RTT compound duration/strict token tests ok")
+21 -13
View File
@@ -23,26 +23,34 @@ export function getUptimeCollectorsState(): { resources: boolean; ping: boolean
return { resources: collectingResources, ping: collectingPing }
}
function parsePingTimeMs(raw: string | undefined): number | null {
export function parsePingTimeMs(raw: string | undefined): number | null {
if (!raw) return null
const s = String(raw).trim().toLowerCase().replace(",", ".")
const us = s.match(/^(\d+(?:\.\d+)?)\s*us$/)
if (us) return Number.parseFloat(us[1]) / 1000
const ms = s.match(/^(\d+(?:\.\d+)?)\s*ms$/)
if (ms) return Number.parseFloat(ms[1])
const sec = s.match(/^(\d+(?:\.\d+)?)\s*s$/)
if (sec) return Number.parseFloat(sec[1]) * 1000
const clock = s.match(/^(\d+):(\d+):(\d+(?:\.\d+)?)$/)
if (clock) {
const h = Number.parseFloat(clock[1])
const m = Number.parseFloat(clock[2])
const sc = Number.parseFloat(clock[3])
return (h * 3600 + m * 60 + sc) * 1000
const total = (h * 3600 + m * 60 + sc) * 1000
return m < 60 && sc < 60 && Number.isFinite(total) ? total : null
}
const n = Number.parseFloat(s)
return Number.isFinite(n) ? n : null
// Bare numbers retain the existing millisecond unit, but never parse a prefix.
if (/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(s)) {
const n = Number(s)
return Number.isFinite(n) ? n : null
}
// Native CHR replies include combined values such as 53ms561us.
const parts = [...s.matchAll(/(\d+(?:\.\d+)?)\s*(ms|us|s)/g)]
if (!parts.length || parts.map(p => p[0]).join("").replace(/\s/g, "") !== s.replace(/\s/g, "")) return null
const factors: Record<string, number> = { s: 1000, ms: 1, us: 0.001 }
let total = 0, previousFactor = Infinity
for (const part of parts) {
const factor = factors[part[2]]
if (factor >= previousFactor) return null
total += Number(part[1]) * factor
previousFactor = factor
}
return Number.isFinite(total) ? total : null
}
export async function getSettings() {
@@ -69,7 +77,7 @@ export function parsePing(results: Array<{ time?: string; status?: string; sent?
const sum = [...results].reverse().find((r) =>
r.sent != null || r.received != null || r["packet-loss"] != null || r["avg-rtt"] != null,
)
const okReplies = results.filter((r) => r.time && String(r.status ?? "").toLowerCase() !== "timeout")
const okReplies = results.filter((r) => parsePingTimeMs(r.time) !== null && String(r.status ?? "").toLowerCase() !== "timeout")
const timedOutReplies = results.filter((r) => String(r.status ?? "").toLowerCase() === "timeout")
const rtts = okReplies
.map((r) => parsePingTimeMs(r.time))