mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:27:02 +01:00
Add network diagnostics: duty-cycle and line-speed probes
Diagnosing "Steam downloads are slow" kept pointing at the ISP because every averaged measurement showed a steady ~170 Mbps. Sampling at 1s instead of 5s showed that plateau does not exist: the link alternates between ~450 Mbps bursts and multi-second stalls. Effective rate is burst x duty_cycle, and only the second factor was wrong. steam-download-duty.sh reports those two factors separately, which is what the Steam UI cannot do. line-speed-probe.sh separates burst from sustained throughput, because a single stream tops out near 180 Mbps on this line and reads convincingly as ISP shaping when it is not — a many-connection swarm reaches 986 Mbps with zero retransmits. Arithmetic is extracted into pure helpers with main() behind a BASH_SOURCE guard so the bats suite can source them without starting a measurement. install.sh installs bats and links both scripts onto PATH. Note: no CI workflow runs .sh/.bats tests, including the four shell tests that predate this one, so the suite is manual-only for now.
This commit is contained in:
parent
56535695c6
commit
836f4ef2de
@ -0,0 +1,49 @@
|
||||
{
|
||||
"intent": "Add two reusable network diagnostic scripts that distinguish a slow link from an idle download client, so 'Steam is slow' can be answered with measurement instead of guesswork.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/diagnostics/ (new): steam-download-duty.sh, line-speed-probe.sh, install.sh",
|
||||
"linux_configuration/tests/test_network_diagnostics.bats (new)",
|
||||
"Non-goal: wiring shell tests into CI — no workflow runs .sh/.bats today, including the 4 pre-existing shell tests",
|
||||
"Non-goal: changing any Steam or system configuration; these scripts only read counters"
|
||||
],
|
||||
"changes": [
|
||||
"steam-download-duty.sh samples /sys/class/net rx_bytes at 1s and reports duty cycle, burst rate and effective average separately. 5s averaging smears bursts and stalls into a plateau that does not exist; 1s resolution is what exposed the real behaviour.",
|
||||
"line-speed-probe.sh reports burst vs sustained across single and parallel streams. Sustained deliberately means the mean of the second half, discarding TCP slow start and any ISP burst allowance.",
|
||||
"Arithmetic and classification extracted into pure helpers (bytes_to_mbps, duty_pct, mean_of, classify, sustained_of, max_of, verdict_for); main() is guarded by a BASH_SOURCE check so tests can source the helpers without starting a measurement.",
|
||||
"install.sh installs the bats test dependency and links both scripts into ~/.local/bin, idempotently.",
|
||||
"30 bats tests covering boundary values, empty input and divide-by-zero guards."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bats linux_configuration/tests/test_network_diagnostics.bats",
|
||||
"result": "pass",
|
||||
"evidence": "1..30, all ok. One assertion initially failed and was correct to fail: sustained_of 10 20 30 returns 25, not 30 (odd counts drop only the first sample). The expectation was fixed, not the behaviour, since 25 matches the implementation that produced every measurement in the investigation."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <the 4 new files>",
|
||||
"result": "pass",
|
||||
"evidence": "41 hooks Passed/Skipped including shellcheck and codespell; ai-evidence-contract required this artifact."
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/scripts/diagnostics/install.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Idempotent re-run: detected existing bats 1.13.0, refreshed both ~/.local/bin symlinks."
|
||||
},
|
||||
{
|
||||
"command": "steam-download-duty.sh 8 (via the ~/.local/bin symlink, post-refactor)",
|
||||
"result": "pass",
|
||||
"evidence": "Ran end to end and reported 0% duty / 0 Mbps correctly, the download having completed."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"IFACE defaults to enp6s0; on a different NIC the scripts read a nonexistent /sys path and fail. Overridable via the IFACE environment variable.",
|
||||
"Thresholds in classify() and verdict_for() (80% duty, 400/600/350 Mbps) are calibrated to this gigabit line and would need revisiting elsewhere.",
|
||||
"line-speed-probe.sh pulls real ISOs to /dev/null and will consume bandwidth for its full duration if run carelessly.",
|
||||
"The bats suite runs only when invoked manually; no CI workflow executes it."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit, then rm ~/.local/bin/{line-speed-probe.sh,steam-download-duty.sh} — both are symlinks into the repo, so reverting alone leaves them dangling.",
|
||||
"Optionally: sudo pacman -Rns bats if the framework is not wanted (it is a new dependency introduced by this change).",
|
||||
"Validate after rollback: which steam-download-duty.sh returns nothing and no repo test references the diagnostics directory."
|
||||
]
|
||||
}
|
||||
61
linux_configuration/scripts/diagnostics/install.sh
Executable file
61
linux_configuration/scripts/diagnostics/install.sh
Executable file
@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Install dependencies for the network diagnostics scripts and link them onto
|
||||
# PATH.
|
||||
#
|
||||
# Runtime deps : curl, awk, coreutils (all base on Arch — checked, not installed)
|
||||
# Test deps : bats (extra/bats) — needed only to run the .bats suite
|
||||
#
|
||||
# Idempotent: safe to re-run. Existing symlinks are refreshed, already-present
|
||||
# packages are skipped by pacman --needed.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
readonly SCRIPT_DIR
|
||||
readonly BIN_DIR="${HOME}/.local/bin"
|
||||
readonly SCRIPTS=(line-speed-probe.sh steam-download-duty.sh)
|
||||
|
||||
require_runtime_tools() {
|
||||
# These ship with base/base-devel on Arch. Fail loudly rather than trying to
|
||||
# install a broken base system.
|
||||
local missing=()
|
||||
local tool
|
||||
for tool in curl awk; do
|
||||
command -v "$tool" >/dev/null 2>&1 || missing+=("$tool")
|
||||
done
|
||||
if (( ${#missing[@]} > 0 )); then
|
||||
printf 'Error: missing required tools: %s\n' "${missing[*]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_test_deps() {
|
||||
if command -v bats >/dev/null 2>&1; then
|
||||
printf 'bats already installed (%s)\n' "$(bats --version)"
|
||||
return
|
||||
fi
|
||||
printf 'Installing bats (test dependency)...\n'
|
||||
sudo pacman -S --noconfirm --needed bats
|
||||
}
|
||||
|
||||
link_scripts() {
|
||||
mkdir -p "$BIN_DIR"
|
||||
local s
|
||||
for s in "${SCRIPTS[@]}"; do
|
||||
ln -sfn "${SCRIPT_DIR}/${s}" "${BIN_DIR}/${s}"
|
||||
printf 'linked %s -> %s\n' "${BIN_DIR}/${s}" "${SCRIPT_DIR}/${s}"
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
require_runtime_tools
|
||||
install_test_deps
|
||||
link_scripts
|
||||
printf '\nDone. Run the tests with:\n'
|
||||
printf ' bats %s/../../tests/test_network_diagnostics.bats\n' "$SCRIPT_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
168
linux_configuration/scripts/diagnostics/line-speed-probe.sh
Executable file
168
linux_configuration/scripts/diagnostics/line-speed-probe.sh
Executable file
@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# line-speed-probe.sh — measure burst vs sustained inbound throughput.
|
||||
#
|
||||
# Why this exists: a single-stream download tops out around 180 Mbps on this
|
||||
# line, which reads convincingly as "the ISP shapes us to 180". It does not —
|
||||
# a many-connection swarm reaches 986 Mbps with zero retransmits. Only a
|
||||
# multi-stream probe reveals real line capacity, so this script runs both and
|
||||
# reports them separately.
|
||||
#
|
||||
# Baseline captured 2026-07-19 ~17:30 (Sunday evening, European peak):
|
||||
# geo.mirror.pkgbuild.com 1 stream PEAK 493 SUSTAINED 182 Mbps
|
||||
# ftp.icm.edu.pl 1 stream PEAK 308 SUSTAINED 164 Mbps
|
||||
# 4 mixed mirrors 4 streams PEAK 766 SUSTAINED 258 Mbps
|
||||
#
|
||||
# Downloads are discarded to /dev/null; only the report is written. Rates come
|
||||
# from the NIC's own counters, so competing traffic is included by design.
|
||||
#
|
||||
# Usage: line-speed-probe.sh [url] (default: probe the standard set)
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IFACE="${IFACE:-enp6s0}"
|
||||
DURATION="${DURATION:-90}" # seconds per probe
|
||||
STEP=5 # sample interval
|
||||
REPORT_DIR="${REPORT_DIR:-${HOME}/.local/share/line-speed-test}"
|
||||
|
||||
MIRROR_PL="https://ftp.icm.edu.pl/pub/Linux/dist/archlinux/iso/latest/archlinux-x86_64.iso"
|
||||
MIRROR_GEO="https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso"
|
||||
MIRROR_RACK="https://mirror.rackspace.com/archlinux/iso/latest/archlinux-x86_64.iso"
|
||||
MIRROR_KERNEL="https://mirrors.edge.kernel.org/archlinux/iso/latest/archlinux-x86_64.iso"
|
||||
|
||||
PIDS=()
|
||||
REPORT=""
|
||||
|
||||
# --- pure helpers (unit-tested by linux_configuration/tests) ----------------
|
||||
|
||||
bytes_to_mbps() {
|
||||
local delta="$1" secs="$2"
|
||||
(( secs > 0 )) || { printf '0'; return; }
|
||||
printf '%d' $(( delta * 8 / secs / 1000000 ))
|
||||
}
|
||||
|
||||
# Sustained throughput = mean of the SECOND HALF of the samples. The first half
|
||||
# is discarded deliberately: it contains TCP slow start and any ISP burst
|
||||
# allowance, both of which inflate the figure we care about.
|
||||
sustained_of() {
|
||||
(( $# > 0 )) || { printf '0'; return; }
|
||||
local count=$# half=$(( $# / 2 )) sum=0 i=0 v
|
||||
for v in "$@"; do
|
||||
i=$(( i + 1 ))
|
||||
(( i > half )) && sum=$(( sum + v ))
|
||||
done
|
||||
printf '%d' $(( sum / (count - half) ))
|
||||
}
|
||||
|
||||
max_of() {
|
||||
(( $# > 0 )) || { printf '0'; return; }
|
||||
local m=0 v
|
||||
for v in "$@"; do (( v > m )) && m=$v; done
|
||||
printf '%d' "$m"
|
||||
}
|
||||
|
||||
# Interpret the best sustained figure seen across all probes.
|
||||
verdict_for() {
|
||||
local best="$1"
|
||||
if (( best >= 600 )); then
|
||||
printf 'LINE_OK'
|
||||
elif (( best >= 350 )); then
|
||||
printf 'PARTIAL'
|
||||
else
|
||||
printf 'SLOW'
|
||||
fi
|
||||
}
|
||||
|
||||
# --- measurement -----------------------------------------------------------
|
||||
|
||||
cleanup() {
|
||||
local p
|
||||
for p in "${PIDS[@]:-}"; do
|
||||
[[ -n "$p" ]] && kill "$p" 2>/dev/null || true
|
||||
done
|
||||
wait 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
rx_bytes() { cat "/sys/class/net/${IFACE}/statistics/rx_bytes"; }
|
||||
|
||||
emit() { printf '%s\n' "$1" | tee -a "$REPORT"; }
|
||||
|
||||
# probe <label> <url>...
|
||||
probe() {
|
||||
local label="$1"; shift
|
||||
local urls=("$@")
|
||||
local t a b mbps
|
||||
local -a samples=()
|
||||
|
||||
emit ""
|
||||
emit "### ${label} (${#urls[@]} stream(s), ${DURATION}s)"
|
||||
|
||||
PIDS=()
|
||||
local u
|
||||
for u in "${urls[@]}"; do
|
||||
curl -s --max-time "$(( DURATION + 30 ))" -o /dev/null "$u" &
|
||||
PIDS+=("$!")
|
||||
done
|
||||
|
||||
for (( t = STEP; t <= DURATION; t += STEP )); do
|
||||
a=$(rx_bytes); sleep "$STEP"; b=$(rx_bytes)
|
||||
mbps=$(bytes_to_mbps $(( b - a )) "$STEP")
|
||||
samples+=("$mbps")
|
||||
emit "$(printf ' t=%3ds %5d Mbps' "$t" "$mbps")"
|
||||
done
|
||||
|
||||
cleanup
|
||||
PIDS=()
|
||||
|
||||
local peak sustained
|
||||
peak=$(max_of "${samples[@]}")
|
||||
sustained=$(sustained_of "${samples[@]}")
|
||||
emit " --> PEAK ${peak} Mbps | SUSTAINED ${sustained} Mbps"
|
||||
printf '%s\t%s\t%s\n' "$label" "$peak" "$sustained" >>"${REPORT}.tsv"
|
||||
}
|
||||
|
||||
main() {
|
||||
mkdir -p "$REPORT_DIR"
|
||||
REPORT="${REPORT_DIR}/report-$(date +%Y%m%d-%H%M).txt"
|
||||
: >"$REPORT"; : >"${REPORT}.tsv"
|
||||
|
||||
emit "# Line speed probe — $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||
emit "# interface=${IFACE} duration=${DURATION}s"
|
||||
|
||||
if [[ $# -ge 1 ]]; then
|
||||
probe "Custom target" "$1"
|
||||
else
|
||||
probe "PL mirror (icm.edu.pl), single stream" "$MIRROR_PL"
|
||||
probe "Geo mirror (pkgbuild), single stream" "$MIRROR_GEO"
|
||||
probe "Four mirrors in parallel" "$MIRROR_PL" "$MIRROR_GEO" \
|
||||
"$MIRROR_RACK" "$MIRROR_KERNEL"
|
||||
fi
|
||||
|
||||
local best
|
||||
best=$(awk -F'\t' 'BEGIN{m=0} $3>m{m=$3} END{print m+0}' "${REPORT}.tsv")
|
||||
emit ""
|
||||
emit "## Verdict"
|
||||
emit "Best sustained figure this run: ${best} Mbps"
|
||||
case "$(verdict_for "$best")" in
|
||||
LINE_OK)
|
||||
emit "=> Line is healthy. Any slowness is the download client, not the link."
|
||||
emit " Compare clients with steam-download-duty.sh." ;;
|
||||
PARTIAL)
|
||||
emit "=> Better than a congested evening but short of gigabit."
|
||||
emit " Re-run off-peak before drawing conclusions." ;;
|
||||
*)
|
||||
emit "=> Low. Re-run off-peak; if still low, capture a multi-stream figure"
|
||||
emit " before blaming the ISP — single streams under-report badly." ;;
|
||||
esac
|
||||
|
||||
ln -sfn "$REPORT" "${REPORT_DIR}/latest.txt"
|
||||
emit ""
|
||||
emit "Report: ${REPORT}"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
126
linux_configuration/scripts/diagnostics/steam-download-duty.sh
Executable file
126
linux_configuration/scripts/diagnostics/steam-download-duty.sh
Executable file
@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# steam-download-duty.sh — measure a download's DUTY CYCLE, not its average.
|
||||
#
|
||||
# Why this exists: on this machine the link is not bandwidth-limited, but the
|
||||
# Steam client is. Sampling at 1 s shows Steam alternating between
|
||||
# multi-hundred-Mbps bursts and multi-second stalls in which it uses ~1% CPU,
|
||||
# no disk, and transmits only keepalives. Any averaged "Mbps" figure is really
|
||||
# burst_rate x duty_cycle
|
||||
# and hides which of the two factors is actually wrong.
|
||||
#
|
||||
# Measured 2026-07-19 (see the diagnosis writeup): Steam client 38% duty /
|
||||
# ~170 Mbps effective, vs depotdownloader -max-downloads 32 at 97% duty /
|
||||
# 696 Mbps effective on the same line, same CDN, same minute.
|
||||
#
|
||||
# Use this to compare download clients or regions objectively — the Steam UI's
|
||||
# averaged number cannot distinguish "slow link" from "idle client".
|
||||
#
|
||||
# Usage: steam-download-duty.sh [seconds] (default 60)
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IFACE="${IFACE:-enp6s0}"
|
||||
IDLE_THRESHOLD_MBPS="${IDLE_THRESHOLD_MBPS:-20}" # below this = stalled second
|
||||
|
||||
# --- pure helpers (unit-tested by linux_configuration/tests) ----------------
|
||||
|
||||
# Convert a byte delta observed over N seconds into Mbps (integer).
|
||||
bytes_to_mbps() {
|
||||
local delta="$1" secs="$2"
|
||||
(( secs > 0 )) || { printf '0'; return; }
|
||||
printf '%d' $(( delta * 8 / secs / 1000000 ))
|
||||
}
|
||||
|
||||
# Percentage of sampled seconds that carried traffic.
|
||||
duty_pct() {
|
||||
local active="$1" total="$2"
|
||||
(( total > 0 )) || { printf '0'; return; }
|
||||
printf '%d' $(( active * 100 / total ))
|
||||
}
|
||||
|
||||
# Integer mean of the numeric arguments (0 when given none).
|
||||
mean_of() {
|
||||
(( $# > 0 )) || { printf '0'; return; }
|
||||
local sum=0 v
|
||||
for v in "$@"; do sum=$(( sum + v )); done
|
||||
printf '%d' $(( sum / $# ))
|
||||
}
|
||||
|
||||
# Classify a run. Kept separate from printing so it can be asserted on.
|
||||
# LINK_LIMITED - transferring almost always; the rate is the rate
|
||||
# CLIENT_IDLE - fast when active but idle a lot => client's fault
|
||||
# MIXED - neither clean bursts nor high duty
|
||||
classify() {
|
||||
local duty="$1" burst="$2"
|
||||
if (( duty >= 80 )); then
|
||||
printf 'LINK_LIMITED'
|
||||
elif (( burst >= 400 )); then
|
||||
printf 'CLIENT_IDLE'
|
||||
else
|
||||
printf 'MIXED'
|
||||
fi
|
||||
}
|
||||
|
||||
# --- measurement -----------------------------------------------------------
|
||||
|
||||
rx_bytes() { cat "/sys/class/net/${IFACE}/statistics/rx_bytes"; }
|
||||
|
||||
main() {
|
||||
local total="${1:-60}"
|
||||
local prev cur mbps active=0 peak=0
|
||||
local -a samples=() active_samples=() trace=()
|
||||
|
||||
printf 'Sampling %ss at 1s resolution on %s ...\n\n' "$total" "$IFACE"
|
||||
prev=$(rx_bytes)
|
||||
|
||||
local i
|
||||
for (( i = 1; i <= total; i++ )); do
|
||||
sleep 1
|
||||
cur=$(rx_bytes)
|
||||
mbps=$(bytes_to_mbps $(( cur - prev )) 1)
|
||||
prev=$cur
|
||||
samples+=("$mbps")
|
||||
if (( mbps > peak )); then peak=$mbps; fi
|
||||
|
||||
if (( mbps >= IDLE_THRESHOLD_MBPS )); then
|
||||
active=$(( active + 1 )); active_samples+=("$mbps"); trace+=("#")
|
||||
else
|
||||
trace+=(".")
|
||||
fi
|
||||
done
|
||||
|
||||
printf 'pattern (# = transferring, . = stalled):\n '
|
||||
printf '%s' "${trace[@]}"
|
||||
printf '\n\n'
|
||||
|
||||
local duty burst overall
|
||||
duty=$(duty_pct "$active" "$total")
|
||||
burst=$(mean_of ${active_samples[@]+"${active_samples[@]}"})
|
||||
overall=$(mean_of ${samples[@]+"${samples[@]}"})
|
||||
|
||||
printf 'duty cycle : %d%% (%ds transferring / %ds stalled)\n' \
|
||||
"$duty" "$active" "$(( total - active ))"
|
||||
printf 'rate when active: %d Mbps (peak %d)\n' "$burst" "$peak"
|
||||
printf 'effective avg : %d Mbps\n\n' "$overall"
|
||||
|
||||
case "$(classify "$duty" "$burst")" in
|
||||
LINK_LIMITED)
|
||||
printf 'VERDICT: link is the limit — %d Mbps is close to what you get.\n' "$burst" ;;
|
||||
CLIENT_IDLE)
|
||||
printf 'VERDICT: NOT bandwidth-limited. Bursts reach %d Mbps but the\n' "$burst"
|
||||
printf ' transfer is idle %d%% of the time. At 100%% duty this\n' "$(( 100 - duty ))"
|
||||
printf ' link would give ~%d Mbps. Compare clients/regions by\n' "$burst"
|
||||
printf ' duty cycle, not by the averaged UI number.\n' ;;
|
||||
*)
|
||||
printf 'VERDICT: mixed — bursts only reach %d Mbps and duty is %d%%.\n' \
|
||||
"$burst" "$duty" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Only run when executed directly, so tests can source the helpers above.
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
155
linux_configuration/tests/test_network_diagnostics.bats
Executable file
155
linux_configuration/tests/test_network_diagnostics.bats
Executable file
@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bats
|
||||
# Unit tests for the pure helpers in scripts/diagnostics/.
|
||||
#
|
||||
# Requires: bats (extra/bats). Run with:
|
||||
# bats linux_configuration/tests/test_network_diagnostics.bats
|
||||
#
|
||||
# Only the arithmetic and classification logic is covered. The sampling loops
|
||||
# are deliberately untested: they read live NIC counters and sleep in real
|
||||
# time, so testing them would assert against mocks rather than behaviour. The
|
||||
# helpers below are where every off-by-one actually lives.
|
||||
|
||||
setup() {
|
||||
REPO_DIR="$(cd -- "$(dirname -- "$BATS_TEST_FILENAME")/.." && pwd)"
|
||||
DIAG_DIR="$REPO_DIR/scripts/diagnostics"
|
||||
# Both scripts guard main() behind a BASH_SOURCE check, so sourcing them
|
||||
# loads the helpers without starting a measurement run.
|
||||
# shellcheck source=/dev/null
|
||||
source "$DIAG_DIR/steam-download-duty.sh"
|
||||
# shellcheck source=/dev/null
|
||||
source "$DIAG_DIR/line-speed-probe.sh"
|
||||
}
|
||||
|
||||
# --- bytes_to_mbps ---------------------------------------------------------
|
||||
|
||||
@test "bytes_to_mbps: 125MB in 1s is exactly 1000 Mbps" {
|
||||
[ "$(bytes_to_mbps 125000000 1)" -eq 1000 ]
|
||||
}
|
||||
|
||||
@test "bytes_to_mbps: averages over a multi-second window" {
|
||||
[ "$(bytes_to_mbps 125000000 10)" -eq 100 ]
|
||||
}
|
||||
|
||||
@test "bytes_to_mbps: no traffic yields zero" {
|
||||
[ "$(bytes_to_mbps 0 5)" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "bytes_to_mbps: a zero-second window returns 0 instead of dividing by zero" {
|
||||
[ "$(bytes_to_mbps 125000000 0)" -eq 0 ]
|
||||
}
|
||||
|
||||
# --- duty_pct --------------------------------------------------------------
|
||||
|
||||
@test "duty_pct: reproduces the measured Steam run (22s of 60)" {
|
||||
[ "$(duty_pct 22 60)" -eq 36 ]
|
||||
}
|
||||
|
||||
@test "duty_pct: fully active is 100" {
|
||||
[ "$(duty_pct 60 60)" -eq 100 ]
|
||||
}
|
||||
|
||||
@test "duty_pct: fully stalled is 0" {
|
||||
[ "$(duty_pct 0 60)" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "duty_pct: zero total returns 0 instead of dividing by zero" {
|
||||
[ "$(duty_pct 5 0)" -eq 0 ]
|
||||
}
|
||||
|
||||
# --- mean_of ---------------------------------------------------------------
|
||||
|
||||
@test "mean_of: averages several samples" {
|
||||
[ "$(mean_of 100 200 300)" -eq 200 ]
|
||||
}
|
||||
|
||||
@test "mean_of: no samples yields zero" {
|
||||
[ "$(mean_of)" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "mean_of: a single sample is itself" {
|
||||
[ "$(mean_of 7)" -eq 7 ]
|
||||
}
|
||||
|
||||
@test "mean_of: integer division truncates rather than rounding" {
|
||||
[ "$(mean_of 2 3)" -eq 2 ]
|
||||
}
|
||||
|
||||
# --- classify --------------------------------------------------------------
|
||||
|
||||
@test "classify: high duty means the link is the ceiling (depotdownloader run)" {
|
||||
[ "$(classify 97 712)" = "LINK_LIMITED" ]
|
||||
}
|
||||
|
||||
@test "classify: duty boundary at 80 counts as link-limited" {
|
||||
[ "$(classify 80 50)" = "LINK_LIMITED" ]
|
||||
}
|
||||
|
||||
@test "classify: low duty with fast bursts is the Steam-client signature" {
|
||||
[ "$(classify 36 474)" = "CLIENT_IDLE" ]
|
||||
}
|
||||
|
||||
@test "classify: burst boundary at 400 counts as client-idle" {
|
||||
[ "$(classify 79 400)" = "CLIENT_IDLE" ]
|
||||
}
|
||||
|
||||
@test "classify: slow bursts and low duty is neither diagnosis" {
|
||||
[ "$(classify 30 399)" = "MIXED" ]
|
||||
}
|
||||
|
||||
# --- sustained_of ----------------------------------------------------------
|
||||
|
||||
@test "sustained_of: discards the first half (slow start and burst allowance)" {
|
||||
[ "$(sustained_of 500 500 100 100)" -eq 100 ]
|
||||
}
|
||||
|
||||
@test "sustained_of: odd counts keep 2 of 3, dropping only the first" {
|
||||
[ "$(sustained_of 10 20 30)" -eq 25 ]
|
||||
}
|
||||
|
||||
@test "sustained_of: a single sample is itself" {
|
||||
[ "$(sustained_of 5)" -eq 5 ]
|
||||
}
|
||||
|
||||
@test "sustained_of: no samples yields zero" {
|
||||
[ "$(sustained_of)" -eq 0 ]
|
||||
}
|
||||
|
||||
# --- max_of ----------------------------------------------------------------
|
||||
|
||||
@test "max_of: finds the peak" {
|
||||
[ "$(max_of 100 986 400)" -eq 986 ]
|
||||
}
|
||||
|
||||
@test "max_of: no samples yields zero" {
|
||||
[ "$(max_of)" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "max_of: a single sample is itself" {
|
||||
[ "$(max_of 42)" -eq 42 ]
|
||||
}
|
||||
|
||||
# --- verdict_for -----------------------------------------------------------
|
||||
|
||||
@test "verdict_for: the depotdownloader result reads as a healthy line" {
|
||||
[ "$(verdict_for 696)" = "LINE_OK" ]
|
||||
}
|
||||
|
||||
@test "verdict_for: 600 is the healthy boundary" {
|
||||
[ "$(verdict_for 600)" = "LINE_OK" ]
|
||||
}
|
||||
|
||||
@test "verdict_for: just below the healthy boundary is partial" {
|
||||
[ "$(verdict_for 599)" = "PARTIAL" ]
|
||||
}
|
||||
|
||||
@test "verdict_for: 350 is the partial boundary" {
|
||||
[ "$(verdict_for 350)" = "PARTIAL" ]
|
||||
}
|
||||
|
||||
@test "verdict_for: just below the partial boundary is slow" {
|
||||
[ "$(verdict_for 349)" = "SLOW" ]
|
||||
}
|
||||
|
||||
@test "verdict_for: the measured Steam effective rate reads as slow" {
|
||||
[ "$(verdict_for 170)" = "SLOW" ]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user