1 // Copyright (c) 2020-2025 Uber Technologies, Inc.
2 //
3 // Permission is hereby granted, free of charge, to any person obtaining a copy
4 // of this software and associated documentation files (the "Software"), to deal
5 // in the Software without restriction, including without limitation the rights
6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 // copies of the Software, and to permit persons to whom the Software is
8 // furnished to do so, subject to the following conditions:
9 //
10 // The above copyright notice and this permission notice shall be included in
11 // all copies or substantial portions of the Software.
12 //
13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 // THE SOFTWARE.
20 21 package atomic
22 23 import (
24 "encoding/json"
25 "next.orly.dev/pkg/utils"
26 "runtime"
27 "sync"
28 "testing"
29 30 "github.com/stretchr/testify/require"
31 )
32 33 func TestBytesNoInitialValue(t *testing.T) {
34 atom := NewBytes([]byte{})
35 require.Equal(t, []byte{}, atom.Load(), "Initial value should be empty")
36 }
37 38 func TestBytes(t *testing.T) {
39 atom := NewBytes([]byte{})
40 require.Equal(
41 t, []byte{}, atom.Load(),
42 "Expected Load to return initialized empty value",
43 )
44 45 emptyBytes := []byte{}
46 atom = NewBytes(emptyBytes)
47 require.Equal(
48 t, emptyBytes, atom.Load(),
49 "Expected Load to return initialized empty value",
50 )
51 52 testBytes := []byte("test data")
53 atom = NewBytes(testBytes)
54 loadedBytes := atom.Load()
55 require.Equal(
56 t, testBytes, loadedBytes, "Expected Load to return initialized value",
57 )
58 59 // Verify that the returned value is a copy
60 loadedBytes[0] = 'X'
61 require.NotEqual(
62 t, loadedBytes, atom.Load(), "Load should return a copy of the data",
63 )
64 65 // Store and verify
66 newBytes := []byte("new data")
67 atom.Store(newBytes)
68 require.Equal(t, newBytes, atom.Load(), "Unexpected value after Store")
69 70 // Modify original data and verify it doesn't affect stored value
71 newBytes[0] = 'X'
72 require.NotEqual(t, newBytes, atom.Load(), "Store should copy the data")
73 74 t.Run(
75 "JSON/Marshal", func(t *testing.T) {
76 jsonBytes := []byte("json data")
77 atom.Store(jsonBytes)
78 bytes, err := json.Marshal(atom)
79 require.NoError(t, err, "json.Marshal errored unexpectedly.")
80 require.Equal(
81 t, []byte(`"anNvbiBkYXRh"`), bytes,
82 "json.Marshal should encode as base64",
83 )
84 },
85 )
86 87 t.Run(
88 "JSON/Unmarshal", func(t *testing.T) {
89 err := json.Unmarshal(
90 []byte(`"dGVzdCBkYXRh"`), &atom,
91 ) // "test data" in base64
92 require.NoError(t, err, "json.Unmarshal errored unexpectedly.")
93 require.Equal(
94 t, []byte("test data"), atom.Load(),
95 "json.Unmarshal didn't set the correct value.",
96 )
97 },
98 )
99 100 t.Run(
101 "JSON/Unmarshal/Error", func(t *testing.T) {
102 err := json.Unmarshal([]byte("42"), &atom)
103 require.Error(t, err, "json.Unmarshal didn't error as expected.")
104 },
105 )
106 }
107 108 func TestBytesConcurrentAccess(t *testing.T) {
109 const (
110 parallelism = 4
111 iterations = 1000
112 )
113 114 atom := NewBytes([]byte("initial"))
115 116 var wg sync.WaitGroup
117 wg.Add(parallelism)
118 119 // Start multiple goroutines that read and write concurrently
120 for i := 0; i < parallelism; i++ {
121 go func(id int) {
122 defer wg.Done()
123 124 // Each goroutine writes a different value
125 myData := []byte{byte(id)}
126 127 for j := 0; j < iterations; j++ {
128 // Store our data
129 atom.Store(myData)
130 131 // Load the data (which might be from another goroutine)
132 loaded := atom.Load()
133 134 // Verify the loaded data is valid (either our data or another goroutine's data)
135 require.LessOrEqual(
136 t, len(loaded), parallelism,
137 "Loaded data length should not exceed parallelism",
138 )
139 140 // If it's our data, verify it's correct
141 if len(loaded) == 1 && loaded[0] == byte(id) {
142 require.Equal(t, myData, loaded, "Data corruption detected")
143 }
144 }
145 }(i)
146 }
147 148 wg.Wait()
149 }
150 151 func TestBytesDataIntegrity(t *testing.T) {
152 // Test that large byte slices maintain integrity under concurrent access
153 const (
154 parallelism = 4
155 dataSize = 1024 // 1KB
156 iterations = 100
157 )
158 159 // Create test data sets, each with a unique pattern
160 testData := make([][]byte, parallelism)
161 for i := 0; i < parallelism; i++ {
162 testData[i] = make([]byte, dataSize)
163 for j := 0; j < dataSize; j++ {
164 testData[i][j] = byte((i + j) % 256)
165 }
166 }
167 168 atom := NewBytes(nil)
169 var wg sync.WaitGroup
170 wg.Add(parallelism)
171 172 for i := 0; i < parallelism; i++ {
173 go func(id int) {
174 defer wg.Done()
175 myData := testData[id]
176 177 for j := 0; j < iterations; j++ {
178 atom.Store(myData)
179 loaded := atom.Load()
180 181 // Verify the loaded data is one of our test data sets
182 for k := 0; k < parallelism; k++ {
183 if utils.FastEqual(loaded, testData[k]) {
184 // Found a match, data is intact
185 break
186 }
187 if k == parallelism-1 {
188 // No match found, data corruption
189 t.Errorf("Data corruption detected: loaded data doesn't match any test set")
190 }
191 }
192 }
193 }(i)
194 }
195 196 wg.Wait()
197 }
198 199 func TestBytesStress(t *testing.T) {
200 defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4))
201 202 atom := NewBytes([]byte("initial"))
203 var wg sync.WaitGroup
204 205 // We'll run 8 goroutines concurrently
206 workers := 8
207 iterations := 1000
208 wg.Add(workers)
209 210 start := make(chan struct{})
211 212 for i := 0; i < workers; i++ {
213 go func(id int) {
214 defer wg.Done()
215 216 // Wait for the start signal
217 <-start
218 219 // Each worker gets its own data
220 myData := []byte{byte(id)}
221 222 for j := 0; j < iterations; j++ {
223 // Alternate between reads and writes
224 if j%2 == 0 {
225 atom.Store(myData)
226 } else {
227 _ = atom.Load()
228 }
229 }
230 }(i)
231 }
232 233 // Start all goroutines simultaneously
234 close(start)
235 wg.Wait()
236 }
237 238 func BenchmarkBytesParallel(b *testing.B) {
239 atom := NewBytes([]byte("benchmark"))
240 241 b.RunParallel(
242 func(pb *testing.PB) {
243 // Each goroutine gets its own data to prevent false sharing
244 myData := []byte("goroutine data")
245 246 for pb.Next() {
247 atom.Store(myData)
248 _ = atom.Load()
249 }
250 },
251 )
252 }
253