11package ffmpeg_test
22
33import (
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