mobench 0.1.33

Rust mobile benchmark CLI with CI contract outputs and BrowserStack automation
Documentation
import importlib.util
import json
import math
import pathlib
import sys
import tempfile
import types
import unittest
from unittest import mock


def load_module():
    path = pathlib.Path(__file__).resolve().parents[1] / "render_sina_plot.py"
    spec = importlib.util.spec_from_file_location("render_sina_plot", path)
    module = importlib.util.module_from_spec(spec)
    assert spec.loader is not None
    spec.loader.exec_module(module)
    return module


class PackStripTests(unittest.TestCase):
    def test_pack_strip_is_deterministic_and_centered(self):
        mod = load_module()
        xs = mod.pack_strip([10.0, 10.0, 10.0, 10.2], epsilon=0.12, step=0.02, max_width=0.4)
        self.assertEqual(
            xs,
            mod.pack_strip([10.0, 10.0, 10.0, 10.2], epsilon=0.12, step=0.02, max_width=0.4),
        )
        self.assertAlmostEqual(sum(xs), 0.0, places=6)

    def test_pack_strip_respects_minimum_distance(self):
        mod = load_module()
        ys = [1.0, 1.0, 1.05, 1.07, 1.10]
        xs = mod.pack_strip(ys, epsilon=0.10, step=0.01, max_width=0.4)
        for i, (x1, y1) in enumerate(zip(xs, ys)):
            for x2, y2 in zip(xs[i + 1 :], ys[i + 1 :]):
                self.assertGreaterEqual(math.hypot(x2 - x1, y2 - y1), 0.10 - 1e-9)

    def test_pack_strip_raises_when_strip_is_saturated(self):
        mod = load_module()
        with self.assertRaises(ValueError):
            mod.pack_strip([0.0, 0.0], epsilon=0.20, step=0.05, max_width=0.10)


