# build-aux/measurestack/test_app_plugins.py - Tests for app_plugins.py # # Copyright (C) 2025 Luke T. Shumaker # SPDX-License-Identifier: AGPL-3.0-or-later # pylint: disable=unused-variable import typing from . import analyze, app_plugins, util, vcg from .analyze import BaseName, Node, QName, SkipModel def aprime_gen(l: int, n: int) -> typing.Sequence[int]: """Return an `l`-length sequence of nonnegative integers such that any `n`-length-or-shorter combination of members with repeats allowed can be uniquely identified by its sum. (If that were "product" instead of "sum", the obvious solution would be the first `l` primes.) """ seq = [1] while len(seq) < l: x = seq[-1] * n + 1 seq.append(x) return seq def aprime_decompose( aprimes: typing.Sequence[int], tot: int ) -> tuple[typing.Collection[int], typing.Collection[int]]: ret_idx = [] ret_val = [] while tot: idx = max(i for i in range(len(aprimes)) if aprimes[i] <= tot) val = aprimes[idx] ret_idx.append(idx) ret_val.append(val) tot -= val return ret_idx, ret_val def aprime_assert( aprimes: typing.Sequence[int], act_sum: int, exp_idxs: typing.Collection[int] ) -> None: act_idxs, act_vals = aprime_decompose(aprimes, act_sum) exp_sum = sum(aprimes[i] for i in exp_idxs) # exp_vals = [aprimes[i] for i in exp] act_str = f"{act_sum}:{[f's[{v}]' for v in sorted(act_idxs)]}" exp_str = f"{exp_sum}:{[f's[{v}]' for v in sorted(exp_idxs)]}" if act_str != exp_str: assert f"act={act_str}" == f"exp={exp_str}" def test_assert_msg_fail() -> None: num_funcs = 7 max_call_depth = 7 s = aprime_gen(num_funcs, max_call_depth) class TestApplication: def extra_nodes(self) -> typing.Collection[Node]: # 1 2 3 4 5 6 7 <= call_depth # - main() s[0] # - __assert_msg_fail() s[1] * # - __lm_light_printf() s[3] # - fmt_vfctprintf() s[6] # - stdio_putchar() s[5] # - __assert_msg_fail() s[1] ** # - __lm_abort() s[2] # - stdio_flush() s[4] (inconsequential) # - __lm_abort() s[2] (inconsequential) # ---- # sum(s[i] for i in [0, 1, 3, 6, 5, 1, 2]) ret = [ # main.c util.synthetic_node("main", s[0], {"__assert_msg_fail"}), # assert.c util.synthetic_node( "__assert_msg_fail", s[1], {"__lm_light_printf", "__lm_abort"} ), # intercept.c / libfmt/libmisc.c util.synthetic_node("__lm_abort", s[2]), util.synthetic_node( "__lm_light_printf", s[3], {"fmt_vfctprintf", "stdio_flush"} ), util.synthetic_node("stdio_flush", s[4]), util.synthetic_node("stdio_putchar", s[5], {"__assert_msg_fail"}), # printf.c util.synthetic_node("fmt_vfctprintf", s[6], {"stdio_putchar"}), ] assert num_funcs == len(s) == len(ret) == len(set(n.nstatic for n in ret)) return ret def indirect_callees( self, elem: vcg.VCGElem ) -> tuple[typing.Collection[QName], bool]: return [], False def skipmodels(self) -> dict[BaseName, SkipModel]: models = app_plugins.LibMiscPlugin(arg_c_fnames=[]).skipmodels() assert BaseName("__assert_msg_fail") in models orig_model = models[BaseName("__assert_msg_fail")] def wrapped_model_fn( chain: typing.Sequence[QName], node: Node, call: QName ) -> bool: dbgstr = ( ("=>".join(str(c) for c in [*chain, node.funcname])) + "=?=>" + str(call) ) assert dbgstr in [ "__assert_msg_fail=?=>__lm_light_printf", "__assert_msg_fail=?=>__lm_abort", "__assert_msg_fail=>__lm_light_printf=>fmt_vfctprintf=>stdio_putchar=>__assert_msg_fail=?=>__lm_light_printf", "__assert_msg_fail=>__lm_light_printf=>fmt_vfctprintf=>stdio_putchar=>__assert_msg_fail=?=>__lm_abort", ] return orig_model.fn(chain, node, call) models[BaseName("__assert_msg_fail")] = SkipModel( orig_model.nchain, wrapped_model_fn ) return models def test_filter(name: QName) -> tuple[int, bool]: if name.base() == BaseName("main"): return 1, True return 0, False result = analyze.analyze( ci_fnames=[], app_func_filters={ "Main": test_filter, }, app=TestApplication(), cfg_max_call_depth=max_call_depth, ) aprime_assert( s, result.groups["Main"].rows[QName("main")].nstatic, [0, 1, 3, 6, 5, 1, 2] ) def test_fct() -> None: num_funcs = 13 max_call_depth = 12 s = aprime_gen(num_funcs, max_call_depth) class TestPlugin: def is_intrhandler(self, name: QName) -> bool: return False def init_array(self) -> typing.Collection[QName]: return [] def extra_includes(self) -> typing.Collection[BaseName]: return [] def indirect_callees( self, loc: str, line: str ) -> tuple[typing.Collection[QName], bool] | None: return None def skipmodels(self) -> dict[BaseName, analyze.SkipModel]: return {} def extra_nodes(self) -> typing.Collection[Node]: # 1. | a +s[0] | b +s[ 1] | c +s[ 2] |* # 2. | fmt_vsnprintf +s[3] | vprintf +s[ 4] | __lm_light_printf +s[ 5] |* # 3. | fmt_vfctprintf +s[6] | fmt_vfctprintf +s[ 6] | fmt_vfctprintf +s[ 6] | # 4. | fmt_state_putchar +s[7] | fmt_state_putchar +s[ 7] | fmt_state_putchar +s[ 7] | # 5. | _out_buffer +s[8] | stdio_buffered_printer +s[ 9] | libfmt_light_fct +s[10] |* # 6. | | __assert_msg_fail +s[11] | __assert_msg_fail +s[11] | # 7. | | a. __lm_light_printf +s[ 5] | a. __lm_light_printf +s[ 5] | # 8. | | a. fmt_vfctprintf +s[ 6] | a. fmt_vfctprintf +s[ 6] | # 9. | | a. fmt_state_putchar +s[ 7] | a. fmt_state_putchar +s[ 7] | # 10. | | a. libfmt_light_fct +s[10] | a. libfmt_light_fct +s[10] | # 11. | | a. __assert_msg_fail +s[11] | a. __assert_msg_fail +s[11] | # 12. | | a. __lm_abort +s[12] | a. __lm_abort +s[12] | # 7. | | b. __lm_abort | b. __lm_abort | return [ # main.c util.synthetic_node("a", s[0], {"fmt_vsnprintf"}), # _out_buffer util.synthetic_node("b", s[1], {"vprintf"}), # stdio_buffered_printer util.synthetic_node( "c", s[2], {"__lm_light_printf"} ), # libfmt_light_printf # wrappers util.synthetic_node("fmt_vsnprintf", s[3], {"fmt_vfctprintf"}), util.synthetic_node("__wrap_vprintf", s[4], {"fmt_vfctprintf"}), util.synthetic_node("__lm_light_printf", s[5], {"fmt_vfctprintf"}), # printf.c util.synthetic_node("fmt_vfctprintf", s[6], {"fmt_state_putchar"}), util.synthetic_node( "fmt_state_putchar", s[7], {"_out_buffer", "stdio_buffered_printer", "libfmt_light_fct"}, ), # fcts util.synthetic_node("_out_buffer", s[8]), util.synthetic_node( "stdio_buffered_printer", s[9], {"__assert_msg_fail"} ), util.synthetic_node("libfmt_light_fct", s[10], {"__assert_msg_fail"}), # assert.c util.synthetic_node( "__assert_msg_fail", s[11], {"__lm_light_printf", "__lm_abort"}, ), # intercept.c / libfmt/libmisc.c util.synthetic_node("__lm_abort", s[12]), ] plugins: list[util.Plugin] = [ TestPlugin(), app_plugins.LibMiscPlugin(arg_c_fnames=[]), # fmt_vsnprintf => fct=_out_buffer # if rp2040: # __wrap_vprintf => fct=stdio_buffered_printer # stdio_vprintf => fct=stdio_buffered_printer # __lm_light_printf => fct=libfmt_light_fct # if host: # __lm_printf => fct=libfmt_libc_fct # __lm_light_printf => fct=libfmt_libc_fct app_plugins.PicoFmtPlugin("rp2040"), ] def test_filter(name: QName) -> tuple[int, bool]: if str(name.base()) in ["a", "b", "c"]: return 1, True return 0, False def _str_location_xform(loc: str) -> str: return loc result = analyze.analyze( ci_fnames=[], app_func_filters={ "Main": test_filter, }, app=util.PluginApplication(_str_location_xform, plugins), cfg_max_call_depth=max_call_depth, ) aprime_assert(s, result.groups["Main"].rows[QName("a")].nstatic, [0, 3, 6, 7, 8]) aprime_assert( s, result.groups["Main"].rows[QName("b")].nstatic, [1, 4, 6, 7, 9, 11, 5, 6, 7, 10, 11, 12], ) aprime_assert( s, result.groups["Main"].rows[QName("c")].nstatic, [2, 5, 6, 7, 10, 11, 5, 6, 7, 10, 11, 12], )