Skip to content

Commit e00fe2e

Browse files
authored
Support running all tests in WebAssembly (#89)
* Support running all tests in WebAssembly * Add test-wasm CI run, on demand only
1 parent efcbd3e commit e00fe2e

File tree

12 files changed

+185
-36
lines changed

12 files changed

+185
-36
lines changed

.github/workflows/test-wasm.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Test WebAssembly build in cockle deployment
2+
name: Test WebAssembly
3+
4+
on:
5+
workflow_dispatch:
6+
7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.ref }}
9+
cancel-in-progress: true
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v6
17+
18+
- name: Setup Python
19+
uses: actions/setup-python@v6
20+
with:
21+
python-version: '3.14'
22+
23+
- name: Install mamba
24+
uses: mamba-org/setup-micromamba@v2
25+
with:
26+
environment-file: wasm/wasm-environment.yml
27+
cache-environment: true
28+
29+
- name: Build
30+
shell: bash -l {0}
31+
working-directory: wasm
32+
run: |
33+
cmake .
34+
make build-recipe
35+
make built-test
36+
37+
- name: Upload artifact containing emscripten-forge package
38+
uses: actions/upload-pages-artifact@v4
39+
with:
40+
path: ./wasm/recipe/em-forge-recipes/output/
41+
42+
- name: Run WebAssembly tests
43+
shell: bash -l {0}
44+
working-directory: wasm
45+
run: |
46+
make test

test/conftest.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ def xtl_clone(git2cpp_path, tmp_path, run_in_tmp_path):
3535

3636
@pytest.fixture
3737
def commit_env_config(monkeypatch):
38-
monkeypatch.setenv("GIT_AUTHOR_NAME", "Jane Doe")
39-
monkeypatch.setenv("GIT_AUTHOR_EMAIL", "jane.doe@blabla.com")
40-
monkeypatch.setenv("GIT_COMMITTER_NAME", "Jane Doe")
41-
monkeypatch.setenv("GIT_COMMITTER_EMAIL", "jane.doe@blabla.com")
38+
config = {
39+
"GIT_AUTHOR_NAME": "Jane Doe",
40+
"GIT_AUTHOR_EMAIL": "jane.doe@blabla.com",
41+
"GIT_COMMITTER_NAME": "Jane Doe",
42+
"GIT_COMMITTER_EMAIL": "jane.doe@blabla.com"
43+
}
44+
for key, value in config.items():
45+
if GIT2CPP_TEST_WASM:
46+
subprocess.run(["export", f"{key}='{value}'"], check=True)
47+
else:
48+
monkeypatch.setenv(key, value)

test/conftest_wasm.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,24 @@
1313
# This can be removed when all tests support wasm.
1414
def pytest_ignore_collect(collection_path: pathlib.Path) -> bool:
1515
return collection_path.name not in [
16+
"test_add.py",
17+
"test_branch.py",
18+
"test_checkout.py"
1619
"test_clone.py",
20+
"test_commit.py",
21+
"test_config.py",
1722
"test_fixtures.py",
1823
"test_git.py",
1924
"test_init.py",
25+
"test_log.py",
26+
"test_merge.py",
27+
"test_rebase.py",
28+
"test_remote.py",
29+
"test_reset.py",
30+
"test_revlist.py",
31+
"test_revparse.py",
32+
"test_stash.py",
33+
"test_status.py",
2034
]
2135

2236

@@ -48,6 +62,10 @@ def os_getcwd():
4862
return subprocess.run(["pwd"], capture_output=True, check=True, text=True).stdout.strip()
4963

5064

65+
def os_remove(file: str):
66+
return subprocess.run(["rm", str(file)], capture_output=True, check=True, text=True)
67+
68+
5169
class MockPath(pathlib.Path):
5270
def __init__(self, path: str = ""):
5371
super().__init__(path)
@@ -69,6 +87,23 @@ def iterdir(self):
6987
for f in filter(lambda f: f not in ['', '.', '..'], re.split(r"\r?\n", p.stdout)):
7088
yield MockPath(self / f)
7189

