import logging
import os
import shutil
import sys
import json
import subprocess
import argparse
from typing import Dict, List
import re
from pathlib import Path
ES2PANDAPATH = "arkcompiler/runtime_core/static_core/out/bin/es2panda"
ARKLINKPATH = "arkcompiler/runtime_core/static_core/out/bin/ark_link"
CONFIGPATH = "arkcompiler/runtime_core/static_core/out/bin/arktsconfig.json"
TOOLSPATH = "test/testfwk/developer_test/libs/arkts1.2"
HYPIUMPATH = "test/testfwk/arkxtest/jsunit/src_static/"
def get_path_code_directory(after_dir):
"""
Concatenate absolute path based on current script location.
"""
current_path = os.path.abspath(__file__)
current_dir = os.path.dirname(current_path)
root_path = current_path.split("/build/ohos/testfwk")[0]
full_path = os.path.join(root_path, after_dir)
logging.debug(f"Resolved path: {after_dir} -> {full_path}")
return full_path
def build_tools(compile_filelist, output_dir, arktsconfig):
"""
Compile ETS files into ABC format.
"""
logging.info(f"Starting compilation, output directory: {output_dir}")
abs_es2panda_path = get_path_code_directory(ES2PANDAPATH)
output_dir = os.path.join(output_dir, "out")
os.makedirs(output_dir, exist_ok=True)
logging.info(f"Output directory created or exists: {output_dir}")
for ets_file in compile_filelist:
try:
file_name = os.path.basename(ets_file)
base_name = os.path.splitext(file_name)[0]
output_filepath = os.path.join(output_dir, f"{base_name}.abc")
if arktsconfig == CONFIGPATH:
command = [abs_es2panda_path, ets_file, f"--output={output_filepath}"]
else:
arktsconfig_path = get_path_code_directory(arktsconfig)
command = [abs_es2panda_path, ets_file, f"--output={output_filepath}",
f"--arktsconfig={arktsconfig_path}"]
logging.info(f"Executing compile command: {' '.join(command)}")
result = subprocess.run(
command,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
logging.info(f"Successfully compiled '{ets_file}' → '{output_filepath}'")
if result.stdout.strip():
logging.debug(f"Compile output: {result.stdout.strip()}")
except subprocess.CalledProcessError as e:
logging.error(f"Compilation failed for '{ets_file}'. Error:\n{e.stderr.strip()}")
raise
except Exception as e:
logging.critical(f"Unexpected error during compilation of '{ets_file}': {str(e)}")
raise
def build_ets_files(target_path, test_files, output_dir, arktsconfig):
"""
Compile test case ETS files.
"""
test_files_list = [f.strip() for f in test_files.split(',') if f.strip()]
test_files = [os.path.join(target_path, file) for file in test_files_list]
logging.info(f"Files to be compiled: {test_files}")
build_tools(test_files, output_dir, arktsconfig)
def collect_abc_files(output_dir, target_path, hypium_output_dir, sources):
"""
Collect all .abc files for linking.
"""
abs_out_path = os.path.join(output_dir, "out")
abc_files = []
if os.path.exists(abs_out_path):
out_files = [
os.path.join(abs_out_path, f)
for f in os.listdir(abs_out_path)
if f.endswith('.abc') and (not f.endswith('_source.abc'))
]
abc_files.extend(out_files)
logging.info(f"Collected {len(out_files)} .abc files from 'out' directory")
else:
logging.warning(f"Output directory does not exist: {abs_out_path}")
hypium_abc = os.path.join(hypium_output_dir, "hypium_tools.abc")
if os.path.exists(hypium_abc):
abc_files.append(hypium_abc)
logging.info(f"Added hypium tool file: {hypium_abc}")
else:
logging.error(f"Missing hypium tool file: {hypium_abc}. Please compile hypium first.")
raise FileNotFoundError(f"Missing hypium tool file: {hypium_abc}")
abc_files.extend(load_abc_from_src_json(target_path, sources))
logging.info(f"Total {len(abc_files)} .abc files collected for linking")
return abc_files
def load_abc_from_src_json(target_path, sources):
"""
Load extra .abc files from src.json.
"""
abc_files = []
sources_list = [f.strip() for f in sources.split(',') if f.strip()]
if not sources_list:
logging.warning("src.json filename is empty, skipping loading.")
return abc_files
source_file = sources_list[0]
src_json_path = os.path.join(target_path, source_file)
if not os.path.exists(src_json_path):
logging.info(f"Config file not found: {src_json_path}, skipping src.json loading.")
return abc_files
try:
with os.fdopen(os.open(src_json_path, os.O_RDONLY), 'r', encoding='utf-8') as f:
src_data = json.load(f)
logging.info(f"Successfully loaded src.json: {src_json_path}")
except json.JSONDecodeError as e:
logging.error(f"Invalid JSON format in {src_json_path}: {e}")
return abc_files
except Exception as e:
logging.error(f"Failed to read src.json {src_json_path}: {e}")
return abc_files
for path in src_data.get("src_path", []):
if path.endswith('.abc'):
abs_path = get_path_code_directory(path)
if os.path.isfile(abs_path):
abc_files.append(abs_path)
logging.debug(f"Added .abc file: {abs_path}")
else:
logging.warning(f"Skipped invalid or non-.abc path: {path}")
return abc_files
def link_abc_files(output_dir, hap_name, target_path, hypium_output_dir, sources):
"""
Link all .abc files into final test.abc.
"""
hypium_abc = os.path.join(hypium_output_dir, "hypium_tools.abc")
if not os.path.exists(hypium_abc):
logging.error(f"Missing hypium tool file: {hypium_abc}. Please compile hypium first.")
sys.exit(1)
abc_files = collect_abc_files(output_dir, target_path, hypium_output_dir, sources)
if not abc_files:
logging.error("No .abc files collected, cannot proceed with linking.")
sys.exit(1)
out_path = os.path.join(output_dir, f"{hap_name}.abc")
execute_abc_link(out_path, abc_files)
def execute_abc_link(out_path, abc_files):
""" execute abc_link Link all .abc files into final test.abc. """
abs_arklink_path = get_path_code_directory(ARKLINKPATH)
command = [abs_arklink_path, f"--output={out_path}", "--", *abc_files]
logging.info(f"Starting linking process, output: {out_path}")
logging.debug(f"Linking command: {' '.join(command)}")
try:
result = subprocess.run(
command,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
logging.info(f"Linking succeeded! Output: {out_path}")
if result.stdout.strip():
logging.debug(f"Linking output: {result.stdout.strip()}")
except subprocess.CalledProcessError as e:
logging.error(f"Linking failed. Details:\n{e.stderr.strip()}")
sys.exit(1)
def build_file_map() -> Dict[str, List[str]]:
file_map = {}
tools_path = get_path_code_directory(TOOLSPATH)
if not os.path.exists(tools_path) or not os.path.isdir(tools_path):
logging.warning(f"Tools path does not exist: {tools_path}")
return file_map
temp_list = [
"@ohos.app.ability.abilityDelegatorRegistry",
"AbilityDelegator",
"AbilityDelegatorArgs",
"ShellCmdResult",
"AbilityMonitor",
"AbilityStageMonitor"
]
for f in os.listdir(tools_path):
if not f.endswith('.ets'):
continue
module_name = os.path.splitext(f)[0]
if module_name not in temp_list:
continue
file_path = os.path.abspath(os.path.join(tools_path, f))
file_map[module_name] = [file_path]
abs_hypium_path = get_path_code_directory(HYPIUMPATH)
if not os.path.exists(abs_hypium_path) or not os.path.isdir(abs_hypium_path):
logging.warning(f"Hypium path does not exist: {abs_hypium_path}")
return file_map
for root, dirs, files in os.walk(abs_hypium_path):
if "testAbility" in dirs:
dirs.remove("testAbility")
if "testrunner" in dirs:
dirs.remove("testrunner")
for f in files:
if not f.endswith(".ets"):
continue
module_name = os.path.splitext(f)[0]
file_path = os.path.abspath(os.path.join(root, f))
file_map.setdefault(module_name, []).append(file_path)
return file_map
def scan_and_add_test_files(args: argparse.Namespace, config: dict) -> None:
"""Scan the target HAP directory for .ets files that are not yet in compileFiles,
and extend config['compileFiles'] with them."""
if not args.base_url.endswith("arkui-preprocessed"):
return
target_dir = os.path.join(args.base_url, args.hap_name)
target_path = Path(target_dir)
if not target_path.exists() or not target_path.is_dir():
return
existing_files = set(config.get("compileFiles", []))
tested_ets_files = set()
for file_path in target_path.rglob("*.ets"):
if file_path.name.startswith("."):
continue
abs_path = str(file_path.resolve())
if abs_path not in existing_files:
tested_ets_files.add(abs_path)
if tested_ets_files:
config["compileFiles"].extend(tested_ets_files)
def write_test_runner_path_file(args: argparse.Namespace, config: dict) -> None:
target_dir = os.path.join(args.output_dir, "out")
file_path = os.path.join(target_dir, f"{args.hap_name}_testRunnerPath.txt")
if os.path.exists(file_path):
os.remove(file_path)
matching_files = [
file for file in config["compileFiles"]
if file.endswith("OpenHarmonyTestRunner.ets")
]
if len(matching_files) == 1:
result = matching_files[0]
path_without_file = os.path.dirname(result)
relative_path = ''
if args.base_url in path_without_file:
relative_path = path_without_file.split(args.base_url, 1)[1]
if relative_path:
relative_path = relative_path.lstrip('/')
os.makedirs(target_dir, exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(relative_path)
def build_driver_config(args: argparse.Namespace) -> None:
paths = {}
if args.paths_keys and args.paths_values:
if len(args.paths_keys) != len(args.paths_values):
raise ValueError(
"paths_keys and paths_values must have the same length"
)
for key, value in zip(args.paths_keys, args.paths_values):
paths[key] = [os.path.abspath(value)]
file_map = build_file_map()
paths.update(file_map)
config = {
"plugins": {},
"packageName": args.package if args.package else "",
"buildType": "build",
"buildMode": "Release",
"moduleRootPath": args.base_url,
"sourceRoots": ["./"],
"paths": paths,
"loaderOutPath": args.dst_file,
"cachePath": args.cache_path,
"buildSdkPath": args.build_sdk_path,
"dependentModuleList": [],
"frameworkMode": True,
"useEmptyPackage": True,
"externalApiPaths": args.scan_path
}
plugins = {}
config["pathsKeys"] = []
config["pathsValues"] = []
config["compileFiles"] = []
if args.memo_plugin is not None:
plugins["memo_plugin"] = args.memo_plugin
if plugins:
config["plugins"] = plugins
if args.paths_keys:
config["pathsKeys"] = args.paths_keys
if args.paths_values:
config["pathsValues"] = args.paths_values
if args.files:
if not os.path.exists(args.files):
print(f"[IO ERROR] File not found: {args.files}", file=sys.stderr)
sys.exit()
fd = os.open(args.files, os.O_RDONLY)
with os.fdopen(fd, 'r') as f:
config["compileFiles"] = [line.strip() for line in f.readlines()]
config["pathsKeys"].extend(list(file_map.keys()))
all_paths = [path for sublist in file_map.values() for path in sublist]
config["pathsValues"].extend(all_paths)
scan_and_add_test_files(args, config)
write_test_runner_path_file(args, config)
os.makedirs(os.path.dirname(args.arktsconfig), exist_ok=True)
fd = os.open(args.arktsconfig, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o777)
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
def run_subprocess(cmd: List[str], timeout: str, env: Dict[str, str]) -> str:
"""Execute a subprocess with timeout and environment settings."""
try:
timeout_sec = int(timeout)
if timeout_sec <= 0:
raise ValueError("Timeout must be a positive integer")
process = subprocess.Popen(
cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
try:
stdout, stderr = process.communicate(timeout=timeout_sec)
except subprocess.TimeoutExpired:
process.kill()
stdout, stderr = process.communicate(timeout=timeout_sec)
raise subprocess.TimeoutExpired(
f"Command '{' '.join(cmd)}' timed out after {timeout_sec} seconds")
if process.returncode != 0:
raise subprocess.CalledProcessError(
f"Command '{' '.join(cmd)}' failed with return code {process.returncode}\n"
f"Standard Error:\n{stderr}\n"
f"Standard Output:\n{stdout}"
)
return stdout
except ValueError as e:
raise ValueError(f"Invalid timeout value: {e}")
except OSError as e:
raise OSError(f"OS error occurred: {e}")
except Exception as e:
raise RuntimeError(f"Unexpected error: {e}")
def execute_driver(
entry_path: str, build_config_path: str, env_path: str, node_path: str, timeout: str
) -> str:
"""Execute es2panda compilation process."""
cmd = ["node", entry_path, build_config_path]
env = os.environ.copy()
env["LD_LIBRARY_PATH"] = env_path
if node_path is not None:
env["PATH"] = f"{node_path}:{env['PATH']}"
return run_subprocess(cmd, timeout, env)
def replace_import_paths(file_path):
"""replace arkui-preprocessed test file import path."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
pattern = r'from\s*([\'"])((?:\.\./)+)arkui-preprocessed(/?[^\'"]*)([\'"])'
new_content = re.sub(
pattern,
lambda m: f'from {m.group(1)}../{m.group(3).lstrip("/")}{m.group(4)}',
content
)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
def ui_enable_link_hypium_abc(dst_file, hypium_output_dir) -> None:
if not dst_file:
raise ValueError("dst_file is empty or None")
if not hypium_output_dir:
raise ValueError("hypium_output_dir is not set")
hypium_abc = os.path.join(hypium_output_dir, "hypium_tools.abc")
if not os.path.isfile(hypium_abc):
logging.error(f"Missing hypium tool file: {hypium_abc}. Please compile hypium first.")
raise FileNotFoundError(f"Missing hypium tool file: {hypium_abc}")
abc_files = [dst_file, hypium_abc]
execute_abc_link(dst_file, abc_files)
def copy_ets_files_to_preprocessed_dir(args, target_dir):
"""
Copy ETS test files into the arkui-preprocessed directory
structure, and replace import paths in each file.
"""
test_files_list = [f.strip() for f in args.test_files.split(',') if f.strip()]
test_files = [os.path.join(args.target_path, file) for file in test_files_list]
for ets_file in test_files:
if not os.path.exists(ets_file):
logging.info(f"Skip non-existent file: {ets_file}")
continue
relative_filename = os.path.basename(ets_file)
target_file = os.path.join(target_dir, relative_filename)
os.makedirs(target_dir, exist_ok=True)
try:
shutil.copy2(ets_file, target_file)
logging.info(f"Copied: {ets_file} → {target_file}")
except Exception as e:
logging.error(f"Failed to copy {ets_file} to {target_file}: {e}")
continue
try:
replace_import_paths(target_file)
logging.info(f"Import paths replaced in: {target_file}")
except Exception as e:
logging.error(f"Failed to replace import paths in {target_file}: {e}")
def main():
parser = argparse.ArgumentParser(description="Compile ETS test cases and link into .abc")
parser.add_argument("--target_path", required=True, help="Root path of test cases")
parser.add_argument("--test_files", required=True, help="Name of src.json file")
parser.add_argument("--output_dir", required=True, help="build output directory")
parser.add_argument("--hap_name", required=True, help="HAP name")
parser.add_argument("--hypium_output_dir", required=True, help="Output directory for hypium build")
parser.add_argument("--sources", required=True, help="List of ETS source files (comma-separated)")
parser.add_argument("--subsystem_name", required=True, help="subsystem name")
parser.add_argument("--part_name", required=True, help="part name")
parser.add_argument("--arktsconfig", required=True, help="arktsconfig file")
parser.add_argument("--scan_path", nargs="+", required=False, help="List of directories to scan for target files")
parser.add_argument("--ui_enable", default=True, required=False,
help="Flag indicating if the compile supports ui syntax")
parser.add_argument("--base_url", required=False, help="Base URL for the project")
parser.add_argument("--build_sdk_path", default=None, required=False,
help="Path for sdk. Required if 'ui-enable' is True")
parser.add_argument("--memo_plugin", default=None, required=False,
help="Path for memo plugin. Required if 'ui-enable' is True")
parser.add_argument("--entry_path", default=None, required=False,
help="Path for driver entry. Required if 'ui-enable' is True")
parser.add_argument("--env_path", type=str, required=False, help="Value for LD_LIBRARY_PATH environment variable")
parser.add_argument("--node_path", default=None, required=False, help="Path for node")
parser.add_argument("--files", required=False, help="File containing a list of specific files to compile")
parser.add_argument("--package", required=False, help="Package name for the project")
parser.add_argument("--paths_keys", nargs="+", required=False, help="List of keys for custom paths")
parser.add_argument("--paths_values", nargs="+", required=False,
help="List of values for custom paths. Each value corresponds to a key in --paths-keys")
parser.add_argument("--cache_path", type=str, default=None, help="Path to cache directory")
parser.add_argument("--dst_file", type=str, required=True, help="Path for final dst file")
parser.add_argument("--timeout_limit", type=str, default="12000",
help="Process timeout in seconds (default: 12000)")
args = parser.parse_args()
target_dir = None
try:
if os.path.exists(args.cache_path):
shutil.rmtree(args.cache_path)
os.makedirs(args.cache_path, exist_ok=True)
if args.ui_enable == "True":
if args.base_url.endswith("arkui-preprocessed"):
target_dir = os.path.join(args.base_url, args.hap_name)
copy_ets_files_to_preprocessed_dir(args, target_dir)
build_driver_config(args)
execute_driver(args.entry_path, args.arktsconfig, args.env_path, args.node_path, args.timeout_limit)
ui_enable_link_hypium_abc(args.dst_file, args.hypium_output_dir)
else:
build_ets_files(args.target_path, args.test_files, args.output_dir, args.arktsconfig)
link_abc_files(args.output_dir, args.hap_name, args.target_path, args.hypium_output_dir, args.sources)
source_abc_dir = os.path.join(args.output_dir, "out", f"{args.hap_name}_source.abc")
abc_files = load_abc_from_src_json(args.target_path, args.sources)
execute_abc_link(source_abc_dir, abc_files)
except Exception as e:
logging.critical(f"Build process failed unexpectedly: {str(e)}")
sys.exit(1)
finally:
if target_dir and os.path.exists(target_dir) and os.path.isdir(target_dir):
shutil.rmtree(target_dir)
if __name__ == '__main__':
sys.exit(main())