Skip to content

Commit 00aa3a2

Browse files
committed
Included tests for internal/signalio/csv
- Included tests for the functions `maybeWriteHeader()` and `marshalValue` in `internal/signalio/csv`. - Included additional comments for `maybeWriteHeader()`. Signed-off-by: nathannaveen <[email protected]>
1 parent 27ad7d2 commit 00aa3a2

File tree

2 files changed

+117
-4
lines changed

2 files changed

+117
-4
lines changed

internal/signalio/csv.go

+31-4
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,40 @@ func (w *csvWriter) WriteSignals(signals []signal.Set, extra ...Field) error {
5050
}
5151

5252
func (w *csvWriter) maybeWriteHeader() error {
53-
// Check headerWritten without the lock to avoid holding the lock if the
54-
// header has already been written.
53+
/*
54+
The variable w.headerWritten is checked twice to avoid what is known as a "race condition".
55+
A race condition can occur when two or more goroutines try to access a shared resource
56+
(in this case, the csvWriter instance) concurrently, and the outcome of the program depends on
57+
the interleaving of their execution.
58+
59+
Imagine the following scenario:
60+
61+
1. Goroutine A reads the value of w.headerWritten as false.
62+
2. Goroutine B reads the value of w.headerWritten as false.
63+
3. Goroutine A acquires the mutex lock and sets w.headerWritten to true.
64+
4. Goroutine B acquires the mutex lock and sets w.headerWritten to true.
65+
66+
If this happens, the header will be written twice, which is not the desired behavior.
67+
By checking w.headerWritten twice, once before acquiring the mutex lock and once after acquiring the lock,
68+
the function can ensure that only one goroutine enters the critical section and writes the header.
69+
70+
Here's how the function works:
71+
72+
1. Goroutine A reads the value of w.headerWritten as false.
73+
2. Goroutine A acquires the mutex lock.
74+
3. Goroutine A re-checks the value of w.headerWritten and finds it to be false.
75+
4. Goroutine A sets w.headerWritten to true and writes the header.
76+
5. Goroutine A releases the mutex lock.
77+
78+
If Goroutine B tries to enter the critical section at any point after step 2,
79+
it will have to wait until Goroutine A releases the lock in step 5. Once the lock is released,
80+
Goroutine B will re-check the value of w.headerWritten and find it to be true,
81+
so it will not write the header again.
82+
*/
83+
5584
if w.headerWritten {
5685
return nil
5786
}
58-
// Grab the lock and re-check headerWritten just in case another goroutine
59-
// entered the same critical section. Also prevent concurrent writes to w.
6087
w.mu.Lock()
6188
defer w.mu.Unlock()
6289
if w.headerWritten {

internal/signalio/csv_test.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package signalio
2+
3+
import (
4+
"encoding/csv"
5+
"sync"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestMarshalValue(t *testing.T) {
11+
tests := []struct {
12+
value any
13+
expected string
14+
wantErr bool
15+
}{
16+
{value: true, expected: "true", wantErr: false},
17+
{value: 1, expected: "1", wantErr: false},
18+
{value: int16(2), expected: "2", wantErr: false},
19+
{value: int32(3), expected: "3", wantErr: false},
20+
{value: int64(4), expected: "4", wantErr: false},
21+
{value: uint(5), expected: "5", wantErr: false},
22+
{value: uint16(6), expected: "6", wantErr: false},
23+
{value: uint32(7), expected: "7", wantErr: false},
24+
{value: uint64(8), expected: "8", wantErr: false},
25+
{value: byte(9), expected: "9", wantErr: false},
26+
{value: float32(10.1), expected: "10.1", wantErr: false},
27+
{value: 11.1, expected: "11.1", wantErr: false}, // float64
28+
{value: "test", expected: "test", wantErr: false},
29+
{value: time.Now(), expected: time.Now().Format(time.RFC3339), wantErr: false},
30+
{value: nil, expected: "", wantErr: false},
31+
{value: []int{1, 2, 3}, expected: "", wantErr: true},
32+
{value: map[string]string{"key": "value"}, expected: "", wantErr: true},
33+
{value: struct{}{}, expected: "", wantErr: true},
34+
}
35+
for _, test := range tests {
36+
res, err := marshalValue(test.value)
37+
if (err != nil) != test.wantErr {
38+
t.Errorf("Unexpected error for value %v: wantErr %v, got %v", test.value, test.wantErr, err)
39+
}
40+
if res != test.expected {
41+
t.Errorf("Unexpected result for value %v: expected %v, got %v", test.value, test.expected, res)
42+
}
43+
}
44+
}
45+
46+
func Test_csvWriter_maybeWriteHeader(t *testing.T) {
47+
type fields struct {
48+
w *csv.Writer
49+
header []string
50+
headerWritten bool
51+
}
52+
tests := []struct {
53+
name string
54+
fields fields
55+
}{
56+
{
57+
name: "write header",
58+
fields: fields{
59+
w: csv.NewWriter(nil),
60+
header: []string{},
61+
headerWritten: false,
62+
},
63+
},
64+
{
65+
name: "header already written",
66+
fields: fields{
67+
w: csv.NewWriter(nil),
68+
header: []string{"a", "b", "c"},
69+
headerWritten: true,
70+
},
71+
},
72+
}
73+
for _, test := range tests {
74+
t.Run(test.name, func(t *testing.T) {
75+
w := &csvWriter{
76+
w: test.fields.w,
77+
header: test.fields.header,
78+
headerWritten: test.fields.headerWritten,
79+
mu: sync.Mutex{},
80+
}
81+
if err := w.maybeWriteHeader(); err != nil { // never want an error
82+
t.Errorf("maybeWriteHeader() error = %v", err)
83+
}
84+
})
85+
}
86+
}

0 commit comments

Comments
 (0)