|
1 | 1 | # git-cl Test Suite |
2 | 2 |
|
3 | | -## Purpose |
| 3 | +Automated integration tests for [git-cl](https://github.com/BHFock/git-cl). Each test script creates a temporary Git repository, runs git-cl commands, and verifies the results. Every test can also be exported as a standalone shell walkthrough — a step-by-step terminal session you can follow to learn how git-cl works. |
4 | 4 |
|
5 | | -This test suite provides automated integration tests for [git-cl](https://github.com/BHFock/git-cl). It serves two goals: |
6 | | - |
7 | | -1. **Regression testing** — verify that git-cl behaves correctly across Python versions, Git versions, and operating systems. |
8 | | -2. **Worked examples** — each test script can be exported as a standalone shell walkthrough for user training. |
9 | | - |
10 | | -git-cl is feature complete. These tests document and protect existing behaviour rather than driving new features. |
11 | | - |
12 | | - |
13 | | -## Design Decisions |
14 | | - |
15 | | -### Why Python tests with shell export? |
16 | | - |
17 | | -The test framework is written in Python, but each test can be exported as a self-contained shell script. This gives us: |
18 | | - |
19 | | -- **Clean test logic** — assertions, JSON inspection, and temporary repo management are natural in Python. |
20 | | -- **Readable training material** — the exported shell scripts read as step-by-step terminal sessions a user can follow. |
21 | | -- **No external dependencies** — the framework uses only the Python standard library. |
22 | | - |
23 | | -### What is tested? |
24 | | - |
25 | | -Each test script creates a temporary Git repository, runs git-cl commands against it, and verifies the results by checking: |
26 | | - |
27 | | -- **Command output** — does `git cl add` print the expected confirmation? |
28 | | -- **Exit codes** — does the command succeed or fail as expected? |
29 | | -- **Internal metadata** — does `.git/cl.json` contain the right changelist entries? |
30 | | -- **Git state** — are the correct files staged, committed, or reverted? |
31 | | -- **Git status codes** — are files in the expected state (`[ M]`, `[A ]`, `[??]`, etc.)? |
32 | | -- **Working directory context** — do commands behave correctly from subdirectories? |
33 | | - |
34 | | -### What is NOT tested? |
35 | | - |
36 | | -- Interactive terminal behaviour (colour output, terminal width) |
37 | | -- Concurrent access or file locking under load |
38 | | -- Platform-specific behaviour (Windows, network filesystems) |
39 | 5 |
|
| 6 | +## Usage |
40 | 7 |
|
41 | | -## Architecture |
| 8 | +Run all tests: |
42 | 9 |
|
43 | 10 | ``` |
44 | | -tests/ |
45 | | -├── README.md ← this file |
46 | | -├── test_helpers.py ← TestRepo class and shell export logic |
47 | | -├── run_tests.py ← test runner (discovers and runs all test scripts) |
48 | | -├── test_basic_add_status.py |
49 | | -├── test_stage_unstage.py |
50 | | -├── test_commit.py |
51 | | -├── test_diff.py |
52 | | -├── test_remove_delete.py |
53 | | -├── test_checkout.py |
54 | | -├── test_stash_unstash.py |
55 | | -├── test_branch.py |
56 | | -├── test_validation.py |
57 | | -├── test_git_states.py |
58 | | -├── test_subdirectory.py |
59 | | -└── test_edge_cases.py |
60 | | -``` |
61 | | - |
62 | | -### test_helpers.py |
63 | | - |
64 | | -Provides the `TestRepo` class — a context manager that: |
65 | | - |
66 | | -- Creates a fresh temporary Git repository with an initial commit |
67 | | -- Offers helper methods for common operations (`write_file`, `run`, `load_cl_json`, ...) |
68 | | -- Records every operation so the session can be exported as a shell script |
69 | | -- Provides assertion methods that print pass/fail results and log themselves for export |
70 | | -- Cleans up the temporary directory on exit |
71 | | - |
72 | | -#### run() — command execution |
73 | | - |
74 | | -`run()` accepts commands as a string or as a list of arguments: |
75 | | - |
76 | | -```python |
77 | | -# Simple commands — string is fine, parsed with shlex.split() |
78 | | -repo.run("git cl add docs file.txt") |
79 | | - |
80 | | -# Arguments containing spaces — use a list for precise control |
81 | | -repo.run(["git", "cl", "commit", "docs", "-m", "Fix the bug"]) |
| 11 | +./run_tests.py |
82 | 12 | ``` |
83 | 13 |
|
84 | | -When a string is passed, it is split using `shlex.split()`, which handles quoted |
85 | | -arguments correctly (e.g. `'git cl commit docs -m "Fix the bug"'` works as expected). |
86 | | - |
87 | | -When a list is passed, it is used directly as the argument vector. |
88 | | - |
89 | | -For shell export, list-form commands are reconstructed with proper quoting. |
90 | | - |
91 | | -#### run_in() — subdirectory execution |
92 | | - |
93 | | -`run_in(subdir, command)` runs a command from within a subdirectory of the |
94 | | -repository, without changing the test process's working directory: |
| 14 | +Run a single test: |
95 | 15 |
|
96 | | -```python |
97 | | -# Test that adding a file from a subdirectory normalises the path |
98 | | -repo.run_in("src", "git cl add my-list main.py") |
99 | 16 | ``` |
100 | | - |
101 | | -This passes `cwd=repo_dir/subdir` to `subprocess.run`. In the shell export, it |
102 | | -becomes a subshell: `(cd src && git cl add my-list main.py)`. |
103 | | - |
104 | | -### Test scripts |
105 | | - |
106 | | -Each test script is executable and follows the same structure: |
107 | | - |
108 | | -```python |
109 | | -#!/usr/bin/env python3 |
110 | | -from test_helpers import TestRepo |
111 | | - |
112 | | -def run_tests(repo: TestRepo): |
113 | | - repo.section("Setup") |
114 | | - # ... prepare repository ... |
115 | | - |
116 | | - repo.section("Test scenario name") |
117 | | - output = repo.run("git cl add my-list file.txt") |
118 | | - repo.assert_in("Added to", output, "confirms addition") |
119 | | - |
120 | | - cl = repo.load_cl_json() |
121 | | - repo.assert_true("my-list" in cl, "changelist exists") |
122 | | - |
123 | | -if __name__ == "__main__": |
124 | | - # Handles both test execution and --export |
125 | | - ... |
| 17 | +./test_basic_add_status.py |
126 | 18 | ``` |
127 | 19 |
|
128 | | -### Shell export |
129 | | - |
130 | | -Every test script supports `--export` to produce a shell walkthrough: |
| 20 | +Export a test as a shell walkthrough: |
131 | 21 |
|
132 | 22 | ``` |
133 | 23 | ./test_basic_add_status.py --export > walkthrough_add_status.sh |
134 | 24 | ``` |
135 | 25 |
|
136 | | -The exported script contains: |
137 | | -- The same git-cl commands as the Python test |
138 | | -- `# Check:` comments where assertions were, describing expected outcomes |
139 | | -- Section headers matching the test structure |
140 | 26 |
|
141 | | -In export mode, assertion output (pass/fail lines) is suppressed. Only the |
142 | | -shell script is written to stdout. |
| 27 | +## Shell Walkthroughs |
143 | 28 |
|
144 | | -The exported scripts are for reading and manual line-by-line execution, not |
145 | | -for automated testing. |
| 29 | +Every test script supports `--export` to produce a self-contained shell script. These walkthroughs mirror the test scenarios but are written for reading and manual line-by-line execution. `# Check:` comments tell you what to expect at each step. |
146 | 30 |
|
| 31 | +Here is a shortened example from `test_basic_add_status.py --export`: |
147 | 32 |
|
148 | | -## Test Plan |
| 33 | +```bash |
| 34 | +#!/usr/bin/env bash |
| 35 | +# git-cl walkthrough: add and status |
| 36 | +# |
| 37 | +# Run it line by line to learn how git-cl works. |
| 38 | +# Lines starting with '# Check:' tell you what to expect. |
149 | 39 |
|
150 | | -### Core Commands |
| 40 | +set -euo pipefail |
151 | 41 |
|
152 | | -| Script | Commands under test | Key behaviours verified | |
153 | | -|-------------------------------|----------------------------------------|------------------------------------------------------------| |
154 | | -| `test_basic_add_status.py` | `add`, `status`, `st` | Creating changelists, assigning files, reassignment, | |
155 | | -| | | status grouping, filtering, `--include-no-cl`, aliases | |
156 | | -| `test_stage_unstage.py` | `stage`, `unstage` | Staging tracked files, skipping untracked files, | |
157 | | -| | | `--delete` flag, round-trip stage/unstage, changelist | |
158 | | -| | | preserved by default | |
159 | | -| `test_commit.py` | `commit`, `ci` | Commit with `-m` and `-F`, `--keep` flag, changelist | |
160 | | -| | | deleted by default, untracked files skipped, alias | |
161 | | -| `test_diff.py` | `diff` | Single changelist diff, multiple changelist diff, | |
162 | | -| | | `--staged` flag | |
163 | | -| `test_remove_delete.py` | `remove`, `rm`, `delete`, `del` | Removing files from changelists, deleting changelists, | |
164 | | -| | | `--all` flag, files untouched on disk, aliases | |
165 | | -| `test_checkout.py` | `checkout`, `co` | Reverting files to HEAD, only affects named changelist, | |
166 | | -| | | changelist kept by default, `--delete` flag, alias | |
| 42 | +# Create a temporary Git repository |
| 43 | +cd $(mktemp -d) |
| 44 | +git init --quiet |
| 45 | +git config user.email "test@git-cl.test" |
| 46 | +git config user.name "git-cl test" |
167 | 47 |
|
168 | | -### Advanced Commands |
| 48 | +# === Setup: create files with an initial commit === |
169 | 49 |
|
170 | | -| Script | Commands under test | Key behaviours verified | |
171 | | -|-------------------------------|----------------------------------------|------------------------------------------------------------| |
172 | | -| `test_stash_unstash.py` | `stash`, `unstash` | Stash single changelist, unstash restores files and | |
173 | | -| | | metadata, `--all` flag, selective unstash after stash all | |
174 | | -| `test_branch.py` | `branch`, `br` | Branch from changelist, custom branch name, `--from` base, | |
175 | | -| | | other changelists stashed, alias | |
176 | | - |
177 | | -### Git States and Working Directory |
178 | | - |
179 | | -| Script | Focus area | Key behaviours verified | |
180 | | -|-------------------------------|----------------------------------------|------------------------------------------------------------| |
181 | | -| `test_git_states.py` | Git status codes | Changelists with files in each common state: | |
182 | | -| | | `[ M]` unstaged modification, `[M ]` staged modification, | |
183 | | -| | | `[MM]` mixed staged/unstaged, `[A ]` newly added, | |
184 | | -| | | `[AM]` added then modified, `[ D]` unstaged deletion, | |
185 | | -| | | `[D ]` staged deletion, `[??]` untracked. | |
186 | | -| | | Correct display in `git cl st`, correct behaviour with | |
187 | | -| | | `--all` flag for uncommon codes. | |
188 | | -| `test_subdirectory.py` | Path resolution | Running git-cl commands from the repo root, a subdirectory,| |
189 | | -| | | and a nested subdirectory. Verifying that paths are stored | |
190 | | -| | | as repo-root-relative in `cl.json` and displayed correctly | |
191 | | -| | | relative to the current working directory. | |
192 | | - |
193 | | -### Validation and Edge Cases |
194 | | - |
195 | | -| Script | Focus area | Key behaviours verified | |
196 | | -|-------------------------------|----------------------------------------|------------------------------------------------------------| |
197 | | -| `test_validation.py` | Input validation | Invalid changelist names (special chars, reserved words, | |
198 | | -| | | dots-only, too long), dangerous paths, directory traversal,| |
199 | | -| | | non-existent files, non-existent changelists | |
200 | | -| `test_edge_cases.py` | Boundary conditions | Empty working directory, no changelists, nested | |
201 | | -| | | subdirectories, deleted files, file reassignment, | |
202 | | -| | | already-staged files, rapid add/remove cycles | |
203 | | - |
204 | | - |
205 | | -## Requirements |
206 | | - |
207 | | -- Python 3.10+ |
208 | | -- Git |
209 | | -- A Unix-like OS (Linux, macOS) |
210 | | -- `git-cl` available in `$PATH` |
| 50 | +echo "hello" > file1.txt |
| 51 | +echo "world" > file2.txt |
| 52 | +git add file1.txt file2.txt |
| 53 | +git commit --quiet -m "Add initial files" |
| 54 | +echo "hello modified" > file1.txt |
| 55 | +echo "world modified" > file2.txt |
211 | 56 |
|
| 57 | +# === Add files to a new changelist === |
212 | 58 |
|
213 | | -## Usage |
| 59 | +git cl add feature1 file1.txt file2.txt |
| 60 | +# Check: output contains 'Added to 'feature1'' |
214 | 61 |
|
215 | | -Run all tests: |
| 62 | +# === View status grouped by changelist === |
216 | 63 |
|
217 | | -``` |
218 | | -./run_tests.py |
| 64 | +git cl st |
| 65 | +# Check: output contains 'feature1:' |
| 66 | +# Check: output contains 'file1.txt' |
| 67 | +# Check: output contains 'file2.txt' |
219 | 68 | ``` |
220 | 69 |
|
221 | | -Run a single test: |
222 | | - |
223 | | -``` |
224 | | -./test_basic_add_status.py |
225 | | -``` |
226 | 70 |
|
227 | | -Export a test as a shell walkthrough: |
| 71 | +## What's Tested |
228 | 72 |
|
229 | | -``` |
230 | | -./test_basic_add_status.py --export > walkthrough_add_status.sh |
231 | | -``` |
| 73 | +### Core Commands |
232 | 74 |
|
| 75 | +| Script | Commands | |
| 76 | +|---|---| |
| 77 | +| `test_basic_add_status.py` | `add`, `status` / `st`, filtering, `--include-no-cl` | |
| 78 | +| `test_stage_unstage.py` | `stage`, `unstage`, `--delete` flag, round-trip | |
| 79 | +| `test_commit.py` | `commit` / `ci`, `-m`, `-F`, `--keep` flag | |
| 80 | +| `test_diff.py` | `diff`, multiple changelists, `--staged` | |
| 81 | +| `test_remove_delete.py` | `remove` / `rm`, `delete` / `del`, `--all` | |
| 82 | +| `test_checkout.py` | `checkout` / `co`, `--delete`, `--force` | |
233 | 83 |
|
234 | | -## Resolved Design Decisions |
| 84 | +### Advanced Commands |
235 | 85 |
|
236 | | -> Decisions made during development of the test framework. |
| 86 | +| Script | Commands | |
| 87 | +|---|---| |
| 88 | +| `test_stash_unstash.py` | `stash` / `sh`, `unstash` / `us`, `--all` | |
| 89 | +| `test_branch.py` | `branch` / `br`, custom name, `--from` base | |
237 | 90 |
|
238 | | -### Commit messages with spaces |
| 91 | +### States, Paths, and Validation |
239 | 92 |
|
240 | | -`run()` uses `shlex.split()` when given a string, which correctly handles quoted |
241 | | -arguments. For full control, a list of arguments can be passed instead. Shell export |
242 | | -reconstructs the command with proper quoting from the list form. |
| 93 | +| Script | Focus | |
| 94 | +|---|---| |
| 95 | +| `test_git_states.py` | All common Git status codes (`[ M]`, `[M ]`, `[MM]`, `[A ]`, `[AM]`, `[ D]`, `[D ]`, `[??]`) | |
| 96 | +| `test_subdirectory.py` | Path normalisation from subdirectories, cross-directory add, relative display | |
| 97 | +| `test_validation.py` | Invalid names, reserved words, path traversal, missing arguments | |
| 98 | +| `test_edge_cases.py` | Empty states, reassignment, duplicate files, deleted files | |
243 | 99 |
|
244 | | -### Subdirectory execution |
245 | 100 |
|
246 | | -`run_in(subdir, command)` sets the `cwd` for the subprocess without affecting the |
247 | | -test process. Exported as `(cd subdir && command)` in shell scripts. |
| 101 | +## How the Tests Work |
248 | 102 |
|
249 | | -### Export noise |
| 103 | +The test framework lives in `test_helpers.py` and provides a single class: `TestRepo`. It is used as a context manager that creates a fresh temporary Git repository on entry and cleans it up on exit. |
250 | 104 |
|
251 | | -In `--export` mode, all assertion output is suppressed. Only the shell script is |
252 | | -written to stdout. This means `./test_basic_add_status.py --export > walkthrough.sh` |
253 | | -produces a clean file with no test noise mixed in. |
| 105 | +`TestRepo` offers helpers for common operations — `write_file`, `run`, `run_in` (execute from a subdirectory), `load_cl_json`, `get_staged_files`, and a set of assertion methods (`assert_in`, `assert_equal`, `assert_exit_code`, etc.) that print pass/fail results during the test run. |
254 | 106 |
|
255 | | -### Test runner |
| 107 | +Every operation is recorded internally. When a test script is run with `--export`, the recorded operations are replayed as a shell script instead of executing assertions. This is how the same test code serves both as an automated check and as a readable walkthrough. |
256 | 108 |
|
257 | | -`run_tests.py` discovers `test_*.py` files in its directory, runs each as a subprocess, |
258 | | -collects exit codes, and prints a summary. Each test script is self-contained and can |
259 | | -be run independently. |
| 109 | +Each `test_*.py` script follows the same pattern: import `TestRepo`, define a `run_tests(repo)` function with sections and assertions, and handle both normal execution and `--export` mode in `__main__`. Test scripts are self-contained and can be run independently. `run_tests.py` discovers and runs all of them, collecting results into a summary. |
0 commit comments