1#!/usr/bin/python3 2# 3# Copyright 2021 Advanced Micro Devices, Inc. 4# 5# SPDX-License-Identifier: MIT 6# 7 8import os 9import sys 10import argparse 11import subprocess 12import shutil 13from datetime import datetime 14import tempfile 15import itertools 16import filecmp 17import multiprocessing 18import csv 19 20 21def print_red(txt, end_line=True, prefix=None): 22 if prefix: 23 print(prefix, end="") 24 print("\033[0;31m{}\033[0m".format(txt), end="\n" if end_line else " ") 25 26 27def print_yellow(txt, end_line=True, prefix=None): 28 if prefix: 29 print(prefix, end="") 30 print("\033[1;33m{}\033[0m".format(txt), end="\n" if end_line else " ") 31 32 33def print_green(txt, end_line=True, prefix=None): 34 if prefix: 35 print(prefix, end="") 36 print("\033[1;32m{}\033[0m".format(txt), end="\n" if end_line else " ") 37 38 39parser = argparse.ArgumentParser( 40 description="radeonsi tester", 41 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 42) 43parser.add_argument( 44 "--jobs", 45 "-j", 46 type=int, 47 help="Number of processes/threads to use.", 48 default=multiprocessing.cpu_count(), 49) 50 51# The path to above the mesa directory, i.e. ../../../../../.. 52path_above_mesa = os.path.realpath(os.path.join(os.path.dirname(__file__), *['..'] * 6)) 53 54parser.add_argument("--piglit-path", type=str, help="Path to piglit source folder.") 55parser.add_argument("--glcts-path", type=str, help="Path to GLCTS source folder.") 56parser.add_argument( 57 "--parent-path", 58 type=str, 59 help="Path to folder containing piglit/GLCTS and dEQP source folders.", 60 default=os.getenv('MAREKO_BUILD_PATH', path_above_mesa), 61) 62parser.add_argument("--verbose", "-v", action="count", default=0) 63parser.add_argument( 64 "--include-tests", 65 "-t", 66 action="append", 67 dest="include_tests", 68 default=[], 69 help="Only run the test matching this expression. This can only be a filename containing a list of failing tests to re-run.", 70) 71parser.add_argument( 72 "--baseline", 73 dest="baseline", 74 help="Folder containing expected results files", 75 default=os.path.dirname(__file__), 76) 77parser.add_argument( 78 "--no-piglit", dest="piglit", help="Disable piglit tests", action="store_false" 79) 80parser.add_argument( 81 "--no-glcts", dest="glcts", help="Disable GLCTS tests", action="store_false" 82) 83parser.add_argument( 84 "--no-escts", dest="escts", help="Disable GLES CTS tests", action="store_false" 85) 86parser.add_argument( 87 "--no-deqp", dest="deqp", help="Disable dEQP tests", action="store_false" 88) 89parser.add_argument( 90 "--no-deqp-egl", 91 dest="deqp_egl", 92 help="Disable dEQP-EGL tests", 93 action="store_false", 94) 95parser.add_argument( 96 "--no-deqp-gles2", 97 dest="deqp_gles2", 98 help="Disable dEQP-gles2 tests", 99 action="store_false", 100) 101parser.add_argument( 102 "--no-deqp-gles3", 103 dest="deqp_gles3", 104 help="Disable dEQP-gles3 tests", 105 action="store_false", 106) 107parser.add_argument( 108 "--no-deqp-gles31", 109 dest="deqp_gles31", 110 help="Disable dEQP-gles31 tests", 111 action="store_false", 112) 113parser.set_defaults(piglit=True) 114parser.set_defaults(glcts=True) 115parser.set_defaults(escts=True) 116parser.set_defaults(deqp=True) 117parser.set_defaults(deqp_egl=True) 118parser.set_defaults(deqp_gles2=True) 119parser.set_defaults(deqp_gles3=True) 120parser.set_defaults(deqp_gles31=True) 121 122parser.add_argument( 123 "output_folder", 124 nargs="?", 125 help="Output folder (logs, etc)", 126 default=os.path.join( 127 # Default is ../../../../../../test-results/datetime 128 os.path.join(path_above_mesa, 'test-results', 129 datetime.now().strftime("%Y-%m-%d-%H-%M-%S")) 130 ), 131) 132 133available_gpus = [] 134for f in os.listdir("/dev/dri/by-path"): 135 idx = f.find("-render") 136 if idx < 0: 137 continue 138 # gbm name is the full path, but DRI_PRIME expects a different 139 # format 140 available_gpus += [ 141 ( 142 os.path.join("/dev/dri/by-path", f), 143 f[:idx].replace(":", "_").replace(".", "_"), 144 ) 145 ] 146 147parser.add_argument( 148 "--gpu", 149 type=int, 150 dest="gpu", 151 default=0, 152 help="Select GPU (0..{})".format(len(available_gpus) - 1), 153) 154parser.add_argument( 155 "--llvmpipe", dest="llvmpipe", help="Test llvmpipe", action="store_true" 156) 157parser.add_argument( 158 "--softpipe", dest="softpipe", help="Test softpipe", action="store_true" 159) 160parser.add_argument( 161 "--virgl", dest="virgl", help="Test virgl", action="store_true" 162) 163parser.add_argument( 164 "--zink", dest="zink", help="Test zink", action="store_true" 165) 166 167args = parser.parse_args(sys.argv[1:]) 168piglit_path = args.piglit_path 169glcts_path = args.glcts_path 170 171if args.parent_path: 172 if args.piglit_path or args.glcts_path: 173 parser.print_help() 174 sys.exit(0) 175 piglit_path = os.path.join(args.parent_path, "piglit") 176 glcts_path = os.path.join(args.parent_path, "glcts") 177else: 178 if not args.piglit_path or not args.glcts_path: 179 parser.print_help() 180 sys.exit(0) 181 182env = os.environ.copy() 183 184if "DISPLAY" not in env: 185 print_red("DISPLAY environment variable missing.") 186 sys.exit(1) 187p = subprocess.run( 188 ["deqp-runner", "--version"], capture_output="True", check=True, env=env 189) 190for line in p.stdout.decode().split("\n"): 191 if line.find("deqp-runner") >= 0: 192 s = line.split(" ")[1].split(".") 193 if args.verbose > 1: 194 print("Checking deqp-version ({})".format(s)) 195 # We want at least 0.9.0 196 if not (int(s[0]) > 0 or int(s[1]) >= 9): 197 print("Expecting deqp-runner 0.9.0+ version (got {})".format(".".join(s))) 198 sys.exit(1) 199 200if "DRI_PRIME" in env: 201 print("Don't use DRI_PRIME. Instead use --gpu N") 202 del env["DRI_PRIME"] 203 204assert "gpu" in args, "--gpu defaults to 0" 205 206gpu_device = available_gpus[args.gpu][1] 207env["DRI_PRIME"] = gpu_device 208env["WAFFLE_GBM_DEVICE"] = available_gpus[args.gpu][0] 209 210# Use piglit's glinfo to determine the GPU name 211gpu_name = "unknown" 212gpu_name_full = "" 213gfx_level = -1 214 215assert args.llvmpipe + args.softpipe + args.virgl + args.zink <= 1 216is_amd = args.llvmpipe + args.softpipe + args.virgl + args.zink == 0 217 218if args.llvmpipe: 219 env["LIBGL_ALWAYS_SOFTWARE"] = '1' 220 baseline = "../../llvmpipe/ci/llvmpipe-fails.txt" 221 flakes_list = "../../llvmpipe/ci/llvmpipe-flakes.txt" 222 skips_list = "../../llvmpipe/ci/llvmpipe-skips.txt" 223elif args.softpipe: 224 env["LIBGL_ALWAYS_SOFTWARE"] = '1' 225 env["GALLIUM_DRIVER"] = 'softpipe' 226 baseline = "../../softpipe/ci/softpipe-fails.txt" 227 flakes_list = "../../softpipe/ci/softpipe-flakes.txt" 228 skips_list = "../../softpipe/ci/softpipe-skips.txt" 229elif args.virgl: 230 env["PIGLIT_PLATFORM"] = "gbm" 231 baseline = '' 232 flakes_list = None 233 skips_list = "skips.csv" 234elif args.zink: 235 env["PIGLIT_PLATFORM"] = "gbm" 236 env["MESA_LOADER_DRIVER_OVERRIDE"] = 'zink' 237 baseline = "../../zink/ci/zink-radv-navi31-fails.txt" 238 flakes_list = "../../zink/ci/zink-radv-navi31-flakes.txt" 239 skips_list = "../../zink/ci/zink-radv-navi31-skips.txt" 240elif is_amd: 241 env["PIGLIT_PLATFORM"] = "gbm" 242 flakes_list = None # it will be determined later 243 skips_list = "skips.csv" 244else: 245 assert False 246 247if not is_amd: 248 baseline = os.path.normpath(os.path.join(os.path.dirname(__file__), baseline)) 249 if flakes_list is not None: 250 flakes_list = os.path.normpath(os.path.join(os.path.dirname(__file__), flakes_list)) 251 252skips_list = os.path.normpath(os.path.join(os.path.dirname(__file__), skips_list)) 253env_glinfo = dict(env) 254env_glinfo["AMD_DEBUG"] = "info" 255 256try: 257 p = subprocess.run( 258 ["./glinfo"], 259 capture_output="True", 260 cwd=os.path.join(piglit_path, "bin"), 261 check=True, 262 env=env_glinfo, 263 ) 264except subprocess.CalledProcessError: 265 print('piglit/bin/glinfo failed to create a GL context') 266 exit(1) 267 268renderer = None 269for line in p.stdout.decode().split("\n"): 270 if "GL_RENDER" in line: 271 line = line.split("=")[1] 272 renderer = line 273 if is_amd: 274 gpu_name_full = "(".join(line.split("(")[:-1]).strip() 275 gpu_name = line.replace("(TM)", "").split("(")[1].split(",")[1].lower().strip() 276 break 277 elif "gfx_level" in line: 278 gfx_level = int(line.split("=")[1]) 279 280if renderer is None: 281 print('piglit/bin/glinfo failed to create a GL context') 282 exit(1) 283 284output_folder = args.output_folder 285if is_amd: 286 print_green("Tested GPU: '{}' ({}) {}".format(gpu_name_full, gpu_name, gpu_device)) 287else: 288 print_green("Renderer: '{}'".format(renderer)) 289print_green("Output folder: '{}'".format(output_folder)) 290 291count = 1 292while os.path.exists(output_folder): 293 output_folder = "{}.{}".format(os.path.abspath(args.output_folder), count) 294 count += 1 295 296os.makedirs(output_folder, exist_ok=True) 297 298logfile = open(os.path.join(output_folder, "{}-run-tests.log".format(gpu_name)), "w") 299 300spin = itertools.cycle("-\\|/") 301 302def gfx_level_to_str(cl): 303 supported = ["gfx6", "gfx7", "gfx8", "gfx9", "gfx10", "gfx10_3", "gfx11", "gfx12"] 304 if 8 <= cl and cl < 8 + len(supported): 305 return supported[cl - 8] 306 return supported[-1] 307 308 309def run_cmd(args, verbosity): 310 if verbosity > 1: 311 print_yellow( 312 "| Command line argument '" 313 + " ".join(['"{}"'.format(a) for a in args]) 314 + "'" 315 ) 316 start = datetime.now() 317 proc = subprocess.Popen( 318 args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env 319 ) 320 while True: 321 line = proc.stdout.readline().decode() 322 if verbosity > 0: 323 if "ERROR" in line: 324 print_red(line.strip(), prefix="| ") 325 else: 326 print("| " + line.strip()) 327 else: 328 sys.stdout.write(next(spin)) 329 sys.stdout.flush() 330 sys.stdout.write("\b") 331 332 logfile.write(line) 333 334 if proc.poll() is not None: 335 break 336 proc.wait() 337 end = datetime.now() 338 339 if verbosity == 0: 340 sys.stdout.write(" ... ") 341 342 print_yellow( 343 "Completed in {} seconds".format(int((end - start).total_seconds())), 344 prefix="└ " if verbosity > 0 else None, 345 ) 346 347 348def verify_results(results): 349 with open(results) as file: 350 lines = file.readlines() 351 if len(lines) == 0: 352 return True 353 print("{} new result{}:".format(len(lines), 's' if len(lines) > 1 else '')) 354 for i in range(min(10, len(lines))): 355 print(" * ", end='') 356 if "Pass" in lines[i]: 357 print_green(lines[i][:-1]) 358 else: 359 print_red(lines[i][:-1]) 360 if len(lines) > 10: 361 print_yellow("...") 362 print("Full results: {}".format(results)) 363 364 return False 365 366 367def parse_test_filters(include_tests, baseline): 368 cmd = [] 369 for t in include_tests: 370 if t == 'baseline': 371 t = baseline 372 373 if os.path.exists(t): 374 with open(t, "r") as file: 375 for row in csv.reader(file, delimiter=","): 376 if not row or row[0][0] == "#": 377 continue 378 cmd += ["-t", row[0]] 379 else: 380 cmd += ["-t", t] 381 return cmd 382 383 384def select_baseline(basepath, gfx_level, gpu_name, suffix): 385 gfx_level_str = gfx_level_to_str(gfx_level) 386 387 # select the best baseline we can find 388 # 1. exact match 389 exact = os.path.join(basepath, "{}-{}-{}.csv".format(gfx_level_str, gpu_name, suffix)) 390 if os.path.exists(exact): 391 return exact 392 # 2. any baseline with the same gfx_level 393 while gfx_level >= 8: 394 gfx_level_str += '-' 395 for subdir, dirs, files in os.walk(basepath): 396 for file in files: 397 if file.find(gfx_level_str) == 0 and file.endswith("-{}.csv".format(suffix)): 398 return os.path.join(basepath, file) 399 # No match. Try an earlier class 400 gfx_level = gfx_level - 1 401 gfx_level_str = gfx_level_to_str(gfx_level) 402 403 return exact 404 405 406if is_amd: 407 baseline = select_baseline(args.baseline, gfx_level, gpu_name, 'fail') 408 flakes_list = select_baseline(args.baseline, gfx_level, gpu_name, 'flakes') 409 410success = True 411filters_args = parse_test_filters(args.include_tests, baseline) 412flakes_args = [] 413 414if os.path.exists(baseline): 415 print_yellow("Baseline: {}".format(baseline)) 416 417if flakes_list is not None and os.path.exists(flakes_list): 418 print_yellow("Flakes: {}".format(flakes_list)) 419 flakes_args = ["--flakes", flakes_list] 420 421print_yellow("Skips: {}".format(skips_list)) 422 423# piglit test 424if args.piglit: 425 out = os.path.join(output_folder, "piglit") 426 print_yellow("Running piglit tests", args.verbose > 0) 427 cmd = [ 428 "piglit-runner", 429 "run", 430 "--piglit-folder", 431 piglit_path, 432 "--profile", 433 "quick", 434 "--output", 435 out, 436 "--process-isolation", 437 "--timeout", 438 "300", 439 "--jobs", 440 str(args.jobs), 441 "--skips", 442 skips_list, 443 "--skips", 444 os.path.join(path_above_mesa, "mesa", ".gitlab-ci", "gbm-skips.txt") 445 ] + filters_args + flakes_args 446 447 if os.path.exists(baseline): 448 cmd += ["--baseline", baseline] 449 450 run_cmd(cmd, args.verbose) 451 452 if not verify_results(os.path.join(out, "failures.csv")): 453 success = False 454 455deqp_args = "-- --deqp-surface-width=256 --deqp-surface-height=256 --deqp-gl-config-name=rgba8888d24s8ms0 --deqp-visibility=hidden".split( 456 " " 457) 458 459# glcts test 460if args.glcts: 461 out = os.path.join(output_folder, "glcts") 462 print_yellow("Running GLCTS tests", args.verbose > 0) 463 os.mkdir(os.path.join(output_folder, "glcts")) 464 465 cmd = [ 466 "deqp-runner", 467 "run", 468 "--tests-per-group", 469 "100", 470 "--deqp", 471 "{}/build/external/openglcts/modules/glcts".format(glcts_path), 472 ] 473 474 if is_amd or args.zink: 475 cmd += [ 476 "--caselist", 477 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl46-main.txt".format(glcts_path), 478 "--caselist", 479 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass_single/4.6.1.x/gl46-khr-single.txt".format(glcts_path), 480 "--caselist", 481 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl46-gtf-main.txt".format(glcts_path), 482 ] 483 elif args.llvmpipe: 484 cmd += [ 485 "--caselist", 486 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl45-main.txt".format(glcts_path), 487 "--caselist", 488 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass_single/4.6.1.x/gl45-khr-single.txt".format(glcts_path), 489 "--caselist", 490 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl45-gtf-main.txt".format(glcts_path), 491 ] 492 elif args.virgl: 493 cmd += [ 494 "--caselist", 495 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl43-main.txt".format(glcts_path), 496 "--caselist", 497 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass_single/4.6.1.x/gl43-khr-single.txt".format(glcts_path), 498 "--caselist", 499 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl43-gtf-main.txt".format(glcts_path), 500 ] 501 elif args.softpipe: 502 # KHR-GL33.info.renderer crashes with softpipe. 503 #cmd += [ 504 # "--caselist", 505 # "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl33-main.txt".format(glcts_path), 506 # "--caselist", 507 # "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl33-gtf-main.txt".format(glcts_path), 508 #] 509 pass 510 else: 511 assert False 512 513 cmd += [ 514 "--output", 515 out, 516 "--skips", 517 skips_list, 518 "--jobs", 519 str(args.jobs), 520 "--timeout", 521 "1000" 522 ] + filters_args + flakes_args 523 524 if os.path.exists(baseline): 525 cmd += ["--baseline", baseline] 526 cmd += deqp_args 527 528 run_cmd(cmd, args.verbose) 529 530 if not verify_results(os.path.join(out, "failures.csv")): 531 success = False 532 533# escts test 534if args.escts: 535 out = os.path.join(output_folder, "escts") 536 print_yellow("Running ESCTS tests", args.verbose > 0) 537 os.mkdir(out) 538 539 cmd = [ 540 "deqp-runner", 541 "run", 542 "--tests-per-group", 543 "100", 544 "--deqp", 545 "{}/build_es/external/openglcts/modules/glcts".format(glcts_path), 546 "--caselist", 547 "{}/external/openglcts/data/gl_cts/data/mustpass/gles/khronos_mustpass/3.2.6.x/gles2-khr-main.txt".format( 548 glcts_path 549 ), 550 "--caselist", 551 "{}/external/openglcts/data/gl_cts/data/mustpass/gles/khronos_mustpass/3.2.6.x/gles3-khr-main.txt".format( 552 glcts_path 553 ), 554 "--caselist", 555 "{}/external/openglcts/data/gl_cts/data/mustpass/gles/khronos_mustpass/3.2.6.x/gles31-khr-main.txt".format( 556 glcts_path 557 ), 558 ] 559 560 if not args.softpipe: 561 cmd += [ 562 "--caselist", 563 "{}/external/openglcts/data/gl_cts/data/mustpass/gles/khronos_mustpass/3.2.6.x/gles32-khr-main.txt".format( 564 glcts_path 565 ), 566 ] 567 568 cmd += [ 569 "--output", 570 out, 571 "--skips", 572 skips_list, 573 "--jobs", 574 str(args.jobs), 575 "--timeout", 576 "1000" 577 ] + filters_args + flakes_args 578 579 if os.path.exists(baseline): 580 cmd += ["--baseline", baseline] 581 cmd += deqp_args 582 583 run_cmd(cmd, args.verbose) 584 585 if not verify_results(os.path.join(out, "failures.csv")): 586 success = False 587 588if args.deqp: 589 print_yellow("Running dEQP tests", args.verbose > 0) 590 591 # Generate a test-suite file 592 out = os.path.join(output_folder, "deqp") 593 suite_filename = os.path.join(output_folder, "deqp-suite.toml") 594 suite = open(suite_filename, "w") 595 os.mkdir(out) 596 597 deqp_tests = { 598 "egl": args.deqp_egl, 599 "gles2": args.deqp_gles2, 600 "gles3": args.deqp_gles3, 601 "gles31": args.deqp_gles31, 602 } 603 604 for k in deqp_tests: 605 if not deqp_tests[k]: 606 continue 607 608 suite.write("[[deqp]]\n") 609 suite.write( 610 'deqp = "{}"\n'.format( 611 "{}/build/modules/{subtest}/deqp-{subtest}".format(glcts_path, subtest=k) 612 ) 613 ) 614 suite.write( 615 'caselists = ["{}"]\n'.format( 616 "{}/external/openglcts/data/gl_cts/data/mustpass/{}/aosp_mustpass/3.2.6.x/{}-main.txt".format(glcts_path, "egl" if k == "egl" else "gles", k) 617 ) 618 ) 619 if os.path.exists(baseline): 620 suite.write('baseline = "{}"\n'.format(baseline)) 621 suite.write('skips = ["{}"]\n'.format(skips_list)) 622 suite.write("deqp_args = [\n") 623 for a in deqp_args[1:-1]: 624 suite.write(' "{}",\n'.format(a)) 625 suite.write(' "{}"\n'.format(deqp_args[-1])) 626 suite.write("]\n") 627 628 suite.close() 629 630 cmd = [ 631 "deqp-runner", 632 "suite", 633 "--jobs", 634 str(args.jobs), 635 "--output", 636 os.path.join(output_folder, "deqp"), 637 "--suite", 638 suite_filename, 639 ] + filters_args + flakes_args 640 641 run_cmd(cmd, args.verbose) 642 643 if not verify_results(os.path.join(out, "failures.csv")): 644 success = False 645 646sys.exit(0 if success else 1) 647