Files
glances-rs/tools/parity.py
T
glances-rs ea9debf71b containers: Docker (and best-effort Podman) plugin
- minimal Docker Engine API client over Unix/TCP sockets with version
  negotiation and chunked response decoding
- drop-in stats schema: active/stopped containers, CPU/memory/IO/network
  rates, ports, uptime (pretty_date), image, engine field
- Docker + Podman engines, config (all, podman_sock, max_name_size,
  disable_stats, show/hide, per-container thresholds)
- export strips the computation sub-dicts (--stdout/--stdout-csv)
- TUI block above the process count
- CI-safe fake Docker socket test + live differential parity
2026-10-10 00:24:39 +00:00

189 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Differential test harness: compares glances-rs stdout-json output with the
Python Glances implementation (the reference oracle).
Usage:
tools/parity.py [--rust BIN] [--python BIN] [--iterations N] [plugin ...]
With no plugin list, a default set of core plugins is tested.
"""
import argparse
import json
import os
import subprocess
import sys
DEFAULT_PLUGINS = [
"cpu",
"percpu",
"mem",
"memswap",
"load",
"fs",
"network",
"diskio",
"sensors",
"now",
"uptime",
"system",
"processcount",
"version",
"psutilversion",
"quicklook",
]
def flatten(value, prefix=""):
out = {}
if isinstance(value, dict):
for key, item in value.items():
out.update(flatten(item, f"{prefix}{key}."))
elif isinstance(value, list):
key_field = value[0].get("key") if value and isinstance(value[0], dict) else None
for index, item in enumerate(value):
identity = item.get(key_field) if key_field and isinstance(item, dict) else index
out.update(flatten(item, f"{prefix}{identity}."))
else:
out[prefix[:-1]] = value
return out
def type_name(value):
if value is None:
return "null"
if isinstance(value, bool):
return "bool"
if isinstance(value, (int, float)):
return "number"
if isinstance(value, str):
return "string"
if isinstance(value, list):
return "array"
if isinstance(value, dict):
return "object"
return type(value).__name__
def run(binary, plugins, iterations, extra):
plugins_arg = ",".join(plugins)
cmd = [
binary,
"--stdout-json",
plugins_arg,
"--disable-plugin",
"all",
"--enable-plugin",
plugins_arg,
"--stop-after",
str(iterations),
"-t",
"1",
"--disable-check-update",
] + extra
env = dict(os.environ)
env["PYTHONUNBUFFERED"] = "1"
result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=120)
if result.returncode != 0:
return None, result.stderr.strip().splitlines()[-3:]
lines = [line for line in result.stdout.splitlines() if line.strip()]
if not lines:
return None, ["no output"]
return json.loads(lines[-1]), None
ORACLE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "glances_oracle.py")
def run_python(python, conf, plugins, iterations, extra):
plugins_arg = ",".join(plugins)
cmd = [
python,
ORACLE,
"--iterations",
str(iterations),
"--refresh",
"1",
"--plugins",
plugins_arg,
]
if conf:
cmd += ["--config", conf]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode != 0:
return None, result.stderr.strip().splitlines()[-3:]
lines = [line for line in result.stdout.splitlines() if line.strip()]
if not lines:
return None, ["no output"]
return json.loads(lines[-1]), None
def compare(plugin, rust, python, errors):
rust_plugin = rust.get(plugin, {})
python_plugin = python.get(plugin, {})
rust_flat = flatten(rust_plugin)
python_flat = flatten(python_plugin)
if plugin not in rust:
errors.append(f"{plugin}: missing in Rust output")
return
if plugin not in python:
return
for key in sorted(set(rust_flat) | set(python_flat)):
if key not in python_flat:
errors.append(f"{plugin}.{key}: only in Rust ({rust_flat[key]!r})")
elif key not in rust_flat:
errors.append(f"{plugin}.{key}: only in Python ({python_flat[key]!r})")
else:
rust_type = type_name(rust_flat[key])
python_type = type_name(python_flat[key])
if rust_type != python_type and not (
rust_type == "null" or python_type == "null"
):
errors.append(
f"{plugin}.{key}: type mismatch Rust={rust_type} Python={python_type} "
f"({rust_flat[key]!r} vs {python_flat[key]!r})"
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--rust", default="./target/debug/glances")
parser.add_argument("--python", default="/tmp/opencode/glances-py/bin/python")
parser.add_argument("--config", default="conf/glances.conf")
parser.add_argument("--iterations", type=int, default=3)
parser.add_argument("plugins", nargs="*")
args = parser.parse_args()
plugins = args.plugins or DEFAULT_PLUGINS
errors = []
for plugin in plugins:
# The containers plugin needs a couple of refreshes for its rates to
# become available (both implementations warm up their second sample).
iterations = max(args.iterations, 6) if plugin == "containers" else args.iterations
extra = ["-C", args.config] if args.config else []
rust, rust_error = run(args.rust, [plugin], iterations, extra)
if rust_error is not None:
errors.append(f"{plugin}: Rust run failed: {rust_error}")
continue
python, python_error = run_python(args.python, args.config, [plugin], iterations, [])
if python_error is not None:
errors.append(f"{plugin}: Python run failed: {python_error}")
continue
compare(plugin, rust, python, errors)
status = "OK" if not any(e.startswith(f"{plugin}.") or e.startswith(f"{plugin}:") for e in errors) else "DIFF"
print(f" {plugin}: {status}")
if errors:
print("\nDifferences:")
for error in errors:
print(f" - {error}")
return 1
print("\nNo differences found")
return 0
if __name__ == "__main__":
sys.exit(main())