308 lines
6.4 KiB
Go
308 lines
6.4 KiB
Go
|
|
package jobrunner
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
)
|
||
|
|
|
||
|
|
// --- Mock source ---
|
||
|
|
|
||
|
|
type mockSource struct {
|
||
|
|
name string
|
||
|
|
signals []*PipelineSignal
|
||
|
|
reports []*ActionResult
|
||
|
|
mu sync.Mutex
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockSource) Name() string { return m.name }
|
||
|
|
|
||
|
|
func (m *mockSource) Poll(_ context.Context) ([]*PipelineSignal, error) {
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
return m.signals, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockSource) Report(_ context.Context, result *ActionResult) error {
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
m.reports = append(m.reports, result)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Mock handler ---
|
||
|
|
|
||
|
|
type mockHandler struct {
|
||
|
|
name string
|
||
|
|
matchFn func(*PipelineSignal) bool
|
||
|
|
executed []*PipelineSignal
|
||
|
|
mu sync.Mutex
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockHandler) Name() string { return m.name }
|
||
|
|
|
||
|
|
func (m *mockHandler) Match(sig *PipelineSignal) bool {
|
||
|
|
if m.matchFn != nil {
|
||
|
|
return m.matchFn(sig)
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockHandler) Execute(_ context.Context, sig *PipelineSignal) (*ActionResult, error) {
|
||
|
|
m.mu.Lock()
|
||
|
|
defer m.mu.Unlock()
|
||
|
|
m.executed = append(m.executed, sig)
|
||
|
|
return &ActionResult{
|
||
|
|
Action: m.name,
|
||
|
|
RepoOwner: sig.RepoOwner,
|
||
|
|
RepoName: sig.RepoName,
|
||
|
|
PRNumber: sig.PRNumber,
|
||
|
|
Success: true,
|
||
|
|
Timestamp: time.Now(),
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPoller_RunOnce_Good(t *testing.T) {
|
||
|
|
sig := &PipelineSignal{
|
||
|
|
EpicNumber: 1,
|
||
|
|
ChildNumber: 2,
|
||
|
|
PRNumber: 10,
|
||
|
|
RepoOwner: "host-uk",
|
||
|
|
RepoName: "core-php",
|
||
|
|
PRState: "OPEN",
|
||
|
|
CheckStatus: "SUCCESS",
|
||
|
|
Mergeable: "MERGEABLE",
|
||
|
|
}
|
||
|
|
|
||
|
|
src := &mockSource{
|
||
|
|
name: "test-source",
|
||
|
|
signals: []*PipelineSignal{sig},
|
||
|
|
}
|
||
|
|
|
||
|
|
handler := &mockHandler{
|
||
|
|
name: "test-handler",
|
||
|
|
matchFn: func(s *PipelineSignal) bool {
|
||
|
|
return s.PRNumber == 10
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
p := NewPoller(PollerConfig{
|
||
|
|
Sources: []JobSource{src},
|
||
|
|
Handlers: []JobHandler{handler},
|
||
|
|
})
|
||
|
|
|
||
|
|
err := p.RunOnce(context.Background())
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
// Handler should have been called with our signal.
|
||
|
|
handler.mu.Lock()
|
||
|
|
defer handler.mu.Unlock()
|
||
|
|
require.Len(t, handler.executed, 1)
|
||
|
|
assert.Equal(t, 10, handler.executed[0].PRNumber)
|
||
|
|
|
||
|
|
// Source should have received a report.
|
||
|
|
src.mu.Lock()
|
||
|
|
defer src.mu.Unlock()
|
||
|
|
require.Len(t, src.reports, 1)
|
||
|
|
assert.Equal(t, "test-handler", src.reports[0].Action)
|
||
|
|
assert.True(t, src.reports[0].Success)
|
||
|
|
assert.Equal(t, 1, src.reports[0].Cycle)
|
||
|
|
assert.Equal(t, 1, src.reports[0].EpicNumber)
|
||
|
|
assert.Equal(t, 2, src.reports[0].ChildNumber)
|
||
|
|
|
||
|
|
// Cycle counter should have incremented.
|
||
|
|
assert.Equal(t, 1, p.Cycle())
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPoller_RunOnce_Good_NoSignals(t *testing.T) {
|
||
|
|
src := &mockSource{
|
||
|
|
name: "empty-source",
|
||
|
|
signals: nil,
|
||
|
|
}
|
||
|
|
|
||
|
|
handler := &mockHandler{
|
||
|
|
name: "unused-handler",
|
||
|
|
}
|
||
|
|
|
||
|
|
p := NewPoller(PollerConfig{
|
||
|
|
Sources: []JobSource{src},
|
||
|
|
Handlers: []JobHandler{handler},
|
||
|
|
})
|
||
|
|
|
||
|
|
err := p.RunOnce(context.Background())
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
// Handler should not have been called.
|
||
|
|
handler.mu.Lock()
|
||
|
|
defer handler.mu.Unlock()
|
||
|
|
assert.Empty(t, handler.executed)
|
||
|
|
|
||
|
|
// Source should not have received reports.
|
||
|
|
src.mu.Lock()
|
||
|
|
defer src.mu.Unlock()
|
||
|
|
assert.Empty(t, src.reports)
|
||
|
|
|
||
|
|
assert.Equal(t, 1, p.Cycle())
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPoller_RunOnce_Good_NoMatchingHandler(t *testing.T) {
|
||
|
|
sig := &PipelineSignal{
|
||
|
|
EpicNumber: 5,
|
||
|
|
ChildNumber: 8,
|
||
|
|
PRNumber: 42,
|
||
|
|
RepoOwner: "host-uk",
|
||
|
|
RepoName: "core-tenant",
|
||
|
|
PRState: "OPEN",
|
||
|
|
}
|
||
|
|
|
||
|
|
src := &mockSource{
|
||
|
|
name: "test-source",
|
||
|
|
signals: []*PipelineSignal{sig},
|
||
|
|
}
|
||
|
|
|
||
|
|
handler := &mockHandler{
|
||
|
|
name: "picky-handler",
|
||
|
|
matchFn: func(s *PipelineSignal) bool {
|
||
|
|
return false // never matches
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
p := NewPoller(PollerConfig{
|
||
|
|
Sources: []JobSource{src},
|
||
|
|
Handlers: []JobHandler{handler},
|
||
|
|
})
|
||
|
|
|
||
|
|
err := p.RunOnce(context.Background())
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
// Handler should not have been called.
|
||
|
|
handler.mu.Lock()
|
||
|
|
defer handler.mu.Unlock()
|
||
|
|
assert.Empty(t, handler.executed)
|
||
|
|
|
||
|
|
// Source should not have received reports (no action taken).
|
||
|
|
src.mu.Lock()
|
||
|
|
defer src.mu.Unlock()
|
||
|
|
assert.Empty(t, src.reports)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPoller_RunOnce_Good_DryRun(t *testing.T) {
|
||
|
|
sig := &PipelineSignal{
|
||
|
|
EpicNumber: 1,
|
||
|
|
ChildNumber: 3,
|
||
|
|
PRNumber: 20,
|
||
|
|
RepoOwner: "host-uk",
|
||
|
|
RepoName: "core-admin",
|
||
|
|
PRState: "OPEN",
|
||
|
|
CheckStatus: "SUCCESS",
|
||
|
|
Mergeable: "MERGEABLE",
|
||
|
|
}
|
||
|
|
|
||
|
|
src := &mockSource{
|
||
|
|
name: "test-source",
|
||
|
|
signals: []*PipelineSignal{sig},
|
||
|
|
}
|
||
|
|
|
||
|
|
handler := &mockHandler{
|
||
|
|
name: "merge-handler",
|
||
|
|
matchFn: func(s *PipelineSignal) bool {
|
||
|
|
return true
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
p := NewPoller(PollerConfig{
|
||
|
|
Sources: []JobSource{src},
|
||
|
|
Handlers: []JobHandler{handler},
|
||
|
|
DryRun: true,
|
||
|
|
})
|
||
|
|
|
||
|
|
assert.True(t, p.DryRun())
|
||
|
|
|
||
|
|
err := p.RunOnce(context.Background())
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
// Handler should NOT have been called in dry-run mode.
|
||
|
|
handler.mu.Lock()
|
||
|
|
defer handler.mu.Unlock()
|
||
|
|
assert.Empty(t, handler.executed)
|
||
|
|
|
||
|
|
// Source should not have received reports.
|
||
|
|
src.mu.Lock()
|
||
|
|
defer src.mu.Unlock()
|
||
|
|
assert.Empty(t, src.reports)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPoller_SetDryRun_Good(t *testing.T) {
|
||
|
|
p := NewPoller(PollerConfig{})
|
||
|
|
|
||
|
|
assert.False(t, p.DryRun())
|
||
|
|
p.SetDryRun(true)
|
||
|
|
assert.True(t, p.DryRun())
|
||
|
|
p.SetDryRun(false)
|
||
|
|
assert.False(t, p.DryRun())
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPoller_AddSourceAndHandler_Good(t *testing.T) {
|
||
|
|
p := NewPoller(PollerConfig{})
|
||
|
|
|
||
|
|
sig := &PipelineSignal{
|
||
|
|
EpicNumber: 1,
|
||
|
|
ChildNumber: 1,
|
||
|
|
PRNumber: 5,
|
||
|
|
RepoOwner: "host-uk",
|
||
|
|
RepoName: "core-php",
|
||
|
|
PRState: "OPEN",
|
||
|
|
}
|
||
|
|
|
||
|
|
src := &mockSource{
|
||
|
|
name: "added-source",
|
||
|
|
signals: []*PipelineSignal{sig},
|
||
|
|
}
|
||
|
|
|
||
|
|
handler := &mockHandler{
|
||
|
|
name: "added-handler",
|
||
|
|
matchFn: func(s *PipelineSignal) bool { return true },
|
||
|
|
}
|
||
|
|
|
||
|
|
p.AddSource(src)
|
||
|
|
p.AddHandler(handler)
|
||
|
|
|
||
|
|
err := p.RunOnce(context.Background())
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
handler.mu.Lock()
|
||
|
|
defer handler.mu.Unlock()
|
||
|
|
require.Len(t, handler.executed, 1)
|
||
|
|
assert.Equal(t, 5, handler.executed[0].PRNumber)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPoller_Run_Good(t *testing.T) {
|
||
|
|
src := &mockSource{
|
||
|
|
name: "tick-source",
|
||
|
|
signals: nil,
|
||
|
|
}
|
||
|
|
|
||
|
|
p := NewPoller(PollerConfig{
|
||
|
|
Sources: []JobSource{src},
|
||
|
|
PollInterval: 50 * time.Millisecond,
|
||
|
|
})
|
||
|
|
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Millisecond)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
err := p.Run(ctx)
|
||
|
|
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||
|
|
|
||
|
|
// Should have completed at least 2 cycles (one immediate + at least one tick).
|
||
|
|
assert.GreaterOrEqual(t, p.Cycle(), 2)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPoller_DefaultInterval_Good(t *testing.T) {
|
||
|
|
p := NewPoller(PollerConfig{})
|
||
|
|
assert.Equal(t, 60*time.Second, p.interval)
|
||
|
|
}
|