283 lines
6.3 KiB
Go
283 lines
6.3 KiB
Go
|
// +build !appengine
|
||
|
|
||
|
package aetest
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"crypto/rand"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"path/filepath"
|
||
|
"regexp"
|
||
|
"time"
|
||
|
|
||
|
"golang.org/x/net/context"
|
||
|
"google.golang.org/appengine/internal"
|
||
|
)
|
||
|
|
||
|
// NewInstance launches a running instance of api_server.py which can be used
|
||
|
// for multiple test Contexts that delegate all App Engine API calls to that
|
||
|
// instance.
|
||
|
// If opts is nil the default values are used.
|
||
|
func NewInstance(opts *Options) (Instance, error) {
|
||
|
i := &instance{
|
||
|
opts: opts,
|
||
|
appID: "testapp",
|
||
|
startupTimeout: 15 * time.Second,
|
||
|
}
|
||
|
if opts != nil {
|
||
|
if opts.AppID != "" {
|
||
|
i.appID = opts.AppID
|
||
|
}
|
||
|
if opts.StartupTimeout > 0 {
|
||
|
i.startupTimeout = opts.StartupTimeout
|
||
|
}
|
||
|
}
|
||
|
if err := i.startChild(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return i, nil
|
||
|
}
|
||
|
|
||
|
func newSessionID() string {
|
||
|
var buf [16]byte
|
||
|
io.ReadFull(rand.Reader, buf[:])
|
||
|
return fmt.Sprintf("%x", buf[:])
|
||
|
}
|
||
|
|
||
|
// instance implements the Instance interface.
|
||
|
type instance struct {
|
||
|
opts *Options
|
||
|
child *exec.Cmd
|
||
|
apiURL *url.URL // base URL of API HTTP server
|
||
|
adminURL string // base URL of admin HTTP server
|
||
|
appDir string
|
||
|
appID string
|
||
|
startupTimeout time.Duration
|
||
|
relFuncs []func() // funcs to release any associated contexts
|
||
|
}
|
||
|
|
||
|
// NewRequest returns an *http.Request associated with this instance.
|
||
|
func (i *instance) NewRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
|
||
|
req, err := http.NewRequest(method, urlStr, body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Associate this request.
|
||
|
release := internal.RegisterTestRequest(req, i.apiURL, func(ctx context.Context) context.Context {
|
||
|
ctx = internal.WithAppIDOverride(ctx, "dev~"+i.appID)
|
||
|
return ctx
|
||
|
})
|
||
|
i.relFuncs = append(i.relFuncs, release)
|
||
|
|
||
|
return req, nil
|
||
|
}
|
||
|
|
||
|
// Close kills the child api_server.py process, releasing its resources.
|
||
|
func (i *instance) Close() (err error) {
|
||
|
for _, rel := range i.relFuncs {
|
||
|
rel()
|
||
|
}
|
||
|
i.relFuncs = nil
|
||
|
child := i.child
|
||
|
if child == nil {
|
||
|
return nil
|
||
|
}
|
||
|
defer func() {
|
||
|
i.child = nil
|
||
|
err1 := os.RemoveAll(i.appDir)
|
||
|
if err == nil {
|
||
|
err = err1
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
if p := child.Process; p != nil {
|
||
|
errc := make(chan error, 1)
|
||
|
go func() {
|
||
|
errc <- child.Wait()
|
||
|
}()
|
||
|
|
||
|
// Call the quit handler on the admin server.
|
||
|
res, err := http.Get(i.adminURL + "/quit")
|
||
|
if err != nil {
|
||
|
p.Kill()
|
||
|
return fmt.Errorf("unable to call /quit handler: %v", err)
|
||
|
}
|
||
|
res.Body.Close()
|
||
|
select {
|
||
|
case <-time.After(15 * time.Second):
|
||
|
p.Kill()
|
||
|
return errors.New("timeout killing child process")
|
||
|
case err = <-errc:
|
||
|
// Do nothing.
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func fileExists(path string) bool {
|
||
|
_, err := os.Stat(path)
|
||
|
return err == nil
|
||
|
}
|
||
|
|
||
|
func findPython() (path string, err error) {
|
||
|
for _, name := range []string{"python2.7", "python"} {
|
||
|
path, err = exec.LookPath(name)
|
||
|
if err == nil {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func findDevAppserver() (string, error) {
|
||
|
if p := os.Getenv("APPENGINE_DEV_APPSERVER"); p != "" {
|
||
|
if fileExists(p) {
|
||
|
return p, nil
|
||
|
}
|
||
|
return "", fmt.Errorf("invalid APPENGINE_DEV_APPSERVER environment variable; path %q doesn't exist", p)
|
||
|
}
|
||
|
return exec.LookPath("dev_appserver.py")
|
||
|
}
|
||
|
|
||
|
var apiServerAddrRE = regexp.MustCompile(`Starting API server at: (\S+)`)
|
||
|
var adminServerAddrRE = regexp.MustCompile(`Starting admin server at: (\S+)`)
|
||
|
|
||
|
func (i *instance) startChild() (err error) {
|
||
|
if PrepareDevAppserver != nil {
|
||
|
if err := PrepareDevAppserver(); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
python, err := findPython()
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Could not find python interpreter: %v", err)
|
||
|
}
|
||
|
devAppserver, err := findDevAppserver()
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Could not find dev_appserver.py: %v", err)
|
||
|
}
|
||
|
|
||
|
i.appDir, err = ioutil.TempDir("", "appengine-aetest")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer func() {
|
||
|
if err != nil {
|
||
|
os.RemoveAll(i.appDir)
|
||
|
}
|
||
|
}()
|
||
|
err = os.Mkdir(filepath.Join(i.appDir, "app"), 0755)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
err = ioutil.WriteFile(filepath.Join(i.appDir, "app", "app.yaml"), []byte(i.appYAML()), 0644)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
err = ioutil.WriteFile(filepath.Join(i.appDir, "app", "stubapp.go"), []byte(appSource), 0644)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
appserverArgs := []string{
|
||
|
devAppserver,
|
||
|
"--port=0",
|
||
|
"--api_port=0",
|
||
|
"--admin_port=0",
|
||
|
"--automatic_restart=false",
|
||
|
"--skip_sdk_update_check=true",
|
||
|
"--clear_datastore=true",
|
||
|
"--clear_search_indexes=true",
|
||
|
"--datastore_path", filepath.Join(i.appDir, "datastore"),
|
||
|
}
|
||
|
if i.opts != nil && i.opts.StronglyConsistentDatastore {
|
||
|
appserverArgs = append(appserverArgs, "--datastore_consistency_policy=consistent")
|
||
|
}
|
||
|
appserverArgs = append(appserverArgs, filepath.Join(i.appDir, "app"))
|
||
|
|
||
|
i.child = exec.Command(python,
|
||
|
appserverArgs...,
|
||
|
)
|
||
|
i.child.Stdout = os.Stdout
|
||
|
var stderr io.Reader
|
||
|
stderr, err = i.child.StderrPipe()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
stderr = io.TeeReader(stderr, os.Stderr)
|
||
|
if err = i.child.Start(); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Read stderr until we have read the URLs of the API server and admin interface.
|
||
|
errc := make(chan error, 1)
|
||
|
go func() {
|
||
|
s := bufio.NewScanner(stderr)
|
||
|
for s.Scan() {
|
||
|
if match := apiServerAddrRE.FindStringSubmatch(s.Text()); match != nil {
|
||
|
u, err := url.Parse(match[1])
|
||
|
if err != nil {
|
||
|
errc <- fmt.Errorf("failed to parse API URL %q: %v", match[1], err)
|
||
|
return
|
||
|
}
|
||
|
i.apiURL = u
|
||
|
}
|
||
|
if match := adminServerAddrRE.FindStringSubmatch(s.Text()); match != nil {
|
||
|
i.adminURL = match[1]
|
||
|
}
|
||
|
if i.adminURL != "" && i.apiURL != nil {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
errc <- s.Err()
|
||
|
}()
|
||
|
|
||
|
select {
|
||
|
case <-time.After(i.startupTimeout):
|
||
|
if p := i.child.Process; p != nil {
|
||
|
p.Kill()
|
||
|
}
|
||
|
return errors.New("timeout starting child process")
|
||
|
case err := <-errc:
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("error reading child process stderr: %v", err)
|
||
|
}
|
||
|
}
|
||
|
if i.adminURL == "" {
|
||
|
return errors.New("unable to find admin server URL")
|
||
|
}
|
||
|
if i.apiURL == nil {
|
||
|
return errors.New("unable to find API server URL")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (i *instance) appYAML() string {
|
||
|
return fmt.Sprintf(appYAMLTemplate, i.appID)
|
||
|
}
|
||
|
|
||
|
const appYAMLTemplate = `
|
||
|
application: %s
|
||
|
version: 1
|
||
|
runtime: go
|
||
|
api_version: go1
|
||
|
|
||
|
handlers:
|
||
|
- url: /.*
|
||
|
script: _go_app
|
||
|
`
|
||
|
|
||
|
const appSource = `
|
||
|
package main
|
||
|
import "google.golang.org/appengine"
|
||
|
func main() { appengine.Main() }
|
||
|
`
|