- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
609 lines
26 KiB
Python
609 lines
26 KiB
Python
"""Tests for speedtest.py — target vetting, rate accounting, pass sizing, and
|
|
JSON we did not write.
|
|
|
|
Run: python3 -m unittest discover -s test
|
|
"""
|
|
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
|
|
|
|
class UntrustedTargets(unittest.TestCase):
|
|
"""fast.com nominates its own download hosts, so that JSON decides
|
|
what this daemon connects to and is hostile input, not a server list.
|
|
|
|
Literal addresses throughout, so nothing here touches DNS.
|
|
"""
|
|
|
|
def setUp(self):
|
|
from nexthopd.speedtest import vet_target
|
|
self.vet = vet_target
|
|
|
|
def test_plaintext_is_refused(self):
|
|
self.assertIsNone(self.vet("http://93.184.216.34/download"))
|
|
|
|
def test_non_http_schemes_are_refused(self):
|
|
for url in ("file:///etc/passwd", "ftp://93.184.216.34/x",
|
|
"gopher://93.184.216.34/x", "scp://93.184.216.34/x",
|
|
"dict://93.184.216.34/x"):
|
|
self.assertIsNone(self.vet(url), url)
|
|
|
|
def test_loopback_is_refused(self):
|
|
for url in ("https://127.0.0.1/x", "https://127.1.2.3/x",
|
|
"https://[::1]/x"):
|
|
self.assertIsNone(self.vet(url), url)
|
|
|
|
def test_private_ranges_are_refused(self):
|
|
for url in ("https://192.168.1.1/x", "https://10.0.0.1/x",
|
|
"https://172.16.4.2/x", "https://[fd00::1]/x"):
|
|
self.assertIsNone(self.vet(url), url)
|
|
|
|
def test_cloud_metadata_address_is_refused(self):
|
|
# The link-local address every SSRF write-up ends at.
|
|
self.assertIsNone(self.vet("https://169.254.169.254/latest/meta-data/"))
|
|
|
|
def test_ipv4_mapped_private_address_is_refused(self):
|
|
# ::ffff:192.168.1.1 is a private address wearing an IPv6 coat.
|
|
self.assertIsNone(self.vet("https://[::ffff:192.168.1.1]/x"))
|
|
|
|
def test_unspecified_and_broadcast_refused(self):
|
|
self.assertIsNone(self.vet("https://0.0.0.0/x"))
|
|
self.assertIsNone(self.vet("https://255.255.255.255/x"))
|
|
|
|
def test_garbage_is_refused_without_raising(self):
|
|
for url in ("", "not a url", "https://", "https:///x", "https://:443/x"):
|
|
self.assertIsNone(self.vet(url), repr(url))
|
|
|
|
def test_public_https_is_accepted_and_pinned(self):
|
|
got = self.vet("https://8.8.8.8/download?size=25000000")
|
|
self.assertIsNotNone(got)
|
|
url, resolve = got
|
|
self.assertEqual(url, "https://8.8.8.8/download?size=25000000")
|
|
# The vetted address is pinned, so curl cannot resolve the name
|
|
# again and be handed a different one.
|
|
self.assertEqual(resolve, "8.8.8.8:443:8.8.8.8")
|
|
|
|
def test_explicit_port_is_carried_into_the_pin(self):
|
|
got = self.vet("https://8.8.8.8:8443/x")
|
|
self.assertIsNotNone(got)
|
|
self.assertEqual(got[1], "8.8.8.8:8443:8.8.8.8")
|
|
|
|
def test_a_bad_target_costs_one_candidate_not_the_test(self):
|
|
urls = ["http://93.184.216.34/a", "https://169.254.169.254/b",
|
|
"https://8.8.8.8/c"]
|
|
vetted = [v for v in (self.vet(u) for u in urls) if v]
|
|
self.assertEqual(len(vetted), 1)
|
|
self.assertEqual(vetted[0][0], "https://8.8.8.8/c")
|
|
|
|
def test_curl_is_invoked_with_a_scheme_floor(self):
|
|
# Belt and braces beside the vetting: curl itself refuses
|
|
# anything but TLS, whatever it is handed.
|
|
import inspect
|
|
from nexthopd import speedtest
|
|
src = inspect.getsource(speedtest._curl)
|
|
self.assertIn('"--proto", "=https"', src)
|
|
|
|
|
|
class PeakSizing(unittest.TestCase):
|
|
"""The sustained pass is sized from the estimate, floored and capped."""
|
|
|
|
def test_sized_for_ten_seconds_at_measured_rate(self):
|
|
from nexthopd import speedtest
|
|
# 160 Mbps line over 4 streams: each stream carries 40 Mbps.
|
|
n = speedtest._sized_pass(160.0 / speedtest.PEAK_STREAMS,
|
|
speedtest.PEAK_DOWN_FLOOR,
|
|
speedtest.CLOUDFLARE_DOWN_MAX)
|
|
self.assertEqual(n, 50_000_000)
|
|
self.assertAlmostEqual(speedtest._pass_seconds(40.0, n), 10.0)
|
|
|
|
def test_slow_line_stays_small(self):
|
|
from nexthopd import speedtest
|
|
n = speedtest._sized_pass(10.0, speedtest.PEAK_DOWN_FLOOR,
|
|
speedtest.CLOUDFLARE_DOWN_MAX)
|
|
self.assertEqual(n, 12_500_000)
|
|
|
|
def test_caps_bound_both_directions(self):
|
|
from nexthopd import speedtest
|
|
# __down 403s at 100 MB and above — the per-stream cap must stay under.
|
|
self.assertLess(speedtest.CLOUDFLARE_DOWN_MAX, 100_000_000)
|
|
self.assertEqual(speedtest._sized_pass(10_000.0, speedtest.PEAK_DOWN_FLOOR,
|
|
speedtest.CLOUDFLARE_DOWN_MAX),
|
|
speedtest.CLOUDFLARE_DOWN_MAX)
|
|
self.assertEqual(speedtest._sized_pass(0.1, speedtest.PEAK_UP_FLOOR,
|
|
speedtest.PEAK_UP_CAP),
|
|
speedtest.PEAK_UP_FLOOR)
|
|
|
|
|
|
class RemoteJsonShapes(unittest.TestCase):
|
|
"""The peak engines parse JSON we did not write. A wrong shape is a
|
|
failed engine, never an exception escaping the worker thread."""
|
|
|
|
def test_fast_com_wrong_shapes_fail_closed(self):
|
|
from nexthopd import speedtest
|
|
|
|
class R:
|
|
returncode = 0
|
|
|
|
orig = speedtest._curl
|
|
self.addCleanup(setattr, speedtest, "_curl", orig)
|
|
for body in ('[1, 2]', '{"targets": [1, 2]}', '{"targets": "x"}',
|
|
'{"targets": [{"url": 5}]}', 'null'):
|
|
r = R()
|
|
r.stdout = body
|
|
speedtest._curl = lambda args, timeout, r=r: r
|
|
self.assertIsNone(speedtest._peak_fast(), body)
|
|
|
|
def test_ookla_wrong_shapes_fail_closed(self):
|
|
from nexthopd import speedtest
|
|
|
|
class R:
|
|
returncode = 0
|
|
|
|
self.addCleanup(setattr, speedtest.subprocess, "run",
|
|
speedtest.subprocess.run)
|
|
self.addCleanup(setattr, speedtest.shutil, "which",
|
|
speedtest.shutil.which)
|
|
speedtest.shutil.which = lambda name: "/usr/bin/true"
|
|
for body in ('{"download": "x"}', '[1]', 'null',
|
|
'{"download": {"bandwidth": "fast"}, "upload": {"bandwidth": 1},'
|
|
' "ping": {"latency": 1}, "server": {}}'):
|
|
r = R()
|
|
r.stdout = body
|
|
speedtest.subprocess.run = lambda *a, r=r, **k: r
|
|
self.assertIsNone(speedtest._peak_ookla(), body)
|
|
|
|
|
|
class PayloadTimedRate(unittest.TestCase):
|
|
"""What the rate is divided by.
|
|
|
|
curl's own speed_download divides the bytes by the WHOLE request, setup
|
|
included. That biases every reading low by a share that grows with the
|
|
line rate, because the setup cost is fixed while the useful part of the
|
|
transfer keeps getting shorter — so the faster the connection, the worse
|
|
it reads. A user with an 893 Mbps line saw ~220 (issue #2), and the check
|
|
could not report above ~480 on a scale whose top anchors are 500 and 750.
|
|
|
|
The property worth pinning is not any one number: it is that the error no
|
|
longer depends on the line.
|
|
"""
|
|
|
|
def rate(self, size, setup_s, payload_s):
|
|
from nexthopd import speedtest
|
|
return speedtest._rate_over_payload(size, setup_s + payload_s, setup_s)
|
|
|
|
def test_setup_time_is_not_counted_as_transfer(self):
|
|
# 1.5 MB (12 Mbit) carried in 100 ms, after 90 ms of connect and TLS.
|
|
self.assertAlmostEqual(self.rate(1_500_000, 0.09, 0.10), 120.0, places=6)
|
|
|
|
def test_the_error_no_longer_grows_with_the_line(self):
|
|
# The check's own shape: 3 MB per stream over four streams, one fixed
|
|
# 90 ms of setup, three very different lines. Timed over the payload
|
|
# each comes back as itself; timed over the whole request the gigabit
|
|
# one would read barely half its rate.
|
|
for line_mbps in (50.0, 400.0, 1000.0):
|
|
per_stream = line_mbps / 4
|
|
payload = (3_000_000 * 8 / 1e6) / per_stream
|
|
got = self.rate(3_000_000, 0.09, payload) * 4
|
|
self.assertAlmostEqual(got / line_mbps, 1.0, places=6,
|
|
msg="%s Mbps read as %s" % (line_mbps, got))
|
|
|
|
def test_the_shipping_sizing_can_time_a_gigabit_line(self):
|
|
# 3 MB over four streams is 96 ms of payload per stream at 1 Gbps —
|
|
# above the floor, so the figure stands. It stops being timeable a
|
|
# little under 2 Gbps, and is then withheld rather than guessed at.
|
|
from nexthopd import speedtest
|
|
per_stream_at_1g = (3_000_000 * 8 / 1e6) / (1000.0 / 4)
|
|
self.assertGreater(per_stream_at_1g, speedtest.MIN_TIMED_WINDOW_S)
|
|
per_stream_at_2g = (3_000_000 * 8 / 1e6) / (2000.0 / 4)
|
|
self.assertLess(per_stream_at_2g, speedtest.MIN_TIMED_WINDOW_S)
|
|
|
|
def test_a_window_too_short_to_time_is_withheld(self):
|
|
# 3 MB at 2 Gbps is 12 ms of payload. Dividing by a window that small
|
|
# publishes the timing error, not the line — and in the flattering
|
|
# direction. Withhold instead, the way every other figure here does.
|
|
self.assertIsNone(self.rate(3_000_000, 0.09, 0.012))
|
|
|
|
def test_the_floor_is_the_window_not_the_total(self):
|
|
# A long setup does not make a short payload measurable.
|
|
self.assertIsNone(self.rate(3_000_000, 5.0, 0.01))
|
|
self.assertIsNotNone(self.rate(3_000_000, 0.0, 0.20))
|
|
|
|
def test_nothing_transferred_is_not_a_rate(self):
|
|
self.assertIsNone(self.rate(0, 0.09, 1.0))
|
|
|
|
def test_an_impossible_window_is_withheld_not_negated(self):
|
|
from nexthopd import speedtest
|
|
self.assertIsNone(speedtest._rate_over_payload(1_000_000, 0.05, 0.20))
|
|
|
|
|
|
class RateAccountingContract(unittest.TestCase):
|
|
"""The curl invocations have to keep asking for the fields we divide by."""
|
|
|
|
def source(self, fn):
|
|
import inspect
|
|
return inspect.getsource(fn)
|
|
|
|
def test_downloads_are_timed_from_the_first_byte(self):
|
|
from nexthopd import speedtest
|
|
for fn in (speedtest._curl_timed_download, speedtest._parallel_download):
|
|
src = self.source(fn)
|
|
self.assertIn("%{time_starttransfer}", src)
|
|
self.assertNotIn("%{speed_download}", src)
|
|
|
|
def test_uploads_are_timed_from_the_handshake(self):
|
|
# Not time_starttransfer: on a POST that is the first byte of the
|
|
# RESPONSE, which arrives after the body has already gone. Measured
|
|
# against a local server: starttransfer landed part way through a
|
|
# 2 MB body, so it cannot mark where the payload began.
|
|
from nexthopd import speedtest
|
|
src = self.source(speedtest._curl_timed_upload)
|
|
self.assertIn("%{time_appconnect}", src)
|
|
self.assertNotIn("%{speed_upload}", src)
|
|
|
|
|
|
class ParallelStreamAccounting(unittest.TestCase):
|
|
"""Summing streams, when some of them cannot be timed."""
|
|
|
|
class FakeProc:
|
|
def __init__(self, out, rc=0):
|
|
self._out, self.returncode = out, rc
|
|
|
|
def communicate(self, timeout=None):
|
|
return self._out, None
|
|
|
|
def kill(self):
|
|
pass
|
|
|
|
def wait(self):
|
|
pass
|
|
|
|
def parallel(self, outputs):
|
|
from nexthopd import speedtest
|
|
queue = list(outputs)
|
|
real_popen = speedtest.subprocess.Popen
|
|
speedtest.subprocess.Popen = lambda *a, **k: self.FakeProc(queue.pop(0))
|
|
try:
|
|
return speedtest._parallel_download("https://x/y", len(outputs), 5)
|
|
finally:
|
|
speedtest.subprocess.Popen = real_popen
|
|
|
|
def test_overlapping_streams_are_the_bytes_over_the_time_they_took(self):
|
|
# Two streams, each 3 MB carried in the same 200 ms window: 6 MB in
|
|
# 0.2 s is 240 Mbps. Note this is NOT 120 + 120 by coincidence of
|
|
# them overlapping exactly — see the staggered case below.
|
|
out = "3000000 0.29 0.09"
|
|
mbps, size = self.parallel([out, out])
|
|
self.assertAlmostEqual(mbps, 240.0, delta=1.0)
|
|
self.assertEqual(size, 6_000_000)
|
|
|
|
def test_a_stream_that_outlives_the_others_does_not_double_the_line(self):
|
|
"""The defect this replaced.
|
|
|
|
Streams finish tens to hundreds of milliseconds apart, and one left
|
|
running alone measures the whole line. Summing that with what its
|
|
siblings measured while sharing counts the same wire twice. Here: one
|
|
stream carries 3 MB in 0.2 s, a second carries 3 MB in a 0.2 s window
|
|
that starts after the first has finished. Six MB crossed the wire in
|
|
0.4 s, which is 120 Mbps. Summing would have said 240.
|
|
"""
|
|
mbps, size = self.parallel(["3000000 0.29 0.09", "3000000 0.49 0.29"])
|
|
self.assertAlmostEqual(mbps, 120.0, delta=2.0)
|
|
self.assertEqual(size, 6_000_000)
|
|
|
|
def test_a_gap_between_streams_is_not_billed_as_throughput(self):
|
|
# Union, not first-start-to-last-finish: nothing crossed the wire in
|
|
# the idle second between them, and it must not be charged as if the
|
|
# line were slow.
|
|
mbps, _ = self.parallel(["3000000 0.29 0.09", "3000000 1.49 1.29"])
|
|
self.assertAlmostEqual(mbps, 120.0, delta=2.0)
|
|
|
|
def test_every_stream_untimeable_reports_no_rate_not_zero(self):
|
|
# Zero is a claim about the line. None is the absence of one, and it
|
|
# is what the score treats as "no Speed component" rather than as a
|
|
# connection that carries nothing.
|
|
mbps, size = self.parallel(["3000000 0.101 0.09", "3000000 0.101 0.09"])
|
|
self.assertIsNone(mbps)
|
|
self.assertEqual(size, 6_000_000)
|
|
|
|
def test_bytes_are_counted_even_when_the_rate_is_withheld(self):
|
|
# The data budget was spent whether or not it produced a number.
|
|
_, size = self.parallel(["3000000 0.101 0.09", "3000000 0.29 0.09"])
|
|
self.assertEqual(size, 6_000_000)
|
|
|
|
|
|
class ParallelUploadAccounting(unittest.TestCase):
|
|
"""Upload was measured on one stream, in both the check and the peak.
|
|
|
|
The download learned in 0.1.x that a single TCP stream reads its own
|
|
ceiling rather than the line's, and grew `_parallel_download` for it. The
|
|
upload never did. Measured against speed.cloudflare.com on a ~450 Mbps
|
|
line: the same 2 MB carried by four streams read 36% higher than by one.
|
|
"""
|
|
|
|
class FakeProc:
|
|
def __init__(self, out, rc=0):
|
|
self._out, self.returncode, self.fed = out, rc, None
|
|
|
|
def communicate(self, input=None, timeout=None):
|
|
self.fed = input
|
|
return self._out, None
|
|
|
|
def kill(self):
|
|
pass
|
|
|
|
def wait(self):
|
|
pass
|
|
|
|
def parallel(self, outputs, per_stream=1_000_000):
|
|
from nexthopd import speedtest
|
|
queue = list(outputs)
|
|
made = []
|
|
real = speedtest.subprocess.Popen
|
|
|
|
def fake(*a, **k):
|
|
proc = self.FakeProc(queue.pop(0))
|
|
made.append(proc)
|
|
return proc
|
|
|
|
speedtest.subprocess.Popen = fake
|
|
try:
|
|
got = speedtest._parallel_upload("https://x/y", per_stream,
|
|
len(outputs), 5)
|
|
finally:
|
|
speedtest.subprocess.Popen = real
|
|
return got, made
|
|
|
|
def test_overlapping_streams_are_the_bytes_over_the_time_they_took(self):
|
|
out = b"1000000 0.29 0.09" # 4 MB in a shared 200 ms window
|
|
(mbps, size), _ = self.parallel([out, out, out, out])
|
|
self.assertAlmostEqual(mbps, 160.0, delta=1.0)
|
|
self.assertEqual(size, 4_000_000)
|
|
|
|
def test_a_stream_that_outlives_the_others_does_not_double_the_line(self):
|
|
(mbps, size), _ = self.parallel(
|
|
[b"1000000 0.29 0.09", b"1000000 0.49 0.29"])
|
|
self.assertAlmostEqual(mbps, 40.0, delta=1.0)
|
|
self.assertEqual(size, 2_000_000)
|
|
|
|
def test_every_stream_is_fed_the_same_buffer(self):
|
|
# One body, N readers: the memory cost is a stream's worth, not N.
|
|
out = b"1000000 0.29 0.09"
|
|
_, made = self.parallel([out, out, out])
|
|
self.assertEqual(len(made), 3)
|
|
self.assertEqual(len(made[0].fed), 1_000_000)
|
|
for proc in made[1:]:
|
|
self.assertIs(proc.fed, made[0].fed)
|
|
|
|
def test_every_stream_untimeable_reports_no_rate_not_zero(self):
|
|
out = b"1000000 0.101 0.09" # 11 ms window: below the floor
|
|
(mbps, size), _ = self.parallel([out, out])
|
|
self.assertIsNone(mbps)
|
|
self.assertEqual(size, 2_000_000)
|
|
|
|
def test_a_failed_stream_costs_its_own_share_and_no_more(self):
|
|
from nexthopd import speedtest
|
|
queue = [b"1000000 0.29 0.09", b""]
|
|
made = []
|
|
real = speedtest.subprocess.Popen
|
|
|
|
def fake(*a, **k):
|
|
proc = self.FakeProc(queue.pop(0), rc=0 if len(made) == 0 else 1)
|
|
made.append(proc)
|
|
return proc
|
|
|
|
speedtest.subprocess.Popen = fake
|
|
try:
|
|
mbps, size = speedtest._parallel_upload("https://x/y", 1_000_000, 2, 5)
|
|
finally:
|
|
speedtest.subprocess.Popen = real
|
|
self.assertAlmostEqual(mbps, 40.0, places=4)
|
|
self.assertEqual(size, 1_000_000)
|
|
|
|
def test_the_upload_invocation_keeps_its_scheme_floor_and_timing_field(self):
|
|
from nexthopd import speedtest
|
|
argv = speedtest._upload_argv("https://x/y", 30)
|
|
self.assertIn("--proto", argv)
|
|
self.assertIn("=https", argv)
|
|
joined = " ".join(argv)
|
|
self.assertIn("%{time_appconnect}", joined)
|
|
self.assertNotIn("%{speed_upload}", joined)
|
|
self.assertNotIn("%{time_starttransfer}", joined)
|
|
|
|
|
|
class ContentStreamSizing(unittest.TestCase):
|
|
"""Each stream is sized for a duration, not by dividing a budget.
|
|
|
|
Dividing a fixed budget is how both speed defects worked: more streams
|
|
meant shorter streams, and a stream too short to time is withheld. It also
|
|
charged the wrong people — 16 MB is a quarter-second on a fast line and
|
|
nine seconds of a saturated link on a 10 Mbps one, every hour.
|
|
"""
|
|
|
|
def sized(self, hint, cap=None, streams=4):
|
|
from nexthopd import speedtest
|
|
if cap is None:
|
|
cap = speedtest.CONTENT_DOWN_STREAM_CAP
|
|
return speedtest.content_stream_bytes(hint, streams, cap)
|
|
|
|
def test_a_slow_line_is_asked_for_far_less(self):
|
|
from nexthopd import speedtest
|
|
# 10 Mbps over four streams: 2.5 Mbps a stream, half a second of it.
|
|
self.assertEqual(self.sized(10.0), speedtest.CONTENT_STREAM_FLOOR)
|
|
# 100 Mbps: 25 Mbps a stream, so about 1.5 MB - still under the cap.
|
|
self.assertLess(self.sized(100.0), speedtest.CONTENT_DOWN_STREAM_CAP)
|
|
self.assertGreater(self.sized(100.0), speedtest.CONTENT_STREAM_FLOOR)
|
|
|
|
def test_a_fast_line_reaches_the_cap_and_stops(self):
|
|
from nexthopd import speedtest
|
|
self.assertEqual(self.sized(1000.0), speedtest.CONTENT_DOWN_STREAM_CAP)
|
|
self.assertEqual(self.sized(10000.0), speedtest.CONTENT_DOWN_STREAM_CAP)
|
|
|
|
def test_the_size_never_leaves_its_bounds(self):
|
|
from nexthopd import speedtest
|
|
for hint in (0.001, 1, 10, 50, 250, 900, 5000):
|
|
n = self.sized(hint)
|
|
self.assertGreaterEqual(n, speedtest.CONTENT_STREAM_FLOOR)
|
|
self.assertLessEqual(n, speedtest.CONTENT_DOWN_STREAM_CAP)
|
|
|
|
def test_no_hint_sends_the_cap(self):
|
|
# The first check on a network has nothing to size against, and it is
|
|
# the one that produces the hint every later check uses.
|
|
from nexthopd import speedtest
|
|
for hint in (None, 0, -5):
|
|
self.assertEqual(self.sized(hint), speedtest.CONTENT_DOWN_STREAM_CAP)
|
|
|
|
def test_a_stream_sized_for_a_rate_can_be_timed_at_that_rate(self):
|
|
"""The property that makes the whole thing safe.
|
|
|
|
Whatever the hint, the stream it produces carries payload for longer
|
|
than MIN_TIMED_WINDOW_S at that same rate — so sizing can never
|
|
produce a transfer its own accounting would then withhold.
|
|
"""
|
|
from nexthopd import speedtest
|
|
for cap in (speedtest.CONTENT_DOWN_STREAM_CAP,
|
|
speedtest.CONTENT_UP_STREAM_CAP):
|
|
for hint in (1, 10, 100, 400, 900, 1200):
|
|
n = self.sized(hint, cap=cap)
|
|
payload_s = (n * 8 / 1e6) / (hint / 4)
|
|
self.assertGreaterEqual(
|
|
payload_s, speedtest.MIN_TIMED_WINDOW_S,
|
|
"hint %s Mbps, cap %s -> %s bytes is %.3f s" % (
|
|
hint, cap, n, payload_s))
|
|
|
|
def test_the_line_speed_each_direction_can_still_time(self):
|
|
"""The ceiling, pinned. Past it the figure is withheld, not guessed.
|
|
|
|
Raising the upload cap moved this from ~640 Mbps to ~1.28 Gbps, which
|
|
covers the 820 Mbps line reported in issue #2 — and it costs nothing
|
|
on a slower line, because the cap is now a ceiling rather than a
|
|
constant everyone pays.
|
|
"""
|
|
from nexthopd import speedtest
|
|
streams = 4
|
|
|
|
def ceiling(cap):
|
|
return (cap * 8 / 1e6) / speedtest.MIN_TIMED_WINDOW_S * streams
|
|
|
|
self.assertAlmostEqual(ceiling(speedtest.CONTENT_DOWN_STREAM_CAP),
|
|
1920.0, places=1)
|
|
self.assertAlmostEqual(ceiling(speedtest.CONTENT_UP_STREAM_CAP),
|
|
1280.0, places=1)
|
|
|
|
def test_the_whole_check_stays_within_its_declared_budget(self):
|
|
"""What the README promises: up to ~20 MB, and only on a fast line."""
|
|
from nexthopd import speedtest
|
|
streams = 4
|
|
worst = (speedtest.CONTENT_DOWN_STREAM_CAP
|
|
+ speedtest.CONTENT_UP_STREAM_CAP) * streams
|
|
self.assertLessEqual(worst, 20_000_000)
|
|
typical = (self.sized(100.0, speedtest.CONTENT_DOWN_STREAM_CAP)
|
|
+ self.sized(20.0, speedtest.CONTENT_UP_STREAM_CAP)) * streams
|
|
self.assertLess(typical, worst / 2)
|
|
|
|
|
|
class AHintThatUnderSizesHealsItself(unittest.TestCase):
|
|
"""The deadlock this avoids.
|
|
|
|
A stream sized from a stale-low hint can finish inside the window too
|
|
short to time, and be withheld. Nothing is stored for a withheld check,
|
|
so the hint never learns better — every check after it would ask for the
|
|
same too-short transfer and report nothing, on a line that is simply
|
|
faster than its own history. Observed for real: a 25 Mbps hint on this
|
|
~450 Mbps line returned `down: None`.
|
|
"""
|
|
|
|
def run_check(self, down_results, up_results, **kwargs):
|
|
from nexthopd import speedtest
|
|
d_calls, u_calls = [], []
|
|
real_d, real_u = speedtest._parallel_download, speedtest._parallel_upload
|
|
|
|
def fake_down(url, streams, timeout):
|
|
d_calls.append(url)
|
|
return down_results.pop(0)
|
|
|
|
def fake_up(url, per_stream, streams, timeout):
|
|
u_calls.append(per_stream)
|
|
return up_results.pop(0)
|
|
|
|
speedtest._parallel_download = fake_down
|
|
speedtest._parallel_upload = fake_up
|
|
try:
|
|
return speedtest.content_test(**kwargs), d_calls, u_calls
|
|
finally:
|
|
speedtest._parallel_download = real_d
|
|
speedtest._parallel_upload = real_u
|
|
|
|
def test_a_withheld_download_is_retried_once_at_the_cap(self):
|
|
from nexthopd import speedtest
|
|
r, d_calls, _ = self.run_check(
|
|
[(None, 2_000_000), (420.0, 12_000_000)],
|
|
[(90.0, 2_000_000)],
|
|
down_hint_mbps=25.0, up_hint_mbps=20.0)
|
|
self.assertEqual(len(d_calls), 2)
|
|
self.assertIn(str(speedtest.CONTENT_DOWN_STREAM_CAP), d_calls[1])
|
|
self.assertEqual(r["down_mbps"], 420.0)
|
|
# Both passes were paid for; the budget must say so.
|
|
self.assertEqual(r["bytes"], 2_000_000 + 12_000_000 + 2_000_000)
|
|
|
|
def test_a_withheld_upload_is_retried_once_at_the_cap(self):
|
|
from nexthopd import speedtest
|
|
r, _, u_calls = self.run_check(
|
|
[(420.0, 12_000_000)],
|
|
[(None, 1_000_000), (95.0, 8_000_000)],
|
|
down_hint_mbps=900.0, up_hint_mbps=20.0)
|
|
self.assertEqual(len(u_calls), 2)
|
|
self.assertEqual(u_calls[1], speedtest.CONTENT_UP_STREAM_CAP)
|
|
self.assertEqual(r["up_mbps"], 95.0)
|
|
|
|
def test_a_check_already_at_the_cap_is_not_retried(self):
|
|
# Nothing larger to ask for: retrying would spend the budget twice
|
|
# to arrive at the same answer.
|
|
r, d_calls, _ = self.run_check(
|
|
[(None, 12_000_000)], [(None, 8_000_000)])
|
|
self.assertEqual(len(d_calls), 1)
|
|
self.assertIsNone(r["down_mbps"])
|
|
self.assertFalse(r["ok"])
|
|
|
|
def test_a_genuinely_slow_line_is_not_retried(self):
|
|
# A small transfer that produced a number is a good measurement, not
|
|
# a failed one. This is the common case and it must stay cheap.
|
|
r, d_calls, u_calls = self.run_check(
|
|
[(9.5, 2_000_000)], [(2.1, 2_000_000)],
|
|
down_hint_mbps=10.0, up_hint_mbps=2.0)
|
|
self.assertEqual(len(d_calls), 1)
|
|
self.assertEqual(len(u_calls), 1)
|
|
self.assertEqual(r["bytes"], 4_000_000)
|
|
|
|
|
|
class PeakUploadCostIsUnchangedByParallelism(unittest.TestCase):
|
|
"""Splitting a pass must not multiply what it spends.
|
|
|
|
The peak's sustained upload used to be one stream of up to PEAK_UP_CAP.
|
|
It is now PEAK_STREAMS of a per-stream size, and the point of sizing per
|
|
stream rather than per pass is that the total stays where it was.
|
|
"""
|
|
|
|
def test_the_cap_still_bounds_the_whole_pass(self):
|
|
from nexthopd import speedtest
|
|
per_stream_cap = speedtest.PEAK_UP_CAP // speedtest.PEAK_STREAMS
|
|
self.assertLessEqual(per_stream_cap * speedtest.PEAK_STREAMS,
|
|
speedtest.PEAK_UP_CAP)
|
|
|
|
def test_a_pass_is_sized_for_the_rate_one_stream_carries(self):
|
|
from nexthopd import speedtest
|
|
# 400 Mbps over four streams is 100 Mbps a stream; ten seconds of that
|
|
# is 125 MB, which the per-stream cap holds down to 25 MB.
|
|
n = speedtest._sized_pass(400.0 / speedtest.PEAK_STREAMS,
|
|
speedtest.PEAK_UP_FLOOR // speedtest.PEAK_STREAMS,
|
|
speedtest.PEAK_UP_CAP // speedtest.PEAK_STREAMS)
|
|
self.assertLessEqual(n * speedtest.PEAK_STREAMS, speedtest.PEAK_UP_CAP)
|
|
self.assertGreaterEqual(n, speedtest.PEAK_UP_FLOOR // speedtest.PEAK_STREAMS)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|