90+
def mkdir(self):
91+
subprocess.run(["mkdir", str(self)], capture_output=True, text=True, check=True)
92+
93+
def read_text(self) -> str:
94+
p = subprocess.run(["cat", str(self)], capture_output=True, text=True, check=True)
95+
text = p.stdout
96+
if text.endswith("\n"):
97+
text = text[:-1]
98+
return text
99+
100+
def write_text(self, data: str):
101+
# Note that in general it is not valid to direct output of a subprocess.run call to a file,
102+
# but we get away with it here as the command arguments are passed straight through to
103+
# cockle without being checked.
104+
p = subprocess.run(["echo", data, ">", str(self)], capture_output=True, text=True)
105+
assert p.returncode == 0
106+
72107
def __truediv__(self, other):
73108
if isinstance(other, str):
74109
return MockPath(f"{self}/{other}")
@@ -82,25 +117,39 @@ def subprocess_run(
82117
capture_output: bool = False,
83118
check: bool = False,
84119
cwd: str | MockPath | None = None,
120+
input: str | None = None,
85121
text: bool | None = None
86122
) -> subprocess.CompletedProcess:
87-
shell_run = "async cmd => await window.cockle.shellRun(cmd)"
123+
shell_run = "async obj => await window.cockle.shellRun(obj.cmd, obj.input)"
88124

89125
# Set cwd.
90126
if cwd is not None:
91-
proc = page.evaluate(shell_run, "pwd")
127+
proc = page.evaluate(shell_run, { "cmd": "pwd" } )
92128
if proc['returncode'] != 0:
93129
raise RuntimeError("Error getting pwd")
94130
old_cwd = proc['stdout'].strip()
95131
if old_cwd == str(cwd):
96132
# cwd is already correct.
97133
cwd = None
98134
else:
99-
proc = page.evaluate(shell_run, f"cd {cwd}")
135+
proc = page.evaluate(shell_run, { "cmd": f"cd {cwd}" } )
100136
if proc['returncode'] != 0:
101137
raise RuntimeError(f"Error setting cwd to {cwd}")
102138

103-
proc = page.evaluate(shell_run, " ".join(cmd))
139+
def maybe_wrap_arg(s: str | MockPath) -> str:
140+
# An argument containing spaces needs to be wrapped in quotes if it is not already, due
141+
# to how the command is passed to cockle as a single string.
142+
# Could do better here.
143+
s = str(s)
144+
if ' ' in s and not s.endswith("'"):
145+
return "'" + s + "'"
146+
return s
147+
148+
shell_run_args = {
149+
"cmd": " ".join([maybe_wrap_arg(s) for s in cmd]),
150+
"input": input
151+
}
152+
proc = page.evaluate(shell_run, shell_run_args)
104153

