Skip to content

Commit

Permalink
request: introduce Response and parse the RawResponse in e.g.: Dispatch
Browse files Browse the repository at this point in the history
  • Loading branch information
thiagokokada committed Jul 25, 2024
1 parent 4fa3ed0 commit fa4304e
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 97 deletions.
128 changes: 76 additions & 52 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,46 +114,67 @@ func prepareRequests(command string, params []string) (requests []RawRequest, er
return requests, nil
}

func (c *RequestClient) validateResponse(params []string, response RawResponse) error {
if !c.Validate {
return nil
}

// Empty response
if len(response) == 0 {
return errors.New("empty response")
}

reader := bufio.NewReader(bytes.NewReader(response))
func parseResponse(raw RawResponse) (response []Response, err error) {
reader := bufio.NewReader(bytes.NewReader(raw))
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines)

i := 0
for ; scanner.Scan(); i++ {
for scanner.Scan() {
resp := strings.TrimSpace(scanner.Text())
if resp == "" {
continue
}
if resp != "ok" {
if i >= len(params) {
return fmt.Errorf("non-ok response from unknown param: %s", response)
} else {
return fmt.Errorf("non-ok response from request: %d, param: %s, response: %s", i, params[i], resp)
}
}
response = append(response, Response(resp))
}

if err := scanner.Err(); err != nil {
return response, err
}

return response, nil
}

func validateResponse(validate bool, params []string, response []Response) ([]Response, error) {
// Empty response, something went terrible wrong
if len(response) == 0 {
return []Response{""}, errors.New("empty response")
}

if !validate {
return response, nil
}

want := len(params)
if want == 0 {
// commands without parameters will have at least one return
want = 1
}
// we have a different number of requests and responses
if want != len(response) {
return response, fmt.Errorf(
"want responses: %d, got: %d, responses: %v",
want,
len(response),
response,
)
}

if i < want {
return fmt.Errorf("got ok: %d, want: %d", i, want)
// validate that all responses are ok
for i, r := range response {
if r != "ok" {
return response, fmt.Errorf("non-ok response from param: %s, response: %s", params[i], r)
}
}

return nil
return response, nil
}

func parseAndValidateResponse(validate bool, params []string, raw RawResponse) ([]Response, error) {
response, err := parseResponse(raw)
if err != nil {
return response, err
}
return validateResponse(validate, params, response)
}

func unmarshalResponse(response RawResponse, v any) (err error) {
Expand Down Expand Up @@ -380,14 +401,14 @@ func (c *RequestClient) Devices() (d Devices, err error) {
// Dispatch commands, similar to 'hyprctl dispatch'.
// Accept multiple commands at the same time, in this case it will use batch
// mode, similar to 'hyprctl dispatch --batch'.
// Returns the raw response, that may be useful for further validations,
// especially when [RequestClient] 'Validation' is set to false.
func (c *RequestClient) Dispatch(params ...string) (r RawResponse, err error) {
response, err := c.doRequest("dispatch", params...)
// Returns a [Response] list for each parameter, that may be useful for further
// validations, especially when [RequestClient] 'Validation' is set to false.
func (c *RequestClient) Dispatch(params ...string) (r []Response, err error) {
raw, err := c.doRequest("dispatch", params...)
if err != nil {
return response, err
return nil, err
}
return response, c.validateResponse(params, response)
return parseAndValidateResponse(c.Validate, params, raw)
}

// Get option command, similar to 'hyprctl getoption'.
Expand All @@ -403,27 +424,28 @@ func (c *RequestClient) GetOption(name string) (o Option, err error) {
// Keyword command, similar to 'hyprctl keyword'.
// Accept multiple commands at the same time, in this case it will use batch
// mode, similar to 'hyprctl keyword --batch'.
// Returns the raw response, that may be useful for further validations,
// especially when [RequestClient] 'Validation' is set to false.
func (c *RequestClient) Keyword(params ...string) (r RawResponse, err error) {
response, err := c.doRequest("keyword", params...)
// Returns a [Response] list for each parameter, that may be useful for further
// validations, especially when [RequestClient] 'Validation' is set to false.
func (c *RequestClient) Keyword(params ...string) (r []Response, err error) {
raw, err := c.doRequest("keyword", params...)
if err != nil {
return response, err
return nil, err
}
return response, c.validateResponse(nil, response)
return parseAndValidateResponse(c.Validate, params, raw)
}

// Kill command, similar to 'hyprctl kill'.
// Kill an app by clicking on it, can exit with ESCAPE. Will NOT wait until the
// user to click in the window.
// Returns the raw response, that may be useful for further validations,
// especially when [RequestClient] 'Validation' is set to false.
func (c *RequestClient) Kill() (r RawResponse, err error) {
response, err := c.doRequest("kill")
// Returns a [Response], that may be useful for further validations, especially
// when [RequestClient] 'Validation' is set to false.
func (c *RequestClient) Kill() (r Response, err error) {
raw, err := c.doRequest("kill")
if err != nil {
return response, err
return "", err
}
return response, c.validateResponse(nil, response)
response, err := parseAndValidateResponse(c.Validate, nil, raw)
return response[0], err // should return only one response
}

// Layer command, similar to 'hyprctl layers'.
Expand All @@ -447,25 +469,27 @@ func (c *RequestClient) Monitors() (m []Monitor, err error) {
}

// Reload command, similar to 'hyprctl reload'.
// Returns the raw response, that may be useful for further validations,
// especially when [RequestClient] 'Validation' is set to false.
func (c *RequestClient) Reload() (r RawResponse, err error) {
response, err := c.doRequest("reload")
// Returns a [Response], that may be useful for further validations, especially
// when [RequestClient] 'Validation' is set to false.
func (c *RequestClient) Reload() (r Response, err error) {
raw, err := c.doRequest("reload")
if err != nil {
return response, err
return "", err
}
return response, c.validateResponse(nil, response)
response, err := parseAndValidateResponse(c.Validate, nil, raw)
return response[0], err // should return only one response
}

// Set cursor command, similar to 'hyprctl setcursor'.
// Returns the raw response, that may be useful for further validations,
// Returns a [Response] object, that may be useful for further validations,
// especially when [RequestClient] 'Validation' is set to false.
func (c *RequestClient) SetCursor(theme string, size int) (r RawResponse, err error) {
response, err := c.doRequest("setcursor", fmt.Sprintf("%s %d", theme, size))
func (c *RequestClient) SetCursor(theme string, size int) (r Response, err error) {
raw, err := c.doRequest("setcursor", fmt.Sprintf("%s %d", theme, size))
if err != nil {
return response, err
return "", err
}
return response, c.validateResponse(nil, response)
response, err := parseAndValidateResponse(c.Validate, nil, raw)
return response[0], err // should return only one response
}

// Splash command, similar to 'hyprctl splash'.
Expand Down
108 changes: 63 additions & 45 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package hyprland
import (
"fmt"
"os"
"reflect"
"strings"
"testing"
"time"
Expand All @@ -30,16 +29,19 @@ func checkEnvironment(t *testing.T) {
}
}

func testCommandRR(t *testing.T, command func() (RawResponse, error)) {
testCommand(t, command, RawResponse(""))
func testCommandR(t *testing.T, command func() (Response, error)) {
testCommand(t, command, "")
}

func testCommandRs(t *testing.T, command func() ([]Response, error)) {
testCommand(t, command, []Response{})
}

func testCommand[T any](t *testing.T, command func() (T, error), emptyValue any) {
checkEnvironment(t)
got, err := command()
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(got), reflect.TypeOf(emptyValue))
assert.True(t, reflect.DeepEqual(got, emptyValue))
assert.DeepNotEqual(t, got, emptyValue)
t.Logf("got: %+v", got)
}

Expand Down Expand Up @@ -76,7 +78,7 @@ func TestPrepareRequests(t *testing.T) {
{"command", []string{"param0", "param1"}, []string{"[[BATCH]]command param0;command param1;"}},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("tests_%v-%v", tt.command, tt.params), func(t *testing.T) {
t.Run(fmt.Sprintf("tests_%s-%s", tt.command, tt.params), func(t *testing.T) {
requests, err := prepareRequests(tt.command, tt.params)
assert.NoError(t, err)
for i, e := range tt.expected {
Expand All @@ -102,7 +104,7 @@ func TestPrepareRequestsMass(t *testing.T) {
{"command", genParams("very big param list", 10000), 35},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("mass_tests_%v-%d", tt.command, len(tt.params)), func(t *testing.T) {
t.Run(fmt.Sprintf("mass_tests_%s-%d", tt.command, len(tt.params)), func(t *testing.T) {
requests, err := prepareRequests(tt.command, tt.params)
assert.NoError(t, err)
assert.Equal(t, len(requests), tt.expected)
Expand Down Expand Up @@ -138,31 +140,58 @@ func BenchmarkPrepareRequests(b *testing.B) {
}
}

func TestValidateResponse(t *testing.T) {
// Dummy client to allow this test to run without Hyprland
c := DummyClient{}
func TestParseResponse(t *testing.T) {
tests := []struct {
response RawResponse
want int
}{
{RawResponse("ok"), 1},
{RawResponse("ok\r\nok"), 2},
{RawResponse(" ok "), 1},
{RawResponse(strings.Repeat("ok\r\n", 5)), 5},
{RawResponse(strings.Repeat("ok\r\n\r\n", 5)), 5},
{RawResponse(strings.Repeat("ok\r\n\n", 10)), 10},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("tests_%s-%d", tt.response, tt.want), func(t *testing.T) {
response, err := parseResponse(tt.response)
assert.NoError(t, err)
assert.Equal(t, len(response), tt.want)
for _, r := range response {
assert.Equal(t, r, "ok")
}
})
}
}

func TestValidateResponse(t *testing.T) {
tests := []struct {
params []string
response RawResponse
validate bool
expectErr bool
validate bool
params []string
response []Response
want []Response
wantErr bool
}{
{genParams("param", 1), RawResponse("ok"), true, false},
{genParams("param", 2), RawResponse("ok\r\nInvalid response"), false, false},
{genParams("param", 2), RawResponse("ok"), true, true},
{genParams("param", 2), RawResponse("ok"), false, false},
{genParams("param", 1), RawResponse("ok\r\nok"), true, false}, // not sure about this case, will leave like this for now
{genParams("param", 5), RawResponse(strings.Repeat("ok\r\n", 5)), true, false},
{genParams("param", 6), RawResponse(strings.Repeat("ok\r\n", 5)), true, true},
{genParams("param", 6), RawResponse(strings.Repeat("ok\r\n\r\n", 5)), false, false},
{genParams("param", 10), RawResponse(strings.Repeat("ok\r\n\n", 10)), true, false},
// empty response should error
{true, genParams("param", 1), []Response{}, []Response{""}, true},
// happy path
{true, genParams("param", 1), []Response{"ok"}, []Response{"ok"}, false},
// happy path
{true, genParams("param", 2), []Response{"ok", "ok"}, []Response{"ok", "ok"}, false},
// missing response
{true, genParams("param", 2), []Response{"ok"}, []Response{"ok"}, true},
// disable validation
{false, genParams("param", 2), []Response{"ok"}, []Response{"ok"}, false},
// non-ok response
{true, genParams("param", 2), []Response{"ok", "Invalid command"}, []Response{"ok", "Invalid command"}, true},
// disable validation
{false, genParams("param", 2), []Response{"ok", "Invalid command"}, []Response{"ok", "Invalid command"}, false},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("tests_%v-%v", tt.params, tt.response), func(t *testing.T) {
c.Validate = tt.validate
err := c.validateResponse(tt.params, tt.response)
if tt.expectErr {
t.Run(fmt.Sprintf("tests_%v-%v-%v", tt.validate, tt.params, tt.response), func(t *testing.T) {
response, err := validateResponse(tt.validate, tt.params, tt.response)
assert.DeepEqual(t, response, tt.want)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
Expand All @@ -171,21 +200,10 @@ func TestValidateResponse(t *testing.T) {
}
}

func BenchmarkValidateResponse(b *testing.B) {
// Dummy client to allow this test to run without Hyprland
c := DummyClient{}
params := genParams("param", 1000)
response := strings.Repeat("ok"+strings.Repeat(" ", 1000), 1000)

for i := 0; i < b.N; i++ {
c.validateResponse(params, RawResponse(response))
}
}

func TestRawRequest(t *testing.T) {
testCommandRR(t, func() (RawResponse, error) {
testCommand(t, func() (RawResponse, error) {
return c.RawRequest([]byte("splash"))
})
}, RawResponse{})
}

func TestActiveWindow(t *testing.T) {
Expand Down Expand Up @@ -230,7 +248,7 @@ func TestDevices(t *testing.T) {
}

func TestDispatch(t *testing.T) {
testCommandRR(t, func() (RawResponse, error) {
testCommandRs(t, func() ([]Response, error) {
return c.Dispatch("exec kitty sh -c 'echo Testing hyprland-go && sleep 1 && exit 0'")
})

Expand Down Expand Up @@ -292,7 +310,7 @@ func TestGetOption(t *testing.T) {
}

func TestKeyword(t *testing.T) {
testCommandRR(t, func() (RawResponse, error) {
testCommandRs(t, func() ([]Response, error) {
return c.Keyword("general:border_size 1", "general:border_size 5")
})
}
Expand All @@ -301,7 +319,7 @@ func TestKill(t *testing.T) {
if testing.Short() {
t.Skip("skip test that kill window")
}
testCommandRR(t, c.Kill)
testCommandR(t, c.Kill)
}

func TestLayers(t *testing.T) {
Expand All @@ -313,11 +331,11 @@ func TestMonitors(t *testing.T) {
}

func TestReload(t *testing.T) {
testCommandRR(t, c.Reload)
testCommandR(t, c.Reload)
}

func TestSetCursor(t *testing.T) {
testCommandRR(t, func() (RawResponse, error) {
testCommandR(t, func() (Response, error) {
return c.SetCursor("Adwaita", 32)
})
}
Expand Down
3 changes: 3 additions & 0 deletions request_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ type RawRequest []byte
// Represents a raw response returned from the Hyprland's socket.
type RawResponse []byte

// Represents a parsed response returned from the Hyprland's socket.
type Response string

// RequestClient is the main struct from hyprland-go.
// You may want to set 'Validate' as false to avoid (possibly costly)
// validations, at the expense of not reporting some errors in the IPC.
Expand Down

0 comments on commit fa4304e

Please sign in to comment.