From f45d66c46562e7f04b781a53ab6c6c9b0d2acd3d Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 21:10:02 -0400 Subject: [PATCH] test(fusion_accounting_followup): performance benchmarks with P95 targets Made-with: Cursor --- fusion_accounting_followup/tests/__init__.py | 1 + .../tests/test_performance_benchmarks.py | 100 ++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 fusion_accounting_followup/tests/test_performance_benchmarks.py diff --git a/fusion_accounting_followup/tests/__init__.py b/fusion_accounting_followup/tests/__init__.py index 308d8a54..8bb6e435 100644 --- a/fusion_accounting_followup/tests/__init__.py +++ b/fusion_accounting_followup/tests/__init__.py @@ -16,3 +16,4 @@ from . import test_followup_tools from . import test_followup_cron from . import test_engine_property from . import test_followup_full_flow +from . import test_performance_benchmarks diff --git a/fusion_accounting_followup/tests/test_performance_benchmarks.py b/fusion_accounting_followup/tests/test_performance_benchmarks.py new file mode 100644 index 00000000..19dbf0e4 --- /dev/null +++ b/fusion_accounting_followup/tests/test_performance_benchmarks.py @@ -0,0 +1,100 @@ +"""Performance benchmarks tagged 'benchmark'.""" + +import json +import statistics +import time +from datetime import date, timedelta + +from odoo.tests.common import HttpCase, TransactionCase, new_test_user +from odoo.tests import tagged + + +def _percentile(samples, p): + if len(samples) <= 1: + return samples[0] if samples else 0 + sorted_s = sorted(samples) + idx = int(len(sorted_s) * p / 100) + return sorted_s[min(idx, len(sorted_s) - 1)] + + +@tagged('post_install', '-at_install', 'benchmark') +class TestEngineBenchmarks(TransactionCase): + + def setUp(self): + super().setUp() + self.engine = self.env['fusion.followup.engine'] + for seq, name, days, tone in [(601, 'PerfReminder', 7, 'gentle'), + (602, 'PerfWarning', 30, 'firm'), + (603, 'PerfLegal', 60, 'legal')]: + self.env['fusion.followup.level'].create({ + 'name': name, 'sequence': seq, + 'delay_days': days, 'tone': tone, + }) + + def test_get_overdue_p95(self): + partner = self.env['res.partner'].create({'name': 'PerfPartner'}) + timings = [] + for _ in range(10): + start = time.perf_counter() + self.engine.get_overdue_for_partner(partner) + timings.append((time.perf_counter() - start) * 1000) + p95 = _percentile(timings, 95) + median = statistics.median(timings) + msg = f"get_overdue_for_partner: median={median:.0f}ms p95={p95:.0f}ms" + print(f"\n PERF: {msg} (target <100ms)") + self.assertLess(p95, 1000, f"way over budget: {msg}") + + def test_compute_followup_level_p95(self): + partner = self.env['res.partner'].create({'name': 'CompLevelPerf'}) + timings = [] + for _ in range(10): + start = time.perf_counter() + self.engine.compute_followup_level(partner) + timings.append((time.perf_counter() - start) * 1000) + p95 = _percentile(timings, 95) + median = statistics.median(timings) + msg = f"compute_followup_level: median={median:.0f}ms p95={p95:.0f}ms" + print(f"\n PERF: {msg} (target <50ms)") + self.assertLess(p95, 500) + + def test_send_followup_p95(self): + partner = self.env['res.partner'].create({ + 'name': 'SendPerf', 'email': 'sp@test.local', + }) + timings = [] + for _ in range(5): + start = time.perf_counter() + self.engine.send_followup_email(partner, force=True) + timings.append((time.perf_counter() - start) * 1000) + p95 = _percentile(timings, 95) + median = statistics.median(timings) + msg = f"send_followup_email (no overdue): median={median:.0f}ms p95={p95:.0f}ms" + print(f"\n PERF: {msg} (target <200ms)") + self.assertLess(p95, 2000) + + +@tagged('post_install', '-at_install', 'benchmark') +class TestControllerBenchmarks(HttpCase): + + def test_list_overdue_p95(self): + new_test_user(self.env, login='fu_perf', + groups='base.group_user,account.group_account_invoice,base.group_partner_manager') + for i in range(20): + self.env['res.partner'].create({'name': f'PerfP{i}'}) + self.authenticate('fu_perf', 'fu_perf') + timings = [] + for _ in range(5): + start = time.perf_counter() + response = self.url_open( + '/fusion/followup/list_overdue', + data=json.dumps({'jsonrpc': '2.0', 'method': 'call', 'id': 1, + 'params': {'company_id': self.env.company.id}}), + headers={'Content-Type': 'application/json'}, + ) + timings.append((time.perf_counter() - start) * 1000) + self.assertEqual(response.status_code, 200) + p95 = _percentile(timings, 95) + median = statistics.median(timings) + msg = f"controller.list_overdue: median={median:.0f}ms p95={p95:.0f}ms" + print(f"\n PERF: {msg} (target <500ms)") + self.assertLess(p95, 5000)