diff options
author | Luke T. Shumaker <lukeshu@lukeshu.com> | 2025-05-09 02:04:44 -0600 |
---|---|---|
committer | Luke T. Shumaker <lukeshu@lukeshu.com> | 2025-05-10 16:51:43 -0600 |
commit | 0d737cd7043caf20a9015896d42218ae0daf9f5d (patch) | |
tree | 5a754f88c1bde545dd21d89debfbf706ccde448f | |
parent | fdc090bb2ee51f0f6e8a10d152ac0df22784c2ac (diff) |
measurestack: test_app_plugins.py: Pull out a testutil.py
-rw-r--r-- | build-aux/measurestack/test_app_plugins.py | 285 | ||||
-rw-r--r-- | build-aux/measurestack/testutil.py | 131 |
2 files changed, 242 insertions, 174 deletions
diff --git a/build-aux/measurestack/test_app_plugins.py b/build-aux/measurestack/test_app_plugins.py index 8aa0a6c..d7e95b1 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: @@ -234,26 +182,15 @@ 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) 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 |