Skip to content

Commit 7acbb03

Browse files
committed
ci: add an AutoHotKey-based integration test
The issue reported in microsoft/git#730 was fixed, but due to missing tests for the issue a regression slipped in within mere weeks. Let's add an integration test that will (hopefully) prevent this issue from regressing again. This integration test is implement as an AutoHotKey script. It might look unnatural to use a script language designed to implement global keyboard shortcuts, but it is a quite powerful approach. While there are miles between the ease of developing AutoHotKey scripts and developing, say, Playwright tests, there is a decent integration into VS Code (including single-step debugging), and AutoHotKey's own development and community are quite vibrant and friendly. I had looked at alternatives to AutoHotKey, such as WinAppDriver, SikuliX, nut.js and AutoIt, in particular searching for a solution that would have a powerful recording feature similar to Playwright, but did not find any that is 1) mature, 2) well-maintained, 3) open source and 4) would be easy to integrate into a GitHub workflow. In the end, AutoHotKey appeared my clearest preference. So how is the test implemented? It lives in `ui-test/` and requires AutoHotKey v2 as well as Windows Terminal (the Legacy Prompt would not reproduce the problem). It then follows the reproducer I gave to the Cygwin team: 1. initialize a Git repository 2. install a `pre-commit` hook 3. this hook shall spawn a non-Cygwin/MSYS2 process in the background 4. that background process shall print to the console after Git exits 5. open a Command Prompt in Windows Terminal 6. run `git commit` 7. wait until the background process is done printing 8. press the Cursor Up key 9. observe that the Command Prompt does not react (in the test, it _does_ expect a reaction: the previous command in the command history should be shown, i.e. `git commit`) In my reproducer, I then also suggested to press the Enter key and to observe that now the "More ?" prompt is shown, but no input is accepted, until Ctrl+Z is pressed. Naturally, the test should not expect _that_ ;-) There were a couple of complications I needed to face when developing this test: - I did not find any easy macro recorder for AutoHotKey that I liked. It would not have helped much, anyway, because intentions are hard to record. - Before I realized that there is excellent AutoHotKey support in VS Code via the AutoHotKey++ and AutoHotKey Debug extensions, I struggled quite a bit to get the syntax right. - Windows Terminal does not use classical Win32 controls that AutoHotKey knows well. In particular, there is no easy way to capture the text that is shown in the Terminal. I tried the (pretty excellent!) [OCR for AutoHotKey](https://github.com/Descolada/OCR), but it uses UWP OCR which does not recognize constructs like "C:\Users\runneradmin>" because it is not English (or any other human language). I ended up with a pretty inelegant method of selecting the text via mouse movements and then copying that into the clipboard. This stops scrolling and I worked around that by emulating the mouse wheel afterwards. - Since Windows Terminal does not use classical Win32 controls, it is relatively hard to get to the exact bounding box of the text, as there is no convenient way to determine the size of the title bar or the amount of padding around the text. I ended up hard-coding those values, I'm not proud of that, but at least it works. - Despite my expectations, `ExitApp` would not actually exit AutoHotKey before the spawned process exits and/or the associated window is closed. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent f1e2918 commit 7acbb03

File tree

3 files changed

+224
-0
lines changed

3 files changed

+224
-0
lines changed

.github/workflows/build.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ jobs:
118118
with:
119119
git-artifacts-extract-location: ${{ needs.minimal-sdk-artifact.outputs.git-artifacts-extract-location }}
120120

121+
ui-tests:
122+
needs: build
123+
uses: ./.github/workflows/ui-tests.yml
124+
with:
125+
msys2-runtime-artifact-name: install
126+
permissions:
127+
contents: read
128+
121129
generate-msys2-tests-matrix:
122130
runs-on: ubuntu-latest
123131
outputs:

