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  }