• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2024 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', 'src/include_v2/c/isa'}
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.match(r'class .+ {', signature):
331            if match := re.search(r'class (.+) .+ : .+ {', signature,
332                                  re.IGNORECASE):
333                domain = match.group(1)
334            elif match := re.search(r'class (.+) : .+ {', signature,
335                                    re.IGNORECASE):
336                domain = match.group(1)
337            elif match := re.search(r'class (.+) (.+){', signature,
338                                    re.IGNORECASE):
339                domain = match.group(1)
340            elif match := re.search(r'class (.+) {', signature, re.IGNORECASE):
341                domain = match.group(1)
342
343        elif (re.match(PUBLIC_API_PATTERN, signature)) and accsess == "public:":
344            if re.match(API_PATTERN, signature.strip()) and check_oop(signature):
345                api_name = re.search(r'^[\w,\d,_, ,*]+\(\*([\w,\d,_]+)\)\(.*', signature.strip()).group(1)
346            elif re.match(CPP_API_PATTERN, signature.strip()) and check_oop(signature):
347                api_name = cut_cpp_sign(signature)
348            else:
349                continue
350            apis[f'{domain}::{api_name}'] = API(api_name, domain, signature, extension)
351        else:
352            continue
353    return apis
354
355
356def collect_api_from_sources(sources, extension):
357    apis = {}
358    for src in sources:
359        for (dirpath, _, filenames) in os.walk(f'{libabckit_dir}/{src}'):
360            headers = list(
361                filter(lambda f: re.fullmatch(r'(.+)(?<!_impl).h$', f),
362                       filenames))
363            for file in headers:
364                apis = dict(apis.items() | collect_api(
365                    os.path.join(dirpath, file), extension).items())
366    return apis
367
368
369def api_test_category(tests, name):
370    return len(
371        list(
372            filter(lambda t: t.kind == 'api' and t.category == name,
373                   tests)))
374
375
376def api_lang(tests, name):
377    return len(
378        list(
379            filter(lambda t: t.kind == 'api' and t.abc_kind == name,
380                   tests)))
381
382
383def scenario_lang(tests, name):
384    return len(
385        list(
386            filter(lambda t: t.kind == 'scenario' and t.abc_kind == name,
387                   tests)))
388
389
390def print_cppapi_stat(tests_pathes, api, expected=0):
391    tests = []
392    for p in tests_pathes:
393        tests += list(
394            filter(lambda t: t.extension == 'cpp', collect_tests(p, api)))
395    api = get_tests_statistics(api, tests)
396
397    def api_tests_kind(kind):
398        return list(filter(lambda t: t.kind == kind, tests))
399
400    def get_element(array, i):
401        if len(array) <= i:
402            return '-'
403        return array[i]
404
405    mock_tests_apis = list(dict.fromkeys(t.api
406                                         for t in api_tests_kind("mock")))
407    api_tests_apis = list(dict.fromkeys(t.api for t in api_tests_kind("api")))
408    internal_tests = list(filter(lambda t: t.kind == 'internal', tests))
409    regression_tests = list(filter(lambda t: t.kind == 'regression', tests))
410
411    csv = ''
412    for i in range(max(len(mock_tests_apis), len(api_tests_apis))):
413        csv += f'{get_element(mock_tests_apis, i)},{get_element(api_tests_apis, i)}\n'
414    with os.fdopen(os.open(os.path.join(libabckit_dir, 'scripts/abckit_cppapi_status.csv'),
415                           os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o755), 'w+') as f:
416        f.write(('mock_tests_apis,api_tests_apis\n'))
417        f.write(csv)
418
419    logging.debug(f'>>> CPP EXTENSION <<<\n')
420
421    logging.debug(
422        'Total API:                                              %s/%s',
423        len(api), expected)
424    logging.debug('')
425    logging.debug('Total Tests:                                            %s',
426                  len(tests))
427    logging.debug('')
428    logging.debug('Total API\'S with api tests:                             %s/%s',
429                  len(api_tests_apis), expected)
430    logging.debug('Total API\'S with mock tests:                            %s/%s',
431                  len(mock_tests_apis), expected)
432    logging.debug('Total internal tests:                                     %s',
433                  len(internal_tests))
434    logging.debug('Total regression tests:                                   %s',
435                  len(regression_tests))
436    logging.debug('Total scenario tests:                                     %s',
437                  len(list(filter(lambda t: t.kind == 'scenario', tests))))
438    logging.debug(
439        'ArkTS1/ArkTS2/JS/TS scenario tests:                     %s/%s/%s/%s',
440        scenario_lang(tests, ARKTS1), scenario_lang(tests, ARKTS2), scenario_lang(tests, JS),
441        scenario_lang(tests, TS))
442    logging.debug(f'\n------------------------------------------------------------------\n')
443
444
445def print_capi_stat(tests_pathes, api):
446    csv = ''
447    tests = []
448    for p in tests_pathes:
449        tests += list(
450            filter(lambda t: t.extension == 'c', collect_tests(p, api)))
451    api = get_tests_statistics(api, tests)
452    for name in api:
453        csv += (
454            f'{name},{api[name].extension},{api[name].dynamic_positive_tests},{api[name].static_positive_tests},'
455            f'{api[name].arkts1_tests},{api[name].arkts2_tests},{api[name].js_tests},{api[name].ts_tests},'
456            f'{api[name].no_abc_tests},{api[name].positive_tests},{api[name].negative_tests},'
457            f'{api[name].negative_nullptr_tests},{api[name].negative_ctx_tests},{api[name].other_tests}\n'
458        )
459
460    with os.fdopen(os.open(os.path.join(libabckit_dir, 'scripts/abckit_capi_status.csv'),
461                           os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o755), 'w+') as f:
462        f.write(('api,extension,dynamic_positive_tests,static_positive_tests,'
463                 'arkts1_tests,arkts2_tests,js_tests,ts_tests,no_abc_tests,positive_tests,'
464                 'negative_tests,negative_nullptr_tests,negative_ctx_tests,other_tests\n'))
465        f.write(csv)
466
467    log_capi_stat(api, tests)
468
469
470def log_capi_stat(api, tests):
471
472    logging.debug('>>> C <<<\n')
473
474    logging.debug('Total API:                                              %s',
475                  len(api))
476    logging.debug('')
477    logging.debug('Total Tests:                                            %s',
478                  len(tests))
479    logging.debug('')
480    logging.debug('Total API tests:                                        %s',
481                  len(list(filter(lambda t: t.kind == 'api', tests))))
482    logging.debug(
483        'Positive/Negative/NullArg/WrongCtx/WrongMode API tests: %s/%s/%s/%s/%s',
484        api_test_category(tests, 'positive'), api_test_category(tests, 'negative'),
485        api_test_category(tests, 'negative-nullptr'),
486        api_test_category(tests, 'negative-file'), api_test_category(tests, 'negative-mode'))
487    logging.debug(
488        'ArkTS1/ArkTS2/JS/TS/NoABC API tests:                    %s/%s/%s/%s/%s',
489        api_lang(tests, ARKTS1), api_lang(tests, ARKTS2), api_lang(tests, JS), api_lang(tests, TS),
490        api_lang(tests, NO_ABC))
491    logging.debug('')
492    logging.debug('Total scenario tests:                                   %s',
493                  len(list(filter(lambda t: t.kind == 'scenario', tests))))
494    logging.debug(
495        'ArkTS1/ArkTS2/JS/TS scenario tests:                     %s/%s/%s/%s',
496        scenario_lang(tests, ARKTS1), scenario_lang(tests, ARKTS2), scenario_lang(tests, JS),
497        scenario_lang(tests, TS))
498    logging.debug('')
499    logging.debug('Internal tests:                                         %s',
500                  len(list(filter(lambda t: t.kind == 'internal', tests))))
501    logging.debug(
502        f'\n------------------------------------------------------------------\n'
503    )
504
505
506cpp_api, c_api = {}, {}
507if args.cppapi:
508    cpp_api = dict(cpp_api.items()
509                   | collect_api_from_sources(cpp_sources, 'cpp').items())
510c_api = dict(c_api.items() | collect_api_from_sources(c_sources, 'c').items())
511
512if args.print_implemented:
513    if args.cppapi:
514        logging.debug('\n'.join(cpp_api))
515    if args.capi:
516        logging.debug('\n'.join(c_api))
517else:
518    if args.cppapi:
519        print_cppapi_stat(cpp_tests, cpp_api, len(c_api))
520    if args.capi:
521        print_capi_stat(c_tests, c_api)
522