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 }