Skip to content

Commit 56d2451

Browse files
aclark4lifeCopilot
andcommitted
spec status: cross-reference patches with stale specs
Patches in .evergreen/spec-patch/ reverse-apply after each sync to exclude tests for unimplemented features. This change makes those relationships visible throughout the output. - Add _patch_dir_map(): builds {test_subdir: [ticket...]} reverse map from all active patch files, so we know which patches touch which spec dirs - Annotate each stale spec with its patch ticket(s): 'dbx spec sync auth 🩹 PYTHON-5445' - Add legend: '🩹 = has an active patch — re-run dbx spec patch apply after syncing' - Update patch summary to show spec dirs each patch covers: '• PYTHON-5052 (14 file(s)) → unified-test-format' - Update patch footer: 'After syncing any of the above spec dirs, run: dbx spec patch apply' - Clarify verify step 2: 'Re-apply patches (removes unimplemented tests)' - Restore accidentally-removed _list_patches() helper - 2 new tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 670a912 commit 56d2451

2 files changed

Lines changed: 120 additions & 6 deletions

File tree

src/dbx_python_cli/commands/spec.py

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,28 @@ def _list_patches(patch_dir: Path) -> list[Path]:
7474
return sorted(patch_dir.glob("*.patch"))
7575

7676

77+
def _patch_dir_map(patch_dir: Path) -> dict[str, list[str]]:
78+
"""Return {test_subdir: [ticket, ...]} for all active patches.
79+
80+
Reads each .patch file, extracts the ``test/<subdir>/`` paths from diff
81+
headers, and builds a reverse map so callers can quickly find which
82+
patches touch a given spec directory.
83+
"""
84+
result: dict[str, list[str]] = {}
85+
for patch_path in _list_patches(patch_dir):
86+
ticket = patch_path.stem
87+
for file_path in _parse_patch_files(patch_path):
88+
parts = file_path.split("/")
89+
if len(parts) >= 3 and parts[0] == "test":
90+
subdir = parts[1]
91+
result.setdefault(subdir, [])
92+
if ticket not in result[subdir]:
93+
result[subdir].append(ticket)
94+
return result
95+
96+
7797
def _show_patch_summary(driver_repo, verbose: bool = False) -> int:
78-
"""Print a summary of active patches. Returns the patch count."""
98+
"""Print a summary of active patches with the spec dirs each covers. Returns patch count."""
7999
patch_dir = _get_patch_dir(driver_repo)
80100
patches = _list_patches(patch_dir)
81101
if not patches:
@@ -84,13 +104,24 @@ def _show_patch_summary(driver_repo, verbose: bool = False) -> int:
84104
for p in patches:
85105
ticket = p.stem
86106
files = _parse_patch_files(p)
107+
subdirs = sorted(
108+
{
109+
parts[1]
110+
for f in files
111+
for parts in [f.split("/")]
112+
if len(parts) >= 3 and parts[0] == "test"
113+
}
114+
)
115+
dirs_str = f" → {', '.join(subdirs)}" if subdirs else ""
87116
if verbose:
88-
typer.echo(f" • {ticket} ({len(files)} file(s)):")
117+
typer.echo(f" • {ticket} ({len(files)} file(s)){dirs_str}:")
89118
for f in files:
90119
typer.echo(f" {f}")
91120
else:
92-
typer.echo(f" • {ticket} ({len(files)} file(s))")
93-
typer.echo("\n Run 'dbx spec patch apply' to apply them.")
121+
typer.echo(f" • {ticket} ({len(files)} file(s)){dirs_str}")
122+
typer.echo(
123+
"\n ⚠ After syncing any of the above spec dirs, run: dbx spec patch apply"
124+
)
94125
return len(patches)
95126

96127

