blob: 7a78141dd8f00d2432b02009dd4a45bde6876e7c [file] [log] [blame]
Yang Gu9536e752017-10-08 05:47:02 +08001# -*- coding: utf-8 -*-
2import argparse
3import atexit
4import datetime
5import inspect
6import json
7import logging
8import os
9import platform
10import re
11import urllib2
12import shutil
13import socket
14import subprocess
15import sys
16import time
17
18try:
19 import selenium
20 from selenium import webdriver
21 from selenium.common.exceptions import NoSuchElementException
22 from selenium.common.exceptions import TimeoutException
23 from selenium.common.exceptions import WebDriverException
24 from selenium.webdriver.support.select import Select
25 from selenium.webdriver.support.ui import WebDriverWait
26except ImportError:
27 print('Please install package selenium')
28 exit(1)
29
30
31class Util(object):
32 LOGGER_NAME = __file__
33
34 @staticmethod
35 def diff_list(a, b):
36 return list(set(a).difference(set(b)))
37
38 @staticmethod
39 def intersect_list(a, b):
40 return list(set(a).intersection(set(b)))
41
42 @staticmethod
43 def ensure_dir(dir_path):
44 if not os.path.exists(dir_path):
45 os.makedirs(dir_path)
46
47 @staticmethod
48 def ensure_nodir(dir_path):
49 if os.path.exists(dir_path):
50 shutil.rmtree(dir_path)
51
52 @staticmethod
53 def ensure_file(file_path):
54 Util.ensure_dir(os.path.dirname(os.path.abspath(file_path)))
55 if not os.path.exists(file_path):
56 Cmd('touch ' + file_path)
57
58 @staticmethod
59 def ensure_nofile(file_path):
60 if os.path.exists(file_path):
61 os.remove(file_path)
62
63 @staticmethod
64 def error(msg):
65 _logger = Util.get_logger()
66 _logger.error(msg)
67 exit(1)
68
69 @staticmethod
70 def not_implemented():
71 Util.error('not_mplemented() at line %s' % inspect.stack()[1][2])
72
73 @staticmethod
74 def get_caller_name():
75 return inspect.stack()[1][3]
76
77 @staticmethod
78 def get_datetime(format='%Y%m%d%H%M%S'):
79 return time.strftime(format, time.localtime())
80
81 @staticmethod
82 def get_env(env):
83 return os.getenv(env)
84
85 @staticmethod
86 def set_env(env, value):
87 if value:
88 os.environ[env] = value
89
90 @staticmethod
91 def unset_env(env):
92 if env in os.environ:
93 del os.environ[env]
94
95 @staticmethod
96 def get_executable_suffix(host_os):
97 if host_os.is_win():
98 return '.exe'
99 else:
100 return ''
101
102 @staticmethod
103 def get_logger():
104 return logging.getLogger(Util.LOGGER_NAME)
105
106 @staticmethod
107 def set_logger(log_file, level, show_time=False):
108 if show_time:
109 formatter = logging.Formatter('[%(asctime)s - %(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S')
110 else:
111 formatter = logging.Formatter('[%(levelname)s] %(message)s')
112 logger = logging.getLogger(Util.LOGGER_NAME)
113 logger.setLevel(level)
114
115 log_file = logging.FileHandler(log_file)
116 log_file.setFormatter(formatter)
117 logger.addHandler(log_file)
118
119 console = logging.StreamHandler()
120 console.setFormatter(formatter)
121 logger.addHandler(console)
122
123 @staticmethod
124 def has_pkg(pkg):
125 cmd = Cmd('dpkg -s ' + pkg)
126 if cmd.status:
127 return False
128 else:
129 return True
130
131 @staticmethod
132 def read_file(file_path):
133 if not os.path.exists(file_path):
134 return []
135
136 f = open(file_path)
137 lines = [line.rstrip('\n') for line in f]
138 if len(lines) > 0:
139 while (lines[-1] == ''):
140 del lines[-1]
141 f.close()
142 return lines
143
144 @staticmethod
145 def use_slash(s):
146 if s:
147 return s.replace('\\', '/')
148 else:
149 return s
150
151
152class AndroidDevice(object):
153 def __init__(self, id):
154 self.id = id
155
156 def get_prop(self, key):
157 cmd = AdbShellCmd('getprop | grep %s' % key, device_id=self.id)
158 match = re.search('\[%s\]: \[(.*)\]' % key, cmd.output)
159 if match:
160 return match.group(1)
161 else:
162 Util.error('Could not find %s' % key)
163
164
165class AndroidDevices():
166 def __init__(self):
167 self.devices = []
168 cmd = Cmd('adb devices')
169 for device_line in cmd.output.split('\n'):
170 if re.match('List of devices attached', device_line):
171 continue
172 elif re.match('^\s*$', device_line):
173 continue
174 elif re.search('offline', device_line):
175 continue
176 else:
177 id = device_line.split()[0]
178 self.devices.append(AndroidDevice(id))
179
180 if len(self.devices) < 1:
181 Util.error('Could not find available Android device')
182
183 def get_device(self, device_id):
184 if not device_id:
185 if len(self.devices) > 1:
186 self._logger.warning('There are more than one devices, and the first one will be used')
187 return self.devices[0]
188 else:
189 for device in self.devices:
190 if device.id == device_id:
191 return device
192 else:
193 Util.error('Cound not find Android device with id %s' % device_id)
194
195
196class Cmd(object):
197 def __init__(self, cmd, show_cmd=False, dryrun=False, abort=False):
198 self._logger = Util.get_logger()
199 self.cmd = cmd
200 self.show_cmd = show_cmd
201 self.dryrun = dryrun
202 self.abort = abort
203
204 if self.show_cmd:
205 self._logger.info('[CMD]: %s' % self.cmd)
206
207 if self.dryrun:
208 self.status = 0
209 self.output = ''
210 self.process = None
211 return
212
213 tmp_output = ''
214 process = subprocess.Popen(self.cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
215 while True:
216 nextline = process.stdout.readline()
217 if nextline == '' and process.poll() is not None:
218 break
219 tmp_output += nextline
220
221 self.status = process.returncode
222 (out, error) = process.communicate()
223 self.output = tmp_output + out + error
224 self.process = process
225
226 if self.abort and self.status:
227 Util.error('Failed to execute %s' % cmd, error_code=self.status)
228
229
230class AdbShellCmd(Cmd):
231 def __init__(self, cmd, device_id, dryrun=False, abort=False):
232 fail_str = 'FAIL'
233 cmd = 'adb -s %s shell "(%s) || echo %s"' % (device_id, cmd, fail_str)
234 super(AdbShellCmd, self).__init__(cmd, dryrun, abort=False)
235 if re.search('FAIL', self.output):
236 if abort:
237 Util.error('Failed to execute %s' % cmd, error_code=self.status)
238 self.status = False
239 else:
240 self.status = True
241
242
243class Timer(object):
244 def __init__(self, use_ms=False):
245 self.use_ms = use_ms
246 self.timer = [0, 0]
247 if self.use_ms:
248 self.timer[0] = datetime.datetime.now()
249 else:
250 self.timer[0] = datetime.datetime.now().replace(microsecond=0)
251
252 def stop(self):
253 if self.use_ms:
254 self.timer[1] = datetime.datetime.now()
255 else:
256 self.timer[1] = datetime.datetime.now().replace(microsecond=0)
257
258 def diff(self):
259 return self.timer[1] - self.timer[0]
260
261
262class GPU(object):
263 VENDOR_NAME_ID = {
264 'amd': '1002',
265 'intel': '8086',
266 'nvidia': '10DE',
267 'qualcomm': '5143',
268 }
269 VENDOR_NAMES = VENDOR_NAME_ID.keys()
270
271 # produce info is from https://en.wikipedia.org/wiki/List_of_Intel_graphics_processing_units
272 INTEL_GEN_ID = {
273 '6': '0102,0106,0112,0116,0122,0126,010A',
274 '7': '0152,0156,015A,0162,0166,016A',
275 '7.5': '0402,0406,040A,040B,040E,0A02,0A06,0A0A,0A0B,0A0E,0C02,0C06,0C0A,0C0B,0C0E,0D02,0D06,0D0A,0D0B,0D0E' +
276 '0412,0416,041A,041B,041E,0A12,0A16,0A1A,0A1B,0A1E,0C12,0C16,0C1A,0C1B,0C1E,0D12,0D16,0D1A,0D1B,0D1E' +
277 '0422,0426,042A,042B,042E,0A22,0A26,0A2A,0A2B,0A2E,0C22,0C26,0C2A,0C2B,0C2E,0D22,0D26,0D2A,0D2B,0D2E',
278 '8': '1606,161E,1616,1612,1626,162B,1622,22B0,22B1,22B2,22B3',
279 '9': '1906,1902,191E,1916,191B,1912,191D,1926,193B,193D,0A84,1A84,1A85,5A84,5A85',
280 '9.5': '5912',
281 }
282
283 def __init__(self, vendor_name, vendor_id, product_name, product_id, driver_version):
284 self.vendor_name = vendor_name.lower()
285 self.vendor_id = vendor_id
286 # We may not get vendor_name and vendor_id at the same time. For example, only vendor_name is available on Android.
287 for vendor_name in self.VENDOR_NAME_ID:
288 if self._is_vendor_name(vendor_name):
289 if not self.vendor_name:
290 self.vendor_name = vendor_name
291 if not self.vendor_id:
292 self.vendor_id = self.VENDOR_NAME_ID[vendor_name]
293
294 self.product_name = product_name
295 self.product_id = product_id
296 self.driver_version = driver_version
297
298 # intel_gen
299 if self.is_intel():
300 for gen in self.INTEL_GEN_ID:
301 if self.product_id in self.INTEL_GEN_ID[gen]:
302 self.intel_gen = gen
303 break
304 else:
305 self.intel_gen = ''
306 else:
307 self.intel_gen = ''
308
309 def is_amd(self):
310 return self._is_vendor_name('amd')
311
312 def is_intel(self):
313 return self._is_vendor_name('intel')
314
315 def is_nvidia(self):
316 return self._is_vendor_name('nvidia')
317
318 def is_qualcomm(self):
319 return self._is_vendor_name('qualcomm')
320
321 def _is_vendor_name(self, vendor_name):
322 return self.vendor_name == vendor_name or self.vendor_id == self.VENDOR_NAME_ID[vendor_name]
323
324 def __str__(self):
325 return json.dumps({
326 'vendor_name': self.vendor_name,
327 'vendor_id': self.vendor_id,
328 'product_name': self.product_name,
329 'product_id': self.product_id,
330 'intel_gen': self.intel_gen
331 })
332
333
334class GPUs(object):
335 def __init__(self, os, android_device, driver=None):
336 self._logger = Util.get_logger()
337 self.gpus = []
338
339 vendor_name = []
340 vendor_id = []
341 product_name = []
342 product_id = []
343 driver_version = []
344
345 if os.is_android():
346 cmd = AdbShellCmd('dumpsys | grep GLES', android_device.id)
347 for line in cmd.output.split('\n'):
348 if re.match('GLES', line):
349 fields = line.replace('GLES:', '').strip().split(',')
350 vendor_name.append(fields[0])
351 vendor_id.append('')
352 product_name.append(fields[1])
353 product_id.append('')
354 driver_version.append('')
355 break
356
357 elif os.is_cros():
358 driver.get('chrome://gpu')
359 try:
360 WebDriverWait(driver, 60).until(lambda driver: driver.find_element_by_id('basic-info'))
361 except TimeoutException:
362 Util.error('Could not get GPU info')
363
364 trs = driver.find_element_by_id('basic-info').find_elements_by_xpath('./div/table/tbody/tr')
365 for tr in trs:
366 tds = tr.find_elements_by_xpath('./td')
367 key = tds[0].find_element_by_xpath('./span').text
368 if key == 'GPU0':
369 value = tds[1].find_element_by_xpath('./span').text
370 match = re.search('VENDOR = 0x(\S{4}), DEVICE.*= 0x(\S{4})', value)
371 vendor_id.append(match.group(1))
372 vendor_name.append('')
373 product_id.append(match.group(2))
374 if key == 'Driver version':
375 driver_version.append(tds[1].find_element_by_xpath('./span').text)
376 if key == 'GL_RENDERER':
377 product_name.append(tds[1].find_element_by_xpath('./span').text)
378 break
379
380 elif os.is_linux():
381 cmd = Cmd('lshw -numeric -c display')
382 lines = cmd.output.split('\n')
383 for line in lines:
384 line = line.strip()
385 match = re.search('product: (.*) \[(.*)\]$', line)
386 if match:
387 product_name.append(match.group(1))
388 product_id.append(match.group(2).split(':')[1].upper())
389 match = re.search('vendor: (.*) \[(.*)\]$', line)
390 if match:
391 vendor_name.append(match.group(1))
392 vendor_id.append(match.group(2).upper())
393 driver_version.append('')
394 break
395
396 elif os.is_mac():
397 cmd = Cmd('system_profiler SPDisplaysDataType')
398 lines = cmd.output.split('\n')
399 for line in lines:
400 line = line.strip()
401 match = re.match('Chipset Model: (.*)', line)
402 if match:
403 product_name.append(match.group(1))
404 match = re.match('Vendor: (.*) \(0x(.*)\)', line)
405 if match:
406 vendor_name.append(match.group(1))
407 vendor_id.append(match.group(2))
408 match = re.match('Device ID: 0x(.*)', line)
409 if match:
410 product_id.append(match.group(1))
411 driver_version.append('')
412
413 elif os.is_win():
414 cmd = Cmd('wmic path win32_videocontroller get /format:list')
415 lines = cmd.output.split('\n')
416 for line in lines:
417 line = line.rstrip('\r')
418 match = re.match('AdapterCompatibility=(.*)', line)
419 if match:
420 vendor_name.append(match.group(1))
421 match = re.match('DriverVersion=(.*)', line)
422 if match:
423 driver_version.append(match.group(1))
424 match = re.match('Name=(.*)', line)
425 if match:
426 product_name.append(match.group(1))
427 match = re.match('PNPDeviceID=.*VEN_(\S{4})&.*DEV_(\S{4})&', line)
428 if match:
429 vendor_id.append(match.group(1))
430 product_id.append(match.group(2))
431
432 for index in range(len(vendor_name)):
433 self.gpus.append(GPU(vendor_name[index], vendor_id[index], product_name[index], product_id[index], driver_version[index]))
434
435 if len(self.gpus) < 1:
436 Util.error('Could not find any GPU')
437
438 def get_active(self, driver):
439 if not driver or len(self.gpus) == 1:
440 return self.gpus[0]
441 else:
442 try:
443 debug_info = driver.execute_script('''
444 var canvas = document.createElement("canvas");
445 var gl = canvas.getContext("webgl");
446 var ext = gl.getExtension("WEBGL_debug_renderer_info");
447 return gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) + " " + gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
448 ''')
449 except WebDriverException:
450 self._logger.warning('WEBGL_debug_renderer_info is not supported, so we assume first GPU from %s will be used' % self.gpus[0].vendor_name)
451 else:
452 for gpu in self.gpus:
453 if re.search(gpu.vendor_name, debug_info, re.I) or re.search(gpu.product_name, debug_info, re.I):
454 return gpu
455 else:
456 self._logger.warning('Could not find the active GPU, so we assume first GPU from %s will be used' % self.gpus[0].vendor_name)
457
458
459class OS(object):
460 def __init__(self, name, version=''):
461 self.name = name
462 self.version = version
463
464 def is_android(self):
465 return self._is_name('android')
466
467 def is_cros(self):
468 return self._is_name('cros')
469
470 def is_linux(self):
471 return self._is_name('linux')
472
473 def is_mac(self):
474 return self._is_name('mac')
475
476 def is_win(self):
477 return self._is_name('win')
478
479 def _is_name(self, name):
480 return self.name == name
481
482 def __str__(self):
483 return json.dumps({
484 'name': self.name,
485 'version': self.version,
486 })
487
488
489class HostOS(OS):
490 def __init__(self):
491 # name
492 system = platform.system().lower()
493 if system == 'linux':
494 cmd = Cmd('cat /etc/lsb-release')
495 if re.search('CHROMEOS', cmd.output, re.I):
496 self.name = 'cros'
497 else:
498 self.name = 'linux'
499 elif system == 'darwin':
500 self.name = 'mac'
501 elif system == 'windows':
502 self.name = 'win'
503
504 # version
505 if self.is_cros():
506 version = platform.platform()
507 elif self.is_linux():
508 version = platform.dist()[1]
509 elif self.is_mac():
510 version = platform.mac_ver()[0]
511 elif self.is_win():
512 version = platform.version()
513
514 super(HostOS, self).__init__(self.name, version)
515
516 # host_os specific variables
517 if self.is_win():
518 self.appdata = Util.use_slash(Util.get_env('APPDATA'))
519 self.programfiles = Util.use_slash(Util.get_env('PROGRAMFILES'))
520 self.programfilesx86 = Util.use_slash(Util.get_env('PROGRAMFILES(X86)'))
521 self.windir = Util.use_slash(Util.get_env('WINDIR'))
522 self.username = os.getenv('USERNAME')
523 else:
524 self.username = os.getenv('USER')
525
526 def __str__(self):
527 str_dict = json.loads(super(HostOS, self).__str__())
528 if self.is_win():
529 str_dict['username'] = self.username
530 return json.dumps(str_dict)
531
532
533class AndroidOS(OS):
534 def __init__(self, device):
535 version = device.get_prop('ro.build.version.release')
536 super(AndroidOS, self).__init__('android', version)
537
538
539class Browser(object):
540 def __init__(self, name, path, options, os):
541 self.name = name
542 self.os = os
543 self.version = ''
544 self._logger = Util.get_logger()
545
546 # path
547 if path:
548 self.path = Util.use_slash(path)
549 elif self.os.is_android():
550 if self.name == 'chrome_stable' or self.name == 'chrome':
551 self.path = '/data/app/com.android.chrome-1'
552 elif self.os.is_cros():
553 self.path = '/opt/google/chrome/chrome'
554 elif self.os.is_linux():
555 if self.name == 'chrome':
556 self.path = '/opt/google/chrome/google-chrome'
557 elif self.os.is_mac():
558 if self.name == 'chrome' or self.name == 'chrome_stable':
559 self.path = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
560 if self.name == 'chrome_canary':
561 self.path = '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
562 elif self.os.is_win():
563 if self.name == 'chrome' or self.name == 'chrome_stable':
564 self.path = '%s/Google/Chrome/Application/chrome.exe' % self.os.programfilesx86
565 if self.name == 'chrome_canary':
566 self.path = '%s/../Local/Google/Chrome SxS/Application/chrome.exe' % self.os.appdata
567 elif self.name == 'firefox' or self.name == 'firefox_stable':
568 self.path = '%s/Mozilla Firefox/firefox.exe' % self.os.programfilesx86
569 elif self.name == 'firefox_nightly':
570 self.path = '%s/Nightly/firefox.exe' % self.os.programfiles
571 elif self.name == 'edge':
572 self.path = '%s/systemapps/Microsoft.MicrosoftEdge_8wekyb3d8bbwe/MicrosoftEdge.exe' % self.os.windir
573 else:
574 Util.not_implemented()
575
576 # option
577 self.options = options
578 if self.is_chrome():
579 if not self.os.is_android() and not self.os.is_cros():
580 self.options.append('--disk-cache-size=1')
581 if self.os.is_linux():
582 self.options.append('--disk-cache-dir=/dev/null')
583
584 # fullscreen to ensure webdriver can test correctly
585 if os.is_linux() or os.is_win():
586 self.options.append('--start-maximized')
587 # --start-maximized doesn't work on mac
588 elif os.is_mac():
589 self.options.append('--start-fullscreen')
590
591 def update(self, driver):
592 # version
593 if not self.os.is_win():
594 ua = driver.execute_script('return navigator.userAgent;')
595 if self.is_chrome():
596 match = re.search('Chrome/(.*) ', ua)
597 elif self.is_edge():
598 match = re.search('Edge/(.*)$', ua)
599 elif self.is_firefox():
600 match = re.search('rv:(.*)\)', ua)
601 if match:
602 self.version = match.group(1)
603
604 def is_chrome(self):
605 return self._is_browser('chrome')
606
607 def is_edge(self):
608 return self._is_browser('edge')
609
610 def is_firefox(self):
611 return self._is_browser('firefox')
612
613 def is_safari(self):
614 return self._is_browser('safari')
615
616 def _is_browser(self, name):
617 return re.search(name, self.name, re.I)
618
619 def __str__(self):
620 return json.dumps({
621 'name': self.name,
622 'path': self.path,
623 'options': ','.join(self.options),
624 })
625
626
627class Webdriver(object):
628 CHROME_WEBDRIVER_NAME = 'chromedriver'
629 EDGE_WEBDRIVER_NAME = 'MicrosoftWebDriver'
630 FIREFOX_WEBDRIVER_NAME = 'geckodriver'
631
632 ANDROID_CHROME_NAME_PKG = {
633 'chrome': 'com.android.chrome',
634 'chrome_stable': 'com.android.chrome',
635 'chrome_beta': 'com.chrome.beta',
636 'chrome_public': 'org.chromium.chrome',
637 }
638
639 def __init__(self, path, browser, host_os, target_os, android_device=None, debug=False):
640 self._logger = Util.get_logger()
641 self.path = path
642 self.target_os = target_os
643
644 # path
645 if target_os.is_cros():
646 self.path = '/usr/local/chromedriver/chromedriver'
647
648 executable_suffix = Util.get_executable_suffix(host_os)
649 if not self.path and browser.is_chrome() and host_os == target_os:
650 if host_os.is_mac():
651 browser_dir = browser.path.replace('/Chromium.app/Contents/MacOS/Chromium', '')
652 else:
653 browser_dir = os.path.dirname(os.path.realpath(browser.path))
654 tmp_path = Util.use_slash(browser_dir + '/chromedriver')
655 tmp_path += executable_suffix
656 if os.path.exists(tmp_path):
657 self.path = tmp_path
658
659 if not self.path:
660 tmp_path = 'webdriver/%s/' % host_os.name
661 if browser.is_chrome():
662 tmp_path += self.CHROME_WEBDRIVER_NAME
663 elif browser.is_edge():
664 tmp_path += self.EDGE_WEBDRIVER_NAME
665 elif browser.is_firefox():
666 tmp_path += self.FIREFOX_WEBDRIVER_NAME
667 tmp_path += executable_suffix
668 if os.path.exists(tmp_path):
669 self.path = tmp_path
670
671 # webdriver
672 if target_os.is_android() or target_os.is_cros():
673 # This needs to be done before server process is created
674 if target_os.is_cros():
675 from telemetry.internal.browser import browser_finder, browser_options
676 finder_options = browser_options.BrowserFinderOptions()
677 finder_options.browser_type = ('system')
678 if browser.options:
679 finder_options.browser_options.AppendExtraBrowserArgs(browser.options)
680 finder_options.verbosity = 0
681 finder_options.CreateParser().parse_args(args=[])
682 b_options = finder_options.browser_options
683 b_options.disable_component_extensions_with_background_pages = False
684 b_options.create_browser_with_oobe = True
685 b_options.clear_enterprise_policy = True
686 b_options.dont_override_profile = False
687 b_options.disable_gaia_services = True
688 b_options.disable_default_apps = True
689 b_options.disable_component_extensions_with_background_pages = True
690 b_options.auto_login = True
691 b_options.gaia_login = False
692 b_options.gaia_id = b_options.gaia_id
693 open('/mnt/stateful_partition/etc/collect_chrome_crashes', 'w').close()
694 browser_to_create = browser_finder.FindBrowser(finder_options)
695 self._browser = browser_to_create.Create(finder_options)
696 self._browser.tabs[0].Close()
697
698 webdriver_args = [self.path]
699 port = self._get_unused_port()
700 webdriver_args.append('--port=%d' % port)
701 self.server_process = subprocess.Popen(webdriver_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, env=None)
702 capabilities = {}
703 capabilities['chromeOptions'] = {}
704 self.server_url = 'http://localhost:%d' % port
705
706 if target_os.is_android():
707 capabilities['chromeOptions']['androidDeviceSerial'] = android_device.id
708 capabilities['chromeOptions']['androidPackage'] = self.ANDROID_CHROME_NAME_PKG[browser.name]
709 capabilities['chromeOptions']['args'] = browser.options
710 elif target_os.is_cros():
711 remote_port = self._get_chrome_remote_debugging_port()
712 urllib2.urlopen('http://localhost:%i/json/new' % remote_port)
713 capabilities['chromeOptions']['debuggerAddress'] = ('localhost:%d' % remote_port)
714
715 self.driver = webdriver.Remote(command_executor=self.server_url, desired_capabilities=capabilities)
716 # other OS
717 else:
718 if browser.is_chrome():
719 chrome_options = selenium.webdriver.ChromeOptions()
720 for option in browser.options:
721 chrome_options.add_argument(option)
722 chrome_options.binary_location = browser.path
723 if debug:
724 service_args = ['--verbose', '--log-path=log/chromedriver.log']
725 else:
726 service_args = []
727 self.driver = selenium.webdriver.Chrome(executable_path=self.path, chrome_options=chrome_options, service_args=service_args)
728 elif browser.is_safari():
729 Util.not_implemented()
730 elif browser.is_edge():
731 self.driver = selenium.webdriver.Edge(self.path)
732 elif browser.is_firefox():
733 from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
734 capabilities = DesiredCapabilities.FIREFOX
735 capabilities['marionette'] = True
736 capabilities['binary'] = browser.path
737 self.driver = selenium.webdriver.Firefox(capabilities=capabilities, executable_path=self.path)
738
739 # check
740 if not browser.path:
741 Util.error('Could not find browser at %s' % browser.path)
742 else:
743 self._logger.info('Use browser at %s' % browser.path)
744 if not self.path:
745 Util.error('Could not find webdriver at %s' % self.path)
746 else:
747 self._logger.info('Use webdriver at %s' % self.path)
748 if not self.driver:
749 Util.error('Could not get webdriver')
750
751 atexit.register(self._quit)
752
753 def _get_chrome_remote_debugging_port(self):
754 chrome_pid = int(subprocess.check_output(['pgrep', '-o', '^chrome$']))
755 command = subprocess.check_output(['ps', '-p', str(chrome_pid), '-o', 'command='])
756 matches = re.search('--remote-debugging-port=([0-9]+)', command)
757 if matches:
758 return int(matches.group(1))
759
760 def _get_unused_port(self):
761 def try_bind(port, socket_type, socket_proto):
762 s = socket.socket(socket.AF_INET, socket_type, socket_proto)
763 try:
764 try:
765 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
766 s.bind(('', port))
767 return s.getsockname()[1]
768 except socket.error:
769 return None
770 finally:
771 s.close()
772
773 while True:
774 port = try_bind(0, socket.SOCK_STREAM, socket.IPPROTO_TCP)
775 if port and try_bind(port, socket.SOCK_DGRAM, socket.IPPROTO_UDP):
776 return port
777
778 def _quit(self):
779 self.driver.quit()
780 if self.target_os.is_android() or self.target_os.is_cros():
781 try:
782 urllib2.urlopen(self.server_url + '/shutdown', timeout=10).close()
783 except Exception:
784 pass
785 self.server_process.stdout.close()
786 self.server_process.stderr.close()
787
788 if self.target_os.is_cros():
789 self._browser.Close()
790 del self._browser
791
792
793class Status(object):
794 PASS = 'PASS'
795 FAIL = 'FAIL'
796 CRASH = 'CRASH'
797 FILTER = 'FILTER'
798 PYTIMEOUT = 'PYTIMEOUT'
799 JSTIMEOUT = 'JSTIMEOUT'
800 NOTEXIST = 'NOTEXIST'
801
802
803class Case(object):
804 def __init__(self, path='', status='', total_count=0, pass_count=0, time=0):
805 self.path = path
806 self.status = status
807 self.total_count = total_count
808 self.pass_count = pass_count
809 self.time = time
810
811 def is_pass(self):
812 return self._is_status(Status.PASS)
813
814 def is_fail(self):
815 return self._is_status(Status.FAIL)
816
817 def is_crash(self):
818 return self._is_status(Status.CRASH)
819
820 def is_filter(self):
821 return self._is_status(Status.FILTER)
822
823 def is_pytimeout(self):
824 return self._is_status(Status.PYTIMEOUT)
825
826 def is_jstimeout(self):
827 return self._is_status(Status.JSTIMEOUT)
828
829 def _is_status(self, status):
830 if self.status == status:
831 return True
832 else:
833 return False
834
835 def __str__(self):
836 return json.dumps({
837 'path': self.path,
838 'status': self.status,
839 'total_count': self.total_count,
840 'pass_count': self.pass_count,
841 'time': self.time
842 })
843
844
845class Suite(object):
846 def __init__(self, exp_suite=None):
847 self.suite = []
848 self.path_index = {}
849 self.count = 0
850 self.exp_suite = exp_suite
851
852 self.issue_path = []
853 self.filter_path = []
854 self.retry_index = []
855
856 def add_case(self, case):
857 self.suite.append(case)
858 self.path_index[case.path] = self.count
859 if not case.is_pass():
860 self.issue_path.append(case.path)
861 if case.is_filter():
862 self.filter_path.append(case.path)
863 if case.is_fail() and self.exp_suite and case.path not in self.exp_suite.issue_path:
864 self.retry_index.append(self.count)
865 self.count += 1
866
867 def get_case(self, index):
868 return self.suite[index]
869
870 def remove_issue(self, index):
871 path = self.suite[index].path
872 del self.path_index[path]
873 self.issue_path.remove(path)
874
875
876class Change(object):
877 def __init__(self, exp_case, cur_case):
878 self.exp_case = exp_case
879 self.cur_case = cur_case
880
881
882class Conformance(object):
883 VERSION_TYPE = {
884 '1.0.0': 'stable',
885 '1.0.1': 'stable',
886 '1.0.2': 'stable',
887 '1.0.3': 'stable',
888 '2.0.0': 'stable',
889 '1.0.4': 'beta',
890 '2.0.1': 'beta',
891 }
892
893 TEST_DONE = 'TESTDONE'
894 TOP_TIME_COUNT = 20
895
896 def __init__(self):
897 # argument
898 parser = argparse.ArgumentParser(description='Khronos WebGL Conformance Test Script', formatter_class=argparse.ArgumentDefaultsHelpFormatter, epilog='''
899 examples:
900 python %(prog)s --browser_name chrome --version 2.0.1 --suite conformance/attribs
901 ''')
902 parser.add_argument('--browser-name', dest='browser_name', help='name of browser')
903 parser.add_argument('--browser-options', dest='browser_options', help='extra options of browser, split by ","')
904 parser.add_argument('--browser-path', dest='browser_path', help='path of browser')
905 parser.add_argument('--webdriver-path', dest='webdriver_path', help='path of webdriver')
906 parser.add_argument('--version', dest='version', help='WebGL conformance test version', default='2.0.1')
907 parser.add_argument('--url', dest='url', help='url for website other than default Khronos WebGL CTS')
908 parser.add_argument('--suite', dest='suite', help='instead of whole suite, we may test specific cases, e.g., conformance/attibs or "conformance/attribs/gl-bindAttribLocation-matrix.html"', default='all')
909 parser.add_argument('--os-name', dest='os_name', help='OS to run test on')
910 parser.add_argument('--android-device-id', dest='android_device_id', help='id of Android device to run test on')
911 parser.add_argument('--mesa-dir', dest='mesa_dir', help='directory of Mesa')
912 parser.add_argument('--gles', dest='gles', help='gles', action='store_true')
913 parser.add_argument('--logging-level', dest='logging_level', help='level of logging', default=logging.INFO)
914 parser.add_argument('--timeout', dest='timeout', help='timeout seconds for each test', type=int, default=60)
915
916 debug_group = parser.add_argument_group('debug')
917 debug_group.add_argument('--fixed-time', dest='fixed_time', help='fixed time', action='store_true')
918 debug_group.add_argument('--dryrun-test', dest='dryrun_test', help='dryrun test', action='store_true')
919 args = parser.parse_args()
920
921 # timestamp
922 if args.fixed_time:
923 self.timestamp = Util.get_datetime(format='%Y%m%d')
924 else:
925 self.timestamp = Util.get_datetime()
926
927 # log
928 work_dir = Util.use_slash(sys.path[0])
929 os.chdir(work_dir)
930 self.log_dir = 'log'
931 Util.ensure_dir(self.log_dir)
932 self.log_file = '%s/%s.log' % (self.log_dir, self.timestamp)
933 Util.ensure_nofile(self.log_file)
934 Util.set_logger(self.log_file, args.logging_level)
935 self._logger = Util.get_logger()
936 self.resume_file = '%s/resume' % self.log_dir
937
938 # result
939 self.result_dir = 'result'
940 Util.ensure_dir(self.result_dir)
941 self.result_file = '%s/%s.html' % (self.result_dir, self.timestamp)
942
943 # device
944 if args.os_name == 'android':
945 self.android_device = AndroidDevices().get_device(args.android_device_id)
946 else:
947 self.android_device = None
948
949 # OS
950 self.host_os = HostOS()
951 if args.os_name == 'android':
952 self.target_os = AndroidOS(self.android_device)
953 else:
954 self.target_os = self.host_os
955
956 # browser
957 if args.browser_name:
958 browser_name = args.browser_name
959 elif self.target_os.is_cros():
960 browser_name = 'chrome'
961 else:
962 Util.error('Please designate browser name')
963 if args.browser_options:
964 browser_options = args.browser_options.split(',')
965 else:
966 browser_options = []
967
968 if args.gles and self.target_os.is_linux() and 'chrome' in browser_name:
969 browser_options.append('--use-gl=egl')
970
971 if 'chrome' in browser_name and not self.target_os.is_android() and not self.target_os.is_cros():
972 user_data_dir = 'user-data-dir-%s' % self.target_os.username
973 browser_options.append('--user-data-dir=%s' % (work_dir + '/' + user_data_dir))
974 Util.ensure_nodir(user_data_dir)
975 Util.ensure_dir(user_data_dir)
976
977 self.browser = Browser(name=browser_name, path=args.browser_path, options=browser_options, os=self.target_os)
978
979 # others
980 self.webdriver_path = args.webdriver_path
981 self.args = args
982 self.timeout = args.timeout
983
984 # url
985 self.version = args.version
986 if args.url:
987 self.url = args.url
988 else:
989 self.url = 'https://www.khronos.org/registry/webgl'
990 if self.version not in self.VERSION_TYPE:
991 Util.error('The version %s is not supported' % self.version)
992 type = self.VERSION_TYPE[self.version]
993 if type == 'stable':
994 self.url += '/conformance-suites/%s/webgl-conformance-tests.html' % self.version
995 elif type == 'beta':
996 self.url += '/sdk/tests/webgl-conformance-tests.html?version=%s' % self.version
997
998 # runtime env
999 mesa_dir = args.mesa_dir
1000 if self.target_os.is_linux() and mesa_dir:
1001 Util.set_env('LD_LIBRARY_PATH', mesa_dir + '/lib')
1002 Util.set_env('LIBGL_DRIVERS_PATH', mesa_dir + '/lib/dri')
1003
1004 if args.gles and self.target_os.is_linux() and self.gpu.is_intel():
1005 pkg = 'libgles-mesa'
1006 if not Util.has_pkg(pkg):
1007 Util.error('Package %s is not installed' % pkg)
1008 if args.gles and self.gpu.is_nvidia():
1009 Util.set_env('LD_LIBRARY_PATH', '/usr/lib/nvidia-' + self.gpu.version.split('.')[0])
1010
1011 # test
1012 if args.dryrun_test:
1013 self.exp_suite = Suite()
1014 self.cur_suite = Suite(self.exp_suite)
1015 self.driver = None
1016 self.gpu = self.gpus.get_active(self.driver)
1017 else:
1018 self._start(is_firstrun=True)
1019 self._run('firstrun')
1020 self._run('retry')
1021
1022 # report
1023 self._gen_report()
1024
1025 # Crash in previous case may only be found in current case, so we just log
1026 # the previous result so that we don't need to modify a record.
1027 def _append_resume(self, f, index):
1028 if index < 1:
1029 return
1030 case = self.cur_suite.get_case(index - 1)
1031 f.write('%s,%s,%s,%s,%s\n' % (case.path, case.status, case.total_count, case.pass_count, case.time))
1032
1033 def _crash(self, mode, index):
1034 if mode == 'firstrun':
1035 crash_case = self.cur_suite.get_case(index - 1)
1036 else:
1037 crash_case = self.cur_suite.get_case(self.cur_suite.retry_index[index - 1])
1038 crash_case.status = Status.CRASH
1039 crash_case.total_count = 1
1040 crash_case.pass_count = 0
1041 self._logger.warning('Case %s crashed' % crash_case.path)
1042 self._start()
1043
1044 def _gen_report(self):
1045 # summary
1046 summary = []
1047 path_index = {}
1048 for case in self.cur_suite.suite:
1049 path = case.path.split('/')[0]
1050 if path not in path_index:
1051 path_index[path] = len(path_index)
1052 case = Case(path, total_count=case.total_count, pass_count=case.pass_count)
1053 summary.append(case)
1054 else:
1055 index = path_index[path]
1056 summary[index].total_count += case.total_count
1057 summary[index].pass_count += case.pass_count
1058 total_count = 0
1059 pass_count = 0
1060 for case in summary:
1061 total_count += case.total_count
1062 pass_count += case.pass_count
1063 case = Case('all', total_count=total_count, pass_count=pass_count)
1064 summary.append(case)
1065
1066 # detail
1067 cur_diff_exp_path = Util.diff_list(self.cur_suite.issue_path, self.exp_suite.issue_path)
1068 exp_diff_cur_path = Util.diff_list(self.exp_suite.issue_path, self.cur_suite.issue_path)
1069 cur_exp_common_path = Util.intersect_list(self.cur_suite.issue_path, self.exp_suite.issue_path)
1070 improve_pass_detail = [] # passrate == 100%
1071 improve_fail_detail = [] # passrate < 100%
1072 regress_detail = []
1073 remain_detail = []
1074
1075 for path in cur_diff_exp_path:
1076 cur_case = self.cur_suite.get_case(self.cur_suite.path_index[path])
1077 exp_case = Case(path, Status.PASS, cur_case.total_count, cur_case.total_count)
1078 regress_detail.append(Change(exp_case, cur_case))
1079 for path in exp_diff_cur_path:
1080 exp_case = self.exp_suite.get_case(self.exp_suite.path_index[path])
1081 if path in self.cur_suite.path_index:
1082 cur_case = self.cur_suite.get_case(self.cur_suite.path_index[path])
1083 category = improve_pass_detail
1084 else:
1085 if exp_case.status == Status.FILTER:
1086 status = exp_case.status
1087 category = remain_detail
1088 else: # case was removed
1089 status = Status.NOTEXIST
1090 category = improve_pass_detail
1091 cur_case = Case(exp_case.path, status)
1092 category.append(Change(exp_case, cur_case))
1093 for path in cur_exp_common_path:
1094 exp_case = self.exp_suite.get_case(self.exp_suite.path_index[path])
1095 cur_case = self.cur_suite.get_case(self.cur_suite.path_index[path])
1096 exp_passrate = self._get_passrate(exp_case.total_count, exp_case.pass_count)
1097 cur_passrate = self._get_passrate(cur_case.total_count, cur_case.pass_count)
1098 if cur_passrate < exp_passrate:
1099 category = regress_detail
1100 elif cur_passrate > exp_passrate:
1101 if cur_passrate == 100:
1102 category = improve_pass_detail
1103 else:
1104 category = improve_fail_detail
1105 else:
1106 category = remain_detail
1107 category.append(Change(exp_case, cur_case))
1108
1109 improve_pass_detail = sorted(improve_pass_detail, cmp=lambda x, y: cmp(x.exp_case.path, y.exp_case.path))
1110 improve_fail_detail = sorted(improve_fail_detail, cmp=lambda x, y: cmp(x.exp_case.path, y.exp_case.path))
1111 regress_detail = sorted(regress_detail, cmp=lambda x, y: cmp(x.exp_case.path, y.exp_case.path))
1112 remain_detail = sorted(remain_detail, cmp=lambda x, y: cmp(x.exp_case.path, y.exp_case.path))
1113
1114 # top_time
1115 top_time = []
1116 all_time = sorted(self.cur_suite.suite, cmp=lambda x, y: cmp(x.time, y.time), reverse=True)
1117 count = 0
1118 for case in all_time:
1119 if not case.path:
1120 break
1121 if count == self.TOP_TIME_COUNT:
1122 break
1123 top_time.append([case.path, case.time])
1124 count += 1
1125
1126 # generate html
1127 content = '''
1128<html>
1129 <head>
1130 <meta http-equiv="content-type" content="text/html; charset=windows-1252">
1131 <style type="text/css">
1132 table {
1133 border: 2px solid black;
1134 border-collapse: collapse;
1135 border-spacing: 0;
1136 }
1137 table tr td {
1138 border: 1px solid black;
1139 }
1140 </style>
1141 </head>
1142 <body>
1143 '''
1144
1145 # environment
1146 content += '''
1147 <h2>Environment</h2>
1148 <table>
1149 <tbody>
1150 '''
1151 for env in ['gpu', 'host_os', 'target_os', 'browser']:
1152 if env == 'target_os' and self.host_os == self.target_os:
1153 continue
1154 content += '''
1155 <tr bgcolor="#FFFF93"><td align="left" colspan="2"><strong>''' + env.upper() + '''</strong></td></tr>
1156 '''
1157 env_dict = json.loads(str(eval('self.' + env)))
1158 for key in env_dict:
1159 content += '''
1160 <tr>
1161 <td align="left"><strong>''' + str(key) + '''</strong></td>
1162 <td align="left">''' + str(env_dict[key]) + '''</td>
1163 </tr>
1164 '''
1165
1166 content += '''
1167 </tbody>
1168 </table>
1169 '''
1170
1171 # summary
1172 content += '''
1173 <h2>Summary</h2>
1174 <table>
1175 <tbody>
1176 <tr>
1177 <td align="left"><strong>Test Case Category </strong></td>
1178 <td align="left"><strong>All</strong> </td>
1179 <td align="left"><strong>Pass </strong> </td>
1180 <td align="left"><strong>Pass Rate %</strong> </td>
1181 </tr>
1182 '''
1183 for case in summary:
1184 content += '''
1185 <tr>
1186 <td align="left"> ''' + case.path + ''' </td>
1187 <td align="left"> ''' + str(case.total_count) + ''' </td>
1188 <td align="left"> ''' + str(case.pass_count) + ''' </td>
1189 <td align="left"> ''' + str(self._get_passrate(case.total_count, case.pass_count)) + ''' </td>
1190 </tr>
1191 '''
1192
1193 content += '''
1194 </tbody>
1195 </table>
1196 '''
1197
1198 # detail
1199 content += '''
1200 <h2>Details</h2>
1201 <table>
1202 <tbody>
1203 <tr>
1204 <td align="left"><strong>Case</strong></td>
1205 <td align="left"><strong>Expectation Status</strong></td>
1206 <td align="left"><strong>Expectation All</strong></td>
1207 <td align="left"><strong>Expectation Pass</strong></td>
1208 <td align="left"><strong>Expectation Pass Rate</strong></td>
1209 <td align="left"><strong>Current Status</strong></td>
1210 <td align="left"><strong>Current All</strong></td>
1211 <td align="left"><strong>Current Pass</strong></td>
1212 <td align="left"><strong>Current Pass Rate</strong></td>
1213 <td align="left"><strong>Change</strong></td>
1214 </tr>
1215 '''
1216
1217 for detail in ['improve_pass_detail', 'improve_fail_detail', 'regress_detail', 'remain_detail']:
1218 if detail == 'improve_pass_detail':
1219 bgcolor = '00FF00'
1220 elif detail == 'improve_fail_detail':
1221 bgcolor = 'A6FFA6'
1222 elif detail == 'regress_detail':
1223 bgcolor = 'FF9797'
1224 elif detail == 'remain_detail':
1225 bgcolor = 'FFFF93'
1226 for change in eval(detail):
1227 content += '''
1228 <tr bgcolor=#''' + bgcolor + '''>
1229 <td align="left"> ''' + str(change.exp_case.path) + '''</td>
1230 <td align="left"> ''' + str(change.exp_case.status) + '''</td>
1231 <td align="left"> ''' + str(change.exp_case.total_count) + '''</td>
1232 <td align="left"> ''' + str(change.exp_case.pass_count) + '''</td>
1233 <td align="left"> ''' + str(self._get_passrate(change.exp_case.total_count, change.exp_case.pass_count)) + '''</td>
1234 <td align="left"> ''' + str(change.cur_case.status) + '''</td>
1235 <td align="left"> ''' + str(change.cur_case.total_count) + '''</td>
1236 <td align="left"> ''' + str(change.cur_case.pass_count) + '''</td>
1237 <td align="left"> ''' + str(self._get_passrate(change.cur_case.total_count, change.cur_case.pass_count)) + '''</td>
1238 <td align="left"> ''' + detail.replace('_detail', '') + '''</td>
1239 </tr>
1240 '''
1241
1242 content += '''
1243 </tbody>
1244 </table>
1245 '''
1246
1247 # retry
1248 content += '''
1249 <h2>Retry Cases</h2>
1250 <table>
1251 <tbody>
1252 <tr>
1253 <td align="left"> <strong>Case</strong> </td>
1254 </tr>
1255 '''
1256 for index in self.cur_suite.retry_index:
1257 content += '''
1258 <tr>
1259 <td align="left"> ''' + self.cur_suite.get_case(index).path + ''' </td>
1260 </tr>
1261 '''
1262 content += '''
1263 </tbody>
1264 </table>
1265'''
1266
1267 # top time consuming
1268 if self.version != '1.0.3':
1269 content += '''
1270 <h2>Top Time Consuming Cases</h2>
1271 <table>
1272 <tbody>
1273 <tr>
1274 <td align="left"><strong>Case</strong> </td>
1275 <td align="left"><strong>Time (ms)</strong> </td>
1276 </tr>
1277 '''
1278 for case in top_time:
1279 content += '''
1280 <tr>
1281 <td align="left"> ''' + case[0] + ''' </td>
1282 <td align="left"> ''' + str(case[1]) + ''' </td>
1283 </tr>
1284 '''
1285 content += '''
1286 </tbody>
1287 </table>
1288'''
1289
1290 # tail
1291 content += '''
1292 </body>
1293</html>
1294 '''
1295 f = open(self.result_file, 'w')
1296 f.write(content)
1297 f.close()
1298
1299 def _get_case_elements(self):
1300 if re.match('all', self.args.suite):
1301 suite = self.args.suite
1302 else:
1303 suite = 'all/' + self.args.suite
1304
1305 if re.search('.html$', suite):
1306 folder_name = os.path.dirname(suite)
1307 case_name = suite.split('/')[-1]
1308 else:
1309 folder_name = suite
1310 case_name = ''
1311
1312 folder_name_elements = self.driver.find_elements_by_class_name('folderName')
1313 for folder_name_element in folder_name_elements:
1314 if folder_name_element.text == folder_name:
1315 tmp_case_elements = folder_name_element.find_elements_by_xpath('../..//*[@class="testpage"]')
1316 if not case_name:
1317 case_elements = tmp_case_elements
1318 break
1319 for case_element in tmp_case_elements:
1320 if '%s/%s' % ('all', case_element.find_element_by_xpath('./div/a').text) == suite:
1321 case_elements = [case_element]
1322 break
1323 if case_elements:
1324 break
1325 else:
1326 Util.error('Could not find suite %s' % suite)
1327
1328 self.case_elements = case_elements
1329
1330 def _get_passrate(self, total, passed):
1331 if float(total) == 0:
1332 return 0
1333 return float('%.2f' % (float(passed) / float(total) * 100))
1334
1335 def _get_result(self, text):
1336 # passed includes both results of passed and skipped
1337 if self.version == '1.0.3':
1338 p = '(\d+) of (\d+) (.+)'
1339 match = re.search(p, text)
1340 if match:
1341 total = int(match.group(2))
1342 passed = int(match.group(1))
1343 else:
1344 total = 1
1345 passed = 0
1346 skipped = 0
1347 time = 0
1348 if total == passed + skipped:
1349 status = Status.PASS
1350 else:
1351 status = Status.FAIL
1352 else:
1353 p = '(.*) in (.+) ms'
1354 match = re.search(p, text)
1355 if match:
1356 time = float(match.group(2))
1357 text_detail = match.group(1)
1358 total = 0
1359 match_detail = re.search('Passed: (\d+)/(\d+)', text_detail)
1360 if match_detail:
1361 passed = int(match_detail.group(1))
1362 total_tmp = int(match_detail.group(2))
1363 if not total:
1364 total = total_tmp
1365 if total != total_tmp:
1366 Util.error('Total is not consistent')
1367 else:
1368 passed = 0
1369 match_detail = re.search('Skipped: (\d+)/(\d+)', text_detail)
1370 if match_detail:
1371 skipped = int(match_detail.group(1))
1372 total_tmp = int(match_detail.group(2))
1373 if not total:
1374 total = total_tmp
1375 if total != total_tmp:
1376 Util.error('Total is not consistent')
1377 else:
1378 skipped = 0
1379 match_detail = re.search('Failed: (\d+)/(\d+)', text_detail)
1380 if match_detail:
1381 failed = int(match_detail.group(1))
1382 total_tmp = int(match_detail.group(2))
1383 if not total:
1384 total = total_tmp
1385 if total != total_tmp:
1386 Util.error('Total is not consistent')
1387 else:
1388 failed = 0
1389
1390 if passed + skipped + failed != total:
1391 Util.error('Total is not the sum of passed, skipped and failed')
1392
1393 if total == passed + skipped:
1394 status = Status.PASS
1395 else:
1396 status = Status.FAIL
1397 else:
1398 total = 1
1399 status = Status.JSTIMEOUT
1400 passed = 0
1401 skipped = 0
1402 time = 0
1403
1404 return (status, total, passed + skipped, time)
1405
1406 def _log_resume(self, index, total_count, msg, case_path):
1407 self._logger.info('(%s/%s) %s %s' % (index + 1, total_count, msg, case_path))
1408
1409 def _run(self, mode):
1410 if mode == 'firstrun':
1411 total_count = len(self.case_elements)
1412 elif mode == 'retry':
1413 total_count = len(self.cur_suite.retry_index)
1414 else:
1415 Util.error('Mode %s is not supported' % mode)
1416
1417 if total_count < 1 and mode == 'firstrun':
1418 Util.error('No case will be tested')
1419 elif total_count < 1 and mode == 'retry':
1420 f = open(self.resume_file, 'a')
1421 f.write(self.TEST_DONE + '\n')
1422 f.close()
1423 self._logger.info('No need the %s' % mode)
1424 return
1425 else:
1426 self._logger.info('Begin the %s...' % mode)
1427
1428 if mode == 'firstrun':
1429 # resume_count
1430 if os.path.exists(self.resume_file):
1431 resume_lines = Util.read_file(self.resume_file)
1432 resume_count = len(resume_lines)
1433 if resume_count > 0 and resume_lines[-1].rstrip('\n') == self.TEST_DONE:
1434 resume_count = 0
1435 else:
1436 resume_count = 0
1437
1438 # resume file
1439 if resume_count == 0:
1440 Util.ensure_nofile(self.resume_file)
1441 Util.ensure_file(self.resume_file)
1442 f = open(self.resume_file, 'a')
1443
1444 # resume
1445 if resume_count > 0:
1446 if resume_count > len(self.case_elements) or resume_lines[-1].split(',')[0] != self.case_elements[resume_count - 1].find_element_by_xpath('./div/a').text:
1447 Util.error('The suite currently tested is different from the resumed one')
1448
1449 for resume_line in resume_lines:
1450 fields = resume_line.split(',')
1451 case = Case(fields[0], fields[1], int(fields[2]), int(fields[3]), float(fields[4]))
1452 self.cur_suite.add_case(case)
1453 self._logger.info('Resume %s cases' % resume_count)
1454
1455 index = resume_count
1456 else:
1457 index = 0
1458 while index < total_count:
1459 if mode == 'firstrun':
1460 case_index = index
1461 case_element = self.case_elements[case_index]
1462 else:
1463 case_index = self.cur_suite.retry_index[index]
1464 case_element = self.case_elements[case_index]
1465 case = self.cur_suite.get_case(case_index)
1466 case_path = case_element.find_element_by_xpath('./div/a').text
1467
1468 # filter
1469 if mode == 'firstrun' and case_path in self.exp_suite.filter_path:
1470 case = Case(case_path, Status.FILTER)
1471 self.cur_suite.add_case(case)
1472 self._log_resume(index, total_count, 'Filter', case_path)
1473 if mode == 'firstrun':
1474 self._append_resume(f, index)
1475 index += 1
1476 continue
1477
1478 # run test
1479 self._log_resume(index, total_count, 'Run', case_path)
1480 try:
1481 button = case_element.find_element_by_xpath('./div/input[@type="button"]')
1482 button.click()
1483 except NoSuchElementException:
1484 self._crash(mode, index)
1485 continue
1486
1487 # handle result
1488 try:
1489 WebDriverWait(self.driver, self.timeout).until(lambda driver: re.search('(passed|skipped|failed|timeout)', case_element.find_element_by_xpath('./div').text, re.I))
1490 except TimeoutException:
1491 if mode == 'firstrun':
1492 case = Case(case_path, Status.PYTIMEOUT)
1493 self.cur_suite.add_case(case)
1494 self._logger.warning('Case %s timeout in python script' % case_path)
1495 self._start()
1496 index += 1
1497 else:
1498 (case_status, case_total_count, case_pass_count, case_time) = self._get_result(case_element.find_element_by_xpath('./div').text)
1499 if mode == 'firstrun':
1500 case = Case(case_path, case_status, case_total_count, case_pass_count, case_time)
1501 self.cur_suite.add_case(case)
1502 else:
1503 case.status = case_status
1504 case.total_count = case_total_count
1505 case.pass_count = case_pass_count
1506 case.time = case_time
1507 self._logger.info(case.status)
1508
1509 if case.is_pass():
1510 if mode == 'retry':
1511 self.cur_suite.remove_issue(case_index)
1512 elif case_element.find_element_by_xpath('./ul').find_elements_by_tag_name('li') and re.search('Unable to fetch WebGL rendering context for Canvas', case_element.find_element_by_xpath('./ul/li').text):
1513 self._crash(mode, index)
1514 continue
1515
1516 if mode == 'firstrun':
1517 self._append_resume(f, index)
1518 index += 1
1519
1520 if mode == 'firstrun':
1521 self._append_resume(f, index)
1522 if mode == 'retry':
1523 f = open(self.resume_file, 'a')
1524 f.write(self.TEST_DONE + '\n')
1525 f.close()
1526
1527 def _start(self, is_firstrun=False):
1528 self.webdriver = Webdriver(browser=self.browser, path=self.webdriver_path, host_os=self.host_os, target_os=self.target_os, android_device=self.android_device)
1529 self.driver = self.webdriver.driver
1530
1531 if is_firstrun:
1532 self.browser.update(self.driver)
1533 self.gpus = GPUs(self.target_os, self.android_device, self.driver)
1534 self.gpu = self.gpus.get_active(self.driver)
1535 self.exp_suite = Suite()
1536 for exp in Expectations().expectations:
1537 if exp.is_valid(self.gpu, self.target_os, self.browser) and (self.args.suite == 'all' or re.match(self.args.suite, exp.path)):
1538 self.exp_suite.add_case(Case(exp.path, exp.status, exp.total_count, exp.pass_count))
1539 self.cur_suite = Suite(self.exp_suite)
1540
1541 self.driver.get(self.url)
1542 try:
1543 WebDriverWait(self.driver, 60).until(lambda driver: self.driver.find_element_by_id('page0'))
1544 except TimeoutException:
1545 Util.error('Could not open %s correctly' % self.url)
1546
1547 if is_firstrun:
1548 option_element = Select(self.driver.find_element_by_id("testVersion")).first_selected_option
1549 real_version = option_element.text
1550 type = self.VERSION_TYPE[self.version]
1551 if type == 'beta':
1552 real_version = real_version.replace(' (beta)', '')
1553 if self.version != real_version:
1554 Util.error('The designated version does not match the real version')
1555
1556 self._get_case_elements()
1557
1558
1559class Expectation(object):
1560 def __init__(self, version, path, status, total_count=0, pass_count=0, gpu=None, os=None, browser=None):
1561 self.version = version
1562 self.path = path
1563 self.status = status
1564 self.total_count = total_count
1565 self.pass_count = pass_count
1566 self.gpu = gpu
1567 self.os = os
1568 self.browser = browser
1569
1570 def is_valid(self, gpu, os, browser):
1571 return True
1572
1573
1574class Expectations(object):
1575 def __init__(self):
1576 self.expectations = []
1577
1578 # win_os = OS('windows')
1579 # self._add_exp('2.0.1', 'deqp/functional/gles3/builtinprecision/atan2.html', Status.FAIL, 25, 17, os=win_os)
1580
1581 def _add_exp(self, version, path, status, total_count=0, pass_count=0, gpu=None, os=None, browser=None):
1582 self.expectations.append(Expectation(version, path, status, total_count, pass_count, gpu, os, browser))
1583
1584
1585if __name__ == '__main__':
1586 conformance = Conformance()