.github/workflows/ui-tests.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: ui-tests
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
msys2-runtime-artifact-name:
7+
required: true
8+
type: string
9+
10+
env:
11+
AUTOHOTKEY_VERSION: 2.0.19
12+
WT_VERSION: 1.22.11141.0
13+
14+
jobs:
15+
ui-tests:
16+
runs-on: windows-latest
17+
steps:
18+
- uses: actions/download-artifact@v4
19+
with:
20+
name: ${{ inputs.msys2-runtime-artifact-name }}
21+
path: ${{ runner.temp }}/artifacts
22+
- name: replace MSYS2 runtime
23+
run: |
24+
$p = Get-ChildItem -Recurse "${env:RUNNER_TEMP}\artifacts" | where {$_.Name -eq "msys-2.0.dll"} | Select -ExpandProperty VersionInfo | Select -First 1 -ExpandProperty FileName
25+
cp $p "c:/Program Files/Git/usr/bin/msys-2.0.dll"
26+
27+
- uses: actions/cache/restore@v4
28+
id: restore-wt
29+
with:
30+
key: wt-${{ env.WT_VERSION }}
31+
path: ${{ runner.temp }}/wt.zip
32+
- name: Download Windows Terminal
33+
if: steps.restore-wt.outputs.cache-hit != 'true'
34+
shell: bash
35+
run: |
36+
curl -fLo "$RUNNER_TEMP/wt.zip" \
37+
https://github.com/microsoft/terminal/releases/download/v$WT_VERSION/Microsoft.WindowsTerminal_${WT_VERSION}_x64.zip
38+
- uses: actions/cache/save@v4
39+
if: steps.restore-wt.outputs.cache-hit != 'true'
40+
with:
41+
key: wt-${{ env.WT_VERSION }}
42+
path: ${{ runner.temp }}/wt.zip
43+
- name: Install Windows Terminal
44+
shell: bash
45+
working-directory: ${{ runner.temp }}
46+
run: |
47+
"$WINDIR/system32/tar.exe" -xf "$RUNNER_TEMP/wt.zip" &&
48+
cygpath -aw terminal-$WT_VERSION >>$GITHUB_PATH
49+
- uses: actions/cache/restore@v4
50+
id: restore-ahk
51+
with:
52+
key: ahk-${{ env.AUTOHOTKEY_VERSION }}
53+
path: ${{ runner.temp }}/ahk.zip
54+
- name: Download AutoHotKey2
55+
if: steps.restore-ahk.outputs.cache-hit != 'true'
56+
shell: bash
57+
run: |
58+
curl -L -o "$RUNNER_TEMP/ahk.zip" \
59+
https://github.com/AutoHotkey/AutoHotkey/releases/download/v$AUTOHOTKEY_VERSION/AutoHotkey_$AUTOHOTKEY_VERSION.zip
60+
- uses: actions/cache/save@v4
61+
if: steps.restore-ahk.outputs.cache-hit != 'true'
62+
with:
63+
key: ahk-${{ env.AUTOHOTKEY_VERSION }}
64+
path: ${{ runner.temp }}/ahk.zip
65+
- name: Install AutoHotKey2
66+
shell: bash
67+
run: |
68+
mkdir -p "$RUNNER_TEMP/ahk" &&
69+
"$WINDIR/system32/tar.exe" -C "$RUNNER_TEMP/ahk" -xf "$RUNNER_TEMP/ahk.zip" &&
70+
cygpath -aw "$RUNNER_TEMP/ahk" >>$GITHUB_PATH
71+
- uses: actions/setup-node@v4 # the hook uses node for the background process
72+
73+
- uses: actions/checkout@v4
74+
with:
75+
sparse-checkout: |
76+
ui-tests
77+
- name: Run UI tests
78+
id: ui-tests
79+
run: |
80+
$p = Start-Process -PassThru -FilePath "${env:RUNNER_TEMP}\ahk\AutoHotKey64.exe" -ArgumentList ui-tests\background-hook.ahk, "$PWD\bg-hook"
81+
$p.WaitForExit()
82+
if ($p.ExitCode -ne 0) { echo "::error::Test failed!" } else { echo "::notice::Test log" }
83+
type bg-hook.log
84+
if ($p.ExitCode -ne 0) { exit 1 }
85+
- name: Show logs, if canceled
86+
if: cancelled()
87+
run: type bg-hook.log

