Skip to content

Commit

Permalink
Support parsing W3C New Session requests. (#142)
Browse files Browse the repository at this point in the history
New Session requests are treated as two sets of capabilities: the OSS set ("desiredCapabilities") and the W3C set ("alwaysMatch"). Only one set of capabilities is provided in browser metadata. When the capabilities from a New Session request are merged onto metadata capabilities, both sets of capabilities are updated.
  • Loading branch information
juangj committed Jun 12, 2017
1 parent 168733c commit 0508435
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 92 deletions.
10 changes: 5 additions & 5 deletions go/launcher/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type Env interface {
// caps is the capabilities sent to the proxy from the client, and
// the return value is the capabilities that should be actually
// sent to the WebDriver server new session command.
StartSession(ctx context.Context, id int, caps map[string]interface{}) (map[string]interface{}, error)
StartSession(ctx context.Context, id int, caps capabilities.Spec) (capabilities.Spec, error)
// StartSession is called for each new WebDriver session, before
// the delete session command is sent to the WebDriver server.
StopSession(ctx context.Context, id int) error
Expand Down Expand Up @@ -86,15 +86,15 @@ func (b *Base) SetUp(ctx context.Context) error {
// StartSession merges the passed in caps with b.Metadata.caps and returns the merged
// capabilities that should be used when calling new session on the WebDriver
// server.
func (b *Base) StartSession(ctx context.Context, id int, caps map[string]interface{}) (map[string]interface{}, error) {
func (b *Base) StartSession(ctx context.Context, id int, caps capabilities.Spec) (capabilities.Spec, error) {
if err := b.Healthy(ctx); err != nil {
return nil, err
return capabilities.Spec{}, err
}
resolved, err := b.Metadata.ResolvedCapabilities()
if err != nil {
return nil, err
return capabilities.Spec{}, err
}
updated := capabilities.Merge(resolved, caps)
updated := capabilities.MergeSpecOntoCaps(resolved, caps)
return updated, nil
}

Expand Down
69 changes: 52 additions & 17 deletions go/launcher/proxy/driverhub/driver_hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,15 @@ func (h *WebDriverHub) Shutdown(ctx context.Context) error {
}

// GetReusableSession grabs a reusable session if one is available that matches caps.
func (h *WebDriverHub) GetReusableSession(ctx context.Context, caps map[string]interface{}) (*WebDriverSession, bool) {
func (h *WebDriverHub) GetReusableSession(ctx context.Context, caps capabilities.Spec) (*WebDriverSession, bool) {
if !capabilities.CanReuseSession(caps) {
return nil, false
}

h.mu.Lock()
defer h.mu.Unlock()
for i, session := range h.reusableSessions {
if capabilities.Equals(caps, session.Desired) {
if capabilities.SpecEquals(caps, session.RequestedCaps) {
h.reusableSessions = append(h.reusableSessions[:i], h.reusableSessions[i+1:]...)
if err := session.WebDriver.Healthy(ctx); err == nil {
return session, true
Expand All @@ -162,7 +162,7 @@ func (h *WebDriverHub) GetReusableSession(ctx context.Context, caps map[string]i

// AddReusableSession adds a session that can be reused.
func (h *WebDriverHub) AddReusableSession(session *WebDriverSession) error {
if !capabilities.CanReuseSession(session.Desired) {
if !capabilities.CanReuseSession(session.RequestedCaps) {
return errors.New(h.Name(), "session is not reusable.")
}
h.reusableSessions = append(h.reusableSessions, session)
Expand All @@ -183,7 +183,6 @@ func (h *WebDriverHub) routeToSession(w http.ResponseWriter, r *http.Request) {
func (h *WebDriverHub) createSession(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log.Print("Creating session\n\n")
var desired map[string]interface{}

if err := h.waitForHealthyEnv(ctx); err != nil {
sessionNotCreated(w, err)
Expand All @@ -197,37 +196,52 @@ func (h *WebDriverHub) createSession(w http.ResponseWriter, r *http.Request) {
}

j := struct {
// OSS capabilities
Desired map[string]interface{} `json:"desiredCapabilities"`
// W3C capabilities
Capabilities struct {
Always map[string]interface{} `json:"alwaysMatch"`
First []map[string]interface{} `json:"firstMatch"`
} `json:"capabilities"`
}{}

if err := json.Unmarshal(body, &j); err != nil {
sessionNotCreated(w, err)
return
}

if j.Desired == nil {
sessionNotCreated(w, errors.New(h.Name(), "new session request body missing desired capabilities"))
if j.Desired == nil && j.Capabilities.Always == nil {
sessionNotCreated(w, errors.New(h.Name(), "new session request body missing capabilities"))
return
}
if !isEmptyish(j.Capabilities.First) {
sessionNotCreated(w, errors.New(h.Name(), "firstMatch capabilities are not yet supported"))
return
}

requestedCaps := capabilities.Spec{
OSSCaps: j.Desired,
W3CCaps: j.Capabilities.Always,
}

id := h.NextID()

desired, err = h.Env.StartSession(ctx, id, j.Desired)
caps, err := h.Env.StartSession(ctx, id, requestedCaps)
if err != nil {
sessionNotCreated(w, err)
return
}

log.Printf("Caps: %+v", desired)
log.Printf("Caps: %+v", caps)

var session *WebDriverSession

if reusable, ok := h.GetReusableSession(ctx, desired); ok {
if reusable, ok := h.GetReusableSession(ctx, caps); ok {
reusable.Unpause(id)
session = reusable
} else {
// TODO(DrMarcII) parameterize attempts based on browser metadata
driver, err := webdriver.CreateSession(ctx, h.Env.WDAddress(ctx), 3, desired)
driver, err := webdriver.CreateSession(ctx, h.Env.WDAddress(ctx), 3, caps)
if err != nil {
if err2 := h.Env.StopSession(ctx, id); err2 != nil {
log.Printf("error stopping session after failing to launch webdriver: %v", err2)
Expand All @@ -236,7 +250,7 @@ func (h *WebDriverHub) createSession(w http.ResponseWriter, r *http.Request) {
return
}

s, err := CreateSession(id, h, driver, desired)
s, err := CreateSession(id, h, driver, caps)
if err != nil {
sessionNotCreated(w, err)
return
Expand All @@ -246,13 +260,21 @@ func (h *WebDriverHub) createSession(w http.ResponseWriter, r *http.Request) {

h.AddSession(session.WebDriver.SessionID(), session)

respJSON := map[string]interface{}{
"sessionId": session.WebDriver.SessionID(),
"value": session.WebDriver.Capabilities(),
}
var respJSON map[string]interface{}

if !session.WebDriver.W3C() {
respJSON["status"] = 0
if session.WebDriver.W3C() {
respJSON = map[string]interface{}{
"value": map[string]interface{}{
"capabilities": session.WebDriver.Capabilities(),
"sessionId": session.WebDriver.SessionID(),
},
}
} else {
respJSON = map[string]interface{}{
"value": session.WebDriver.Capabilities(),
"sessionId": session.WebDriver.SessionID(),
"status": 0,
}
}

bytes, err := json.Marshal(respJSON)
Expand Down Expand Up @@ -286,3 +308,16 @@ func (h *WebDriverHub) waitForHealthyEnv(ctx context.Context) error {
})
return h.Env.Healthy(ctx)
}

// isEmptyish returns whether a firstMatch capabilities list is effectively empty.
func isEmptyish(x []map[string]interface{}) bool {
if len(x) == 0 {
return true
}
if len(x) == 1 && len(x[0]) == 0 {
// A list containing a single empty object, i.e., [{}], means to leave the
// alwaysMatch capabilities unmodified.
return true
}
return false
}
26 changes: 14 additions & 12 deletions go/launcher/proxy/driverhub/driver_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,19 @@ type WebDriverSession struct {
*mux.Router
*WebDriverHub
webdriver.WebDriver
ID int
handler HandlerFunc
sessionPath string
Desired map[string]interface{}
ID int
handler HandlerFunc
sessionPath string
RequestedCaps capabilities.Spec

mu sync.RWMutex
stopped bool
}

// A handlerProvider wraps another HandlerFunc to create a new HandlerFunc.
// If the second return value is false, then the provider did not construct a new HandlerFunc.
type handlerProvider func(session *WebDriverSession, desired map[string]interface{}, base HandlerFunc) (HandlerFunc, bool)
// TODO(juangj): Update type of caps to capabilities.Spec.
type handlerProvider func(session *WebDriverSession, caps map[string]interface{}, base HandlerFunc) (HandlerFunc, bool)

// HandlerFunc is a func for handling a request to a WebDriver session.
type HandlerFunc func(context.Context, Request) (Response, error)
Expand Down Expand Up @@ -86,29 +87,30 @@ var providers = []handlerProvider{}
// HandlerProviderFunc(hp3)
//
// The generated handler will be constructed as follows:
// hp3(session, desired, hp2(session, desired, hp1(session, desired, base)))
// hp3(session, caps, hp2(session, caps, hp1(session, caps, base)))
// where base is the a default function that forwards commands to WebDriver unchanged.
func HandlerProviderFunc(provider handlerProvider) {
providers = append(providers, provider)
}

func createHandler(session *WebDriverSession, desired map[string]interface{}) HandlerFunc {
func createHandler(session *WebDriverSession, caps capabilities.Spec) HandlerFunc {
handler := createBaseHandler(session.WebDriver)

for _, provider := range providers {
if h, ok := provider(session, desired, handler); ok {
// TODO(juangj): Modify all handler providers to deal with capabilities.Specs.
if h, ok := provider(session, caps.OSSCaps, handler); ok {
handler = h
}
}
return handler
}

// CreateSession creates a WebDriverSession object.
func CreateSession(id int, hub *WebDriverHub, driver webdriver.WebDriver, desired map[string]interface{}) (*WebDriverSession, error) {
func CreateSession(id int, hub *WebDriverHub, driver webdriver.WebDriver, caps capabilities.Spec) (*WebDriverSession, error) {
sessionPath := fmt.Sprintf("/wd/hub/session/%s", driver.SessionID())
session := &WebDriverSession{ID: id, WebDriverHub: hub, WebDriver: driver, sessionPath: sessionPath, Router: mux.NewRouter(), Desired: desired}
session := &WebDriverSession{ID: id, WebDriverHub: hub, WebDriver: driver, sessionPath: sessionPath, Router: mux.NewRouter(), RequestedCaps: caps}

session.handler = createHandler(session, desired)
session.handler = createHandler(session, caps)
// Route for commands for this session.
session.PathPrefix(sessionPath).HandlerFunc(session.defaultHandler)
// Route for commands for some other session. If this happens, the hub has
Expand Down Expand Up @@ -139,7 +141,7 @@ func (s *WebDriverSession) unknownCommand(w http.ResponseWriter, r *http.Request

// Quit can be called by handlers to quit this session.
func (s *WebDriverSession) Quit(ctx context.Context, _ Request) (Response, error) {
if err := s.quit(ctx, capabilities.CanReuseSession(s.Desired)); err != nil {
if err := s.quit(ctx, capabilities.CanReuseSession(s.RequestedCaps)); err != nil {
return ResponseFromError(err)
}

Expand Down
1 change: 1 addition & 0 deletions go/launcher/webdriver/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ go_library(
deps = [
"//go/launcher/errors:go_default_library",
"//go/launcher/healthreporter:go_default_library",
"//go/metadata/capabilities:go_default_library",
],
)

Expand Down
32 changes: 25 additions & 7 deletions go/launcher/webdriver/webdriver.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (

"github.com/bazelbuild/rules_webtesting/go/launcher/errors"
"github.com/bazelbuild/rules_webtesting/go/launcher/healthreporter"
"github.com/bazelbuild/rules_webtesting/go/metadata/capabilities"
)

const (
Expand Down Expand Up @@ -158,12 +159,29 @@ func (j *jsonResp) isError() bool {

// CreateSession creates a new WebDriver session with desired capabilities from server at addr
// and ensures that the browser connection is working. It retries up to attempts - 1 times.
func CreateSession(ctx context.Context, addr string, attempts int, desired map[string]interface{}) (WebDriver, error) {
if desired == nil {
desired = map[string]interface{}{}
func CreateSession(ctx context.Context, addr string, attempts int, spec capabilities.Spec) (WebDriver, error) {
if spec.OSSCaps == nil && spec.W3CCaps == nil {
spec = capabilities.Spec{
OSSCaps: map[string]interface{}{},
W3CCaps: map[string]interface{}{},
}
}

reqBody := map[string]interface{}{"desiredCapabilities": desired}
reqBody := map[string]interface{}{}
if spec.OSSCaps != nil {
reqBody["desiredCapabilities"] = spec.OSSCaps
}
if spec.W3CCaps != nil {
reqBody["capabilities"] = map[string]interface{}{"alwaysMatch": spec.W3CCaps}
}
if len(reqBody) == 0 {
reqBody = map[string]interface{}{
"desiredCapabilities": map[string]interface{}{},
"capabilities": map[string]interface{}{
"alwaysMatch": map[string]interface{}{},
},
}
}

urlPrefix, err := url.Parse(addr)
if err != nil {
Expand Down Expand Up @@ -234,7 +252,7 @@ func CreateSession(ctx context.Context, addr string, attempts int, desired map[s
sessionID: session,
capabilities: caps,
client: client,
scriptTimeout: scriptTimeout(desired),
scriptTimeout: scriptTimeout(spec),
w3c: respBody.Status == nil,
}

Expand Down Expand Up @@ -637,8 +655,8 @@ return {"X0": Math.round(left), "Y0": Math.round(top), "X1": Math.round(left + r
return image.Rect(bounds.X0, bounds.Y0, bounds.X1, bounds.Y1), err
}

func scriptTimeout(desired map[string]interface{}) time.Duration {
timeouts, ok := desired["timeouts"].(map[string]interface{})
func scriptTimeout(caps capabilities.Spec) time.Duration {
timeouts, ok := caps.OSSCaps["timeouts"].(map[string]interface{})
if !ok {
return 0
}
Expand Down

0 comments on commit 0508435

Please sign in to comment.