1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright (c) 2022 Huawei Device Co., Ltd.
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import os
17import json
18import re
19import argparse
20from prettytable import PrettyTable, ALL
21
22
23def get_args():
24    parser = argparse.ArgumentParser(add_help=True)
25    parser.add_argument(
26        "-p",
27        "--project_path",
28        default=r"./",
29        type=str,
30        help="root path of project. default: ./",
31    )
32    parser.add_argument(
33        "-t",
34        "--check_target",
35        type=str,
36        choices=["component_codec", "component_sdk", "sdk_codec"],
37        required=True,
38        help="the target to be compared",
39    )
40    parser.add_argument(
41        "-b",
42        "--bundles",
43        nargs="*",
44        type=str,
45        help="option take effect only when the check_target is component_codec. allow multiple json file. "
46             "default: all bundle.json file",
47    )
48    args = parser.parse_args()
49    return args
50
51
52def list_to_multiline(target_list: list):
53    return str(target_list).lstrip("[").rstrip("]").replace(", ", "\n")
54
55
56def convert_set_to_sorted_list(target_set) -> list:
57    return sorted(list(target_set))
58
59
60def add_dict_as_table_row(f_table: PrettyTable, d_dict: dict, out_converter=list_to_multiline) -> None:
61    s_keys = convert_set_to_sorted_list(d_dict.keys())
62    for i, k in enumerate(s_keys):
63        f_table.add_row([i + 1, k, out_converter(convert_set_to_sorted_list(d_dict.get(k)))])
64
65
66def bundle_syscap_post_handler(syscap: str) -> str:
67    return syscap.split('=')[0].strip()
68
69
70def read_value_from_json(filepath: str,
71                         key_hierarchy: tuple,
72                         result_dict: dict,
73                         post_handler=None
74                         ) -> None:
75    if os.path.exists(filepath) is False:
76        print('error: file "{}" not exist.'.format(filepath))
77        return
78    if not os.path.isfile(filepath):
79        print('error: "{}" is not a file.')
80        return
81    with open(filepath, 'r', encoding='utf-8') as f:
82        data = json.load(f)
83        for key in key_hierarchy:
84            try:
85                data = data[key]
86            except KeyError:
87                return
88            finally:
89                pass
90    data = [post_handler(x) for x in data if len(x) != 0 and not x.isspace()]
91    if len(data) != 0:
92        result_dict[filepath] = data
93
94
95def collect_syscap_from_codec(filepath: str, pattern: str = r'{"(.*)"') -> tuple:
96    """
97    从syscap_define.h收集syscap
98    :param filepath: 文件的路径
99    :param pattern: 匹配syscap的规则
100    :return: syscap_define.h中声明的syscap集合
101    """
102    array_syscap_set = set()
103    array_syscap_dict = dict()
104    ptrn = re.compile(pattern)
105    with open(filepath, "r") as f:
106        content = f.read()
107        array_syscap_set.update(re.findall(ptrn, content))
108    array_syscap_dict[filepath] = list()
109    for v in array_syscap_set:
110        array_syscap_dict.get(filepath).append(v)
111    return array_syscap_set, array_syscap_dict
112
113
114def collect_syscap_from_component(project_path: str,
115                                  black_dirs: tuple,
116                                  key_heirarchy: tuple,
117                                  bundles: list = None) -> tuple:
118    """
119    从部件的bundle.json中收集syscap
120    :param project_path: 项目根路径
121    :param black_dirs: 根路径下的一级目录的黑名单,不扫描这些bundle.json下面的内容
122    :param key_heirarchy: bundle.json中syscap所在的key的元组
123    :param bundles: 指定的bundle.json
124    :return: syscap_set,文件名和其syscap list组成的dict
125    """
126    result_dict = dict()
127    if bundles is None:
128        search_dir_list = [
129            os.path.join(project_path, x)
130            for x in os.listdir(project_path)
131            if os.path.isdir(os.path.join(project_path, x)) and x not in black_dirs
132        ]
133        for folder in search_dir_list:
134            output = os.popen("find {} -name bundle.json".format(folder))
135            for line in output:
136                line = line.strip()
137                read_value_from_json(line, key_heirarchy, result_dict, post_handler=bundle_syscap_post_handler)
138            output.close()
139    else:
140        for bundle in bundles:
141            read_value_from_json(bundle, key_heirarchy, result_dict, post_handler=bundle_syscap_post_handler)
142    result_set = set()
143    for v in result_dict.values():
144        result_set.update(v)
145    return result_set, result_dict
146
147
148def sdk_syscap_post_handler(syscap: str) -> str:
149    return syscap.strip().lstrip("*").lstrip().lstrip("*@syscap").strip()
150
151
152def collect_syscap_from_sdk(ts_path: str,
153                            pattern: str = r"\* *@syscap +((?i)SystemCapability\..*)",
154                            post_handler=None
155                            ) -> tuple:
156    """
157    从sdk下的*.d.ts中收集syscap
158    :param ts_path: ts文件的所在路径
159    :param pattern: 匹配syscap的pattern
160    :param post_handler: 对匹配到的syscap进行后置处理
161    :return: syscap_set,和由文件名、syscap list组成的dict
162    """
163    ts_list = list()
164    for item in os.listdir(ts_path):
165        file_path = os.path.join(ts_path, item)
166        if file_path.endswith(".d.ts") and os.path.isfile(file_path):
167            ts_list.append(file_path)
168    syscap_dict = dict()
169    ptrn = re.compile(pattern)
170    for ts in ts_list:
171        with open(ts, 'r') as f:
172            content = f.read()
173            sub_syscap_list = re.findall(ptrn, content)
174            sub_syscap_set = set()
175            for syscap in sub_syscap_list:
176                sub_syscap_set.add(post_handler(syscap))
177            syscap_dict[ts] = sub_syscap_set
178    syscap_set = set()
179    for v in syscap_dict.values():
180        syscap_set.update(v)
181    return syscap_set, syscap_dict
182
183
184def find_files_containes_value(value_set: set, file_values_dict: dict) -> dict:
185    """
186    查看包含指定值的文件
187    :param value_set: 指定值
188    :param file_values_dict: 文件和值组成的dict
189    :return: 值和文件组成的dict
190    """
191    value_files_dict = dict()
192    for v in value_set:
193        filename_set = set()
194        for file in file_values_dict.keys():
195            if v in file_values_dict[file]:
196                filename_set.add(file)
197        if 0 != len(filename_set):
198            value_files_dict[v] = filename_set
199    return value_files_dict
200
201
202def mutual_diff(a_set: set, b_set: set) -> tuple:
203    return a_set.difference(b_set), b_set.difference(a_set)
204
205
206def print_inconsistent(diff_set: set, a_name: str, b_name: str, table: PrettyTable,
207                       value_file_dict: dict) -> None:
208    table.clear()
209    if len(diff_set) != 0:
210        table.field_names = ["index", "SysCap only in {}".format(a_name), "Files"]
211        add_dict_as_table_row(table, value_file_dict)
212    elif len(diff_set) == 0:
213        table.field_names = ["All SysCap in {} have been Covered by {}".format(a_name, b_name)]
214    print(table)
215    print()
216
217
218def print_consistent(a_diff_b_set: set, b_diff_a_set: set, table: PrettyTable, a_name: str, b_name: str) -> bool:
219    if len(a_diff_b_set) == 0 and len(b_diff_a_set) == 0:
220        table.field_names = ["{} and {} are Consistent".format(a_name, b_name)]
221        print(table)
222        print()
223        return True
224    return False
225
226
227def print_check_result(a_diff_b_set: set,
228                       b_diff_a_set: set,
229                       a_name: str,
230                       b_name: str,
231                       a_value_file_dict: dict,
232                       b_value_file_dict: dict,
233                       ) -> None:
234    f_table = PrettyTable()
235    f_table.hrules = ALL
236    consistent_flag = print_consistent(a_diff_b_set, b_diff_a_set, f_table, a_name, b_name)
237    if not consistent_flag:
238        print_inconsistent(a_diff_b_set, a_name, b_name, f_table, a_value_file_dict)
239        print_inconsistent(b_diff_a_set, b_name, a_name, f_table, b_value_file_dict)
240
241
242def check_component_and_codec(project_path, syscap_define_path: str, component_black_dirs: tuple,
243                              bundle_key_heirarchy: tuple, bundles=None):
244    component_syscap_set, component_syscap_dict = collect_syscap_from_component(
245        project_path, black_dirs=component_black_dirs, key_heirarchy=bundle_key_heirarchy, bundles=bundles
246    )
247    array_syscap_set, array_syscap_dict = collect_syscap_from_codec(syscap_define_path)
248    component_diff_array, array_diff_component = mutual_diff(component_syscap_set, array_syscap_set)
249    value_component_dict = find_files_containes_value(
250        component_diff_array, component_syscap_dict
251    )
252    value_h_dict = find_files_containes_value(array_diff_component, array_syscap_dict)
253    print_check_result(component_diff_array, array_diff_component, a_name="Component", b_name="Codec",
254                       a_value_file_dict=value_component_dict, b_value_file_dict=value_h_dict)
255
256
257def check_component_and_sdk(project_path, component_black_dirs: tuple, component_key_heirarchy: tuple, sdk_path: str):
258    component_syscap_set, component_syscap_dict = collect_syscap_from_component(
259        project_path, black_dirs=component_black_dirs, key_heirarchy=component_key_heirarchy
260    )
261    ts_syscap_set, ts_syscap_dict = collect_syscap_from_sdk(ts_path=sdk_path,
262                                                            post_handler=sdk_syscap_post_handler)
263    component_diff_ts, ts_diff_component = mutual_diff(component_syscap_set, ts_syscap_set)
264    value_ts_dict = find_files_containes_value(ts_diff_component, ts_syscap_dict)
265    value_component_dict = find_files_containes_value(
266        component_diff_ts, component_syscap_dict
267    )
268    print_check_result(component_diff_ts, ts_diff_component, a_name="Component", b_name="SDK",
269                       a_value_file_dict=value_component_dict, b_value_file_dict=value_ts_dict)
270
271
272def check_sdk_and_codec(syscap_define_path: str, sdk_path: str) -> None:
273    ts_syscap_set, ts_syscap_dict = collect_syscap_from_sdk(ts_path=sdk_path,
274                                                            post_handler=sdk_syscap_post_handler)
275    array_syscap_set, array_syscap_dict = collect_syscap_from_codec(syscap_define_path)
276    ts_diff_array, array_diff_ts = mutual_diff(ts_syscap_set, array_syscap_set)
277    value_ts_dict = find_files_containes_value(ts_diff_array, ts_syscap_dict)
278    value_h_dict = find_files_containes_value(array_diff_ts, array_syscap_dict)
279    print_check_result(ts_diff_array, array_diff_ts, a_name="SDK", b_name="Codec", a_value_file_dict=value_ts_dict,
280                       b_value_file_dict=value_h_dict)
281
282
283def main():
284    args = get_args()
285    project_path = args.project_path
286    check_target = args.check_target
287    bundles = args.bundles
288    syscap_define_path = os.path.join(project_path, "developtools", "syscap_codec", "include", "syscap_define.h")
289    ts_path = os.path.join(project_path, "interface", "sdk-js", "api")
290    component_black_dirs = ("out",)
291    bundle_syscap_heirarchy = ("component", "syscap")
292    if (bundles is not None or (bundles is not None and len(bundles) != 0)) and check_target != "component_codec":
293        print("error: --bundles could only be used with -t component_codec")
294        return
295    if "component_sdk" == check_target:
296        check_component_and_sdk(project_path, component_black_dirs, bundle_syscap_heirarchy, sdk_path=ts_path)
297    elif "sdk_codec" == check_target:
298        check_sdk_and_codec(syscap_define_path=syscap_define_path, sdk_path=ts_path)
299    elif "component_codec" == check_target:
300        if bundles is not None and len(bundles) == 0:
301            print(r"error: '--bundles' parameter is specified, but has no value")
302        else:
303            check_component_and_codec(project_path, syscap_define_path, component_black_dirs=component_black_dirs,
304                                      bundle_key_heirarchy=bundle_syscap_heirarchy, bundles=bundles)
305
306
307if __name__ == "__main__":
308    main()
309
310