mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 21:09:05 +00:00
280 lines
8.3 KiB
Go
280 lines
8.3 KiB
Go
package periodic_scanner
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/photoview/photoview/api/graphql/models"
|
|
"github.com/photoview/photoview/api/test_utils"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// MockScannerQueue implements the ScannerQueue interface for testing
|
|
type MockScannerQueue struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockScannerQueue) AddAllToQueue() error {
|
|
return m.Called().Error(0)
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
test_utils.UnitTestRun(m)
|
|
}
|
|
|
|
func resetPeriodicScanner() {
|
|
|
|
if mainPeriodicScanner != nil {
|
|
select {
|
|
case <-mainPeriodicScanner.done:
|
|
// Already closed
|
|
default:
|
|
close(mainPeriodicScanner.done)
|
|
}
|
|
if mainPeriodicScanner.ticker != nil {
|
|
mainPeriodicScanner.ticker.Stop()
|
|
}
|
|
mainPeriodicScanner = nil
|
|
}
|
|
}
|
|
|
|
func createTestSiteInfo(db *gorm.DB, interval int) error {
|
|
siteInfo := models.SiteInfo{
|
|
InitialSetup: false,
|
|
PeriodicScanInterval: interval,
|
|
ConcurrentWorkers: 1,
|
|
}
|
|
return db.Create(&siteInfo).Error
|
|
}
|
|
|
|
func TestGetPeriodicScanInterval(t *testing.T) {
|
|
db := test_utils.DatabaseTest(t)
|
|
|
|
t.Run("successful retrieval", func(t *testing.T) {
|
|
assert.NoError(t, createTestSiteInfo(db, 300), "Failed to create test site info with 300 second interval")
|
|
|
|
duration, err := getPeriodicScanInterval(db)
|
|
assert.NoError(t, err, "Failed to retrieve periodic scan interval from database")
|
|
assert.Equal(t, 300*time.Second, duration,
|
|
"Periodic scan interval should be 300 seconds but got %v", duration)
|
|
})
|
|
|
|
t.Run("database error - no site info", func(t *testing.T) {
|
|
db.Exec("DELETE FROM site_info")
|
|
|
|
duration, err := getPeriodicScanInterval(db)
|
|
assert.Error(t, err, "Expected error when no site info exists in database")
|
|
assert.Equal(t, time.Duration(0), duration,
|
|
"Duration should be zero when database error occurs, but got %v", duration)
|
|
})
|
|
}
|
|
|
|
func TestInitializePeriodicScanner(t *testing.T) {
|
|
db := test_utils.DatabaseTest(t)
|
|
|
|
t.Run("successful initialization with injection", func(t *testing.T) {
|
|
defer resetPeriodicScanner()
|
|
|
|
mockQueue := &MockScannerQueue{}
|
|
assert.NoError(t, createTestSiteInfo(db, 300), "Failed to create test site info with 300 second interval")
|
|
assert.NoError(t, InitializePeriodicScannerWithQueue(db, mockQueue),
|
|
"Failed to initialize periodic scanner with mock queue")
|
|
|
|
// Verify initialization
|
|
assert.NotNil(t, mainPeriodicScanner, "mainPeriodicScanner should not be nil after successful initialization")
|
|
assert.NotNil(t, mainPeriodicScanner.scannerQueue, "Scanner queue should not be nil after initialization")
|
|
assert.Equal(t, mockQueue, mainPeriodicScanner.scannerQueue, "Scanner should use the injected mock queue instance")
|
|
|
|
// Verify ticker is set up
|
|
mainPeriodicScanner.tickerLocker.Lock()
|
|
tickerExists := mainPeriodicScanner.ticker != nil
|
|
mainPeriodicScanner.tickerLocker.Unlock()
|
|
assert.True(t, tickerExists, "Ticker should be created and set up after scanner initialization")
|
|
})
|
|
|
|
t.Run("backward compatibility with original function", func(t *testing.T) {
|
|
defer resetPeriodicScanner()
|
|
|
|
assert.NoError(t, createTestSiteInfo(db, 300), "Failed to create test site info with 300 second interval")
|
|
assert.NoError(t, InitializePeriodicScanner(db),
|
|
"Failed to initialize periodic scanner using original function")
|
|
|
|
// Verify it uses RealScannerQueue
|
|
assert.NotNil(t, mainPeriodicScanner,
|
|
"mainPeriodicScanner should be initialized by original InitializePeriodicScanner function")
|
|
assert.IsType(t, &RealScannerQueue{}, mainPeriodicScanner.scannerQueue,
|
|
"Original InitializePeriodicScanner should use RealScannerQueue by default")
|
|
})
|
|
|
|
t.Run("double initialization error", func(t *testing.T) {
|
|
defer resetPeriodicScanner()
|
|
|
|
mockQueue := &MockScannerQueue{}
|
|
assert.NoError(t, createTestSiteInfo(db, 300), "Failed to create test site info with 300 second interval")
|
|
assert.NoError(t, InitializePeriodicScannerWithQueue(db, mockQueue),
|
|
"Failed first initialization for double initialization test")
|
|
|
|
err := InitializePeriodicScannerWithQueue(db, mockQueue)
|
|
assert.Error(t, err, "Second initialization attempt should return an error")
|
|
assert.Contains(t, err.Error(), "already been initialized",
|
|
"Double initialization error should contain 'already been initialized' message")
|
|
})
|
|
}
|
|
|
|
func TestScanIntervalRunnerWithMocking(t *testing.T) {
|
|
t.Run("runner calls queue on ticker events", func(t *testing.T) {
|
|
mockQueue := &MockScannerQueue{}
|
|
|
|
// Use a channel to synchronize and count calls
|
|
callChan := make(chan struct{}, 5) // Buffer for multiple calls
|
|
mockQueue.On("AddAllToQueue").Return(nil).Run(func(args mock.Arguments) {
|
|
select {
|
|
case callChan <- struct{}{}:
|
|
default:
|
|
// Channel full, but that's okay
|
|
}
|
|
}).Maybe()
|
|
|
|
ps := &periodicScanner{
|
|
ticker: time.NewTicker(50 * time.Millisecond),
|
|
ticker_changed: make(chan bool, 1),
|
|
done: make(chan struct{}),
|
|
tickerLocker: sync.Mutex{},
|
|
scannerQueue: mockQueue,
|
|
}
|
|
|
|
// Start runner
|
|
go ps.scanIntervalRunner()
|
|
|
|
// Wait for at least one call with timeout
|
|
select {
|
|
case <-callChan:
|
|
// Success - at least one call received
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("Expected at least one call to AddAllToQueue within 200ms timeout, but ticker events were not processed")
|
|
}
|
|
|
|
// Proper cleanup - stop ticker first, then close done
|
|
ps.ticker.Stop()
|
|
close(ps.done)
|
|
|
|
// Give time for goroutine to finish
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Verify the queue was called as expected
|
|
mockQueue.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("runner handles queue errors gracefully", func(t *testing.T) {
|
|
mockQueue := &MockScannerQueue{}
|
|
// Mock queue to return an error
|
|
mockQueue.On("AddAllToQueue").Return(errors.New("queue error")).Maybe()
|
|
|
|
ps := &periodicScanner{
|
|
ticker: time.NewTicker(30 * time.Millisecond),
|
|
ticker_changed: make(chan bool, 1),
|
|
done: make(chan struct{}),
|
|
tickerLocker: sync.Mutex{},
|
|
scannerQueue: mockQueue,
|
|
}
|
|
|
|
// Start runner and let it run briefly
|
|
go ps.scanIntervalRunner()
|
|
time.Sleep(80 * time.Millisecond)
|
|
|
|
// Proper cleanup
|
|
ps.ticker.Stop()
|
|
close(ps.done)
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Test passes if no panic occurred - errors should be logged gracefully
|
|
mockQueue.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("runner responds to shutdown signal", func(t *testing.T) {
|
|
mockQueue := &MockScannerQueue{}
|
|
|
|
ps := &periodicScanner{
|
|
ticker: nil, // No ticker to avoid timing issues
|
|
ticker_changed: make(chan bool, 1),
|
|
done: make(chan struct{}),
|
|
tickerLocker: sync.Mutex{},
|
|
scannerQueue: mockQueue,
|
|
}
|
|
|
|
runnerDone := make(chan bool)
|
|
go func() {
|
|
ps.scanIntervalRunner()
|
|
close(runnerDone)
|
|
}()
|
|
|
|
close(ps.done)
|
|
|
|
select {
|
|
case <-runnerDone:
|
|
// Success
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("scanIntervalRunner goroutine did not exit within 1 second after closing done channel")
|
|
}
|
|
|
|
mockQueue.AssertExpectations(t)
|
|
})
|
|
|
|
t.Run("runner responds to ticker changes", func(t *testing.T) {
|
|
mockQueue := &MockScannerQueue{}
|
|
|
|
ps := &periodicScanner{
|
|
ticker: nil, // Start without ticker
|
|
ticker_changed: make(chan bool, 1),
|
|
done: make(chan struct{}),
|
|
tickerLocker: sync.Mutex{},
|
|
scannerQueue: mockQueue,
|
|
}
|
|
|
|
runnerDone := make(chan bool)
|
|
go func() {
|
|
ps.scanIntervalRunner()
|
|
close(runnerDone)
|
|
}()
|
|
|
|
// Send ticker change signal
|
|
select {
|
|
case ps.ticker_changed <- true:
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("Could not send ticker change signal within 100ms - channel may be blocked")
|
|
}
|
|
|
|
// Clean shutdown
|
|
close(ps.done)
|
|
|
|
// Wait for completion
|
|
select {
|
|
case <-runnerDone:
|
|
// Success
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("scanIntervalRunner did not exit within 500ms after receiving ticker change signal and shutdown")
|
|
}
|
|
|
|
// Test passes if no deadlock occurs
|
|
mockQueue.AssertExpectations(t)
|
|
})
|
|
}
|
|
|
|
func TestRealScannerQueue(t *testing.T) {
|
|
t.Run("real scanner queue interface compliance", func(t *testing.T) {
|
|
queue := &RealScannerQueue{}
|
|
|
|
// Just verify it implements the interface correctly
|
|
var _ ScannerQueue = queue
|
|
|
|
// Test that it doesn't panic when created
|
|
assert.NotNil(t, queue, "RealScannerQueue instance should not be nil after creation")
|
|
|
|
// We don't test the actual call since it requires external setup
|
|
})
|
|
}
|