runrate-lab / tests /test_runrate_lab.py
ramanaprabhusana's picture
Deploy RunRate Lab cricket analytics coach
16a19cb verified
import unittest
from streamlit.testing.v1 import AppTest
import app
class RunRateLabAnalyticsTests(unittest.TestCase):
def test_probabilities_sum_to_approximately_100(self):
probabilities, metrics = app.simulate_outcome_distribution(
"Length Ball",
timing_error_ms=0,
power=70,
required_rate=8.5,
samples=5000,
)
self.assertAlmostEqual(probabilities["Probability"].sum(), 100.0, places=6)
self.assertGreater(metrics["expected_runs"], 0)
self.assertEqual(metrics["shot_intent"], "Drive")
self.assertEqual(metrics["aim_lane"], "Straight")
def test_perfect_timing_beats_large_timing_error(self):
pitch = app.get_pitch("Length Ball")
perfect_contact = app.calculate_contact(0, 70, pitch)
late_contact = app.calculate_contact(180, 70, pitch)
self.assertGreater(perfect_contact, late_contact)
def test_required_rate_increases_wicket_risk(self):
pitch = app.get_pitch("Cutter")
contact = app.calculate_contact(20, 85, pitch)
normal_risk = app.calculate_wicket_risk(contact, 85, 20, 8.0, pitch)
pressure_risk = app.calculate_wicket_risk(contact, 85, 20, 12.0, pitch)
self.assertGreater(pressure_risk, normal_risk)
def test_overcharged_power_increases_wicket_risk(self):
pitch = app.get_pitch("Bouncer")
contact = app.calculate_contact(0, 95, pitch)
normal_risk = app.calculate_wicket_risk(contact, 80, 0, 8.0, pitch)
overhit_risk = app.calculate_wicket_risk(contact, 95, 0, 8.0, pitch)
self.assertGreater(overhit_risk, normal_risk)
def test_loft_has_higher_upside_and_risk_than_defend(self):
pitch = app.get_pitch("Half Volley")
defend_contact = app.calculate_contact(0, 88, pitch, "Defend", "Straight")
defend_risk = app.calculate_wicket_risk(defend_contact, 88, 0, 8.0, pitch, "Defend", "Straight")
defend_xruns = app.calculate_expected_runs(defend_contact, 88, defend_risk, "Defend", "Straight")
loft_contact = app.calculate_contact(0, 88, pitch, "Loft", "Straight")
loft_risk = app.calculate_wicket_risk(loft_contact, 88, 0, 8.0, pitch, "Loft", "Straight")
loft_xruns = app.calculate_expected_runs(loft_contact, 88, loft_risk, "Loft", "Straight")
self.assertGreater(loft_risk, defend_risk)
self.assertGreater(loft_xruns, defend_xruns)
def test_correct_shot_delivery_matchup_improves_contact(self):
pitch = app.get_pitch("Bouncer")
drive_contact = app.calculate_contact(0, 76, pitch, "Drive", "Straight")
pull_contact = app.calculate_contact(0, 76, pitch, "Cut / Pull", "Straight")
self.assertGreater(pull_contact, drive_contact)
def test_decision_quality_thresholds(self):
self.assertEqual(app.classify_decision_quality(4.5, 0.12), "Excellent Process")
self.assertEqual(app.classify_decision_quality(3.0, 0.20), "Good Process")
self.assertEqual(app.classify_decision_quality(4.0, 0.35), "Risky But Rational")
self.assertEqual(app.classify_decision_quality(1.2, 0.18), "Low-Value Shot")
self.assertEqual(app.classify_decision_quality(1.9, 0.35), "Poor Risk Tradeoff")
def test_luck_classification(self):
self.assertEqual(app.classify_luck(6, 2.0, False, 0.15), "Lucky Result")
self.assertEqual(app.classify_luck(0, 4.0, False, 0.12), "Unlucky Result")
self.assertEqual(app.classify_luck(0, 3.0, True, 0.10), "Unlucky Result")
self.assertEqual(app.classify_luck(2, 2.2, False, 0.15), "Expected Result")
def test_generated_game_model_uses_python_constants(self):
rendered = app.build_game_html(180)
self.assertNotIn("__DELIVERIES_JSON__", rendered)
self.assertNotIn("__SHOTS_JSON__", rendered)
self.assertNotIn("__AIMS_JSON__", rendered)
for delivery in app.PITCH_MODEL["Delivery"]:
self.assertIn(delivery, rendered)
for shot in app.SHOT_MODEL["Shot"]:
self.assertIn(shot, rendered)
for aim in app.AIM_MODEL["Aim"]:
self.assertIn(aim, rendered)
def test_sample_table_has_ten_trials(self):
table = app.simulate_sample_table("Yorker", 0, 70, 9.0, shot_intent="Defend", aim_lane="Straight")
self.assertEqual(len(table), 10)
self.assertIn("Shot", table.columns)
self.assertIn("Aim", table.columns)
self.assertIn("Decision", table.columns)
def test_learning_summary_handles_partial_logs(self):
summary = app.build_learning_summary([{"actual_runs": 4}, {"unexpected": 99}])
self.assertEqual(summary["balls"], 2.0)
self.assertEqual(summary["avg_xruns"], 0.0)
self.assertEqual(summary["avg_wicket_risk"], 0.0)
self.assertEqual(summary["avg_actual_runs"], 4.0)
def test_game_pauses_for_review_between_balls(self):
self.assertIn('id="nextBallBtn"', app.GAME_HTML)
self.assertIn('state.mode = "between"', app.GAME_HTML)
self.assertIn('state.mode === "between" ? "Review"', app.GAME_HTML)
self.assertIn("Shot Intent", app.GAME_HTML)
self.assertIn("Aim Lane", app.GAME_HTML)
self.assertIn("Process Score", app.GAME_HTML)
self.assertIn('id="processPill"', app.GAME_HTML)
self.assertIn('id="settingsModal"', app.GAME_HTML)
self.assertIn("Reduce Flashing", app.GAME_HTML)
self.assertIn("hidden-during-play", app.GAME_HTML)
self.assertIn("hidden-for-review", app.GAME_HTML)
self.assertIn("Coach ready. Press Next Ball when done.", app.GAME_HTML)
self.assertIn("Restart Match", app.GAME_HTML)
class RunRateLabStreamlitTests(unittest.TestCase):
def test_streamlit_app_renders(self):
test_app = AppTest.from_file("app.py")
test_app.run(timeout=15)
self.assertFalse(test_app.exception)
self.assertTrue(any("RunRate Lab" in title.value for title in test_app.title))
self.assertGreaterEqual(len(test_app.metric), 4)
if __name__ == "__main__":
unittest.main()