| #!/usr/bin/env python3 | |
| """ | |
| Unit tests for struct/union member extractor class. | |
| """ | |
| import os | |
| import re | |
| import unittest | |
| import sys | |
| from unittest.mock import MagicMock | |
| SRC_DIR = os.path.dirname(os.path.realpath(__file__)) | |
| sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python")) | |
| from kdoc.c_lex import CToken, CTokenizer | |
| from unittest_helper import run_unittest | |
| # | |
| # List of tests. | |
| # | |
| # The code will dynamically generate one test for each key on this dictionary. | |
| # | |
| def tokens_to_list(tokens): | |
| tuples = [] | |
| for tok in tokens: | |
| if tok.kind == CToken.SPACE: | |
| continue | |
| tuples += [(tok.kind, tok.value, tok.level)] | |
| return tuples | |
| def make_tokenizer_test(name, data): | |
| """ | |
| Create a test named ``name`` using parameters given by ``data`` dict. | |
| """ | |
| def test(self): | |
| """In-lined lambda-like function to run the test""" | |
| # | |
| # Check if logger is working | |
| # | |
| if "log_msg" in data: | |
| with self.assertLogs() as cm: | |
| tokenizer = CTokenizer(data["source"]) | |
| msg_found = False | |
| for result in cm.output: | |
| if data["log_msg"] in result: | |
| msg_found = True | |
| self.assertTrue(msg_found, f"Missing log {data['log_msg']}") | |
| return | |
| # | |
| # Check if tokenizer is producing expected results | |
| # | |
| tokens = CTokenizer(data["source"]).tokens | |
| result = tokens_to_list(tokens) | |
| expected = tokens_to_list(data["expected"]) | |
| self.assertEqual(result, expected, msg=f"{name}") | |
| return test | |
| #: Tokenizer tests. | |
| TESTS_TOKENIZER = { | |
| "__run__": make_tokenizer_test, | |
| "basic_tokens": { | |
| "source": """ | |
| int a; // comment | |
| float b = 1.23; | |
| """, | |
| "expected": [ | |
| CToken(CToken.NAME, "int"), | |
| CToken(CToken.NAME, "a"), | |
| CToken(CToken.ENDSTMT, ";"), | |
| CToken(CToken.COMMENT, "// comment"), | |
| CToken(CToken.NAME, "float"), | |
| CToken(CToken.NAME, "b"), | |
| CToken(CToken.OP, "="), | |
| CToken(CToken.NUMBER, "1.23"), | |
| CToken(CToken.ENDSTMT, ";"), | |
| ], | |
| }, | |
| "depth_counters": { | |
| "source": """ | |
| struct X { | |
| int arr[10]; | |
| func(a[0], (b + c)); | |
| } | |
| """, | |
| "expected": [ | |
| CToken(CToken.STRUCT, "struct"), | |
| CToken(CToken.NAME, "X"), | |
| CToken(CToken.BEGIN, "{", brace_level=1), | |
| CToken(CToken.NAME, "int", brace_level=1), | |
| CToken(CToken.NAME, "arr", brace_level=1), | |
| CToken(CToken.BEGIN, "[", brace_level=1, bracket_level=1), | |
| CToken(CToken.NUMBER, "10", brace_level=1, bracket_level=1), | |
| CToken(CToken.END, "]", brace_level=1), | |
| CToken(CToken.ENDSTMT, ";", brace_level=1), | |
| CToken(CToken.NAME, "func", brace_level=1), | |
| CToken(CToken.BEGIN, "(", brace_level=1, paren_level=1), | |
| CToken(CToken.NAME, "a", brace_level=1, paren_level=1), | |
| CToken(CToken.BEGIN, "[", brace_level=1, paren_level=1, bracket_level=1), | |
| CToken(CToken.NUMBER, "0", brace_level=1, paren_level=1, bracket_level=1), | |
| CToken(CToken.END, "]", brace_level=1, paren_level=1), | |
| CToken(CToken.PUNC, ",", brace_level=1, paren_level=1), | |
| CToken(CToken.BEGIN, "(", brace_level=1, paren_level=2), | |
| CToken(CToken.NAME, "b", brace_level=1, paren_level=2), | |
| CToken(CToken.OP, "+", brace_level=1, paren_level=2), | |
| CToken(CToken.NAME, "c", brace_level=1, paren_level=2), | |
| CToken(CToken.END, ")", brace_level=1, paren_level=1), | |
| CToken(CToken.END, ")", brace_level=1), | |
| CToken(CToken.ENDSTMT, ";", brace_level=1), | |
| CToken(CToken.END, "}"), | |
| ], | |
| }, | |
| "mismatch_error": { | |
| "source": "int a$ = 5;", # $ is illegal | |
| "log_msg": "Unexpected token", | |
| }, | |
| } | |
| def make_private_test(name, data): | |
| """ | |
| Create a test named ``name`` using parameters given by ``data`` dict. | |
| """ | |
| def test(self): | |
| """In-lined lambda-like function to run the test""" | |
| tokens = CTokenizer(data["source"]) | |
| result = str(tokens) | |
| # | |
| # Avoid whitespace false positives | |
| # | |
| result = re.sub(r"\s++", " ", result).strip() | |
| expected = re.sub(r"\s++", " ", data["trimmed"]).strip() | |
| msg = f"failed when parsing this source:\n{data['source']}" | |
| self.assertEqual(result, expected, msg=msg) | |
| return test | |
| #: Tests to check if CTokenizer is handling properly public/private comments. | |
| TESTS_PRIVATE = { | |
| # | |
| # Simplest case: no private. Ensure that trimming won't affect struct | |
| # | |
| "__run__": make_private_test, | |
| "no private": { | |
| "source": """ | |
| struct foo { | |
| int a; | |
| int b; | |
| int c; | |
| }; | |
| """, | |
| "trimmed": """ | |
| struct foo { | |
| int a; | |
| int b; | |
| int c; | |
| }; | |
| """, | |
| }, | |
| # | |
| # Play "by the books" by always having a public in place | |
| # | |
| "balanced_private": { | |
| "source": """ | |
| struct foo { | |
| int a; | |
| /* private: */ | |
| int b; | |
| /* public: */ | |
| int c; | |
| }; | |
| """, | |
| "trimmed": """ | |
| struct foo { | |
| int a; | |
| int c; | |
| }; | |
| """, | |
| }, | |
| "balanced_non_greddy_private": { | |
| "source": """ | |
| struct foo { | |
| int a; | |
| /* private: */ | |
| int b; | |
| /* public: */ | |
| int c; | |
| /* private: */ | |
| int d; | |
| /* public: */ | |
| int e; | |
| }; | |
| """, | |
| "trimmed": """ | |
| struct foo { | |
| int a; | |
| int c; | |
| int e; | |
| }; | |
| """, | |
| }, | |
| "balanced_inner_private": { | |
| "source": """ | |
| struct foo { | |
| struct { | |
| int a; | |
| /* private: ignore below */ | |
| int b; | |
| /* public: but this should not be ignored */ | |
| }; | |
| int b; | |
| }; | |
| """, | |
| "trimmed": """ | |
| struct foo { | |
| struct { | |
| int a; | |
| }; | |
| int b; | |
| }; | |
| """, | |
| }, | |
| # | |
| # Test what happens if there's no public after private place | |
| # | |
| "unbalanced_private": { | |
| "source": """ | |
| struct foo { | |
| int a; | |
| /* private: */ | |
| int b; | |
| int c; | |
| }; | |
| """, | |
| "trimmed": """ | |
| struct foo { | |
| int a; | |
| }; | |
| """, | |
| }, | |
| "unbalanced_inner_private": { | |
| "source": """ | |
| struct foo { | |
| struct { | |
| int a; | |
| /* private: ignore below */ | |
| int b; | |
| /* but this should not be ignored */ | |
| }; | |
| int b; | |
| }; | |
| """, | |
| "trimmed": """ | |
| struct foo { | |
| struct { | |
| int a; | |
| }; | |
| int b; | |
| }; | |
| """, | |
| }, | |
| "unbalanced_struct_group_tagged_with_private": { | |
| "source": """ | |
| struct page_pool_params { | |
| struct_group_tagged(page_pool_params_fast, fast, | |
| unsigned int order; | |
| unsigned int pool_size; | |
| int nid; | |
| struct device *dev; | |
| struct napi_struct *napi; | |
| enum dma_data_direction dma_dir; | |
| unsigned int max_len; | |
| unsigned int offset; | |
| }; | |
| struct_group_tagged(page_pool_params_slow, slow, | |
| struct net_device *netdev; | |
| unsigned int queue_idx; | |
| unsigned int flags; | |
| /* private: used by test code only */ | |
| void (*init_callback)(netmem_ref netmem, void *arg); | |
| void *init_arg; | |
| }; | |
| }; | |
| """, | |
| "trimmed": """ | |
| struct page_pool_params { | |
| struct_group_tagged(page_pool_params_fast, fast, | |
| unsigned int order; | |
| unsigned int pool_size; | |
| int nid; | |
| struct device *dev; | |
| struct napi_struct *napi; | |
| enum dma_data_direction dma_dir; | |
| unsigned int max_len; | |
| unsigned int offset; | |
| }; | |
| struct_group_tagged(page_pool_params_slow, slow, | |
| struct net_device *netdev; | |
| unsigned int queue_idx; | |
| unsigned int flags; | |
| }; | |
| }; | |
| """, | |
| }, | |
| "unbalanced_two_struct_group_tagged_first_with_private": { | |
| "source": """ | |
| struct page_pool_params { | |
| struct_group_tagged(page_pool_params_slow, slow, | |
| struct net_device *netdev; | |
| unsigned int queue_idx; | |
| unsigned int flags; | |
| /* private: used by test code only */ | |
| void (*init_callback)(netmem_ref netmem, void *arg); | |
| void *init_arg; | |
| }; | |
| struct_group_tagged(page_pool_params_fast, fast, | |
| unsigned int order; | |
| unsigned int pool_size; | |
| int nid; | |
| struct device *dev; | |
| struct napi_struct *napi; | |
| enum dma_data_direction dma_dir; | |
| unsigned int max_len; | |
| unsigned int offset; | |
| }; | |
| }; | |
| """, | |
| "trimmed": """ | |
| struct page_pool_params { | |
| struct_group_tagged(page_pool_params_slow, slow, | |
| struct net_device *netdev; | |
| unsigned int queue_idx; | |
| unsigned int flags; | |
| }; | |
| struct_group_tagged(page_pool_params_fast, fast, | |
| unsigned int order; | |
| unsigned int pool_size; | |
| int nid; | |
| struct device *dev; | |
| struct napi_struct *napi; | |
| enum dma_data_direction dma_dir; | |
| unsigned int max_len; | |
| unsigned int offset; | |
| }; | |
| }; | |
| """, | |
| }, | |
| "unbalanced_without_end_of_line": { | |
| "source": """ \ | |
| struct page_pool_params { \ | |
| struct_group_tagged(page_pool_params_slow, slow, \ | |
| struct net_device *netdev; \ | |
| unsigned int queue_idx; \ | |
| unsigned int flags; | |
| /* private: used by test code only */ | |
| void (*init_callback)(netmem_ref netmem, void *arg); \ | |
| void *init_arg; \ | |
| }; \ | |
| struct_group_tagged(page_pool_params_fast, fast, \ | |
| unsigned int order; \ | |
| unsigned int pool_size; \ | |
| int nid; \ | |
| struct device *dev; \ | |
| struct napi_struct *napi; \ | |
| enum dma_data_direction dma_dir; \ | |
| unsigned int max_len; \ | |
| unsigned int offset; \ | |
| }; \ | |
| }; | |
| """, | |
| "trimmed": """ | |
| struct page_pool_params { | |
| struct_group_tagged(page_pool_params_slow, slow, | |
| struct net_device *netdev; | |
| unsigned int queue_idx; | |
| unsigned int flags; | |
| }; | |
| struct_group_tagged(page_pool_params_fast, fast, | |
| unsigned int order; | |
| unsigned int pool_size; | |
| int nid; | |
| struct device *dev; | |
| struct napi_struct *napi; | |
| enum dma_data_direction dma_dir; | |
| unsigned int max_len; | |
| unsigned int offset; | |
| }; | |
| }; | |
| """, | |
| }, | |
| } | |
| #: Dict containing all test groups fror CTokenizer | |
| TESTS = { | |
| "TestPublicPrivate": TESTS_PRIVATE, | |
| "TestTokenizer": TESTS_TOKENIZER, | |
| } | |
| def setUp(self): | |
| self.maxDiff = None | |
| def build_test_class(group_name, table): | |
| """ | |
| Dynamically creates a class instance using type() as a generator | |
| for a new class derivated from unittest.TestCase. | |
| We're opting to do it inside a function to avoid the risk of | |
| changing the globals() dictionary. | |
| """ | |
| class_dict = { | |
| "setUp": setUp | |
| } | |
| run = table["__run__"] | |
| for test_name, data in table.items(): | |
| if test_name == "__run__": | |
| continue | |
| class_dict[f"test_{test_name}"] = run(test_name, data) | |
| cls = type(group_name, (unittest.TestCase,), class_dict) | |
| return cls.__name__, cls | |
| # | |
| # Create classes and add them to the global dictionary | |
| # | |
| for group, table in TESTS.items(): | |
| t = build_test_class(group, table) | |
| globals()[t[0]] = t[1] | |
| # | |
| # main | |
| # | |
| if __name__ == "__main__": | |
| run_unittest(__file__) |