diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7e3abb5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,105 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test-windows: + name: Test on Windows + runs-on: windows-latest + + strategy: + matrix: + go-version: [ 'stable', 'oldstable' ] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~\AppData\Local\go-build + ~\go\pkg\mod + key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ matrix.go-version }}- + ${{ runner.os }}-go- + + - name: Download dependencies + run: go mod download + + - name: Verify dependencies + run: go mod verify + + - name: Build + run: go build -v ./... + + - name: Run tests + run: go test -v -race -coverprofile="coverage.out" . + + - name: Run tests with short flag + run: go test -v -short ./... + + - name: Check for vulnerabilities + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.out + flags: windows + name: codecov-windows-go${{ matrix.go-version }} + fail_ci_if_error: false + + build-cross-platform: + name: Cross-platform build verification + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 'stable' + + - name: Build for Windows (amd64) + run: GOOS=windows GOARCH=amd64 go build -v ./... + + - name: Build for Windows (arm64) + run: GOOS=windows GOARCH=arm64 go build -v ./... + + - name: Test compilation for Windows + run: GOOS=windows go test -c ./... + + lint: + name: Lint + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 'stable' + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: latest + args: --timeout=5m \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index d3ed710..4e74d9b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,24 +1,76 @@ +version: "2" linters: - enable-all: true + default: all disable: - - golint - - interfacer - - scopelint - - maligned - - rowserrcheck - - funlen - depguard - - goerr113 - - exhaustivestruct - - testpackage + - dupl + - err113 + - exhaustruct - gochecknoglobals + - godot + - ireturn + - mnd + - nlreturn + - paralleltest + - perfsprint + - testpackage + - varnamelen - wrapcheck - - forbidigo - - ifshort - - cyclop - - gomoddirectives -linters-settings: - exhaustive: - default-signifies-exhaustive: true -issues: - exclude-use-default: false + - wsl + settings: + cyclop: + max-complexity: 20 + gosec: + excludes: + - G103 + - G115 + revive: + rules: + - name: var-naming + disabled: true + - name: exported + disabled: true + exclusions: + generated: lax + rules: + - path: (.+)\.go$ + text: var-naming.*VK_ + - path: (.+)\.go$ + text: var-naming.*FROM_LEFT + - path: (.+)\.go$ + text: var-naming.*RIGHT.*_BUTTON + - path: (.+)\.go$ + text: var-naming.*CAPS.*_ON + - path: (.+)\.go$ + text: var-naming.*ENABLE_ + - path: (.+)\.go$ + text: var-naming.*MOUSE_ + - path: (.+)\.go$ + text: var-naming.*DOUBLE_CLICK + - path: (.+)\.go$ + text: should have a package comment + - path: (.+)\.go$ + text: exported.*should have comment.*VK_ + - path: (.+)\.go$ + text: exported.*should have comment.*Contains + - path: (.+)\.go$ + text: exported.*should have comment.*IsReleased + - path: (.+)\.go$ + text: exported.*should have comment.*WheelDirectionName + - path: (.+)\.go$ + text: exported.*should have comment.*FlushConsoleInputBuffer + - path: (.+)\.go$ + text: comment on exported function.*should be of the form + - path: (.+)\.go$ + text: G103.*Use of unsafe calls + - path: (.+)\.go$ + text: G115.*integer overflow conversion + - path: (.+)_test\.go$ + linters: + - funlen + - wsl_v5 + - path: read.go + linters: + - wsl_v5 + paths: + - example/main.go diff --git a/example/main.go b/example/main.go index d86b311..636fc98 100644 --- a/example/main.go +++ b/example/main.go @@ -52,10 +52,7 @@ func run() (err error) { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() - for { - if ctx.Err() != nil { - break - } + for ctx.Err() == nil { events, err := coninput.ReadNConsoleInputs(con, 16) if err != nil { diff --git a/go.mod b/go.mod index bb24a50..30a3549 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module github.com/erikgeiser/coninput -go 1.16 +go 1.23.0 -require golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e +toolchain go1.23.11 + +require golang.org/x/sys v0.30.0 diff --git a/go.sum b/go.sum index ee6deba..241f4ca 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/keycodes.go b/keycodes.go index 902ee1b..3f573fb 100644 --- a/keycodes.go +++ b/keycodes.go @@ -1,3 +1,4 @@ +// Package coninput provides Go bindings for Windows Console Input API. package coninput // VirtualKeyCode holds a virtual key code (see diff --git a/mode_test.go b/mode_test.go new file mode 100644 index 0000000..691209e --- /dev/null +++ b/mode_test.go @@ -0,0 +1,256 @@ +//go:build windows +// +build windows + +package coninput + +import ( + "reflect" + "testing" + + "golang.org/x/sys/windows" +) + +func TestAddInputModes(t *testing.T) { + tests := []struct { + name string + mode uint32 + enableModes []uint32 + expected uint32 + }{ + { + name: "add single mode", + mode: 0, + enableModes: []uint32{windows.ENABLE_ECHO_INPUT}, + expected: windows.ENABLE_ECHO_INPUT, + }, + { + name: "add multiple modes", + mode: 0, + enableModes: []uint32{windows.ENABLE_ECHO_INPUT, windows.ENABLE_LINE_INPUT}, + expected: windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT, + }, + { + name: "add to existing mode", + mode: windows.ENABLE_ECHO_INPUT, + enableModes: []uint32{windows.ENABLE_LINE_INPUT}, + expected: windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT, + }, + { + name: "add duplicate mode", + mode: windows.ENABLE_ECHO_INPUT, + enableModes: []uint32{windows.ENABLE_ECHO_INPUT}, + expected: windows.ENABLE_ECHO_INPUT, + }, + { + name: "add no modes", + mode: windows.ENABLE_ECHO_INPUT, + enableModes: []uint32{}, + expected: windows.ENABLE_ECHO_INPUT, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AddInputModes(tt.mode, tt.enableModes...) + if result != tt.expected { + t.Errorf("AddInputModes() = %d, want %d", result, tt.expected) + } + }) + } +} + +func TestRemoveInputModes(t *testing.T) { + tests := []struct { + name string + mode uint32 + disableModes []uint32 + expected uint32 + }{ + { + name: "remove single mode", + mode: windows.ENABLE_ECHO_INPUT, + disableModes: []uint32{windows.ENABLE_ECHO_INPUT}, + expected: 0, + }, + { + name: "remove multiple modes", + mode: windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT, + disableModes: []uint32{windows.ENABLE_ECHO_INPUT, windows.ENABLE_LINE_INPUT}, + expected: 0, + }, + { + name: "remove from multiple modes", + mode: windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_MOUSE_INPUT, + disableModes: []uint32{windows.ENABLE_LINE_INPUT}, + expected: windows.ENABLE_ECHO_INPUT | windows.ENABLE_MOUSE_INPUT, + }, + { + name: "remove non-existent mode", + mode: windows.ENABLE_ECHO_INPUT, + disableModes: []uint32{windows.ENABLE_LINE_INPUT}, + expected: windows.ENABLE_ECHO_INPUT, + }, + { + name: "remove no modes", + mode: windows.ENABLE_ECHO_INPUT, + disableModes: []uint32{}, + expected: windows.ENABLE_ECHO_INPUT, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := RemoveInputModes(tt.mode, tt.disableModes...) + if result != tt.expected { + t.Errorf("RemoveInputModes() = %d, want %d", result, tt.expected) + } + }) + } +} + +func TestToggleInputModes(t *testing.T) { + tests := []struct { + name string + mode uint32 + toggleModes []uint32 + expected uint32 + }{ + { + name: "toggle single mode on", + mode: 0, + toggleModes: []uint32{windows.ENABLE_ECHO_INPUT}, + expected: windows.ENABLE_ECHO_INPUT, + }, + { + name: "toggle single mode off", + mode: windows.ENABLE_ECHO_INPUT, + toggleModes: []uint32{windows.ENABLE_ECHO_INPUT}, + expected: 0, + }, + { + name: "toggle multiple modes", + mode: windows.ENABLE_ECHO_INPUT, + toggleModes: []uint32{windows.ENABLE_ECHO_INPUT, windows.ENABLE_LINE_INPUT}, + expected: windows.ENABLE_LINE_INPUT, + }, + { + name: "toggle no modes", + mode: windows.ENABLE_ECHO_INPUT, + toggleModes: []uint32{}, + expected: windows.ENABLE_ECHO_INPUT, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ToggleInputModes(tt.mode, tt.toggleModes...) + if result != tt.expected { + t.Errorf("ToggleInputModes() = %d, want %d", result, tt.expected) + } + }) + } +} + +func TestListInputModes(t *testing.T) { + tests := []struct { + name string + mode uint32 + expected []uint32 + }{ + { + name: "no modes", + mode: 0, + expected: []uint32{}, + }, + { + name: "single mode", + mode: windows.ENABLE_ECHO_INPUT, + expected: []uint32{windows.ENABLE_ECHO_INPUT}, + }, + { + name: "multiple modes", + mode: windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT, + expected: []uint32{windows.ENABLE_ECHO_INPUT, windows.ENABLE_LINE_INPUT}, + }, + { + name: "all common modes", + mode: windows.ENABLE_ECHO_INPUT | windows.ENABLE_INSERT_MODE | windows.ENABLE_LINE_INPUT, + expected: []uint32{windows.ENABLE_ECHO_INPUT, windows.ENABLE_INSERT_MODE, windows.ENABLE_LINE_INPUT}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ListInputModes(tt.mode) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("ListInputModes() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestListInputModeNames(t *testing.T) { + tests := []struct { + name string + mode uint32 + expected []string + }{ + { + name: "no modes", + mode: 0, + expected: []string{}, + }, + { + name: "single mode", + mode: windows.ENABLE_ECHO_INPUT, + expected: []string{"ENABLE_ECHO_INPUT"}, + }, + { + name: "multiple modes", + mode: windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT, + expected: []string{"ENABLE_ECHO_INPUT", "ENABLE_LINE_INPUT"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ListInputModeNames(tt.mode) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("ListInputModeNames() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestDescribeInputMode(t *testing.T) { + tests := []struct { + name string + mode uint32 + expected string + }{ + { + name: "no modes", + mode: 0, + expected: "", + }, + { + name: "single mode", + mode: windows.ENABLE_ECHO_INPUT, + expected: "ENABLE_ECHO_INPUT", + }, + { + name: "multiple modes", + mode: windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT, + expected: "ENABLE_ECHO_INPUT|ENABLE_LINE_INPUT", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := DescribeInputMode(tt.mode) + if result != tt.expected { + t.Errorf("DescribeInputMode() = %q, want %q", result, tt.expected) + } + }) + } +} \ No newline at end of file diff --git a/read.go b/read.go index b2dd82f..375f1e8 100644 --- a/read.go +++ b/read.go @@ -31,9 +31,9 @@ func NewStdinHandle() (windows.Handle, error) { // ReadNConsoleInputs. func WinReadConsoleInput(consoleInput windows.Handle, buffer *InputRecord, length uint32, numberOfEventsRead *uint32) error { - r, _, e := syscall.Syscall6(procReadConsoleInputW.Addr(), 4, + r, _, e := syscall.SyscallN(procReadConsoleInputW.Addr(), uintptr(consoleInput), uintptr(unsafe.Pointer(buffer)), uintptr(length), - uintptr(unsafe.Pointer(numberOfEventsRead)), 0, 0) + uintptr(unsafe.Pointer(numberOfEventsRead))) if r == 0 { return error(e) } @@ -78,15 +78,14 @@ func ReadConsoleInput(console windows.Handle, inputRecords []InputRecord) (uint3 // PeekNConsoleInputs. func WinPeekConsoleInput(consoleInput windows.Handle, buffer *InputRecord, length uint32, numberOfEventsRead *uint32) error { - r, _, e := syscall.Syscall6(procPeekConsoleInputW.Addr(), 4, + r, _, e := syscall.SyscallN(procPeekConsoleInputW.Addr(), uintptr(consoleInput), uintptr(unsafe.Pointer(buffer)), uintptr(length), - uintptr(unsafe.Pointer(numberOfEventsRead)), 0, 0) + uintptr(unsafe.Pointer(numberOfEventsRead))) if r == 0 { return error(e) } return nil - } // PeekNConsoleInputs is a wrapper around PeekConsoleInput (see @@ -124,9 +123,8 @@ func PeekConsoleInput(console windows.Handle, inputRecords []InputRecord) (uint3 // Windows console API function GetNumberOfConsoleInputEvents (see // https://docs.microsoft.com/en-us/windows/console/getnumberofconsoleinputevents). func WinGetNumberOfConsoleInputEvents(consoleInput windows.Handle, numberOfEvents *uint32) error { - r, _, e := syscall.Syscall6(procGetNumberOfConsoleInputEvents.Addr(), 2, - uintptr(consoleInput), uintptr(unsafe.Pointer(numberOfEvents)), 0, - 0, 0, 0) + r, _, e := syscall.SyscallN(procGetNumberOfConsoleInputEvents.Addr(), + uintptr(consoleInput), uintptr(unsafe.Pointer(numberOfEvents))) if r == 0 { return error(e) } @@ -145,7 +143,7 @@ func GetNumberOfConsoleInputEvents(console windows.Handle) (uint32, error) { } func FlushConsoleInputBuffer(consoleInput windows.Handle) error { - r, _, e := syscall.Syscall(procFlushConsoleInputBuffer.Addr(), 1, uintptr(consoleInput), 0, 0) + r, _, e := syscall.SyscallN(procFlushConsoleInputBuffer.Addr(), uintptr(consoleInput)) if r == 0 { return error(e) } diff --git a/read_test.go b/read_test.go new file mode 100644 index 0000000..48ccfcf --- /dev/null +++ b/read_test.go @@ -0,0 +1,280 @@ +//go:build windows +// +build windows + +package coninput + +import ( + "testing" + + "golang.org/x/sys/windows" +) + +func TestReadNConsoleInputs(t *testing.T) { + tests := []struct { + name string + maxEvents uint32 + wantError bool + errorMsg string + }{ + { + name: "zero max events", + maxEvents: 0, + wantError: true, + errorMsg: "maxEvents cannot be zero", + }, + { + name: "valid max events", + maxEvents: 1, + wantError: false, + }, + { + name: "multiple max events", + maxEvents: 10, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock handle - this will fail on actual Windows API call + // but we're testing the validation logic + handle := windows.Handle(0) + + _, err := ReadNConsoleInputs(handle, tt.maxEvents) + + if tt.wantError { + if err == nil { + t.Error("Expected error, got nil") + } else if err.Error() != tt.errorMsg { + t.Errorf("Expected error %q, got %q", tt.errorMsg, err.Error()) + } + } else { + // For non-zero maxEvents, we expect a Windows API error since we're not on Windows + // or using a valid handle, but not our validation error + if err != nil && err.Error() == tt.errorMsg { + t.Errorf("Got validation error when expecting Windows API error: %v", err) + } + } + }) + } +} + +func TestReadConsoleInput(t *testing.T) { + tests := []struct { + name string + inputRecords []InputRecord + wantError bool + errorMsg string + }{ + { + name: "empty input records", + inputRecords: []InputRecord{}, + wantError: true, + errorMsg: "size of input record buffer cannot be zero", + }, + { + name: "single input record", + inputRecords: make([]InputRecord, 1), + wantError: false, + }, + { + name: "multiple input records", + inputRecords: make([]InputRecord, 5), + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock handle + handle := windows.Handle(0) + + _, err := ReadConsoleInput(handle, tt.inputRecords) + + if tt.wantError { + if err == nil { + t.Error("Expected error, got nil") + } else if err.Error() != tt.errorMsg { + t.Errorf("Expected error %q, got %q", tt.errorMsg, err.Error()) + } + } else { + // For non-empty slices, we expect a Windows API error since we're not on Windows + // or using a valid handle, but not our validation error + if err != nil && err.Error() == tt.errorMsg { + t.Errorf("Got validation error when expecting Windows API error: %v", err) + } + } + }) + } +} + +func TestPeekNConsoleInputs(t *testing.T) { + tests := []struct { + name string + maxEvents uint32 + wantError bool + errorMsg string + }{ + { + name: "zero max events", + maxEvents: 0, + wantError: true, + errorMsg: "maxEvents cannot be zero", + }, + { + name: "valid max events", + maxEvents: 1, + wantError: false, + }, + { + name: "multiple max events", + maxEvents: 10, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock handle + handle := windows.Handle(0) + + _, err := PeekNConsoleInputs(handle, tt.maxEvents) + + if tt.wantError { + if err == nil { + t.Error("Expected error, got nil") + } else if err.Error() != tt.errorMsg { + t.Errorf("Expected error %q, got %q", tt.errorMsg, err.Error()) + } + } else { + // For non-zero maxEvents, we expect a Windows API error + if err != nil && err.Error() == tt.errorMsg { + t.Errorf("Got validation error when expecting Windows API error: %v", err) + } + } + }) + } +} + +func TestPeekConsoleInput(t *testing.T) { + tests := []struct { + name string + inputRecords []InputRecord + wantError bool + errorMsg string + }{ + { + name: "empty input records", + inputRecords: []InputRecord{}, + wantError: true, + errorMsg: "size of input record buffer cannot be zero", + }, + { + name: "single input record", + inputRecords: make([]InputRecord, 1), + wantError: false, + }, + { + name: "multiple input records", + inputRecords: make([]InputRecord, 5), + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock handle + handle := windows.Handle(0) + + _, err := PeekConsoleInput(handle, tt.inputRecords) + + if tt.wantError { + if err == nil { + t.Error("Expected error, got nil") + } else if err.Error() != tt.errorMsg { + t.Errorf("Expected error %q, got %q", tt.errorMsg, err.Error()) + } + } else { + // For non-empty slices, we expect a Windows API error + if err != nil && err.Error() == tt.errorMsg { + t.Errorf("Got validation error when expecting Windows API error: %v", err) + } + } + }) + } +} + +// Test helper functions that don't require Windows API calls + +func TestNewStdinHandle(t *testing.T) { + // This will fail on non-Windows systems, but we can test that it returns something + _, err := NewStdinHandle() + // We expect this to fail on non-Windows systems + if err == nil { + t.Log("NewStdinHandle() succeeded (running on Windows)") + } else { + t.Logf("NewStdinHandle() failed as expected on non-Windows: %v", err) + } +} + +// Test validation logic without actual Windows API calls +func TestValidationLogic(t *testing.T) { + t.Run("ReadNConsoleInputs validation", func(t *testing.T) { + // Test the validation logic by checking if zero maxEvents produces the right error + handle := windows.Handle(0) + _, err := ReadNConsoleInputs(handle, 0) + if err == nil || err.Error() != "maxEvents cannot be zero" { + t.Errorf("Expected 'maxEvents cannot be zero' error, got: %v", err) + } + }) + + t.Run("ReadConsoleInput validation", func(t *testing.T) { + // Test the validation logic by checking if empty slice produces the right error + handle := windows.Handle(0) + emptySlice := []InputRecord{} + _, err := ReadConsoleInput(handle, emptySlice) + if err == nil || err.Error() != "size of input record buffer cannot be zero" { + t.Errorf("Expected 'size of input record buffer cannot be zero' error, got: %v", err) + } + }) + + t.Run("PeekNConsoleInputs validation", func(t *testing.T) { + handle := windows.Handle(0) + _, err := PeekNConsoleInputs(handle, 0) + if err == nil || err.Error() != "maxEvents cannot be zero" { + t.Errorf("Expected 'maxEvents cannot be zero' error, got: %v", err) + } + }) + + t.Run("PeekConsoleInput validation", func(t *testing.T) { + handle := windows.Handle(0) + emptySlice := []InputRecord{} + _, err := PeekConsoleInput(handle, emptySlice) + if err == nil || err.Error() != "size of input record buffer cannot be zero" { + t.Errorf("Expected 'size of input record buffer cannot be zero' error, got: %v", err) + } + }) +} + +// Test error handling for Windows API functions that we can't easily mock +func TestWindowsAPIErrorHandling(t *testing.T) { + // These tests verify that the functions properly handle invalid handles + // and return appropriate errors rather than panicking + invalidHandle := windows.Handle(0xFFFFFFFF) // Invalid handle + + t.Run("GetNumberOfConsoleInputEvents with invalid handle", func(t *testing.T) { + _, err := GetNumberOfConsoleInputEvents(invalidHandle) + // We expect an error, not a panic + if err == nil { + t.Error("Expected error with invalid handle") + } + }) + + t.Run("FlushConsoleInputBuffer with invalid handle", func(t *testing.T) { + err := FlushConsoleInputBuffer(invalidHandle) + // We expect an error, not a panic + if err == nil { + t.Error("Expected error with invalid handle") + } + }) +} \ No newline at end of file diff --git a/records.go b/records.go index cccf7fb..3bfc18c 100644 --- a/records.go +++ b/records.go @@ -152,7 +152,7 @@ type KeyEventRecord struct { // zero for some keys. Char rune - //ControlKeyState holds the state of the control keys. + // ControlKeyState holds the state of the control keys. ControlKeyState ControlKeyState } @@ -334,7 +334,7 @@ func (e UnknownEvent) Type() string { return "UnknownEvent" } // String ensures that UnknownEvent satisfies EventRecord and fmt.Stringer // interfaces. func (e UnknownEvent) String() string { - return fmt.Sprintf("%s[Type: %d, Data: %v]", e.Type(), e.InputRecord.EventType, e.InputRecord.Event[:]) + return fmt.Sprintf("%s[Type: %d, Data: %v]", e.Type(), e.EventType, e.Event[:]) } // Coord represent the COORD structure from the Windows diff --git a/records_test.go b/records_test.go new file mode 100644 index 0000000..2f0fa86 --- /dev/null +++ b/records_test.go @@ -0,0 +1,406 @@ +package coninput + +import ( + "encoding/binary" + "strings" + "testing" +) + +func TestInputRecordUnwrap(t *testing.T) { + tests := []struct { + name string + eventType EventType + eventData EventUnion + expected EventRecord + }{ + { + name: "focus event - set focus true", + eventType: FocusEventType, + eventData: func() EventUnion { + var data EventUnion + data[0] = 1 + return data + }(), + expected: FocusEventRecord{SetFocus: true}, + }, + { + name: "focus event - set focus false", + eventType: FocusEventType, + eventData: func() EventUnion { + var data EventUnion + data[0] = 0 + return data + }(), + expected: FocusEventRecord{SetFocus: false}, + }, + { + name: "key event - key down", + eventType: KeyEventType, + eventData: func() EventUnion { + var data EventUnion + binary.LittleEndian.PutUint32(data[0:4], 1) // KeyDown = true + binary.LittleEndian.PutUint16(data[4:6], 2) // RepeatCount = 2 + binary.LittleEndian.PutUint16(data[6:8], 65) // VirtualKeyCode = 'A' + binary.LittleEndian.PutUint16(data[8:10], 30) // VirtualScanCode = 30 + binary.LittleEndian.PutUint16(data[10:12], 97) // Char = 'a' + binary.LittleEndian.PutUint32(data[12:16], 0) // ControlKeyState = 0 + return data + }(), + expected: KeyEventRecord{ + KeyDown: true, + RepeatCount: 2, + VirtualKeyCode: 65, + VirtualScanCode: 30, + Char: 'a', + ControlKeyState: 0, + }, + }, + { + name: "mouse event", + eventType: MouseEventType, + eventData: func() EventUnion { + var data EventUnion + binary.LittleEndian.PutUint16(data[0:2], 10) // X = 10 + binary.LittleEndian.PutUint16(data[2:4], 20) // Y = 20 + binary.LittleEndian.PutUint32(data[4:8], 1) // ButtonState = 1 + binary.LittleEndian.PutUint32(data[8:12], 0) // ControlKeyState = 0 + binary.LittleEndian.PutUint32(data[12:16], 0) // EventFlags = 0 + return data + }(), + expected: MouseEventRecord{ + MousePositon: Coord{X: 10, Y: 20}, + ButtonState: 1, + ControlKeyState: 0, + EventFlags: 0, + WheelDirection: 0, + }, + }, + { + name: "window buffer size event", + eventType: WindowBufferSizeEventType, + eventData: func() EventUnion { + var data EventUnion + binary.LittleEndian.PutUint16(data[0:2], 80) // X = 80 + binary.LittleEndian.PutUint16(data[2:4], 25) // Y = 25 + return data + }(), + expected: WindowBufferSizeEventRecord{ + Size: Coord{X: 80, Y: 25}, + }, + }, + { + name: "menu event", + eventType: MenuEventType, + eventData: func() EventUnion { + var data EventUnion + binary.LittleEndian.PutUint32(data[0:4], 100) // CommandID = 100 + return data + }(), + expected: MenuEventRecord{ + CommandID: 100, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ir := InputRecord{ + EventType: tt.eventType, + Event: tt.eventData, + } + result := ir.Unwrap() + + switch expected := tt.expected.(type) { + case FocusEventRecord: + if focus, ok := result.(FocusEventRecord); !ok || focus != expected { + t.Errorf("Unwrap() = %v, want %v", result, expected) + } + case KeyEventRecord: + if key, ok := result.(KeyEventRecord); !ok || key != expected { + t.Errorf("Unwrap() = %v, want %v", result, expected) + } + case MouseEventRecord: + if mouse, ok := result.(MouseEventRecord); !ok || mouse != expected { + t.Errorf("Unwrap() = %v, want %v", result, expected) + } + case WindowBufferSizeEventRecord: + if win, ok := result.(WindowBufferSizeEventRecord); !ok || win != expected { + t.Errorf("Unwrap() = %v, want %v", result, expected) + } + case MenuEventRecord: + if menu, ok := result.(MenuEventRecord); !ok || menu != expected { + t.Errorf("Unwrap() = %v, want %v", result, expected) + } + } + }) + } +} + +func TestInputRecordUnwrapUnknownEvent(t *testing.T) { + ir := InputRecord{ + EventType: EventType(999), // Unknown event type + Event: EventUnion{}, + } + + result := ir.Unwrap() + if unknown, ok := result.(*UnknownEvent); !ok { + t.Errorf("Expected UnknownEvent, got %T", result) + } else if unknown.EventType != 999 { + t.Errorf("Expected EventType 999, got %d", unknown.EventType) + } +} + +func TestInputRecordString(t *testing.T) { + ir := InputRecord{ + EventType: FocusEventType, + Event: func() EventUnion { + var data EventUnion + data[0] = 1 + return data + }(), + } + + result := ir.String() + if !strings.Contains(result, "FocusEvent") { + t.Errorf("Expected string to contain 'FocusEvent', got %q", result) + } +} + +func TestFocusEventRecord(t *testing.T) { + event := FocusEventRecord{SetFocus: true} + + if event.Type() != "FocusEvent" { + t.Errorf("Type() = %q, want %q", event.Type(), "FocusEvent") + } + + str := event.String() + if !strings.Contains(str, "FocusEvent") || !strings.Contains(str, "true") { + t.Errorf("String() = %q, want to contain 'FocusEvent' and 'true'", str) + } +} + +func TestKeyEventRecord(t *testing.T) { + event := KeyEventRecord{ + KeyDown: true, + RepeatCount: 1, + VirtualKeyCode: 65, + VirtualScanCode: 30, + Char: 'A', + ControlKeyState: SHIFT_PRESSED, + } + + if event.Type() != "KeyEvent" { + t.Errorf("Type() = %q, want %q", event.Type(), "KeyEvent") + } + + str := event.String() + expected := []string{"KeyEvent", "'A'", "down", "Shift", "KeyCode: 65", "ScanCode: 30"} + for _, exp := range expected { + if !strings.Contains(str, exp) { + t.Errorf("String() = %q, want to contain %q", str, exp) + } + } +} + +func TestMouseEventRecord(t *testing.T) { + event := MouseEventRecord{ + MousePositon: Coord{X: 10, Y: 20}, + ButtonState: FROM_LEFT_1ST_BUTTON_PRESSED, + ControlKeyState: NO_CONTROL_KEY, + EventFlags: CLICK, + WheelDirection: 0, + } + + if event.Type() != "MouseEvent" { + t.Errorf("Type() = %q, want %q", event.Type(), "MouseEvent") + } + + str := event.String() + expected := []string{"MouseEvent", "(10, 20)", "Left", "Click"} + for _, exp := range expected { + if !strings.Contains(str, exp) { + t.Errorf("String() = %q, want to contain %q", str, exp) + } + } +} + +func TestMouseEventRecordWheelDirection(t *testing.T) { + tests := []struct { + name string + eventFlags EventFlags + wheelDirection int + expected string + }{ + { + name: "wheel forward", + eventFlags: MOUSE_WHEELED, + wheelDirection: 1, + expected: "Forward", + }, + { + name: "wheel backward", + eventFlags: MOUSE_WHEELED, + wheelDirection: -1, + expected: "Backward", + }, + { + name: "horizontal wheel right", + eventFlags: MOUSE_HWHEELED, + wheelDirection: 1, + expected: "Right", + }, + { + name: "horizontal wheel left", + eventFlags: MOUSE_HWHEELED, + wheelDirection: -1, + expected: "Left", + }, + { + name: "no wheel event", + eventFlags: CLICK, + wheelDirection: 0, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := MouseEventRecord{ + EventFlags: tt.eventFlags, + WheelDirection: tt.wheelDirection, + } + result := event.WheelDirectionName() + if result != tt.expected { + t.Errorf("WheelDirectionName() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestCoordString(t *testing.T) { + coord := Coord{X: 42, Y: 24} + expected := "(42, 24)" + result := coord.String() + + if result != expected { + t.Errorf("String() = %q, want %q", result, expected) + } +} + +func TestButtonState(t *testing.T) { + tests := []struct { + name string + state ButtonState + expected string + }{ + {"left button", FROM_LEFT_1ST_BUTTON_PRESSED, "Left"}, + {"right button", RIGHTMOST_BUTTON_PRESSED, "Right"}, + {"second button", FROM_LEFT_2ND_BUTTON_PRESSED, "2"}, + {"third button", FROM_LEFT_3RD_BUTTON_PRESSED, "3"}, + {"fourth button", FROM_LEFT_4TH_BUTTON_PRESSED, "4"}, + {"no button", ButtonState(0), "No Button"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.state.String() + if result != tt.expected { + t.Errorf("String() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestButtonStateContains(t *testing.T) { + state := FROM_LEFT_1ST_BUTTON_PRESSED | FROM_LEFT_2ND_BUTTON_PRESSED + + if !state.Contains(FROM_LEFT_1ST_BUTTON_PRESSED) { + t.Error("Expected state to contain FROM_LEFT_1ST_BUTTON_PRESSED") + } + + if !state.Contains(FROM_LEFT_2ND_BUTTON_PRESSED) { + t.Error("Expected state to contain FROM_LEFT_2ND_BUTTON_PRESSED") + } + + if state.Contains(RIGHTMOST_BUTTON_PRESSED) { + t.Error("Expected state to not contain RIGHTMOST_BUTTON_PRESSED") + } +} + +func TestControlKeyStateString(t *testing.T) { + tests := []struct { + name string + state ControlKeyState + expected string + }{ + {"no control keys", NO_CONTROL_KEY, ""}, + {"caps lock", CAPSLOCK_ON, "CapsLock"}, + {"shift", SHIFT_PRESSED, "Shift"}, + {"ctrl", LEFT_CTRL_PRESSED, "CTRL"}, + {"alt", LEFT_ALT_PRESSED, "Alt"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.state.String() + if result != tt.expected { + t.Errorf("String() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestControlKeyStateContains(t *testing.T) { + state := SHIFT_PRESSED | LEFT_CTRL_PRESSED + + if !state.Contains(SHIFT_PRESSED) { + t.Error("Expected state to contain SHIFT_PRESSED") + } + + if !state.Contains(LEFT_CTRL_PRESSED) { + t.Error("Expected state to contain LEFT_CTRL_PRESSED") + } + + if state.Contains(LEFT_ALT_PRESSED) { + t.Error("Expected state to not contain LEFT_ALT_PRESSED") + } +} + +func TestEventFlagsString(t *testing.T) { + tests := []struct { + name string + flags EventFlags + expected string + }{ + {"click", CLICK, "Click"}, + {"double click", DOUBLE_CLICK, "DoubleClick"}, + {"mouse moved", MOUSE_MOVED, "Moved"}, + {"mouse wheeled", MOUSE_WHEELED, "Wheeled"}, + {"mouse hwheeled", MOUSE_HWHEELED, "HWheeld"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.flags.String() + if result != tt.expected { + t.Errorf("String() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestEventFlagsContains(t *testing.T) { + flags := MOUSE_WHEELED | DOUBLE_CLICK + + if !flags.Contains(MOUSE_WHEELED) { + t.Error("Expected flags to contain MOUSE_WHEELED") + } + + if !flags.Contains(DOUBLE_CLICK) { + t.Error("Expected flags to contain DOUBLE_CLICK") + } + + if flags.Contains(MOUSE_MOVED) { + t.Error("Expected flags to not contain MOUSE_MOVED") + } +} \ No newline at end of file