memos/store/test/main_test.go
Johnny db57f4456a fix: resolve data races in store tests with atomic operations
Fix all data races detected by -race flag in parallel store tests.

Problems fixed:
1. TestMain not propagating exit code
   - Was: m.Run(); return
   - Now: os.Exit(m.Run())

2. Data races on global DSN variables
   - mysqlBaseDSN and postgresBaseDSN written in sync.Once
   - Read outside Once without synchronization
   - Race detector: write/read without happens-before relationship

3. Data races on container pointers
   - mysqlContainer, postgresContainer, testDockerNetwork
   - Same pattern: write in Once, read in cleanup

Solution: Use atomic operations
- atomic.Value for DSN strings (Store/Load)
- atomic.Pointer for container pointers (Store/Load)
- Provides proper memory synchronization
- Race-free reads and writes

Why sync.Once alone wasn't enough:
- sync.Once guarantees function runs once
- Does NOT provide memory barrier for variables written inside
- Reads outside Once have no synchronization with writes
- Race detector correctly flags this as violation

Technical details:
- atomic.Value.Store() provides release semantics
- atomic.Value.Load() provides acquire semantics
- Guarantees happens-before relationship per Go memory model
- All 159 parallel tests can safely access globals

Impact:
- Tests now pass with -race flag
- No performance degradation (atomic ops are fast)
- Maintains parallel execution benefits (8-10x speedup)
- Proper Go memory model compliance

Related: Issues #2, #3 from race analysis
2026-01-14 23:37:12 +08:00

64 lines
1.8 KiB
Go

package test
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
)
func TestMain(m *testing.M) {
// If DRIVER is set, run tests for that driver only
if os.Getenv("DRIVER") != "" {
defer TerminateContainers()
os.Exit(m.Run())
}
// No DRIVER set - run tests for all drivers sequentially
runAllDrivers()
}
func runAllDrivers() {
drivers := []string{"sqlite", "mysql", "postgres"}
_, currentFile, _, _ := runtime.Caller(0)
projectRoot := filepath.Dir(filepath.Dir(filepath.Dir(currentFile)))
// Build the docker image once for all tests to use
fmt.Println("Building memos docker image for tests (memos-test:local)...")
buildCmd := exec.Command("docker", "build", "-f", "store/test/Dockerfile", "-t", "memos-test:local", ".")
buildCmd.Dir = projectRoot
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil {
fmt.Printf("Failed to build docker image: %v\n", err)
// We don't exit here, we let the tests try to run (and maybe fail or rebuild)
// strictly speaking we should probably fail, but let's be robust.
// Actually, if build fails, tests relying on it will fail or try to rebuild.
// Let's exit to be clear.
panic(fmt.Sprintf("failed to build docker image: %v", err))
}
var failed []string
for _, driver := range drivers {
fmt.Printf("\n==================== %s ====================\n\n", driver)
cmd := exec.Command("go", "test", "-v", "-count=1", "./store/test/...")
cmd.Dir = projectRoot
cmd.Env = append(os.Environ(), "DRIVER="+driver, "MEMOS_TEST_IMAGE_BUILT=1")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
failed = append(failed, driver)
}
}
fmt.Println()
if len(failed) > 0 {
fmt.Printf("FAIL: %v\n", failed)
panic("some drivers failed")
}
fmt.Println("PASS: all drivers")
}