diff options
author | Luke T. Shumaker <lukeshu@lukeshu.com> | 2025-05-10 17:11:48 -0600 |
---|---|---|
committer | Luke T. Shumaker <lukeshu@lukeshu.com> | 2025-05-10 17:11:48 -0600 |
commit | c474fa0ade3409efa9eb1e45743afb82fd377cbd (patch) | |
tree | 51757128739021b900d04732ce8782907d8e820b /build-aux/measurestack | |
parent | fdc090bb2ee51f0f6e8a10d152ac0df22784c2ac (diff) | |
parent | b68138677750800764453c7498c8436eca813f4b (diff) |
Diffstat (limited to 'build-aux/measurestack')
-rw-r--r-- | build-aux/measurestack/app_main.py | 2 | ||||
-rw-r--r-- | build-aux/measurestack/app_plugins.py | 26 | ||||
-rw-r--r-- | build-aux/measurestack/test_analyze.py | 55 | ||||
-rw-r--r-- | build-aux/measurestack/test_app_plugins.py | 468 | ||||
-rw-r--r-- | build-aux/measurestack/testutil.py | 131 |
5 files changed, 492 insertions, 190 deletions
diff --git a/build-aux/measurestack/app_main.py b/build-aux/measurestack/app_main.py index c670325..f705876 100644 --- a/build-aux/measurestack/app_main.py +++ b/build-aux/measurestack/app_main.py @@ -48,7 +48,7 @@ def main( plugins += [ app_plugins.CmdPlugin(), libmisc_plugin, - app_plugins.PicoFmtPlugin(arg_pico_platform), + app_plugins.PicoFmtPlugin(arg_pico_platform, lib9p_plugin.formatters), app_plugins.LibHWPlugin(arg_pico_platform, libmisc_plugin), app_plugins.LibCRPlugin(), app_plugins.LibCRIPCPlugin(), diff --git a/build-aux/measurestack/app_plugins.py b/build-aux/measurestack/app_plugins.py index 8eda36c..e365f82 100644 --- a/build-aux/measurestack/app_plugins.py +++ b/build-aux/measurestack/app_plugins.py @@ -410,9 +410,6 @@ class Lib9PPlugin: 1, self._skipmodel__lib9p_validate_unmarshal_marshal, ), - BaseName("_vfctprintf"): analyze.SkipModel( - self.formatters, self._skipmodel__vfctprintf - ), } return ret @@ -427,18 +424,14 @@ class Lib9PPlugin: return False return m_caller.group("grp") != m_callee.group("grp") - def _skipmodel__vfctprintf( - self, chain: typing.Sequence[QName], node: Node, call: QName - ) -> bool: - if call.base() == BaseName("libfmt_conv_formatter"): - return any(c.base() in self.formatters for c in chain) - return False - class PicoFmtPlugin: known_fct: dict[BaseName, BaseName] + wont_call_v: typing.Collection[BaseName] - def __init__(self, arg_pico_platform: str) -> None: + def __init__( + self, arg_pico_platform: str, wont_call_v: typing.Collection[BaseName] + ) -> None: self.known_fct = { # pico_fmt BaseName("fmt_vsnprintf"): BaseName("_out_buffer"), @@ -462,6 +455,7 @@ class PicoFmtPlugin: BaseName("__lm_light_printf"): BaseName("libfmt_libc_fct"), } ) + self.wont_call_v = set([*self.known_fct.values(), *wont_call_v]) def is_intrhandler(self, name: QName) -> bool: return False @@ -506,6 +500,9 @@ class PicoFmtPlugin: BaseName("fmt_state_putchar"): analyze.SkipModel( self.known_fct.keys(), self._skipmodel_fmt_state_putchar ), + BaseName("_vfctprintf"): analyze.SkipModel( + self.wont_call_v, self._skipmodel__vfctprintf + ), } return ret @@ -521,6 +518,13 @@ class PicoFmtPlugin: return True return False + def _skipmodel__vfctprintf( + self, chain: typing.Sequence[QName], node: Node, call: QName + ) -> bool: + if call.base() == BaseName("libfmt_conv_formatter"): + return any(c.base() in self.wont_call_v for c in chain) + return False + class PicoSDKPlugin: get_init_array: typing.Callable[[], typing.Collection[QName]] diff --git a/build-aux/measurestack/test_analyze.py b/build-aux/measurestack/test_analyze.py index ff1732d..df205e8 100644 --- a/build-aux/measurestack/test_analyze.py +++ b/build-aux/measurestack/test_analyze.py @@ -5,17 +5,20 @@ # pylint: disable=unused-variable +import re +import typing + import pytest -from .analyze import BaseName, QName +from . import analyze, testutil, util def test_name_base() -> None: - assert QName("foo.c:bar.1").base() == BaseName("bar") + assert analyze.QName("foo.c:bar.1").base() == analyze.BaseName("bar") def test_name_pretty() -> None: - name = QName("foo.c:bar.1") + name = analyze.QName("foo.c:bar.1") assert f"{name}" == "QName('foo.c:bar.1')" assert f"{name.base()}" == "BaseName('bar')" assert f"{[name]}" == "[QName('foo.c:bar.1')]" @@ -23,7 +26,7 @@ def test_name_pretty() -> None: def test_name_eq() -> None: - name = QName("foo.c:bar.1") + name = analyze.QName("foo.c:bar.1") with pytest.raises(AssertionError) as e: if name == "foo": pass @@ -32,3 +35,47 @@ def test_name_eq() -> None: if name.base() == "foo": pass assert "comparing BaseName with str" in str(e) + + +def test_max_call_depth() -> None: + graph: typing.Sequence[tuple[str, typing.Collection[str]]] = [ + ("a", {"b"}), # 1 + ("b", {"c"}), # 2 + ("c", {"d"}), # 3 + ("d", {"e"}), # 4 + ("e", {}), # 5 + ] + + testcases: dict[int, bool] = { + 1: True, + 2: True, + 3: True, + 4: True, + 5: False, + 6: False, + 7: False, + } + + def test_filter(name: analyze.QName) -> tuple[int, bool]: + if str(name.base()) in ["a"]: + return 1, True + return 0, False + + def doit(depth: int, graph_plugin: util.Plugin) -> None: + analyze.analyze( + ci_fnames=[], + app_func_filters={"Main": test_filter}, + app=util.PluginApplication(testutil.nop_location_xform, [graph_plugin]), + cfg_max_call_depth=depth, + ) + + pat = re.compile("^max call depth exceeded: ") + + for depth, should_fail in testcases.items(): + graph_plugin = testutil.GraphProviderPlugin(depth, graph) + + if should_fail: + with pytest.raises(ValueError, match=pat): + doit(depth, graph_plugin) + else: + doit(depth, graph_plugin) diff --git a/build-aux/measurestack/test_app_plugins.py b/build-aux/measurestack/test_app_plugins.py index 8aa0a6c..da8be65 100644 --- a/build-aux/measurestack/test_app_plugins.py +++ b/build-aux/measurestack/test_app_plugins.py @@ -7,98 +7,47 @@ import typing -from . import analyze, app_plugins, util, vcg +from . import analyze, app_plugins, testutil, util 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 + # 1 2 3 4 5 6 7 <= call_depth + # - main() + # - __assert_msg_fail() * + # - __lm_light_printf() + # - fmt_vfctprintf() + # - stdio_putchar() + # - __assert_msg_fail() ** + # - __lm_abort() + # - stdio_flush() (inconsequential) + # - __lm_abort() (inconsequential) 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 + exp = [ + "main", + "__assert_msg_fail", + "__lm_light_printf", + "fmt_vfctprintf", + "stdio_putchar", + "__assert_msg_fail", + "__lm_abort", + ] + graph: typing.Sequence[tuple[str, typing.Collection[str]]] = [ + # main.c + ("main", {"__assert_msg_fail"}), + # assert.c + ("__assert_msg_fail", {"__lm_light_printf", "__lm_abort"}), + # intercept.c / libfmt/libmisc.c + ("__lm_abort", {}), + ("__lm_light_printf", {"fmt_vfctprintf", "stdio_flush"}), + ("stdio_flush", {}), + ("stdio_putchar", {"__assert_msg_fail"}), + # printf.c + ("fmt_vfctprintf", {"stdio_putchar"}), + ] + graph_plugin = testutil.GraphProviderPlugin(max_call_depth, graph) + class SkipPlugin(testutil.NopPlugin): def skipmodels(self) -> dict[BaseName, SkipModel]: models = app_plugins.LibMiscPlugin(arg_c_fnames=[]).skipmodels() assert BaseName("__assert_msg_fail") in models @@ -135,88 +84,87 @@ def test_assert_msg_fail() -> None: app_func_filters={ "Main": test_filter, }, - app=TestApplication(), + app=util.PluginApplication( + testutil.nop_location_xform, [graph_plugin, SkipPlugin()] + ), cfg_max_call_depth=max_call_depth, ) - aprime_assert( - s, result.groups["Main"].rows[QName("main")].nstatic, [0, 1, 3, 6, 5, 1, 2] - ) + graph_plugin.assert_nstatic(result.groups["Main"].rows[QName("main")].nstatic, exp) def test_fct() -> None: - num_funcs = 13 + # 1. | a + | b + | c + |* + # 2. | fmt_vsnprintf + | vprintf + | __lm_light_printf + |* + # 3. | fmt_vfctprintf + | fmt_vfctprintf + | fmt_vfctprintf + | + # 4. | fmt_state_putchar + | fmt_state_putchar + | fmt_state_putchar + | + # 5. | _out_buffer + | stdio_buffered_printer + | libfmt_light_fct + |* + # 6. | | __assert_msg_fail + | __assert_msg_fail + | + # 7. | | a. __lm_light_printf + | a. __lm_light_printf + | + # 8. | | a. fmt_vfctprintf + | a. fmt_vfctprintf + | + # 9. | | a. fmt_state_putchar + | a. fmt_state_putchar + | + # 10. | | a. libfmt_light_fct + | a. libfmt_light_fct + | + # 11. | | a. __assert_msg_fail + | a. __assert_msg_fail + | + # 12. | | a. __lm_abort + | a. __lm_abort + | + # 7. | | b. __lm_abort | b. __lm_abort | 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]), - ] + exp_a = ["a", "fmt_vsnprintf", "fmt_vfctprintf", "fmt_state_putchar", "_out_buffer"] + exp_b = [ + "b", + "__wrap_vprintf", + "fmt_vfctprintf", + "fmt_state_putchar", + "stdio_buffered_printer", + "__assert_msg_fail", + "__lm_light_printf", + "fmt_vfctprintf", + "fmt_state_putchar", + "libfmt_light_fct", + "__assert_msg_fail", + "__lm_abort", + ] + exp_c = [ + "c", + "__lm_light_printf", + "fmt_vfctprintf", + "fmt_state_putchar", + "libfmt_light_fct", + "__assert_msg_fail", + "__lm_light_printf", + "fmt_vfctprintf", + "fmt_state_putchar", + "libfmt_light_fct", + "__assert_msg_fail", + "__lm_abort", + ] + graph: typing.Sequence[tuple[str, typing.Collection[str]]] = [ + # main.c + ("a", {"fmt_vsnprintf"}), # _out_buffer + ("b", {"vprintf"}), # stdio_buffered_printer + ("c", {"__lm_light_printf"}), # libfmt_light_printf + # wrappers + ("fmt_vsnprintf", {"fmt_vfctprintf"}), + ("__wrap_vprintf", {"fmt_vfctprintf"}), + ("__lm_light_printf", {"fmt_vfctprintf"}), + # printf.c + ("fmt_vfctprintf", {"fmt_state_putchar"}), + ( + "fmt_state_putchar", + {"_out_buffer", "stdio_buffered_printer", "libfmt_light_fct"}, + ), + # fcts + ("_out_buffer", {}), + ("stdio_buffered_printer", {"__assert_msg_fail"}), + ("libfmt_light_fct", {"__assert_msg_fail"}), + # assert.c + ("__assert_msg_fail", {"__lm_light_printf", "__lm_abort"}), + # intercept.c / libfmt/libmisc.c + ("__lm_abort", {}), + ] + graph_plugin = testutil.GraphProviderPlugin(max_call_depth, graph) plugins: list[util.Plugin] = [ - TestPlugin(), + graph_plugin, app_plugins.LibMiscPlugin(arg_c_fnames=[]), # fmt_vsnprintf => fct=_out_buffer # if rp2040: @@ -226,7 +174,7 @@ def test_fct() -> None: # if host: # __lm_printf => fct=libfmt_libc_fct # __lm_light_printf => fct=libfmt_libc_fct - app_plugins.PicoFmtPlugin("rp2040"), + app_plugins.PicoFmtPlugin("rp2040", []), ] def test_filter(name: QName) -> tuple[int, bool]: @@ -234,26 +182,198 @@ def test_fct() -> None: 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), + app=util.PluginApplication(testutil.nop_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], + graph_plugin.assert_nstatic(result.groups["Main"].rows[QName("a")].nstatic, exp_a) + graph_plugin.assert_nstatic(result.groups["Main"].rows[QName("b")].nstatic, exp_b) + graph_plugin.assert_nstatic(result.groups["Main"].rows[QName("c")].nstatic, exp_c) + + +def test_assert_formatter() -> None: + # _________________________________________________________ + # | | + # | | + # | main | + # | | | + # | _ __wrap_vprintf | + # | / \ | _______________ | + # | | fmt_vfctprintf / \ | + # | | \ fmt_state_printf | | + # | | \____ ____/ | | + # | | \ / | | + # | | _vfctprintf | | + # | | ____/ \____ ^ | + # | | / ?<---snip | | + # | | conv_builtin \ | | + # | | | libfmt_conv_formatter | | + # | | | | | | + # | ^ \ lib9p_msg_Rread_format | | + # | | \ _____________/ | \______/ | + # | | \ / \ | + # | | fmt_state_putchar \ | + # | | ?<-?<--------snip | | + # | | / \_________ | | + # | | / \ | | + # | | stdio_buffered_printer \ | | + # | | \ libfmt_light_fct | | + # | | \ | / | + # | | \_______ | ________/ | + # | | \ | / | + # | | __assert_msg_fail | + # | | ___/ \____ | + # | | snip--->? \ | + # | | / \ | + # | | __lm_light_printf \ | + # | \____________/ __lm_abort | + # | | + # |_________________________________________________________| + # + graph: typing.Sequence[tuple[str, typing.Collection[str]]] = [ + ("main", {"vprintf"}), + ("__wrap_vprintf", {"fmt_vfctprintf"}), + ("fmt_vfctprintf", {"_vfctprintf"}), + ("fmt_state_printf", {"_vfctprintf"}), + ("_vfctprintf", {"conv_builtin", "libfmt_conv_formatter"}), + ("conv_builtin", {"fmt_state_putchar"}), + ("libfmt_conv_formatter", {"lib9p_msg_Rread_format"}), + ( + "lib9p_msg_Rread_format", + {"fmt_state_putchar", "__assert_msg_fail", "fmt_state_printf"}, + ), + ("fmt_state_putchar", {"stdio_buffered_printer", "libfmt_light_fct"}), + ("stdio_buffered_printer", {"__assert_msg_fail"}), + ("libfmt_light_fct", {"__assert_msg_fail"}), + ("__assert_msg_fail", {"__lm_light_printf", "__lm_abort"}), + ("__lm_light_printf", {"fmt_vfctprintf"}), + ("__lm_abort", {}), + ] + + # fct-determining wrappers have their callees marked with "|": + # + # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <= call_depth + # - main() ; + + # - __wrap__vprintf() ; + + # |- fmt_vfctprintf() ; + + # | - _vfctprintf() ; + + # | - conv_builtin() ; + # | - fmt_state_putchar() ; + # | - stdio_buffered_printer() ; + # | - __assert_msg_fail() ; + # | - __lm_light_printf() ; + # | |- fmt_vfctprintf() ; + # | | - _vfctprintf() ; + # | | - conv_builtin() ; + # | | - fmt_state_putchar() ; + # | | - stdio_buffered_printer() ; skip (wrong fct) + # | | - libfmt_light_fct() ; + # | | - __assert_msg_fail() ; + # | | - __lm_light_printf() ; skip (nested __assert_msg_fail) + # | | - __lm_abort() ; + # | | - libfmt_conv_formatter() ; skip (fct won't use %v) + # | - __lm_abort() ; + # | - libfmt_light_fct() ; skip (wrong fct) + # | - libfmt_conv_formatter() ; + + # | - lib9p_msg_Rread_format() ; + + # | - fmt_state_putchar() ; + # | - stdio_buffered_printer() ; + # | - __assert_msg_fail() ; + # | - __lm_light_printf() ; + # | |- fmt_vfctprintf() ; + # | | - _vfctprintf() ; + # | | - conv_builtin() ; + # | | - fmt_state_putchar() ; + # | | - stdio_buffered_printer() ; skip (wrong fct) + # | | - libfmt_light_fct() ; + # | | - __assert_msg_fail() ; + # | | - __lm_light_printf() ; skip (neseted __assert_msg_fail) + # | | - __lm_abort() ; + # | | - libfmt_conv_formatter() ; skip (fct won't use %v) + # | - __lm_abort() ; + # | - libfmt_light_fct() ; skip (wrong fct) + # | - __assert_msg_fail() ; + # | - __lm_light_printf() ; + # | |- fmt_vfctprintf() ; + # | | - _vfctprintf() ; + # | | - conv_builtin() ; + # | | - fmt_state_putchar() ; + # | | - stdio_buffered_printer() ; skip (wrong fct) + # | | - libfmt_light_fct() ; + # | | - __assert_msg_fail() ; + # | | - __lm_light_printf() ; skip (nested__assert_msg_fail) + # | | - __lm_abort() ; + # | | - libfmt_conv_formatter() ; skip (formatter won't use %v) + # | - __lm_abort() ; + # | - fmt_state_printf() ; + + # | - _vfctprintf() ; + + # | - conv_builtin() ; + + # | - fmt_state_putchar() ; + + # | - stdio_buffered_printer() ; + + # | - __assert_msg_fail() ; + + # | - __lm_light_printf() ; + + # | |- fmt_vfctprintf() ; + + # | | - _vfctprintf() ; + + # | | - conv_builtin() ; + + # | | - fmt_state_putchar() ; + + # | | - stdio_buffered_printer() ; skip (wrong fct) + # | | - libfmt_light_fct() ; + + # | | - __assert_msg_fail() ; + + # | | - __lm_light_printf() ; skip (neseted __assert_msg_fail) + # | | - __lm_abort() ; + + # | | - libfmt_conv_formatter() ; skip (fct won't use %v) + # | - __lm_abort() ; + # | - libfmt_light_fct() ; skip (wrong fct) + # | - libfmt_conv_formatter() ; skip (formatter won't use %v) + max_call_depth = 20 + exp = [ + "main", + "__wrap_vprintf", + "fmt_vfctprintf", + "_vfctprintf", + "libfmt_conv_formatter", + "lib9p_msg_Rread_format", + "fmt_state_printf", + "_vfctprintf", + "conv_builtin", + "fmt_state_putchar", + "stdio_buffered_printer", + "__assert_msg_fail", + "__lm_light_printf", + "fmt_vfctprintf", + "_vfctprintf", + "conv_builtin", + "fmt_state_putchar", + "libfmt_light_fct", + "__assert_msg_fail", + "__lm_abort", + ] + + graph_plugin = testutil.GraphProviderPlugin(max_call_depth, graph) + + plugins: list[util.Plugin] = [ + graph_plugin, + app_plugins.LibMiscPlugin(arg_c_fnames=[]), + app_plugins.PicoFmtPlugin("rp2040", [BaseName("lib9p_msg_Rread_format")]), + ] + + 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=util.PluginApplication(testutil.nop_location_xform, plugins), + cfg_max_call_depth=max_call_depth, ) + + graph_plugin.assert_nstatic(result.groups["Main"].rows[QName("main")].nstatic, exp) diff --git a/build-aux/measurestack/testutil.py b/build-aux/measurestack/testutil.py new file mode 100644 index 0000000..751e57f --- /dev/null +++ b/build-aux/measurestack/testutil.py @@ -0,0 +1,131 @@ +# build-aux/measurestack/testutil.py - Utilities for writing tests +# +# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com> +# SPDX-License-Identifier: AGPL-3.0-or-later + +import typing + +from . import analyze, util + +# pylint: disable=unused-variable +__all__ = [ + "aprime_gen", + "aprime_decompose", + "NopPlugin", + "GraphProviderPlugin", + "nop_location_xform", +] + + +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 + + +class NopPlugin: + def is_intrhandler(self, name: analyze.QName) -> bool: + return False + + def init_array(self) -> typing.Collection[analyze.QName]: + return [] + + def extra_includes(self) -> typing.Collection[analyze.BaseName]: + return [] + + def indirect_callees( + self, loc: str, line: str + ) -> tuple[typing.Collection[analyze.QName], bool] | None: + return None + + def skipmodels(self) -> dict[analyze.BaseName, analyze.SkipModel]: + return {} + + def extra_nodes(self) -> typing.Collection[analyze.Node]: + return [] + + +class GraphProviderPlugin(NopPlugin): + _nodes: typing.Sequence[analyze.Node] + + def __init__( + self, + max_call_depth: int, + graph: typing.Sequence[tuple[str, typing.Collection[str]]], + ) -> None: + seq = aprime_gen(len(graph), max_call_depth) + nodes: list[analyze.Node] = [] + for i, (name, calls) in enumerate(graph): + nodes.append(util.synthetic_node(name, seq[i], calls)) + assert ( + len(graph) + == len(nodes) + == len(set(n.nstatic for n in nodes)) + == len(set(str(n.funcname.base()) for n in nodes)) + ) + self._nodes = nodes + + def extra_nodes(self) -> typing.Collection[analyze.Node]: + return self._nodes + + def decode_nstatic(self, tot: int) -> typing.Collection[str]: + idxs, _ = aprime_decompose([n.nstatic for n in self._nodes], tot) + return [str(self._nodes[i].funcname.base()) for i in idxs] + + def encode_nstatic(self, calls: typing.Collection[str]) -> int: + tot = 0 + d: dict[str, int] = {} + for node in self._nodes: + d[str(node.funcname.base())] = node.nstatic + print(d) + for call in calls: + tot += d[call] + return tot + + def sorted_calls(self, calls: typing.Collection[str]) -> typing.Sequence[str]: + d: dict[str, int] = {} + for node in self._nodes: + d[str(node.funcname.base())] = node.nstatic + + def k(call: str) -> int: + return d[call] + + return sorted(calls, key=k) + + def assert_nstatic(self, act_tot: int, exp_calls: typing.Collection[str]) -> None: + exp_tot = self.encode_nstatic(exp_calls) + if act_tot != exp_tot: + act_str = f"{act_tot}: {self.sorted_calls(self.decode_nstatic(act_tot))}" + exp_str = f"{exp_tot}: {self.sorted_calls(exp_calls)}" + assert ( + False + ), f"act:{act_tot} != exp:{exp_tot}\n\t-exp = {exp_str}\n\t+act = {act_str}" + + +def nop_location_xform(loc: str) -> str: + return loc |