[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/QuantStack/git2cpp/0.0.10/test/conftest_wasm.py [Back]  [Original]

# Extra fixtures used for wasm testing, including some that override the default pytest fixtures.
from functools import partial
import os
import pathlib
from playwright.sync_api import Page
import pytest
import re
import subprocess
import time


# Only include particular test files when testing wasm.
# This can be removed when all tests support wasm.
def pytest_ignore_collect(collection_path: pathlib.Path) -> bool:
    return collection_path.name not in [
        "test_add.py",
        "test_branch.py",
        "test_checkout.py",
        "test_clone.py",
        "test_commit.py",
        "test_config.py",
        "test_diff.py",
        "test_fetch.py",
        "test_fixtures.py",
        "test_git.py",
        "test_init.py",
        "test_log.py",
        "test_merge.py",
        "test_mv.py",
        "test_push.py",
        "test_rebase.py",
        "test_remote.py",
        "test_reset.py",
        "test_revlist.py",
        "test_revparse.py",
        "test_rm.py",
        "test_showref.py",
        "test_stash.py",
        "test_status.py",
        "test_tag.py",
    ]


@pytest.fixture(scope="session", autouse=True)
def run_web_server():
    with open("serve.log", "w") as f:
        cwd = pathlib.Path(__file__).parent.parent / "wasm/test"
        proc = subprocess.Popen(["npm", "run", "serve"], stdout=f, stderr=f, cwd=cwd)
        # Wait a bit until server ready to receive connections.
        time.sleep(1)
        yield
        proc.terminate()


@pytest.fixture(scope="function", autouse=True)
def load_page(page: Page):
    # Load web page at start of every test.
    page.goto("http://localhost:8000")
    page.locator("#loaded").wait_for()


class MockPath(pathlib.Path):
    def __init__(self, path: str = ""):
        super().__init__(path)

    def exists(self) -> bool:
        p = subprocess.run(["stat", str(self)])
        return p.returncode == 0

    def is_dir(self) -> bool:
        p = subprocess.run(["stat", "-c", "%F", str(self)], capture_output=True, text=True)
        return p.returncode == 0 and p.stdout.strip() == "directory"

    def is_file(self) -> bool:
        p = subprocess.run(["stat", "-c", "%F", str(self)], capture_output=True, text=True)
        return p.returncode == 0 and p.stdout.strip() == "regular file"

    def iterdir(self):
        p = subprocess.run(
            ["ls", str(self), "-a", "-1"], capture_output=True, text=True, check=True
        )
        for f in filter(lambda f: f not in ["", ".", ".."], re.split(r"\r?\n", p.stdout)):
            yield MockPath(self / f)

    def mkdir(self, *, parents=False):
        args = [str(self)]
        if parents:
            args.append("-p")
        subprocess.run(["mkdir"] + args, capture_output=True, text=True, check=True)

    def read_bytes(self) -> bytes:
        raise RuntimeError("Not implemented")

    def read_text(self) -> str:
        p = subprocess.run(["cat", str(self)], capture_output=True, text=True, check=True)
        text = p.stdout
        if text.endswith("\n"):
            text = text[:-1]
        return text

    def write_bytes(self, data: bytes):
        # Convert binary data to a string where each element is backslash-escaped so that we can
        # write to file in cockle using `echo -e `.
        encoded_string = "".join(map(lambda d: f"\\x{d:02x}", data))
        cmd = ["echo", "-e", encoded_string, ">", str(self)]
        subprocess.run(cmd, capture_output=True, text=True, check=True)
        return len(data)

    def write_text(self, data: str):
        # Note that in general it is not valid to direct output of a subprocess.run call to a file,
        # but we get away with it here as the command arguments are passed straight through to
        # cockle without being checked.
        if data.endswith("\n"):
            data = data[:-1]
        cmd = ["echo", data, ">", str(self)]
        subprocess.run(cmd, capture_output=True, text=True, check=True)
        return len(data)

    def __truediv__(self, other):
        if isinstance(other, str):
            return MockPath(f"{self}/{other}")
        raise RuntimeError("MockPath.__truediv__ only supports strings")


def os_chdir(dir: str):
    subprocess.run(["cd", str(dir)], capture_output=True, check=True, text=True)


def os_getcwd():
    return subprocess.run(["pwd"], capture_output=True, check=True, text=True).stdout.strip()


def os_remove(file: str):
    return subprocess.run(["rm", str(file)], capture_output=True, check=True, text=True)


def os_rename(src: str | MockPath, dst: str | MockPath):
    return subprocess.run(["mv", str(src), str(dst)], capture_output=True, check=True, text=True)


def os_symlink(src: str | MockPath, dst: str | MockPath):
    return subprocess.run(
        ["ln", "-s", str(src), str(dst)], capture_output=True, check=True, text=True
    )


def subprocess_run(
    page: Page,
    cmd: list[str],
    *,
    capture_output: bool = False,
    check: bool = False,
    cwd: str | MockPath | None = None,
    input: str | None = None,
    text: bool | None = None,
) -> subprocess.CompletedProcess:
    shell_run = "async obj => await window.cockle.shellRun(obj.cmd, obj.input)"

    # Set cwd.
    if cwd is not None:
        proc = page.evaluate(shell_run, {"cmd": "pwd"})
        if proc["returncode"] != 0:
            raise RuntimeError("Error getting pwd")
        old_cwd = proc["stdout"].strip()
        if old_cwd == str(cwd):
            # cwd is already correct.
            cwd = None
        else:
            proc = page.evaluate(shell_run, {"cmd": f"cd {cwd}"})
            if proc["returncode"] != 0:
                raise RuntimeError(f"Error setting cwd to {cwd}")

    def maybe_wrap_arg(s: str | MockPath) -> str:
        # An argument containing spaces needs to be wrapped in quotes if it is not already, due
        # to how the command is passed to cockle as a single string.
        # Could do better here.
        s = str(s)
        if " " in s and not s.endswith("'"):
            return "'" + s + "'"
        return s

    shell_run_args = {"cmd": " ".join([maybe_wrap_arg(s) for s in cmd]), "input": input}
    proc = page.evaluate(shell_run, shell_run_args)

    # TypeScript object is auto converted to Python dict.
    # Want to return subprocess.CompletedProcess, consider namedtuple if this fails in future.
    returncode = proc["returncode"]
    stdout = proc["stdout"] if capture_output else ""
    stderr = proc["stderr"] if capture_output else ""
    if not text:
        stdout = stdout.encode("utf-8")
        stderr = stderr.encode("utf-8")

    # Reset cwd.
    if cwd is not None:
        proc = page.evaluate(shell_run, {"cmd": "cd " + old_cwd})
        if proc["returncode"] != 0:
            raise RuntimeError(f"Error setting cwd to {old_cwd}")

    if check and returncode != 0:
        raise subprocess.CalledProcessError(returncode, cmd, stdout, stderr)

    return subprocess.CompletedProcess(
        args=cmd, returncode=returncode, stdout=stdout, stderr=stderr
    )


@pytest.fixture(scope="function")
def tmp_path() -> MockPath:
    # Assumes only one tmp_path needed per test.
    path = MockPath("/drive/tmp0")
    subprocess.run(["mkdir", str(path)], check=True)
    assert path.exists()
    assert path.is_dir()
    return path


@pytest.fixture(scope="function", autouse=True)
def mock_subprocess_run(page: Page, monkeypatch):
    monkeypatch.setattr(subprocess, "run", partial(subprocess_run, page))
    monkeypatch.setattr(os, "chdir", os_chdir)
    monkeypatch.setattr(os, "getcwd", os_getcwd)
    monkeypatch.setattr(os, "remove", os_remove)
    monkeypatch.setattr(os, "rename", os_rename)
    monkeypatch.setattr(os, "symlink", os_symlink)

Web Proxy Viewer  |  New URL  |  Original Page