Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
ignore:
- "pkg/testing/**/*"
- "pkg/readers/testutils/**/*"
25 changes: 15 additions & 10 deletions pkg/config/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,23 @@ import (

// TUIConfig holds TUI-specific configuration.
type TUIConfig struct {
Theme string `toml:"theme"`
WriteFormat string `toml:"write_format"`
Mouse bool `toml:"mouse"`
CRTMode bool `toml:"crt_mode"`
OnScreenKeyboard bool `toml:"on_screen_keyboard"`
Theme string `toml:"theme"`
WriteFormat string `toml:"write_format"`
Mouse bool `toml:"mouse"`
CRTMode bool `toml:"crt_mode"`
OnScreenKeyboard bool `toml:"on_screen_keyboard"`
ErrorReportingPrompted bool `toml:"error_reporting_prompted"`
}

// tuiConfigRaw is used for TOML unmarshalling with pointer fields
// to distinguish between missing values and explicit false/empty.
type tuiConfigRaw struct {
Theme *string `toml:"theme"`
WriteFormat *string `toml:"write_format"`
Mouse *bool `toml:"mouse"`
CRTMode *bool `toml:"crt_mode"`
OnScreenKeyboard *bool `toml:"on_screen_keyboard"`
Theme *string `toml:"theme"`
WriteFormat *string `toml:"write_format"`
Mouse *bool `toml:"mouse"`
CRTMode *bool `toml:"crt_mode"`
OnScreenKeyboard *bool `toml:"on_screen_keyboard"`
ErrorReportingPrompted *bool `toml:"error_reporting_prompted"`
}

const (
Expand Down Expand Up @@ -109,6 +111,9 @@ func applyTUIDefaults(raw tuiConfigRaw, platformID string) TUIConfig {
if raw.OnScreenKeyboard != nil {
cfg.OnScreenKeyboard = *raw.OnScreenKeyboard
}
if raw.ErrorReportingPrompted != nil {
cfg.ErrorReportingPrompted = *raw.ErrorReportingPrompted
}
return cfg
}

Expand Down
225 changes: 225 additions & 0 deletions pkg/config/tui_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Zaparoo Core
// Copyright (c) 2026 The Zaparoo Project Contributors.
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of Zaparoo Core.
//
// Zaparoo Core is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Zaparoo Core is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Zaparoo Core. If not, see <http://www.gnu.org/licenses/>.

package config

import (
"os"
"path/filepath"
"testing"

platformids "github.com/ZaparooProject/zaparoo-core/v2/pkg/platforms/ids"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestApplyTUIDefaults_AllNil(t *testing.T) {
t.Parallel()

cfg := applyTUIDefaults(tuiConfigRaw{}, "generic")

assert.Equal(t, defaultTUITheme, cfg.Theme)
assert.Equal(t, defaultTUIWriteFormat, cfg.WriteFormat)
assert.True(t, cfg.Mouse)
assert.False(t, cfg.CRTMode)
assert.False(t, cfg.OnScreenKeyboard)
assert.False(t, cfg.ErrorReportingPrompted)
}

func TestApplyTUIDefaults_AllSet(t *testing.T) {
t.Parallel()

theme := "dark"
writeFormat := "uid"
mouse := false
crt := true
osk := true
prompted := true

raw := tuiConfigRaw{
Theme: &theme,
WriteFormat: &writeFormat,
Mouse: &mouse,
CRTMode: &crt,
OnScreenKeyboard: &osk,
ErrorReportingPrompted: &prompted,
}

cfg := applyTUIDefaults(raw, "generic")

assert.Equal(t, "dark", cfg.Theme)
assert.Equal(t, "uid", cfg.WriteFormat)
assert.False(t, cfg.Mouse)
assert.True(t, cfg.CRTMode)
assert.True(t, cfg.OnScreenKeyboard)
assert.True(t, cfg.ErrorReportingPrompted)
}

func TestApplyTUIDefaults_MisterPlatformDefaults(t *testing.T) {
t.Parallel()

cfg := applyTUIDefaults(tuiConfigRaw{}, platformids.Mister)

assert.True(t, cfg.CRTMode, "MiSTer should default CRT mode to true")
assert.True(t, cfg.OnScreenKeyboard, "MiSTer should default OSK to true")
}

func TestApplyTUIDefaults_MistexPlatformDefaults(t *testing.T) {
t.Parallel()

cfg := applyTUIDefaults(tuiConfigRaw{}, platformids.Mistex)

assert.True(t, cfg.CRTMode, "MiSTex should default CRT mode to true")
assert.True(t, cfg.OnScreenKeyboard, "MiSTex should default OSK to true")
}

func TestApplyTUIDefaults_PlatformDefaultsOverriddenByUser(t *testing.T) {
t.Parallel()

crt := false
osk := false
raw := tuiConfigRaw{
CRTMode: &crt,
OnScreenKeyboard: &osk,
}

cfg := applyTUIDefaults(raw, platformids.Mister)

assert.False(t, cfg.CRTMode, "User override should take precedence over platform default")
assert.False(t, cfg.OnScreenKeyboard, "User override should take precedence over platform default")
}

func TestApplyTUIDefaults_ErrorReportingPromptedExplicitFalse(t *testing.T) {
t.Parallel()

prompted := false
raw := tuiConfigRaw{
ErrorReportingPrompted: &prompted,
}

cfg := applyTUIDefaults(raw, "generic")

assert.False(t, cfg.ErrorReportingPrompted, "Explicit false should be preserved")
}

// TestTUIConfigGlobalState tests functions that mutate the global tuiCfg atomic.
// These cannot run in parallel with each other.
func TestTUIConfigGlobalState(t *testing.T) {
t.Run("GetSetTUIConfig", func(t *testing.T) {
cfg := TUIConfig{
Theme: "custom",
WriteFormat: "uid",
Mouse: false,
CRTMode: true,
OnScreenKeyboard: true,
ErrorReportingPrompted: true,
}

SetTUIConfig(cfg)
got := GetTUIConfig()

assert.Equal(t, cfg, got)
})

t.Run("LoadTUIConfig_CreatesDefaultWhenMissing", func(t *testing.T) {
dir := t.TempDir()

err := LoadTUIConfig(dir, "generic")
require.NoError(t, err)

tuiPath := filepath.Join(dir, TUIFile)
_, err = os.Stat(tuiPath)
require.NoError(t, err, "TUI config file should be created")

cfg := GetTUIConfig()
assert.Equal(t, defaultTUITheme, cfg.Theme)
assert.Equal(t, defaultTUIWriteFormat, cfg.WriteFormat)
assert.True(t, cfg.Mouse)
assert.False(t, cfg.ErrorReportingPrompted)
})

t.Run("LoadTUIConfig_ReadsExistingFile", func(t *testing.T) {
dir := t.TempDir()
tuiPath := filepath.Join(dir, TUIFile)

content := `theme = "retro"
write_format = "uid"
mouse = false
crt_mode = true
on_screen_keyboard = false
error_reporting_prompted = true
`
err := os.WriteFile(tuiPath, []byte(content), 0o600)
require.NoError(t, err)

err = LoadTUIConfig(dir, "generic")
require.NoError(t, err)

cfg := GetTUIConfig()
assert.Equal(t, "retro", cfg.Theme)
assert.Equal(t, "uid", cfg.WriteFormat)
assert.False(t, cfg.Mouse)
assert.True(t, cfg.CRTMode)
assert.False(t, cfg.OnScreenKeyboard)
assert.True(t, cfg.ErrorReportingPrompted)
})

t.Run("LoadTUIConfig_FillsMissingWithDefaults", func(t *testing.T) {
dir := t.TempDir()
tuiPath := filepath.Join(dir, TUIFile)

content := `theme = "minimal"
`
err := os.WriteFile(tuiPath, []byte(content), 0o600)
require.NoError(t, err)

err = LoadTUIConfig(dir, "generic")
require.NoError(t, err)

cfg := GetTUIConfig()
assert.Equal(t, "minimal", cfg.Theme)
assert.Equal(t, defaultTUIWriteFormat, cfg.WriteFormat)
assert.True(t, cfg.Mouse)
assert.False(t, cfg.CRTMode)
assert.False(t, cfg.ErrorReportingPrompted)
})

t.Run("SaveTUIConfig_RoundTrip", func(t *testing.T) {
dir := t.TempDir()

original := TUIConfig{
Theme: "saved",
WriteFormat: "uid",
Mouse: true,
CRTMode: false,
OnScreenKeyboard: true,
ErrorReportingPrompted: true,
}
SetTUIConfig(original)

err := SaveTUIConfig(dir)
require.NoError(t, err)

err = LoadTUIConfig(dir, "generic")
require.NoError(t, err)

loaded := GetTUIConfig()
assert.Equal(t, original, loaded)
})
}
6 changes: 0 additions & 6 deletions pkg/database/mediadb/mediadb.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,6 @@ func (db *MediaDB) Open() error {
log.Warn().Err(err).Msg("failed to run WAL checkpoint on startup")
}

if exists {
if cacheErr := db.RebuildSlugSearchCache(); cacheErr != nil {
log.Warn().Err(cacheErr).Msg("failed to build slug search cache on startup")
}
}

return nil
}

Expand Down
9 changes: 6 additions & 3 deletions pkg/readers/pn532uart/pn532uart_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ func TestOpen_ErrorCountExceedsMaxWithActiveToken(t *testing.T) {
assert.True(t, scan2.ReaderError, "ReaderError should be true to prevent on_remove execution")

// Reader should have stopped polling
time.Sleep(100 * time.Millisecond)
assert.False(t, reader.Connected(), "reader should have stopped after max errors")
assert.True(t, testutils.WaitForCondition(func() bool {
return !reader.Connected()
}, 2*time.Second), "reader should have stopped after max errors")
}

// TestOpen_ErrorCountExceedsMaxWithoutActiveToken verifies that ReaderError is sent
Expand Down Expand Up @@ -191,7 +192,9 @@ func TestOpen_ErrorCountExceedsMaxWithoutActiveToken(t *testing.T) {
assert.True(t, scan.ReaderError, "ReaderError should be sent even without active token")

// Reader should have stopped polling
assert.False(t, reader.Connected(), "reader should have stopped after max errors")
assert.True(t, testutils.WaitForCondition(func() bool {
return !reader.Connected()
}, 2*time.Second), "reader should have stopped after max errors")
}

// TestOpen_TokenDetectionAndRemoval tests normal token detection and removal flow.
Expand Down
13 changes: 13 additions & 0 deletions pkg/readers/testutils/testutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ func AssertNoScan(t *testing.T, ch chan readers.Scan, timeout time.Duration) {
}
}

// WaitForCondition polls a condition function until it returns true or the timeout expires.
// Returns true if the condition was met, false if timed out.
func WaitForCondition(condition func() bool, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if condition() {
return true
}
time.Sleep(5 * time.Millisecond)
}
return false
}

// CreateTempDevicePath creates a temporary file to represent a device path for testing.
// On Windows systems, it returns a COM port path. On Unix systems, it creates a temporary
// file and registers cleanup with t.Cleanup().
Expand Down
11 changes: 11 additions & 0 deletions pkg/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,17 @@ func Start(
apiNotifications, _ := notifBroker.Subscribe(100)
go api.Start(pl, cfg, st, itq, db, limitsManager, apiNotifications, discoveryService.InstanceName(), player)

// Build slug search cache after API is listening to avoid blocking startup
if db.MediaDB != nil {
db.MediaDB.TrackBackgroundOperation()
go func() {
defer db.MediaDB.BackgroundOperationDone()
if cacheErr := db.MediaDB.RebuildSlugSearchCache(); cacheErr != nil {
log.Warn().Err(cacheErr).Msg("failed to build slug search cache")
}
}()
Comment thread
sentry[bot] marked this conversation as resolved.
}

log.Info().Msg("starting publishers")
publisherNotifications, _ := notifBroker.Subscribe(100)
activePublishers, cancelPublisherFanOut := startPublishers(st, cfg, publisherNotifications)
Expand Down
Loading
Loading