spark: add JSON linter with test suite
This commit is contained in:
1 parent
3f7a29c606
commit
0f0d02b4b1
2 files changed
+150
No files matched your search
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""JSON Lint CLI - validates JSON from stdin or --text flag."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def validate(text: str) -> tuple[bool, str]:
|
||||||
|
"""Validate JSON text. Returns (is_valid, error_msg)."""
|
||||||
|
try:
|
||||||
|
json.loads(text)
|
||||||
|
return True, ""
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return False, f"Line {e.lineno}, Col {e.colno}: {e.msg}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) == 3 and sys.argv[1] == "--text":
|
||||||
|
# Validate inline JSON text
|
||||||
|
text = sys.argv[2]
|
||||||
|
elif len(sys.argv) == 2 and sys.argv[1] == "-":
|
||||||
|
# Validate from stdin
|
||||||
|
text = sys.stdin.read()
|
||||||
|
elif len(sys.argv) == 2:
|
||||||
|
# Validate from file path
|
||||||
|
with open(sys.argv[1], 'r') as f:
|
||||||
|
text = f.read()
|
||||||
|
else:
|
||||||
|
print("Usage: jsonlint.py [--text 'json'] | [-]", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
ok, err = validate(text)
|
||||||
|
if ok:
|
||||||
|
print("VALID")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print(f"INVALID: {err}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test script for jsonlint.py - verifies correct behavior."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
SCRIPT = "/root/.hermes/wizard-outputs/spark/jsonlint.py"
|
||||||
|
|
||||||
|
def run_script(args):
|
||||||
|
"""Run jsonlint.py and return (exit_code, stdout, stderr)."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["python3", SCRIPT] + args,
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
||||||
|
|
||||||
|
def test_valid_inline_json():
|
||||||
|
"""Valid JSON via --text should return 0."""
|
||||||
|
code, out, err = run_script(["--text", '{"key": "value"}'])
|
||||||
|
assert code == 0, f"Expected exit 0, got {code}"
|
||||||
|
assert out == "VALID", f"Expected 'VALID', got '{out}'"
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_invalid_inline_json():
|
||||||
|
"""Invalid JSON via --text should return 1."""
|
||||||
|
code, out, err = run_script(["--text", "{not json}"])
|
||||||
|
assert code == 1, f"Expected exit 1, got {code}"
|
||||||
|
assert "INVALID" in err, f"Expected 'INVALID' in stderr, got '{err}'"
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_valid_file():
|
||||||
|
"""Valid JSON from file should return 0."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
f.write('{"arr": [1, 2, 3]}')
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
code, out, err = run_script([path])
|
||||||
|
assert code == 0, f"Expected exit 0, got {code}"
|
||||||
|
assert out == "VALID"
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_invalid_file():
|
||||||
|
"""Invalid JSON from file should return 1."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
f.write('{"missing": }')
|
||||||
|
path = f.name
|
||||||
|
try:
|
||||||
|
code, out, err = run_script([path])
|
||||||
|
assert code == 1, f"Expected exit 1, got {code}"
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
def test_empty_json():
|
||||||
|
"""Empty object should be valid."""
|
||||||
|
code, out, err = run_script(["--text", "{}"])
|
||||||
|
assert code == 0, f"Expected exit 0, got {code}"
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_nested_json():
|
||||||
|
"""Complex nested JSON should be valid."""
|
||||||
|
json_text = '{"a": {"b": {"c": [1, 2, {"d": true}]}}}'
|
||||||
|
code, out, err = run_script(["--text", json_text])
|
||||||
|
assert code == 0, f"Expected exit 0, got {code}"
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_missing_quote():
|
||||||
|
"""Unclosed string should be invalid."""
|
||||||
|
code, out, err = run_script(["--text", '{"a": "unclosed}'])
|
||||||
|
assert code == 1, f"Expected exit 1, got {code}"
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_trailing_comma():
|
||||||
|
"""Trailing comma should be invalid (standard JSON)."""
|
||||||
|
code, out, err = run_script(["--text", '{"a": 1,}'])
|
||||||
|
assert code == 1, f"Expected exit 1, got {code}"
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
tests = [
|
||||||
|
test_valid_inline_json,
|
||||||
|
test_invalid_inline_json,
|
||||||
|
test_valid_file,
|
||||||
|
test_invalid_file,
|
||||||
|
test_empty_json,
|
||||||
|
test_nested_json,
|
||||||
|
test_missing_quote,
|
||||||
|
test_trailing_comma,
|
||||||
|
]
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
for test in tests:
|
||||||
|
try:
|
||||||
|
test()
|
||||||
|
print(f"PASS: {test.__name__}")
|
||||||
|
passed += 1
|
||||||
|
except AssertionError as e:
|
||||||
|
print(f"FAIL: {test.__name__}: {e}")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\nResults: {passed} passed, {failed} failed")
|
||||||
|
sys.exit(0 if failed == 0 else 1)
|
||||||
Reference in new issue
Block a user