|
| 1 | +# E2E Test Suite |
| 2 | + |
| 3 | +End-to-end tests for the oh-my-posh shell integrations themselves (not the Go internals under |
| 4 | +`src/`). They generate real init scripts with the `oh-my-posh` binary, feed them to the actual |
| 5 | +shells, and drive interactive sessions in a pseudo-terminal. |
| 6 | + |
| 7 | +This is a separate Go module (`github.com/jandedobbeleer/oh-my-posh/e2e`, own `go.mod`/`go.sum`) |
| 8 | +so its dependencies never leak into `src/`. |
| 9 | + |
| 10 | +## Layers |
| 11 | + |
| 12 | +1. **Syntax** (`syntax_test.go`) — generate the init script per shell x config overlay and |
| 13 | + validate it with the shell's own parser (`bash -n`, `zsh -n`, `fish --no-execute`, a |
| 14 | + `System.Management.Automation.Language.Parser` call for pwsh, `nu-check` for nu). |
| 15 | +2. **Smoke** (`smoke_test.go`) — boot each shell interactively in a real pty with the generated |
| 16 | + init script, assert the prompt renders cleanly, a typed command runs, and the shell exits. |
| 17 | +3. **Behavior** (`features_test.go`) — per-feature scenarios: exit-code propagation, transient |
| 18 | + prompt, right prompt, styling colors, and FTCS marks. |
| 19 | + |
| 20 | +Both layers 2 and 3 drive the shell through `harness.Session`, a pty wrapper with a vt10x screen |
| 21 | +emulator for rendered-screen assertions and a raw-byte buffer for escape-sequence assertions. |
| 22 | + |
| 23 | +The suite targets the "big five" shells: bash, zsh, fish, pwsh, nu. |
| 24 | + |
| 25 | +## Running locally |
| 26 | + |
| 27 | +```shell |
| 28 | +cd e2e |
| 29 | +go test -count=1 ./... |
| 30 | +``` |
| 31 | + |
| 32 | +Add `-v` for per-shell/per-overlay output, or `-run <Test>/<shell>` to scope to one case. |
| 33 | + |
| 34 | +Every test skips cleanly (`t.Skip`) when a shell's binary is not on `PATH`, or when the shell is |
| 35 | +not supported on the current platform. Set `OMP_E2E_REQUIRE` to a comma-separated list of shell |
| 36 | +names (e.g. `pwsh,nu`) to turn those skips into hard failures for the listed shells instead — |
| 37 | +useful to catch a shell that's supposed to be installed but silently isn't. It defaults to unset |
| 38 | +(everything skips); CI sets it to the shells each job installs so an expected shell hard-fails |
| 39 | +instead of skipping quietly. |
| 40 | + |
| 41 | +| Shell | Linux/macOS | Windows | |
| 42 | +|-------|------------------------------|------------------------------------------------------------| |
| 43 | +| bash | yes (skip if binary missing) | skipped (msys/WSL bash under ConPTY is not representative) | |
| 44 | +| zsh | yes (skip if binary missing) | skipped | |
| 45 | +| fish | yes (skip if binary missing) | skipped | |
| 46 | +| pwsh | yes (skip if binary missing) | yes (skip if binary missing) | |
| 47 | +| nu | yes (skip if binary missing) | yes (skip if binary missing) | |
| 48 | + |
| 49 | +Layer 1 (syntax) still runs its non-empty/no-placeholder assertions unconditionally for every |
| 50 | +shell; only the parser check itself is skipped when the binary is missing. |
| 51 | + |
| 52 | +### Building the omp binary |
| 53 | + |
| 54 | +The suite builds `../src` once per test run (guarded by `sync.Once`) into a temp directory and |
| 55 | +uses that binary for every `init` invocation. Set `OMP_E2E_BINARY` to an absolute path to skip |
| 56 | +the build and reuse a prebuilt binary instead — useful when iterating on tests without rebuilding |
| 57 | +oh-my-posh every run. |
| 58 | + |
| 59 | +### Isolation |
| 60 | + |
| 61 | +Every omp invocation and shell session gets `OMP_CACHE_DIR` pointed at a fresh `t.TempDir()`, so |
| 62 | +runs never share or pollute a developer's real cache. Sessions also fix `TERM=xterm-256color` |
| 63 | +and a 120x30 pty size. |
| 64 | + |
| 65 | +## Adding a new shell |
| 66 | + |
| 67 | +Add an entry to the `Shells` table in `harness/shells.go`. A `ShellDef` needs: |
| 68 | + |
| 69 | +- `Name` — the omp shell name passed to `oh-my-posh init <Name>`. |
| 70 | +- `Binary` — the executable looked up via `LookupShellBinary`; missing means skip. |
| 71 | +- `SyntaxCheck(scriptPath) *exec.Cmd` — a command that parses (not executes) the script and |
| 72 | + exits non-zero on a syntax error. |
| 73 | +- `Launch(t, scriptPath, workDir) (bin string, args []string, env []string)` — how to boot the |
| 74 | + shell interactively with `scriptPath` sourced (e.g. a temp rc file plus `-i`). |
| 75 | +- `Fail` — an `ExitCommand{Command, Code}`: a command line that reliably exits non-zero when |
| 76 | + typed interactively, and the exit code oh-my-posh should report in the next prompt. |
| 77 | + |
| 78 | +If the shell can't be driven faithfully on every platform, add a case to |
| 79 | +`ShellDef.SupportedOnHost` (see the bash/zsh/fish Windows exclusion for the reasoning). |
| 80 | + |
| 81 | +Layers 1-3 all iterate `harness.Shells`, so a correctly filled-in entry is picked up everywhere |
| 82 | +automatically — no test file needs editing for a new shell on its own. |
| 83 | + |
| 84 | +## Adding a new feature scenario |
| 85 | + |
| 86 | +`features_test.go` drives every scenario through one table-driven test, `TestFeatures`, whose |
| 87 | +subtests are named `TestFeatures/<scenario>/<shell>`. To add a scenario: |
| 88 | + |
| 89 | +1. If the feature needs a config change, add an `Overlay` function to `harness/config.go` that |
| 90 | + mutates the base config map (see `Transient`, `RPrompt`, `Colored`, `ShellIntegration` for |
| 91 | + examples). If the overlay changes the generated init script, register it in `overlaySets` in |
| 92 | + `syntax_test.go` so layer 1 covers it too (`Colored` doesn't change the script, so it's left |
| 93 | + out of that matrix). |
| 94 | +2. Append a `scenario` entry to `featureScenarios` in `features_test.go`: |
| 95 | + - `overlays` — the `Overlay`s to apply, if any. |
| 96 | + - `skips` — a `map[string]string` of shell name to skip reason, for shells that don't support |
| 97 | + the feature (see the bash entries on `transient`/`rprompt`, or the nu entry on `ftcs`, for the |
| 98 | + pattern). Leave it `nil` when every shell is expected to pass. |
| 99 | + - `run` — a `func(t *testing.T, sh harness.ShellDef, s *harness.Session)` with the scenario's |
| 100 | + assertions. The `TestFeatures` runner has already applied the overlays, skipped |
| 101 | + unsupported shells, started the session and waited for the first prompt by the time `run` |
| 102 | + is called; drive the rest of the session (`SendLine`, `WaitFor`) and assert against |
| 103 | + `Screen()`/`ScreenLines()` for rendered output, `Raw()` for escape-sequence assertions, or |
| 104 | + `MarkerColor()` for a rendered marker's cell colors. |
| 105 | +3. Never make a shell silently succeed or fail a feature it doesn't support — add it to `skips` |
| 106 | + with a comment explaining why instead. |
| 107 | + |
| 108 | +## Harness internals |
| 109 | + |
| 110 | +Things the harness does that are easy to break by accident: |
| 111 | + |
| 112 | +- **DSR replies** — PSReadLine on a Unix pty repeatedly queries the cursor position (`CSI 6n`) |
| 113 | + and blocks rendering until it gets a reply. ConPTY answers this internally on Windows; on |
| 114 | + Linux/macOS the harness itself replies with the vt10x cursor position from its reader |
| 115 | + goroutine (`harness/session.go`). Remove that and pwsh-on-Linux wedges until the 30s timeout. |
| 116 | +- **Single pty reader** — exactly one goroutine reads the pty and feeds both the vt10x screen |
| 117 | + and the raw buffer under one mutex. A second reader silently loses buffered data. |
| 118 | +- **nu vendor autoload** — nu loads every `.nu` under `$nu.vendor-autoload-dirs` after |
| 119 | + `--config`, so a machine with the real oh-my-posh nu integration installed would clobber the |
| 120 | + test prompt. Sessions point `XDG_DATA_HOME` at an empty temp directory to prevent this. |
| 121 | +- **Absolute binary paths** — go-pty's Windows `Cmd` resolves bare executable names relative to |
| 122 | + `Cmd.Dir` when `Dir` is set, so `Start` always resolves the shell binary to an absolute path |
| 123 | + first. |
| 124 | +- **bash lookup on Windows** — plain `PATH` lookup finds System32's WSL launcher `bash.exe`, |
| 125 | + which cannot run Windows-style script paths; `LookupShellBinary` derives Git Bash's location |
| 126 | + from `git.exe` instead. |
| 127 | + |
| 128 | +## Known limitations |
| 129 | + |
| 130 | +- bash only renders a transient prompt or right prompt inside a `ble.sh` session (gated on |
| 131 | + `BLE_SESSION_ID`, see `src/shell/bash.go`); this harness's plain |
| 132 | + `bash --noprofile --rcfile ... -i` session doesn't provide one, so `TestFeatures/transient` and |
| 133 | + `TestFeatures/rprompt` skip bash explicitly. |
| 134 | +- nu never emits any FTCS mark: `Features().Nu()`'s switch does list a case for `FTCSMarks` (see |
| 135 | + `src/shell/nu.go`), but that case deliberately returns an empty `Code`, so the generated script |
| 136 | + never gets the hook that prints them. `TestFeatures/ftcs` skips nu explicitly. |
| 137 | +- cmd, elvish, xonsh and yash are not covered by any layer. |
| 138 | + |
| 139 | +## CI |
| 140 | + |
| 141 | +`.github/workflows/e2e.yml` runs this suite on `ubuntu-latest` (bash, pwsh preinstalled; zsh and |
| 142 | +fish installed via `apt`; nu installed from a pinned GitHub release) and on `windows-latest` |
| 143 | +(pwsh preinstalled; nu installed from a pinned release; bash/zsh/fish skip by design). |
0 commit comments