Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions samcli/commands/sync/sync_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,14 @@ def _toml_document_to_sync_state(toml_document: Dict) -> Optional[SyncState]:
if resource_sync_states_toml_table:
for resource_id in resource_sync_states_toml_table:
resource_sync_state_toml_table = resource_sync_states_toml_table.get(resource_id)
sync_time_str = resource_sync_state_toml_table.get(SYNC_TIME)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be that the datetime has a Z at the end right, like 2024-05-08T15:16:43Z, should we do something to handle this like 2024-05-08T15:16:43Z

# Parse datetime and ensure it's timezone-aware UTC (consistent with how we write)
sync_time = datetime.fromisoformat(sync_time_str)
if sync_time.tzinfo is None:
sync_time = sync_time.replace(tzinfo=timezone.utc)
resource_sync_state = ResourceSyncState(
resource_sync_state_toml_table.get(HASH),
datetime.fromisoformat(resource_sync_state_toml_table.get(SYNC_TIME)),
sync_time,
)

# For Nested stack resources, replace "-" with "/"
Expand All @@ -142,9 +147,12 @@ def _toml_document_to_sync_state(toml_document: Dict) -> Optional[SyncState]:
latest_infra_sync_time = None
if sync_state_toml_table:
dependency_layer = sync_state_toml_table.get(DEPENDENCY_LAYER)
latest_infra_sync_time = sync_state_toml_table.get(LATEST_INFRA_SYNC_TIME)
if latest_infra_sync_time:
latest_infra_sync_time = datetime.fromisoformat(str(latest_infra_sync_time))
latest_infra_sync_time_str = sync_state_toml_table.get(LATEST_INFRA_SYNC_TIME)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit but this is the same logic as above, it can probably be made into a method to use in both places

if latest_infra_sync_time_str:
# Parse datetime and ensure it's timezone-aware UTC (consistent with how we write)
latest_infra_sync_time = datetime.fromisoformat(str(latest_infra_sync_time_str))
if latest_infra_sync_time.tzinfo is None:
latest_infra_sync_time = latest_infra_sync_time.replace(tzinfo=timezone.utc)
sync_state = SyncState(dependency_layer, resource_sync_states, latest_infra_sync_time)

return sync_state
Expand Down
117 changes: 116 additions & 1 deletion tests/unit/commands/sync/test_sync_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
)
from samcli.lib.build.build_graph import DEFAULT_DEPENDENCIES_DIR

MOCK_RESOURCE_SYNC_TIME = datetime(2023, 2, 8, 12, 12, 12)
MOCK_RESOURCE_SYNC_TIME = datetime(2023, 2, 8, 12, 12, 12, tzinfo=timezone.utc)
MOCK_INFRA_SYNC_TIME = datetime.now(timezone.utc)


