1// +build !appengine 2 3package aetest 4 5import ( 6 "bufio" 7 "crypto/rand" 8 "errors" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "net/url" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "regexp" 18 "time" 19 20 "golang.org/x/net/context" 21 "google.golang.org/appengine/internal" 22) 23 24// NewInstance launches a running instance of api_server.py which can be used 25// for multiple test Contexts that delegate all App Engine API calls to that 26// instance. 27// If opts is nil the default values are used. 28func NewInstance(opts *Options) (Instance, error) { 29 i := &instance{ 30 opts: opts, 31 appID: "testapp", 32 startupTimeout: 15 * time.Second, 33 } 34 if opts != nil { 35 if opts.AppID != "" { 36 i.appID = opts.AppID 37 } 38 if opts.StartupTimeout > 0 { 39 i.startupTimeout = opts.StartupTimeout 40 } 41 } 42 if err := i.startChild(); err != nil { 43 return nil, err 44 } 45 return i, nil 46} 47 48func newSessionID() string { 49 var buf [16]byte 50 io.ReadFull(rand.Reader, buf[:]) 51 return fmt.Sprintf("%x", buf[:]) 52} 53 54// instance implements the Instance interface. 55type instance struct { 56 opts *Options 57 child *exec.Cmd 58 apiURL *url.URL // base URL of API HTTP server 59 adminURL string // base URL of admin HTTP server 60 appDir string 61 appID string 62 startupTimeout time.Duration 63 relFuncs []func() // funcs to release any associated contexts 64} 65 66// NewRequest returns an *http.Request associated with this instance. 67func (i *instance) NewRequest(method, urlStr string, body io.Reader) (*http.Request, error) { 68 req, err := http.NewRequest(method, urlStr, body) 69 if err != nil { 70 return nil, err 71 } 72 73 // Associate this request. 74 req, release := internal.RegisterTestRequest(req, i.apiURL, func(ctx context.Context) context.Context { 75 ctx = internal.WithAppIDOverride(ctx, "dev~"+i.appID) 76 return ctx 77 }) 78 i.relFuncs = append(i.relFuncs, release) 79 80 return req, nil 81} 82 83// Close kills the child api_server.py process, releasing its resources. 84func (i *instance) Close() (err error) { 85 for _, rel := range i.relFuncs { 86 rel() 87 } 88 i.relFuncs = nil 89 child := i.child 90 if child == nil { 91 return nil 92 } 93 defer func() { 94 i.child = nil 95 err1 := os.RemoveAll(i.appDir) 96 if err == nil { 97 err = err1 98 } 99 }() 100 101 if p := child.Process; p != nil { 102 errc := make(chan error, 1) 103 go func() { 104 errc <- child.Wait() 105 }() 106 107 // Call the quit handler on the admin server. 108 res, err := http.Get(i.adminURL + "/quit") 109 if err != nil { 110 p.Kill() 111 return fmt.Errorf("unable to call /quit handler: %v", err) 112 } 113 res.Body.Close() 114 select { 115 case <-time.After(15 * time.Second): 116 p.Kill() 117 return errors.New("timeout killing child process") 118 case err = <-errc: 119 // Do nothing. 120 } 121 } 122 return 123} 124 125func fileExists(path string) bool { 126 _, err := os.Stat(path) 127 return err == nil 128} 129 130func findPython() (path string, err error) { 131 for _, name := range []string{"python2.7", "python"} { 132 path, err = exec.LookPath(name) 133 if err == nil { 134 return 135 } 136 } 137 return 138} 139 140func findDevAppserver() (string, error) { 141 if p := os.Getenv("APPENGINE_DEV_APPSERVER"); p != "" { 142 if fileExists(p) { 143 return p, nil 144 } 145 return "", fmt.Errorf("invalid APPENGINE_DEV_APPSERVER environment variable; path %q doesn't exist", p) 146 } 147 return exec.LookPath("dev_appserver.py") 148} 149 150var apiServerAddrRE = regexp.MustCompile(`Starting API server at: (\S+)`) 151var adminServerAddrRE = regexp.MustCompile(`Starting admin server at: (\S+)`) 152 153func (i *instance) startChild() (err error) { 154 if PrepareDevAppserver != nil { 155 if err := PrepareDevAppserver(); err != nil { 156 return err 157 } 158 } 159 python, err := findPython() 160 if err != nil { 161 return fmt.Errorf("Could not find python interpreter: %v", err) 162 } 163 devAppserver, err := findDevAppserver() 164 if err != nil { 165 return fmt.Errorf("Could not find dev_appserver.py: %v", err) 166 } 167 168 i.appDir, err = ioutil.TempDir("", "appengine-aetest") 169 if err != nil { 170 return err 171 } 172 defer func() { 173 if err != nil { 174 os.RemoveAll(i.appDir) 175 } 176 }() 177 err = os.Mkdir(filepath.Join(i.appDir, "app"), 0755) 178 if err != nil { 179 return err 180 } 181 err = ioutil.WriteFile(filepath.Join(i.appDir, "app", "app.yaml"), []byte(i.appYAML()), 0644) 182 if err != nil { 183 return err 184 } 185 err = ioutil.WriteFile(filepath.Join(i.appDir, "app", "stubapp.go"), []byte(appSource), 0644) 186 if err != nil { 187 return err 188 } 189 190 appserverArgs := []string{ 191 devAppserver, 192 "--port=0", 193 "--api_port=0", 194 "--admin_port=0", 195 "--automatic_restart=false", 196 "--skip_sdk_update_check=true", 197 "--clear_datastore=true", 198 "--clear_search_indexes=true", 199 "--datastore_path", filepath.Join(i.appDir, "datastore"), 200 } 201 if i.opts != nil && i.opts.StronglyConsistentDatastore { 202 appserverArgs = append(appserverArgs, "--datastore_consistency_policy=consistent") 203 } 204 appserverArgs = append(appserverArgs, filepath.Join(i.appDir, "app")) 205 206 i.child = exec.Command(python, 207 appserverArgs..., 208 ) 209 i.child.Stdout = os.Stdout 210 var stderr io.Reader 211 stderr, err = i.child.StderrPipe() 212 if err != nil { 213 return err 214 } 215 stderr = io.TeeReader(stderr, os.Stderr) 216 if err = i.child.Start(); err != nil { 217 return err 218 } 219 220 // Read stderr until we have read the URLs of the API server and admin interface. 221 errc := make(chan error, 1) 222 go func() { 223 s := bufio.NewScanner(stderr) 224 for s.Scan() { 225 if match := apiServerAddrRE.FindStringSubmatch(s.Text()); match != nil { 226 u, err := url.Parse(match[1]) 227 if err != nil { 228 errc <- fmt.Errorf("failed to parse API URL %q: %v", match[1], err) 229 return 230 } 231 i.apiURL = u 232 } 233 if match := adminServerAddrRE.FindStringSubmatch(s.Text()); match != nil { 234 i.adminURL = match[1] 235 } 236 if i.adminURL != "" && i.apiURL != nil { 237 break 238 } 239 } 240 errc <- s.Err() 241 }() 242 243 select { 244 case <-time.After(i.startupTimeout): 245 if p := i.child.Process; p != nil { 246 p.Kill() 247 } 248 return errors.New("timeout starting child process") 249 case err := <-errc: 250 if err != nil { 251 return fmt.Errorf("error reading child process stderr: %v", err) 252 } 253 } 254 if i.adminURL == "" { 255 return errors.New("unable to find admin server URL") 256 } 257 if i.apiURL == nil { 258 return errors.New("unable to find API server URL") 259 } 260 return nil 261} 262 263func (i *instance) appYAML() string { 264 return fmt.Sprintf(appYAMLTemplate, i.appID) 265} 266 267const appYAMLTemplate = ` 268application: %s 269version: 1 270runtime: go 271api_version: go1 272 273handlers: 274- url: /.* 275 script: _go_app 276` 277 278const appSource = ` 279package main 280import "google.golang.org/appengine" 281func main() { appengine.Main() } 282` 283