v2.0.0 ae3e752

collector_test.go RawHistoryFolder

1package logging
2
3import (
4	"context"
5	"errors"
6	"io"
7	"strings"
8	"sync"
9	"testing"
10	"time"
11
12	"github.com/stretchr/testify/assert"
13	"github.com/stretchr/testify/require"
14	"go.uber.org/mock/gomock"
15	"gordon/internal/config"
16	"gordon/internal/testutils/mocks"
17)
18
19// mockLogStream implements io.ReadCloser for testing
20type mockLogStream struct {
21	data   string
22	closed bool
23	reader *strings.Reader
24	ctx    context.Context
25}
26
27func newMockLogStream(data string) *mockLogStream {
28	return &mockLogStream{
29		data:   data,
30		reader: strings.NewReader(data),
31	}
32}
33
34func newContextAwareMockLogStream(ctx context.Context) *mockLogStream {
35	return &mockLogStream{
36		data:   "",
37		reader: strings.NewReader(""),
38		ctx:    ctx,
39	}
40}
41
42func (m *mockLogStream) Read(p []byte) (n int, err error) {
43	if m.closed {
44		return 0, io.EOF
45	}
46	
47	// If we have a context, check if it's cancelled
48	if m.ctx != nil {
49		select {
50		case <-m.ctx.Done():
51			return 0, io.EOF
52		default:
53			// Block until context is cancelled
54			<-m.ctx.Done()
55			return 0, io.EOF
56		}
57	}
58	
59	return m.reader.Read(p)
60}
61
62func (m *mockLogStream) Close() error {
63	m.closed = true
64	return nil
65}
66
67func TestNewLogManager(t *testing.T) {
68	ctrl := gomock.NewController(t)
69	defer ctrl.Finish()
70
71	cfg := &config.Config{}
72	runtime := mocks.NewMockRuntime(ctrl)
73
74	manager := NewLogManager(cfg, runtime)
75
76	assert.NotNil(t, manager)
77	assert.NotNil(t, manager.collectors)
78	assert.Equal(t, cfg, manager.config)
79	assert.Equal(t, runtime, manager.runtime)
80	assert.Empty(t, manager.collectors)
81}
82
83func TestLogManager_StartCollection_LoggingDisabled(t *testing.T) {
84	ctrl := gomock.NewController(t)
85	defer ctrl.Finish()
86
87	cfg := &config.Config{
88		Logging: config.LoggingConfig{
89			Enabled: false,
90		},
91	}
92	runtime := mocks.NewMockRuntime(ctrl)
93	manager := NewLogManager(cfg, runtime)
94
95	err := manager.StartCollection("container123", "test-container")
96	assert.NoError(t, err)
97
98	// Should not create any collectors
99	assert.Equal(t, 0, manager.GetActiveCollectors())
100}
101
102func TestLogManager_StartCollection_Success(t *testing.T) {
103	ctrl := gomock.NewController(t)
104	defer ctrl.Finish()
105
106	tempDir := t.TempDir()
107	cfg := &config.Config{
108		Logging: config.LoggingConfig{
109			Enabled:           true,
110			Dir:               tempDir,
111			ContainerLogDir:   "containers",
112			MainLogFile:       "gordon.log",
113			ProxyLogFile:      "proxy.log",
114			Level:             "info",
115			MaxSize:           10,
116			MaxBackups:        3,
117			MaxAge:            7,
118			Compress:          false,
119		},
120	}
121
122	runtime := mocks.NewMockRuntime(ctrl)
123	manager := NewLogManager(cfg, runtime)
124
125	containerID := "container123"
126	containerName := "test-container"
127
128	err := manager.StartCollection(containerID, containerName)
129	require.NoError(t, err)
130
131	// Should create one collector
132	assert.Equal(t, 1, manager.GetActiveCollectors())
133
134	// Check collector exists
135	manager.mu.RLock()
136	collector, exists := manager.collectors[containerID]
137	manager.mu.RUnlock()
138
139	assert.True(t, exists)
140	assert.Equal(t, containerID, collector.containerID)
141	assert.Equal(t, containerName, collector.containerName)
142	assert.NotNil(t, collector.logWriter)
143	assert.NotNil(t, collector.ctx)
144	assert.NotNil(t, collector.cancel)
145
146	// Clean up
147	manager.StopAll()
148}
149
150func TestLogManager_StartCollection_ReplaceExisting(t *testing.T) {
151	ctrl := gomock.NewController(t)
152	defer ctrl.Finish()
153
154	tempDir := t.TempDir()
155	cfg := &config.Config{
156		Logging: config.LoggingConfig{
157			Enabled:           true,
158			Dir:               tempDir,
159			ContainerLogDir:   "containers",
160			MainLogFile:       "gordon.log",
161			ProxyLogFile:      "proxy.log",
162			Level:             "info",
163			MaxSize:           10,
164			MaxBackups:        3,
165			MaxAge:            7,
166			Compress:          false,
167		},
168	}
169
170	runtime := mocks.NewMockRuntime(ctrl)
171	manager := NewLogManager(cfg, runtime)
172
173	containerID := "container123"
174
175	// Start first collector
176	err := manager.StartCollection(containerID, "first-container")
177	require.NoError(t, err)
178	assert.Equal(t, 1, manager.GetActiveCollectors())
179
180	// Get reference to first collector
181	manager.mu.RLock()
182	firstCollector := manager.collectors[containerID]
183	manager.mu.RUnlock()
184
185	// Start second collector with same ID (should replace)
186	err = manager.StartCollection(containerID, "second-container")
187	require.NoError(t, err)
188	assert.Equal(t, 1, manager.GetActiveCollectors())
189
190	// Get reference to second collector
191	manager.mu.RLock()
192	secondCollector := manager.collectors[containerID]
193	manager.mu.RUnlock()
194
195	// Should be different collectors
196	assert.NotEqual(t, firstCollector, secondCollector)
197	assert.Equal(t, "second-container", secondCollector.containerName)
198
199	// Clean up
200	manager.StopAll()
201}
202
203func TestLogManager_StopCollection(t *testing.T) {
204	ctrl := gomock.NewController(t)
205	defer ctrl.Finish()
206
207	tempDir := t.TempDir()
208	cfg := &config.Config{
209		Logging: config.LoggingConfig{
210			Enabled:           true,
211			Dir:               tempDir,
212			ContainerLogDir:   "containers",
213			MainLogFile:       "gordon.log",
214			ProxyLogFile:      "proxy.log",
215			Level:             "info",
216			MaxSize:           10,
217			MaxBackups:        3,
218			MaxAge:            7,
219			Compress:          false,
220		},
221	}
222
223	runtime := mocks.NewMockRuntime(ctrl)
224	manager := NewLogManager(cfg, runtime)
225
226	containerID := "container123"
227
228	// Start collector
229	err := manager.StartCollection(containerID, "test-container")
230	require.NoError(t, err)
231	assert.Equal(t, 1, manager.GetActiveCollectors())
232
233	// Stop collector
234	manager.StopCollection(containerID)
235	assert.Equal(t, 0, manager.GetActiveCollectors())
236
237	// Stopping non-existent collector should not panic
238	manager.StopCollection("non-existent")
239	assert.Equal(t, 0, manager.GetActiveCollectors())
240}
241
242func TestLogManager_StopAll(t *testing.T) {
243	ctrl := gomock.NewController(t)
244	defer ctrl.Finish()
245
246	tempDir := t.TempDir()
247	cfg := &config.Config{
248		Logging: config.LoggingConfig{
249			Enabled:           true,
250			Dir:               tempDir,
251			ContainerLogDir:   "containers",
252			MainLogFile:       "gordon.log",
253			ProxyLogFile:      "proxy.log",
254			Level:             "info",
255			MaxSize:           10,
256			MaxBackups:        3,
257			MaxAge:            7,
258			Compress:          false,
259		},
260	}
261
262	runtime := mocks.NewMockRuntime(ctrl)
263	
264	// Mock GetContainerLogs calls for each container - return context-aware streams
265	runtime.EXPECT().GetContainerLogs(gomock.Any(), "container1", true).DoAndReturn(
266		func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
267			return newContextAwareMockLogStream(ctx), nil
268		}).AnyTimes()
269	runtime.EXPECT().GetContainerLogs(gomock.Any(), "container2", true).DoAndReturn(
270		func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
271			return newContextAwareMockLogStream(ctx), nil
272		}).AnyTimes()
273	runtime.EXPECT().GetContainerLogs(gomock.Any(), "container3", true).DoAndReturn(
274		func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
275			return newContextAwareMockLogStream(ctx), nil
276		}).AnyTimes()
277	
278	manager := NewLogManager(cfg, runtime)
279
280	// Start multiple collectors
281	err := manager.StartCollection("container1", "test-container1")
282	require.NoError(t, err)
283	err = manager.StartCollection("container2", "test-container2")
284	require.NoError(t, err)
285	err = manager.StartCollection("container3", "test-container3")
286	require.NoError(t, err)
287
288	assert.Equal(t, 3, manager.GetActiveCollectors())
289
290	// Stop all collectors
291	manager.StopAll()
292	assert.Equal(t, 0, manager.GetActiveCollectors())
293
294	// Stopping again should be safe
295	manager.StopAll()
296	assert.Equal(t, 0, manager.GetActiveCollectors())
297}
298
299func TestLogManager_GetActiveCollectors(t *testing.T) {
300	ctrl := gomock.NewController(t)
301	defer ctrl.Finish()
302
303	cfg := &config.Config{
304		Logging: config.LoggingConfig{
305			Enabled: false,
306		},
307	}
308	runtime := mocks.NewMockRuntime(ctrl)
309	manager := NewLogManager(cfg, runtime)
310
311	// Initially no collectors
312	assert.Equal(t, 0, manager.GetActiveCollectors())
313
314	// Test concurrent access
315	var wg sync.WaitGroup
316	for i := 0; i < 10; i++ {
317		wg.Add(1)
318		go func() {
319			defer wg.Done()
320			count := manager.GetActiveCollectors()
321			assert.GreaterOrEqual(t, count, 0)
322		}()
323	}
324	wg.Wait()
325}
326
327func TestLogCollector_Stop(t *testing.T) {
328	tempDir := t.TempDir()
329	cfg := &config.Config{
330		Logging: config.LoggingConfig{
331			Enabled:           true,
332			Dir:               tempDir,
333			ContainerLogDir:   "containers",
334			MaxSize:           10,
335			MaxBackups:        3,
336			MaxAge:            7,
337			Compress:          false,
338		},
339	}
340
341	logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
342	require.NoError(t, err)
343
344	ctrl := gomock.NewController(t)
345	defer ctrl.Finish()
346
347	ctx, cancel := context.WithCancel(context.Background())
348	collector := &LogCollector{
349		containerID:   "test123",
350		containerName: "test-container",
351		runtime:       mocks.NewMockRuntime(ctrl),
352		logWriter:     logWriter,
353		ctx:           ctx,
354		cancel:        cancel,
355	}
356
357	// Test that Stop doesn't panic
358	assert.NotPanics(t, func() {
359		collector.Stop()
360	})
361
362	// Test that Stop can be called multiple times
363	assert.NotPanics(t, func() {
364		collector.Stop()
365	})
366}
367
368func TestLogCollector_CollectLogsOnce_Success(t *testing.T) {
369	ctrl := gomock.NewController(t)
370	defer ctrl.Finish()
371
372	tempDir := t.TempDir()
373	cfg := &config.Config{
374		Logging: config.LoggingConfig{
375			Enabled:           true,
376			Dir:               tempDir,
377			ContainerLogDir:   "containers",
378			MaxSize:           10,
379			MaxBackups:        3,
380			MaxAge:            7,
381			Compress:          false,
382		},
383	}
384
385	logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
386	require.NoError(t, err)
387
388	// Create mock runtime that returns log stream
389	logData := "line 1\nline 2\nline 3"
390	mockStream := newMockLogStream(logData)
391	
392	runtime := mocks.NewMockRuntime(ctrl)
393	runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).Return(mockStream, nil)
394
395	ctx, cancel := context.WithCancel(context.Background())
396	defer cancel()
397
398	collector := &LogCollector{
399		containerID:   "test123",
400		containerName: "test-container",
401		runtime:       runtime,
402		logWriter:     logWriter,
403		ctx:           ctx,
404		cancel:        cancel,
405	}
406
407	err = collector.collectLogsOnce()
408	assert.NoError(t, err)
409	assert.True(t, mockStream.closed)
410}
411
412func TestLogCollector_CollectLogsOnce_Error(t *testing.T) {
413	ctrl := gomock.NewController(t)
414	defer ctrl.Finish()
415
416	tempDir := t.TempDir()
417	cfg := &config.Config{
418		Logging: config.LoggingConfig{
419			Enabled:           true,
420			Dir:               tempDir,
421			ContainerLogDir:   "containers",
422			MaxSize:           10,
423			MaxBackups:        3,
424			MaxAge:            7,
425			Compress:          false,
426		},
427	}
428
429	logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
430	require.NoError(t, err)
431
432	// Create mock runtime that returns error
433	runtime := mocks.NewMockRuntime(ctrl)
434	runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).Return(nil, errors.New("failed to get logs"))
435
436	ctx, cancel := context.WithCancel(context.Background())
437	defer cancel()
438
439	collector := &LogCollector{
440		containerID:   "test123",
441		containerName: "test-container",
442		runtime:       runtime,
443		logWriter:     logWriter,
444		ctx:           ctx,
445		cancel:        cancel,
446	}
447
448	err = collector.collectLogsOnce()
449	assert.Error(t, err)
450	assert.Contains(t, err.Error(), "failed to get log stream")
451}
452
453func TestLogCollector_CollectLogsOnce_ContextCancelled(t *testing.T) {
454	ctrl := gomock.NewController(t)
455	defer ctrl.Finish()
456
457	tempDir := t.TempDir()
458	cfg := &config.Config{
459		Logging: config.LoggingConfig{
460			Enabled:           true,
461			Dir:               tempDir,
462			ContainerLogDir:   "containers",
463			MaxSize:           10,
464			MaxBackups:        3,
465			MaxAge:            7,
466			Compress:          false,
467		},
468	}
469
470	logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
471	require.NoError(t, err)
472
473	// Create mock stream that never ends
474	mockStream := newMockLogStream("line 1\nline 2\n")
475	
476	runtime := mocks.NewMockRuntime(ctrl)
477	runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).Return(mockStream, nil)
478
479	ctx, cancel := context.WithCancel(context.Background())
480	
481	collector := &LogCollector{
482		containerID:   "test123",
483		containerName: "test-container",
484		runtime:       runtime,
485		logWriter:     logWriter,
486		ctx:           ctx,
487		cancel:        cancel,
488	}
489
490	// Cancel context immediately
491	cancel()
492
493	err = collector.collectLogsOnce()
494	assert.NoError(t, err) // Should return nil when context is cancelled
495}
496
497func TestLogCollector_CollectLogs_RetryLogic(t *testing.T) {
498	ctrl := gomock.NewController(t)
499	defer ctrl.Finish()
500
501	tempDir := t.TempDir()
502	cfg := &config.Config{
503		Logging: config.LoggingConfig{
504			Enabled:           true,
505			Dir:               tempDir,
506			ContainerLogDir:   "containers",
507			MaxSize:           10,
508			MaxBackups:        3,
509			MaxAge:            7,
510			Compress:          false,
511		},
512	}
513
514	logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
515	require.NoError(t, err)
516
517	// Create mock runtime that fails first time, succeeds second time
518	runtime := mocks.NewMockRuntime(ctrl)
519	runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).
520		Return(nil, errors.New("temporary failure")).Times(1)
521	runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).DoAndReturn(
522		func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
523			return newContextAwareMockLogStream(ctx), nil
524		}).AnyTimes()
525
526	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
527	defer cancel()
528
529	collector := &LogCollector{
530		containerID:   "test123",
531		containerName: "test-container",
532		runtime:       runtime,
533		logWriter:     logWriter,
534		ctx:           ctx,
535		cancel:        cancel,
536	}
537
538	// Start collection in goroutine
539	collector.wg.Add(1)
540	go collector.collectLogs()
541
542	// Wait a bit for retry to happen
543	time.Sleep(1500 * time.Millisecond)
544	
545	// Cancel and wait for completion
546	cancel()
547	collector.wg.Wait()
548
549	// Test passes if no panic occurs and the mocks were called as expected
550}
551
552func TestLogCollector_CollectLogs_ContextCancellation(t *testing.T) {
553	ctrl := gomock.NewController(t)
554	defer ctrl.Finish()
555
556	tempDir := t.TempDir()
557	cfg := &config.Config{
558		Logging: config.LoggingConfig{
559			Enabled:           true,
560			Dir:               tempDir,
561			ContainerLogDir:   "containers",
562			MaxSize:           10,
563			MaxBackups:        3,
564			MaxAge:            7,
565			Compress:          false,
566		},
567	}
568
569	logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
570	require.NoError(t, err)
571
572	runtime := mocks.NewMockRuntime(ctrl)
573	// Should not be called if context is cancelled immediately
574
575	ctx, cancel := context.WithCancel(context.Background())
576
577	collector := &LogCollector{
578		containerID:   "test123",
579		containerName: "test-container",
580		runtime:       runtime,
581		logWriter:     logWriter,
582		ctx:           ctx,
583		cancel:        cancel,
584	}
585
586	// Cancel context immediately
587	cancel()
588
589	// Start collection
590	collector.wg.Add(1)
591	go collector.collectLogs()
592
593	// Wait for completion
594	collector.wg.Wait()
595
596	// Test passes if no panic occurs and goroutine exits cleanly
597}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close