summaryrefslogtreecommitdiff
path: root/build-aux
diff options
context:
space:
mode:
Diffstat (limited to 'build-aux')
-rw-r--r--build-aux/measurestack/app_main.py2
-rw-r--r--build-aux/measurestack/app_plugins.py26
-rw-r--r--build-aux/measurestack/test_analyze.py55
-rw-r--r--build-aux/measurestack/test_app_plugins.py468
-rw-r--r--build-aux/measurestack/testutil.py131
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