• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2024-2025 Huawei Device Co., Ltd.
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import re
18import os
19import argparse
20import logging
21
22script_dir = os.path.dirname(os.path.realpath(__file__))
23
24logging.basicConfig(format='%(message)s', level=logging.DEBUG)
25
26
27def get_args():
28    parser = argparse.ArgumentParser(description='Abckit status script')
29    parser.add_argument('--print-implemented',
30                        action='store_true',
31                        default=False,
32                        help=f'Print list of implemented API and exit')
33    parser.add_argument('--cppapi',
34                        action='store_true',
35                        default=False,
36                        help=f'Fill table with tests for cpp api')
37    parser.add_argument('--capi',
38                        action='store_true',
39                        default=False,
40                        help=f'Fill table with tests for c api')
41    return parser.parse_args()
42
43
44args = get_args()
45
46API_PATTERN = r'^[\w,\d,_, ,*]+\(\*([\w,\d,_]+)\)\(.*'
47CPP_API_PATTERN = r'.+ \**\&*[\w\d]+\('
48PUBLIC_API_PATTERN = r'^(?!explicit)(.+) (?!~)[\&\*]*(?!operator)(.+)\(.*'
49domain_patterns = [
50    r'struct Abckit\S*Api\s\{', r'struct Abckit\S*ApiStatic\s\{',
51    r'struct Abckit\S*ApiDynamic\s\{'
52]
53specs = ['public:', 'private:', 'protected:']
54
55libabckit_dir = script_dir.rsplit('/', 1)[0]
56abckit_tests = os.path.join(libabckit_dir, 'tests')
57
58c_sources = {'include/c'}
59
60c_tests = {
61    'tests/canary', 'tests/helpers', 'tests/internal', 'tests/null_args_tests',
62    'tests/sanitizers', 'tests/scenarios', 'tests/scenarios_c_api_clean',
63    'tests/stress', 'tests/ut tests/wrong_ctx_tests', 'tests/wrong_mode_tests'
64}
65
66cpp_sources = {'include/cpp'}
67
68cpp_tests = {
69    'tests/cpp/tests',
70    'tests/mock',
71    'tests/regression',
72    'tests/scenarios_cpp_api_clean'
73}
74
75TS = 'TS'
76JS = 'JS'
77ARKTS1 = 'ArkTS1'
78ARKTS2 = 'ArkTS2'
79NO_ABC = 'NoABC'
80
81
82def check_test_anno_line(line):
83    mul_lines = False
84    anno_line = False
85    if re.fullmatch(r'^\/\/ Test: test-kind=.*\n', line):
86        anno_line = True
87        mul_lines = line.strip()[-1] == ','
88    return {'is_annotation_line': anno_line, 'mul_annotation_lines': mul_lines}
89
90
91def get_full_annotation(it, annotation_start):
92    annotation = annotation_start.strip()
93    next_anno_line = True
94    while next_anno_line:
95        new_line = next(it)
96        annotation += new_line.replace('//', '').strip()
97        if not re.fullmatch(r'^\/\/.*,$', new_line):
98            next_anno_line = False
99    return annotation
100
101
102class Test:
103
104    def __init__(self, s):
105        err = f'Wrong test annotation: "{s}"'
106
107        self.kind = ''
108        self.abc_kind = ''
109        self.api = ''
110        self.category = ''
111        self.extension = ''
112
113        check('// Test:' in s, err)
114        s = s.replace('// Test:', '')
115        entries = [x.strip() for x in s.split(',') if x]
116        for entry in entries:
117            key, value = entry.strip().split('=')
118            if key == 'test-kind':
119                check(value in ['api', 'scenario', 'internal', 'mock', 'regression'], err)
120                self.kind = value
121            elif key == 'abc-kind':
122                check(value in [ARKTS1, ARKTS2, JS, TS, NO_ABC], err)
123                self.abc_kind = value
124            elif key == 'api':
125                self.api = value
126            elif key == 'category':
127                possible_values = [
128                    'positive', 'negative-mode', 'negative-nullptr',
129                    'negative-file', 'internal', 'negative'
130                ]
131                check(value in possible_values, err)
132                self.category = value
133            elif key == 'extension':
134                self.extension = value
135            else:
136                check(False, f'Wrong key: {key}')
137
138        check(self.kind, err)
139        check(self.extension, err)
140        check(self.abc_kind, err)
141        check(self.category, err)
142        if self.kind == 'api' or self.kind == 'mock':
143            check(self.api, err)
144
145
146def is_first_test_line(line):
147    if re.fullmatch(r'TEST_F\(.*,.*\n', line):
148        return True
149    return False
150
151
152def get_test_from_annotation(api, annotation):
153    test = Test(annotation)
154    if ('api=' in annotation
155            or 'mock=' in annotation) and test.api != 'ApiImpl::GetLastError':
156        check(test.api in api, f'No such API: {test.api}')
157    return test
158
159
160def collect_tests_from_path(path, api):
161    with open(path, 'r') as f:
162        lines = f.readlines()
163        it = iter(lines)
164        tests = []
165
166        ano_count, test_count = 0, 0
167        annotation = ''
168        line = '\n'
169        while True:
170            line = next(it, None)
171            if (line is None):
172                break
173
174            if is_first_test_line(line):
175                test_count += 1
176                check(test_count <= ano_count,
177                      f'Test has no annotation:\n{path}\n{line}\n')
178                continue
179
180            info = check_test_anno_line(line)
181            if not info.get('is_annotation_line'):
182                annotation = ''
183                continue
184            ano_count += 1
185            annotation += line.strip()
186
187            if info.get('mul_annotation_lines'):
188                annotation = get_full_annotation(it, line)
189            else:
190                annotation = line.strip()
191
192            if annotation:
193                tests.append(get_test_from_annotation(api, annotation))
194
195    check(ano_count == test_count, f'Annotation without test:\n{path}\n')
196    return tests
197
198
199def collect_tests(path, api):
200    tests = []
201    for dirpath, _, filenames in os.walk(f'{libabckit_dir}/{path}'):
202        for name in filenames:
203            if name.endswith('.cpp'):
204                tests += collect_tests_from_path(os.path.join(dirpath, name),
205                                                 api)
206    return tests
207
208
209def get_tests_statistics(api, tests):
210    for test in tests:
211        if (test.kind != 'api' and test.kind
212                != 'mock') or test.api == 'ApiImpl::GetLastError':
213            continue
214        if test.abc_kind == ARKTS1:
215            api[test.api].arkts1_tests += 1
216        if test.abc_kind == ARKTS2:
217            api[test.api].arkts2_tests += 1
218        elif test.abc_kind == JS:
219            api[test.api].js_tests += 1
220        elif test.abc_kind == TS:
221            api[test.api].ts_tests += 1
222        elif test.abc_kind == NO_ABC:
223            api[test.api].no_abc_tests += 1
224
225        if test.category == 'positive':
226            api[test.api].positive_tests += 1
227            if test.abc_kind == TS or test.abc_kind == JS or test.abc_kind == ARKTS1:
228                api[test.api].dynamic_positive_tests += 1
229            if test.abc_kind == ARKTS2:
230                api[test.api].static_positive_tests += 1
231        elif test.category == 'negative':
232            api[test.api].negative_tests += 1
233        elif test.category == 'negative-nullptr':
234            api[test.api].negative_nullptr_tests += 1
235        elif test.category == 'negative-file':
236            api[test.api].negative_ctx_tests += 1
237        elif test.category == 'negative-mode':
238            api[test.api].negative_mode_tests += 1
239        else:
240            api[test.api].other_tests += 1
241    return api
242
243
244def check(cond, msg=''):
245    if not cond:
246        raise Exception(msg)
247
248
249class API:
250
251    def __init__(self, name, domain, sig='', extension=''):
252        self.name = name
253        self.domain = domain
254        self.sig = sig
255        self.extension = extension
256        self.dynamic_positive_tests = 0
257        self.static_positive_tests = 0
258        self.arkts1_tests = 0
259        self.arkts2_tests = 0
260        self.js_tests = 0
261        self.ts_tests = 0
262        self.no_abc_tests = 0
263        self.positive_tests = 0
264        self.negative_tests = 0
265        self.negative_nullptr_tests = 0
266        self.negative_ctx_tests = 0
267        self.negative_mode_tests = 0
268        self.other_tests = 0
269
270
271def next_line(it):
272    return next(it).strip()
273
274
275def determine_doclet(it, line):
276    if not line or line.strip() != "/**":
277        return ''
278
279    line = next_line(it)
280    funс_declaration = ''
281
282    while not funс_declaration:
283        if line and line == '*/':
284            l = next_line(it)
285            if 'template <' in l:
286                return next_line(it)
287            return l
288        line = next_line(it)
289
290
291def check_oop(sign):
292    if ' = default' in sign or ' = delete' in sign:
293        if any([x in sign for x in [' = default', ' = delete', ' : ', 'operator']]):
294            return False
295    return True
296
297
298def cut_cpp_sign(sign):
299    name_with_return = re.search(r'[^\s] [^\(]+', sign.strip()).group(0)
300    name = re.search(r' [^\s]([\w\d]+)$', name_with_return).group(0).strip()
301    return re.search(r'[^\**\&*]([\w\d]+)$', name).group(0).strip()
302
303
304def collect_api(path, extension):
305    apis = {}
306    domain = ''
307    with open(path, 'r') as f:
308        accsess = 'public:'
309        lines = f.readlines()
310    it = iter(lines)
311
312    while True:
313        if (line := next(it, None)) is None:
314            break
315        line = line.strip()
316
317        if line in specs:
318            accsess = line
319
320        signature = determine_doclet(it, line)
321        api_name = ''
322        if not signature:
323            continue
324
325        if re.search(r'struct Abckit(.*)Api(.*)\s\{', signature):
326            domain = re.search(r'struct Abckit(.*)\s\{',
327                               signature.strip()).group(1)
328            domain = f'{domain}Impl'
329
330        elif re.search(r'struct (.+) Abckit(.*)Api(.*)\s\{', signature):
331            domain = re.search(r'struct (.+) Abckit(.*)\s\{', signature.strip()).group(2)
332            domain = f'{domain}Impl'
333
334        elif re.match(r'class .+ {', signature):
335            if match := re.search(r'class (.+) .+ : .+ {', signature,
336                                  re.IGNORECASE):
337                domain = match.group(1)
338            elif match := re.search(r'class (.+) : .+ {', signature,
339                                    re.IGNORECASE):
340                domain = match.group(1)
341            elif match := re.search(r'class (.+) (.+){', signature,
342                                    re.IGNORECASE):
343                domain = match.group(1)
344            elif match := re.search(r'class (.+) {', signature, re.IGNORECASE):
345                domain = match.group(1)
346
347        elif (re.match(PUBLIC_API_PATTERN, signature)) and accsess == "public:":
348            if re.match(API_PATTERN, signature.strip()) and check_oop(signature):
349                api_name = re.search(r'^[\w,\d,_, ,*]+\(\*([\w,\d,_]+)\)\(.*', signature.strip()).group(1)
350            elif re.match(CPP_API_PATTERN, signature.strip()) and check_oop(signature):
351                api_name = cut_cpp_sign(signature)
352            else:
353                continue
354            apis[f'{domain}::{api_name}'] = API(api_name, domain, signature, extension)
355    return apis
356
357
358def collect_api_from_sources(sources, extension):
359    apis = {}
360    for src in sources:
361        for (dirpath, _, filenames) in os.walk(f'{libabckit_dir}/{src}'):
362            headers = list(
363                filter(lambda f: re.fullmatch(r'(.+)(?<!_impl).h$', f),
364                       filenames))
365            for file in headers:
366                apis = dict(apis.items() | collect_api(
367                    os.path.join(dirpath, file), extension).items())
368    return apis
369
370
371def api_test_category(tests, name):
372    return len(
373        list(
374            filter(lambda t: t.kind == 'api' and t.category == name,
375                   tests)))
376
377
378def api_lang(tests, name):
379    return len(
380        list(
381            filter(lambda t: t.kind == 'api' and t.abc_kind == name,
382                   tests)))
383
384
385def scenario_lang(tests, name):
386    return len(
387        list(
388            filter(lambda t: t.kind == 'scenario' and t.abc_kind == name,
389                   tests)))
390
391
392def print_cppapi_stat(tests_pathes, api, expected=0):
393    tests = []
394    for p in tests_pathes:
395        tests += list(
396            filter(lambda t: t.extension == 'cpp', collect_tests(p, api)))
397    api = get_tests_statistics(api, tests)
398
399    def api_tests_kind(kind):
400        return list(filter(lambda t: t.kind == kind, tests))
401
402    def get_element(array, i):
403        if len(array) <= i:
404            return '-'
405        return array[i]
406
407    mock_tests_apis = list(dict.fromkeys(t.api
408                                         for t in api_tests_kind("mock")))
409    api_tests_apis = list(dict.fromkeys(t.api for t in api_tests_kind("api")))
410    internal_tests = list(filter(lambda t: t.kind == 'internal', tests))
411    regression_tests = list(filter(lambda t: t.kind == 'regression', tests))
412
413    csv = ''
414    for i in range(max(len(mock_tests_apis), len(api_tests_apis))):
415        csv += f'{get_element(mock_tests_apis, i)},{get_element(api_tests_apis, i)}\n'
416    with os.fdopen(os.open(os.path.join(libabckit_dir, 'scripts/abckit_cppapi_status.csv'),
417                           os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o755), 'w+') as f:
418        f.write(('mock_tests_apis,api_tests_apis\n'))
419        f.write(csv)
420
421    logging.debug(f'>>> CPP EXTENSION <<<\n')
422
423    logging.debug(
424        'Total API:                                              %s/%s',
425        len(api), expected)
426    logging.debug('')
427    logging.debug('Total Tests:                                            %s',
428                  len(tests))
429    logging.debug('')
430    logging.debug('Total API\'S with api tests:                             %s/%s',
431                  len(api_tests_apis), expected)
432    logging.debug('Total API\'S with mock tests:                            %s/%s',
433                  len(mock_tests_apis), expected)
434    logging.debug('Total internal tests:                                     %s',
435                  len(internal_tests))
436    logging.debug('Total regression tests:                                   %s',
437                  len(regression_tests))
438    logging.debug('Total scenario tests:                                     %s',
439                  len(list(filter(lambda t: t.kind == 'scenario', tests))))
440    logging.debug(
441        'ArkTS1/ArkTS2/JS/TS scenario tests:                     %s/%s/%s/%s',
442        scenario_lang(tests, ARKTS1), scenario_lang(tests, ARKTS2), scenario_lang(tests, JS),
443        scenario_lang(tests, TS))
444    logging.debug(f'\n------------------------------------------------------------------\n')
445
446
447def print_capi_stat(tests_pathes, api):
448    csv = ''
449    tests = []
450    for p in tests_pathes:
451        tests += list(
452            filter(lambda t: t.extension == 'c', collect_tests(p, api)))
453    api = get_tests_statistics(api, tests)
454    for name in api:
455        csv += (
456            f'{name},{api[name].extension},{api[name].dynamic_positive_tests},{api[name].static_positive_tests},'
457            f'{api[name].arkts1_tests},{api[name].arkts2_tests},{api[name].js_tests},{api[name].ts_tests},'
458            f'{api[name].no_abc_tests},{api[name].positive_tests},{api[name].negative_tests},'
459            f'{api[name].negative_nullptr_tests},{api[name].negative_ctx_tests},{api[name].other_tests}\n'
460        )
461
462    with os.fdopen(os.open(os.path.join(libabckit_dir, 'scripts/abckit_capi_status.csv'),
463                           os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o755), 'w+') as f:
464        f.write(('api,extension,dynamic_positive_tests,static_positive_tests,'
465                 'arkts1_tests,arkts2_tests,js_tests,ts_tests,no_abc_tests,positive_tests,'
466                 'negative_tests,negative_nullptr_tests,negative_ctx_tests,other_tests\n'))
467        f.write(csv)
468
469    log_capi_stat(api, tests)
470
471
472def log_capi_stat(api, tests):
473
474    logging.debug('>>> C <<<\n')
475
476    logging.debug('Total API:                                              %s',
477                  len(api))
478    logging.debug('')
479    logging.debug('Total Tests:                                            %s',
480                  len(tests))
481    logging.debug('')
482    logging.debug('Total API tests:                                        %s',
483                  len(list(filter(lambda t: t.kind == 'api', tests))))
484    logging.debug(
485        'Positive/Negative/NullArg/WrongCtx/WrongMode API tests: %s/%s/%s/%s/%s',
486        api_test_category(tests, 'positive'), api_test_category(tests, 'negative'),
487        api_test_category(tests, 'negative-nullptr'),
488        api_test_category(tests, 'negative-file'), api_test_category(tests, 'negative-mode'))
489    logging.debug(
490        'ArkTS1/ArkTS2/JS/TS/NoABC API tests:                    %s/%s/%s/%s/%s',
491        api_lang(tests, ARKTS1), api_lang(tests, ARKTS2), api_lang(tests, JS), api_lang(tests, TS),
492        api_lang(tests, NO_ABC))
493    logging.debug('')
494    logging.debug('Total scenario tests:                                   %s',
495                  len(list(filter(lambda t: t.kind == 'scenario', tests))))
496    logging.debug(
497        'ArkTS1/ArkTS2/JS/TS scenario tests:                     %s/%s/%s/%s',
498        scenario_lang(tests, ARKTS1), scenario_lang(tests, ARKTS2), scenario_lang(tests, JS),
499        scenario_lang(tests, TS))
500    logging.debug('')
501    logging.debug('Internal tests:                                         %s',
502                  len(list(filter(lambda t: t.kind == 'internal', tests))))
503    logging.debug(
504        f'\n------------------------------------------------------------------\n'
505    )
506
507
508cpp_api, c_api = {}, {}
509if args.cppapi:
510    cpp_api = dict(cpp_api.items()
511                   | collect_api_from_sources(cpp_sources, 'cpp').items())
512c_api = dict(c_api.items() | collect_api_from_sources(c_sources, 'c').items())
513
514if args.print_implemented:
515    if args.cppapi:
516        logging.debug('\n'.join(cpp_api))
517    if args.capi:
518        logging.debug('\n'.join(c_api))
519else:
520    if args.cppapi:
521        print_cppapi_stat(cpp_tests, cpp_api, len(c_api))
522    if args.capi:
523        print_capi_stat(c_tests, c_api)
524