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
23 changes: 18 additions & 5 deletions desloppify/base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,16 @@ def _migrate_single_state_file(config: dict, path: Path) -> bool:
return False

_merge_legacy_state_config(config, old_config)
_strip_config_from_state_file(path, state_data)
return True


def _state_files_for_migration(state_dir: Path) -> list[Path]:
"""Return state files in deterministic migration order."""
scoped = sorted(state_dir.glob("state-*.json"), key=lambda p: p.name)
root = sorted(state_dir.glob("state.json"), key=lambda p: p.name)
return [*scoped, *root]


def _migrate_from_state_files(config_path: Path) -> dict:
"""Migrate config keys from state-*.json files into config.json.

Expand All @@ -393,12 +399,13 @@ def _migrate_from_state_files(config_path: Path) -> dict:
if not state_dir.exists():
return config

state_files = list(state_dir.glob("state-*.json")) + list(
state_dir.glob("state.json")
)
state_files = _state_files_for_migration(state_dir)
migrated_any = False
migrated_files: list[Path] = []
for sf in state_files:
migrated_any = _migrate_single_state_file(config, sf) or migrated_any
if _migrate_single_state_file(config, sf):
migrated_any = True
migrated_files.append(sf)

if migrated_any and config:
try:
Expand All @@ -407,6 +414,12 @@ def _migrate_from_state_files(config_path: Path) -> dict:
log_best_effort_failure(
logger, f"save migrated config to {config_path}", exc
)
else:
for sf in migrated_files:
state_data = _load_state_file_payload(sf)
if state_data is None:
continue
_strip_config_from_state_file(sf, state_data)

return config

Expand Down
46 changes: 46 additions & 0 deletions desloppify/tests/core/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest

import desloppify.base.config as config_mod
from desloppify.base.config import (
CONFIG_SCHEMA,
_migrate_from_state_files,
Expand Down Expand Up @@ -321,6 +322,27 @@ def test_merges_multiple_state_files(self, tmp_path):
assert "ex1" in result.get("exclude", [])
assert "ex2" in result.get("exclude", [])

def test_scalar_merge_order_is_deterministic(self, tmp_path):
state_dir = tmp_path
state_dir.mkdir(exist_ok=True)

state_a = {
"version": 1,
"config": {"target_strict_score": 90},
"issues": {},
}
state_z = {
"version": 1,
"config": {"target_strict_score": 10},
"issues": {},
}
(state_dir / "state-z.json").write_text(json.dumps(state_z))
(state_dir / "state-a.json").write_text(json.dumps(state_a))
config_path = state_dir / "config.json"

result = _migrate_from_state_files(config_path)
assert result.get("target_strict_score") == 90

def test_no_state_files_returns_empty(self, tmp_path):
config_path = tmp_path / "config.json"
result = _migrate_from_state_files(config_path)
Expand Down Expand Up @@ -378,3 +400,27 @@ def test_legacy_language_keys_from_state_are_not_auto_migrated(self, tmp_path):
result = _migrate_from_state_files(config_path)
assert "csharp_corroboration_min_signals" not in result
assert "csharp_high_fanout_threshold" not in result

def test_save_failure_does_not_strip_source_state_config(
self, tmp_path, monkeypatch
):
state_dir = tmp_path
state_dir.mkdir(exist_ok=True)
state_data = {
"version": 1,
"config": {"ignore": ["smells::*::debug"]},
"issues": {},
}
state_file = state_dir / "state-python.json"
state_file.write_text(json.dumps(state_data))
config_path = state_dir / "config.json"

def _raise_save(*_args, **_kwargs):
raise OSError("disk full")

monkeypatch.setattr(config_mod, "save_config", _raise_save)

result = _migrate_from_state_files(config_path)
assert "smells::*::debug" in result.get("ignore", [])
updated_state = json.loads(state_file.read_text())
assert "config" in updated_state