|
| 1 | +From 7acbb031654404c9fd711eee9974c88475de98fd Mon Sep 17 00:00:00 2001 |
| 2 | +From: Johannes Schindelin < [email protected]> |
| 3 | +Date: Wed, 7 May 2025 11:27:42 +0200 |
| 4 | +Subject: [PATCH 52/N] ci: add an AutoHotKey-based integration test |
| 5 | + |
| 6 | +The issue reported in https://github.com/microsoft/git/issues/730 was |
| 7 | +fixed, but due to missing tests for the issue a regression slipped in |
| 8 | +within mere weeks. |
| 9 | + |
| 10 | +Let's add an integration test that will (hopefully) prevent this issue |
| 11 | +from regressing again. |
| 12 | + |
| 13 | +This integration test is implement as an AutoHotKey script. It might |
| 14 | +look unnatural to use a script language designed to implement global |
| 15 | +keyboard shortcuts, but it is a quite powerful approach. While |
| 16 | +there are miles between the ease of developing AutoHotKey scripts and |
| 17 | +developing, say, Playwright tests, there is a decent integration into VS |
| 18 | +Code (including single-step debugging), and AutoHotKey's own development |
| 19 | +and community are quite vibrant and friendly. |
| 20 | + |
| 21 | +I had looked at alternatives to AutoHotKey, such as WinAppDriver, |
| 22 | +SikuliX, nut.js and AutoIt, in particular searching for a solution that |
| 23 | +would have a powerful recording feature similar to Playwright, but did |
| 24 | +not find any that is 1) mature, 2) well-maintained, 3) open source and |
| 25 | +4) would be easy to integrate into a GitHub workflow. In the end, |
| 26 | +AutoHotKey appeared my clearest preference. |
| 27 | + |
| 28 | +So how is the test implemented? It lives in `ui-test/` and requires |
| 29 | +AutoHotKey v2 as well as Windows Terminal (the Legacy Prompt would not |
| 30 | +reproduce the problem). It then follows the reproducer I gave to the |
| 31 | +Cygwin team: |
| 32 | + |
| 33 | +1. initialize a Git repository |
| 34 | +2. install a `pre-commit` hook |
| 35 | +3. this hook shall spawn a non-Cygwin/MSYS2 process in the background |
| 36 | +4. that background process shall print to the console after Git exits |
| 37 | +5. open a Command Prompt in Windows Terminal |
| 38 | +6. run `git commit` |
| 39 | +7. wait until the background process is done printing |
| 40 | +8. press the Cursor Up key |
| 41 | +9. observe that the Command Prompt does not react (in the test, it |
| 42 | + _does_ expect a reaction: the previous command in the command |
| 43 | + history should be shown, i.e. `git commit`) |
| 44 | + |
| 45 | +In my reproducer, I then also suggested to press the Enter key and to |
| 46 | +observe that now the "More ?" prompt is shown, but no input is accepted, |
| 47 | +until Ctrl+Z is pressed. Naturally, the test should not expect _that_ |
| 48 | +;-) |
| 49 | + |
| 50 | +There were a couple of complications I needed to face when developing |
| 51 | +this test: |
| 52 | + |
| 53 | +- I did not find any easy macro recorder for AutoHotKey that I liked. It |
| 54 | + would not have helped much, anyway, because intentions are hard to |
| 55 | + record. |
| 56 | + |
| 57 | +- Before I realized that there is excellent AutoHotKey support in VS |
| 58 | + Code via the AutoHotKey++ and AutoHotKey Debug extensions, I struggled |
| 59 | + quite a bit to get the syntax right. |
| 60 | + |
| 61 | +- Windows Terminal does not use classical Win32 controls that AutoHotKey |
| 62 | + knows well. In particular, there is no easy way to capture the text |
| 63 | + that is shown in the Terminal. I tried the (pretty excellent!) [OCR |
| 64 | + for AutoHotKey](https://github.com/Descolada/OCR), but it uses UWP OCR |
| 65 | + which does not recognize constructs like "C:\Users\runneradmin>" |
| 66 | + because it is not English (or any other human language). I ended up |
| 67 | + with a pretty inelegant method of selecting the text via mouse |
| 68 | + movements and then copying that into the clipboard. This stops |
| 69 | + scrolling and I worked around that by emulating the mouse wheel |
| 70 | + afterwards. |
| 71 | + |
| 72 | +- Since Windows Terminal does not use classical Win32 controls, it is |
| 73 | + relatively hard to get to the exact bounding box of the text, as there |
| 74 | + is no convenient way to determine the size of the title bar or the |
| 75 | + amount of padding around the text. I ended up hard-coding those |
| 76 | + values, I'm not proud of that, but at least it works. |
| 77 | + |
| 78 | +- Despite my expectations, `ExitApp` would not actually exit AutoHotKey |
| 79 | + before the spawned process exits and/or the associated window is |
| 80 | + closed. |
| 81 | + |
| 82 | +Signed-off-by: Johannes Schindelin < [email protected]> |
| 83 | +--- |
| 84 | + ui-tests/background-hook.ahk | 129 +++++++++++++++++++++++++++++++++++ |
| 85 | + 1 file changed, 129 insertions(+) |
| 86 | + create mode 100755 ui-tests/background-hook.ahk |
| 87 | + |
| 88 | +diff --git a/ui-tests/background-hook.ahk b/ui-tests/background-hook.ahk |
| 89 | +new file mode 100755 |
| 90 | +index 0000000..43f723b |
| 91 | +--- /dev/null |
| 92 | ++++ b/ui-tests/background-hook.ahk |
| 93 | +@@ -0,0 +1,129 @@ |
| 94 | ++#Requires AutoHotkey v2.0 |
| 95 | ++ |
| 96 | ++; This script is an integration test for the following scenario: |
| 97 | ++; A Git hook spawns a background process that outputs some text |
| 98 | ++; to the console even after Git has exited. |
| 99 | ++ |
| 100 | ++; At some point in time, the Cygwin/MSYS2 runtime left the console |
| 101 | ++; in a state where it was not possible to navigate the history via |
| 102 | ++; CursorUp/Down, as reported in https://github.com/microsoft/git/issues/730. |
| 103 | ++; This was fixed in the Cygwin/MSYS2 runtime, but then regressed again. |
| 104 | ++; This test is meant to verify that the issue is fixed and remains so. |
| 105 | ++ |
| 106 | ++; First, set the worktree path; This path will be reused |
| 107 | ++; for the `.log` file). |
| 108 | ++if A_Args.Length > 0 |
| 109 | ++ workTree := A_Args[1] |
| 110 | ++else |
| 111 | ++{ |
| 112 | ++ ; Create a unique worktree path in the TEMP directory. |
| 113 | ++ workTree := EnvGet('TEMP') . '\git-test-background-hook' |
| 114 | ++ if FileExist(workTree) |
| 115 | ++ { |
| 116 | ++ counter := 0 |
| 117 | ++ while FileExist(workTree '-' counter) |
| 118 | ++ counter++ |
| 119 | ++ workTree := workTree '-' counter |
| 120 | ++ } |
| 121 | ++} |
| 122 | ++ |
| 123 | ++Info(text) { |
| 124 | ++ FileAppend text '`n', workTree '.log' |
| 125 | ++} |
| 126 | ++ |
| 127 | ++closeWindow := false |
| 128 | ++childPid := 0 |
| 129 | ++ExitWithError(error) { |
| 130 | ++ Info 'Error: ' error |
| 131 | ++ if closeWindow |
| 132 | ++ WinClose "A" |
| 133 | ++ else if childPid != 0 |
| 134 | ++ ProcessClose childPid |
| 135 | ++ ExitApp 1 |
| 136 | ++} |
| 137 | ++ |
| 138 | ++RunWaitOne(command) { |
| 139 | ++ shell := ComObject("WScript.Shell") |
| 140 | ++ ; Execute a single command via cmd.exe |
| 141 | ++ exec := shell.Exec(A_ComSpec " /C " command) |
| 142 | ++ ; Read and return the command's output |
| 143 | ++ return exec.StdOut.ReadAll() |
| 144 | ++} |
| 145 | ++ |
| 146 | ++SetWorkingDir(EnvGet('TEMP')) |
| 147 | ++Info 'uname: ' RunWaitOne('uname -a') |
| 148 | ++Info RunWaitOne('git version --build-options') |
| 149 | ++ |
| 150 | ++RunWait('git init "' workTree '"', '', 'Hide') |
| 151 | ++if A_LastError |
| 152 | ++ ExitWithError 'Could not initialize Git worktree at: ' workTree |
| 153 | ++ |
| 154 | ++SetWorkingDir(workTree) |
| 155 | ++if A_LastError |
| 156 | ++ ExitWithError 'Could not set working directory to: ' workTree |
| 157 | ++ |
| 158 | ++if not FileExist('.git/hooks') and not DirCreate('.git/hooks') |
| 159 | ++ ExitWithError 'Could not create hooks directory: ' workTree |
| 160 | ++ |
| 161 | ++FileAppend("#!/bin/sh`npowershell -command 'for ($i = 0; $i -lt 50; $i++) { echo $i; sleep -milliseconds 10 }' &`n", '.git/hooks/pre-commit') |
| 162 | ++if A_LastError |
| 163 | ++ ExitWithError 'Could not create pre-commit hook: ' A_LastError |
| 164 | ++ |
| 165 | ++Run 'wt.exe -d . ' A_ComSpec ' /d', , , &childPid |
| 166 | ++if A_LastError |
| 167 | ++ ExitWithError 'Error launching CMD: ' A_LastError |
| 168 | ++Info 'Launched CMD: ' childPid |
| 169 | ++if not WinWait(A_ComSpec, , 9) |
| 170 | ++ ExitWithError 'CMD window did not appear' |
| 171 | ++Info 'Got window' |
| 172 | ++WinActivate |
| 173 | ++CloseWindow := true |
| 174 | ++WinMove 0, 0 |
| 175 | ++Info 'Moved window to top left (so that the bottom is not cut off)' |
| 176 | ++ |
| 177 | ++CaptureText() { |
| 178 | ++ ControlGetPos &cx, &cy, &cw, &ch, 'Windows.UI.Composition.DesktopWindowContentBridge1', "A" |
| 179 | ++ titleBarHeight := 54 |
| 180 | ++ scrollBarWidth := 28 |
| 181 | ++ pad := 8 |
| 182 | ++ |
| 183 | ++ SavedClipboard := ClipboardAll |
| 184 | ++ A_Clipboard := '' |
| 185 | ++ SendMode('Event') |
| 186 | ++ MouseMove cx + pad, cy + titleBarHeight + pad |
| 187 | ++ MouseClickDrag 'Left', , , cx + cw - scrollBarWidth, cy + ch - pad, , '' |
| 188 | ++ MouseClick 'Right' |
| 189 | ++ ClipWait() |
| 190 | ++ Result := A_Clipboard |
| 191 | ++ Clipboard := SavedClipboard |
| 192 | ++ return Result |
| 193 | ++} |
| 194 | ++ |
| 195 | ++Info('Setting committer identity') |
| 196 | ++Send('git config user.name Test{Enter}git config user.email [email protected]{Enter}') |
| 197 | ++ |
| 198 | ++Info('Committing') |
| 199 | ++Send('git commit --allow-empty -m zOMG{Enter}') |
| 200 | ++; Wait for the hook to finish printing |
| 201 | ++While not RegExMatch(CaptureText(), '`n49$') |
| 202 | ++{ |
| 203 | ++ Sleep 100 |
| 204 | ++ if A_Index > 1000 |
| 205 | ++ ExitWithError 'Timed out waiting for commit to finish' |
| 206 | ++ MouseClick 'WheelDown', , , 20 |
| 207 | ++} |
| 208 | ++Info('Hook finished') |
| 209 | ++ |
| 210 | ++; Verify that CursorUp shows the previous command |
| 211 | ++Send('{Up}') |
| 212 | ++Sleep 150 |
| 213 | ++Text := CaptureText() |
| 214 | ++if not RegExMatch(Text, 'git commit --allow-empty -m zOMG *$') |
| 215 | ++ ExitWithError 'Cursor Up did not work: ' Text |
| 216 | ++Info('Match!') |
| 217 | ++ |
| 218 | ++Send('^C') |
| 219 | ++Send('exit{Enter}') |
| 220 | ++Sleep 50 |
| 221 | ++SetWorkingDir(EnvGet('TEMP')) |
| 222 | ++DirDelete(workTree, true) |
| 223 | +\ No newline at end of file |
0 commit comments