Skip to content

Commit 5d2a303

Browse files
committed
test: add CStr memory management tests with race detection
- Add TestGlobalCStr_ConcurrentAccess verifying thread-safe map access: - Concurrent reads of same key return identical instance - Concurrent writes of different keys work safely - Mixed reads/writes with key overlap - Add TestCStr_DoubleFreeProtection verifying dontFree flag: - GlobalCStr Free() is a no-op - Instance persists after Free() attempt - Regular AllocCStr and ToCStr remain freeable - Add TestCStr_BasicOperations for content preservation - All tests pass with -race flag enabled
1 parent f2f2dff commit 5d2a303

1 file changed

Lines changed: 219 additions & 0 deletions

File tree

ffmpeg_test.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package ffmpeg_test
22

33
import (
4+
"fmt"
5+
"sync"
46
"testing"
57

68
"github.com/linuxmatters/ffmpeg-statigo"
@@ -12,3 +14,220 @@ func TestVersions(t *testing.T) {
1214
assert.Equal(t, 4066148, int(ffmpeg.AVCodecVersion()), "AVCodec version should match expected")
1315
assert.Equal(t, ffmpeg.LIBAVCodecVersionInt, int(ffmpeg.AVCodecVersion()), "AVCodec version func and const should match")
1416
}
17+
18+
// =============================================================================
19+
// Test 2.1: GlobalCStr Thread Safety
20+
// =============================================================================
21+
22+
// TestGlobalCStr_ConcurrentAccess verifies that GlobalCStr is race-safe under
23+
// concurrent access. This test should be run with -race flag to detect data races.
24+
func TestGlobalCStr_ConcurrentAccess(t *testing.T) {
25+
t.Run("concurrent_reads_same_key", func(t *testing.T) {
26+
// Pre-populate a key
27+
key := "concurrent_read_test_key"
28+
initial := ffmpeg.GlobalCStr(key)
29+
30+
var wg sync.WaitGroup
31+
const numGoroutines = 100
32+
33+
results := make([]*ffmpeg.CStr, numGoroutines)
34+
35+
for i := 0; i < numGoroutines; i++ {
36+
wg.Add(1)
37+
go func(idx int) {
38+
defer wg.Done()
39+
results[idx] = ffmpeg.GlobalCStr(key)
40+
}(i)
41+
}
42+
43+
wg.Wait()
44+
45+
// All goroutines should get the same pointer
46+
for i, result := range results {
47+
if result != initial {
48+
t.Errorf("Goroutine %d got different CStr instance", i)
49+
}
50+
}
51+
})
52+
53+
t.Run("concurrent_writes_different_keys", func(t *testing.T) {
54+
var wg sync.WaitGroup
55+
const numGoroutines = 100
56+
57+
// Each goroutine writes a unique key
58+
for i := 0; i < numGoroutines; i++ {
59+
wg.Add(1)
60+
go func(idx int) {
61+
defer wg.Done()
62+
key := fmt.Sprintf("unique_key_%d", idx)
63+
result := ffmpeg.GlobalCStr(key)
64+
if result == nil {
65+
t.Errorf("GlobalCStr returned nil for key %s", key)
66+
}
67+
if result.String() != key {
68+
t.Errorf("GlobalCStr value mismatch: expected %s, got %s", key, result.String())
69+
}
70+
}(i)
71+
}
72+
73+
wg.Wait()
74+
})
75+
76+
t.Run("concurrent_mixed_reads_writes", func(t *testing.T) {
77+
var wg sync.WaitGroup
78+
const numGoroutines = 100
79+
const numKeys = 10
80+
81+
// Multiple goroutines read and write overlapping keys
82+
for i := 0; i < numGoroutines; i++ {
83+
wg.Add(1)
84+
go func(idx int) {
85+
defer wg.Done()
86+
// Use modulo to create key overlap
87+
key := fmt.Sprintf("mixed_key_%d", idx%numKeys)
88+
result := ffmpeg.GlobalCStr(key)
89+
if result == nil {
90+
t.Errorf("GlobalCStr returned nil for key %s", key)
91+
}
92+
}(i)
93+
}
94+
95+
wg.Wait()
96+
})
97+
98+
t.Run("same_key_returns_same_instance", func(t *testing.T) {
99+
key := "identity_test_key"
100+
first := ffmpeg.GlobalCStr(key)
101+
second := ffmpeg.GlobalCStr(key)
102+
third := ffmpeg.GlobalCStr(key)
103+
104+
if first != second || second != third {
105+
t.Error("GlobalCStr should return the same instance for the same key")
106+
}
107+
})
108+
}
109+
110+
// =============================================================================
111+
// Test 2.2: CStr Double-Free Protection
112+
// =============================================================================
113+
114+
// TestCStr_DoubleFreeProtection verifies that CStr instances from GlobalCStr
115+
// are protected from being freed (dontFree flag), preventing memory corruption.
116+
func TestCStr_DoubleFreeProtection(t *testing.T) {
117+
t.Run("globalcstr_free_is_noop", func(t *testing.T) {
118+
key := "free_test_key"
119+
cstr := ffmpeg.GlobalCStr(key)
120+
121+
// This should be a no-op due to dontFree flag
122+
cstr.Free()
123+
124+
// Should still be accessible after Free()
125+
if cstr.String() != key {
126+
t.Errorf("GlobalCStr string changed after Free(): expected %s, got %s", key, cstr.String())
127+
}
128+
129+
// Calling Free() multiple times should also be safe
130+
cstr.Free()
131+
cstr.Free()
132+
133+
// Still accessible
134+
if cstr.String() != key {
135+
t.Errorf("GlobalCStr string corrupted after multiple Free() calls")
136+
}
137+
})
138+
139+
t.Run("globalcstr_same_after_free_attempt", func(t *testing.T) {
140+
key := "persistence_test_key"
141+
cstr1 := ffmpeg.GlobalCStr(key)
142+
cstr1.Free() // Should be no-op
143+
144+
// Getting the same key should return the same instance
145+
cstr2 := ffmpeg.GlobalCStr(key)
146+
if cstr1 != cstr2 {
147+
t.Error("GlobalCStr returned different instance after Free() attempt")
148+
}
149+
})
150+
151+
t.Run("allocated_cstr_can_be_freed", func(t *testing.T) {
152+
// Regular allocated CStr should be freeable
153+
cstr := ffmpeg.AllocCStr(64)
154+
if cstr == nil {
155+
t.Fatal("AllocCStr returned nil")
156+
}
157+
158+
// Should not panic
159+
cstr.Free()
160+
})
161+
162+
t.Run("tocstr_can_be_freed", func(t *testing.T) {
163+
// ToCStr creates a freeable CStr
164+
cstr := ffmpeg.ToCStr("freeable_string")
165+
if cstr == nil {
166+
t.Fatal("ToCStr returned nil")
167+
}
168+
169+
// Store the value before freeing
170+
val := cstr.String()
171+
if val != "freeable_string" {
172+
t.Errorf("ToCStr value mismatch: expected freeable_string, got %s", val)
173+
}
174+
175+
// Should not panic
176+
cstr.Free()
177+
})
178+
}
179+
180+
// TestCStr_BasicOperations verifies basic CStr functionality.
181+
func TestCStr_BasicOperations(t *testing.T) {
182+
t.Run("alloc_creates_zeroed_buffer", func(t *testing.T) {
183+
cstr := ffmpeg.AllocCStr(10)
184+
defer cstr.Free()
185+
186+
// Should be empty string (zeroed buffer)
187+
if cstr.String() != "" {
188+
t.Errorf("AllocCStr should create empty string, got %q", cstr.String())
189+
}
190+
})
191+
192+
t.Run("tocstr_preserves_content", func(t *testing.T) {
193+
testCases := []string{
194+
"simple",
195+
"with spaces",
196+
"unicode: 日本語",
197+
"", // empty string
198+
}
199+
200+
for _, tc := range testCases {
201+
cstr := ffmpeg.ToCStr(tc)
202+
defer cstr.Free()
203+
204+
if cstr.String() != tc {
205+
t.Errorf("ToCStr content mismatch: expected %q, got %q", tc, cstr.String())
206+
}
207+
}
208+
})
209+
210+
t.Run("globalcstr_preserves_content", func(t *testing.T) {
211+
testCases := []string{
212+
"global_simple",
213+
"global with spaces",
214+
"global_unicode: 日本語",
215+
}
216+
217+
for _, tc := range testCases {
218+
cstr := ffmpeg.GlobalCStr(tc)
219+
if cstr.String() != tc {
220+
t.Errorf("GlobalCStr content mismatch: expected %q, got %q", tc, cstr.String())
221+
}
222+
}
223+
})
224+
225+
t.Run("rawptr_not_nil", func(t *testing.T) {
226+
cstr := ffmpeg.ToCStr("test")
227+
defer cstr.Free()
228+
229+
if cstr.RawPtr() == nil {
230+
t.Error("RawPtr should not return nil for valid CStr")
231+
}
232+
})
233+
}

0 commit comments

Comments
 (0)