@@ -2,15 +2,235 @@ package main
22
33import (
44 "context"
5+ "encoding/json"
56 "fmt"
7+ "log"
8+ "os"
9+ "path/filepath"
10+ "testing"
11+ "time"
612
713 "golang-simple-notes/model"
814 "golang-simple-notes/storage"
915
1016 "github.com/testcontainers/testcontainers-go"
17+ "github.com/testcontainers/testcontainers-go/modules/mongodb"
1118 "github.com/testcontainers/testcontainers-go/wait"
1219)
1320
21+ var (
22+ // Shared container instances for all tests in the main package
23+ sharedMongoContainer testcontainers.Container
24+ sharedMongoURI string
25+ sharedCouchContainer testcontainers.Container
26+ sharedCouchURL string
27+ shouldCleanupContainers bool
28+ )
29+
30+ // containerState holds the connection details for shared containers
31+ type containerState struct {
32+ MongoURI string `json:"mongo_uri"`
33+ CouchURL string `json:"couch_url"`
34+ OwnerPID int `json:"owner_pid"`
35+ }
36+
37+ // getStateFilePath returns the path to the shared state file
38+ func getStateFilePath () string {
39+ return filepath .Join (os .TempDir (), "testcontainers-shared-state.json" )
40+ }
41+
42+ // getLockFilePath returns the path to the lock file
43+ func getLockFilePath () string {
44+ return filepath .Join (os .TempDir (), "testcontainers-shared-state.lock" )
45+ }
46+
47+ // acquireLock attempts to acquire a file-based lock
48+ func acquireLock () (* os.File , error ) {
49+ lockFile := getLockFilePath ()
50+ // Try to create lock file exclusively
51+ for i := 0 ; i < 50 ; i ++ {
52+ f , err := os .OpenFile (lockFile , os .O_CREATE | os .O_EXCL | os .O_WRONLY , 0644 )
53+ if err == nil {
54+ // Write our PID to the lock file
55+ fmt .Fprintf (f , "%d" , os .Getpid ())
56+ return f , nil
57+ }
58+ // Lock file exists, wait and retry
59+ time .Sleep (100 * time .Millisecond )
60+ }
61+ return nil , fmt .Errorf ("failed to acquire lock after 5 seconds" )
62+ }
63+
64+ // releaseLock releases the file-based lock
65+ func releaseLock (f * os.File ) {
66+ if f != nil {
67+ f .Close ()
68+ os .Remove (getLockFilePath ())
69+ }
70+ }
71+
72+ // loadContainerState loads the container state from file
73+ func loadContainerState () (* containerState , error ) {
74+ stateFile := getStateFilePath ()
75+ data , err := os .ReadFile (stateFile )
76+ if err != nil {
77+ return nil , err
78+ }
79+
80+ var state containerState
81+ if err := json .Unmarshal (data , & state ); err != nil {
82+ return nil , err
83+ }
84+
85+ return & state , nil
86+ }
87+
88+ // saveContainerState saves the container state to file
89+ func saveContainerState (state * containerState ) error {
90+ stateFile := getStateFilePath ()
91+ data , err := json .Marshal (state )
92+ if err != nil {
93+ return err
94+ }
95+
96+ return os .WriteFile (stateFile , data , 0644 )
97+ }
98+
99+ // TestMain sets up shared test containers for the main package
100+ func TestMain (m * testing.M ) {
101+ ctx := context .Background ()
102+
103+ // Acquire lock to prevent race condition with other packages
104+ lock , err := acquireLock ()
105+ if err != nil {
106+ log .Printf ("Warning: Failed to acquire lock: %v. Tests may fail." , err )
107+ os .Exit (1 )
108+ }
109+
110+ // Try to load existing container state
111+ state , err := loadContainerState ()
112+ if err == nil && state != nil {
113+ // Reuse existing containers from another package
114+ log .Printf ("Reusing existing MongoDB container at %s" , state .MongoURI )
115+ log .Printf ("Reusing existing CouchDB container at %s" , state .CouchURL )
116+ sharedMongoURI = state .MongoURI
117+ sharedCouchURL = state .CouchURL
118+ shouldCleanupContainers = false
119+ releaseLock (lock )
120+ } else {
121+ // Start new containers and save state
122+ shouldCleanupContainers = true
123+
124+ // Start shared MongoDB container
125+ if err := startSharedMongoDBContainer (ctx ); err != nil {
126+ log .Printf ("Warning: Failed to start shared MongoDB container: %v. MongoDB tests may fail." , err )
127+ }
128+
129+ // Start shared CouchDB container
130+ if err := startSharedCouchDBContainer (ctx ); err != nil {
131+ log .Printf ("Warning: Failed to start shared CouchDB container: %v. CouchDB tests may fail." , err )
132+ }
133+
134+ // Save container state for other packages to reuse
135+ if sharedMongoURI != "" && sharedCouchURL != "" {
136+ state := & containerState {
137+ MongoURI : sharedMongoURI ,
138+ CouchURL : sharedCouchURL ,
139+ OwnerPID : os .Getpid (),
140+ }
141+ if err := saveContainerState (state ); err != nil {
142+ log .Printf ("Warning: Failed to save container state: %v" , err )
143+ }
144+ }
145+
146+ releaseLock (lock )
147+ }
148+
149+ // Run tests
150+ code := m .Run ()
151+
152+ // Cleanup containers only if we started them
153+ if shouldCleanupContainers {
154+ cleanupCtx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
155+ defer cancel ()
156+
157+ if sharedMongoContainer != nil {
158+ if err := sharedMongoContainer .Terminate (cleanupCtx ); err != nil {
159+ log .Printf ("Failed to terminate MongoDB container: %v" , err )
160+ }
161+ }
162+
163+ if sharedCouchContainer != nil {
164+ if err := sharedCouchContainer .Terminate (cleanupCtx ); err != nil {
165+ log .Printf ("Failed to terminate CouchDB container: %v" , err )
166+ }
167+ }
168+
169+ // Clean up state file
170+ os .Remove (getStateFilePath ())
171+ }
172+
173+ os .Exit (code )
174+ }
175+
176+ func startSharedMongoDBContainer (ctx context.Context ) error {
177+ container , err := mongodb .RunContainer (ctx , testcontainers .WithImage ("mongo:7.0.23-jammy" ))
178+ if err != nil {
179+ return fmt .Errorf ("failed to start MongoDB container: %w" , err )
180+ }
181+
182+ sharedMongoContainer = container
183+
184+ // Get connection string
185+ mongoURI , err := container .ConnectionString (ctx )
186+ if err != nil {
187+ return fmt .Errorf ("failed to get MongoDB connection string: %w" , err )
188+ }
189+
190+ sharedMongoURI = mongoURI
191+
192+ log .Printf ("Started shared MongoDB container at %s" , sharedMongoURI )
193+ return nil
194+ }
195+
196+ func startSharedCouchDBContainer (ctx context.Context ) error {
197+ req := testcontainers.ContainerRequest {
198+ Image : "couchdb:3.4.3" ,
199+ ExposedPorts : []string {"5984/tcp" },
200+ WaitingFor : wait .ForListeningPort ("5984/tcp" ),
201+ Env : map [string ]string {
202+ "COUCHDB_USER" : "admin" ,
203+ "COUCHDB_PASSWORD" : "password" ,
204+ },
205+ }
206+
207+ container , err := testcontainers .GenericContainer (ctx , testcontainers.GenericContainerRequest {
208+ ContainerRequest : req ,
209+ Started : true ,
210+ })
211+ if err != nil {
212+ return fmt .Errorf ("failed to start CouchDB container: %w" , err )
213+ }
214+
215+ sharedCouchContainer = container
216+
217+ // Get connection details
218+ host , err := container .Host (ctx )
219+ if err != nil {
220+ return fmt .Errorf ("failed to get CouchDB container host: %w" , err )
221+ }
222+
223+ port , err := container .MappedPort (ctx , "5984" )
224+ if err != nil {
225+ return fmt .Errorf ("failed to get CouchDB container port: %w" , err )
226+ }
227+
228+ sharedCouchURL = fmt .Sprintf ("http://admin:password@%s:%s" , host , port .Port ())
229+
230+ log .Printf ("Started shared CouchDB container at %s" , sharedCouchURL )
231+ return nil
232+ }
233+
14234// MockStorage is a simple implementation of NoteStorage for testing
15235type MockStorage struct {
16236 notes []* model.Note
0 commit comments