105154
# TypeScript object is auto converted to Python dict.
106155
# Want to return subprocess.CompletedProcess, consider namedtuple if this fails in future.
@@ -112,7 +161,7 @@ def subprocess_run(
112161

113162
# Reset cwd.
114163
if cwd is not None:
115-
proc = page.evaluate(shell_run, "cd " + old_cwd)
164+
proc = page.evaluate(shell_run, { "cmd": "cd " + old_cwd } )
116165
if proc['returncode'] != 0:
117166
raise RuntimeError(f"Error setting cwd to {old_cwd}")
118167

@@ -142,3 +191,4 @@ def mock_subprocess_run(page: Page, monkeypatch):
142191
monkeypatch.setattr(subprocess, "run", partial(subprocess_run, page))
143192
monkeypatch.setattr(os, "chdir", os_chdir)
144193
monkeypatch.setattr(os, "getcwd", os_getcwd)
194+
monkeypatch.setattr(os, "remove", os_remove)

test/test_add.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import subprocess
22

33
import pytest
4+
from .conftest import GIT2CPP_TEST_WASM
45

56

67
@pytest.mark.parametrize("all_flag", ["", "-A", "--all", "--no-ignore-removal"])
@@ -38,5 +39,8 @@ def test_add_nogit(git2cpp_path, tmp_path):
3839
p.write_text('')
3940

4041
cmd_add = [git2cpp_path, 'add', 'mook_file.txt']
41-
p_add = subprocess.run(cmd_add, cwd=tmp_path, text=True)
42-
assert p_add.returncode != 0
42+
p_add = subprocess.run(cmd_add, cwd=tmp_path, text=True, capture_output=True)
43+
if not GIT2CPP_TEST_WASM:
44+
# TODO: fix this in wasm build
45+
assert p_add.returncode != 0
46+
assert "error: could not find repository at" in p_add.stderr

test/test_branch.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import subprocess
22

33
import pytest
4+
from .conftest import GIT2CPP_TEST_WASM
45

56

67
def test_branch_list(xtl_clone, git2cpp_path, tmp_path):
@@ -37,7 +38,11 @@ def test_branch_create_delete(xtl_clone, git2cpp_path, tmp_path):
3738
def test_branch_nogit(git2cpp_path, tmp_path):
3839
cmd = [git2cpp_path, 'branch']
3940
p = subprocess.run(cmd, capture_output=True, cwd=tmp_path, text=True)
40-
assert p.returncode != 0
41+
if not GIT2CPP_TEST_WASM:
42+
# TODO: fix this in wasm build
43+
assert p.returncode != 0
44+
assert "error: could not find repository at" in p.stderr
45+
4146

4247
def test_branch_new_repo(git2cpp_path, tmp_path, run_in_tmp_path):
4348
# tmp_path exists and is empty.

test/test_config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import subprocess
22

33
import pytest
4+
from .conftest import GIT2CPP_TEST_WASM
45

56

67
def test_config_list(commit_env_config, git2cpp_path, tmp_path):
@@ -52,5 +53,7 @@ def test_config_unset(git2cpp_path, tmp_path):
5253

5354
cmd_get = [git2cpp_path, "config", "get", "core.bare"]
5455
p_get = subprocess.run(cmd_get, capture_output=True, cwd=tmp_path, text=True)
55-
assert p_get.returncode != 0
56+
if not GIT2CPP_TEST_WASM:
57+
# TODO: fix this in wasm build
58+
assert p_get.returncode != 0
5659
assert p_get.stderr == "error: config value 'core.bare' was not found\n"

test/test_log.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import subprocess
22

33
import pytest
4+
from .conftest import GIT2CPP_TEST_WASM
45

56

67
@pytest.mark.parametrize("format_flag", ["", "--format=full", "--format=fuller"])
@@ -40,7 +41,10 @@ def test_log(xtl_clone, commit_env_config, git2cpp_path, tmp_path, format_flag):
4041
def test_log_nogit(commit_env_config, git2cpp_path, tmp_path):
4142
cmd_log = [git2cpp_path, "log"]
4243
p_log = subprocess.run(cmd_log, capture_output=True, cwd=tmp_path, text=True)
43-
assert p_log.returncode != 0
44+
if not GIT2CPP_TEST_WASM:
45+
# TODO: fix this in wasm build
46+
assert p_log.returncode != 0
47+
assert "error: could not find repository at" in p_log.stderr
4448

4549

4650
@pytest.mark.parametrize("max_count_flag", ["", "-n", "--max-count"])

test/test_merge.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,12 @@ def test_merge_conflict(xtl_clone, commit_env_config, git2cpp_path, tmp_path, fl
169169
)
170170
assert p_abort.returncode == 0
171171
assert (xtl_path / "mook_file.txt").exists()
172-
with open(xtl_path / "mook_file.txt") as f:
173-
if answer == "y":
174-
assert "BLA" in f.read()
175-
assert "bla" not in f.read()
176-
else:
177-
assert "Abort." in p_abort.stdout
172+
text = (xtl_path / "mook_file.txt").read_text()
173+
if answer == "y":
174+
assert "BLA" in text
175+
assert "bla" not in text
176+
else:
177+
assert "Abort." in p_abort.stdout
178178

179179
elif flag == "--quit":
180180
pass

test/test_rebase.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import subprocess
22

33
import pytest
4+
from .conftest import GIT2CPP_TEST_WASM
45

56

67
def test_rebase_basic(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
@@ -195,9 +196,7 @@ def test_rebase_abort(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
195196
assert "Rebase aborted" in p_abort.stdout
196197

197198
# Verify we're back to original state
198-
with open(conflict_file) as f:
199-
content = f.read()
200-
assert content == "feature content"
199+
assert conflict_file.read_text() == "feature content"
201200

202201

203202
def test_rebase_continue(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
@@ -237,9 +236,7 @@ def test_rebase_continue(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
237236
assert "Successfully rebased" in p_continue.stdout
238237

239238
# Verify resolution
240-
with open(conflict_file) as f:
241-
content = f.read()
242-
assert content == "resolved content"
239+
assert conflict_file.read_text() == "resolved content"
243240

244241

245242
def test_rebase_skip(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
@@ -349,7 +346,10 @@ def test_rebase_no_upstream_error(xtl_clone, commit_env_config, git2cpp_path, tm
349346

350347
rebase_cmd = [git2cpp_path, "rebase"]
351348
p_rebase = subprocess.run(rebase_cmd, capture_output=True, cwd=xtl_path, text=True)
352-
assert p_rebase.returncode != 0
349+
if not GIT2CPP_TEST_WASM:
350+
# TODO: fix this in wasm build
351+
assert p_rebase.returncode != 0
352+
assert "upstream is required for rebase" in p_rebase.stderr
353353

354354

355355
def test_rebase_invalid_upstream_error(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
@@ -359,7 +359,9 @@ def test_rebase_invalid_upstream_error(xtl_clone, commit_env_config, git2cpp_pat
359359

360360
rebase_cmd = [git2cpp_path, "rebase", "nonexistent-branch"]
361361
p_rebase = subprocess.run(rebase_cmd, capture_output=True, cwd=xtl_path, text=True)
362-
assert p_rebase.returncode != 0
362+
if not GIT2CPP_TEST_WASM:
363+
# TODO: fix this in wasm build
364+
assert p_rebase.returncode != 0
363365
assert "could not resolve upstream" in p_rebase.stderr or "could not resolve upstream" in p_rebase.stdout
364366

365367

@@ -388,7 +390,9 @@ def test_rebase_already_in_progress_error(xtl_clone, commit_env_config, git2cpp_
388390
# Try to start another rebase
389391
rebase_cmd = [git2cpp_path, "rebase", "master"]
390392
p_rebase = subprocess.run(rebase_cmd, capture_output=True, cwd=xtl_path, text=True)
391-
assert p_rebase.returncode != 0
393+
if not GIT2CPP_TEST_WASM:
394+
# TODO: fix this in wasm build
395+
assert p_rebase.returncode != 0
392396
assert "rebase is already in progress" in p_rebase.stderr or "rebase is already in progress" in p_rebase.stdout
393397

394398

@@ -399,7 +403,9 @@ def test_rebase_continue_without_rebase_error(xtl_clone, commit_env_config, git2
399403

400404
continue_cmd = [git2cpp_path, "rebase", "--continue"]
401405
p_continue = subprocess.run(continue_cmd, capture_output=True, cwd=xtl_path, text=True)
402-
assert p_continue.returncode != 0
406+
if not GIT2CPP_TEST_WASM:
407+
# TODO: fix this in wasm build
408+
assert p_continue.returncode != 0
403409
assert "No rebase in progress" in p_continue.stderr or "No rebase in progress" in p_continue.stdout
404410

405411

@@ -427,5 +433,7 @@ def test_rebase_continue_with_unresolved_conflicts(xtl_clone, commit_env_config,
427433
# Try to continue without resolving
428434
continue_cmd = [git2cpp_path, "rebase", "--continue"]
429435
p_continue = subprocess.run(continue_cmd, capture_output=True, cwd=xtl_path, text=True)
430-
assert p_continue.returncode != 0
436+
if not GIT2CPP_TEST_WASM:
437+
# TODO: fix this in wasm build
438+
assert p_continue.returncode != 0
431439
assert "resolve conflicts" in p_continue.stderr or "resolve conflicts" in p_continue.stdout

test/test_reset.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import subprocess
22

33
import pytest
4+
from .conftest import GIT2CPP_TEST_WASM
45

56

67
def test_reset(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
@@ -36,4 +37,7 @@ def test_reset(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
3637
def test_reset_nogit(git2cpp_path, tmp_path):
3738
cmd_reset = [git2cpp_path, "reset", "--hard", "HEAD~1"]
3839
p_reset = subprocess.run(cmd_reset, capture_output=True, cwd=tmp_path, text=True)
39-
assert p_reset.returncode != 0
40+
if not GIT2CPP_TEST_WASM:
41+
# TODO: fix this in wasm build
42+
assert p_reset.returncode != 0
43+
assert "error: could not find repository at" in p_reset.stderr

0 commit comments

Comments
 (0)