DEV Community

Pravin Kumar
Pravin Kumar

Posted on

Zero Dependencies, Zero Regressions: What It Took to Build CLI-KDG from Scratch

When building a CLI testing, discovery, and behavioral regression engine in Python, the standard reaction in modern software development is to reach for pip. A typical production setup might pull in click for CLI parsing, pexpect for process control, hypothesis for test generation, pydantic for serialization, deepdiff for regression detection, and pytest-snapshot for snapshot management.

Before you know it, a "lightweight" testing tool drags along 20+ third-party dependencies, hundreds of transitive sub-packages, vendor lock-in, and security vulnerability supply-chain risks.

In CLI-KDG, we took a radically different engineering constraint: Zero Third-Party Dependencies. No pip install. No requirements.txt. No external binaries. Pure Python 3.11+ standard library and bare POSIX system calls.

Here is an honest, deep-dive breakdown of what packages developers normally install, and what it actually took to replace them with zero dependencies.


1. Process Control & Pipe Multiplexing

📦 What You'd Normally Install:

  • pexpect: For spawning child processes and interacting with pseudo-ttys.
  • subprocess (high-level wrappers): subprocess.run(), subprocess.Popen().
  • psutil: For process lifecycle monitoring and killing child trees.

🛠️ What It Actually Took to Replace It:

High-level wrappers like subprocess.run() block execution and suffer from POSIX pipe buffer deadlocks if a process outputs more than ~64 KB to STDOUT or STDERR concurrently.

To solve this without pexpect or psutil, we went down to raw POSIX system call primitives in cli_kdg/process.py:

  1. Process Spawning via os.fork() and os.execvp():
    We fork the current process using os.fork(), set up isolated OS-level pipes using os.pipe(), and duplicate file descriptors via os.dup2() to redirect standard output and standard error handles before replacing the child image with os.execvp().

  2. Deadlock Protection via Non-Blocking fcntl & select.select():
    To prevent buffer deadlocks on massive output runs, we mark pipe read descriptors non-blocking using fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK). We multiplex stream reads inside a non-blocking select.select() event loop that drains STDOUT and STDERR concurrently into dynamic byte buffers.

  3. Monotonic Timeout & Deterministic Child Reaping:
    Instead of system clock-dependent timers, we use time.monotonic() to track deadline execution. If a child process exceeds its deadline, we issue os.kill(pid, signal.SIGTERM) (followed by SIGKILL if necessary) and strictly reap the process using non-blocking status calls (os.waitpid(pid, os.WNOHANG)), preventing zombie processes.

# Low-level non-blocking pipe setup in cli_kdg/process.py
r_out, w_out = os.pipe()
r_err, w_err = os.pipe()

flags = fcntl.fcntl(r_out, fcntl.F_GETFL)
fcntl.fcntl(r_out, fcntl.F_SETFL, flags | os.O_NONBLOCK)
fcntl.fcntl(r_err, fcntl.F_SETFL, flags | os.O_NONBLOCK)
Enter fullscreen mode Exit fullscreen mode

2. CLI Argument Parsing & Option Interrogation

📦 What You'd Normally Install:

  • argparse / click / typer: For subcommand parsing and CLI options.
  • docopt: For regex-based --help text parsing.

🛠️ What It Actually Took to Replace It:

  1. Manual Command Vector Parser (parse_args() in cli_kdg/parser.py):
    Instead of heavy CLI frameworks, we built a zero-dependency argument vector parser in parse_args(). It manually processes sys.argv[1:] to extract subcommands (run, discover, snapshot, replay), parses --timeout and -o/--output flags, supports --timeout=SEC equals syntax, and handles positional argument separators (--) gracefully.

  2. Deterministic --help Text Tokenizer (parse_help_text() in cli_kdg/help_parser.py):
    To discover CLI option structures automatically, we wrote a line-by-line help parser in parse_help_text() without regex or external packages. It extracts short options (-v), long options (--verbose), inline assignments (--output=FILE), and value descriptors (FILE, INTEGER, <path>) by inspecting whitespace indentation and uppercase token patterns.

# Token-based option extraction in cli_kdg/help_parser.py
if "=" in token and token.startswith("-"):
    flag_part, val_part = token.split("=", 1)
    requires_value, value_hint = True, val_part.strip("<>[]")
Enter fullscreen mode Exit fullscreen mode

3. Automated Test Case Generation

📦 What You'd Normally Install:

  • hypothesis: For property-based testing and input fuzzing.
  • pytest-quickcheck: For generating test vectors.

🛠️ What It Actually Took to Replace It:

Random fuzzing creates noisy, non-deterministic test suites. In generate_test_cases() (cli_kdg/generator.py), we built a deterministic category-classified test case generator.

