Skip to content

Commit 8d7af7d

Browse files
Merge pull request steam-bell-92#1856 from jyotish6699/feat/bidirectional-registry-validation
feat: add bidirectional repository consistency validation to registry validator
2 parents d0e223b + fd0dc55 commit 8d7af7d

2 files changed

Lines changed: 185 additions & 3 deletions

File tree

tests/test_registry_validator.py

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,11 @@ def test_missing_project_file(tmp_path):
188188
validator = RegistryValidator(registry)
189189
validator.validate()
190190

191-
assert any("missing project file" in e.lower() for e in validator.errors)
191+
assert any(
192+
"registered project not found in repository"
193+
in e.lower()
194+
for e in validator.errors
195+
)
192196

193197
def test_invalid_json(tmp_path):
194198
registry = tmp_path / "projects_registry.json"
@@ -300,7 +304,79 @@ def test_json_report(tmp_path, capsys):
300304

301305
output = json.loads(captured.out)
302306

307+
assert "repository_consistency" in output
308+
309+
assert output["repository_consistency"] == {
310+
"unregistered_projects": []
311+
}
312+
303313
assert output["projects"] == 1
304314
assert output["errors"] == 0
305315
assert output["warnings"] == 0
306-
assert output["status"] == "passed"
316+
assert output["status"] == "passed"
317+
318+
319+
def test_unregistered_repository_project(tmp_path):
320+
"""Repository project should be reported if missing from registry."""
321+
322+
utilities = tmp_path / "utilities"
323+
utilities.mkdir()
324+
325+
project = utilities / "DemoProject"
326+
project.mkdir()
327+
328+
(project / "DemoProject.py").write_text(
329+
"print('hello')",
330+
encoding="utf-8",
331+
)
332+
333+
registry = write_registry(tmp_path, [])
334+
335+
validator = RegistryValidator(registry)
336+
337+
validator.validate()
338+
339+
assert "utilities/DemoProject" in validator.unregistered_projects
340+
341+
assert any(
342+
"missing from registry" in error.lower()
343+
for error in validator.errors
344+
)
345+
346+
347+
def test_repository_consistency_passes(tmp_path):
348+
"""Registry and repository should be fully synchronized."""
349+
350+
utilities = tmp_path / "utilities"
351+
utilities.mkdir()
352+
353+
project = utilities / "DemoProject"
354+
project.mkdir()
355+
356+
(project / "DemoProject.py").write_text(
357+
"print('hello')",
358+
encoding="utf-8",
359+
)
360+
361+
registry = write_registry(
362+
tmp_path,
363+
[
364+
{
365+
"name": "Demo",
366+
"emoji": "🔥",
367+
"category": "utilities",
368+
"difficulty": "beginner",
369+
"description": "Demo project",
370+
"keywords": ["demo"],
371+
"path": "utilities/DemoProject/DemoProject.py",
372+
}
373+
],
374+
)
375+
376+
validator = RegistryValidator(registry)
377+
378+
validator.validate()
379+
380+
assert validator.unregistered_projects == []
381+
382+

utils/registry_validator.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(self, registry_path: str = "projects_registry.json"):
4242
self.registry_path = Path(registry_path)
4343
self.errors = []
4444
self.warnings = []
45+
self.unregistered_projects = []
4546
self.projects = []
4647
self.validation_status = "PENDING"
4748

@@ -156,9 +157,26 @@ def validate_project_paths(self):
156157

157158
if not project_file.exists():
158159
self.errors.append(
159-
f"Missing project file: {path}"
160+
f"Registered project not found in repository: {path}"
160161
)
161162

163+
def validate_repository_consistency(self):
164+
"""Validate repository and registry consistency."""
165+
166+
registry_dirs = self.get_registry_project_directories()
167+
discovered_dirs = self.discover_repository_projects()
168+
169+
unregistered_projects = sorted(
170+
discovered_dirs - registry_dirs
171+
)
172+
173+
self.unregistered_projects = unregistered_projects
174+
175+
for project in self.unregistered_projects:
176+
self.errors.append(
177+
f"Project exists in repository but is missing from registry: {project}"
178+
)
179+
162180
def validate_keywords(self):
163181
"""Validate keywords field."""
164182

@@ -177,6 +195,76 @@ def validate_keywords(self):
177195
f"{project['name']} has no keywords."
178196
)
179197

198+
def get_scan_directories(self):
199+
"""Return repository roots that contain projects."""
200+
201+
root = self.registry_path.parent
202+
203+
return {
204+
directory.name
205+
for directory in root.iterdir()
206+
if directory.is_dir()
207+
and directory.name != "__pycache__"
208+
and not directory.name.startswith(".")
209+
}
210+
211+
def get_registry_project_directories(self):
212+
"""Return project directories defined in the registry."""
213+
214+
registry_dirs = set()
215+
216+
for project in self.projects:
217+
path = project.get("path")
218+
219+
if not path:
220+
continue
221+
222+
registry_dirs.add(
223+
Path(path).parent.as_posix()
224+
)
225+
226+
return registry_dirs
227+
228+
def discover_repository_projects(self):
229+
"""Discover project directories from the repository."""
230+
231+
root = self.registry_path.parent
232+
discovered_projects = set()
233+
234+
for directory_name in self.get_scan_directories():
235+
directory = root / directory_name
236+
237+
if (
238+
not directory.exists()
239+
or not directory.is_dir()
240+
):
241+
continue
242+
243+
# Standalone project (e.g. expression_parser)
244+
if directory_name == "expression_parser":
245+
if any(directory.glob("*.py")):
246+
discovered_projects.add(
247+
directory.relative_to(root).as_posix()
248+
)
249+
continue
250+
251+
# Category containing project folders
252+
for project_dir in directory.iterdir():
253+
if not project_dir.is_dir():
254+
continue
255+
256+
if project_dir.name == "__pycache__":
257+
continue
258+
259+
# Only consider directories that contain at least one
260+
# top-level Python file.
261+
if any(project_dir.glob("*.py")):
262+
discovered_projects.add(
263+
project_dir.relative_to(root).as_posix()
264+
)
265+
266+
return discovered_projects
267+
180268
def validate(self):
181269
"""Run all validation checks."""
182270

@@ -191,6 +279,7 @@ def validate(self):
191279
self.validate_duplicate_names()
192280
self.validate_duplicate_paths()
193281
self.validate_project_paths()
282+
self.validate_repository_consistency()
194283
self.validate_keywords()
195284

196285
if self.errors:
@@ -207,6 +296,9 @@ def report(self, json_output=False):
207296
"errors": len(self.errors),
208297
"warnings": len(self.warnings),
209298
"status": self.validation_status.lower(),
299+
"repository_consistency": {
300+
"unregistered_projects": self.unregistered_projects,
301+
},
210302
},
211303
indent=2,
212304
)
@@ -238,9 +330,23 @@ def report(self, json_output=False):
238330

239331
if self.errors:
240332
print("\nErrors:")
333+
241334
for error in self.errors:
335+
if error.startswith(
336+
"Project exists in repository but is missing from registry:"
337+
):
338+
continue
339+
242340
print(f" - {error}")
243341

342+
if self.unregistered_projects:
343+
print("\nRepository Consistency:")
344+
345+
print("\n Unregistered projects:")
346+
347+
for project in self.unregistered_projects:
348+
print(f" - {project}")
349+
244350
if self.warnings:
245351
print("\nWarnings:")
246352
for warning in self.warnings:

0 commit comments

Comments
 (0)