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()