ui-tests/background-hook.ahk

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#Requires AutoHotkey v2.0
2+
3+
; This script is an integration test for the following scenario:
4+
; A Git hook spawns a background process that outputs some text
5+
; to the console even after Git has exited.
6+
7+
; At some point in time, the Cygwin/MSYS2 runtime left the console
8+
; in a state where it was not possible to navigate the history via
9+
; CursorUp/Down, as reported in https://github.com/microsoft/git/issues/730.
10+
; This was fixed in the Cygwin/MSYS2 runtime, but then regressed again.
11+
; This test is meant to verify that the issue is fixed and remains so.
12+
13+
; First, set the worktree path; This path will be reused
14+
; for the `.log` file).
15+
if A_Args.Length > 0
16+
workTree := A_Args[1]
17+
else
18+
{
19+
; Create a unique worktree path in the TEMP directory.
20+
workTree := EnvGet('TEMP') . '\git-test-background-hook'
21+
if FileExist(workTree)
22+
{
23+
counter := 0
24+
while FileExist(workTree '-' counter)
25+
counter++
26+
workTree := workTree '-' counter
27+
}
28+
}
29+
30+
Info(text) {
31+
FileAppend text '`n', workTree '.log'
32+
}
33+
34+
closeWindow := false
35+
childPid := 0
36+
ExitWithError(error) {
37+
Info 'Error: ' error
38+
if closeWindow
39+
WinClose "A"
40+
else if childPid != 0
41+
ProcessClose childPid
42+
ExitApp 1
43+
}
44+
45+
RunWaitOne(command) {
46+
shell := ComObject("WScript.Shell")
47+
; Execute a single command via cmd.exe
48+
exec := shell.Exec(A_ComSpec " /C " command)
49+
; Read and return the command's output
50+
return exec.StdOut.ReadAll()
51+
}
52+
53+
SetWorkingDir(EnvGet('TEMP'))
54+
Info 'uname: ' RunWaitOne('uname -a')
55+
Info RunWaitOne('git version --build-options')
56+
57+
RunWait('git init "' workTree '"', '', 'Hide')
58+
if A_LastError
59+
ExitWithError 'Could not initialize Git worktree at: ' workTree
60+
61+
SetWorkingDir(workTree)
62+
if A_LastError
63+
ExitWithError 'Could not set working directory to: ' workTree
64+
65+
if not FileExist('.git/hooks') and not DirCreate('.git/hooks')
66+
ExitWithError 'Could not create hooks directory: ' workTree
67+
68+
FileAppend("#!/bin/sh`npowershell -command 'for ($i = 0; $i -lt 50; $i++) { echo $i; sleep -milliseconds 10 }' &`n", '.git/hooks/pre-commit')
69+
if A_LastError
70+
ExitWithError 'Could not create pre-commit hook: ' A_LastError
71+
72+
Run 'wt.exe -d . ' A_ComSpec ' /d', , , &childPid
73+
if A_LastError
74+
ExitWithError 'Error launching CMD: ' A_LastError
75+
Info 'Launched CMD: ' childPid
76+
if not WinWait(A_ComSpec, , 9)
77+
ExitWithError 'CMD window did not appear'
78+
Info 'Got window'
79+
WinActivate
80+
CloseWindow := true
81+
WinMove 0, 0
82+
Info 'Moved window to top left (so that the bottom is not cut off)'
83+
84+
CaptureText() {
85+
ControlGetPos &cx, &cy, &cw, &ch, 'Windows.UI.Composition.DesktopWindowContentBridge1', "A"
86+
titleBarHeight := 54
87+
scrollBarWidth := 28
88+
pad := 8
89+
90+
SavedClipboard := ClipboardAll
91+
A_Clipboard := ''
92+
SendMode('Event')
93+
MouseMove cx + pad, cy + titleBarHeight + pad
94+
MouseClickDrag 'Left', , , cx + cw - scrollBarWidth, cy + ch - pad, , ''
95+
MouseClick 'Right'
96+
ClipWait()
97+
Result := A_Clipboard
98+
Clipboard := SavedClipboard
99+
return Result
100+
}
101+
102+
Info('Setting committer identity')
103+
Send('git config user.name Test{Enter}git config user.email [email protected]{Enter}')
104+
105+
Info('Committing')
106+
Send('git commit --allow-empty -m zOMG{Enter}')
107+
; Wait for the hook to finish printing
108+
While not RegExMatch(CaptureText(), '`n49$')
109+
{
110+
Sleep 100
111+
if A_Index > 1000
112+
ExitWithError 'Timed out waiting for commit to finish'
113+
MouseClick 'WheelDown', , , 20
114+
}
115+
Info('Hook finished')
116+
117+
; Verify that CursorUp shows the previous command
118+
Send('{Up}')
119+
Sleep 150
120+
Text := CaptureText()
121+
if not RegExMatch(Text, 'git commit --allow-empty -m zOMG *$')
122+
ExitWithError 'Cursor Up did not work: ' Text
123+
Info('Match!')
124+
125+
Send('^C')
126+
Send('exit{Enter}')
127+
Sleep 50
128+
SetWorkingDir(EnvGet('TEMP'))
129+
DirDelete(workTree, true)

0 commit comments

Comments
 (0)