diff --git a/samcli/commands/sync/sync_context.py b/samcli/commands/sync/sync_context.py index ccdd79cfae..3f38322df6 100644 --- a/samcli/commands/sync/sync_context.py +++ b/samcli/commands/sync/sync_context.py @@ -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) + # 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 "/" @@ -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) + 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 diff --git a/tests/unit/commands/sync/test_sync_context.py b/tests/unit/commands/sync/test_sync_context.py index 39b6eaa471..c6a90bcf11 100644 --- a/tests/unit/commands/sync/test_sync_context.py +++ b/tests/unit/commands/sync/test_sync_context.py @@ -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) @@ -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}")