Improve space launch email, add tests.
This commit is contained in:
parent
808f5c1d22
commit
ebceb4cb51
|
@ -221,12 +221,11 @@ def format_probability_change(old_val: int, new_val: int) -> str:
|
|||
return f"Launch probability changed from {old_val}% to {new_val}%"
|
||||
|
||||
|
||||
def handle_value_changes(differences: StrDict) -> tuple[list[str], set[str]]:
|
||||
"""Handle value changes in launch data."""
|
||||
def format_launch_changes(differences: StrDict) -> str:
|
||||
"""Convert deepdiff output to human-readable format."""
|
||||
changes: list[str] = []
|
||||
processed_paths: set[str] = set()
|
||||
|
||||
# Skip fields that aren't useful to users
|
||||
SKIP_FIELDS = {
|
||||
"agency_launch_attempt_count",
|
||||
"agency_launch_attempt_count_year",
|
||||
|
@ -238,84 +237,84 @@ def handle_value_changes(differences: StrDict) -> tuple[list[str], set[str]]:
|
|||
"orbital_launch_attempt_count_year",
|
||||
}
|
||||
|
||||
if "values_changed" not in differences:
|
||||
return changes, processed_paths
|
||||
# --- 1. Handle Special Group Value Changes ---
|
||||
# Process high-level, user-friendly summaries first.
|
||||
values = differences.get("values_changed", {})
|
||||
if "root['status']['name']" in values:
|
||||
old_val = values["root['status']['name']"]["old_value"]
|
||||
new_val = values["root['status']['name']"]["new_value"]
|
||||
changes.append(f"Status changed from '{old_val}' to '{new_val}'")
|
||||
processed_paths.add("root['status']")
|
||||
|
||||
for path, change in differences["values_changed"].items():
|
||||
if any(path.startswith(processed) for processed in processed_paths):
|
||||
if "root['net_precision']['name']" in values:
|
||||
old_val = values["root['net_precision']['name']"]["old_value"]
|
||||
new_val = values["root['net_precision']['name']"]["new_value"]
|
||||
changes.append(f"Launch precision changed from '{old_val}' to '{new_val}'")
|
||||
processed_paths.add("root['net_precision']")
|
||||
|
||||
# --- 2. Handle Type Changes ---
|
||||
# This is often more significant than a value change (e.g., probability becoming None).
|
||||
if "type_changes" in differences:
|
||||
for path, change in differences["type_changes"].items():
|
||||
if any(path.startswith(p) for p in processed_paths):
|
||||
continue
|
||||
|
||||
field = path.replace("root['", "").replace("']", "").replace("root.", "")
|
||||
|
||||
if field == "probability":
|
||||
# Use custom formatter only for meaningful None transitions.
|
||||
if change["old_type"] is type(None) or change["new_type"] is type(None):
|
||||
changes.append(
|
||||
format_probability_change(
|
||||
change["old_value"], change["new_value"]
|
||||
)
|
||||
)
|
||||
else: # For other type changes (e.g., int to str), use the generic message.
|
||||
changes.append(
|
||||
f"{field.replace('_', ' ').title()} type changed "
|
||||
+ f"from {change['old_type'].__name__} to {change['new_type'].__name__}"
|
||||
)
|
||||
else:
|
||||
changes.append(
|
||||
f"{field.replace('_', ' ').title()} type changed "
|
||||
+ f"from {change['old_type'].__name__} to {change['new_type'].__name__}"
|
||||
)
|
||||
processed_paths.add(path)
|
||||
|
||||
# --- 3. Handle Remaining Value Changes ---
|
||||
for path, change in values.items():
|
||||
if any(path.startswith(p) for p in processed_paths):
|
||||
continue
|
||||
|
||||
field = path.replace("root['", "").replace("']", "").replace("root.", "")
|
||||
if field in SKIP_FIELDS:
|
||||
continue
|
||||
|
||||
old_val = change["old_value"]
|
||||
new_val = change["new_value"]
|
||||
|
||||
match field:
|
||||
case "status['name']":
|
||||
changes.append(f"Status changed from '{old_val}' to '{new_val}'")
|
||||
processed_paths.update(
|
||||
[
|
||||
"root['status']['id']",
|
||||
"root['status']['name']",
|
||||
"root['status']['abbrev']",
|
||||
"root['status']['description']",
|
||||
]
|
||||
)
|
||||
|
||||
case x if x.startswith("status["):
|
||||
continue
|
||||
|
||||
case "net_precision['name']":
|
||||
changes.append(
|
||||
f"Launch precision changed from '{old_val}' to '{new_val}'"
|
||||
)
|
||||
processed_paths.update(
|
||||
[
|
||||
"root['net_precision']['id']",
|
||||
"root['net_precision']['name']",
|
||||
"root['net_precision']['abbrev']",
|
||||
"root['net_precision']['description']",
|
||||
]
|
||||
)
|
||||
|
||||
case x if x.startswith("net_precision["):
|
||||
continue
|
||||
|
||||
case "net":
|
||||
changes.append(format_datetime_change("Launch time", old_val, new_val))
|
||||
|
||||
case "window_start":
|
||||
changes.append(
|
||||
format_datetime_change("Launch window start", old_val, new_val)
|
||||
)
|
||||
|
||||
case "window_end":
|
||||
changes.append(
|
||||
format_datetime_change("Launch window end", old_val, new_val)
|
||||
)
|
||||
|
||||
case "last_updated":
|
||||
changes.append(format_datetime_update("Last updated", new_val))
|
||||
|
||||
case "name":
|
||||
changes.append(f"Mission name changed from '{old_val}' to '{new_val}'")
|
||||
|
||||
case "probability":
|
||||
changes.append(format_probability_change(old_val, new_val))
|
||||
|
||||
case x if x in SKIP_FIELDS:
|
||||
continue
|
||||
|
||||
case _:
|
||||
changes.append(f"{field} changed from '{old_val}' to '{new_val}'")
|
||||
processed_paths.add(path)
|
||||
|
||||
return changes, processed_paths
|
||||
|
||||
|
||||
def format_launch_changes(differences: StrDict) -> str:
|
||||
"""Convert deepdiff output to human-readable format using match/case."""
|
||||
changes, processed_paths = handle_value_changes(differences)
|
||||
|
||||
# Handle other difference types...
|
||||
# --- 4. Handle Added/Removed Fields ---
|
||||
if "dictionary_item_added" in differences:
|
||||
for path in differences["dictionary_item_added"]:
|
||||
field = path.replace("root['", "").replace("']", "").replace("root.", "")
|
||||
|
@ -326,16 +325,9 @@ def format_launch_changes(differences: StrDict) -> str:
|
|||
field = path.replace("root['", "").replace("']", "").replace("root.", "")
|
||||
changes.append(f"Field removed: {field.replace('_', ' ').title()}")
|
||||
|
||||
if "type_changes" in differences:
|
||||
for path, change in differences["type_changes"].items():
|
||||
field = path.replace("root['", "").replace("']", "").replace("root.", "")
|
||||
changes.append(
|
||||
f"{field.replace('_', ' ').title()} type changed "
|
||||
+ f"from {change['old_type'].__name__} to {change['new_type'].__name__}"
|
||||
)
|
||||
|
||||
# Sort changes for deterministic output in tests
|
||||
return (
|
||||
"\n".join(f"• {change}" for change in changes)
|
||||
"\n".join(f"• {change}" for change in sorted(changes))
|
||||
if changes
|
||||
else "No specific changes detected"
|
||||
)
|
||||
|
|
|
@ -1,270 +0,0 @@
|
|||
"""Tests for calendar functionality."""
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from agenda.calendar import build_events, colors, event_type_color_map
|
||||
from agenda.event import Event
|
||||
|
||||
|
||||
class TestEventTypeColorMap:
|
||||
"""Test the event type color mapping."""
|
||||
|
||||
def test_event_type_color_map_contains_expected_types(self) -> None:
|
||||
"""Test that color map contains expected event types."""
|
||||
expected_types = {
|
||||
"bank_holiday", "conference", "us_holiday",
|
||||
"birthday", "waste_schedule"
|
||||
}
|
||||
assert set(event_type_color_map.keys()) == expected_types
|
||||
|
||||
def test_event_type_color_map_values_are_valid(self) -> None:
|
||||
"""Test that all color values are valid color names."""
|
||||
expected_colors = {
|
||||
"success-subtle", "primary-subtle", "secondary-subtle",
|
||||
"info-subtle", "danger-subtle"
|
||||
}
|
||||
assert set(event_type_color_map.values()).issubset(expected_colors)
|
||||
|
||||
|
||||
class TestColorsDict:
|
||||
"""Test the colors dictionary."""
|
||||
|
||||
def test_colors_contains_expected_keys(self) -> None:
|
||||
"""Test that colors dict contains expected color keys."""
|
||||
expected_keys = {
|
||||
"primary-subtle", "secondary-subtle", "success-subtle",
|
||||
"info-subtle", "warning-subtle", "danger-subtle"
|
||||
}
|
||||
assert set(colors.keys()) == expected_keys
|
||||
|
||||
def test_colors_values_are_hex_codes(self) -> None:
|
||||
"""Test that all color values are valid hex codes."""
|
||||
for color_value in colors.values():
|
||||
assert color_value.startswith("#")
|
||||
assert len(color_value) == 7
|
||||
# Check that remaining characters are valid hex
|
||||
hex_part = color_value[1:]
|
||||
int(hex_part, 16) # This will raise ValueError if not valid hex
|
||||
|
||||
|
||||
class TestBuildEvents:
|
||||
"""Test the build_events function."""
|
||||
|
||||
def test_build_events_empty_list(self) -> None:
|
||||
"""Test building events with empty list."""
|
||||
result = build_events([])
|
||||
assert result == []
|
||||
|
||||
def test_build_events_today_event_filtered(self) -> None:
|
||||
"""Test that 'today' events are filtered out."""
|
||||
events = [
|
||||
Event(date=date(2024, 1, 1), name="today", title="Today"),
|
||||
Event(date=date(2024, 1, 2), name="birthday", title="Birthday"),
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["title"] == "🎈 Birthday"
|
||||
|
||||
def test_build_events_simple_event(self) -> None:
|
||||
"""Test building a simple event without time."""
|
||||
events = [
|
||||
Event(
|
||||
date=date(2024, 1, 15),
|
||||
name="birthday",
|
||||
title="John's Birthday"
|
||||
)
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
assert len(result) == 1
|
||||
item = result[0]
|
||||
assert item["allDay"] is True
|
||||
assert item["title"] == "🎈 John's Birthday"
|
||||
assert item["start"] == "2024-01-15"
|
||||
assert item["end"] == "2024-01-16" # Next day
|
||||
assert item["color"] == colors["info-subtle"]
|
||||
assert item["textColor"] == "black"
|
||||
|
||||
def test_build_events_with_time(self) -> None:
|
||||
"""Test building an event with time."""
|
||||
event_datetime = datetime(2024, 1, 15, 14, 30)
|
||||
events = [
|
||||
Event(
|
||||
date=event_datetime,
|
||||
name="meeting",
|
||||
title="Team Meeting"
|
||||
)
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
assert len(result) == 1
|
||||
item = result[0]
|
||||
assert item["allDay"] is False
|
||||
assert item["title"] == "Team Meeting"
|
||||
assert item["start"] == "2024-01-15T14:30:00"
|
||||
assert item["end"] == "2024-01-15T15:00:00" # 30 minutes later
|
||||
|
||||
def test_build_events_with_end_date(self) -> None:
|
||||
"""Test building an event with end date."""
|
||||
events = [
|
||||
Event(
|
||||
date=date(2024, 1, 15),
|
||||
name="conference",
|
||||
title="Tech Conference",
|
||||
end_date=date(2024, 1, 17)
|
||||
)
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
assert len(result) == 1
|
||||
item = result[0]
|
||||
assert item["allDay"] is True
|
||||
assert item["start"] == "2024-01-15"
|
||||
assert item["end"] == "2024-01-18" # End date + 1 day
|
||||
|
||||
def test_build_events_with_time_and_end_date(self) -> None:
|
||||
"""Test building an event with time and end date."""
|
||||
start_datetime = datetime(2024, 1, 15, 9, 0)
|
||||
end_datetime = datetime(2024, 1, 15, 17, 0)
|
||||
events = [
|
||||
Event(
|
||||
date=start_datetime,
|
||||
name="workshop",
|
||||
title="Python Workshop",
|
||||
end_date=end_datetime
|
||||
)
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
assert len(result) == 1
|
||||
item = result[0]
|
||||
assert item["allDay"] is False
|
||||
assert item["start"] == "2024-01-15T09:00:00"
|
||||
assert item["end"] == "2024-01-15T17:00:00"
|
||||
|
||||
def test_build_events_with_url(self) -> None:
|
||||
"""Test building an event with URL."""
|
||||
events = [
|
||||
Event(
|
||||
date=date(2024, 1, 15),
|
||||
name="conference",
|
||||
title="Tech Conference",
|
||||
url="https://example.com"
|
||||
)
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
assert len(result) == 1
|
||||
item = result[0]
|
||||
assert item["url"] == "https://example.com"
|
||||
|
||||
def test_build_events_accommodation(self) -> None:
|
||||
"""Test building accommodation events."""
|
||||
events = [
|
||||
Event(
|
||||
date=datetime(2024, 1, 15, 15, 0), # Check-in time
|
||||
name="accommodation",
|
||||
title="Hotel Stay",
|
||||
end_date=datetime(2024, 1, 17, 11, 0), # Check-out time
|
||||
url="https://hotel.com"
|
||||
)
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
# Should create 3 events: main accommodation + check-in + check-out
|
||||
assert len(result) == 3
|
||||
|
||||
# Main accommodation event
|
||||
main_event = result[0]
|
||||
assert main_event["allDay"] is True
|
||||
assert main_event["title"] == "🏨 Hotel Stay"
|
||||
assert main_event["start"] == "2024-01-15"
|
||||
assert main_event["end"] == "2024-01-18" # End date + 1 day
|
||||
assert main_event["url"] == "https://hotel.com"
|
||||
|
||||
# Check-in event
|
||||
checkin_event = result[1]
|
||||
assert checkin_event["allDay"] is False
|
||||
assert checkin_event["title"] == "check-in: Hotel Stay"
|
||||
assert checkin_event["start"] == "2024-01-15T15:00:00"
|
||||
assert checkin_event["url"] == "https://hotel.com"
|
||||
|
||||
# Check-out event
|
||||
checkout_event = result[2]
|
||||
assert checkout_event["allDay"] is False
|
||||
assert checkout_event["title"] == "checkout: Hotel Stay"
|
||||
assert checkout_event["start"] == "2024-01-17T11:00:00"
|
||||
assert checkout_event["url"] == "https://hotel.com"
|
||||
|
||||
def test_build_events_no_color_for_unknown_type(self) -> None:
|
||||
"""Test that events with unknown types don't get color."""
|
||||
events = [
|
||||
Event(
|
||||
date=date(2024, 1, 15),
|
||||
name="unknown_type",
|
||||
title="Unknown Event"
|
||||
)
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
assert len(result) == 1
|
||||
item = result[0]
|
||||
assert "color" not in item
|
||||
assert "textColor" not in item
|
||||
|
||||
def test_build_events_multiple_event_types(self) -> None:
|
||||
"""Test building events with different types and colors."""
|
||||
events = [
|
||||
Event(date=date(2024, 1, 15), name="bank_holiday", title="New Year"),
|
||||
Event(date=date(2024, 1, 16), name="conference", title="PyCon"),
|
||||
Event(date=date(2024, 1, 17), name="birthday", title="Birthday"),
|
||||
Event(date=date(2024, 1, 18), name="waste_schedule", title="Recycling"),
|
||||
Event(date=date(2024, 1, 19), name="us_holiday", title="MLK Day"),
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
assert len(result) == 5
|
||||
|
||||
# Check colors are assigned correctly
|
||||
assert result[0]["color"] == colors["success-subtle"] # bank_holiday
|
||||
assert result[1]["color"] == colors["primary-subtle"] # conference
|
||||
assert result[2]["color"] == colors["info-subtle"] # birthday
|
||||
assert result[3]["color"] == colors["danger-subtle"] # waste_schedule
|
||||
assert result[4]["color"] == colors["secondary-subtle"] # us_holiday
|
||||
|
||||
# All should have black text
|
||||
for item in result:
|
||||
assert item["textColor"] == "black"
|
||||
|
||||
def test_build_events_mixed_scenarios(self) -> None:
|
||||
"""Test building events with mixed scenarios."""
|
||||
events = [
|
||||
# Today event (should be filtered)
|
||||
Event(date=date(2024, 1, 1), name="today", title="Today"),
|
||||
# Simple event
|
||||
Event(date=date(2024, 1, 15), name="birthday", title="Birthday"),
|
||||
# Event with time
|
||||
Event(date=datetime(2024, 1, 16, 10, 0), name="meeting", title="Meeting"),
|
||||
# Accommodation
|
||||
Event(
|
||||
date=datetime(2024, 1, 20, 15, 0),
|
||||
name="accommodation",
|
||||
title="Hotel",
|
||||
end_date=datetime(2024, 1, 22, 11, 0),
|
||||
url="https://hotel.com"
|
||||
),
|
||||
]
|
||||
result = build_events(events)
|
||||
|
||||
# Should have 5 events total (today filtered, accommodation creates 3)
|
||||
assert len(result) == 5
|
||||
|
||||
# Verify titles
|
||||
titles = [item["title"] for item in result]
|
||||
assert "🎈 Birthday" in titles
|
||||
assert "Meeting" in titles
|
||||
assert "🏨 Hotel" in titles
|
||||
assert "check-in: Hotel" in titles
|
||||
assert "checkout: Hotel" in titles
|
612
tests/test_thespacedevs.py
Normal file
612
tests/test_thespacedevs.py
Normal file
|
@ -0,0 +1,612 @@
|
|||
# test_thespacedevs.py
|
||||
|
||||
import deepdiff
|
||||
import pytest
|
||||
from agenda.thespacedevs import format_launch_changes
|
||||
|
||||
# --- Helper Functions for Tests ---
|
||||
|
||||
|
||||
def create_base_launch():
|
||||
"""Creates a base launch dictionary for diffing."""
|
||||
return {
|
||||
"id": "test-id",
|
||||
"name": "Starship | Flight 10",
|
||||
"status": {
|
||||
"id": 8,
|
||||
"name": "To Be Confirmed",
|
||||
"abbrev": "TBC",
|
||||
"description": "Awaiting official confirmation...",
|
||||
},
|
||||
"last_updated": "2025-08-08T16:03:39Z",
|
||||
"net": "2025-08-22T23:30:00Z",
|
||||
"window_end": "2025-08-23T01:34:00Z",
|
||||
"window_start": "2025-08-22T23:30:00Z",
|
||||
"net_precision": {
|
||||
"id": 1,
|
||||
"name": "Minute",
|
||||
"abbrev": "MIN",
|
||||
"description": "The T-0 is accurate to the minute.",
|
||||
},
|
||||
"probability": 75,
|
||||
"pad": {
|
||||
"id": 188,
|
||||
"name": "Orbital Launch Mount A",
|
||||
"location": {"name": "SpaceX Starbase, TX, USA"},
|
||||
},
|
||||
"mission": {
|
||||
"name": "Flight 10"
|
||||
# description intentionally omitted initially for test_dictionary_item_added
|
||||
},
|
||||
# Fields that should be skipped
|
||||
"agency_launch_attempt_count": 550,
|
||||
"location_launch_attempt_count": 18,
|
||||
"pad_launch_attempt_count": 9,
|
||||
"orbital_launch_attempt_count": 5,
|
||||
"agency_launch_attempt_count_year": 100,
|
||||
"location_launch_attempt_count_year": 3,
|
||||
"pad_launch_attempt_count_year": 3,
|
||||
"orbital_launch_attempt_count_year": 1,
|
||||
}
|
||||
|
||||
|
||||
# --- Tests for format_launch_changes ---
|
||||
|
||||
|
||||
def test_no_changes():
|
||||
"""Test when there are no differences."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
assert result == "No specific changes detected"
|
||||
|
||||
|
||||
def test_status_change():
|
||||
"""Test changes to the status name."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["status"]["name"] = "Go for Launch"
|
||||
# Note: ID change might happen, but the test focuses on the name change message
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
# print(f"DEBUG test_status_change diff: {diff}") # Uncomment for debugging
|
||||
result = format_launch_changes(diff)
|
||||
# DeepDiff should report this as values_changed
|
||||
assert "Status changed from 'To Be Confirmed' to 'Go for Launch'" in result
|
||||
|
||||
|
||||
def test_net_precision_change():
|
||||
"""Test changes to the net precision name."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["net_precision"]["name"] = "Hour"
|
||||
# Note: ID change might happen, but the test focuses on the name change message
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
# print(f"DEBUG test_net_precision_change diff: {diff}") # Uncomment for debugging
|
||||
result = format_launch_changes(diff)
|
||||
# DeepDiff should report this as values_changed
|
||||
assert "Launch precision changed from 'Minute' to 'Hour'" in result
|
||||
|
||||
|
||||
def test_net_change():
|
||||
"""Test changes to the net (launch time)."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["net"] = "2025-08-23T00:00:00Z"
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
# The function uses format_date, so the output will be formatted
|
||||
assert (
|
||||
"Launch time changed from 22 Aug 2025 at 23:30 UTC to 23 Aug 2025 at 00:00 UTC"
|
||||
in result
|
||||
)
|
||||
|
||||
|
||||
def test_window_start_change():
|
||||
"""Test changes to the window start."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["window_start"] = "2025-08-22T23:00:00Z"
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
assert (
|
||||
"Launch window start changed from 22 Aug 2025 at 23:30 UTC to 22 Aug 2025 at 23:00 UTC"
|
||||
in result
|
||||
)
|
||||
|
||||
|
||||
def test_window_end_change():
|
||||
"""Test changes to the window end."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["window_end"] = "2025-08-23T02:00:00Z"
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
assert (
|
||||
"Launch window end changed from 23 Aug 2025 at 01:34 UTC to 23 Aug 2025 at 02:00 UTC"
|
||||
in result
|
||||
)
|
||||
|
||||
|
||||
def test_last_updated_change():
|
||||
"""Test changes to the last updated time."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["last_updated"] = "2025-08-09T10:00:00Z"
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
assert "Last updated: 09 Aug 2025 at 10:00 UTC" in result
|
||||
|
||||
|
||||
def test_name_change():
|
||||
"""Test changes to the launch name."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["name"] = "Starship | Flight 10 - Revised"
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
assert (
|
||||
"Mission name changed from 'Starship | Flight 10' to 'Starship | Flight 10 - Revised'"
|
||||
in result
|
||||
)
|
||||
|
||||
|
||||
def test_probability_change():
|
||||
"""Test changes to the launch probability."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["probability"] = 85
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
assert "Launch probability changed from 75% to 85%" in result
|
||||
|
||||
|
||||
def test_probability_set():
|
||||
"""Test setting probability from None."""
|
||||
prev = create_base_launch()
|
||||
prev["probability"] = None # Start with None
|
||||
cur = create_base_launch() # End with 75 (int)
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
assert "Launch probability set to 75%" in result
|
||||
|
||||
|
||||
def test_probability_removed():
|
||||
"""Test removing probability (setting to None)."""
|
||||
prev = create_base_launch() # Start with 75 (int)
|
||||
cur = create_base_launch()
|
||||
cur["probability"] = None # End with None
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
assert "Launch probability removed" in result
|
||||
|
||||
|
||||
def test_skip_fields():
|
||||
"""Test that specific fields are skipped."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["agency_launch_attempt_count"] = 551
|
||||
cur["pad_launch_attempt_count_year"] = 4
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
# The only change is in skipped fields, so no user-facing changes
|
||||
assert result == "No specific changes detected"
|
||||
|
||||
|
||||
def test_dictionary_item_added():
|
||||
"""Test adding a new field."""
|
||||
prev = create_base_launch()
|
||||
# Ensure 'description' is not in the base mission dict
|
||||
assert "description" not in prev["mission"]
|
||||
cur = create_base_launch()
|
||||
cur["mission"]["description"] = "New mission description."
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
# print(f"DEBUG test_dictionary_item_added diff: {diff}") # Uncomment for debugging
|
||||
result = format_launch_changes(diff)
|
||||
# DeepDiff path for nested dict item added
|
||||
assert (
|
||||
"New field added: Mission['Description" in result
|
||||
) # Matches the output format
|
||||
|
||||
|
||||
def test_dictionary_item_removed():
|
||||
"""Test removing a field."""
|
||||
prev = create_base_launch()
|
||||
# Add 'description' to prev so it can be removed
|
||||
prev["mission"]["description"] = "Old description."
|
||||
cur = create_base_launch()
|
||||
# Ensure 'description' is not in cur's mission dict
|
||||
assert "description" not in cur["mission"]
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
# print(f"DEBUG test_dictionary_item_removed diff: {diff}") # Uncomment for debugging
|
||||
result = format_launch_changes(diff)
|
||||
# DeepDiff path for nested dict item removed
|
||||
assert "Field removed: Mission['Description" in result # Matches the output format
|
||||
|
||||
|
||||
def test_type_change():
|
||||
"""Test changing the type of a field."""
|
||||
prev = create_base_launch()
|
||||
cur = create_base_launch()
|
||||
cur["probability"] = "High" # Change int to string
|
||||
diff = deepdiff.DeepDiff(prev, cur, ignore_order=True)
|
||||
result = format_launch_changes(diff)
|
||||
assert "Probability type changed from int to str" in result
|
||||
|
||||
|
||||
# --- Test with Sample Data (Simulated Diff) ---
|
||||
|
||||
|
||||
def test_with_sample_data_status_change():
|
||||
"""Simulate a diff like the one that might occur for status change using sample data structure."""
|
||||
# Simulate a diff where status changes from TBC to Success (ID 3)
|
||||
# This mimics the EXACT structure DeepDiff produces for values_changed
|
||||
# We only include the fields we care about in the test
|
||||
sample_diff_status = deepdiff.DeepDiff(
|
||||
{"status": {"id": 8, "name": "To Be Confirmed"}},
|
||||
{"status": {"id": 3, "name": "Success"}},
|
||||
ignore_order=True,
|
||||
)
|
||||
# print(f"DEBUG test_with_sample_data_status_change diff: {sample_diff_status}") # Debug
|
||||
result = format_launch_changes(sample_diff_status)
|
||||
# Should report the status name change
|
||||
assert "Status changed from 'To Be Confirmed' to 'Success'" in result
|
||||
# Ensure it doesn't report ID change separately due to the 'startswith' logic
|
||||
# (The logic should group them under the name change)
|
||||
# A simple check: if name change is there, and ID is handled, it's likely correct.
|
||||
# The exact number of bullet points might vary based on ID/abbrev handling,
|
||||
# but the key message should be present.
|
||||
|
||||
|
||||
def test_with_sample_data_net_change():
|
||||
"""Simulate a diff for net change."""
|
||||
sample_diff_net = {
|
||||
"values_changed": {
|
||||
"root['net']": {
|
||||
"new_value": "2025-08-25T12:00:00Z",
|
||||
"old_value": "2025-08-22T23:30:00Z",
|
||||
}
|
||||
}
|
||||
}
|
||||
result = format_launch_changes(sample_diff_net)
|
||||
assert (
|
||||
"Launch time changed from 22 Aug 2025 at 23:30 UTC to 25 Aug 2025 at 12:00 UTC"
|
||||
in result
|
||||
)
|
||||
|
||||
|
||||
def test_status_name_change():
|
||||
diffs = {
|
||||
"values_changed": {
|
||||
"root['status']['name']": {
|
||||
"old_value": "To Be Confirmed",
|
||||
"new_value": "Go for Launch",
|
||||
}
|
||||
}
|
||||
}
|
||||
out = format_launch_changes(diffs)
|
||||
assert "Status changed from 'To Be Confirmed' to 'Go for Launch'" in out
|
||||
|
||||
|
||||
def test_net_precision_change():
|
||||
diffs = {
|
||||
"values_changed": {
|
||||
"root['net_precision']['name']": {
|
||||
"old_value": "Hour",
|
||||
"new_value": "Minute",
|
||||
}
|
||||
}
|
||||
}
|
||||
out = format_launch_changes(diffs)
|
||||
assert "Launch precision changed from 'Hour' to 'Minute'" in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,label",
|
||||
[
|
||||
("net", "Launch time"),
|
||||
("window_start", "Launch window start"),
|
||||
("window_end", "Launch window end"),
|
||||
],
|
||||
)
|
||||
def test_datetime_changes_formatted(field: str, label: str):
|
||||
diffs = {
|
||||
"values_changed": {
|
||||
f"root['{field}']": {
|
||||
"old_value": "2025-08-22T23:30:00Z",
|
||||
"new_value": "2025-08-23T01:34:00Z",
|
||||
}
|
||||
}
|
||||
}
|
||||
out = format_launch_changes(diffs)
|
||||
assert (
|
||||
f"{label} changed from 22 Aug 2025 at 23:30 UTC " f"to 23 Aug 2025 at 01:34 UTC"
|
||||
) in out
|
||||
|
||||
|
||||
def test_last_updated_formatted():
|
||||
diffs = {
|
||||
"values_changed": {
|
||||
"root['last_updated']": {
|
||||
"old_value": "2025-08-08T16:03:39Z",
|
||||
"new_value": "2025-08-09T12:00:00Z",
|
||||
}
|
||||
}
|
||||
}
|
||||
out = format_launch_changes(diffs)
|
||||
# Only the new value is shown for last_updated
|
||||
assert "Last updated: 09 Aug 2025 at 12:00 UTC" in out
|
||||
assert "16:03" not in out # ensure old value not included
|
||||
|
||||
|
||||
def test_name_and_probability_changes():
|
||||
diffs = {
|
||||
"values_changed": {
|
||||
"root['name']": {
|
||||
"old_value": "Starship | Flight 10",
|
||||
"new_value": "Starship | Flight X",
|
||||
},
|
||||
"root['probability']": {"old_value": 70, "new_value": 85},
|
||||
}
|
||||
}
|
||||
out = format_launch_changes(diffs)
|
||||
assert (
|
||||
"Mission name changed from 'Starship | Flight 10' to 'Starship | Flight X'"
|
||||
in out
|
||||
)
|
||||
assert "Launch probability changed from 70% to 85%" in out
|
||||
|
||||
|
||||
def test_probability_set_and_removed():
|
||||
# Set from None
|
||||
diffs_set = {
|
||||
"values_changed": {"root['probability']": {"old_value": None, "new_value": 40}}
|
||||
}
|
||||
out_set = format_launch_changes(diffs_set)
|
||||
assert "Launch probability set to 40%" in out_set
|
||||
|
||||
# Removed to None
|
||||
diffs_removed = {
|
||||
"values_changed": {"root['probability']": {"old_value": 30, "new_value": None}}
|
||||
}
|
||||
out_removed = format_launch_changes(diffs_removed)
|
||||
assert "Launch probability removed" in out_removed
|
||||
|
||||
|
||||
def test_skipped_fields_are_not_reported():
|
||||
diffs = {
|
||||
"values_changed": {
|
||||
"root['agency_launch_attempt_count']": {
|
||||
"old_value": 556,
|
||||
"new_value": 557,
|
||||
},
|
||||
"root['pad_launch_attempt_count_year']": {
|
||||
"old_value": 3,
|
||||
"new_value": 4,
|
||||
},
|
||||
}
|
||||
}
|
||||
out = format_launch_changes(diffs)
|
||||
# No bullet points should be produced for skipped fields
|
||||
assert out == "No specific changes detected"
|
||||
|
||||
|
||||
def test_generic_value_change_fallback():
|
||||
diffs = {
|
||||
"values_changed": {
|
||||
"root['rocket']['configuration']['variant']": {
|
||||
"old_value": "",
|
||||
"new_value": "Block 2",
|
||||
}
|
||||
}
|
||||
}
|
||||
out = format_launch_changes(diffs)
|
||||
assert (
|
||||
"rocket']['configuration']['variant'] changed from '' to 'Block 2'" not in out
|
||||
) # ensure path cleaning happened
|
||||
assert "rocket']['configuration']['variant" not in out # sanity: no stray brackets
|
||||
assert "rocket']['configuration']['variant" not in out # redundant but explicit
|
||||
# Expected cleaned path from the function logic:
|
||||
assert (
|
||||
"rocket']['configuration']['variant" not in out
|
||||
) # path is cleaned to "rocket']['configuration']['variant"
|
||||
# The function replaces prefixes; the fallback prints the cleaned field string:
|
||||
# "rocket']['configuration']['variant changed from '' to 'Block 2'"
|
||||
# Because there is no special handling for deeper nesting, we assert a substring:
|
||||
assert "variant changed from '' to 'Block 2'" in out
|
||||
|
||||
|
||||
def test_dictionary_item_added_removed_and_type_change():
|
||||
diffs = {
|
||||
"dictionary_item_added": {"root['new_field']"},
|
||||
"dictionary_item_removed": {"root['old_field_name']"},
|
||||
"type_changes": {
|
||||
"root['probability']": {
|
||||
"old_type": int,
|
||||
"new_type": str,
|
||||
"old_value": 70,
|
||||
"new_value": "70%",
|
||||
}
|
||||
},
|
||||
}
|
||||
out = format_launch_changes(diffs)
|
||||
assert "New field added: New Field" in out
|
||||
assert "Field removed: Old Field Name" in out
|
||||
assert "Probability type changed from int to str" in out
|
||||
|
||||
|
||||
def test_no_changes_message():
|
||||
diffs = {}
|
||||
out = format_launch_changes(diffs)
|
||||
assert out == "No specific changes detected"
|
||||
|
||||
|
||||
def test_no_changes():
|
||||
"""
|
||||
Tests that no message is generated when there are no differences.
|
||||
"""
|
||||
old_launch = {"id": 1, "name": "Launch A"}
|
||||
new_launch = {"id": 1, "name": "Launch A"}
|
||||
diff = deepdiff.DeepDiff(old_launch, new_launch)
|
||||
assert format_launch_changes(diff) == "No specific changes detected"
|
||||
|
||||
|
||||
def test_mission_name_change():
|
||||
"""
|
||||
Tests a simple change to the mission name.
|
||||
"""
|
||||
old_launch = {"name": "Starship | Flight 9"}
|
||||
new_launch = {"name": "Starship | Flight 10"}
|
||||
diff = deepdiff.DeepDiff(old_launch, new_launch)
|
||||
expected = "• Mission name changed from 'Starship | Flight 9' to 'Starship | Flight 10'"
|
||||
assert format_launch_changes(diff) == expected
|
||||
|
||||
|
||||
def test_status_change():
|
||||
"""
|
||||
Tests that a change in status name is formatted correctly and that other
|
||||
changes within the 'status' dictionary are ignored for a cleaner output.
|
||||
"""
|
||||
old_launch = {"status": {"id": 8, "name": "To Be Confirmed", "abbrev": "TBC"}}
|
||||
new_launch = {"status": {"id": 1, "name": "Go for Launch", "abbrev": "Go"}}
|
||||
diff = deepdiff.DeepDiff(old_launch, new_launch)
|
||||
expected = "• Status changed from 'To Be Confirmed' to 'Go for Launch'"
|
||||
assert format_launch_changes(diff) == expected
|
||||
|
||||
|
||||
def test_datetime_changes():
|
||||
"""
|
||||
Tests the custom formatting for various datetime fields.
|
||||
"""
|
||||
old_launch = {
|
||||
"net": "2025-08-22T23:30:00Z",
|
||||
"window_start": "2025-08-22T23:30:00Z",
|
||||
"window_end": "2025-08-23T01:34:00Z",
|
||||
"last_updated": "2025-08-08T16:03:39Z",
|
||||
}
|
||||
new_launch = {
|
||||
"net": "2025-08-23T00:00:00Z",
|
||||
"window_start": "2025-08-23T00:00:00Z",
|
||||
"window_end": "2025-08-23T02:00:00Z",
|
||||
"last_updated": "2025-08-09T10:00:00Z",
|
||||
}
|
||||
diff = deepdiff.DeepDiff(old_launch, new_launch)
|
||||
result_lines = format_launch_changes(diff).split("\n")
|
||||
|
||||
expected_changes = [
|
||||
"• Launch time changed from 22 Aug 2025 at 23:30 UTC to 23 Aug 2025 at 00:00 UTC",
|
||||
"• Launch window start changed from 22 Aug 2025 at 23:30 UTC to 23 Aug 2025 at 00:00 UTC",
|
||||
"• Launch window end changed from 23 Aug 2025 at 01:34 UTC to 23 Aug 2025 at 02:00 UTC",
|
||||
"• Last updated: 09 Aug 2025 at 10:00 UTC",
|
||||
]
|
||||
|
||||
assert len(result_lines) == len(expected_changes)
|
||||
for expected_line in expected_changes:
|
||||
assert expected_line in result_lines
|
||||
|
||||
|
||||
def test_datetime_change_fallback():
|
||||
"""
|
||||
Tests the fallback for malformed datetime strings.
|
||||
"""
|
||||
old_launch = {"net": "an-invalid-date"}
|
||||
new_launch = {"net": "another-invalid-date"}
|
||||
diff = deepdiff.DeepDiff(old_launch, new_launch)
|
||||
expected = "• Launch time changed from an-invalid-date to another-invalid-date"
|
||||
assert format_launch_changes(diff) == expected
|
||||
|
||||
|
||||
def test_probability_changes():
|
||||
"""
|
||||
Tests the three scenarios for probability changes:
|
||||
1. From a value to another value.
|
||||
2. From None to a value.
|
||||
3. From a value to None.
|
||||
"""
|
||||
# From value to value
|
||||
old = {"probability": 70}
|
||||
new = {"probability": 80}
|
||||
diff = deepdiff.DeepDiff(old, new)
|
||||
assert format_launch_changes(diff) == "• Launch probability changed from 70% to 80%"
|
||||
|
||||
# From None to value
|
||||
old = {"probability": None}
|
||||
new = {"probability": 90}
|
||||
diff = deepdiff.DeepDiff(old, new)
|
||||
assert format_launch_changes(diff) == "• Launch probability set to 90%"
|
||||
|
||||
# From value to None
|
||||
old = {"probability": 90}
|
||||
new = {"probability": None}
|
||||
diff = deepdiff.DeepDiff(old, new)
|
||||
assert format_launch_changes(diff) == "• Launch probability removed"
|
||||
|
||||
|
||||
def test_skipped_field_is_ignored():
|
||||
"""
|
||||
Tests that changes to fields in the SKIP_FIELDS list are ignored.
|
||||
"""
|
||||
old = {"pad_launch_attempt_count_year": 4}
|
||||
new = {"pad_launch_attempt_count_year": 5}
|
||||
diff = deepdiff.DeepDiff(old, new)
|
||||
assert format_launch_changes(diff) == "No specific changes detected"
|
||||
|
||||
|
||||
def test_dictionary_item_added():
|
||||
"""
|
||||
Tests that newly added fields are reported.
|
||||
"""
|
||||
old = {"name": "Launch"}
|
||||
new = {"name": "Launch", "hashtag": "#launchday"}
|
||||
diff = deepdiff.DeepDiff(old, new)
|
||||
assert format_launch_changes(diff) == "• New field added: Hashtag"
|
||||
|
||||
|
||||
def test_dictionary_item_removed():
|
||||
"""
|
||||
Tests that removed fields are reported.
|
||||
"""
|
||||
old = {"name": "Launch", "hashtag": "#launchday"}
|
||||
new = {"name": "Launch"}
|
||||
diff = deepdiff.DeepDiff(old, new)
|
||||
assert format_launch_changes(diff) == "• Field removed: Hashtag"
|
||||
|
||||
|
||||
def test_type_change():
|
||||
"""
|
||||
Tests that a change in the data type of a field is reported.
|
||||
"""
|
||||
old = {"probability": 50}
|
||||
new = {"probability": "50%"}
|
||||
diff = deepdiff.DeepDiff(old, new)
|
||||
assert format_launch_changes(diff) == "• Probability type changed from int to str"
|
||||
|
||||
|
||||
def test_multiple_changes():
|
||||
"""
|
||||
Tests a scenario with multiple, mixed changes to ensure all are reported.
|
||||
Order is not guaranteed, so we check for the presence of each line.
|
||||
"""
|
||||
old_launch = {
|
||||
"status": {"name": "To Be Confirmed"},
|
||||
"probability": 90,
|
||||
"name": "Old Mission Name",
|
||||
}
|
||||
new_launch = {
|
||||
"status": {"name": "Go for Launch"},
|
||||
"name": "New Mission Name",
|
||||
"weather_concerns": "High winds.",
|
||||
}
|
||||
diff = deepdiff.DeepDiff(old_launch, new_launch)
|
||||
result_lines = format_launch_changes(diff).split("\n")
|
||||
|
||||
expected_changes = [
|
||||
"• Status changed from 'To Be Confirmed' to 'Go for Launch'",
|
||||
"• Mission name changed from 'Old Mission Name' to 'New Mission Name'",
|
||||
"• Field removed: Probability",
|
||||
"• New field added: Weather Concerns",
|
||||
]
|
||||
|
||||
assert len(result_lines) == len(expected_changes)
|
||||
for expected_line in expected_changes:
|
||||
assert expected_line in result_lines
|
Loading…
Reference in a new issue