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 "--slow", dest="slow", help="Include slowest glcts tests", action="store_true" 91) 92parser.add_argument( 93 "--no-deqp-egl", 94 dest="deqp_egl", 95 help="Disable dEQP-EGL tests", 96 action="store_false", 97) 98parser.add_argument( 99 "--no-deqp-gles2", 100 dest="deqp_gles2", 101 help="Disable dEQP-gles2 tests", 102 action="store_false", 103) 104parser.add_argument( 105 "--no-deqp-gles3", 106 dest="deqp_gles3", 107 help="Disable dEQP-gles3 tests", 108 action="store_false", 109) 110parser.add_argument( 111 "--no-deqp-gles31", 112 dest="deqp_gles31", 113 help="Disable dEQP-gles31 tests", 114 action="store_false", 115) 116parser.set_defaults(piglit=True) 117parser.set_defaults(glcts=True) 118parser.set_defaults(escts=True) 119parser.set_defaults(deqp=True) 120parser.set_defaults(deqp_egl=True) 121parser.set_defaults(deqp_gles2=True) 122parser.set_defaults(deqp_gles3=True) 123parser.set_defaults(deqp_gles31=True) 124parser.set_defaults(slow=False) 125 126parser.add_argument( 127 "output_folder", 128 nargs="?", 129 help="Output folder (logs, etc)", 130 default=os.path.join( 131 # Default is ../../../../../../test-results/datetime 132 os.path.join(path_above_mesa, 'test-results', 133 datetime.now().strftime("%Y-%m-%d-%H-%M-%S")) 134 ), 135) 136 137available_gpus = [] 138for f in os.listdir("/dev/dri/by-path"): 139 idx = f.find("-render") 140 if idx < 0: 141 continue 142 # gbm name is the full path, but DRI_PRIME expects a different 143 # format 144 available_gpus += [ 145 ( 146 os.path.join("/dev/dri/by-path", f), 147 f[:idx].replace(":", "_").replace(".", "_"), 148 ) 149 ] 150 151parser.add_argument( 152 "--gpu", 153 type=int, 154 dest="gpu", 155 default=0, 156 help="Select GPU (0..{})".format(len(available_gpus) - 1), 157) 158 159args = parser.parse_args(sys.argv[1:]) 160piglit_path = args.piglit_path 161glcts_path = args.glcts_path 162 163if args.parent_path: 164 if args.piglit_path or args.glcts_path: 165 parser.print_help() 166 sys.exit(0) 167 piglit_path = os.path.join(args.parent_path, "piglit") 168 glcts_path = os.path.join(args.parent_path, "glcts") 169else: 170 if not args.piglit_path or not args.glcts_path: 171 parser.print_help() 172 sys.exit(0) 173 174base = args.baseline 175skips = os.path.join(os.path.dirname(__file__), "skips.csv") 176 177env = os.environ.copy() 178 179if "DISPLAY" not in env: 180 print_red("DISPLAY environment variable missing.") 181 sys.exit(1) 182p = subprocess.run( 183 ["deqp-runner", "--version"], capture_output="True", check=True, env=env 184) 185for line in p.stdout.decode().split("\n"): 186 if line.find("deqp-runner") >= 0: 187 s = line.split(" ")[1].split(".") 188 if args.verbose > 1: 189 print("Checking deqp-version ({})".format(s)) 190 # We want at least 0.9.0 191 if not (int(s[0]) > 0 or int(s[1]) >= 9): 192 print("Expecting deqp-runner 0.9.0+ version (got {})".format(".".join(s))) 193 sys.exit(1) 194 195env["PIGLIT_PLATFORM"] = "gbm" 196 197if "DRI_PRIME" in env: 198 print("Don't use DRI_PRIME. Instead use --gpu N") 199 del env["DRI_PRIME"] 200 201assert "gpu" in args, "--gpu defaults to 0" 202 203gpu_device = available_gpus[args.gpu][1] 204env["DRI_PRIME"] = gpu_device 205env["WAFFLE_GBM_DEVICE"] = available_gpus[args.gpu][0] 206 207# Use piglit's glinfo to determine the GPU name 208gpu_name = "unknown" 209gpu_name_full = "" 210gfx_level = -1 211 212amd_debug = env["AMD_DEBUG"] if "AMD_DEBUG" in env else "" 213env["AMD_DEBUG"] = "info" 214p = subprocess.run( 215 ["./glinfo"], 216 capture_output="True", 217 cwd=os.path.join(piglit_path, "bin"), 218 check=True, 219 env=env, 220) 221del env["AMD_DEBUG"] 222env["AMD_DEBUG"] = amd_debug 223 224for line in p.stdout.decode().split("\n"): 225 if "GL_RENDER" in line: 226 line = line.split("=")[1] 227 gpu_name_full = "(".join(line.split("(")[:-1]).strip() 228 gpu_name = line.replace("(TM)", "").split("(")[1].split(",")[1].lower().strip() 229 break 230 elif "gfx_level" in line: 231 gfx_level = int(line.split("=")[1]) 232 233output_folder = args.output_folder 234print_green("Tested GPU: '{}' ({}) {}".format(gpu_name_full, gpu_name, gpu_device)) 235print_green("Output folder: '{}'".format(output_folder)) 236 237count = 1 238while os.path.exists(output_folder): 239 output_folder = "{}.{}".format(os.path.abspath(args.output_folder), count) 240 count += 1 241 242os.makedirs(output_folder, exist_ok=True) 243 244logfile = open(os.path.join(output_folder, "{}-run-tests.log".format(gpu_name)), "w") 245 246spin = itertools.cycle("-\\|/") 247 248shutil.copy(skips, output_folder) 249skips = os.path.join(output_folder, "skips.csv") 250if not args.slow: 251 # Exclude these 4 tests slow tests 252 with open(skips, "a") as f: 253 print("KHR-GL46.copy_image.functional", file=f) 254 print("KHR-GL46.texture_swizzle.smoke", file=f) 255 print( 256 "KHR-GL46.tessellation_shader.tessellation_control_to_tessellation_evaluation.gl_MaxPatchVertices_Position_PointSize", 257 file=f, 258 ) 259 print("KHR-Single-GL46.arrays_of_arrays_gl.AtomicUsage", file=f) 260 261 262def gfx_level_to_str(cl): 263 supported = ["gfx6", "gfx7", "gfx8", "gfx9", "gfx10", "gfx10_3", "gfx11", "gfx12"] 264 if 8 <= cl and cl < 8 + len(supported): 265 return supported[cl - 8] 266 return supported[-1] 267 268 269def run_cmd(args, verbosity): 270 if verbosity > 1: 271 print_yellow( 272 "| Command line argument '" 273 + " ".join(['"{}"'.format(a) for a in args]) 274 + "'" 275 ) 276 start = datetime.now() 277 proc = subprocess.Popen( 278 args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env 279 ) 280 while True: 281 line = proc.stdout.readline().decode() 282 if verbosity > 0: 283 if "ERROR" in line: 284 print_red(line.strip(), prefix="| ") 285 else: 286 print("| " + line.strip()) 287 else: 288 sys.stdout.write(next(spin)) 289 sys.stdout.flush() 290 sys.stdout.write("\b") 291 292 logfile.write(line) 293 294 if proc.poll() is not None: 295 break 296 proc.wait() 297 end = datetime.now() 298 299 if verbosity == 0: 300 sys.stdout.write(" ... ") 301 302 print_yellow( 303 "Completed in {} seconds".format(int((end - start).total_seconds())), 304 prefix="└ " if verbosity > 0 else None, 305 ) 306 307 308def verify_results(results): 309 with open(results) as file: 310 lines = file.readlines() 311 if len(lines) == 0: 312 return True 313 print("{} new result{}:".format(len(lines), 's' if len(lines) > 1 else '')) 314 for i in range(min(10, len(lines))): 315 print(" * ", end='') 316 if "Pass" in lines[i]: 317 print_green(lines[i][:-1]) 318 else: 319 print_red(lines[i][:-1]) 320 if len(lines) > 10: 321 print_yellow("...") 322 print("Full results: {}".format(results)) 323 324 return False 325 326 327def parse_test_filters(include_tests, baseline): 328 cmd = [] 329 for t in include_tests: 330 if t == 'baseline': 331 t = baseline 332 333 if os.path.exists(t): 334 with open(t, "r") as file: 335 for row in csv.reader(file, delimiter=","): 336 if not row or row[0][0] == "#": 337 continue 338 cmd += ["-t", row[0]] 339 else: 340 cmd += ["-t", t] 341 return cmd 342 343 344def select_baseline(basepath, gfx_level, gpu_name): 345 gfx_level_str = gfx_level_to_str(gfx_level) 346 347 # select the best baseline we can find 348 # 1. exact match 349 exact = os.path.join(base, "{}-{}-fail.csv".format(gfx_level_str, gpu_name)) 350 if os.path.exists(exact): 351 return exact 352 # 2. any baseline with the same gfx_level 353 while gfx_level >= 8: 354 gfx_level_str += '-' 355 for subdir, dirs, files in os.walk(basepath): 356 for file in files: 357 if file.find(gfx_level_str) == 0 and file.endswith("-fail.csv"): 358 return os.path.join(base, file) 359 # No match. Try an earlier class 360 gfx_level = gfx_level - 1 361 gfx_level_str = gfx_level_to_str(gfx_level) 362 363 return exact 364 365 366success = True 367baseline = select_baseline(base, gfx_level, gpu_name) 368filters_args = parse_test_filters(args.include_tests, baseline) 369flakes = [ 370 f 371 for f in ( 372 os.path.join(base, g) 373 for g in [ 374 "radeonsi-flakes.csv", 375 "{}-{}-flakes.csv".format(gfx_level_to_str(gfx_level), gpu_name), 376 ] 377 ) 378 if os.path.exists(f) 379] 380flakes_args = [] 381for f in flakes: 382 flakes_args += ["--flakes", f] 383 384if os.path.exists(baseline): 385 print_yellow("Baseline: {}".format(baseline)) 386if flakes_args: 387 print_yellow("Flakes: {}".format(flakes_args)) 388 389# piglit test 390if args.piglit: 391 out = os.path.join(output_folder, "piglit") 392 print_yellow("Running piglit tests", args.verbose > 0) 393 cmd = [ 394 "piglit-runner", 395 "run", 396 "--piglit-folder", 397 piglit_path, 398 "--profile", 399 "quick", 400 "--output", 401 out, 402 "--process-isolation", 403 "--timeout", 404 "300", 405 "--jobs", 406 str(args.jobs), 407 "--skips", 408 skips, 409 "--skips", 410 os.path.join(path_above_mesa, "mesa", ".gitlab-ci", "gbm-skips.txt") 411 ] + filters_args + flakes_args 412 413 if os.path.exists(baseline): 414 cmd += ["--baseline", baseline] 415 416 run_cmd(cmd, args.verbose) 417 418 if not verify_results(os.path.join(out, "failures.csv")): 419 success = False 420 421deqp_args = "-- --deqp-surface-width=256 --deqp-surface-height=256 --deqp-gl-config-name=rgba8888d24s8ms0 --deqp-visibility=hidden".split( 422 " " 423) 424 425# glcts test 426if args.glcts: 427 out = os.path.join(output_folder, "glcts") 428 print_yellow("Running GLCTS tests", args.verbose > 0) 429 os.mkdir(os.path.join(output_folder, "glcts")) 430 431 cmd = [ 432 "deqp-runner", 433 "run", 434 "--tests-per-group", 435 "100", 436 "--deqp", 437 "{}/build/external/openglcts/modules/glcts".format(glcts_path), 438 "--caselist", 439 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl46-main.txt".format( 440 glcts_path 441 ), 442 "--caselist", 443 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass_single/4.6.1.x/gl46-khr-single.txt".format( 444 glcts_path 445 ), 446 "--caselist", 447 "{}/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/4.6.1.x/gl46-gtf-main.txt".format( 448 glcts_path 449 ), 450 "--output", 451 out, 452 "--skips", 453 skips, 454 "--jobs", 455 str(args.jobs), 456 "--timeout", 457 "1000" 458 ] + filters_args + flakes_args 459 460 if os.path.exists(baseline): 461 cmd += ["--baseline", baseline] 462 cmd += deqp_args 463 464 run_cmd(cmd, args.verbose) 465 466 if not verify_results(os.path.join(out, "failures.csv")): 467 success = False 468 469# escts test 470if args.escts: 471 out = os.path.join(output_folder, "escts") 472 print_yellow("Running ESCTS tests", args.verbose > 0) 473 os.mkdir(out) 474 475 cmd = [ 476 "deqp-runner", 477 "run", 478 "--tests-per-group", 479 "100", 480 "--deqp", 481 "{}/build_es/external/openglcts/modules/glcts".format(glcts_path), 482 "--caselist", 483 "{}/external/openglcts/data/gl_cts/data/mustpass/gles/khronos_mustpass/3.2.6.x/gles2-khr-main.txt".format( 484 glcts_path 485 ), 486 "--caselist", 487 "{}/external/openglcts/data/gl_cts/data/mustpass/gles/khronos_mustpass/3.2.6.x/gles3-khr-main.txt".format( 488 glcts_path 489 ), 490 "--caselist", 491 "{}/external/openglcts/data/gl_cts/data/mustpass/gles/khronos_mustpass/3.2.6.x/gles31-khr-main.txt".format( 492 glcts_path 493 ), 494 "--caselist", 495 "{}/external/openglcts/data/gl_cts/data/mustpass/gles/khronos_mustpass/3.2.6.x/gles32-khr-main.txt".format( 496 glcts_path 497 ), 498 "--output", 499 out, 500 "--skips", 501 skips, 502 "--jobs", 503 str(args.jobs), 504 "--timeout", 505 "1000" 506 ] + filters_args + flakes_args 507 508 if os.path.exists(baseline): 509 cmd += ["--baseline", baseline] 510 cmd += deqp_args 511 512 run_cmd(cmd, args.verbose) 513 514 if not verify_results(os.path.join(out, "failures.csv")): 515 success = False 516 517if args.deqp: 518 print_yellow("Running dEQP tests", args.verbose > 0) 519 520 # Generate a test-suite file 521 out = os.path.join(output_folder, "deqp") 522 suite_filename = os.path.join(output_folder, "deqp-suite.toml") 523 suite = open(suite_filename, "w") 524 os.mkdir(out) 525 526 deqp_tests = { 527 "egl": args.deqp_egl, 528 "gles2": args.deqp_gles2, 529 "gles3": args.deqp_gles3, 530 "gles31": args.deqp_gles31, 531 } 532 533 for k in deqp_tests: 534 if not deqp_tests[k]: 535 continue 536 537 suite.write("[[deqp]]\n") 538 suite.write( 539 'deqp = "{}"\n'.format( 540 "{}/build/modules/{subtest}/deqp-{subtest}".format(glcts_path, subtest=k) 541 ) 542 ) 543 suite.write( 544 'caselists = ["{}"]\n'.format( 545 "{}/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) 546 ) 547 ) 548 if os.path.exists(baseline): 549 suite.write('baseline = "{}"\n'.format(baseline)) 550 suite.write('skips = ["{}"]\n'.format(skips)) 551 suite.write("deqp_args = [\n") 552 for a in deqp_args[1:-1]: 553 suite.write(' "{}",\n'.format(a)) 554 suite.write(' "{}"\n'.format(deqp_args[-1])) 555 suite.write("]\n") 556 557 suite.close() 558 559 cmd = [ 560 "deqp-runner", 561 "suite", 562 "--jobs", 563 str(args.jobs), 564 "--output", 565 os.path.join(output_folder, "deqp"), 566 "--suite", 567 suite_filename, 568 ] + filters_args + flakes_args 569 570 run_cmd(cmd, args.verbose) 571 572 if not verify_results(os.path.join(out, "failures.csv")): 573 success = False 574 575sys.exit(0 if success else 1) 576