1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00
Files
DankMaterialShell/core/internal/server/clipboard/manager_test.go

532 lines
11 KiB
Go

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) {
original := Entry{
ID: 12345,
Data: []byte("hello world"),
MimeType: "text/plain;charset=utf-8",
Preview: "hello world",
Size: 11,
Timestamp: time.Now().Truncate(time.Second),
IsImage: false,
}
encoded, err := encodeEntry(original)
assert.NoError(t, err)
decoded, err := decodeEntry(encoded)
assert.NoError(t, err)
assert.Equal(t, original.ID, decoded.ID)
assert.Equal(t, original.Data, decoded.Data)
assert.Equal(t, original.MimeType, decoded.MimeType)
assert.Equal(t, original.Preview, decoded.Preview)
assert.Equal(t, original.Size, decoded.Size)
assert.Equal(t, original.Timestamp.Unix(), decoded.Timestamp.Unix())
assert.Equal(t, original.IsImage, decoded.IsImage)
}
func TestEncodeDecodeEntry_Image(t *testing.T) {
original := Entry{
ID: 99999,
Data: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A},
MimeType: "image/png",
Preview: "[[ image 8 B png 100x100 ]]",
Size: 8,
Timestamp: time.Now().Truncate(time.Second),
IsImage: true,
}
encoded, err := encodeEntry(original)
assert.NoError(t, err)
decoded, err := decodeEntry(encoded)
assert.NoError(t, err)
assert.Equal(t, original.ID, decoded.ID)
assert.Equal(t, original.Data, decoded.Data)
assert.True(t, decoded.IsImage)
assert.Equal(t, original.Preview, decoded.Preview)
}
func TestEncodeDecodeEntry_EmptyData(t *testing.T) {
original := Entry{
ID: 1,
Data: []byte{},
MimeType: "text/plain",
Preview: "",
Size: 0,
Timestamp: time.Now().Truncate(time.Second),
IsImage: false,
}
encoded, err := encodeEntry(original)
assert.NoError(t, err)
decoded, err := decodeEntry(encoded)
assert.NoError(t, err)
assert.Equal(t, original.ID, decoded.ID)
assert.Empty(t, decoded.Data)
}
func TestEncodeDecodeEntry_LargeData(t *testing.T) {
largeData := make([]byte, 100000)
for i := range largeData {
largeData[i] = byte(i % 256)
}
original := Entry{
ID: 777,
Data: largeData,
MimeType: "application/octet-stream",
Preview: "binary data...",
Size: len(largeData),
Timestamp: time.Now().Truncate(time.Second),
IsImage: false,
}
encoded, err := encodeEntry(original)
assert.NoError(t, err)
decoded, err := decodeEntry(encoded)
assert.NoError(t, err)
assert.Equal(t, original.Data, decoded.Data)
assert.Equal(t, original.Size, decoded.Size)
}
func TestStateEqual_BothNil(t *testing.T) {
assert.False(t, stateEqual(nil, nil))
}
func TestStateEqual_OneNil(t *testing.T) {
s := &State{Enabled: true}
assert.False(t, stateEqual(s, nil))
assert.False(t, stateEqual(nil, s))
}
func TestStateEqual_EnabledDiffers(t *testing.T) {
a := &State{Enabled: true, History: []Entry{}}
b := &State{Enabled: false, History: []Entry{}}
assert.False(t, stateEqual(a, b))
}
func TestStateEqual_HistoryLengthDiffers(t *testing.T) {
a := &State{Enabled: true, History: []Entry{{ID: 1}}}
b := &State{Enabled: true, History: []Entry{}}
assert.False(t, stateEqual(a, b))
}
func TestStateEqual_BothEqual(t *testing.T) {
a := &State{Enabled: true, History: []Entry{{ID: 1}, {ID: 2}}}
b := &State{Enabled: true, History: []Entry{{ID: 3}, {ID: 4}}}
assert.True(t, stateEqual(a, b))
}
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
m := &Manager{
subscribers: make(map[string]chan State),
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_ConcurrentGetState(t *testing.T) {
m := &Manager{
state: &State{
Enabled: true,
History: []Entry{{ID: 1}, {ID: 2}},
},
}
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.Enabled
_ = len(s.History)
}
}()
}
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{
Enabled: j%2 == 0,
History: []Entry{{ID: uint64(j)}},
}
m.stateMutex.Unlock()
}
}(i)
}
wg.Wait()
}
func TestManager_ConcurrentConfigAccess(t *testing.T) {
m := &Manager{
config: DefaultConfig(),
}
var wg sync.WaitGroup
const goroutines = 30
const iterations = 100
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
cfg := m.getConfig()
_ = cfg.MaxHistory
_ = cfg.MaxEntrySize
}
}()
}
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.configMutex.Lock()
m.config.MaxHistory = 50 + j
m.config.MaxEntrySize = int64(1024 * j)
m.configMutex.Unlock()
}
}(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_ConcurrentOfferAccess(t *testing.T) {
m := &Manager{
offerMimeTypes: make(map[any][]string),
offerRegistry: make(map[uint32]any),
}
var wg sync.WaitGroup
const goroutines = 20
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.offerMutex.Lock()
m.offerRegistry[key] = struct{}{}
m.offerMimeTypes[key] = []string{"text/plain"}
m.offerMutex.Unlock()
m.offerMutex.RLock()
_ = m.offerRegistry[key]
_ = m.offerMimeTypes[key]
m.offerMutex.RUnlock()
m.offerMutex.Lock()
delete(m.offerRegistry, key)
delete(m.offerMimeTypes, key)
m.offerMutex.Unlock()
}
}(i)
}
wg.Wait()
}
func TestManager_ConcurrentPersistAccess(t *testing.T) {
m := &Manager{
persistData: make(map[string][]byte),
persistMimeTypes: []string{},
}
var wg sync.WaitGroup
const goroutines = 20
const iterations = 50
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.persistMutex.RLock()
_ = m.persistData
_ = m.persistMimeTypes
m.persistMutex.RUnlock()
}
}()
}
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.persistMutex.Lock()
m.persistMimeTypes = []string{"text/plain", "text/html"}
m.persistData = map[string][]byte{
"text/plain": []byte("test"),
}
m.persistMutex.Unlock()
}
}(i)
}
wg.Wait()
}
func TestManager_ConcurrentOwnerAccess(t *testing.T) {
m := &Manager{}
var wg sync.WaitGroup
const goroutines = 30
const iterations = 100
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.ownerLock.Lock()
_ = m.isOwner
m.ownerLock.Unlock()
}
}()
}
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.ownerLock.Lock()
m.isOwner = j%2 == 0
m.ownerLock.Unlock()
}
}()
}
wg.Wait()
}
func TestItob(t *testing.T) {
tests := []struct {
input uint64
expected []byte
}{
{0, []byte{0, 0, 0, 0, 0, 0, 0, 0}},
{1, []byte{0, 0, 0, 0, 0, 0, 0, 1}},
{256, []byte{0, 0, 0, 0, 0, 0, 1, 0}},
{0xFFFFFFFFFFFFFFFF, []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}},
}
for _, tt := range tests {
result := itob(tt.input)
assert.Equal(t, tt.expected, result)
}
}
func TestSizeStr(t *testing.T) {
tests := []struct {
input int
expected string
}{
{0, "0 B"},
{100, "100 B"},
{1024, "1 KiB"},
{2048, "2 KiB"},
{1048576, "1 MiB"},
{5242880, "5 MiB"},
}
for _, tt := range tests {
result := sizeStr(tt.input)
assert.Equal(t, tt.expected, result)
}
}
func TestSelectMimeType(t *testing.T) {
m := &Manager{}
tests := []struct {
mimes []string
expected string
}{
{[]string{"text/plain;charset=utf-8", "text/html"}, "text/plain;charset=utf-8"},
{[]string{"text/html", "text/plain"}, "text/plain"},
{[]string{"application/json", "image/png"}, "image/png"},
{[]string{"application/json", "application/xml"}, ""},
{[]string{}, ""},
}
for _, tt := range tests {
result := m.selectMimeType(tt.mimes)
assert.Equal(t, tt.expected, result)
}
}
func TestIsImageMimeType(t *testing.T) {
m := &Manager{}
assert.True(t, m.isImageMimeType("image/png"))
assert.True(t, m.isImageMimeType("image/jpeg"))
assert.True(t, m.isImageMimeType("image/gif"))
assert.False(t, m.isImageMimeType("text/plain"))
assert.False(t, m.isImageMimeType("application/json"))
}
func TestTextPreview(t *testing.T) {
m := &Manager{}
short := m.textPreview([]byte("hello world"))
assert.Equal(t, "hello world", short)
withWhitespace := m.textPreview([]byte(" hello world "))
assert.Equal(t, "hello world", withWhitespace)
longText := make([]byte, 200)
for i := range longText {
longText[i] = 'a'
}
preview := m.textPreview(longText)
assert.True(t, len(preview) > 100)
assert.Contains(t, preview, "…")
}
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
assert.Equal(t, 100, cfg.MaxHistory)
assert.Equal(t, int64(5*1024*1024), cfg.MaxEntrySize)
assert.Equal(t, 0, cfg.AutoClearDays)
assert.False(t, cfg.ClearAtStartup)
assert.False(t, cfg.Disabled)
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())
}