""" Unit tests for calc.py helper functions. These tests verify pure calculation logic without hitting the database or the Swiss Ephemeris — all inputs are fixed synthetic data. Run: pyswiss/.venv/Scripts/python pyswiss/manage.py test pyswiss/apps/charts """ from django.test import SimpleTestCase from apps.charts.calc import calculate_aspects, get_element_counts # --------------------------------------------------------------------------- # FAKE_PLANETS_ASPECTS — degrees only; used by calculate_aspects tests. # Each planet also carries a speed (deg/day) for applying_planet tests. # --------------------------------------------------------------------------- FAKE_PLANETS = { 'Sun': {'degree': 10.0, 'speed': 1.00}, # Aries 'Moon': {'degree': 130.0, 'speed': 13.00}, # Leo — 120° from Sun → Trine 'Mercury': {'degree': 250.0, 'speed': 1.50}, # Sagittarius — 120° from Sun → Trine 'Venus': {'degree': 40.0, 'speed': 1.10}, # Taurus — 90° from Moon → Square 'Mars': {'degree': 160.0, 'speed': 0.50}, # Virgo — 60° from Neptune → Sextile 'Jupiter': {'degree': 280.0, 'speed': 0.08}, # Capricorn — 120° from Mars → Trine 'Saturn': {'degree': 70.0, 'speed': 0.03}, # Gemini — 120° from Uranus → Trine 'Uranus': {'degree': 310.0, 'speed': 0.01}, # Aquarius — 60° from Sun (wrap) → Sextile 'Neptune': {'degree': 100.0, 'speed': 0.006}, # Cancer 'Pluto': {'degree': 340.0, 'speed': 0.003}, # Pisces } # --------------------------------------------------------------------------- # FAKE_PLANETS_ELEMENTS — sign + degree + speed; used by get_element_counts. # Designed to produce a known stellium and parade. # # Occupied signs: Aries(0), Taurus(1), Gemini(2), Leo(4), Virgo(5), # Scorpio(7), Capricorn(9), Aquarius(10) # Gaps at Cancer(3), Libra(6), Sagittarius(8), Pisces(11) prevent wrap-around. # # Consecutive runs: Aries→Taurus→Gemini = 3 ← parade (Space = 2) # Leo→Virgo = 2 # Capricorn→Aquarius = 2 # # Time = 2 (Aries has Sun+Mercury+Venus → stellium of 3, bonus = 2) # Space = 2 (Aries→Taurus→Gemini = 3-sign parade, bonus = 2) # Classic: Fire=4, Earth=3, Air=2, Water=1 # --------------------------------------------------------------------------- FAKE_PLANETS_ELEMENTS = { 'Sun': {'sign': 'Aries', 'degree': 10.0, 'speed': 1.00}, # Fire, stellium 'Moon': {'sign': 'Taurus', 'degree': 40.0, 'speed': 13.00}, # Earth, parade 'Mercury': {'sign': 'Aries', 'degree': 20.0, 'speed': 1.50}, # Fire, stellium 'Venus': {'sign': 'Aries', 'degree': 25.0, 'speed': 1.10}, # Fire, stellium 'Mars': {'sign': 'Leo', 'degree': 130.0, 'speed': 0.50}, # Fire 'Jupiter': {'sign': 'Scorpio', 'degree': 220.0, 'speed': 0.08}, # Water 'Saturn': {'sign': 'Gemini', 'degree': 70.0, 'speed': 0.03}, # Air, parade 'Uranus': {'sign': 'Aquarius', 'degree': 310.0, 'speed': 0.01}, # Air 'Neptune': {'sign': 'Capricorn', 'degree': 270.0, 'speed': 0.006}, # Earth 'Pluto': {'sign': 'Virgo', 'degree': 160.0, 'speed': 0.003}, # Earth } def _aspect_pairs(aspects): """Return a set of (planet1, planet2, type) tuples for easy assertion.""" return {(a['planet1'], a['planet2'], a['type']) for a in aspects} # =========================================================================== # get_element_counts — enriched shape # =========================================================================== class GetElementCountsTest(SimpleTestCase): def setUp(self): self.counts = get_element_counts(FAKE_PLANETS_ELEMENTS) # ── top-level keys ─────────────────────────────────────────────────────── def test_returns_all_six_elements(self): for key in ('Fire', 'Earth', 'Air', 'Water', 'Time', 'Space'): with self.subTest(key=key): self.assertIn(key, self.counts) # ── classic four — count + contributors ────────────────────────────────── def test_classic_element_has_count_key(self): self.assertIn('count', self.counts['Fire']) def test_classic_element_has_contributors_key(self): self.assertIn('contributors', self.counts['Fire']) def test_fire_count_is_correct(self): # Sun + Mercury + Venus (Aries) + Mars (Leo) = 4 self.assertEqual(self.counts['Fire']['count'], 4) def test_earth_count_is_correct(self): # Moon (Taurus) + Neptune (Capricorn) + Pluto (Virgo) = 3 self.assertEqual(self.counts['Earth']['count'], 3) def test_air_count_is_correct(self): # Saturn (Gemini) + Uranus (Aquarius) = 2 self.assertEqual(self.counts['Air']['count'], 2) def test_water_count_is_correct(self): # Jupiter (Scorpio) = 1 self.assertEqual(self.counts['Water']['count'], 1) def test_fire_contributors_contains_expected_planets(self): planets = {c['planet'] for c in self.counts['Fire']['contributors']} self.assertEqual(planets, {'Sun', 'Mercury', 'Venus', 'Mars'}) def test_contributor_has_planet_and_sign_keys(self): contrib = self.counts['Fire']['contributors'][0] self.assertIn('planet', contrib) self.assertIn('sign', contrib) def test_fire_contributor_signs_are_correct(self): sign_map = {c['planet']: c['sign'] for c in self.counts['Fire']['contributors']} self.assertEqual(sign_map['Sun'], 'Aries') self.assertEqual(sign_map['Mercury'], 'Aries') self.assertEqual(sign_map['Venus'], 'Aries') self.assertEqual(sign_map['Mars'], 'Leo') # ── Time — count + stellia ─────────────────────────────────────────────── def test_time_has_count_key(self): self.assertIn('count', self.counts['Time']) def test_time_has_stellia_key(self): self.assertIn('stellia', self.counts['Time']) def test_time_count_is_correct(self): # Aries has 3 planets → bonus = 2 self.assertEqual(self.counts['Time']['count'], 2) def test_time_stellia_is_a_list(self): self.assertIsInstance(self.counts['Time']['stellia'], list) def test_time_stellia_contains_one_entry(self): self.assertEqual(len(self.counts['Time']['stellia']), 1) def test_time_stellium_sign_is_aries(self): self.assertEqual(self.counts['Time']['stellia'][0]['sign'], 'Aries') def test_time_stellium_planets_are_correct(self): planet_names = {p['planet'] for p in self.counts['Time']['stellia'][0]['planets']} self.assertEqual(planet_names, {'Sun', 'Mercury', 'Venus'}) def test_time_stellium_planet_entries_have_sign(self): for entry in self.counts['Time']['stellia'][0]['planets']: with self.subTest(planet=entry['planet']): self.assertEqual(entry['sign'], 'Aries') # ── Space — count + parades ────────────────────────────────────────────── def test_space_has_count_key(self): self.assertIn('count', self.counts['Space']) def test_space_has_parades_key(self): self.assertIn('parades', self.counts['Space']) def test_space_count_is_correct(self): # Aries→Taurus→Gemini = 3 consecutive → bonus = 2 self.assertEqual(self.counts['Space']['count'], 2) def test_space_parades_is_a_list(self): self.assertIsInstance(self.counts['Space']['parades'], list) def test_space_parades_contains_one_entry(self): self.assertEqual(len(self.counts['Space']['parades']), 1) def test_space_parade_signs_are_correct(self): self.assertEqual( self.counts['Space']['parades'][0]['signs'], ['Aries', 'Taurus', 'Gemini'], ) def test_space_parade_planets_are_correct(self): planet_names = {p['planet'] for p in self.counts['Space']['parades'][0]['planets']} self.assertEqual(planet_names, {'Sun', 'Mercury', 'Venus', 'Moon', 'Saturn'}) def test_space_parade_planet_entries_have_planet_and_sign(self): for entry in self.counts['Space']['parades'][0]['planets']: with self.subTest(planet=entry['planet']): self.assertIn('planet', entry) self.assertIn('sign', entry) # =========================================================================== # calculate_aspects # =========================================================================== class CalculateAspectsTest(SimpleTestCase): def setUp(self): self.aspects = calculate_aspects(FAKE_PLANETS) # ── return shape ────────────────────────────────────────────────────── def test_returns_a_list(self): self.assertIsInstance(self.aspects, list) def test_each_aspect_has_required_keys(self): for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertIn('planet1', aspect) self.assertIn('planet2', aspect) self.assertIn('type', aspect) self.assertIn('angle', aspect) self.assertIn('orb', aspect) def test_each_aspect_has_applying_planet_key(self): for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertIn('applying_planet', aspect) def test_applying_planet_is_one_of_the_pair(self): for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertIn( aspect['applying_planet'], (aspect['planet1'], aspect['planet2']), ) def test_applying_planet_is_the_faster_body(self): """Moon (13.0°/day) applies to Sun (1.0°/day) in their Trine.""" sun_moon = next( a for a in self.aspects if {a['planet1'], a['planet2']} == {'Sun', 'Moon'} ) self.assertEqual(sun_moon['applying_planet'], 'Moon') def test_each_aspect_type_is_a_known_name(self): known = { 'Conjunction', 'Semisextile', 'Sextile', 'Square', 'Trine', 'Quincunx', 'Opposition', } for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertIn(aspect['type'], known) def test_angle_and_orb_are_floats(self): for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertIsInstance(aspect['angle'], float) self.assertIsInstance(aspect['orb'], float) def test_no_self_aspects(self): for aspect in self.aspects: self.assertNotEqual(aspect['planet1'], aspect['planet2']) def test_no_duplicate_pairs(self): pairs = [(a['planet1'], a['planet2']) for a in self.aspects] self.assertEqual(len(pairs), len(set(pairs))) # ── known aspects in FAKE_PLANETS ──────────────────────────────────── def test_sun_moon_trine(self): """Moon at 130° is exactly 120° from Sun at 10°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Sun', 'Moon', 'Trine'), pairs) def test_sun_mercury_trine(self): """Mercury at 250° wraps to 120° from Sun at 10° (360-250+10=120).""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Sun', 'Mercury', 'Trine'), pairs) def test_moon_mercury_trine(self): """Moon 130° → Mercury 250° = 120°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Moon', 'Mercury', 'Trine'), pairs) def test_moon_venus_square(self): """Moon 130° → Venus 40° = 90°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Moon', 'Venus', 'Square'), pairs) def test_venus_neptune_sextile(self): """Venus 40° → Neptune 100° = 60°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Venus', 'Neptune', 'Sextile'), pairs) def test_mars_neptune_sextile(self): """Mars 160° → Neptune 100° = 60°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Mars', 'Neptune', 'Sextile'), pairs) def test_sun_uranus_sextile(self): """Sun 10° → Uranus 310° — angle = |10-310| = 300° → 360-300 = 60°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Sun', 'Uranus', 'Sextile'), pairs) def test_mars_jupiter_trine(self): """Mars 160° → Jupiter 280° = 120°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Mars', 'Jupiter', 'Trine'), pairs) def test_saturn_uranus_trine(self): """Saturn 70° → Uranus 310° = |70-310| = 240° → 360-240 = 120°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Saturn', 'Uranus', 'Trine'), pairs) # ── orb bounds ──────────────────────────────────────────────────────── def test_orb_is_within_allowed_maximum(self): max_orbs = { 'Conjunction': 8.0, 'Semisextile': 4.0, 'Sextile': 6.0, 'Square': 8.0, 'Trine': 8.0, 'Quincunx': 5.0, 'Opposition': 10.0, } for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertLessEqual( aspect['orb'], max_orbs[aspect['type']], msg=f"{aspect['planet1']}-{aspect['planet2']} orb exceeds maximum", ) def test_exact_trine_has_zero_orb(self): """Sun-Moon at exactly 120° should report orb of 0.0.""" sun_moon = next( a for a in self.aspects if a['planet1'] == 'Sun' and a['planet2'] == 'Moon' ) self.assertAlmostEqual(sun_moon['orb'], 0.0, places=5)