From any discovered CLIModel, generate_test_cases() systematically generates bounded, classified test cases:

  • HELP: Verifies standard --help execution.
  • UNKNOWN_OPTION: Verifies error handling on unrecognized flags (--unknown-flag-xyz-kdg).
  • FLAG_VALID & FLAG_UNEXPECTED_ARG: Tests boolean flag activation and boundary argument handling.
  • OPTION_MISSING_VAL, OPTION_VALID_VAL, OPTION_INVALID_VAL: Tests value-requiring options against zero (0), negative (-1), and non-numeric string (abc) boundaries.
  • Bounded Safety: Automatically caps execution at 15 deterministic cases per run.

4. Snapshot Persistence & Data Modeling

📦 What You'd Normally Install:

  • pydantic / marshmallow: For schema validation and JSON serialization.
  • pytest-snapshot: For snapshot file management.

🛠️ What It Actually Took to Replace It:

  1. Standard Python Dataclass Equivalents (cli_kdg/models.py):
    We implemented clean, lightweight Python classes (ExecutionResult, TestObservation, Snapshot, ReplayResult) equipped with explicit to_dict() and from_dict() methods.

  2. Versioned JSON Persistence (create_snapshot(), save_snapshot(), load_snapshot() in cli_kdg/snapshot.py):
    Snapshots created by create_snapshot() are saved via save_snapshot() and loaded via load_snapshot() in standard json format with explicit versioning ("version": 1). If a snapshot file is corrupt, missing, or has an incompatible version schema, CLI-KDG catches the error and raises a controlled CLKDGUserError, producing clean terminal error messages without uncaught tracebacks.


5. Comparative Behavioral Regression Engine

📦 What You'd Normally Install:

  • deepdiff: For structural object comparisons and diff generation.
  • pytest-regressions: For visual regression reporting.

🛠️ What It Actually Took to Replace It:

In replay_snapshot() and compare_observations() (cli_kdg/replay.py), we built an exact comparative replay engine. It loads historical test cases from a snapshot and replays them against an updated CLI target.

  1. Field-by-Field Discrepancy Tracking via compare_observations():
    compare_observations() compares exit_code, status, stdout, stderr, and termination_type field-by-field, recording structured ComparisonDetail objects for any changes.

  2. Built-in Runtime Invariance:
    A major flaw in naive snapshot tools is flagging minor execution time variations as test failures. In CLI-KDG, execution duration (runtime_ms) is treated strictly as metadata — runtime differences NEVER trigger behavioral regression diffs.


6. Dependency Policy Auditing & CI Enforcement

📦 What You'd Normally Install:

  • pip-audit / safety: For scanning dependency vulnerabilities.
  • import-linter: For enforcing module boundary rules.

🛠️ What It Actually Took to Replace It:

To enforce our zero-dependency policy permanently in CI/CD, we wrote an automated AST import auditor in test_ast_import_audit() directly inside run_tests.py.

Using Python's built-in ast and importlib modules, test_ast_import_audit() traverses syntax trees via ast.walk() for every Python file in the codebase, inspecting all Import and ImportFrom nodes. It resolves module paths via importlib.import_module() and verifies that zero imported modules resolve to site-packages or dist-packages directories, asserting that no dependency manifests (requirements.txt, Pipfile, pyproject.toml) exist.

# AST import auditor snippet in run_tests.py
for node in ast.walk(tree):
    if isinstance(node, ast.Import):
        for alias in node.names:
            mod = importlib.import_module(alias.name.split('.')[0])
            assert "site-packages" not in getattr(mod, "__file__", "")
Enter fullscreen mode Exit fullscreen mode

Summary Comparison Matrix

Capability Standard Third-Party Stack CLI-KDG Zero-Dependency Solution Core Functions / Calls Used
Process Control pexpect, psutil POSIX process engine (process.py) os.fork(), os.execvp(), os.pipe(), os.dup2(), os.waitpid(), os.kill()
I/O Multiplexing asyncio, twisted Non-blocking pipe event loop (process.py) fcntl.fcntl(), select.select()
Argument Parsing click, argparse Manual vector parser (parser.py) parse_args()
Help Interrogation docopt, regex Token-based text parser (help_parser.py) parse_help_text()
Test Case Generation hypothesis Classified deterministic generator (generator.py) generate_test_cases()
Snapshot Persistence pydantic, pytest-snapshot Built-in json + schema (snapshot.py) create_snapshot(), save_snapshot(), load_snapshot(), to_dict(), from_dict()
Regression Engine deepdiff Field comparator + Runtime Invariance (replay.py) replay_snapshot(), compare_observations()
Dependency Audit pip-audit, import-linter AST import scanner (run_tests.py) test_ast_import_audit(), ast.walk(), importlib.import_module()

Conclusion

Replacing 6 major third-party packages required discipline, clean modular design, and a deep understanding of POSIX process semantics.

The result?

  • Zero supply-chain risk: 0 external dependencies to audit, patch, or update.
  • Ultra-minimal codebase: Under 1,900 total lines of clean, human-interpretable Python.
  • Lightning fast execution: Runs a full 38-test integration suite in under 2.5 seconds.
  • 100% portable: Runs out-of-the-box on any standard Python 3.11+ environment on Linux, macOS, or BSD.

Top comments (0)