aboutsummaryrefslogtreecommitdiff
path: root/scripts/windows_input
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/windows_input')
-rw-r--r--scripts/windows_input/Native.cs18
-rw-r--r--scripts/windows_input/README.md19
-rw-r--r--scripts/windows_input/report.py17
3 files changed, 39 insertions, 15 deletions
diff --git a/scripts/windows_input/Native.cs b/scripts/windows_input/Native.cs
index 76cb8af7..9d635753 100644
--- a/scripts/windows_input/Native.cs
+++ b/scripts/windows_input/Native.cs
@@ -108,7 +108,15 @@ namespace HerdrInputGauntlet {
[DllImport("user32.dll", SetLastError=true)] static extern uint SendInput(uint count, Input[] events, int size);
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hwnd, out Rect rect);
[DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hwnd, IntPtr after, int x, int y, int width, int height, uint flags);
- [DllImport("user32.dll")] public static extern int CountClipboardFormats();
+ [DllImport("user32.dll",EntryPoint="CountClipboardFormats",SetLastError=true)] static extern int NativeCountClipboardFormats();
+ public static int CountClipboardFormats() {
+ int count=NativeCountClipboardFormats();
+ if(count==0) {
+ int error=Marshal.GetLastWin32Error();
+ if(error!=0) throw new Win32Exception(error,"Clipboard format count failed");
+ }
+ return count;
+ }
[DllImport("user32.dll")] public static extern uint GetClipboardSequenceNumber();
[DllImport("user32.dll")] static extern IntPtr GetClipboardOwner();
[DllImport("user32.dll")] static extern bool OpenClipboard(IntPtr owner);
@@ -141,6 +149,14 @@ namespace HerdrInputGauntlet {
if(result<0) Marshal.ThrowExceptionForHR(result);
return checked((int)pid);
}
+ public static int ClearClipboardForRun() {
+ if(!OpenClipboard(IntPtr.Zero)) throw new Exception("Clipboard busy; cannot start paste qualification");
+ try {
+ int formats=CountClipboardFormats();
+ if(formats!=0 && !EmptyClipboard()) throw new Exception("Clipboard clear failed");
+ return formats;
+ } finally { CloseClipboard(); }
+ }
public static uint SetEmptyClipboard(IntPtr owner,string text) {
if(owner==IntPtr.Zero) throw new Exception("Clipboard owner is required");
diff --git a/scripts/windows_input/README.md b/scripts/windows_input/README.md
index edebc8fc..61b76bd2 100644
--- a/scripts/windows_input/README.md
+++ b/scripts/windows_input/README.md
@@ -33,11 +33,14 @@ or mouse buttons while starting a run. Normal controller cancellation runs clean
bootstrap/probe leases also expire if the controller disappears. Lease expiry is
not a replacement for a secure isolated desktop.
-Clipboard tests require an **empty clipboard**. Clear it yourself only after
-saving anything you need. The runner will not replace existing text, images,
-rich formats, or files. It writes its synthetic text while holding the clipboard
-lock and clears it afterward only if its sequence number is unchanged. A later
-user clipboard update is left alone. No original clipboard contents are logged.
+`just test-windows-input` clears the clipboard once before the build when the
+selected cases use it. **Save anything you need first:** existing text, images,
+files, and other formats are discarded and cannot be restored. Direct script
+calls need `-ClearClipboard` for the same behavior; without it, a nonempty
+clipboard stops the run before the build. The runner writes its synthetic data
+while holding the clipboard lock and clears it afterward only if its sequence
+number is unchanged. A later user clipboard update is left alone. No original
+clipboard contents are logged.
## Run
@@ -70,7 +73,7 @@ Or invoke the script directly:
```powershell
pwsh -NoProfile -File scripts/test_windows_input.ps1 `
- -ExePath 'C:\test-app\herdr.exe' -AllowInputInjection `
+ -ExePath 'C:\test-app\herdr.exe' -AllowInputInjection -ClearClipboard `
-StablePath 'C:\TerminalStable\WindowsTerminal.exe' `
-PreviewPath 'C:\TerminalPreview\WindowsTerminal.exe'
```
@@ -83,7 +86,7 @@ arrays normally:
```powershell
.\scripts\test_windows_input.ps1 -ExePath 'C:\test-app\herdr.exe' `
- -AllowInputInjection -Modes legacy,kitty -Cases mouse-interleave,mode-transitions `
+ -AllowInputInjection -ClearClipboard -Modes legacy,kitty -Cases mouse-interleave,mode-transitions `
-Widths 120 -Heights 30
```
@@ -92,7 +95,7 @@ server with the remote clipboard bridge active. It needs no SSH host. A focused
clipboard-image qualification is:
```powershell
-.\scripts\test_windows_input.ps1 -AllowInputInjection `
+.\scripts\test_windows_input.ps1 -AllowInputInjection -ClearClipboard `
-Channels stable -Paths direct,herdr-remote -Modes legacy `
-Cases clipboard-image,clipboard-mixed -Widths 80 -Heights 24
```
diff --git a/scripts/windows_input/report.py b/scripts/windows_input/report.py
index cf37f30e..74ce92cd 100644
--- a/scripts/windows_input/report.py
+++ b/scripts/windows_input/report.py
@@ -419,18 +419,23 @@ def qualification_matrix(result):
return "MANUAL"
if {row.get("case") for row in matched} != case_ids:
return "PARTIAL" if "pass" in statuses else "INCONCLUSIVE" if "inconclusive" in statuses else "NOT TESTED"
- if required_channels and {(row.get("host"), row.get("case")) for row in matched} != {
- (channel, case_id) for channel in required_channels for case_id in case_ids}:
+ expected_pairs = {(channel, case_id) for channel in required_channels for case_id in case_ids}
+ if required_channels and {(row.get("host"), row.get("case")) for row in matched} != expected_pairs:
return "PARTIAL"
if case_ids == {"mode-transitions"} and path == "direct" and statuses <= {"unsupported", "inconclusive"}:
return "X - mOK ignored"
- if path == "direct" and modes == {"legacy"} and statuses == {"unsupported"}:
+ if path == "direct" and modes == {"legacy"} and statuses - {"not_run"} == {"unsupported"}:
+ if {(row.get("host"), row.get("case")) for row in matched if row.get("status") == "unsupported"} != expected_pairs:
+ return "PARTIAL"
return "X - becomes Enter" if case_ids == {"shift-enter"} else "X - loses modifier"
- def case_passed(case_id):
- statuses_for_case = {row.get("status") for row in matched if row.get("case") == case_id}
+ def case_passed(case_id, channel=None):
+ statuses_for_case = {row.get("status") for row in matched if row.get("case") == case_id
+ and (channel is None or row.get("host") == channel)}
return "pass" in statuses_for_case or (width == 80 and path == "direct" and modes == {"legacy"}
- and case_id == "shift-enter" and statuses_for_case == {"unsupported"})
+ and case_id == "shift-enter" and statuses_for_case - {"not_run"} == {"unsupported"})
per_case_passed = all(case_passed(case_id) for case_id in case_ids)
+ if "not_run" in statuses and all(case_passed(case_id, channel) for channel in required_channels for case_id in case_ids):
+ return "PARTIAL"
allowed = {"pass", "inconclusive"}
if width == 80 and path == "direct" and modes == {"legacy"}:
allowed.add("unsupported")