aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-04 02:20:57 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-04 02:20:57 +0200
commitd11e571c5b6c24b586ef5b8fb2cfcf6a6bfa6d6d (patch)
treefe3c4fbce3db02a8b84a15c413735622a51eefb0
parent7a4b905ddb64bdc92b7f9acf2ccde9bd84d7a6f3 (diff)
downloadmeshbay-d11e571c5b6c24b586ef5b8fb2cfcf6a6bfa6d6d.tar.gz
test(node): make the suite pass on Windows
- `.read_text()` on source files now `encoding="utf-8"` — cp1252 chokes on the em dashes / box-drawing chars those files contain. - test node.toml templates embed paths via `Path.as_posix()`: a raw Windows path in a basic TOML string is a parse error (`\U`, `\a`, ... are escapes). - new `test_platform.py` covers `meshbay_node.platform` by mocking `sys.platform` / `os.environ` — runs on both OSes. - `skipif(sys.platform == "win32")`, in `conftest.needs_subprocess` and inline, for the documented gaps: ffmpeg/ffprobe via asyncio subprocess (the win32 selector loop, forced for aiortc, cannot spawn one), the systemd `reload`/`restart-daemon` delegation (Windows path is W3), the keystore `st_mode == 600` assertion (NTFS ignores mode bits), and the symlink-escape test (needs Developer Mode). Windows: 781 passed, 25 skipped. No change on Linux. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-rw-r--r--packages/meshbay-node/tests/conftest.py19
-rw-r--r--packages/meshbay-node/tests/test_audio_transcode.py4
-rw-r--r--packages/meshbay-node/tests/test_cli_dispatch.py4
-rw-r--r--packages/meshbay-node/tests/test_enrich.py9
-rw-r--r--packages/meshbay-node/tests/test_enrich_photo.py2
-rw-r--r--packages/meshbay-node/tests/test_hot_reload_survives_client_close.py9
-rw-r--r--packages/meshbay-node/tests/test_keystore.py6
-rw-r--r--packages/meshbay-node/tests/test_platform.py94
-rw-r--r--packages/meshbay-node/tests/test_roots.py5
-rw-r--r--packages/meshbay-node/tests/test_roster_pairing.py20
-rw-r--r--packages/meshbay-node/tests/test_security_regressions.py14
-rw-r--r--packages/meshbay-node/tests/test_stream_audio_transcode.py3
-rw-r--r--packages/meshbay-node/tests/test_stream_capacity_config.py2
-rw-r--r--packages/meshbay-node/tests/test_stream_hevc_transcode.py3
-rw-r--r--packages/meshbay-node/tests/test_task_lifetime.py2
15 files changed, 167 insertions, 29 deletions
diff --git a/packages/meshbay-node/tests/conftest.py b/packages/meshbay-node/tests/conftest.py
index 03dfb42..20724aa 100644
--- a/packages/meshbay-node/tests/conftest.py
+++ b/packages/meshbay-node/tests/conftest.py
@@ -1,9 +1,28 @@
"""Shared fixtures and helpers for node tests."""
+import sys
from pathlib import Path
+import pytest
from meshbay_node.roots import RootSet
+# ffmpeg / ffprobe run via asyncio.create_subprocess_exec, which needs the
+# ProactorEventLoop — but the repo-root conftest forces the SelectorEventLoop
+# on win32 so aiortc's ICE stack works there (see devel/windows-devel.md §5).
+# The two are mutually exclusive on one Windows asyncio loop; until the media
+# path gets a thread-based subprocess runner, these tests can't run on win32.
+needs_subprocess = pytest.mark.skipif(
+ sys.platform == "win32",
+ reason="ffmpeg subprocess needs ProactorEventLoop; win32 conftest forces "
+ "SelectorEventLoop for aiortc",
+)
+
+# Windows-only gaps still to close (see devel/windows-devel.md §5/§6).
+win32_todo = pytest.mark.skipif(
+ sys.platform == "win32",
+ reason="Windows behaviour not implemented yet (W3 / platform specifics)",
+)
+
def one_root(path: Path, *, name: str = "", kind: str = "generic") -> RootSet:
"""
diff --git a/packages/meshbay-node/tests/test_audio_transcode.py b/packages/meshbay-node/tests/test_audio_transcode.py
index 725839b..c2f7f64 100644
--- a/packages/meshbay-node/tests/test_audio_transcode.py
+++ b/packages/meshbay-node/tests/test_audio_transcode.py
@@ -22,10 +22,10 @@ from meshbay_node.media_cache import MediaCache
from meshbay_node.transport import webrtc_server as wrs
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
-from conftest import one_root
+from conftest import needs_subprocess, one_root
_HAVE_FFMPEG = shutil.which("ffmpeg") and shutil.which("ffprobe")
-pytestmark = pytest.mark.asyncio
+pytestmark = [pytest.mark.asyncio, needs_subprocess]
@pytest.fixture
diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py
index c774dc0..76eb6d5 100644
--- a/packages/meshbay-node/tests/test_cli_dispatch.py
+++ b/packages/meshbay-node/tests/test_cli_dispatch.py
@@ -140,6 +140,8 @@ def test_the_verb_list_here_matches_the_parser():
assert "ui" not in declared, "the `ui` verb came back"
+@pytest.mark.skipif(sys.platform == "win32",
+ reason="systemd lifecycle; Windows path (schtasks) is W3, not built")
@pytest.mark.parametrize("argv,verb", [
(["reload"], "reload"),
(["restart-daemon"], "restart"),
@@ -167,6 +169,8 @@ def test_lifecycle_commands_delegate_to_systemctl_user(
assert not capsys.readouterr().err
+@pytest.mark.skipif(sys.platform == "win32",
+ reason="systemd lifecycle; Windows path (schtasks) is W3, not built")
@pytest.mark.parametrize("argv", [["reload"], ["restart-daemon"]])
def test_lifecycle_commands_report_systemctl_failure(
argv, stub_daemon, monkeypatch, capsys):
diff --git a/packages/meshbay-node/tests/test_enrich.py b/packages/meshbay-node/tests/test_enrich.py
index a35c713..75cf0ee 100644
--- a/packages/meshbay-node/tests/test_enrich.py
+++ b/packages/meshbay-node/tests/test_enrich.py
@@ -5,6 +5,8 @@ import shutil
import subprocess
from pathlib import Path
+import sys
+
import pytest
from meshbay_common.protocol import IndexEntry
@@ -131,7 +133,12 @@ def test_synthetic_episode_number_does_not_collide_across_per_season_bonus_folde
# ── end-to-end against a real (tiny, synthetic) video file ──────────────────
-pytestmark_ffmpeg = pytest.mark.skipif(not _HAVE_FFMPEG, reason="ffmpeg/ffprobe not installed")
+# ffprobe runs via asyncio subprocess, which the win32 selector loop (forced
+# for aiortc, see devel/windows-devel.md §5) cannot spawn.
+pytestmark_ffmpeg = pytest.mark.skipif(
+ not _HAVE_FFMPEG or sys.platform == "win32",
+ reason="needs ffprobe installed and a ProactorEventLoop",
+)
def _make_clip(path: Path) -> None:
diff --git a/packages/meshbay-node/tests/test_enrich_photo.py b/packages/meshbay-node/tests/test_enrich_photo.py
index 877a71d..7684f40 100644
--- a/packages/meshbay-node/tests/test_enrich_photo.py
+++ b/packages/meshbay-node/tests/test_enrich_photo.py
@@ -185,6 +185,6 @@ def test_gps_is_never_read_by_this_module():
what this test is guarding against.
"""
src = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
- / "indexer" / "enrich_photo.py").read_text()
+ / "indexer" / "enrich_photo.py").read_text(encoding="utf-8")
for needle in ("GPSInfo", "GPSTAGS", "0x8825", "34853"):
assert needle not in src, f"found {needle!r} — GPS extraction must never be added here"
diff --git a/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py b/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py
index 7cb74cb..fc6af70 100644
--- a/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py
+++ b/packages/meshbay-node/tests/test_hot_reload_survives_client_close.py
@@ -17,6 +17,7 @@ finished and the new group became available on its own.
import asyncio
import base64
import os
+from pathlib import Path
import pytest
from cryptography.hazmat.primitives import serialization
@@ -45,8 +46,10 @@ def _toml(data_dir, first_group_dir, second_group_id=None, second_group_dir=None
# (~/.local/share/meshbay) instead of erroring, which is exactly how this
# test once ran a whole daemon — including _shutdown()'s unlink of
# ui-token — against the developer's real, already-running node.
+ # Forward slashes: a raw Windows path in a basic TOML string is a parse
+ # error (`\U`, `\a`, ... are escape sequences). pathlib reads `/` fine.
text = f"""
-data_dir = "{data_dir}"
+data_dir = "{Path(data_dir).as_posix()}"
[hub]
url = "http://localhost:9999"
@@ -59,7 +62,7 @@ ui_port = {_free_port()}
[[groups]]
id = "{"a" * 32}"
name = "first"
-shared_dir = "{first_group_dir}"
+shared_dir = "{Path(first_group_dir).as_posix()}"
visibility = "private"
"""
if second_group_id:
@@ -67,7 +70,7 @@ visibility = "private"
[[groups]]
id = "{second_group_id}"
name = "slow-new-group"
-shared_dir = "{second_group_dir}"
+shared_dir = "{Path(second_group_dir).as_posix()}"
visibility = "private"
"""
return text
diff --git a/packages/meshbay-node/tests/test_keystore.py b/packages/meshbay-node/tests/test_keystore.py
index b3c2b56..7dfefce 100644
--- a/packages/meshbay-node/tests/test_keystore.py
+++ b/packages/meshbay-node/tests/test_keystore.py
@@ -1,5 +1,7 @@
"""Tests for meshbay_node.keystore."""
+import sys
+
import pytest
from pathlib import Path
from meshbay_node.keystore import (
@@ -22,7 +24,9 @@ def test_create_and_load(tmp_path):
assert len(keys.pk_ed25519_b64) == 44 # 32 bytes → 44 base64 chars
assert len(keys.pk_x25519_b64) == 44
assert path.exists()
- assert oct(path.stat().st_mode)[-3:] == "600"
+ if sys.platform != "win32":
+ # NTFS ignores POSIX mode bits; chmod_private is a no-op there (W5).
+ assert oct(path.stat().st_mode)[-3:] == "600"
loaded = load_keystore(path=path, password="testpass99")
assert loaded.pk_ed25519_b64 == keys.pk_ed25519_b64
diff --git a/packages/meshbay-node/tests/test_platform.py b/packages/meshbay-node/tests/test_platform.py
new file mode 100644
index 0000000..2725df6
--- /dev/null
+++ b/packages/meshbay-node/tests/test_platform.py
@@ -0,0 +1,94 @@
+"""Tests for meshbay_node.platform — the OS-specific paths and tool resolution.
+
+The behaviour is selected on `sys.platform` at call time, so it is exercised
+here by monkeypatching that (and `os.environ`) rather than only on the OS the
+suite happens to run on.
+"""
+
+import asyncio
+import sys
+from pathlib import Path
+from unittest.mock import Mock
+
+import pytest
+from meshbay_node import platform as plat
+
+# ── Directories ──────────────────────────────────────────────────────────────
+
+@pytest.mark.parametrize("fn,tail", [
+ (plat.config_dir, ()),
+ (plat.data_dir, ("data",)),
+ (plat.state_dir, ("state",)),
+])
+def test_windows_dirs_live_under_localappdata(fn, tail, monkeypatch, tmp_path):
+ monkeypatch.setattr(sys, "platform", "win32")
+ monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
+ assert fn() == tmp_path.joinpath("meshbay", *tail)
+
+
+@pytest.mark.parametrize("fn,expected_tail", [
+ (plat.config_dir, (".config", "meshbay")),
+ (plat.data_dir, (".local", "share", "meshbay")),
+ (plat.state_dir, (".local", "state", "meshbay")),
+])
+def test_posix_dirs_follow_xdg(fn, expected_tail, monkeypatch):
+ monkeypatch.setattr(sys, "platform", "linux")
+ assert fn() == Path.home().joinpath(*expected_tail)
+
+
+def test_windows_dirs_fall_back_to_home_without_localappdata(monkeypatch):
+ monkeypatch.setattr(sys, "platform", "win32")
+ monkeypatch.delenv("LOCALAPPDATA", raising=False)
+ assert plat.config_dir() == Path.home() / "meshbay"
+
+
+# ── File permissions ─────────────────────────────────────────────────────────
+
+def test_chmod_private_is_a_noop_on_windows(monkeypatch):
+ monkeypatch.setattr(sys, "platform", "win32")
+ fake = Mock(spec=Path)
+ plat.chmod_private(fake)
+ fake.chmod.assert_not_called()
+
+
+def test_chmod_private_applies_mode_off_windows(monkeypatch, tmp_path):
+ monkeypatch.setattr(sys, "platform", "linux")
+ f = tmp_path / "secret"
+ f.write_text("x", encoding="utf-8")
+ called = {}
+ monkeypatch.setattr(type(f), "chmod", lambda self, m: called.setdefault("mode", m))
+ plat.chmod_private(f)
+ assert called["mode"] == 0o600
+
+
+# ── Media tools ──────────────────────────────────────────────────────────────
+
+def test_check_media_tools_raises_when_ffmpeg_is_missing(monkeypatch):
+ monkeypatch.setattr(plat.shutil, "which", lambda _n: None)
+ with pytest.raises(RuntimeError, match="not found in PATH"):
+ plat.check_media_tools()
+
+
+def test_check_media_tools_stores_the_resolved_paths(monkeypatch):
+ monkeypatch.setattr(plat.shutil, "which",
+ lambda n: f"/opt/bin/{n}.exe")
+ plat.check_media_tools("ffmpeg", "ffprobe")
+ assert plat.ffmpeg_cmd() == "/opt/bin/ffmpeg.exe"
+ assert plat.ffprobe_cmd() == "/opt/bin/ffprobe.exe"
+
+
+# ── Event loop ───────────────────────────────────────────────────────────────
+
+def test_use_compatible_event_loop_is_a_noop_off_windows(monkeypatch):
+ monkeypatch.setattr(sys, "platform", "linux")
+ before = asyncio.get_event_loop_policy()
+ plat.use_compatible_event_loop()
+ assert asyncio.get_event_loop_policy() is before
+
+
+@pytest.mark.skipif(sys.platform != "win32",
+ reason="WindowsSelectorEventLoopPolicy exists only on win32")
+def test_use_compatible_event_loop_selects_the_selector_loop_on_windows():
+ plat.use_compatible_event_loop()
+ assert isinstance(asyncio.get_event_loop_policy(),
+ asyncio.WindowsSelectorEventLoopPolicy)
diff --git a/packages/meshbay-node/tests/test_roots.py b/packages/meshbay-node/tests/test_roots.py
index fc5bd64..1beb220 100644
--- a/packages/meshbay-node/tests/test_roots.py
+++ b/packages/meshbay-node/tests/test_roots.py
@@ -170,7 +170,10 @@ def test_a_symlink_out_of_the_root_is_refused(tmp_path):
(tmp_path / "Media").mkdir()
outside = tmp_path / "outside"
outside.mkdir()
- (tmp_path / "Media" / "escape").symlink_to(outside)
+ try:
+ (tmp_path / "Media" / "escape").symlink_to(outside)
+ except OSError as e: # Windows without Developer Mode / SeCreateSymbolicLink
+ pytest.skip(f"cannot create a symlink here: {e}")
roots = RootSet.build([_spec(tmp_path / "Media")])
assert roots.resolve("Media/escape") is None
diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py
index 8bedfad..eb13c61 100644
--- a/packages/meshbay-node/tests/test_roster_pairing.py
+++ b/packages/meshbay-node/tests/test_roster_pairing.py
@@ -529,11 +529,11 @@ def test_join_policy_is_carried_from_node_config():
instead, a hub could declare any group open and be handed its key.
"""
daemon_src = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "daemon.py").read_text()
+ / "src" / "meshbay_node" / "daemon.py").read_text(encoding="utf-8")
assert '"join_policy": group_cfg.join_policy' in daemon_src
config_src = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "config.py").read_text()
+ / "src" / "meshbay_node" / "config.py").read_text(encoding="utf-8")
assert "join_policy" in config_src, "GroupConfig must carry the admission policy"
@@ -570,7 +570,7 @@ def test_challenge_carries_node_pk_in_source():
builds, whatever the surrounding handshake does.
"""
source = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "transport" / "webrtc_server.py").read_text()
+ / "src" / "meshbay_node" / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
challenge = source[source.find("MNP.HANDSHAKE_CHALLENGE,"):]
challenge = challenge[:challenge.find("})")]
assert "node_pk" in challenge, (
@@ -830,11 +830,13 @@ def _run_cli(monkeypatch, tmp_path, argv, responses):
monkeypatch.setattr(_daemon, "_daemon_api", fake_api)
conf = tmp_path / "node.toml"
+ tp = tmp_path.as_posix() # a raw Windows path is a TOML escape error
conf.write_text(
- f'data_dir = "{tmp_path}"\n'
+ f'data_dir = "{tp}"\n'
'[hub]\nurl = "https://example.org"\nusername = "grenet"\n'
f'[[groups]]\nid = "{GROUP}"\nname = "demo"\n'
- f'shared_dir = "{tmp_path}"\n'
+ f'shared_dir = "{tp}"\n',
+ encoding="utf-8",
)
monkeypatch.setattr(_sys, "argv",
["meshbay-node", *argv, "--config", str(conf)])
@@ -884,7 +886,7 @@ def test_daemon_does_not_auto_pin_keystore_key():
Authority now comes from the roster, or from an explicit node.toml value.
"""
source = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "daemon.py").read_text()
+ / "src" / "meshbay_node" / "daemon.py").read_text(encoding="utf-8")
assert "Auto-pinning admin key" not in source
assert "_resolve_admin_pk" not in source, (
"the auto-pin resolver is back — node authority must be established "
@@ -898,7 +900,7 @@ def test_admin_authority_is_never_fetched_from_the_hub():
"""
src = Path(__file__).parent.parent / "src" / "meshbay_node"
- verifier = (src / "transport" / "webrtc_server.py").read_text()
+ verifier = (src / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
body = verifier[verifier.index("async def _verify_admin_sig"):]
body = body[:body.index("\n def ", 1)]
assert "operator_pks" in body, "the roster is where authority comes from"
@@ -910,7 +912,7 @@ def test_admin_authority_is_never_fetched_from_the_hub():
f"_verify_admin_sig mentions {forbidden!r} — authority must come from "
"the local roster and nothing else")
- daemon = (src / "daemon.py").read_text()
+ daemon = (src / "daemon.py").read_text(encoding="utf-8")
assert "has_operator()" in daemon, "the daemon reads authority from the roster"
assert "admin_pk_ed25519" not in daemon, (
"the node.toml operator key is gone; it must not come back as a second "
@@ -940,7 +942,7 @@ async def test_group_add_appends_without_rewriting_the_file(tmp_path):
with conf.open("a") as f:
f.write(block)
- assert "# keep me" in conf.read_text(), "comments must survive"
+ assert "# keep me" in conf.read_text(encoding="utf-8"), "comments must survive"
cfg = load_config(conf)
assert [g.name for g in cfg.groups] == ["first", "second"]
assert [g.shared_dir for g in cfg.groups] == ["/tmp/a", "/tmp/b"]
diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py
index 10182d3..7f71da5 100644
--- a/packages/meshbay-node/tests/test_security_regressions.py
+++ b/packages/meshbay-node/tests/test_security_regressions.py
@@ -308,7 +308,7 @@ def test_chat_store_and_peers_are_per_group(tmp_path):
def test_daemon_sets_no_global_chat_store(tmp_path):
"""H1: the daemon must not hoist one group's chat store onto the transport."""
source = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "daemon.py").read_text()
+ / "src" / "meshbay_node" / "daemon.py").read_text(encoding="utf-8")
assert '_ctx["chat_store"]' not in source, (
"daemon must not assign a transport-wide chat_store — it leaks chat "
"across groups (H1)"
@@ -336,7 +336,7 @@ def test_no_member_can_hand_the_node_key_material(tmp_path):
)
source = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "transport" / "webrtc_server.py").read_text()
+ / "src" / "meshbay_node" / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
assert "_do_gek_bundle_store" not in source
assert "_admin_exec_bundle_store" not in source
@@ -370,7 +370,7 @@ def test_gek_auto_activation_is_gone():
node a GEK of their choosing. Nothing arriving over MNP may set a live GEK.
"""
source = (Path(__file__).parent.parent / "src" / "meshbay_node"
- / "transport" / "webrtc_server.py").read_text()
+ / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
assert "_try_activate_gek" not in source
assert 'unwrap_gek_aes' not in source, (
"the MNP path must not unwrap a GEK — activation is local-admin only"
@@ -485,7 +485,7 @@ def test_swarm_registration_skips_private_groups():
dormant leak into a live one.
"""
source = (Path(__file__).parent.parent / "src" / "meshbay_node"
- / "daemon.py").read_text()
+ / "daemon.py").read_text(encoding="utf-8")
assert 'visibility' in source and '_register_swarm' in source
# Both registration sites must gate on public visibility.
for marker in ['gctx.get("visibility") == "public"',
@@ -509,7 +509,7 @@ def test_keystore_records_argon2_params_for_migration(tmp_path):
path = tmp_path / "keystore.enc"
created = create_keystore(path=path, password="correct horse battery")
- envelope = json.loads(path.read_text())
+ envelope = json.loads(path.read_text(encoding="utf-8"))
assert envelope["argon2"]["memory_cost"] >= 262144
reopened = load_keystore(path=path, password="correct horse battery")
@@ -575,7 +575,7 @@ def test_peer_errors_do_not_leak_internals():
that purpose. The check targets the generic `except Exception as e` path.
"""
source = (Path(__file__).parent.parent / "src" / "meshbay_node"
- / "transport" / "webrtc_server.py").read_text()
+ / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
assert '"detail": str(e)' not in source, (
"generic exception text relayed to peer — use a fixed message"
)
@@ -616,7 +616,7 @@ def test_stream_segment_is_not_synchronous():
assert inspect.iscoroutinefunction(WebRTCPeerSession._do_stream_segment_async)
source = (Path(__file__).parent.parent / "src" / "meshbay_node"
- / "transport" / "webrtc_server.py").read_text()
+ / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
tree = ast.parse(source)
blocking = [
node for node in ast.walk(tree)
diff --git a/packages/meshbay-node/tests/test_stream_audio_transcode.py b/packages/meshbay-node/tests/test_stream_audio_transcode.py
index aaf1595..959cc91 100644
--- a/packages/meshbay-node/tests/test_stream_audio_transcode.py
+++ b/packages/meshbay-node/tests/test_stream_audio_transcode.py
@@ -28,12 +28,13 @@ from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.transport.webrtc_server import WebRTCPeerSession, _probe_video
-from conftest import one_root
+from conftest import needs_subprocess, one_root
_HAVE_FFMPEG = shutil.which("ffmpeg") and shutil.which("ffprobe")
pytestmark = [
pytest.mark.asyncio,
pytest.mark.skipif(not _HAVE_FFMPEG, reason="ffmpeg/ffprobe not installed"),
+ needs_subprocess,
]
diff --git a/packages/meshbay-node/tests/test_stream_capacity_config.py b/packages/meshbay-node/tests/test_stream_capacity_config.py
index cf90e22..442d3ef 100644
--- a/packages/meshbay-node/tests/test_stream_capacity_config.py
+++ b/packages/meshbay-node/tests/test_stream_capacity_config.py
@@ -138,7 +138,7 @@ def test_the_daemon_passes_it( ):
first time somebody plays a video, which is not where anyone would look.
"""
daemon = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
- / "daemon.py").read_text()
+ / "daemon.py").read_text(encoding="utf-8")
i = daemon.index("WebRTCTransport(")
call = daemon[i:daemon.index(")", daemon.index("denylist=denylist", i))]
assert "max_concurrent_streams=" in call, (
diff --git a/packages/meshbay-node/tests/test_stream_hevc_transcode.py b/packages/meshbay-node/tests/test_stream_hevc_transcode.py
index b3f0474..7d831ee 100644
--- a/packages/meshbay-node/tests/test_stream_hevc_transcode.py
+++ b/packages/meshbay-node/tests/test_stream_hevc_transcode.py
@@ -26,7 +26,7 @@ from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.transport.webrtc_server import WebRTCPeerSession, _probe_video
-from conftest import one_root
+from conftest import needs_subprocess, one_root
_HAVE_FFMPEG = shutil.which("ffmpeg") and shutil.which("ffprobe")
_HAVE_HEVC_ENCODER = _HAVE_FFMPEG and b"libx265" in subprocess.run(
@@ -34,6 +34,7 @@ _HAVE_HEVC_ENCODER = _HAVE_FFMPEG and b"libx265" in subprocess.run(
pytestmark = [
pytest.mark.asyncio,
pytest.mark.skipif(not _HAVE_HEVC_ENCODER, reason="ffmpeg/libx265 not installed"),
+ needs_subprocess,
]
diff --git a/packages/meshbay-node/tests/test_task_lifetime.py b/packages/meshbay-node/tests/test_task_lifetime.py
index 592579c..3a5b8a5 100644
--- a/packages/meshbay-node/tests/test_task_lifetime.py
+++ b/packages/meshbay-node/tests/test_task_lifetime.py
@@ -30,7 +30,7 @@ pytestmark = pytest.mark.skipif(
@pytest.fixture(scope="module")
def source():
- return SERVER.read_text()
+ return SERVER.read_text(encoding="utf-8")
@pytest.fixture(scope="module")