github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/shared/sharedtest/util.go (about) 1 // Copyright 2018 The WPT Dashboard Project. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package sharedtest 6 7 import ( 8 "bytes" 9 "context" 10 "errors" 11 "fmt" 12 "io" 13 "net/http" 14 "net/http/httptest" 15 "os" 16 "os/exec" 17 "time" 18 19 "github.com/golang/mock/gomock" 20 "github.com/phayes/freeport" 21 22 "github.com/web-platform-tests/wpt.fyi/shared" 23 ) 24 25 // Instance represents a running instance of the development API Server. 26 type Instance interface { 27 // Close kills the child api_server.py process, releasing its resources. 28 io.Closer 29 // NewRequest returns an *http.Request associated with this instance. 30 NewRequest(method, urlStr string, body io.Reader) (*http.Request, error) 31 } 32 33 type aeInstance struct { 34 // Google Cloud Datastore emulator 35 gcd *exec.Cmd 36 hostPort string 37 dataDir string 38 } 39 40 func (i aeInstance) Close() error { 41 shared.Clients.Close() 42 return i.stop() 43 } 44 45 func (i aeInstance) NewRequest(method, urlStr string, body io.Reader) (*http.Request, error) { 46 req := httptest.NewRequest(method, urlStr, body) 47 return req.WithContext(ctxWithNilLogger(context.Background())), nil 48 } 49 50 func (i *aeInstance) start(stronglyConsistentDatastore bool) error { 51 consistency := "1.0" 52 if !stronglyConsistentDatastore { 53 consistency = "0.5" 54 } 55 // Project ID isn't important as long as it's valid. 56 project := "test-app" 57 port, err := freeport.GetFreePort() 58 if err != nil { 59 return err 60 } 61 dir, err := os.MkdirTemp("", "wpt_fyi_datastore") 62 if err != nil { 63 fmt.Println("unable to create temporary datastore data directory") 64 return err 65 } 66 i.dataDir = dir 67 i.hostPort = fmt.Sprintf("127.0.0.1:%d", port) 68 i.gcd = exec.Command("gcloud", "beta", "emulators", "datastore", "start", 69 "--data-dir="+i.dataDir, 70 "--consistency="+consistency, 71 "--project="+project, 72 "--host-port="+i.hostPort) 73 // Store the output to use in case it fails to start 74 var stdoutBuffer, stderrBuffer bytes.Buffer 75 i.gcd.Stdout = &stdoutBuffer 76 i.gcd.Stderr = &stderrBuffer 77 if err := i.gcd.Start(); err != nil { 78 return err 79 } 80 81 started := make(chan bool) 82 go func() { 83 for { 84 res, err := http.Get("http://" + i.hostPort) 85 if err == nil { 86 res.Body.Close() 87 if res.StatusCode == http.StatusOK { 88 started <- true 89 return 90 } 91 } 92 time.Sleep(time.Millisecond * 100) 93 } 94 }() 95 select { 96 case <-started: 97 break 98 case <-time.After(time.Second * 10): 99 i.stop() 100 fmt.Printf("datastore emulator unable to start in time:\nstdout:\n%s\nstderr:\n%s\n", 101 stdoutBuffer.String(), 102 stderrBuffer.String()) 103 return errors.New("timed out starting Datastore emulator") 104 } 105 106 os.Setenv("DATASTORE_PROJECT_ID", project) 107 os.Setenv("DATASTORE_EMULATOR_HOST", i.hostPort) 108 return nil 109 } 110 111 func (i aeInstance) stop() error { 112 // Do not kill, terminate or interrupt the emulator process; its subprocesses will keep running. 113 // https://github.com/googleapis/google-cloud-go/issues/224#issuecomment-218327626 114 postShutdown := func() { 115 res, err := http.PostForm(fmt.Sprintf("http://%s/shutdown", i.hostPort), nil) 116 if err == nil { 117 res.Body.Close() 118 } 119 } 120 121 stopped := make(chan error) 122 go func() { 123 postShutdown() 124 for { 125 select { 126 case <-stopped: 127 return 128 case <-time.After(time.Second): 129 postShutdown() 130 } 131 } 132 }() 133 stopped <- i.gcd.Wait() 134 135 if i.dataDir != "" { 136 err := os.RemoveAll(i.dataDir) 137 if err != nil { 138 // Do not need to return error. Just warn. 139 fmt.Printf("warning: unable to delete temporary data directory %s. %s\n", 140 i.dataDir, 141 err.Error()) 142 } 143 i.dataDir = "" 144 } 145 146 return nil 147 } 148 149 // NewAEInstance creates a new test instance backed by Cloud Datastore emulator. 150 // It takes a boolean argument for whether the Datastore emulation should be 151 // strongly consistent. 152 func NewAEInstance(stronglyConsistentDatastore bool) (Instance, error) { 153 i := aeInstance{} 154 if err := i.start(stronglyConsistentDatastore); err != nil { 155 return nil, err 156 } 157 if err := shared.Clients.Init(context.Background()); err != nil { 158 i.Close() 159 return nil, err 160 } 161 return i, nil 162 } 163 164 // NewAEContext creates a new aetest context backed by dev_appserver whose 165 // logs are suppressed. It takes a boolean argument for whether the Datastore 166 // emulation should be strongly consistent. 167 func NewAEContext(stronglyConsistentDatastore bool) (context.Context, func(), error) { 168 inst, err := NewAEInstance(stronglyConsistentDatastore) 169 if err != nil { 170 return nil, nil, err 171 } 172 req, err := inst.NewRequest("GET", "/", nil) 173 if err != nil { 174 inst.Close() 175 return nil, nil, err 176 } 177 ctx := ctxWithNilLogger(req.Context()) 178 return ctx, func() { 179 inst.Close() 180 }, nil 181 } 182 183 // NewTestContext creates a new context.Context for small tests. 184 func NewTestContext() context.Context { 185 return ctxWithNilLogger(context.Background()) 186 } 187 188 func ctxWithNilLogger(ctx context.Context) context.Context { 189 return context.WithValue(ctx, shared.DefaultLoggerCtxKey(), shared.NewNilLogger()) 190 } 191 192 type sameStringSpec struct { 193 spec string 194 } 195 196 type stringifiable interface { 197 String() string 198 } 199 200 func (s sameStringSpec) Matches(x interface{}) bool { 201 if p, ok := x.(stringifiable); ok && p.String() == s.spec { 202 return true 203 } else if str, ok := x.(string); ok && str == s.spec { 204 return true 205 } 206 return false 207 } 208 func (s sameStringSpec) String() string { 209 return s.spec 210 } 211 212 // SameProductSpec returns a gomock matcher for a product spec. 213 func SameProductSpec(spec string) gomock.Matcher { 214 return sameStringSpec{ 215 spec: spec, 216 } 217 } 218 219 // SameDiffFilter returns a gomock matcher for a diff filter. 220 func SameDiffFilter(filter string) gomock.Matcher { 221 return sameStringSpec{ 222 spec: filter, 223 } 224 } 225 226 type sameKeys struct { 227 ids []int64 228 } 229 230 func (s sameKeys) Matches(x interface{}) bool { 231 if keys, ok := x.([]shared.Key); ok { 232 for i := range keys { 233 if i >= len(s.ids) || keys[i] == nil || s.ids[i] != keys[i].IntID() { 234 return false 235 } 236 } 237 return true 238 } 239 if ids, ok := x.(shared.TestRunIDs); ok { 240 for i := range ids { 241 if i >= len(s.ids) || s.ids[i] != ids[i] { 242 return false 243 } 244 } 245 return true 246 } 247 return false 248 } 249 func (s sameKeys) String() string { 250 return fmt.Sprintf("%v", s.ids) 251 } 252 253 // SameKeys returns a gomock matcher for a Key slice. 254 func SameKeys(ids []int64) gomock.Matcher { 255 return sameKeys{ids} 256 } 257 258 // MultiRuns returns a DoAndReturn func that puts the given test runs in the dst interface 259 // for a shared.Datastore.GetMulti call. 260 func MultiRuns(runs shared.TestRuns) func(keys []shared.Key, dst interface{}) error { 261 return func(keys []shared.Key, dst interface{}) error { 262 out, ok := dst.(shared.TestRuns) 263 if !ok || len(out) != len(keys) || len(runs) != len(out) { 264 return errors.New("invalid destination array") 265 } 266 for i := range runs { 267 out[i] = runs[i] 268 } 269 return nil 270 } 271 } 272 273 // MockKey is a (very simple) mock shared.Key.MockKey. It is used because gomock 274 // can end up in a deadlock when, during a Matcher, we create another Matcher, 275 // e.g. mocking Datastore.GetKey(int64) with a DoAndReturn that creates a 276 // gomock generated MockKey, for which we'd mock Key.IntID(), resulted in deadlock. 277 type MockKey struct { 278 ID int64 279 Name string 280 TypeName string 281 } 282 283 // IntID returns the ID. 284 func (m MockKey) IntID() int64 { 285 return m.ID 286 } 287 288 // StringID returns the Name. 289 func (m MockKey) StringID() string { 290 return m.Name 291 } 292 293 // Kind returns the TypeName 294 func (m MockKey) Kind() string { 295 return m.TypeName 296 }