@@ -602,14 +633,34 @@ def spec_status(
602633
else:
603634
up_to_date.append(spec_name)
604635

636+
# --- Build patch→dir map before output --------------------------------- #
637+
patch_dir_lookup = _patch_dir_map(_get_patch_dir(driver_repo))
638+
# reverse: spec_driver_dir → patch tickets that cover it
639+
dir_to_patches: dict[str, list[str]] = patch_dir_lookup
640+
641+
# For each spec_name, collect the driver dirs it maps to
642+
def _patches_for_spec(spec_name: str) -> list[str]:
643+
tickets: list[str] = []
644+
for _, driver_dir in spec_map.get(spec_name, []):
645+
for t in dir_to_patches.get(driver_dir, []):
646+
if t not in tickets:
647+
tickets.append(t)
648+
return tickets
649+
605650
# --- Output ------------------------------------------------------------ #
606651
if stale:
607652
typer.echo(f" ❌ Still needs syncing ({len(stale)}):\n")
608653
for i, (name, reason) in enumerate(stale):
609654
is_last = i == len(stale) - 1
610655
prefix = " └──" if is_last else " ├──"
611656
reason_str = f" [{reason}]" if verbose else ""
612-
typer.echo(f"{prefix} dbx spec sync {name}{reason_str}")
657+
tickets = _patches_for_spec(name)
658+
patch_str = f" 🩹 {', '.join(tickets)}" if tickets else ""
659+
typer.echo(f"{prefix} dbx spec sync {name}{reason_str}{patch_str}")
660+
if any(_patches_for_spec(n) for n, _ in stale):
661+
typer.echo(
662+
"\n 🩹 = has an active patch — re-run 'dbx spec patch apply' after syncing"
663+
)
613664
else:
614665
typer.echo(" ✅ All checked specs are up to date.")
615666

@@ -634,7 +685,7 @@ def spec_status(
634685
typer.echo(f" dbx spec sync {spec_names}")
635686
step += 1
636687
if patch_count:
637-
typer.echo(f" {step}. Re-apply patches after syncing:")
688+
typer.echo(f" {step}. Re-apply patches (removes unimplemented tests):")
638689
typer.echo(" dbx spec patch apply")
639690
step += 1
640691
if branch_dirs:

tests/test_spec_command.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,3 +891,66 @@ def test_spec_status_verify_section_no_stale(tmp_path):
891891
result = runner.invoke(app, ["spec", "status"])
892892
assert result.exit_code == 0
893893
assert "Nothing outstanding" in result.output
894+
895+
896+
def test_patch_dir_map(tmp_path):
897+
"""_patch_dir_map returns a dir→tickets reverse map from patch files."""
898+
from dbx_python_cli.commands.spec import _patch_dir_map
899+
900+
patch_dir = tmp_path / "spec-patch"
901+
patch_dir.mkdir()
902+
(patch_dir / "PYTHON-1234.patch").write_text(
903+
"diff --git a/test/crud/foo.json b/test/crud/foo.json\n"
904+
"diff --git a/test/unified-test-format/bar.json b/test/unified-test-format/bar.json\n"
905+
)
906+
(patch_dir / "PYTHON-5678.patch").write_text(
907+
"diff --git a/test/crud/baz.json b/test/crud/baz.json\n"
908+
)
909+
910+
result = _patch_dir_map(patch_dir)
911+
912+
assert "crud" in result
913+
assert "PYTHON-1234" in result["crud"]
914+
assert "PYTHON-5678" in result["crud"]
915+
assert "unified-test-format" in result
916+
assert result["unified-test-format"] == ["PYTHON-1234"]
917+
918+
919+
def test_spec_status_annotates_patches(tmp_path):
920+
"""Stale specs that have an active patch should show the ticket."""
921+
_, cfg = _make_status_repos(tmp_path, content_same=False)
922+
923+
# Add a patch covering the auth test dir
924+
patch_dir = tmp_path / "repos" / "mongo-python-driver" / ".evergreen" / "spec-patch"
925+
(patch_dir / "PYTHON-9999.patch").write_text(
926+
"diff --git a/test/auth/auth.json b/test/auth/auth.json\n"
927+
"index abc..def 100644\n"
928+
"--- a/test/auth/auth.json\n"
929+
"+++ b/test/auth/auth.json\n"
930+
"@@ -1 +1 @@\n"
931+
"-old\n"
932+
"+new\n"
933+
)
934+
935+
with (
936+
patch("dbx_python_cli.utils.repo.get_config_path", return_value=cfg),
937+
patch(
938+
"dbx_python_cli.commands.spec._get_current_branch",
939+
return_value="spec-resync-test",
940+
),
941+
patch(
942+
"dbx_python_cli.commands.spec._find_recent_resync_commits",
943+
return_value=["abc resyncing"],
944+
),
945+
patch(
946+
"dbx_python_cli.commands.spec._commit_relative_date",
947+
return_value="1 day ago",
948+
),
949+
patch("dbx_python_cli.commands.spec._commit_spec_dirs", return_value=[]),
950+
patch("dbx_python_cli.commands.spec._branch_summary", return_value=([], 0)),
951+
):
952+
result = runner.invoke(app, ["spec", "status"])
953+
954+
assert result.exit_code == 0
955+
assert "PYTHON-9999" in result.output
956+
assert "🩹" in result.output

0 commit comments

Comments
 (0)