From 6d66f93565403c009a7fddfeda80f6c5af6dd868 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 11 Dec 2025 14:50:02 -0500 Subject: [PATCH] core: mock wayland context for tests & add i18n guidance to CONTRIBUTING --- CONTRIBUTING.md | 15 + core/.mockery.yml | 12 + core/cmd/dms/commands_clipboard.go | 78 ++-- core/cmd/dms/commands_matugen.go | 66 +-- core/cmd/dms/commands_open.go | 34 +- core/cmd/dms/server_client.go | 51 ++- .../mocks/wlclient/mock_WaylandDisplay.go | 229 ++++++++++ .../mocks/wlcontext/mock_WaylandContext.go | 226 ++++++++++ core/internal/server/clipboard/manager.go | 2 +- .../internal/server/clipboard/manager_test.go | 75 ++++ core/internal/server/clipboard/types.go | 4 +- core/internal/server/dwl/manager.go | 2 +- core/internal/server/dwl/manager_test.go | 14 + core/internal/server/dwl/types.go | 2 +- core/internal/server/extworkspace/manager.go | 2 +- .../server/extworkspace/manager_test.go | 392 ++++++++++++++++++ core/internal/server/extworkspace/types.go | 2 +- core/internal/server/wayland/manager.go | 2 +- core/internal/server/wayland/manager_test.go | 28 ++ core/internal/server/wayland/types.go | 2 +- core/internal/server/wlcontext/context.go | 10 + core/internal/server/wlroutput/manager.go | 2 +- .../internal/server/wlroutput/manager_test.go | 14 + core/internal/server/wlroutput/types.go | 2 +- core/pkg/go-wayland/wayland/client/common.go | 9 + 25 files changed, 1145 insertions(+), 130 deletions(-) create mode 100644 core/internal/mocks/wlclient/mock_WaylandDisplay.go create mode 100644 core/internal/mocks/wlcontext/mock_WaylandContext.go create mode 100644 core/internal/server/extworkspace/manager_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6b85e53..8ccb5897 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,6 +21,7 @@ nix develop ``` This will provide: + - Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make - Quickshell and required QML packages - Properly configured QML2_IMPORT_PATH @@ -54,6 +55,20 @@ touch .qmlls.ini 5. Make your changes, test, and open a pull request. +### I18n/Localization + +When adding user-facing strings, ensure they are wrapped in `I18n.tr()` with context, for example. + +```qml +import qs.Common + +Text { + text: I18n.tr("Hello World", " Hello world greeting that appears on the lock screen") +} +``` + +Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated) + ### GO (`core` directory) 1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go) diff --git a/core/.mockery.yml b/core/.mockery.yml index 18be7864..e028d257 100644 --- a/core/.mockery.yml +++ b/core/.mockery.yml @@ -56,3 +56,15 @@ packages: outpkg: mocks_version interfaces: VersionFetcher: + github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext: + config: + dir: "internal/mocks/wlcontext" + outpkg: mocks_wlcontext + interfaces: + WaylandContext: + github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client: + config: + dir: "internal/mocks/wlclient" + outpkg: mocks_wlclient + interfaces: + WaylandDisplay: diff --git a/core/cmd/dms/commands_clipboard.go b/core/cmd/dms/commands_clipboard.go index 0092ab78..0c5d985e 100644 --- a/core/cmd/dms/commands_clipboard.go +++ b/core/cmd/dms/commands_clipboard.go @@ -14,6 +14,7 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/spf13/cobra" ) @@ -281,10 +282,9 @@ func runCommand(args []string, stdin []byte) { } func runClipHistory(cmd *cobra.Command, args []string) { - req := map[string]any{ - "id": 1, - "method": "clipboard.getHistory", - "params": map[string]any{}, + req := models.Request{ + ID: 1, + Method: "clipboard.getHistory", } resp, err := sendServerRequest(req) @@ -305,7 +305,7 @@ func runClipHistory(cmd *cobra.Command, args []string) { return } - historyList, ok := resp.Result.([]any) + historyList, ok := (*resp.Result).([]any) if !ok { log.Fatal("Invalid response format") } @@ -353,10 +353,10 @@ func runClipGet(cmd *cobra.Command, args []string) { } if clipGetCopy { - req := map[string]any{ - "id": 1, - "method": "clipboard.copyEntry", - "params": map[string]any{ + req := models.Request{ + ID: 1, + Method: "clipboard.copyEntry", + Params: map[string]any{ "id": id, }, } @@ -374,10 +374,10 @@ func runClipGet(cmd *cobra.Command, args []string) { return } - req := map[string]any{ - "id": 1, - "method": "clipboard.getEntry", - "params": map[string]any{ + req := models.Request{ + ID: 1, + Method: "clipboard.getEntry", + Params: map[string]any{ "id": id, }, } @@ -395,7 +395,7 @@ func runClipGet(cmd *cobra.Command, args []string) { log.Fatal("Entry not found") } - entry, ok := resp.Result.(map[string]any) + entry, ok := (*resp.Result).(map[string]any) if !ok { log.Fatal("Invalid response format") } @@ -420,10 +420,10 @@ func runClipDelete(cmd *cobra.Command, args []string) { log.Fatalf("Invalid ID: %v", err) } - req := map[string]any{ - "id": 1, - "method": "clipboard.deleteEntry", - "params": map[string]any{ + req := models.Request{ + ID: 1, + Method: "clipboard.deleteEntry", + Params: map[string]any{ "id": id, }, } @@ -441,10 +441,9 @@ func runClipDelete(cmd *cobra.Command, args []string) { } func runClipClear(cmd *cobra.Command, args []string) { - req := map[string]any{ - "id": 1, - "method": "clipboard.clearHistory", - "params": map[string]any{}, + req := models.Request{ + ID: 1, + Method: "clipboard.clearHistory", } resp, err := sendServerRequest(req) @@ -477,10 +476,10 @@ func runClipSearch(cmd *cobra.Command, args []string) { params["isImage"] = false } - req := map[string]any{ - "id": 1, - "method": "clipboard.search", - "params": params, + req := models.Request{ + ID: 1, + Method: "clipboard.search", + Params: params, } resp, err := sendServerRequest(req) @@ -492,7 +491,11 @@ func runClipSearch(cmd *cobra.Command, args []string) { log.Fatalf("Error: %s", resp.Error) } - result, ok := resp.Result.(map[string]any) + if resp.Result == nil { + log.Fatal("No results") + } + + result, ok := (*resp.Result).(map[string]any) if !ok { log.Fatal("Invalid response format") } @@ -540,10 +543,9 @@ func runClipSearch(cmd *cobra.Command, args []string) { } func runClipConfigGet(cmd *cobra.Command, args []string) { - req := map[string]any{ - "id": 1, - "method": "clipboard.getConfig", - "params": map[string]any{}, + req := models.Request{ + ID: 1, + Method: "clipboard.getConfig", } resp, err := sendServerRequest(req) @@ -555,7 +557,11 @@ func runClipConfigGet(cmd *cobra.Command, args []string) { log.Fatalf("Error: %s", resp.Error) } - cfg, ok := resp.Result.(map[string]any) + if resp.Result == nil { + log.Fatal("No config returned") + } + + cfg, ok := (*resp.Result).(map[string]any) if !ok { log.Fatal("Invalid response format") } @@ -603,10 +609,10 @@ func runClipConfigSet(cmd *cobra.Command, args []string) { return } - req := map[string]any{ - "id": 1, - "method": "clipboard.setConfig", - "params": params, + req := models.Request{ + ID: 1, + Method: "clipboard.setConfig", + Params: params, } resp, err := sendServerRequest(req) diff --git a/core/cmd/dms/commands_matugen.go b/core/cmd/dms/commands_matugen.go index c2081e41..48ce8969 100644 --- a/core/cmd/dms/commands_matugen.go +++ b/core/cmd/dms/commands_matugen.go @@ -2,15 +2,12 @@ package main import ( "context" - "encoding/json" "fmt" - "net" - "os" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/matugen" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/spf13/cobra" ) @@ -100,33 +97,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) { wait, _ := cmd.Flags().GetBool("wait") timeout, _ := cmd.Flags().GetDuration("timeout") - socketPath := os.Getenv("DMS_SOCKET") - if socketPath == "" { - var err error - socketPath, err = server.FindSocket() - if err != nil { - log.Info("No socket available, running synchronously") - if err := matugen.Run(opts); err != nil { - log.Fatalf("Theme generation failed: %v", err) - } - return - } - } - - conn, err := net.Dial("unix", socketPath) - if err != nil { - log.Info("Socket connection failed, running synchronously") - if err := matugen.Run(opts); err != nil { - log.Fatalf("Theme generation failed: %v", err) - } - return - } - defer conn.Close() - - request := map[string]any{ - "id": 1, - "method": "matugen.queue", - "params": map[string]any{ + request := models.Request{ + ID: 1, + Method: "matugen.queue", + Params: map[string]any{ "stateDir": opts.StateDir, "shellDir": opts.ShellDir, "configDir": opts.ConfigDir, @@ -144,11 +118,14 @@ func runMatugenQueue(cmd *cobra.Command, args []string) { }, } - if err := json.NewEncoder(conn).Encode(request); err != nil { - log.Fatalf("Failed to send request: %v", err) - } - if !wait { + if err := sendServerRequestFireAndForget(request); err != nil { + log.Info("Server unavailable, running synchronously") + if err := matugen.Run(opts); err != nil { + log.Fatalf("Theme generation failed: %v", err) + } + return + } fmt.Println("Theme generation queued") return } @@ -158,17 +135,18 @@ func runMatugenQueue(cmd *cobra.Command, args []string) { resultCh := make(chan error, 1) go func() { - var response struct { - ID int `json:"id"` - Result any `json:"result"` - Error string `json:"error"` - } - if err := json.NewDecoder(conn).Decode(&response); err != nil { - resultCh <- fmt.Errorf("failed to read response: %w", err) + resp, ok := tryServerRequest(request) + if !ok { + log.Info("Server unavailable, running synchronously") + if err := matugen.Run(opts); err != nil { + resultCh <- err + return + } + resultCh <- nil return } - if response.Error != "" { - resultCh <- fmt.Errorf("server error: %s", response.Error) + if resp.Error != "" { + resultCh <- fmt.Errorf("server error: %s", resp.Error) return } resultCh <- nil diff --git a/core/cmd/dms/commands_open.go b/core/cmd/dms/commands_open.go index e6a730e1..5f162a92 100644 --- a/core/cmd/dms/commands_open.go +++ b/core/cmd/dms/commands_open.go @@ -1,17 +1,14 @@ package main import ( - "encoding/json" "fmt" "mime" - "net" "net/url" "os" "path/filepath" "strings" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/spf13/cobra" ) @@ -93,32 +90,6 @@ func mimeTypeToCategories(mimeType string) []string { } func runOpen(target string) { - socketPath, err := server.FindSocket() - if err != nil { - log.Warnf("DMS socket not found: %v", err) - fmt.Println("DMS is not running. Please start DMS first.") - os.Exit(1) - } - - conn, err := net.Dial("unix", socketPath) - if err != nil { - log.Warnf("DMS socket connection failed: %v", err) - fmt.Println("DMS is not running. Please start DMS first.") - os.Exit(1) - } - defer conn.Close() - - buf := make([]byte, 1) - for { - _, err := conn.Read(buf) - if err != nil { - return - } - if buf[0] == '\n' { - break - } - } - // Parse file:// URIs to extract the actual file path actualTarget := target detectedMimeType := openMimeType @@ -219,8 +190,9 @@ func runOpen(target string) { log.Infof("Sending request - Method: %s, Params: %+v", method, params) - if err := json.NewEncoder(conn).Encode(req); err != nil { - log.Fatalf("Failed to send request: %v", err) + if err := sendServerRequestFireAndForget(req); err != nil { + fmt.Println("DMS is not running. Please start DMS first.") + os.Exit(1) } log.Infof("Request sent successfully") diff --git a/core/cmd/dms/server_client.go b/core/cmd/dms/server_client.go index 3a283750..871d21d5 100644 --- a/core/cmd/dms/server_client.go +++ b/core/cmd/dms/server_client.go @@ -9,15 +9,10 @@ import ( "path/filepath" "github.com/AvengeMedia/DankMaterialShell/core/internal/server" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" ) -type serverResponse struct { - ID int `json:"id,omitempty"` - Result any `json:"result,omitempty"` - Error string `json:"error,omitempty"` -} - -func sendServerRequest(req map[string]any) (*serverResponse, error) { +func sendServerRequest(req models.Request) (*models.Response[any], error) { socketPath := getServerSocketPath() conn, err := net.Dial("unix", socketPath) @@ -46,7 +41,7 @@ func sendServerRequest(req map[string]any) (*serverResponse, error) { return nil, fmt.Errorf("failed to read response") } - var resp serverResponse + var resp models.Response[any] if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } @@ -54,6 +49,46 @@ func sendServerRequest(req map[string]any) (*serverResponse, error) { return &resp, nil } +// sendServerRequestFireAndForget sends a request without waiting for a response. +// Useful for commands that trigger UI or async operations. +func sendServerRequestFireAndForget(req models.Request) error { + socketPath := getServerSocketPath() + + conn, err := net.Dial("unix", socketPath) + if err != nil { + return fmt.Errorf("failed to connect to server (is it running?): %w", err) + } + defer conn.Close() + + scanner := bufio.NewScanner(conn) + scanner.Scan() // discard initial capabilities message + + reqData, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + if _, err := conn.Write(reqData); err != nil { + return fmt.Errorf("failed to write request: %w", err) + } + + if _, err := conn.Write([]byte("\n")); err != nil { + return fmt.Errorf("failed to write newline: %w", err) + } + + return nil +} + +// tryServerRequest attempts to send a request but returns false if server unavailable. +// Does not log errors - caller can decide what to do on failure. +func tryServerRequest(req models.Request) (*models.Response[any], bool) { + resp, err := sendServerRequest(req) + if err != nil { + return nil, false + } + return resp, true +} + func getServerSocketPath() string { runtimeDir := os.Getenv("XDG_RUNTIME_DIR") if runtimeDir == "" { diff --git a/core/internal/mocks/wlclient/mock_WaylandDisplay.go b/core/internal/mocks/wlclient/mock_WaylandDisplay.go new file mode 100644 index 00000000..45d944bf --- /dev/null +++ b/core/internal/mocks/wlclient/mock_WaylandDisplay.go @@ -0,0 +1,229 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks_wlclient + +import ( + client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" + mock "github.com/stretchr/testify/mock" +) + +// MockWaylandDisplay is an autogenerated mock type for the WaylandDisplay type +type MockWaylandDisplay struct { + mock.Mock +} + +type MockWaylandDisplay_Expecter struct { + mock *mock.Mock +} + +func (_m *MockWaylandDisplay) EXPECT() *MockWaylandDisplay_Expecter { + return &MockWaylandDisplay_Expecter{mock: &_m.Mock} +} + +// Context provides a mock function with no fields +func (_m *MockWaylandDisplay) Context() *client.Context { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Context") + } + + var r0 *client.Context + if rf, ok := ret.Get(0).(func() *client.Context); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.Context) + } + } + + return r0 +} + +// MockWaylandDisplay_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' +type MockWaylandDisplay_Context_Call struct { + *mock.Call +} + +// Context is a helper method to define mock.On call +func (_e *MockWaylandDisplay_Expecter) Context() *MockWaylandDisplay_Context_Call { + return &MockWaylandDisplay_Context_Call{Call: _e.mock.On("Context")} +} + +func (_c *MockWaylandDisplay_Context_Call) Run(run func()) *MockWaylandDisplay_Context_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockWaylandDisplay_Context_Call) Return(_a0 *client.Context) *MockWaylandDisplay_Context_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockWaylandDisplay_Context_Call) RunAndReturn(run func() *client.Context) *MockWaylandDisplay_Context_Call { + _c.Call.Return(run) + return _c +} + +// Destroy provides a mock function with no fields +func (_m *MockWaylandDisplay) Destroy() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Destroy") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockWaylandDisplay_Destroy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Destroy' +type MockWaylandDisplay_Destroy_Call struct { + *mock.Call +} + +// Destroy is a helper method to define mock.On call +func (_e *MockWaylandDisplay_Expecter) Destroy() *MockWaylandDisplay_Destroy_Call { + return &MockWaylandDisplay_Destroy_Call{Call: _e.mock.On("Destroy")} +} + +func (_c *MockWaylandDisplay_Destroy_Call) Run(run func()) *MockWaylandDisplay_Destroy_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockWaylandDisplay_Destroy_Call) Return(_a0 error) *MockWaylandDisplay_Destroy_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockWaylandDisplay_Destroy_Call) RunAndReturn(run func() error) *MockWaylandDisplay_Destroy_Call { + _c.Call.Return(run) + return _c +} + +// GetRegistry provides a mock function with no fields +func (_m *MockWaylandDisplay) GetRegistry() (*client.Registry, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetRegistry") + } + + var r0 *client.Registry + var r1 error + if rf, ok := ret.Get(0).(func() (*client.Registry, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *client.Registry); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.Registry) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockWaylandDisplay_GetRegistry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRegistry' +type MockWaylandDisplay_GetRegistry_Call struct { + *mock.Call +} + +// GetRegistry is a helper method to define mock.On call +func (_e *MockWaylandDisplay_Expecter) GetRegistry() *MockWaylandDisplay_GetRegistry_Call { + return &MockWaylandDisplay_GetRegistry_Call{Call: _e.mock.On("GetRegistry")} +} + +func (_c *MockWaylandDisplay_GetRegistry_Call) Run(run func()) *MockWaylandDisplay_GetRegistry_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockWaylandDisplay_GetRegistry_Call) Return(_a0 *client.Registry, _a1 error) *MockWaylandDisplay_GetRegistry_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockWaylandDisplay_GetRegistry_Call) RunAndReturn(run func() (*client.Registry, error)) *MockWaylandDisplay_GetRegistry_Call { + _c.Call.Return(run) + return _c +} + +// Roundtrip provides a mock function with no fields +func (_m *MockWaylandDisplay) Roundtrip() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Roundtrip") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockWaylandDisplay_Roundtrip_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Roundtrip' +type MockWaylandDisplay_Roundtrip_Call struct { + *mock.Call +} + +// Roundtrip is a helper method to define mock.On call +func (_e *MockWaylandDisplay_Expecter) Roundtrip() *MockWaylandDisplay_Roundtrip_Call { + return &MockWaylandDisplay_Roundtrip_Call{Call: _e.mock.On("Roundtrip")} +} + +func (_c *MockWaylandDisplay_Roundtrip_Call) Run(run func()) *MockWaylandDisplay_Roundtrip_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockWaylandDisplay_Roundtrip_Call) Return(_a0 error) *MockWaylandDisplay_Roundtrip_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockWaylandDisplay_Roundtrip_Call) RunAndReturn(run func() error) *MockWaylandDisplay_Roundtrip_Call { + _c.Call.Return(run) + return _c +} + +// NewMockWaylandDisplay creates a new instance of MockWaylandDisplay. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockWaylandDisplay(t interface { + mock.TestingT + Cleanup(func()) +}) *MockWaylandDisplay { + mock := &MockWaylandDisplay{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/internal/mocks/wlcontext/mock_WaylandContext.go b/core/internal/mocks/wlcontext/mock_WaylandContext.go new file mode 100644 index 00000000..f5b0c357 --- /dev/null +++ b/core/internal/mocks/wlcontext/mock_WaylandContext.go @@ -0,0 +1,226 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks_wlcontext + +import ( + client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" + mock "github.com/stretchr/testify/mock" +) + +// MockWaylandContext is an autogenerated mock type for the WaylandContext type +type MockWaylandContext struct { + mock.Mock +} + +type MockWaylandContext_Expecter struct { + mock *mock.Mock +} + +func (_m *MockWaylandContext) EXPECT() *MockWaylandContext_Expecter { + return &MockWaylandContext_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with no fields +func (_m *MockWaylandContext) Close() { + _m.Called() +} + +// MockWaylandContext_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockWaylandContext_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *MockWaylandContext_Expecter) Close() *MockWaylandContext_Close_Call { + return &MockWaylandContext_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *MockWaylandContext_Close_Call) Run(run func()) *MockWaylandContext_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockWaylandContext_Close_Call) Return() *MockWaylandContext_Close_Call { + _c.Call.Return() + return _c +} + +func (_c *MockWaylandContext_Close_Call) RunAndReturn(run func()) *MockWaylandContext_Close_Call { + _c.Run(run) + return _c +} + +// Display provides a mock function with no fields +func (_m *MockWaylandContext) Display() *client.Display { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Display") + } + + var r0 *client.Display + if rf, ok := ret.Get(0).(func() *client.Display); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.Display) + } + } + + return r0 +} + +// MockWaylandContext_Display_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Display' +type MockWaylandContext_Display_Call struct { + *mock.Call +} + +// Display is a helper method to define mock.On call +func (_e *MockWaylandContext_Expecter) Display() *MockWaylandContext_Display_Call { + return &MockWaylandContext_Display_Call{Call: _e.mock.On("Display")} +} + +func (_c *MockWaylandContext_Display_Call) Run(run func()) *MockWaylandContext_Display_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockWaylandContext_Display_Call) Return(_a0 *client.Display) *MockWaylandContext_Display_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockWaylandContext_Display_Call) RunAndReturn(run func() *client.Display) *MockWaylandContext_Display_Call { + _c.Call.Return(run) + return _c +} + +// FatalError provides a mock function with no fields +func (_m *MockWaylandContext) FatalError() <-chan error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FatalError") + } + + var r0 <-chan error + if rf, ok := ret.Get(0).(func() <-chan error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan error) + } + } + + return r0 +} + +// MockWaylandContext_FatalError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FatalError' +type MockWaylandContext_FatalError_Call struct { + *mock.Call +} + +// FatalError is a helper method to define mock.On call +func (_e *MockWaylandContext_Expecter) FatalError() *MockWaylandContext_FatalError_Call { + return &MockWaylandContext_FatalError_Call{Call: _e.mock.On("FatalError")} +} + +func (_c *MockWaylandContext_FatalError_Call) Run(run func()) *MockWaylandContext_FatalError_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockWaylandContext_FatalError_Call) Return(_a0 <-chan error) *MockWaylandContext_FatalError_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockWaylandContext_FatalError_Call) RunAndReturn(run func() <-chan error) *MockWaylandContext_FatalError_Call { + _c.Call.Return(run) + return _c +} + +// Post provides a mock function with given fields: fn +func (_m *MockWaylandContext) Post(fn func()) { + _m.Called(fn) +} + +// MockWaylandContext_Post_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Post' +type MockWaylandContext_Post_Call struct { + *mock.Call +} + +// Post is a helper method to define mock.On call +// - fn func() +func (_e *MockWaylandContext_Expecter) Post(fn interface{}) *MockWaylandContext_Post_Call { + return &MockWaylandContext_Post_Call{Call: _e.mock.On("Post", fn)} +} + +func (_c *MockWaylandContext_Post_Call) Run(run func(fn func())) *MockWaylandContext_Post_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(func())) + }) + return _c +} + +func (_c *MockWaylandContext_Post_Call) Return() *MockWaylandContext_Post_Call { + _c.Call.Return() + return _c +} + +func (_c *MockWaylandContext_Post_Call) RunAndReturn(run func(func())) *MockWaylandContext_Post_Call { + _c.Run(run) + return _c +} + +// Start provides a mock function with no fields +func (_m *MockWaylandContext) Start() { + _m.Called() +} + +// MockWaylandContext_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type MockWaylandContext_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +func (_e *MockWaylandContext_Expecter) Start() *MockWaylandContext_Start_Call { + return &MockWaylandContext_Start_Call{Call: _e.mock.On("Start")} +} + +func (_c *MockWaylandContext_Start_Call) Run(run func()) *MockWaylandContext_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockWaylandContext_Start_Call) Return() *MockWaylandContext_Start_Call { + _c.Call.Return() + return _c +} + +func (_c *MockWaylandContext_Start_Call) RunAndReturn(run func()) *MockWaylandContext_Start_Call { + _c.Run(run) + return _c +} + +// NewMockWaylandContext creates a new instance of MockWaylandContext. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockWaylandContext(t interface { + mock.TestingT + Cleanup(func()) +}) *MockWaylandContext { + mock := &MockWaylandContext{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/internal/server/clipboard/manager.go b/core/internal/server/clipboard/manager.go index 9eb6ce52..e04a05ab 100644 --- a/core/internal/server/clipboard/manager.go +++ b/core/internal/server/clipboard/manager.go @@ -27,7 +27,7 @@ import ( wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" ) -func NewManager(wlCtx *wlcontext.SharedContext, config Config) (*Manager, error) { +func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) { if config.Disabled { return nil, fmt.Errorf("clipboard disabled in config") } diff --git a/core/internal/server/clipboard/manager_test.go b/core/internal/server/clipboard/manager_test.go index e1686588..ff6e7d32 100644 --- a/core/internal/server/clipboard/manager_test.go +++ b/core/internal/server/clipboard/manager_test.go @@ -2,10 +2,14 @@ package clipboard import ( "sync" + "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext" ) func TestEncodeDecodeEntry_Roundtrip(t *testing.T) { @@ -454,3 +458,74 @@ func TestDefaultConfig(t *testing.T) { assert.False(t, cfg.DisableHistory) assert.False(t, cfg.DisablePersist) } + +func TestManager_PostDelegatesToWlContext(t *testing.T) { + mockCtx := mocks_wlcontext.NewMockWaylandContext(t) + + var called atomic.Bool + mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) { + called.Store(true) + fn() + }).Once() + + m := &Manager{ + wlCtx: mockCtx, + } + + executed := false + m.post(func() { + executed = true + }) + + assert.True(t, called.Load()) + assert.True(t, executed) +} + +func TestManager_PostExecutesFunctionViaContext(t *testing.T) { + mockCtx := mocks_wlcontext.NewMockWaylandContext(t) + + var capturedFn func() + mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) { + capturedFn = fn + }).Times(3) + + m := &Manager{ + wlCtx: mockCtx, + } + + counter := 0 + m.post(func() { counter++ }) + m.post(func() { counter += 10 }) + m.post(func() { counter += 100 }) + + assert.NotNil(t, capturedFn) + capturedFn() + assert.Equal(t, 100, counter) +} + +func TestManager_ConcurrentPostWithMock(t *testing.T) { + mockCtx := mocks_wlcontext.NewMockWaylandContext(t) + + var postCount atomic.Int32 + mockCtx.EXPECT().Post(mock.AnythingOfType("func()")).Run(func(fn func()) { + postCount.Add(1) + }).Times(100) + + m := &Manager{ + wlCtx: mockCtx, + } + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 10; j++ { + m.post(func() {}) + } + }() + } + + wg.Wait() + assert.Equal(t, int32(100), postCount.Load()) +} diff --git a/core/internal/server/clipboard/types.go b/core/internal/server/clipboard/types.go index b611945d..372071e1 100644 --- a/core/internal/server/clipboard/types.go +++ b/core/internal/server/clipboard/types.go @@ -115,8 +115,8 @@ type Manager struct { configMutex sync.RWMutex configPath string - display *wlclient.Display - wlCtx *wlcontext.SharedContext + display wlclient.WaylandDisplay + wlCtx wlcontext.WaylandContext registry *wlclient.Registry dataControlMgr any diff --git a/core/internal/server/dwl/manager.go b/core/internal/server/dwl/manager.go index e73623ba..0d21afa0 100644 --- a/core/internal/server/dwl/manager.go +++ b/core/internal/server/dwl/manager.go @@ -10,7 +10,7 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc" ) -func NewManager(display *wlclient.Display) (*Manager, error) { +func NewManager(display wlclient.WaylandDisplay) (*Manager, error) { m := &Manager{ display: display, ctx: display.Context(), diff --git a/core/internal/server/dwl/manager_test.go b/core/internal/server/dwl/manager_test.go index 03129af8..13af1379 100644 --- a/core/internal/server/dwl/manager_test.go +++ b/core/internal/server/dwl/manager_test.go @@ -1,11 +1,14 @@ package dwl import ( + "errors" "sync" "testing" "time" "github.com/stretchr/testify/assert" + + mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" ) func TestStateChanged_BothNil(t *testing.T) { @@ -350,3 +353,14 @@ func TestStateChanged_TagsLengthDiffers(t *testing.T) { } assert.True(t, stateChanged(a, b)) } + +func TestNewManager_GetRegistryError(t *testing.T) { + mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) + + mockDisplay.EXPECT().Context().Return(nil) + mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) + + _, err := NewManager(mockDisplay) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get registry") +} diff --git a/core/internal/server/dwl/types.go b/core/internal/server/dwl/types.go index 72756a8d..2b175377 100644 --- a/core/internal/server/dwl/types.go +++ b/core/internal/server/dwl/types.go @@ -38,7 +38,7 @@ type cmd struct { } type Manager struct { - display *wlclient.Display + display wlclient.WaylandDisplay ctx *wlclient.Context registry *wlclient.Registry manager any diff --git a/core/internal/server/extworkspace/manager.go b/core/internal/server/extworkspace/manager.go index 35a6943b..b791b393 100644 --- a/core/internal/server/extworkspace/manager.go +++ b/core/internal/server/extworkspace/manager.go @@ -38,7 +38,7 @@ func CheckCapability() bool { return found } -func NewManager(display *wlclient.Display) (*Manager, error) { +func NewManager(display wlclient.WaylandDisplay) (*Manager, error) { m := &Manager{ display: display, ctx: display.Context(), diff --git a/core/internal/server/extworkspace/manager_test.go b/core/internal/server/extworkspace/manager_test.go new file mode 100644 index 00000000..b20d4576 --- /dev/null +++ b/core/internal/server/extworkspace/manager_test.go @@ -0,0 +1,392 @@ +package extworkspace + +import ( + "errors" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" +) + +func TestStateChanged_BothNil(t *testing.T) { + assert.True(t, stateChanged(nil, nil)) +} + +func TestStateChanged_OneNil(t *testing.T) { + s := &State{Groups: []*WorkspaceGroup{}} + assert.True(t, stateChanged(s, nil)) + assert.True(t, stateChanged(nil, s)) +} + +func TestStateChanged_GroupCountDiffers(t *testing.T) { + a := &State{Groups: []*WorkspaceGroup{{ID: "group-1"}}} + b := &State{Groups: []*WorkspaceGroup{}} + assert.True(t, stateChanged(a, b)) +} + +func TestStateChanged_GroupIDDiffers(t *testing.T) { + a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}} + b := &State{Groups: []*WorkspaceGroup{{ID: "group-2", Outputs: []string{}, Workspaces: []*Workspace{}}}} + assert.True(t, stateChanged(a, b)) +} + +func TestStateChanged_OutputCountDiffers(t *testing.T) { + a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}} + b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}} + assert.True(t, stateChanged(a, b)) +} + +func TestStateChanged_OutputNameDiffers(t *testing.T) { + a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}} + b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"HDMI-A-1"}, Workspaces: []*Workspace{}}}} + assert.True(t, stateChanged(a, b)) +} + +func TestStateChanged_WorkspaceCountDiffers(t *testing.T) { + a := &State{Groups: []*WorkspaceGroup{{ + ID: "group-1", + Outputs: []string{}, + Workspaces: []*Workspace{{ID: "1", Name: "ws1"}}, + }}} + b := &State{Groups: []*WorkspaceGroup{{ + ID: "group-1", + Outputs: []string{}, + Workspaces: []*Workspace{}, + }}} + assert.True(t, stateChanged(a, b)) +} + +func TestStateChanged_WorkspaceFieldsDiffer(t *testing.T) { + a := &State{Groups: []*WorkspaceGroup{{ + ID: "group-1", + Outputs: []string{}, + Workspaces: []*Workspace{{ + ID: "1", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false, + }}, + }}} + b := &State{Groups: []*WorkspaceGroup{{ + ID: "group-1", + Outputs: []string{}, + Workspaces: []*Workspace{{ + ID: "2", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false, + }}, + }}} + assert.True(t, stateChanged(a, b)) + + b.Groups[0].Workspaces[0].ID = "1" + b.Groups[0].Workspaces[0].Name = "ws2" + assert.True(t, stateChanged(a, b)) + + b.Groups[0].Workspaces[0].Name = "ws1" + b.Groups[0].Workspaces[0].State = 1 + assert.True(t, stateChanged(a, b)) + + b.Groups[0].Workspaces[0].State = 0 + b.Groups[0].Workspaces[0].Active = true + assert.True(t, stateChanged(a, b)) + + b.Groups[0].Workspaces[0].Active = false + b.Groups[0].Workspaces[0].Urgent = true + assert.True(t, stateChanged(a, b)) + + b.Groups[0].Workspaces[0].Urgent = false + b.Groups[0].Workspaces[0].Hidden = true + assert.True(t, stateChanged(a, b)) +} + +func TestStateChanged_WorkspaceCoordinatesDiffer(t *testing.T) { + a := &State{Groups: []*WorkspaceGroup{{ + ID: "group-1", + Outputs: []string{}, + Workspaces: []*Workspace{{ + ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, + }}, + }}} + b := &State{Groups: []*WorkspaceGroup{{ + ID: "group-1", + Outputs: []string{}, + Workspaces: []*Workspace{{ + ID: "1", Name: "ws1", Coordinates: []uint32{1, 0}, + }}, + }}} + assert.True(t, stateChanged(a, b)) + + b.Groups[0].Workspaces[0].Coordinates = []uint32{0} + assert.True(t, stateChanged(a, b)) +} + +func TestStateChanged_Equal(t *testing.T) { + a := &State{Groups: []*WorkspaceGroup{{ + ID: "group-1", + Outputs: []string{"eDP-1", "HDMI-A-1"}, + Workspaces: []*Workspace{ + {ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true}, + {ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false}, + }, + }}} + b := &State{Groups: []*WorkspaceGroup{{ + ID: "group-1", + Outputs: []string{"eDP-1", "HDMI-A-1"}, + Workspaces: []*Workspace{ + {ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true}, + {ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false}, + }, + }}} + assert.False(t, stateChanged(a, b)) +} + +func TestManager_ConcurrentGetState(t *testing.T) { + m := &Manager{ + state: &State{ + Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}}, + }, + } + + var wg sync.WaitGroup + const goroutines = 50 + const iterations = 100 + + for i := 0; i < goroutines/2; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < iterations; j++ { + s := m.GetState() + _ = s.Groups + } + }() + } + + for i := 0; i < goroutines/2; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < iterations; j++ { + m.stateMutex.Lock() + m.state = &State{ + Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}}, + } + m.stateMutex.Unlock() + } + }(i) + } + + wg.Wait() +} + +func TestManager_ConcurrentSubscriberAccess(t *testing.T) { + m := &Manager{ + stopChan: make(chan struct{}), + dirty: make(chan struct{}, 1), + } + + var wg sync.WaitGroup + const goroutines = 20 + + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + subID := string(rune('a' + id)) + ch := m.Subscribe(subID) + assert.NotNil(t, ch) + time.Sleep(time.Millisecond) + m.Unsubscribe(subID) + }(i) + } + + wg.Wait() +} + +func TestManager_SyncmapGroupsConcurrentAccess(t *testing.T) { + m := &Manager{} + + var wg sync.WaitGroup + const goroutines = 30 + const iterations = 50 + + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + key := uint32(id) + + for j := 0; j < iterations; j++ { + state := &workspaceGroupState{ + id: key, + outputIDs: map[uint32]bool{1: true}, + workspaceIDs: []uint32{uint32(j)}, + } + m.groups.Store(key, state) + + if loaded, ok := m.groups.Load(key); ok { + assert.Equal(t, key, loaded.id) + } + + m.groups.Range(func(k uint32, v *workspaceGroupState) bool { + _ = v.id + _ = v.outputIDs + return true + }) + } + + m.groups.Delete(key) + }(i) + } + + wg.Wait() +} + +func TestManager_SyncmapWorkspacesConcurrentAccess(t *testing.T) { + m := &Manager{} + + var wg sync.WaitGroup + const goroutines = 30 + const iterations = 50 + + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + key := uint32(id) + + for j := 0; j < iterations; j++ { + state := &workspaceState{ + id: key, + workspaceID: "ws-1", + name: "workspace", + state: uint32(j % 4), + coordinates: []uint32{uint32(j), 0}, + } + m.workspaces.Store(key, state) + + if loaded, ok := m.workspaces.Load(key); ok { + assert.Equal(t, key, loaded.id) + } + + m.workspaces.Range(func(k uint32, v *workspaceState) bool { + _ = v.name + _ = v.state + return true + }) + } + + m.workspaces.Delete(key) + }(i) + } + + wg.Wait() +} + +func TestManager_SyncmapOutputNamesConcurrentAccess(t *testing.T) { + m := &Manager{} + + var wg sync.WaitGroup + const goroutines = 30 + const iterations = 50 + + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + key := uint32(id) + + for j := 0; j < iterations; j++ { + m.outputNames.Store(key, "eDP-1") + + if loaded, ok := m.outputNames.Load(key); ok { + assert.NotEmpty(t, loaded) + } + + m.outputNames.Range(func(k uint32, v string) bool { + _ = v + return true + }) + } + + m.outputNames.Delete(key) + }(i) + } + + wg.Wait() +} + +func TestManager_NotifySubscribersNonBlocking(t *testing.T) { + m := &Manager{ + dirty: make(chan struct{}, 1), + } + + for i := 0; i < 10; i++ { + m.notifySubscribers() + } + + assert.Len(t, m.dirty, 1) +} + +func TestManager_PostQueueFull(t *testing.T) { + m := &Manager{ + cmdq: make(chan cmd, 2), + stopChan: make(chan struct{}), + } + + m.post(func() {}) + m.post(func() {}) + m.post(func() {}) + m.post(func() {}) + + assert.Len(t, m.cmdq, 2) +} + +func TestManager_GetStateNilState(t *testing.T) { + m := &Manager{} + + s := m.GetState() + assert.NotNil(t, s.Groups) + assert.Empty(t, s.Groups) +} + +func TestWorkspace_Fields(t *testing.T) { + ws := Workspace{ + ID: "ws-1", + Name: "workspace 1", + Coordinates: []uint32{0, 0}, + State: 1, + Active: true, + Urgent: false, + Hidden: false, + } + + assert.Equal(t, "ws-1", ws.ID) + assert.Equal(t, "workspace 1", ws.Name) + assert.True(t, ws.Active) + assert.False(t, ws.Urgent) + assert.False(t, ws.Hidden) +} + +func TestWorkspaceGroup_Fields(t *testing.T) { + group := WorkspaceGroup{ + ID: "group-1", + Outputs: []string{"eDP-1", "HDMI-A-1"}, + Workspaces: []*Workspace{ + {ID: "ws-1", Name: "workspace 1"}, + }, + } + + assert.Equal(t, "group-1", group.ID) + assert.Len(t, group.Outputs, 2) + assert.Len(t, group.Workspaces, 1) +} + +func TestNewManager_GetRegistryError(t *testing.T) { + mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) + + mockDisplay.EXPECT().Context().Return(nil) + mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) + + _, err := NewManager(mockDisplay) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get registry") +} diff --git a/core/internal/server/extworkspace/types.go b/core/internal/server/extworkspace/types.go index 6e30198a..903a1cb6 100644 --- a/core/internal/server/extworkspace/types.go +++ b/core/internal/server/extworkspace/types.go @@ -33,7 +33,7 @@ type cmd struct { } type Manager struct { - display *wlclient.Display + display wlclient.WaylandDisplay ctx *wlclient.Context registry *wlclient.Registry manager *ext_workspace.ExtWorkspaceManagerV1 diff --git a/core/internal/server/wayland/manager.go b/core/internal/server/wayland/manager.go index 974ff8ca..62a9f2b7 100644 --- a/core/internal/server/wayland/manager.go +++ b/core/internal/server/wayland/manager.go @@ -19,7 +19,7 @@ import ( const animKelvinStep = 25 -func NewManager(display *wlclient.Display, config Config) (*Manager, error) { +func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) { if err := config.Validate(); err != nil { return nil, err } diff --git a/core/internal/server/wayland/manager_test.go b/core/internal/server/wayland/manager_test.go index 8581536c..3ff00a9c 100644 --- a/core/internal/server/wayland/manager_test.go +++ b/core/internal/server/wayland/manager_test.go @@ -1,11 +1,14 @@ package wayland import ( + "errors" "sync" "testing" "time" "github.com/stretchr/testify/assert" + + mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" ) func TestManager_ActorSerializesOutputStateAccess(t *testing.T) { @@ -384,3 +387,28 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) { assert.Len(t, m.dirty, 1) } + +func TestNewManager_GetRegistryError(t *testing.T) { + mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) + + mockDisplay.EXPECT().Context().Return(nil) + mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) + + config := DefaultConfig() + _, err := NewManager(mockDisplay, config) + assert.Error(t, err) + assert.Contains(t, err.Error(), "get registry") +} + +func TestNewManager_InvalidConfig(t *testing.T) { + mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) + + config := Config{ + LowTemp: 500, + HighTemp: 6500, + Gamma: 1.0, + } + + _, err := NewManager(mockDisplay, config) + assert.Error(t, err) +} diff --git a/core/internal/server/wayland/types.go b/core/internal/server/wayland/types.go index a4397586..d7e2bd0b 100644 --- a/core/internal/server/wayland/types.go +++ b/core/internal/server/wayland/types.go @@ -65,7 +65,7 @@ type Manager struct { state *State stateMutex sync.RWMutex - display *wlclient.Display + display wlclient.WaylandDisplay ctx *wlclient.Context registry *wlclient.Registry gammaControl any diff --git a/core/internal/server/wlcontext/context.go b/core/internal/server/wlcontext/context.go index 27c285af..01f7f11a 100644 --- a/core/internal/server/wlcontext/context.go +++ b/core/internal/server/wlcontext/context.go @@ -12,6 +12,16 @@ import ( wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" ) +type WaylandContext interface { + Display() *wlclient.Display + Post(fn func()) + FatalError() <-chan error + Start() + Close() +} + +var _ WaylandContext = (*SharedContext)(nil) + type SharedContext struct { display *wlclient.Display stopChan chan struct{} diff --git a/core/internal/server/wlroutput/manager.go b/core/internal/server/wlroutput/manager.go index 001f43ef..22e8aedd 100644 --- a/core/internal/server/wlroutput/manager.go +++ b/core/internal/server/wlroutput/manager.go @@ -9,7 +9,7 @@ import ( wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" ) -func NewManager(display *wlclient.Display) (*Manager, error) { +func NewManager(display wlclient.WaylandDisplay) (*Manager, error) { m := &Manager{ display: display, ctx: display.Context(), diff --git a/core/internal/server/wlroutput/manager_test.go b/core/internal/server/wlroutput/manager_test.go index 2c7a5042..e90c70c9 100644 --- a/core/internal/server/wlroutput/manager_test.go +++ b/core/internal/server/wlroutput/manager_test.go @@ -1,11 +1,14 @@ package wlroutput import ( + "errors" "sync" "testing" "time" "github.com/stretchr/testify/assert" + + mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" ) func TestStateChanged_BothNil(t *testing.T) { @@ -398,3 +401,14 @@ func TestStateChanged_IndexOutOfBounds(t *testing.T) { b := &State{Serial: 1, Outputs: []Output{{Name: "eDP-1"}, {Name: "HDMI-A-1"}}} assert.True(t, stateChanged(a, b)) } + +func TestNewManager_GetRegistryError(t *testing.T) { + mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) + + mockDisplay.EXPECT().Context().Return(nil) + mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) + + _, err := NewManager(mockDisplay) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get registry") +} diff --git a/core/internal/server/wlroutput/types.go b/core/internal/server/wlroutput/types.go index 71b5c0a8..1d265ab6 100644 --- a/core/internal/server/wlroutput/types.go +++ b/core/internal/server/wlroutput/types.go @@ -45,7 +45,7 @@ type cmd struct { } type Manager struct { - display *wlclient.Display + display wlclient.WaylandDisplay ctx *wlclient.Context registry *wlclient.Registry manager *wlr_output_management.ZwlrOutputManagerV1 diff --git a/core/pkg/go-wayland/wayland/client/common.go b/core/pkg/go-wayland/wayland/client/common.go index a3451755..b97d1b8e 100644 --- a/core/pkg/go-wayland/wayland/client/common.go +++ b/core/pkg/go-wayland/wayland/client/common.go @@ -15,6 +15,15 @@ type Proxy interface { MarkZombie() } +type WaylandDisplay interface { + Context() *Context + GetRegistry() (*Registry, error) + Roundtrip() error + Destroy() error +} + +var _ WaylandDisplay = (*Display)(nil) + type BaseProxy struct { ctx *Context id uint32