1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4""" 5Copyright (c) 2023-2024 Huawei Device Co., Ltd. 6Licensed under the Apache License, Version 2.0 (the "License"); 7you may not use this file except in compliance with the License. 8You may obtain a copy of the License at 9 10 http://www.apache.org/licenses/LICENSE-2.0 11 12Unless required by applicable law or agreed to in writing, software 13distributed under the License is distributed on an "AS IS" BASIS, 14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15See the License for the specific language governing permissions and 16limitations under the License. 17 18Description: run regress test case 19""" 20import argparse 21import dataclasses 22import datetime 23import json 24import logging 25import multiprocessing 26import os 27import platform 28import re 29import shutil 30import stat 31import subprocess 32import sys 33from abc import ABC 34from typing import Optional, List, Type, Dict, Set, Tuple, Callable 35from os.path import dirname, join 36from pathlib import Path 37import xml.etree.cElementTree as XTree 38from enum import Enum, auto 39 40from regress_test_config import RegressTestConfig 41 42ENV_PATTERN = re.compile(r"//\s+Environment Variables:(.*)") 43 44 45def init_log_file(args): 46 logging.basicConfig(filename=args.out_log, format=RegressTestConfig.DEFAULT_LOG_FORMAT, level=logging.INFO) 47 48 49def parse_args(): 50 parser = argparse.ArgumentParser() 51 parser.add_argument('--test-dir', metavar='DIR', 52 help='Directory to test ') 53 parser.add_argument('--test-file', metavar='FILE', 54 help='File to test') 55 parser.add_argument('--test-list', metavar='FILE', dest="test_list", default=None, 56 help='File with list of tests to run') 57 parser.add_argument('--ignore-list', metavar='FILE', dest="ignore_list", default=None, 58 help='File with known failed tests list') 59 parser.add_argument('--timeout', default=RegressTestConfig.DEFAULT_TIMEOUT, type=int, 60 help='Set a custom test timeout in seconds !!!\n') 61 parser.add_argument('--processes', default=RegressTestConfig.DEFAULT_PROCESSES, type=int, 62 help='set number of processes to use. Default value: 1\n') 63 parser.add_argument('--stub-path', 64 help="stub file for run in AOT modes") 65 parser.add_argument('--LD_LIBRARY_PATH', '--libs-dir', 66 dest='ld_library_path', default=None, help='LD_LIBRARY_PATH') 67 parser.add_argument('--icu-path', 68 dest='icu_path', help='icu-data-path') 69 parser.add_argument('--out-dir', 70 default=None, help='target out dir') 71 parser = parse_args_run_mode(parser) 72 return parser.parse_args() 73 74 75def parse_args_run_mode(parser): 76 parser.add_argument('--merge-abc-binary', 77 help="merge-abc's binary tool") 78 parser.add_argument('--ark-tool', 79 help="ark's binary tool") 80 parser.add_argument('--ark-aot-tool', 81 help="ark_aot's binary tool") 82 parser.add_argument('--ark-aot', default=False, action='store_true', 83 help="runs in ark-aot mode") 84 parser.add_argument('--run-pgo', default=False, action='store_true', 85 help="runs in pgo mode") 86 parser.add_argument('--enable-litecg', default=False, action='store_true', 87 help="runs in litecg mode") 88 parser.add_argument('--ark-frontend-binary', 89 help="ark frontend conversion binary tool") 90 parser.add_argument('--force-clone', action="store_true", 91 default=False, help='Force to clone tests folder') 92 parser.add_argument('--ark-arch', 93 default=RegressTestConfig.DEFAULT_ARK_ARCH, 94 required=False, 95 nargs='?', choices=RegressTestConfig.ARK_ARCH_LIST, type=str) 96 parser.add_argument('--ark-arch-root', 97 default=RegressTestConfig.DEFAULT_ARK_ARCH, 98 required=False, 99 help="the root path for qemu-aarch64 or qemu-arm") 100 parser.add_argument('--disable-force-gc', action='store_true', 101 help="Run regress tests with close force-gc") 102 parser.add_argument('--compiler-opt-track-field', default=False, action='store', 103 dest="compiler_opt_track_field", 104 help='enable compiler opt track field. Default value: False\n') 105 return parser 106 107 108def check_ark_frontend_binary(args) -> bool: 109 if args.ark_frontend_binary is None: 110 output('ark_frontend_binary is required, please add this parameter') 111 return False 112 return True 113 114 115def check_frontend_library(args) -> bool: 116 current_dir = str(os.getcwd()) 117 current_frontend_binary = os.path.join(current_dir, str(args.ark_frontend_binary)) 118 test_tool_frontend_binary = os.path.join(RegressTestConfig.TEST_TOOL_FILE_DIR, args.ark_frontend_binary) 119 if not os.path.exists(current_frontend_binary) and not os.path.exists(test_tool_frontend_binary): 120 output('entered ark_frontend_binary does not exist. please confirm') 121 return False 122 args.ark_frontend_binary = current_frontend_binary if os.path.exists( 123 current_frontend_binary) else test_tool_frontend_binary 124 args.ark_frontend_binary = os.path.abspath(args.ark_frontend_binary) 125 return True 126 127 128def check_ark_tool(args) -> bool: 129 current_dir = str(os.getcwd()) 130 if args.ark_tool is None: 131 output('ark_tool is required, please add this parameter') 132 return False 133 134 current_ark_tool = os.path.join(current_dir, str(args.ark_tool)) 135 test_tool_ark_tool = os.path.join(RegressTestConfig.TEST_TOOL_FILE_DIR, args.ark_tool) 136 if not os.path.exists(current_ark_tool) and not os.path.exists(test_tool_ark_tool): 137 output('entered ark_tool does not exist. please confirm') 138 return False 139 140 args.ark_tool = current_ark_tool if os.path.exists(current_ark_tool) else test_tool_ark_tool 141 args.ark_tool = os.path.abspath(args.ark_tool) 142 return True 143 144 145def check_ark_aot(args) -> bool: 146 if args.ark_aot: 147 current_dir = str(os.getcwd()) 148 current_ark_aot_tool = os.path.join(current_dir, str(args.ark_aot_tool)) 149 test_tool_ark_aot_tool = os.path.join(RegressTestConfig.TEST_TOOL_FILE_DIR, args.ark_aot_tool) 150 if not os.path.exists(current_ark_aot_tool) and not os.path.exists(test_tool_ark_aot_tool): 151 output(f'entered ark_aot_tool "{args.ark_aot_tool}" does not exist. Please check') 152 return False 153 args.ark_aot_tool = current_ark_aot_tool if os.path.exists(current_ark_aot_tool) else test_tool_ark_aot_tool 154 args.ark_aot_tool = os.path.abspath(args.ark_aot_tool) 155 return True 156 if args.run_pgo and not args.ark_aot: 157 output('pgo mode cannot be used without aot') 158 return False 159 return True 160 161 162def check_stub_path(args) -> bool: 163 if args.stub_path: 164 current_dir = str(os.getcwd()) 165 stub_path = os.path.join(current_dir, str(args.stub_path)) 166 if not os.path.exists(stub_path): 167 output(f'entered stub-file "{args.stub_path}" does not exist. Please check') 168 return False 169 args.stub_path = os.path.abspath(args.stub_path) 170 return True 171 172 173def is_ignore_file_present(ignore_path: str) -> bool: 174 if os.path.exists(ignore_path): 175 return True 176 output(f"Cannot find ignore list '{ignore_path}'") 177 return False 178 179 180def check_ignore_list(args) -> bool: 181 if args.ignore_list: 182 if os.path.isabs(args.ignore_list): 183 return is_ignore_file_present(args.ignore_list) 184 args.ignore_list = str(os.path.join(RegressTestConfig.TEST_TOOL_FILE_DIR, args.ignore_list)) 185 return is_ignore_file_present(args.ignore_list) 186 return True 187 188 189def check_args(args): 190 result = check_ark_frontend_binary(args) 191 result = result and check_frontend_library(args) 192 result = result and check_ark_tool(args) 193 result = result and check_ark_aot(args) 194 result = result and check_stub_path(args) 195 result = result and check_ignore_list(args) 196 197 if not result: 198 return False 199 200 if args.ld_library_path is not None: 201 libs = args.ld_library_path.split(":") 202 current_dir = str(os.getcwd()) 203 libs = [os.path.abspath(os.path.join(current_dir, str(lib))) for lib in libs] 204 args.ld_library_path = ":".join(libs) 205 else: 206 args.ld_library_path = RegressTestConfig.DEFAULT_LIBS_DIR 207 if args.icu_path is None: 208 args.icu_path = RegressTestConfig.ICU_PATH 209 if args.out_dir is None: 210 args.out_dir = RegressTestConfig.PROJECT_BASE_OUT_DIR 211 else: 212 args.out_dir = os.path.abspath(os.path.join(RegressTestConfig.CURRENT_PATH, args.out_dir)) 213 if not args.out_dir.endswith("/"): 214 args.out_dir = f"{args.out_dir}/" 215 args.regress_out_dir = os.path.join(args.out_dir, "regresstest") 216 args.out_result = os.path.join(args.regress_out_dir, 'result.txt') 217 args.junit_report = os.path.join(args.regress_out_dir, 'report.xml') 218 args.out_log = os.path.join(args.regress_out_dir, 'test.log') 219 args.test_case_out_dir = os.path.join(args.regress_out_dir, RegressTestConfig.REGRESS_GIT_REPO) 220 return True 221 222 223def remove_dir(path): 224 if os.path.exists(path): 225 shutil.rmtree(path) 226 227 228def output(msg): 229 print(str(msg)) 230 logging.info(str(msg)) 231 232 233def output_debug(msg): 234 logging.debug(str(msg)) 235 236 237def get_extra_error_message(ret_code: int) -> str: 238 error_messages = { 239 0: '', 240 -6: 'Aborted (core dumped)', 241 -4: 'Aborted (core dumped)', 242 -11: 'Segmentation fault (core dumped)', 243 255: '(uncaught error)' 244 } 245 error_message = error_messages.get(ret_code, f'Unknown Error: {str(ret_code)}') 246 return error_message 247 248 249@dataclasses.dataclass 250class StepResult: 251 step_name: str # a copy of the step name 252 is_passed: bool = False # True if passed, any other state is False 253 command: List[str] = dataclasses.field(default_factory=list) # command to run 254 return_code: int = -1 255 stdout: Optional[str] = None # present only if there is some output 256 stderr: Optional[str] = None # can be present only if is_passed == False 257 fileinfo: Optional[str] = None # content of fileinfo file if present 258 259 def report(self) -> str: 260 stdout = self.stdout if self.stdout else '' 261 stderr = self.stderr if self.stderr else '' 262 cmd = " ".join([str(cmd) for cmd in self.command]) 263 result: List[str] = [ 264 f"{self.step_name}:", 265 f"\tCommand: {cmd}", 266 f"\treturn code={self.return_code}", 267 f"\toutput='{stdout}'", 268 f"\terrors='{stderr}'"] 269 if self.fileinfo: 270 result.append(f"\tFileInfo:\n{self.fileinfo}") 271 return "\n".join(result) 272 273 274@dataclasses.dataclass 275class TestReport: 276 src_path: str # full path to the source test 277 test_id: str = "" # path starting from regresstest 278 out_path: str = "" # full path to intermediate files up to folder 279 passed: bool = False # False if the test has not started or failed 280 is_skipped: bool = False # True if the test has found in the skipped (excluded) list 281 is_ignored: bool = False # True if the test has found in the ignored list 282 steps: List[StepResult] = dataclasses.field(default_factory=list) # list of results 283 284 def report(self) -> str: 285 result: List[str] = [f"{self.test_id}:"] 286 if self.steps is None: 287 return "" 288 for step in self.steps: 289 result.append(f"\t{step.report()}") 290 return "\n".join(result) 291 292 293class RegressTestStep(ABC): 294 step_obj: Optional['RegressTestStep'] = None 295 296 def __init__(self, args, name): 297 output(f"--- Start step {name} ---") 298 self.args = args 299 self.__start: Optional[datetime.datetime] = None 300 self.__end: Optional[datetime.datetime] = None 301 self.__duration: Optional[datetime.timedelta] = None 302 self.name: str = name 303 304 @staticmethod 305 def run(args, test_reports: Optional[List[TestReport]] = None) -> List[TestReport]: 306 pass 307 308 def get_duration(self) -> datetime.timedelta: 309 if self.__duration is None: 310 output(f"Step {self.name} not started or not completed") 311 sys.exit(1) 312 return self.__duration 313 314 def _start(self): 315 self.__start = datetime.datetime.now() 316 317 def _end(self): 318 self.__end = datetime.datetime.now() 319 self.__duration = self.__end - self.__start 320 321 322class RegressTestRepoPrepare(RegressTestStep): 323 def __init__(self, args): 324 RegressTestStep.__init__(self, args, "Repo preparation") 325 self.test_list: List[str] = self.read_test_list(args.test_list) 326 327 @staticmethod 328 def read_test_list(test_list_name: Optional[str]) -> List[str]: 329 if test_list_name is None: 330 return [] 331 filename = join(dirname(__file__), test_list_name) 332 if not Path(filename).exists(): 333 output(f"File {filename} set as --test-list value cannot be found") 334 exit(1) 335 with open(filename, 'r') as stream: 336 return stream.read().split("\n") 337 338 @staticmethod 339 def run(args, test_reports: Optional[List[TestReport]] = None) -> List[TestReport]: 340 repo = RegressTestRepoPrepare(args) 341 RegressTestRepoPrepare.step_obj = repo 342 repo._start() 343 344 repo.run_regress_test_prepare() 345 repo.prepare_clean_data() 346 repo.get_test_case() 347 test_list = repo.get_regress_test_files() 348 skip_list = repo.get_skip_test_cases() 349 if test_reports is None: 350 test_reports = [] 351 for test in test_list: 352 shorten = Utils.get_inside_path(test) 353 test_id = f"regresstest/ark-regress/{shorten}" 354 if shorten not in skip_list: 355 report = TestReport(src_path=test, test_id=test_id) 356 test_reports.append(report) 357 358 repo._end() 359 return test_reports 360 361 @staticmethod 362 def git_checkout(checkout_options, check_out_dir=os.getcwd()): 363 cmds = ['git', 'checkout', checkout_options] 364 result = True 365 with subprocess.Popen(cmds, cwd=check_out_dir) as proc: 366 ret = proc.wait() 367 if ret: 368 output(f"\n error: git checkout '{checkout_options}' failed.") 369 result = False 370 return result 371 372 @staticmethod 373 def git_pull(check_out_dir=os.getcwd()): 374 cmds = ['git', 'pull', '--rebase'] 375 with subprocess.Popen(cmds, cwd=check_out_dir) as proc: 376 proc.wait() 377 378 @staticmethod 379 def git_clean(clean_dir=os.getcwd()): 380 cmds = ['git', 'checkout', '--', '.'] 381 with subprocess.Popen(cmds, cwd=clean_dir) as proc: 382 proc.wait() 383 384 @staticmethod 385 def git_clone(git_url, code_dir): 386 cmds = ['git', 'clone', git_url, code_dir] 387 retries = RegressTestConfig.DEFAULT_RETRIES 388 while retries > 0: 389 with subprocess.Popen(cmds) as proc: 390 ret = proc.wait() 391 if ret: 392 output(f"\n Error: Cloning '{git_url}' failed. Retry remaining '{retries}' times") 393 retries -= 1 394 else: 395 return True 396 sys.exit(1) 397 398 @staticmethod 399 def get_skip_test_cases() -> List[str]: 400 return Utils.read_skip_list(RegressTestConfig.SKIP_LIST_FILE) 401 402 def get_test_case(self): 403 if not os.path.isdir(os.path.join(RegressTestConfig.REGRESS_TEST_CASE_DIR, '.git')): 404 self.git_clone(RegressTestConfig.REGRESS_GIT_URL, RegressTestConfig.REGRESS_TEST_CASE_DIR) 405 return self.git_checkout(RegressTestConfig.REGRESS_GIT_HASH, RegressTestConfig.REGRESS_TEST_CASE_DIR) 406 return True 407 408 def prepare_clean_data(self): 409 self.git_clean(RegressTestConfig.REGRESS_TEST_CASE_DIR) 410 self.git_pull(RegressTestConfig.REGRESS_TEST_CASE_DIR) 411 self.git_checkout(RegressTestConfig.REGRESS_GIT_HASH, RegressTestConfig.REGRESS_TEST_CASE_DIR) 412 413 def run_regress_test_prepare(self): 414 if self.args.force_clone: 415 remove_dir(self.args.regress_out_dir) 416 remove_dir(RegressTestConfig.REGRESS_TEST_CASE_DIR) 417 os.makedirs(self.args.regress_out_dir, exist_ok=True) 418 os.makedirs(RegressTestConfig.REGRESS_TEST_CASE_DIR, exist_ok=True) 419 init_log_file(self.args) 420 421 def get_regress_test_files(self) -> List[str]: 422 result: List[str] = [] 423 if self.args.test_file is not None and len(self.args.test_file) > 0: 424 test_file_list = os.path.join(RegressTestConfig.REGRESS_TEST_CASE_DIR, self.args.test_file) 425 result.append(str(test_file_list)) 426 return result 427 elif self.args.test_dir is not None and len(self.args.test_dir) > 0: 428 test_file_list = os.path.join(RegressTestConfig.REGRESS_TEST_CASE_DIR, self.args.test_dir) 429 else: 430 test_file_list = RegressTestConfig.REGRESS_TEST_CASE_DIR 431 for dir_path, path, filenames in os.walk(test_file_list): 432 if dir_path.find(".git") != -1: 433 continue 434 for filename in filenames: 435 if filename.endswith(".js") or filename.endswith(".mjs"): 436 result.append(str(os.path.join(dir_path, filename))) 437 return result 438 439 440class RegressTestCompile(RegressTestStep): 441 def __init__(self, args, test_reports: List[TestReport]): 442 RegressTestStep.__init__(self, args, "Regress test compilation") 443 self.out_dir = args.out_dir 444 self.test_reports = test_reports 445 for test in self.test_reports: 446 test.out_path = os.path.dirname(os.path.join(self.out_dir, test.test_id)) 447 448 @staticmethod 449 def run(args, test_reports: Optional[List[TestReport]] = None) -> List[TestReport]: 450 if test_reports is None: 451 output("No tests loaded") 452 exit(-1) 453 test_prepare = RegressTestCompile(args, test_reports) 454 RegressTestCompile.step_obj = test_prepare 455 test_prepare._start() 456 test_reports = test_prepare.gen_abc_files() 457 test_prepare._end() 458 return test_reports 459 460 @staticmethod 461 def create_files_info(test_report: TestReport) -> Tuple[str, str]: 462 src_files_info = [ 463 RegressTestConfig.REGRESS_TEST_TOOL_DIR, 464 test_report.src_path 465 ] 466 file_info_content: List[str] = [] 467 file_info_path = str(os.path.join( 468 test_report.out_path, 469 f"{Utils.get_file_only_name(test_report.src_path)}-filesInfo.txt")) 470 os.makedirs(test_report.out_path, exist_ok=True) 471 with os.fdopen( 472 os.open(file_info_path, flags=os.O_RDWR | os.O_CREAT, mode=stat.S_IRUSR | stat.S_IWUSR), 473 mode="w+", encoding="utf-8" 474 ) as fp: 475 for src_file_info in src_files_info: 476 line = f"{src_file_info};{Utils.get_file_only_name(src_file_info)};esm;xxx;yyy\n" 477 file_info_content.append(line) 478 fp.write(line) 479 return file_info_path, "\n".join(file_info_content) 480 481 def gen_abc_files(self) -> List[TestReport]: 482 with multiprocessing.Pool(processes=self.args.processes) as pool: 483 results = pool.imap_unordered(self.gen_abc_file, self.test_reports) 484 results = list(results) 485 pool.close() 486 pool.join() 487 488 return results 489 490 def gen_abc_file(self, test_report: TestReport) -> Optional[TestReport]: 491 if test_report.src_path == RegressTestConfig.REGRESS_TEST_TOOL_DIR: 492 return None 493 file_info_path, file_info_content = self.create_files_info(test_report) 494 out_file = change_extension(test_report.src_path, '.out') 495 expect_file_exists = os.path.exists(out_file) 496 output_file = change_extension( 497 os.path.join(test_report.out_path, Utils.get_file_name(test_report.test_id)), 498 ".abc") 499 command = [ 500 self.args.ark_frontend_binary, 501 f"@{file_info_path}", 502 "--merge-abc", 503 "--module", 504 f'--output={output_file}' 505 ] 506 step_result = StepResult(self.name, command=command, fileinfo=file_info_content) 507 Utils.exec_command(command, test_report.test_id, step_result, self.args.timeout, 508 lambda rt, _, _2: get_extra_error_message(rt)) 509 test_report.steps.append(step_result) 510 test_report.passed = step_result.is_passed 511 if expect_file_exists: 512 out_file_path = os.path.join(test_report.out_path, change_extension(test_report.test_id, '.out')) 513 shutil.copy(str(out_file), str(out_file_path)) 514 return test_report 515 516 517class RegressTestPgo(RegressTestStep): 518 def __init__(self, args): 519 RegressTestStep.__init__(self, args, "Regress Test PGO ") 520 521 @staticmethod 522 def run(args, test_reports: Optional[List[TestReport]] = None) -> List[TestReport]: 523 pgo = RegressTestPgo(args) 524 RegressTestPgo.step_obj = pgo 525 pgo._start() 526 test_reports = pgo.generate_aps(test_reports) 527 pgo._end() 528 return test_reports 529 530 def get_test_ap_cmd(self, test_report: TestReport) -> List[str]: 531 abc_file = change_extension( 532 os.path.join(test_report.out_path, Utils.get_file_name(test_report.test_id)), 533 ".abc") 534 ap_file = change_extension(abc_file, ".ap") 535 entry_point = Utils.get_file_only_name(RegressTestConfig.TEST_TOOL_FILE_JS_NAME) 536 os.environ["LD_LIBRARY_PATH"] = self.args.ld_library_path 537 gen_ap_cmd = [] 538 if self.args.ark_arch == RegressTestConfig.ARK_ARCH_LIST[1]: 539 qemu_tool = "qemu-aarch64" 540 gen_ap_cmd = [ 541 qemu_tool, 542 "-L", 543 self.args.ark_arch_root 544 ] 545 gen_ap_cmd.append(self.args.ark_tool) 546 gen_ap_cmd.append("--log-level=info") 547 gen_ap_cmd.append(f"--icu-data-path={self.args.icu_path}") 548 gen_ap_cmd.append("--compiler-target-triple=aarch64-unknown-linux-gn") 549 gen_ap_cmd.append("--enable-pgo-profiler=true") 550 gen_ap_cmd.append("--compiler-opt-inlining=true") 551 gen_ap_cmd.append(f"--compiler-pgo-profiler-path={ap_file}") 552 gen_ap_cmd.append("--asm-interpreter=true") 553 gen_ap_cmd.append(f"--entry-point={entry_point}") 554 gen_ap_cmd.append(f"{abc_file}") 555 return gen_ap_cmd 556 557 def generate_ap(self, test_report: Optional[TestReport]) -> Optional[TestReport]: 558 if test_report is None or not test_report.passed: 559 return test_report 560 command = self.get_test_ap_cmd(test_report) 561 step = StepResult(self.name, command=command) 562 Utils.exec_command(command, test_report.test_id, step, self.args.timeout, 563 lambda rt, _, _2: get_extra_error_message(rt)) 564 test_report.steps.append(step) 565 test_report.passed = step.is_passed 566 return test_report 567 568 def generate_aps(self, test_reports: List[TestReport]) -> List[TestReport]: 569 with multiprocessing.Pool(processes=self.args.processes) as pool: 570 results = pool.imap_unordered(self.generate_ap, test_reports) 571 results = list(results) 572 pool.close() 573 pool.join() 574 575 return results 576 577 578class Utils: 579 ark_regress = "ark-regress" 580 581 @staticmethod 582 def get_file_only_name(full_file_name: str) -> str: 583 _, file_name = os.path.split(full_file_name) 584 only_name, _ = os.path.splitext(file_name) 585 return only_name 586 587 @staticmethod 588 def get_file_name(full_file_name: str) -> str: 589 _, file_name = os.path.split(full_file_name) 590 return file_name 591 592 @staticmethod 593 def mk_dst_dir(file, src_dir, dist_dir): 594 idx = file.rfind(src_dir) 595 fpath, _ = os.path.split(file[idx:]) 596 fpath = fpath.replace(src_dir, dist_dir) 597 os.makedirs(fpath, exist_ok=True) 598 599 @staticmethod 600 def get_inside_path(file_path: str, marker: Optional[str] = None) -> str: 601 if marker is None: 602 marker = Utils.ark_regress 603 index = file_path.find(marker) 604 if index > -1: 605 return file_path[index + len(marker) + 1:] 606 return file_path 607 608 @staticmethod 609 def exec_command(cmd_args, test_id: str, step_result: StepResult, timeout=RegressTestConfig.DEFAULT_TIMEOUT, 610 get_extra_error_msg: Optional[Callable[[int, str, str], str]] = None) -> None: 611 code_format = 'utf-8' 612 if platform.system() == "Windows": 613 code_format = 'gbk' 614 cmd_string = "\n\t".join([str(arg).strip() for arg in cmd_args if arg is not None]) 615 msg_cmd = "\n".join([ 616 f"Run command:\n{cmd_string}", 617 f"Env: {os.environ.get('LD_LIBRARY_PATH')}" 618 ]) 619 msg_result = f"TEST ({step_result.step_name.strip()}): {test_id}" 620 try: 621 with subprocess.Popen(cmd_args, stderr=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=True, 622 start_new_session=True) as process: 623 output_res, errs = process.communicate(timeout=timeout) 624 ret_code = process.poll() 625 step_result.return_code = ret_code 626 stderr = str(errs.decode(code_format, 'ignore').strip()) 627 stdout = str(output_res.decode(code_format, 'ignore').strip()) 628 extra_message = get_extra_error_msg(ret_code, stdout, stderr) if get_extra_error_msg is not None else "" 629 step_result.stderr = f"{extra_message + '. ' if extra_message else '' }{stderr if stderr else ''}" 630 step_result.stdout = stdout 631 if ret_code == 0: 632 msg_result = f"{msg_result} PASSED" 633 step_result.is_passed = True 634 else: 635 msg_result = f"{msg_result} FAILED" 636 except subprocess.TimeoutExpired: 637 process.kill() 638 process.terminate() 639 step_result.return_code = -1 640 step_result.stderr = f"Timeout: timed out after {timeout} seconds" 641 msg_result = f"{msg_result} FAILED" 642 except Exception as exc: 643 step_result.return_code = -1 644 step_result.stderr = f"Unknown error: {exc}" 645 msg_result = f"{msg_result} FAILED" 646 if step_result.is_passed: 647 output(msg_result) 648 output_debug(msg_cmd) 649 else: 650 output(f"{msg_result}\n{step_result.stderr}\n{msg_cmd}") 651 652 @staticmethod 653 def read_skip_list(skip_list_path: str) -> List[str]: 654 skip_tests_list = [] 655 with os.fdopen(os.open(skip_list_path, os.O_RDONLY, stat.S_IRUSR), "r") as file_object: 656 json_data = json.load(file_object) 657 for key in json_data: 658 skip_tests_list.extend(key["files"]) 659 return skip_tests_list 660 661 @staticmethod 662 def read_file_as_str(file_name: str) -> str: 663 with os.fdopen(os.open(file_name, os.O_RDONLY, stat.S_IRUSR), "r") as file_object: 664 content = [line.strip() for line in file_object.readlines()] 665 return "\n".join(content) 666 667 668class RegressTestAot(RegressTestStep): 669 def __init__(self, args): 670 RegressTestStep.__init__(self, args, "Regress Test AOT mode") 671 672 @staticmethod 673 def run(args, test_reports: Optional[List[TestReport]] = None) -> List[TestReport]: 674 aot = RegressTestAot(args) 675 RegressTestAot.step_obj = aot 676 aot._start() 677 test_reports = aot.compile_aots(test_reports) 678 aot._end() 679 return test_reports 680 681 def get_test_aot_cmd(self, test_report: TestReport) -> List[str]: 682 abc_file = change_extension( 683 os.path.join(test_report.out_path, Utils.get_file_name(test_report.test_id)), 684 ".abc") 685 ap_file = change_extension(abc_file, ".ap") 686 aot_file = change_extension(abc_file, "") 687 compiler_opt_track_field = self.args.compiler_opt_track_field 688 os.environ["LD_LIBRARY_PATH"] = self.args.ld_library_path 689 if self.args.ark_arch == RegressTestConfig.ARK_ARCH_LIST[1]: 690 aot_cmd = [ 691 "qemu-aarch64", 692 "-L", 693 self.args.ark_arch_root, 694 self.args.ark_aot_tool, 695 "--compiler-target-triple=aarch64-unknown-linux-gnu", 696 f"--aot-file={aot_file}" 697 ] 698 else: 699 aot_cmd = [ 700 self.args.ark_aot_tool, 701 f"--aot-file={aot_file}", 702 ] 703 pgo = [ 704 "--compiler-opt-loop-peeling=true", 705 "--compiler-fast-compile=false", 706 f"--compiler-opt-track-field={compiler_opt_track_field}", 707 "--compiler-opt-inlining=true", 708 "--compiler-max-inline-bytecodes=45", 709 "--compiler-opt-level=2", 710 f"--compiler-pgo-profiler-path={ap_file}", 711 ] 712 litecg = [ 713 "--compiler-enable-litecg=true", 714 ] 715 aot_cmd_tail = [ 716 f"{abc_file}", 717 ] 718 719 if self.args.run_pgo: 720 aot_cmd.extend(pgo) 721 if self.args.enable_litecg: 722 aot_cmd.extend(litecg) 723 aot_cmd.extend(aot_cmd_tail) 724 return aot_cmd 725 726 def compile_aot(self, test_report: Optional[TestReport]) -> Optional[TestReport]: 727 if test_report is None or not test_report.passed: 728 return test_report 729 command = self.get_test_aot_cmd(test_report) 730 step = StepResult(self.name, command=command) 731 Utils.exec_command(command, test_report.test_id, step, self.args.timeout, 732 lambda rt, _, _2: get_extra_error_message(rt)) 733 test_report.steps.append(step) 734 test_report.passed = step.is_passed 735 return test_report 736 737 def compile_aots(self, test_reports: List[TestReport]) -> List[TestReport]: 738 with multiprocessing.Pool(processes=self.args.processes) as pool: 739 results = pool.imap_unordered(self.compile_aot, test_reports) 740 results = list(results) 741 pool.close() 742 pool.join() 743 744 return results 745 746 747class RegressOption(Enum): 748 NO_FORCE_GC = auto() 749 ELEMENTS_KIND = auto() 750 751 752def get_regress_groups() -> Dict[RegressOption, List[str]]: 753 groups = {} 754 with os.fdopen(os.open(RegressTestConfig.REGRESS_TEST_OPTIONS, os.O_RDONLY, stat.S_IRUSR), "r") as file: 755 for group in json.load(file): 756 groups[RegressOption[group["name"]]] = group["files"] 757 return groups 758 759 760def get_test_options(test: str, test_groups: Dict[RegressOption, List[str]], regress_option: RegressOption) -> List[str]: 761 opt_values: Dict[RegressOption, str] = { 762 RegressOption.NO_FORCE_GC: "--enable-force-gc=", 763 RegressOption.ELEMENTS_KIND: "--enable-elements-kind=" 764 } 765 766 def match(opt: RegressOption) -> bool: 767 return test in test_groups.get(opt, []) 768 769 def to_flag(b: bool) -> str: 770 return str(b).lower() 771 772 try: 773 return [opt_values.get(regress_option) + to_flag(not match(regress_option))] 774 except KeyError: 775 return [] 776 777 778class RegressTestRun(RegressTestStep): 779 def __init__(self, args): 780 RegressTestStep.__init__(self, args, "Regress Test Run ") 781 self.test_groups: Dict[RegressOption, List[str]] = get_regress_groups() 782 783 @staticmethod 784 def run(args, test_reports: Optional[List[TestReport]] = None) -> List[TestReport]: 785 runner = RegressTestRun(args) 786 RegressTestRun.step_obj = runner 787 runner._start() 788 test_reports = runner.run_test_case_dir(test_reports) 789 runner._end() 790 return test_reports 791 792 @staticmethod 793 def extra_check_with_expect(ret_code: int, test_report: TestReport, expect_file, stdout: str, stderr: str) -> str: 794 expect_output_str = read_expect_file(expect_file, test_report.src_path) 795 test_case_file = Utils.read_file_as_str(test_report.src_path) 796 if stdout == expect_output_str.strip() or stderr == expect_output_str.strip(): 797 if ret_code == 0 or (ret_code == 255 and "/fail/" in test_case_file): 798 return "" 799 else: 800 return get_extra_error_message(ret_code) 801 msg = f'expect: [{expect_output_str}]\nbut got: [{stderr}]' 802 return msg 803 804 @staticmethod 805 def extra_check_with_assert(ret_code: int, stderr: Optional[str]) -> str: 806 if ret_code == 0 and stderr and "[ecmascript] Stack overflow" not in stderr: 807 return str(stderr) 808 return get_extra_error_message(ret_code) 809 810 def run_test_case_dir(self, test_reports: List[TestReport]) -> List[TestReport]: 811 with multiprocessing.Pool(processes=self.args.processes, initializer=init_worker, 812 initargs=(self.args,)) as pool: 813 results = pool.imap_unordered(self.run_test_case, test_reports) 814 results = list(results) 815 pool.close() 816 pool.join() 817 818 return results 819 820 def run_test_case(self, test_report: TestReport) -> Optional[TestReport]: 821 self.args = worker_wrapper_args 822 if self.args is None or test_report is None or not test_report.passed: 823 return test_report 824 if test_report.src_path.endswith(RegressTestConfig.TEST_TOOL_FILE_JS_NAME): 825 return None 826 827 abc_file = change_extension( 828 os.path.join(test_report.out_path, Utils.get_file_name(test_report.test_id)), 829 ".abc") 830 aot_file = change_extension(abc_file, "") 831 expect_file = change_extension(abc_file, ".out") 832 entry_point = Utils.get_file_only_name(RegressTestConfig.TEST_TOOL_FILE_JS_NAME) 833 834 os.environ["LD_LIBRARY_PATH"] = self.args.ld_library_path 835 836 # test environ LC_ALL/TZ 837 test_name = test_report.test_id.replace('regresstest/ark-regress/', '') 838 set_test_environ(test_report.src_path) 839 command = [] 840 if self.args.ark_arch == RegressTestConfig.ARK_ARCH_LIST[1]: 841 qemu_tool = "qemu-aarch64" 842 qemu_arg1 = "-L" 843 qemu_arg2 = self.args.ark_arch_root 844 command = [qemu_tool, qemu_arg1, qemu_arg2] 845 command.append(self.args.ark_tool) 846 command.append(f"--icu-data-path={self.args.icu_path}") 847 command.append(f"--entry-point={entry_point}") 848 if self.args.ark_aot: 849 command.append(f"--stub-file={self.args.stub_path}") 850 command.append(f"--aot-file={aot_file}") 851 if self.args.disable_force_gc: 852 command.append("--enable-force-gc=false") 853 else: 854 command.extend(get_test_options(test_name, self.test_groups, RegressOption.NO_FORCE_GC)) 855 command.extend(get_test_options(test_name, self.test_groups, RegressOption.ELEMENTS_KIND)) 856 command.append(abc_file) 857 858 return self.run_test_case_file(command, test_report, expect_file) 859 860 def run_test_case_file(self, command, test_report: TestReport, expect_file) -> TestReport: 861 expect_file_exits = os.path.exists(expect_file) 862 step = StepResult(self.name, command=command) 863 if expect_file_exits: 864 self.run_test_case_with_expect(command, step, test_report, expect_file) 865 else: 866 self.run_test_case_with_assert(command, step, test_report) 867 test_report.steps.append(step) 868 test_report.passed = step.is_passed 869 return test_report 870 871 def run_test_case_with_expect(self, command, step: StepResult, test_report: TestReport, expect_file) -> None: 872 Utils.exec_command(command, test_report.test_id, step, self.args.timeout, 873 lambda rt, out, err: self.extra_check_with_expect(rt, test_report, expect_file, out, err)) 874 875 def run_test_case_with_assert(self, command, step: StepResult, test_report: TestReport) -> None: 876 Utils.exec_command(command, test_report.test_id, step, self.args.timeout, 877 lambda rt, _, err: self.extra_check_with_assert(rt, err)) 878 879 880class Stats: 881 def __init__(self, args, test_reports: List[TestReport]): 882 self.args = args 883 self.pass_count = 0 884 self.fail_count = 0 885 self.test_reports = test_reports 886 self.errors: Dict[str, List[TestReport]] = {} 887 888 def read_ignore_list(self) -> Optional[Set[str]]: 889 if self.args.ignore_list is None: 890 return None 891 with os.fdopen(os.open(self.args.ignore_list, os.O_RDWR, stat.S_IRUSR), "r+") as file_object: 892 lines = file_object.readlines() 893 lines = [line.strip() for line in lines if not line.strip().startswith('#')] 894 return set(lines) 895 896 def get_new_failures(self) -> Optional[List[TestReport]]: 897 ignore_list = self.read_ignore_list() 898 if ignore_list is None: 899 return None 900 new_failures: List[TestReport] = [] 901 for test_report in self.test_reports: 902 if test_report and not test_report.passed and test_report.steps: 903 if test_report.test_id not in ignore_list: 904 new_failures.append(test_report) 905 return new_failures 906 907 def statistics(self): 908 root = XTree.Element("testsuite") 909 root.set("name", "Regression") 910 911 result_file = open_write_file(self.args.out_result, False) 912 for test_report in self.test_reports: 913 if test_report is None: 914 continue 915 testcase = XTree.SubElement(root, "testcase") 916 testcase.set("name", f"{test_report.test_id}") 917 if test_report.passed: 918 write_result_file(f"PASS: {test_report.test_id}", result_file) 919 self.pass_count += 1 920 else: 921 self.fail_count += 1 922 write_result_file(f"FAIL: {test_report.test_id}", result_file) 923 failed_step = test_report.steps[-1] 924 if failed_step.step_name not in self.errors: 925 self.errors[failed_step.step_name] = [] 926 self.errors[failed_step.step_name].append(test_report) 927 XTree.SubElement(testcase, "failure").text = f"<![CDATA[{test_report.report()}]]>" 928 929 root.set("tests", f"{self.pass_count + self.fail_count}") 930 root.set("failures", f"{self.fail_count}") 931 932 tree = XTree.ElementTree(root) 933 tree.write(self.args.junit_report, xml_declaration=True, encoding="UTF-8") 934 result_file.close() 935 936 def print_result(self, args, steps): 937 result_file = open_write_file(args.out_result, True) 938 summary_duration = datetime.timedelta() 939 for step in steps: 940 output(f"Step {step.step_obj.name} - duration {step.step_obj.get_duration()}") 941 summary_duration += step.step_obj.get_duration() 942 msg = f'\npass count: {self.pass_count}' 943 write_result_file(msg, result_file) 944 output(msg) 945 msg = f'fail count: {self.fail_count}' 946 write_result_file(msg, result_file) 947 output(msg) 948 msg = f'total count: {self.fail_count + self.pass_count}' 949 write_result_file(msg, result_file) 950 output(msg) 951 msg = f'total used time is: {str(summary_duration)}' 952 write_result_file(msg, result_file) 953 output(msg) 954 result_file.close() 955 956 def print_failed_tests(self): 957 output("=== Failed tests ===") 958 for key, values in self.errors.items(): 959 output(f"{key}: {len(values)} tests") 960 961 962def change_extension(path, new_ext: str): 963 base_path, ext = os.path.splitext(path) 964 if ext: 965 new_path = base_path + new_ext 966 else: 967 new_path = path + new_ext 968 return new_path 969 970 971def get_files_by_ext(start_dir, suffix): 972 result = [] 973 for dir_path, dir_names, filenames in os.walk(start_dir): 974 for filename in filenames: 975 if filename.endswith(suffix): 976 result.append(os.path.join(dir_path, filename)) 977 return result 978 979 980def read_expect_file(expect_file, test_case_file): 981 with os.fdopen(os.open(expect_file, os.O_RDWR, stat.S_IRUSR), "r+") as file_object: 982 lines = file_object.readlines() 983 lines = [line for line in lines if not line.strip().startswith('#')] 984 expect_output = ''.join(lines) 985 if test_case_file.startswith("/"): 986 test_case_file = test_case_file.lstrip("/") 987 expect_file = test_case_file.replace('regresstest/', '') 988 test_file_path = os.path.join(RegressTestConfig.REGRESS_BASE_TEST_DIR, expect_file) 989 expect_output_str = expect_output.replace('*%(basename)s', test_file_path) 990 return expect_output_str 991 992 993def open_write_file(file_path, append): 994 if append: 995 args = os.O_RDWR | os.O_CREAT | os.O_APPEND 996 else: 997 args = os.O_RDWR | os.O_CREAT 998 file_descriptor = os.open(file_path, args, stat.S_IRUSR | stat.S_IWUSR) 999 file_object = os.fdopen(file_descriptor, "w+") 1000 return file_object 1001 1002 1003def open_result_excel(file_path): 1004 file_descriptor = os.open(file_path, os.O_RDWR | os.O_CREAT | os.O_APPEND, stat.S_IRUSR | stat.S_IWUSR) 1005 file_object = os.fdopen(file_descriptor, "w+") 1006 return file_object 1007 1008 1009def get_file_source(file): 1010 with open(file, encoding='ISO-8859-1') as f: 1011 return f.read() 1012 1013 1014def set_test_environ(case): 1015 # intl environ LC_ALL 1016 if 'LC_ALL' in os.environ: 1017 del os.environ['LC_ALL'] 1018 if 'TZ' in os.environ: 1019 del os.environ['TZ'] 1020 if not os.path.exists(case): 1021 return 1022 source = get_file_source(case) 1023 env_match = ENV_PATTERN.search(source) 1024 if env_match: 1025 for env_pair in env_match.group(1).strip().split(): 1026 var, value = env_pair.split('=') 1027 if var.find('TZ') >= 0: 1028 os.environ['TZ'] = value 1029 if var.find('LC_ALL') >= 0: 1030 os.environ['LC_ALL'] = value 1031 break 1032 1033 1034# pylint: disable=invalid-name,global-statement 1035worker_wrapper_args = None 1036 1037 1038def init_worker(args): 1039 global worker_wrapper_args 1040 worker_wrapper_args = args 1041 1042 1043def write_result_file(msg: str, result_file): 1044 result_file.write(f'{msg}\n') 1045 1046 1047def main(args): 1048 if not check_args(args): 1049 return 1 1050 output("\nStart regresstest........") 1051 steps: List[Type[RegressTestStep]] = [ 1052 RegressTestRepoPrepare, 1053 RegressTestCompile, 1054 ] 1055 if args.ark_aot: 1056 if args.run_pgo: 1057 steps.append(RegressTestPgo) 1058 steps.append(RegressTestAot) 1059 steps.append(RegressTestRun) 1060 1061 test_reports: List[TestReport] = [] 1062 for step in steps: 1063 test_reports = step.run(args, test_reports) 1064 1065 stats = Stats(args, test_reports) 1066 stats.statistics() 1067 stats.print_result(args, steps) 1068 stats.print_failed_tests() 1069 new_failures = stats.get_new_failures() 1070 if new_failures is None: 1071 return 0 1072 if len(new_failures) > 0: 1073 msg = [f"Found {len(new_failures)} new failures:"] 1074 for failure in new_failures: 1075 msg.append(f"\t{failure.test_id}") 1076 output("\n".join(msg)) 1077 else: 1078 output("No new failures have been found") 1079 return len(new_failures) 1080 1081 1082if __name__ == "__main__": 1083 sys.exit(main(parse_args())) 1084