agenda/tests/test_thespacedevs.py

613 lines
21 KiB
Python

# 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