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