Expand Down Expand Up @@ -288,3 +288,118 @@ def test_sync_context_has_no_previous_state_if_file_doesnt_exist(self, patched_r
self.assertIsNone(self.sync_context._previous_state)
self.assertIsNotNone(self.sync_context._current_state)
patched_rmtree_if_exists.assert_not_called()


class TestTimezoneNaiveDatetimeHandling(TestCase):
"""Tests for Issue #8477: Timezone-naive datetime handling in sync.toml"""

def test_toml_to_sync_state_with_timezone_naive_timestamps(self):
"""Test that timezone-naive timestamps are converted to UTC timezone-aware"""
# Create TOML with timezone-naive timestamps (missing +00:00)
toml_str = """
[sync_state]
dependency_layer = true
latest_infra_sync_time = "2025-12-03T22:10:11.916279"

[resource_sync_states]

[resource_sync_states.MockResourceId]
hash = "mock-hash"
sync_time = "2025-12-03T22:10:35.345701"
"""
toml_doc = tomlkit.loads(toml_str)
sync_state = _toml_document_to_sync_state(toml_doc)

# Verify latest_infra_sync_time is timezone-aware UTC
self.assertIsNotNone(sync_state.latest_infra_sync_time)
self.assertIsNotNone(sync_state.latest_infra_sync_time.tzinfo)
self.assertEqual(sync_state.latest_infra_sync_time.tzinfo, timezone.utc)

# Verify resource sync_time is timezone-aware UTC
resource_sync_state = sync_state.resource_sync_states.get("MockResourceId")
self.assertIsNotNone(resource_sync_state)
self.assertIsNotNone(resource_sync_state.sync_time.tzinfo)
self.assertEqual(resource_sync_state.sync_time.tzinfo, timezone.utc)

def test_toml_to_sync_state_with_timezone_aware_timestamps(self):
"""Test that timezone-aware timestamps are preserved correctly"""
# Create TOML with timezone-aware timestamps (with +00:00)
toml_str = """
[sync_state]
dependency_layer = true
latest_infra_sync_time = "2025-12-03T22:10:11.916279+00:00"

[resource_sync_states]

[resource_sync_states.MockResourceId]
hash = "mock-hash"
sync_time = "2025-12-03T22:10:35.345701+00:00"
"""
toml_doc = tomlkit.loads(toml_str)
sync_state = _toml_document_to_sync_state(toml_doc)

# Verify latest_infra_sync_time is timezone-aware UTC
self.assertIsNotNone(sync_state.latest_infra_sync_time)
self.assertIsNotNone(sync_state.latest_infra_sync_time.tzinfo)
self.assertEqual(sync_state.latest_infra_sync_time.tzinfo, timezone.utc)

# Verify resource sync_time is timezone-aware UTC
resource_sync_state = sync_state.resource_sync_states.get("MockResourceId")
self.assertIsNotNone(resource_sync_state)
self.assertIsNotNone(resource_sync_state.sync_time.tzinfo)
self.assertEqual(resource_sync_state.sync_time.tzinfo, timezone.utc)

def test_toml_to_sync_state_mixed_timezone_formats(self):
"""Test handling of mixed timezone-aware and timezone-naive timestamps"""
# Create TOML with mixed formats
toml_str = """
[sync_state]
dependency_layer = true
latest_infra_sync_time = "2025-12-03T22:10:11.916279"

[resource_sync_states]

[resource_sync_states.Resource1]
hash = "hash1"
sync_time = "2025-12-03T22:10:35.345701+00:00"

[resource_sync_states.Resource2]
hash = "hash2"
sync_time = "2025-12-03T22:10:40.123456"
"""
toml_doc = tomlkit.loads(toml_str)
sync_state = _toml_document_to_sync_state(toml_doc)

# All timestamps should be timezone-aware UTC
self.assertIsNotNone(sync_state.latest_infra_sync_time.tzinfo)
self.assertEqual(sync_state.latest_infra_sync_time.tzinfo, timezone.utc)

resource1 = sync_state.resource_sync_states.get("Resource1")
self.assertIsNotNone(resource1.sync_time.tzinfo)
self.assertEqual(resource1.sync_time.tzinfo, timezone.utc)

resource2 = sync_state.resource_sync_states.get("Resource2")
self.assertIsNotNone(resource2.sync_time.tzinfo)
self.assertEqual(resource2.sync_time.tzinfo, timezone.utc)

def test_datetime_comparison_after_fix(self):
"""Test that datetime comparison works after loading timezone-naive timestamps"""
# Simulate the bug scenario: load timezone-naive timestamp and compare with current time
toml_str = """
[sync_state]
dependency_layer = true
latest_infra_sync_time = "2025-12-03T22:10:11.916279"

[resource_sync_states]
"""
toml_doc = tomlkit.loads(toml_str)
sync_state = _toml_document_to_sync_state(toml_doc)

# This should not raise TypeError: can't subtract offset-naive and offset-aware datetimes
current_time = datetime.now(timezone.utc)
try:
time_diff = current_time - sync_state.latest_infra_sync_time
# If we get here, the fix worked
self.assertIsNotNone(time_diff)
except TypeError as e:
self.fail(f"DateTime comparison failed with TypeError: {e}")
Loading