bytes_test.go raw

   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