Skip to content

Commit 830c9fa

Browse files
🧹 Refactor main function in check_graphify.py
Break down monolithic main() into modular helper functions. Co-authored-by: Tcode-Motion <188012755+Tcode-Motion@users.noreply.github.com>
1 parent dddc025 commit 830c9fa

1 file changed

Lines changed: 330 additions & 0 deletions

File tree

‎tools/scripts/check_graphify.py‎

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
# tools/scripts/check_graphify.py
2+
import os
3+
import sys
4+
import json
5+
import re
6+
import subprocess
7+
from pathlib import Path
8+
9+
# Standardized Exit Codes
10+
EXIT_SUCCESS = 0
11+
EXIT_VALIDATION_FAILED = 1
12+
EXIT_MISSING_DEPENDENCY = 2
13+
EXIT_EXECUTION_FAILED = 3
14+
EXIT_INVALID_OUTPUT = 4
15+
EXIT_CONFIG_ERROR = 5
16+
17+
def find_repo_root():
18+
current = Path(__file__).resolve().parent
19+
for _ in range(10):
20+
if (current / ".git").exists() or (current / "Cargo.toml").exists() or (current / ".graphifyignore").exists():
21+
return current
22+
parent = current.parent
23+
if parent == current:
24+
break
25+
current = parent
26+
return Path(__file__).resolve().parent.parent
27+
28+
def check_python_dependencies():
29+
missing = []
30+
try:
31+
import dotenv
32+
except ImportError:
33+
missing.append("python-dotenv")
34+
try:
35+
import requests
36+
except ImportError:
37+
missing.append("requests")
38+
if sys.version_info < (3, 11):
39+
try:
40+
import tomli
41+
except ImportError:
42+
missing.append("tomli")
43+
return missing
44+
45+
def validate_graphify_outputs(output_dir):
46+
"""
47+
Dynamically scans output_dir and validates generated files.
48+
Excludes files starting with '.' and manifest.json or cache folder.
49+
Returns (success, results)
50+
"""
51+
if not output_dir.exists():
52+
return False, {"error": f"Output directory does not exist: {output_dir}"}
53+
54+
# Get all files, excluding .*, manifest.json, cache/
55+
generated_files = []
56+
try:
57+
for p in output_dir.iterdir():
58+
if p.name.startswith("."):
59+
continue
60+
if p.name == "manifest.json":
61+
continue
62+
if p.is_dir() and p.name == "cache":
63+
continue
64+
if p.is_file():
65+
generated_files.append(p)
66+
except Exception as e:
67+
return False, {"error": f"Failed to list output directory: {e}"}
68+
69+
if not generated_files:
70+
return False, {"error": f"No generated output files found in {output_dir}"}
71+
72+
validation_results = {}
73+
overall_success = True
74+
75+
for f in generated_files:
76+
filename = f.name
77+
size = f.stat().st_size
78+
if size == 0:
79+
validation_results[filename] = {"status": "FAIL", "reason": "File is empty (0 bytes)"}
80+
overall_success = False
81+
continue
82+
83+
if f.suffix == ".json":
84+
try:
85+
with open(f, "r", encoding="utf-8") as file:
86+
data = json.load(file)
87+
if not isinstance(data, dict):
88+
validation_results[filename] = {"status": "FAIL", "reason": "Root element is not a JSON object"}
89+
overall_success = False
90+
elif "nodes" not in data or ("edges" not in data and "links" not in data):
91+
validation_results[filename] = {"status": "FAIL", "reason": "Missing 'nodes' and either 'edges' or 'links' keys"}
92+
overall_success = False
93+
else:
94+
edges_key = "links" if "links" in data else "edges"
95+
validation_results[filename] = {
96+
"status": "PASS",
97+
"info": f"Valid JSON with {len(data.get('nodes', []))} nodes and {len(data.get(edges_key, []))} {edges_key} ({size / 1024:.2f} KB)"
98+
}
99+
except json.JSONDecodeError as e:
100+
validation_results[filename] = {"status": "FAIL", "reason": f"Invalid JSON format: {e}"}
101+
overall_success = False
102+
except Exception as e:
103+
validation_results[filename] = {"status": "FAIL", "reason": f"Error reading file: {e}"}
104+
overall_success = False
105+
106+
elif f.suffix == ".html":
107+
try:
108+
with open(f, "r", encoding="utf-8") as file:
109+
content = file.read(500) # Read start
110+
if "<html>" not in content.lower() and "<!doctype html>" not in content.lower():
111+
validation_results[filename] = {"status": "FAIL", "reason": "Not a valid HTML file"}
112+
overall_success = False
113+
else:
114+
validation_results[filename] = {
115+
"status": "PASS",
116+
"info": f"Valid HTML structure ({size / 1024:.2f} KB)"
117+
}
118+
except Exception as e:
119+
validation_results[filename] = {"status": "FAIL", "reason": f"Error reading file: {e}"}
120+
overall_success = False
121+
122+
elif f.suffix == ".md":
123+
try:
124+
with open(f, "r", encoding="utf-8") as file:
125+
content = file.read(500)
126+
# Check for markdown headers or content
127+
if not any(line.strip().startswith("#") for line in content.splitlines()):
128+
validation_results[filename] = {"status": "FAIL", "reason": "Markdown does not contain headings"}
129+
overall_success = False
130+
else:
131+
validation_results[filename] = {
132+
"status": "PASS",
133+
"info": f"Valid Markdown with headings ({size / 1024:.2f} KB)"
134+
}
135+
except Exception as e:
136+
validation_results[filename] = {"status": "FAIL", "reason": f"Error reading file: {e}"}
137+
overall_success = False
138+
else:
139+
# Other file types, check size only
140+
validation_results[filename] = {
141+
"status": "PASS",
142+
"info": f"Generic file checked ({size / 1024:.2f} KB)"
143+
}
144+
145+
return overall_success, validation_results
146+
147+
def check_rust_toolchain():
148+
rustc_ver = None
149+
try:
150+
res = subprocess.run(["rustc", "--version"], capture_output=True, text=True, check=False)
151+
if res.returncode == 0:
152+
rustc_ver = res.stdout.strip()
153+
except Exception:
154+
pass
155+
156+
rustc_check = ("PASS", rustc_ver) if rustc_ver else ("FAIL", "rustc is not installed. Install via https://rustup.rs/")
157+
158+
cargo_ver = None
159+
try:
160+
res = subprocess.run(["cargo", "--version"], capture_output=True, text=True, check=False)
161+
if res.returncode == 0:
162+
cargo_ver = res.stdout.strip()
163+
except Exception:
164+
pass
165+
166+
cargo_check = ("PASS", cargo_ver) if cargo_ver else ("FAIL", "cargo is not installed. Install via https://rustup.rs/")
167+
168+
return rustc_check, cargo_check
169+
170+
171+
def check_graphify_cli():
172+
graphify_cli_ver = None
173+
try:
174+
res = subprocess.run(["graphify", "--version"], capture_output=True, text=True, check=False)
175+
if res.returncode == 0:
176+
graphify_cli_ver = res.stdout.strip()
177+
except Exception:
178+
pass
179+
180+
if not graphify_cli_ver:
181+
try:
182+
res = subprocess.run([sys.executable, "-m", "graphify", "--version"], capture_output=True, text=True, check=False)
183+
if res.returncode == 0:
184+
graphify_cli_ver = f"python -m {res.stdout.strip()}"
185+
except Exception:
186+
pass
187+
188+
if graphify_cli_ver:
189+
match = re.search(r"(\d+\.\d+\.\d+)", graphify_cli_ver)
190+
if match:
191+
return ("PASS", f"Graphify version {match.group(1)}")
192+
else:
193+
return ("PASS", f"Graphify version found but unparsed format: {graphify_cli_ver}")
194+
else:
195+
return ("FAIL", "Graphify is not installed. Run: pip install -r requirements.txt")
196+
197+
198+
def check_graphify_config(workspace_dir):
199+
ignore_path = workspace_dir / ".graphifyignore"
200+
if ignore_path.exists():
201+
try:
202+
with open(ignore_path, "r", encoding="utf-8") as f:
203+
content = f.read()
204+
if "graphify-out/" in content or "graphify-out" in content:
205+
return ("PASS", ".graphifyignore exists and contains graphify-out")
206+
else:
207+
return ("FAIL", ".graphifyignore exists but does not ignore graphify-out/ to prevent recursion")
208+
except Exception as e:
209+
return ("FAIL", f"Failed to read .graphifyignore: {e}")
210+
else:
211+
return ("FAIL", ".graphifyignore is missing")
212+
213+
214+
def run_environment_checks(workspace_dir, output_dir):
215+
checks = {}
216+
217+
# 1. Python version check
218+
py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
219+
if sys.version_info >= (3, 10):
220+
checks["Python Version"] = ("PASS", f"Python {py_version} (>= 3.10)")
221+
else:
222+
checks["Python Version"] = ("FAIL", f"Python {py_version} is unsupported (requires >= 3.10)")
223+
224+
# 2. Python dependencies check
225+
missing_deps = check_python_dependencies()
226+
if not missing_deps:
227+
checks["Python Dependencies"] = ("PASS", "All packages imported successfully")
228+
else:
229+
checks["Python Dependencies"] = ("FAIL", f"Missing packages: {', '.join(missing_deps)}. Run: pip install -r requirements.txt")
230+
231+
# 3 & 4. Rust toolchain check
232+
checks["Rust Installed"], checks["Cargo Installed"] = check_rust_toolchain()
233+
234+
# 5. Graphify CLI check
235+
checks["Graphify CLI"] = check_graphify_cli()
236+
237+
# 6. Configuration Check (.graphifyignore)
238+
checks["Graphify Config"] = check_graphify_config(workspace_dir)
239+
240+
# 7. Output directory check
241+
if output_dir.exists() and output_dir.is_dir():
242+
checks["Output Directory"] = ("PASS", f"Exists: {output_dir}")
243+
else:
244+
checks["Output Directory"] = ("FAIL", f"Does not exist: {output_dir}")
245+
246+
return checks
247+
248+
249+
def print_environment_checks(checks):
250+
print("\nEnvironment & Configuration Checks:")
251+
print("-----------------------------------")
252+
env_failed = False
253+
for name, (status, detail) in checks.items():
254+
symbol = "[OK]" if status == "PASS" else "[FAIL]"
255+
print(f"[{status}] {symbol} {name}: {detail}")
256+
if status == "FAIL":
257+
env_failed = True
258+
return env_failed
259+
260+
261+
def handle_output_validation(checks, output_dir):
262+
output_failed = False
263+
if checks["Output Directory"][0] == "PASS":
264+
print("\nDynamic Output Validation:")
265+
print("---------------------------")
266+
valid, results = validate_graphify_outputs(output_dir)
267+
if not valid:
268+
output_failed = True
269+
if "error" in results:
270+
print(f"[FAIL] [ERROR] Error scanning: {results['error']}")
271+
else:
272+
for file, res in results.items():
273+
symbol = "[OK]" if res["status"] == "PASS" else "[FAIL]"
274+
reason = f" ({res['reason']})" if res["status"] == "FAIL" else f" - {res.get('info', '')}"
275+
print(f"[{res['status']}] {symbol} {file}{reason}")
276+
else:
277+
for file, res in results.items():
278+
print(f"[PASS] [OK] {file} - {res.get('info', '')}")
279+
else:
280+
output_failed = True
281+
return output_failed
282+
283+
284+
def determine_exit_code(checks, env_failed, output_failed):
285+
if env_failed:
286+
if checks["Python Version"][0] == "FAIL":
287+
print("Summary: FAIL (Python version unsupported)")
288+
return EXIT_VALIDATION_FAILED
289+
elif checks["Python Dependencies"][0] == "FAIL":
290+
print("Summary: FAIL (Missing Python dependencies)")
291+
return EXIT_MISSING_DEPENDENCY
292+
elif checks["Graphify CLI"][0] == "FAIL":
293+
print("Summary: FAIL (Graphify CLI not installed)")
294+
return EXIT_MISSING_DEPENDENCY
295+
elif checks["Graphify Config"][0] == "FAIL":
296+
print("Summary: FAIL (Graphify configuration error)")
297+
return EXIT_CONFIG_ERROR
298+
elif checks["Rust Installed"][0] == "FAIL" or checks["Cargo Installed"][0] == "FAIL":
299+
print("Summary: FAIL (Rust/Cargo toolchain missing)")
300+
return EXIT_MISSING_DEPENDENCY
301+
else:
302+
print("Summary: FAIL (Environment checks failed)")
303+
return EXIT_VALIDATION_FAILED
304+
305+
if output_failed:
306+
print("Summary: FAIL (Graph generation outputs missing or invalid)")
307+
return EXIT_INVALID_OUTPUT
308+
309+
print("Summary: PASS (All checks completed successfully)")
310+
return EXIT_SUCCESS
311+
312+
313+
def main():
314+
print("==================================================")
315+
print(" Graphify Verification Utility ")
316+
print("==================================================")
317+
318+
workspace_dir = find_repo_root()
319+
output_dir = workspace_dir / "graphify-out"
320+
321+
checks = run_environment_checks(workspace_dir, output_dir)
322+
env_failed = print_environment_checks(checks)
323+
output_failed = handle_output_validation(checks, output_dir)
324+
325+
print("\n==================================================")
326+
exit_code = determine_exit_code(checks, env_failed, output_failed)
327+
sys.exit(exit_code)
328+
329+
if __name__ == "__main__":
330+
main()

0 commit comments

Comments
 (0)