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