aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_transfers.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_transfers.py')
-rw-r--r--packages/meshbay-hub/tests/test_transfers.py169
1 files changed, 169 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_transfers.py b/packages/meshbay-hub/tests/test_transfers.py
new file mode 100644
index 0000000..08743f6
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_transfers.py
@@ -0,0 +1,169 @@
+"""
+The transfer store, which is what keeps a download alive after you leave a group.
+
+Run under Node, because the behaviour worth pinning is timing and lifetime:
+that a cancel actually stops the work rather than only greying out a row, that a
+stalled transfer reads as slow instead of reporting its historical average, and
+that a transport handed over by a departing page is closed by the last transfer
+using it — not before, and not never.
+"""
+
+import json
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
+TRANSFERS = STATIC / "transfers.js"
+
+pytestmark = pytest.mark.skipif(
+ shutil.which("node") is None or not TRANSFERS.exists(),
+ reason="node or the SPA sources are not available")
+
+
+def _run(body, tmp_path):
+ module = tmp_path / "transfers.mjs"
+ module.write_text(TRANSFERS.read_text())
+ script = tmp_path / "case.mjs"
+ script.write_text(
+ f"import {{ TransferStore, formatSpeed }} from '{module.as_posix()}';\n"
+ "const out = [];\n"
+ "const say = (...a) => out.push(...a);\n"
+ f"{body}\n"
+ "console.log(JSON.stringify(out));\n")
+ proc = subprocess.run(["node", str(script)], capture_output=True, text=True)
+ assert proc.returncode == 0, proc.stderr
+ return json.loads(proc.stdout)
+
+
+def test_a_cancel_actually_stops_the_work(tmp_path):
+ """
+ The flag has to be read by the thing doing the work. A store that only
+ marks a row cancelled gives you a button that lies.
+ """
+ result = _run("""
+const store = new TransferStore();
+let chunksWritten = 0;
+const id = store.start({
+ kind: 'download', name: 'big.mp4', total: 100,
+ run: async ({ signal, onProgress }) => {
+ for (let i = 0; i < 100; i++) {
+ if (signal.aborted) throw Object.assign(new Error('stop'), { name: 'AbortError' });
+ chunksWritten++;
+ onProgress(i + 1, 100);
+ await new Promise(r => setTimeout(r, 1));
+ }
+ },
+});
+await new Promise(r => setTimeout(r, 20));
+store.cancel(id);
+await new Promise(r => setTimeout(r, 40));
+say(chunksWritten < 100, store.list()[0].status, store.active);
+""", tmp_path)
+ assert result[0] is True, "the work ran to completion despite being cancelled"
+ assert result[1] == "cancelled"
+ assert result[2] == 0
+
+
+def test_speed_is_measured_over_a_window_not_since_the_start(tmp_path):
+ """A transfer that stalls should read as slow now, not as its own average."""
+ result = _run("""
+let clock = 0;
+const store = new TransferStore(() => clock);
+let report;
+store.start({
+ kind: 'download', name: 'x', total: 100_000_000,
+ run: async ({ onProgress }) => { report = onProgress; await new Promise(() => {}); },
+});
+await new Promise(r => setTimeout(r, 1)); // start() defers run() by a tick
+// A fast megabyte a second, for six seconds.
+for (let i = 1; i <= 6; i++) { clock = i * 1000; report(i * 1_000_000, 100_000_000); }
+say(Math.round(store.list()[0].speed));
+// Then it stalls: the clock moves, the bytes do not.
+for (let i = 7; i <= 12; i++) { clock = i * 1000; report(6_000_000, 100_000_000); }
+say(Math.round(store.list()[0].speed));
+""", tmp_path)
+ assert 900_000 <= result[0] <= 1_100_000, f"expected ~1 MB/s, got {result[0]}"
+ assert result[1] == 0, f"a stalled transfer still reports {result[1]} B/s"
+
+
+def test_a_transport_is_closed_by_the_last_transfer_that_needed_it(tmp_path):
+ """
+ Leaving a group page must not kill a running download, and must not leak
+ the connection either.
+ """
+ result = _run("""
+const store = new TransferStore();
+let closed = 0;
+const transport = { close: () => { closed++; }, onIndexSync: () => {} };
+let done1, done2;
+store.start({ kind: 'download', name: 'a', transport,
+ run: () => new Promise(r => { done1 = r; }) });
+store.start({ kind: 'download', name: 'b', transport,
+ run: () => new Promise(r => { done2 = r; }) });
+
+store.releaseWhenIdle(transport); // the page goes away
+await new Promise(r => setTimeout(r, 5));
+say(closed); // still working: must stay open
+
+done1(); await new Promise(r => setTimeout(r, 5));
+say(closed); // one left: still open
+
+done2(); await new Promise(r => setTimeout(r, 5));
+say(closed); // now it can go
+""", tmp_path)
+ assert result == [0, 0, 1], f"close() calls after each step: {result}"
+
+
+def test_an_idle_transport_is_closed_straight_away(tmp_path):
+ result = _run("""
+const store = new TransferStore();
+let closed = 0;
+store.releaseWhenIdle({ close: () => { closed++; } });
+say(closed);
+""", tmp_path)
+ assert result == [1], "leaving a group with nothing running should close it"
+
+
+def test_signing_out_cancels_everything_and_lets_go(tmp_path):
+ """
+ Navigating is not signing out. The second one has to stop transfers running
+ on tokens that are about to stop being ours.
+ """
+ result = _run("""
+const store = new TransferStore();
+let closed = 0;
+const transport = { close: () => { closed++; } };
+store.start({ kind: 'upload', name: 'a', transport,
+ run: () => new Promise(() => {}) });
+store.reset();
+await new Promise(r => setTimeout(r, 5));
+say(store.list().length, closed, store.active);
+""", tmp_path)
+ assert result == [0, 1, 0]
+
+
+def test_a_failure_is_kept_and_named(tmp_path):
+ """A transfer that dies silently is one the user retries at random."""
+ result = _run("""
+const store = new TransferStore();
+store.start({ kind: 'download', name: 'gone.mp4',
+ run: async () => { throw new Error('File not found'); } });
+await new Promise(r => setTimeout(r, 5));
+const row = store.list()[0];
+say(row.status, row.error);
+store.clearFinished();
+say(store.list().length);
+""", tmp_path)
+ assert result[0] == "failed"
+ assert result[1] == "File not found"
+ assert result[2] == 0
+
+
+def test_the_rate_is_readable(tmp_path):
+ result = _run("""
+say(formatSpeed(0), formatSpeed(2048), formatSpeed(5 * 1024 * 1024));
+""", tmp_path)
+ assert result == ["", "2 KB/s", "5.0 MB/s"]