• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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