aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-common')
-rw-r--r--packages/meshbay-common/src/meshbay_common/paths.py4
-rw-r--r--packages/meshbay-common/tests/test_portable_name_parity.py78
2 files changed, 81 insertions, 1 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/paths.py b/packages/meshbay-common/src/meshbay_common/paths.py
index 45bf34e..b673649 100644
--- a/packages/meshbay-common/src/meshbay_common/paths.py
+++ b/packages/meshbay-common/src/meshbay_common/paths.py
@@ -137,7 +137,9 @@ def sanitize_for_download(name: str, *, replacement: str = "_") -> str:
For the client saving a file, never for the node storing one. Returns the
name unchanged when it is already portable, so the common case is identity
- and the caller can tell whether it renamed anything by comparing.
+ and the caller can tell whether it renamed anything by comparing. The
+ browser's copy is `static/portable-name.js`, held to this one by
+ `test_portable_name_parity.py`.
"""
if is_portable_name(name):
return name
diff --git a/packages/meshbay-common/tests/test_portable_name_parity.py b/packages/meshbay-common/tests/test_portable_name_parity.py
new file mode 100644
index 0000000..3a491a7
--- /dev/null
+++ b/packages/meshbay-common/tests/test_portable_name_parity.py
@@ -0,0 +1,78 @@
+"""
+The browser makes a name writable everywhere exactly as Python does.
+
+`static/portable-name.js` renames a file at the moment a member saves it;
+`meshbay_common.paths.sanitize_for_download` is the same rule in Python. Two
+copies of a rule that differ decide differently which files get renamed, so the
+real module runs under node here against the real Python.
+
+Skipped when node is unavailable; that is a coverage gap, not a pass.
+"""
+
+import json
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+from meshbay_common.paths import is_portable_name, sanitize_for_download
+
+PORTABLE_JS = (Path(__file__).resolve().parents[2]
+ / "meshbay-hub" / "src" / "meshbay_hub" / "static" / "portable-name.js")
+
+pytestmark = pytest.mark.skipif(
+ shutil.which("node") is None or not PORTABLE_JS.exists(),
+ reason="node or portable-name.js unavailable — parity cannot be checked",
+)
+
+NAMES = [
+ "plain.txt", "Réunion 12:30.pdf", 'a<b>c:d"e/f\\g|h?i*j.txt', "tab\tnew\nline",
+ "ends with dot.", "ends with space ", "trailing . . ", "CON", "con.txt", "aux.tar.gz",
+ "COM1", "com10.txt", "LPT9.log", "nul.", ".", "..", "", " ", ".bashrc", "...",
+ "名前:ファイル.mkv", "emoji 🙂?.png", "\x01\x1f.bin", "prn .txt", "Con",
+ "a.b.c", "COM1.", "normal name (2).mp4",
+]
+
+_HARNESS = r"""
+const fs = require('fs');
+const src = fs.readFileSync(process.argv[2], 'utf8').replace(/^export /gm, '');
+const M = new Function(src + '\nreturn { portableName, portablePath };')();
+const names = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
+process.stdout.write(JSON.stringify({
+ names: names.map((n) => M.portableName(n)),
+ path: M.portablePath('Top:Folder/sub//aux.txt'),
+}));
+"""
+
+
+@pytest.fixture(scope="module")
+def js(tmp_path_factory):
+ d = tmp_path_factory.mktemp("portable")
+ (d / "harness.js").write_text(_HARNESS, encoding="utf-8")
+ (d / "names.json").write_text(json.dumps(NAMES), encoding="utf-8")
+ proc = subprocess.run(
+ ["node", str(d / "harness.js"), str(PORTABLE_JS), str(d / "names.json")],
+ capture_output=True, text=True, encoding="utf-8", timeout=60)
+ if proc.returncode != 0:
+ pytest.fail(f"node harness failed:\n{proc.stderr}")
+ return json.loads(proc.stdout)
+
+
+@pytest.mark.parametrize("i,name", list(enumerate(NAMES)))
+def test_the_browser_renames_as_python_does(i, name, js):
+ assert js["names"][i] == sanitize_for_download(name)
+
+
+@pytest.mark.parametrize("name", NAMES)
+def test_what_comes_out_can_be_written_everywhere(name):
+ out = sanitize_for_download(name)
+ assert is_portable_name(out), (name, out)
+ assert sanitize_for_download(out) == out
+
+
+def test_a_portable_name_is_left_alone():
+ assert sanitize_for_download("plain.txt") == "plain.txt"
+
+
+def test_a_path_is_made_portable_segment_by_segment(js):
+ assert js["path"] == "Top_Folder/sub//aux_.txt"