The old check imported json, subprocess and datetime -- all stdlib -- so its failure condition was unreachable, and its version-reporting helper check_package was defined and never called. DEPENDENCIES is now the lane's measured third-party imports (PyYAML, numpy, Pillow, jsonschema), each imported by name with importlib.metadata version; missing -> rc 1, broken -> rc 1, usage error -> rc 2. email_validator is not declared: spark's own email_validator.py is a local stdlib-only module. 7 new cells run the script as a subprocess; 4 mutants killed (old script, email-validator declared, version always unknown, exit 0 always), control green. Queue Q120 closed with the record appended and docs/queue-index.md re-rendered in the same commit.
130 lines
4.8 KiB
Python
130 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Cells for wizard-work/spark/python-env-check.py (queue Q120).
|
|
|
|
The script under test is run as a subprocess, exactly as an operator runs
|
|
it; nothing here imports it. The red cell picks its missing package by
|
|
measuring the host first, so it is a real absence rather than a name the
|
|
test hopes is absent.
|
|
|
|
Run: python3 -m unittest discover -p 'test_python_env_check.py' -v
|
|
(from wizard-work/spark, or by path from anywhere)
|
|
"""
|
|
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
SCRIPT = os.path.join(HERE, "python-env-check.py")
|
|
|
|
# Real, widely-installed packages the spark lane does not depend on. The red
|
|
# cell requires whichever of these the host genuinely lacks.
|
|
RED_CANDIDATES = ("flask", "django", "pandas", "scipy", "tensorflow", "torch", "black")
|
|
|
|
|
|
def run_script(*args, cwd=None):
|
|
proc = subprocess.run(
|
|
[sys.executable, SCRIPT] + list(args),
|
|
cwd=cwd or HERE,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return proc
|
|
|
|
|
|
def absent_package():
|
|
"""Return the first candidate module that cannot be found on this host."""
|
|
for name in RED_CANDIDATES:
|
|
try:
|
|
found = importlib.util.find_spec(name)
|
|
except (ImportError, ValueError):
|
|
found = None
|
|
if found is None:
|
|
return name
|
|
return None
|
|
|
|
|
|
class EnvCheckContract(unittest.TestCase):
|
|
def setUp(self):
|
|
self.proc = run_script()
|
|
self.assertEqual(
|
|
self.proc.returncode in (0, 1),
|
|
True,
|
|
"expected rc 0 or 1, got %r: %s" % (self.proc.returncode, self.proc.stderr),
|
|
)
|
|
self.report = json.loads(self.proc.stdout)
|
|
|
|
def test_declared_list_is_not_the_standard_library(self):
|
|
# The defect Q120 filed: the only names checked were json/subprocess/
|
|
# datetime, so no healthy interpreter could ever fail this script.
|
|
packages = self.report["components"]["packages"]
|
|
self.assertTrue(packages, "no packages reported at all")
|
|
stdlib = set(getattr(sys, "stdlib_module_names", ()))
|
|
for import_name in packages:
|
|
self.assertNotIn(
|
|
import_name,
|
|
stdlib,
|
|
"%r is a standard-library module, not a lane dependency" % import_name,
|
|
)
|
|
|
|
def test_declared_list_is_the_lane_dependencies(self):
|
|
packages = self.report["components"]["packages"]
|
|
self.assertEqual(
|
|
set(packages), {"yaml", "numpy", "PIL", "jsonschema"}
|
|
)
|
|
# email_validator is the lane's own stdlib-only module, not the PyPI
|
|
# distribution; declaring it would test something the lane never uses.
|
|
self.assertNotIn("email_validator", packages)
|
|
|
|
def test_every_declared_dependency_reports_a_version(self):
|
|
packages = self.report["components"]["packages"]
|
|
for import_name, result in packages.items():
|
|
self.assertEqual(result["status"], "ok", "%s: %r" % (import_name, result))
|
|
self.assertIn("version", result)
|
|
self.assertIn("distribution", result)
|
|
self.assertFalse(
|
|
result["version"] in ("", "unknown"),
|
|
"%s reported no version: %r" % (import_name, result),
|
|
)
|
|
|
|
def test_all_ok_agrees_with_the_missing_list(self):
|
|
self.assertEqual(self.report["all_ok"], self.report["missing"] == [])
|
|
self.assertEqual(self.proc.returncode, 0 if self.report["all_ok"] else 1)
|
|
|
|
def test_genuinely_absent_package_turns_the_reading_red(self):
|
|
name = absent_package()
|
|
if name is None:
|
|
self.skipTest(
|
|
"every candidate %r is installed here; no real package to "
|
|
"prove the red path with" % (RED_CANDIDATES,)
|
|
)
|
|
proc = run_script("--require", name)
|
|
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
|
|
report = json.loads(proc.stdout)
|
|
self.assertFalse(report["all_ok"])
|
|
self.assertEqual(report["missing"], [name])
|
|
result = report["components"]["packages"][name]
|
|
self.assertEqual(result["status"], "missing")
|
|
self.assertIn(name, result["error"])
|
|
# The healthy declared dependencies still report alongside it.
|
|
self.assertEqual(report["dependencies_declared"], 5)
|
|
self.assertEqual(
|
|
report["components"]["packages"]["yaml"]["status"], "ok"
|
|
)
|
|
|
|
def test_usage_error_is_rc_two(self):
|
|
proc = run_script("--require")
|
|
self.assertEqual(proc.returncode, 2, proc.stderr)
|
|
|
|
def test_same_reading_from_a_different_cwd(self):
|
|
other = run_script(cwd="/")
|
|
self.assertEqual(other.returncode, self.proc.returncode)
|
|
self.assertEqual(json.loads(other.stdout)["missing"], self.report["missing"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|