Files
glances-rs/tools/parity.py
T
glances-rs 975385da5d M1: core engine, config, plugin model and core plugins
- Cargo workspace (core, plugins, tui, cli) with drop-in compatible CLI
- Python-compatible config file handling, limits, views, rates, MMM, history
- process collection via sysinfo + procfs, filters, thresholds, events, actions
- 20 core plugins (cpu, percpu, mem, memswap, load, fs, network, diskio, ...)
- stdout, stdout-csv, stdout-json, --issue, --modules-list output modes
- differential test harness against the Python implementation
2026-10-09 23:25:35 +00:00

186 lines
5.5 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:
extra = ["-C", args.config] if args.config else []
rust, rust_error = run(args.rust, [plugin], args.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], args.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())