class CliTests(unittest.TestCase):
    def test_main_dispatches_input_json_to_renderer(self):
        mod = load_module()
        payload = {
            "function_name": "nullifier-proof-generation",
            "function_label": "Nullifier proof generation",
            "target": "benchmark-1",
            "devices": [
                {
                    "device_name": "iPhone 15",
                    "os_version": "iOS 17.4",
                    "samples_ns": [10_000_000, 11_000_000],
                }
            ],
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = pathlib.Path(tmpdir)
            input_path = tmpdir_path / "input.json"
            output_path = tmpdir_path / "output.svg"
            input_path.write_text(json.dumps(payload), encoding="utf-8")

            with mock.patch.object(mod, "render_plot") as render_plot:
                exit_code = mod.main([
                    "--input",
                    str(input_path),
                    "--output",
                    str(output_path),
                ])

        self.assertEqual(exit_code, 0)
        render_plot.assert_called_once()
        called_spec, called_output = render_plot.call_args.args
        self.assertEqual(called_spec, payload)
        self.assertEqual(called_output, output_path)


class FakeAxes:
    def __init__(self):
        self.scatter_calls = []
        self.hline_calls = []
        self.title = None
        self.xlabel = None
        self.ylabel = None
        self.xticks = None
        self.xticklabels = None
        self.xlim = None
        self.margins_args = None
        self.ylim = None

    def scatter(self, x, y, **kwargs):
        self.scatter_calls.append((list(x), list(y), kwargs))

    def hlines(self, y, xmin, xmax, **kwargs):
        self.hline_calls.append((y, xmin, xmax, kwargs))

    def set_title(self, value):
        self.title = value

    def set_xlabel(self, value):
        self.xlabel = value

    def set_ylabel(self, value):
        self.ylabel = value

    def set_xticks(self, values):
        self.xticks = list(values)

    def set_xticklabels(self, values, **kwargs):
        self.xticklabels = list(values)

    def set_xlim(self, left, right):
        self.xlim = (left, right)

    def margins(self, **kwargs):
        self.margins_args = kwargs

    def set_ylim(self, bottom, top):
        self.ylim = (bottom, top)


class FakeFigure:
    def __init__(self):
        self.saved = None

    def savefig(self, path, **kwargs):
        self.saved = (path, kwargs)


class FakeStyle:
    def __init__(self):
        self.paths = []

    def use(self, path):
        self.paths.append(path)


class FakePyplot(types.SimpleNamespace):
    def __init__(self):
        super().__init__()
        self.style = FakeStyle()
        self.figure = FakeFigure()
        self.axes = FakeAxes()

    def subplots(self, figsize=None):
        self.figsize = figsize
        return self.figure, self.axes

    def get_cmap(self, name):
        self.cmap_name = name

        def color(idx):
            return f"color-{idx}"

        return color

    def close(self, fig):
        self.closed = fig


class ValidationTests(unittest.TestCase):
    def test_render_plot_rejects_device_with_empty_samples(self):
        mod = load_module()
        spec = {
            "function_name": "nullifier-proof-generation",
            "function_label": "Nullifier proof generation",
            "target": "benchmark-1",
            "devices": [
                {
                    "device_name": "iPhone 15",
                    "os_version": "iOS 17.4",
                    "samples_ns": [10_000_000, 11_000_000],
                },
                {
                    "device_name": "Pixel 8",
                    "os_version": "Android 15",
                    "samples_ns": [],
                },
            ],
        }

        with self.assertRaises(ValueError):
            mod.render_plot(spec, pathlib.Path("/tmp/out.svg"))


class LayoutTests(unittest.TestCase):
    def test_render_plot_expands_dense_columns_without_overlap(self):
        mod = load_module()
        dense_samples = [10_000_000] * 30
        spec = {
            "function_name": "nullifier-proof-generation",
            "function_label": "Nullifier proof generation",
            "target": "benchmark-1",
            "devices": [
                {
                    "device_name": "iPhone 15",
                    "os_version": "iOS 17.4",
                    "samples_ns": dense_samples,
                },
                {
                    "device_name": "Pixel 8",
                    "os_version": "Android 15",
                    "samples_ns": dense_samples,
                },
            ],
        }

        fake_pyplot = FakePyplot()
        fake_matplotlib = types.ModuleType("matplotlib")
        fake_matplotlib.__path__ = []
        fake_matplotlib.use = lambda backend: None
        with mock.patch.dict(
            sys.modules,
            {
                "matplotlib": fake_matplotlib,
                "matplotlib.pyplot": fake_pyplot,
            },
        ):
            mod.render_plot(spec, pathlib.Path("/tmp/out.svg"))

        self.assertEqual(len(fake_pyplot.axes.scatter_calls), 2)
        first_xs = fake_pyplot.axes.scatter_calls[0][0]
        second_xs = fake_pyplot.axes.scatter_calls[1][0]
        self.assertLess(max(first_xs), min(second_xs))

    def test_render_plot_centers_median_marker_on_adaptive_device_center(self):
        mod = load_module()
        dense_samples = [10_000_000] * 30
        spec = {
            "function_name": "nullifier-proof-generation",
            "function_label": "Nullifier proof generation",
            "target": "benchmark-1",
            "devices": [
                {
                    "device_name": "iPhone 15",
                    "os_version": "iOS 17.4",
                    "samples_ns": dense_samples,
                },
                {
                    "device_name": "Pixel 8",
                    "os_version": "Android 15",
                    "samples_ns": dense_samples,
                },
            ],
        }

        fake_pyplot = FakePyplot()
        fake_matplotlib = types.ModuleType("matplotlib")
        fake_matplotlib.__path__ = []
        fake_matplotlib.use = lambda backend: None
        with mock.patch.dict(
            sys.modules,
            {
                "matplotlib": fake_matplotlib,
                "matplotlib.pyplot": fake_pyplot,
            },
        ):
            mod.render_plot(spec, pathlib.Path("/tmp/out.svg"))

        self.assertEqual(len(fake_pyplot.axes.scatter_calls), 2)
        self.assertEqual(len(fake_pyplot.axes.hline_calls), 2)
        for scatter_call, hline_call in zip(
            fake_pyplot.axes.scatter_calls, fake_pyplot.axes.hline_calls
        ):
            xs, _, _ = scatter_call
            _, xmin, xmax, _ = hline_call
            expected_center = sum(xs) / len(xs)
            self.assertAlmostEqual((xmin + xmax) / 2.0, expected_center, places=6)


@unittest.skipIf(importlib.util.find_spec("matplotlib") is None, "matplotlib not installed")
class MatplotlibSmokeTests(unittest.TestCase):
    def test_render_plot_writes_svg(self):
        mod = load_module()
        spec = {
            "function_name": "nullifier-proof-generation",
            "function_label": "Nullifier proof generation",
            "target": "benchmark-1",
            "devices": [
                {
                    "device_name": "iPhone 15",
                    "os_version": "iOS 17.4",
                    "samples_ns": [10_000_000, 10_100_000, 10_200_000],
                }
            ],
        }

        with tempfile.TemporaryDirectory() as tmpdir:
            output_path = pathlib.Path(tmpdir) / "plot.svg"
            mod.render_plot(spec, output_path)
            self.assertTrue(output_path.exists())
            svg = output_path.read_text(encoding="utf-8")
            self.assertIn("<svg", svg)