Skip to content

Commit e702596

Browse files
committed
feat: Add draft implementation of E2E tests
1 parent 35fb30e commit e702596

7 files changed

Lines changed: 190 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Test
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
test-e2e:
11+
name: E2E Tests (${{ matrix.os }})
12+
runs-on: ${{ matrix.os }}
13+
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
os: [ubuntu-latest, macos-latest, windows-latest]
18+
python-version: ["3.13"]
19+
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Python ${{ matrix.python-version }}
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: ${{ matrix.python-version }}
28+
29+
- name: Install dependencies
30+
run: |
31+
python -m pip install --upgrade pip
32+
pip install -r requirements.txt
33+
34+
- name: Build binary
35+
run: |
36+
pyinstaller --onefile main.py --name gitmastery
37+
38+
- name: Set binary path (Unix)
39+
if: runner.os != 'Windows'
40+
run: echo "GITMASTERY_BINARY=./dist/gitmastery" >> $GITHUB_ENV
41+
42+
- name: Set binary path (Windows)
43+
if: runner.os == 'Windows'
44+
run: echo "GITMASTERY_BINARY=./dist/gitmastery.exe" >> $env:GITHUB_ENV
45+
46+
- name: Run E2E tests
47+
run: |
48+
python -m pytest tests/e2e/ -v

scripts/build_test_e2e.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
# Build and run E2E tests for gitmastery
3+
4+
set -e
5+
FILENAME="gitmastery"
6+
7+
echo "Building gitmastery binary..."
8+
pyinstaller --onefile main.py --name $FILENAME
9+
10+
echo "Running E2E tests..."
11+
pytest tests/e2e -v
12+
13+
echo "All E2E tests passed!"

tests/e2e/__init__.py

Whitespace-only changes.

tests/e2e/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import pytest
2+
3+
from .runner import BinaryRunner
4+
5+
6+
@pytest.fixture(scope="session")
7+
def runner() -> BinaryRunner:
8+
return BinaryRunner.from_env()

tests/e2e/result.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from dataclasses import dataclass
2+
from typing import List, Self
3+
import re
4+
5+
6+
@dataclass(frozen=True)
7+
class RunResult:
8+
"""Immutable result from a binary execution."""
9+
10+
stdout: str
11+
stderr: str
12+
returncode: int
13+
command: List[str]
14+
15+
def assert_success(self) -> Self:
16+
"""Assert the command exited with code 0."""
17+
ERROR_MSG = (
18+
f"Expected exit code 0, got {self.returncode}\n"
19+
f"Command: {' '.join(self.command)}\n"
20+
f"stdout:\n{self.stdout}\n"
21+
f"stderr:\n{self.stderr}"
22+
)
23+
assert self.returncode == 0, ERROR_MSG
24+
return self
25+
26+
def assert_stdout_contains(self, text: str) -> Self:
27+
"""Assert stdout contains the given text."""
28+
ERROR_MSG = (
29+
f"Expected stdout to contain {text!r}\nActual stdout:\n{self.stdout}"
30+
)
31+
assert text in self.stdout, ERROR_MSG
32+
return self
33+
34+
def assert_stdout_matches(self, pattern: str, flags: int = 0) -> Self:
35+
"""Assert stdout matches a regex pattern."""
36+
ERROR_MSG = (
37+
f"Expected stdout to match pattern {pattern!r}\n"
38+
f"Actual stdout:\n{self.stdout}"
39+
)
40+
assert re.search(pattern, self.stdout, flags), ERROR_MSG
41+
return self

tests/e2e/runner.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import os
2+
import platform
3+
import subprocess
4+
from dataclasses import dataclass
5+
from pathlib import Path
6+
from typing import Dict, Optional, Sequence, Self
7+
from .result import RunResult
8+
9+
10+
@dataclass
11+
class BinaryRunner:
12+
"""Cross-platform runner for the gitmastery binary."""
13+
14+
binary_path: str
15+
project_root: Path
16+
17+
@classmethod
18+
def from_env(
19+
cls,
20+
env_var: str = "GITMASTERY_BINARY",
21+
project_root: Optional[Path] = None,
22+
) -> Self:
23+
"""Build a runner from an environment variable."""
24+
if project_root is None:
25+
project_root = Path(__file__).resolve().parents[2]
26+
27+
raw = os.environ.get(env_var, "").strip()
28+
if raw:
29+
binary_path = raw
30+
else:
31+
system = platform.system().lower()
32+
if system == "windows":
33+
binary_path = str(project_root / "dist" / "gitmastery.exe")
34+
else:
35+
binary_path = str(project_root / "dist" / "gitmastery")
36+
37+
return cls(binary_path=binary_path, project_root=project_root)
38+
39+
def run(
40+
self,
41+
args: Sequence[str] = (),
42+
*,
43+
cwd: Optional[Path] = None,
44+
env: Optional[Dict[str, str]] = None,
45+
timeout: int = 30,
46+
stdin_text: Optional[str] = None,
47+
) -> RunResult:
48+
"""Execute the binary with args and return a RunResult."""
49+
cmd = [self.binary_path] + list(args)
50+
run_env = os.environ.copy()
51+
if env:
52+
run_env.update(env)
53+
54+
run_env.setdefault("NO_COLOR", "1")
55+
run_env.setdefault("PYTHONIOENCODING", "utf-8")
56+
57+
proc = subprocess.run(
58+
cmd,
59+
cwd=str(cwd) if cwd else str(self.project_root),
60+
env=run_env,
61+
capture_output=True,
62+
text=True,
63+
timeout=timeout if timeout > 0 else None,
64+
input=stdin_text,
65+
)
66+
return RunResult(
67+
stdout=proc.stdout,
68+
stderr=proc.stderr,
69+
returncode=proc.returncode,
70+
command=cmd,
71+
)

tests/e2e/test_version.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .runner import BinaryRunner
2+
3+
4+
def test_version(runner: BinaryRunner) -> None:
5+
"""Test the version command output."""
6+
res = runner.run(["version"])
7+
res.assert_success()
8+
res.assert_stdout_contains("Git-Mastery app is")
9+
res.assert_stdout_matches(r"v\d+\.\d+\.\d+")

0 commit comments

Comments
 (0)