github.com/bazelbuild/rules_webtesting@v0.2.0/go/metadata/capabilities/capabilities.go (about)

     1  // Copyright 2016 Google Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package capabilities performs operations on maps representing WebDriver capabilities.
    16  package capabilities
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"reflect"
    22  	"regexp"
    23  	"strings"
    24  )
    25  
    26  // See https://w3c.github.io/webdriver/webdriver-spec.html#capabilities
    27  var w3cSupportedCapabilities = []string{
    28  	"acceptInsecureCerts",
    29  	"browserName",
    30  	"browserVersion",
    31  	"pageLoadStrategy",
    32  	"platformName",
    33  	"proxy",
    34  	"setWindowRect",
    35  	"timeouts",
    36  	"unhandledPromptBehavior",
    37  }
    38  
    39  // Capabilities is a WebDriver capabilities object. It is modeled after W3C capabilities, but supports
    40  // use as W3C, JWP, or mixed-mode.
    41  type Capabilities struct {
    42  	AlwaysMatch  map[string]interface{}
    43  	FirstMatch   []map[string]interface{}
    44  	W3CSupported bool
    45  }
    46  
    47  // FromNewSessionArgs creates a Capabilities object from the arguments to new session.
    48  // AlwaysMatch will be the result of merging alwaysMatch, requiredCapabilities, and desiredCapabilities.
    49  // Unlike Metadata capabilities merging and MergeOver, this is a shallow merge, and any conflicts will
    50  // result in an error.
    51  // Additionally if any capability in firstMatch conflicts with a capability in alwaysMatch, requiredCapabilities,
    52  // or desiredCapabilities, an error will be returned.
    53  func FromNewSessionArgs(args map[string]interface{}) (*Capabilities, error) {
    54  	always := map[string]interface{}{}
    55  	var first []map[string]interface{}
    56  
    57  	w3c, _ := args["capabilities"].(map[string]interface{})
    58  
    59  	if w3c != nil {
    60  		if am, ok := w3c["alwaysMatch"].(map[string]interface{}); ok {
    61  			for k, v := range am {
    62  				always[k] = normalize(k, v)
    63  			}
    64  		}
    65  	}
    66  
    67  	if required, ok := args["requiredCapabilities"].(map[string]interface{}); ok {
    68  		for k, v := range required {
    69  			nv := normalize(k, v)
    70  			if a, ok := always[k]; ok {
    71  				if !reflect.DeepEqual(a, nv) {
    72  					return nil, fmt.Errorf("alwaysMatch[%q] == %+v, required[%q] == %+v, they must be equal", k, a, k, v)
    73  				}
    74  				continue
    75  			}
    76  			always[k] = nv
    77  
    78  		}
    79  	}
    80  
    81  	if desired, ok := args["desiredCapabilities"].(map[string]interface{}); ok {
    82  		for k, v := range desired {
    83  			nv := normalize(k, v)
    84  			if a, ok := always[k]; ok {
    85  				if !reflect.DeepEqual(a, nv) {
    86  					return nil, fmt.Errorf("alwaysMatch|required[%q] == %+v, desired[%q] == %+v, they must be equal", k, a, k, v)
    87  				}
    88  				continue
    89  			}
    90  			always[k] = nv
    91  		}
    92  	}
    93  
    94  	if w3c != nil {
    95  		fm, _ := w3c["firstMatch"].([]interface{})
    96  
    97  		for _, e := range fm {
    98  			fme, ok := e.(map[string]interface{})
    99  			if !ok {
   100  				return nil, fmt.Errorf("firstMatch entries must be JSON Objects, found %#v", e)
   101  			}
   102  			newFM := map[string]interface{}{}
   103  			for k, v := range fme {
   104  				nv := normalize(k, v)
   105  				if a, ok := always[k]; ok {
   106  					if !reflect.DeepEqual(a, nv) {
   107  						return nil, fmt.Errorf("alwaysMatch|required|desired[%q] == %+v, firstMatch[%q] == %+v, they must be equal", k, a, k, v)
   108  					}
   109  					continue
   110  				}
   111  				newFM[k] = nv
   112  
   113  			}
   114  			first = append(first, newFM)
   115  		}
   116  	}
   117  
   118  	return &Capabilities{
   119  		AlwaysMatch:  always,
   120  		FirstMatch:   first,
   121  		W3CSupported: w3c != nil,
   122  	}, nil
   123  }
   124  
   125  func normalize(key string, value interface{}) interface{} {
   126  	if key != "proxy" {
   127  		return value
   128  	}
   129  
   130  	proxy, ok := value.(map[string]interface{})
   131  	if !ok {
   132  		return value
   133  	}
   134  
   135  	// If the value if a proxy config, normalize by removing nulls and ensuring proxyType is lower case.
   136  	out := map[string]interface{}{}
   137  
   138  	for k, v := range proxy {
   139  		if v == nil {
   140  			continue
   141  		}
   142  		if k == "proxyType" {
   143  			out[k] = strings.ToLower(v.(string))
   144  			continue
   145  		}
   146  		out[k] = v
   147  	}
   148  
   149  	return out
   150  }
   151  
   152  // MergeOver creates a new Capabilities with AlwaysMatch == (c.AlwaysMatch deeply merged over other),
   153  // FirstMatch == c.FirstMatch, and W3Supported == c.W3CSupported.
   154  func (c *Capabilities) MergeOver(other map[string]interface{}) *Capabilities {
   155  	if c == nil {
   156  		return &Capabilities{
   157  			AlwaysMatch: other,
   158  		}
   159  	}
   160  
   161  	if len(other) == 0 {
   162  		return c
   163  	}
   164  	always := map[string]interface{}{}
   165  	first := map[string]interface{}{}
   166  
   167  	for k, v := range other {
   168  		if anyContains(c.FirstMatch, k) {
   169  			first[k] = v
   170  		} else {
   171  			always[k] = v
   172  		}
   173  	}
   174  
   175  	firstMatch := c.FirstMatch
   176  	if len(first) != 0 {
   177  		firstMatch = nil
   178  		for _, fm := range c.FirstMatch {
   179  			firstMatch = append(firstMatch, Merge(first, fm))
   180  		}
   181  	}
   182  
   183  	alwaysMatch := Merge(always, c.AlwaysMatch)
   184  
   185  	return &Capabilities{
   186  		AlwaysMatch:  alwaysMatch,
   187  		FirstMatch:   firstMatch,
   188  		W3CSupported: c.W3CSupported,
   189  	}
   190  }
   191  
   192  func anyContains(maps []map[string]interface{}, key string) bool {
   193  	for _, m := range maps {
   194  		_, ok := m[key]
   195  		if ok {
   196  			return true
   197  		}
   198  	}
   199  
   200  	return false
   201  }
   202  
   203  // ToJWP creates a map suitable for use as arguments to a New Session request for JSON Wire Protocol remote ends.
   204  // Since JWP does not support an equivalent to FirstMatch, if FirstMatch contains more than 1 entry
   205  // then this returns an error (if it contains exactly 1 entry, it will be merged over AlwaysMatch).
   206  func (c *Capabilities) ToJWP() (map[string]interface{}, error) {
   207  	if c == nil {
   208  		return map[string]interface{}{
   209  			"desiredCapabilities": map[string]interface{}{},
   210  		}, nil
   211  	}
   212  
   213  	if len(c.FirstMatch) > 1 {
   214  		return nil, errors.New("can not convert Capabilities with multiple FirstMatch entries to JWP")
   215  	}
   216  
   217  	desired := c.AlwaysMatch
   218  	if len(c.FirstMatch) == 1 {
   219  		desired = Merge(desired, c.FirstMatch[0])
   220  	}
   221  
   222  	return map[string]interface{}{
   223  		"desiredCapabilities": desired,
   224  	}, nil
   225  }
   226  
   227  // ToW3C creates a map suitable for use as arguments to a New Session request for W3C remote ends.
   228  func (c *Capabilities) ToW3C() map[string]interface{} {
   229  	if c == nil {
   230  		return map[string]interface{}{
   231  			"capabilities": map[string]interface{}{},
   232  		}
   233  	}
   234  
   235  	caps := map[string]interface{}{}
   236  
   237  	alwaysMatch := w3cCapabilities(c.AlwaysMatch)
   238  	var firstMatch []map[string]interface{}
   239  
   240  	for _, fm := range c.FirstMatch {
   241  		firstMatch = append(firstMatch, w3cCapabilities(fm))
   242  	}
   243  
   244  	if len(alwaysMatch) != 0 {
   245  		caps["alwaysMatch"] = alwaysMatch
   246  	}
   247  
   248  	if len(firstMatch) != 0 {
   249  		caps["firstMatch"] = firstMatch
   250  	}
   251  
   252  	return map[string]interface{}{
   253  		"capabilities": caps,
   254  	}
   255  }
   256  
   257  // w3cCapabilities remove non-W3C capabilities.
   258  func w3cCapabilities(in map[string]interface{}) map[string]interface{} {
   259  	out := map[string]interface{}{}
   260  
   261  	for k, v := range in {
   262  		// extension capabilities
   263  		if strings.Contains(k, ":") {
   264  			out[k] = v
   265  			continue
   266  		}
   267  		for _, a := range w3cSupportedCapabilities {
   268  			if k == a {
   269  				out[k] = v
   270  				break
   271  			}
   272  		}
   273  	}
   274  
   275  	return out
   276  }
   277  
   278  // ToMixedMode creates a map suitable for use as arguments to a New Session request for arbitrary remote ends.
   279  // If FirstMatch contains more than 1 entry then this returns W3C-only capabilities.
   280  // If W3CSupported is false then this will return JWP-only capabilities.
   281  func (c *Capabilities) ToMixedMode() map[string]interface{} {
   282  	if c == nil {
   283  		return map[string]interface{}{
   284  			"capabilities":        map[string]interface{}{},
   285  			"desiredCapabilities": map[string]interface{}{},
   286  		}
   287  	}
   288  
   289  	jwp, err := c.ToJWP()
   290  	if err != nil {
   291  		return c.ToW3C()
   292  	}
   293  	if !c.W3CSupported {
   294  		return jwp
   295  	}
   296  
   297  	w3c := c.ToW3C()
   298  
   299  	return map[string]interface{}{
   300  		"capabilities":        w3c["capabilities"],
   301  		"desiredCapabilities": jwp["desiredCapabilities"],
   302  	}
   303  }
   304  
   305  // Strip returns a copy of c with all top-level capabilities capsToStrip and with nil values removed.
   306  func (c *Capabilities) Strip(capsToStrip ...string) *Capabilities {
   307  	am := map[string]interface{}{}
   308  	var fms []map[string]interface{}
   309  
   310  	for k, v := range c.AlwaysMatch {
   311  		if v != nil {
   312  			am[k] = v
   313  		}
   314  	}
   315  
   316  	for _, fm := range c.FirstMatch {
   317  		newFM := map[string]interface{}{}
   318  		for k, v := range fm {
   319  			if v != nil {
   320  				newFM[k] = v
   321  			}
   322  		}
   323  		fms = append(fms, newFM)
   324  	}
   325  
   326  	for _, c := range capsToStrip {
   327  		delete(am, c)
   328  		for _, fm := range fms {
   329  			delete(fm, c)
   330  		}
   331  	}
   332  
   333  	return &Capabilities{
   334  		AlwaysMatch:  am,
   335  		FirstMatch:   fms,
   336  		W3CSupported: c.W3CSupported,
   337  	}
   338  }
   339  
   340  // Merge takes two JSON objects, and merges them.
   341  //
   342  // The resulting object will have all of the keys in the two input objects.
   343  // For each key that is in both objects:
   344  //   - if both objects have objects for values, then the result object will have
   345  //     a value resulting from recursively calling Merge.
   346  //   - if both objects have lists for values, then the result object will have
   347  //     a value resulting from concatenating the two lists.
   348  //   - Otherwise the result object will have the value from the second object.
   349  func Merge(m1, m2 map[string]interface{}) map[string]interface{} {
   350  	if m1 == nil {
   351  		return m2
   352  	}
   353  	if m2 == nil {
   354  		return m1
   355  	}
   356  	nm := map[string]interface{}{}
   357  	for k, v := range m1 {
   358  		nm[k] = v
   359  	}
   360  	for k, v := range m2 {
   361  		nm[k] = mergeValues(nm[k], v, k)
   362  	}
   363  	return nm
   364  }
   365  
   366  func mergeValues(j1, j2 interface{}, name string) interface{} {
   367  	switch t1 := j1.(type) {
   368  	case map[string]interface{}:
   369  		if t2, ok := j2.(map[string]interface{}); ok {
   370  			return Merge(t1, t2)
   371  		}
   372  	case []interface{}:
   373  		if t2, ok := j2.([]interface{}); ok {
   374  			if name == "args" {
   375  				return mergeArgs(t1, t2)
   376  			}
   377  			return mergeLists(t1, t2)
   378  		}
   379  	}
   380  	return j2
   381  }
   382  
   383  func mergeLists(m1, m2 []interface{}) []interface{} {
   384  	if m1 == nil {
   385  		return m2
   386  	}
   387  	if m2 == nil {
   388  		return m1
   389  	}
   390  	nl := []interface{}{}
   391  	nl = append(nl, m1...)
   392  	nl = append(nl, m2...)
   393  	return nl
   394  }
   395  
   396  func mergeArgs(m1, m2 []interface{}) []interface{} {
   397  	m2Opts := map[string]bool{}
   398  
   399  	for _, a := range m2 {
   400  		if arg, ok := a.(string); ok {
   401  			if strings.HasPrefix(arg, "--") {
   402  				tokens := strings.Split(arg, "=")
   403  				m2Opts[tokens[0]] = true
   404  			}
   405  		}
   406  	}
   407  
   408  	nl := []interface{}{}
   409  
   410  	for _, a := range m1 {
   411  		if arg, ok := a.(string); ok {
   412  			if strings.HasPrefix(arg, "--") {
   413  				tokens := strings.Split(arg, "=")
   414  				// Skip options from m1 that are redefined in m2
   415  				if m2Opts[tokens[0]] {
   416  					continue
   417  				}
   418  			}
   419  		}
   420  		nl = append(nl, a)
   421  	}
   422  
   423  	nl = append(nl, m2...)
   424  	return nl
   425  }
   426  
   427  // CanReuseSession returns true if the "google:canReuseSession" is set.
   428  func CanReuseSession(caps *Capabilities) bool {
   429  	reuse, _ := caps.AlwaysMatch["google:canReuseSession"].(bool)
   430  	return reuse
   431  }
   432  
   433  // A Resolver resolves a prefix, name pair to a replacement value.
   434  type Resolver func(prefix, name string) (string, error)
   435  
   436  // NoOPResolver resolves to %prefix:name%.
   437  func NoOPResolver(prefix, name string) (string, error) {
   438  	return "%" + prefix + ":" + name + "%", nil
   439  }
   440  
   441  // MapResolver returns a new Resolver that uses key-value pairs in names to
   442  // resolve names for prefix, and otherwise uses the NoOPResolver.
   443  func MapResolver(prefix string, names map[string]string) Resolver {
   444  	return func(p, n string) (string, error) {
   445  		if p == prefix {
   446  			v, ok := names[n]
   447  			if !ok {
   448  				return "", fmt.Errorf("unable to resolve %s:%s", p, n)
   449  			}
   450  			return v, nil
   451  		}
   452  		return NoOPResolver(p, n)
   453  	}
   454  }
   455  
   456  // Resolve returns a new Capabilities object with all %PREFIX:NAME% substrings replaced using resolver.
   457  func (c *Capabilities) Resolve(resolver Resolver) (*Capabilities, error) {
   458  	am, err := resolveMap(c.AlwaysMatch, resolver)
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  
   463  	var fms []map[string]interface{}
   464  
   465  	for _, fm := range c.FirstMatch {
   466  		u, err := resolveMap(fm, resolver)
   467  		if err != nil {
   468  			return nil, err
   469  		}
   470  		fms = append(fms, u)
   471  	}
   472  
   473  	return &Capabilities{
   474  		AlwaysMatch:  am,
   475  		FirstMatch:   fms,
   476  		W3CSupported: c.W3CSupported,
   477  	}, nil
   478  }
   479  
   480  func resolve(v interface{}, resolver Resolver) (interface{}, error) {
   481  	switch tv := v.(type) {
   482  	case string:
   483  		return resolveString(tv, resolver)
   484  	case []interface{}:
   485  		return resolveSlice(tv, resolver)
   486  	case map[string]interface{}:
   487  		return resolveMap(tv, resolver)
   488  	default:
   489  		return v, nil
   490  	}
   491  }
   492  
   493  func resolveMap(m map[string]interface{}, resolver Resolver) (map[string]interface{}, error) {
   494  	caps := map[string]interface{}{}
   495  
   496  	for k, v := range m {
   497  		u, err := resolve(v, resolver)
   498  		if err != nil {
   499  			return nil, err
   500  		}
   501  
   502  		caps[k] = u
   503  	}
   504  
   505  	return caps, nil
   506  }
   507  
   508  func resolveSlice(l []interface{}, resolver Resolver) ([]interface{}, error) {
   509  	caps := []interface{}{}
   510  
   511  	for _, v := range l {
   512  		u, err := resolve(v, resolver)
   513  		if err != nil {
   514  			return nil, err
   515  		}
   516  		caps = append(caps, u)
   517  	}
   518  
   519  	return caps, nil
   520  }
   521  
   522  var varRegExp = regexp.MustCompile(`%(\w+):(\w+)%`)
   523  
   524  func resolveString(s string, resolver Resolver) (string, error) {
   525  	result := ""
   526  	previous := 0
   527  	for _, match := range varRegExp.FindAllStringSubmatchIndex(s, -1) {
   528  		// Append everything after the previous match to the beginning of this match
   529  		result += s[previous:match[0]]
   530  		// Set previous to the first character after this match
   531  		previous = match[1]
   532  
   533  		prefix := s[match[2]:match[3]]
   534  		varName := s[match[4]:match[5]]
   535  
   536  		value, err := resolver(prefix, varName)
   537  		if err != nil {
   538  			return "", err
   539  		}
   540  
   541  		result += value
   542  	}
   543  
   544  	// Append everything after the last match
   545  	result += s[previous:]
   546  
   547  	return result, nil
   548  }