"""Drive procfs (python-linux-procfs 0.7.3) and print one key=value per line. ONE INTERPRETER START DOES THE WHOLE OF IT. Every python3 start on this arm is a fresh load of one large static image (99-python3 records the cost), so the suite that drives this asserts on the OUTPUT rather than calling python3 once per case. Every line is `key=value'; a key is emitted at most once. The suite reads the keys it names and ignores the rest, so adding a fact here cannot break it. Failure is reported as a key too -- `*_error' -- rather than only as an exit status, because the case that reds should say which stage stopped and why. /proc/cpuinfo IS NEVER OPENED, AND THAT IS THE ONE DELIBERATE OMISSION HERE. Reading it traps this kernel -- "null function or function signature mismatch" in seq_read_iter, brintos/linux#50 -- and the trap has been observed to take the whole guest down mid-run (77-tokyocabinet's header records that happening). A probe that called the bare `procfs.cpuinfo()' would therefore not produce a red, it would delete this suite's evidence and everything running after it. The `cpuinfo' PARSER is still asserted, and asserted harder than a live read could: `cpuinfo(filename=...)' takes the path as an argument, so it is driven over a committed fixture whose expected nr_cpus/nr_sockets/nr_cores are known exactly. What that cannot say is what THIS kernel's /proc/cpuinfo contains; nothing can, until #50 closes. BRINTOS-WORKAROUND: brintos/linux#50 revisit THE ORDER OF THE SECTIONS IS LOAD-BEARING. Everything that touches no live /proc file runs FIRST -- the import, the pure functions, the parsers over staged trees -- and the live-/proc walks run LAST. If a kernel trap takes the guest during the live section, every key above it has already been printed and its cases are scored; the reverse order would lose the lot. 77-tokyocabinet learned this the expensive way and its header says so. EVERY LIVE-/proc GROUP IS ITS OWN try/except. An ENOSYS from a file this kernel does not implement arrives in Python as OSError, and one uncaught traceback would end the probe and red every remaining case with got:'' -- each naming a different symptom for one cause. Caught, it becomes a single `*_error' key that says which file refused and how. """ import os import sys _seen = set() def emit(key, value): if key in _seen: return _seen.add(key) # One line per fact: the suite greps by key, and an embedded newline would # land as a second, unkeyed line. \r and \t corrupt the line the same way. s = str(value) s = s.replace("\n", " ").replace("\r", " ").replace("\t", " ") print("%s=%s" % (key, s)) def err(key, exc): emit(key, "%s: %s" % (type(exc).__name__, exc)) def fail(key, exc): err(key, exc) raise SystemExit(1) FIXTURE = sys.argv[1] if len(sys.argv) > 1 else "" STAGE = sys.argv[2] if len(sys.argv) > 2 else "" # ------------------------------------------------------------------ the import try: import procfs except Exception as exc: # noqa: BLE001 - reported, not raised fail("import_error", exc) emit("module_file", getattr(procfs, "__file__", "")) emit("version", getattr(procfs, "VERSION", "")) emit("procfs_submodule_file", getattr(procfs.procfs, "__file__", "")) # The installed metadata, read independently of the module's own VERSION. A # module that landed while its dist-info did not would import fine here and be # invisible to any resolver on the image; a dist-info from one release beside a # module from another is a mixed install this names rather than averages over. # The DISTRIBUTION is `python-linux-procfs', with hyphens -- setup.py's `name' # -- while the IMPORTABLE PACKAGE is `procfs'. They are different strings and # both are worth having on the record. try: from importlib.metadata import version as _dist_version emit("dist_version", _dist_version("python-linux-procfs")) except Exception as exc: # noqa: BLE001 err("dist_version_error", exc) # six IS A HARD RUNTIME IMPORT, not an optional one: procfs/utilist.py does # `from six.moves import range' at module scope and procfs/procfs.py does the # same, so an image without python3-six cannot import procfs at all. The import # above already proves it resolved; this records WHICH six answered, because the # recipe's RDEPENDS is the only thing that puts it there. try: import six emit("six_file", getattr(six, "__file__", "")) emit("six_version", getattr(six, "__version__", "")) except Exception as exc: # noqa: BLE001 err("six_error", exc) # THE PUBLIC SURFACE, ONE KEY EACH. `procfs/__init__.py' is `from .procfs import # *' plus `from .utilist import *', so dir() would report what is BOUND rather # than what the package promises. Each documented name is asserted on its own. for _n in ("pidstat", "pidstatus", "process", "pidstats", "interrupts", "cmdline", "cpuinfo", "smaps", "smaps_lib", "cpustat", "cpusstats", "process_cmdline", "is_s390", "bitmasklist", "hexbitmask"): emit("has_" + _n, "yes" if hasattr(procfs, _n) else "no") # ============================================================================ # SECTION 1 -- THE PURE FUNCTIONS. No filesystem at all. # ============================================================================ # # utilist is two bit-twiddling functions and they are the only part of this # package that is a pure computation. They are asserted by value because they # CAN be: `bitmasklist' turns an smp_affinity line into a CPU list and # `hexbitmask' turns a CPU list back into 32-bit words, and both answers are # fixed by arithmetic rather than by what this kernel provides. try: emit("bitmasklist_f_4", ",".join(str(i) for i in procfs.bitmasklist("f\n", 4))) emit("bitmasklist_f_2", ",".join(str(i) for i in procfs.bitmasklist("f\n", 2))) emit("bitmasklist_5_8", ",".join(str(i) for i in procfs.bitmasklist("5", 8))) # The comma grouping the kernel writes into smp_affinity for wide masks must # be stripped, not parsed as a separator -- `strip().replace(",", "")' is # what does it, and a regression there would silently halve the mask. emit("bitmasklist_comma", ",".join(str(i) for i in procfs.bitmasklist("00000000,00000003", 64))) emit("hexbitmask_0_1", ",".join(str(i) for i in procfs.hexbitmask([0, 1], 4))) emit("hexbitmask_wide", ",".join(str(i) for i in procfs.hexbitmask([0, 32], 64))) except Exception as exc: # noqa: BLE001 err("utilist_error", exc) # is_s390() reads platform.machine(), which on this arm is not an s390. It is a # one-line predicate but it GATES a branch of cpuinfo.parse(), so a wrong answer # would change how the parser counts CPUs. try: import platform emit("machine", platform.machine()) emit("is_s390", "yes" if procfs.is_s390() else "no") except Exception as exc: # noqa: BLE001 err("is_s390_error", exc) # ============================================================================ # SECTION 2 -- THE PARSERS, DRIVEN OVER STAGED INPUT. # ============================================================================ # # Everything below reads a file this probe wrote, or the fixture the suite # shipped. That makes the expected values EXACT rather than whatever this # machine happens to be doing, and it is the only way the cpuinfo parser can be # asserted at all (see the module docstring). # ---- cpuinfo, over the committed fixture ----------------------------------- try: _ci = procfs.cpuinfo(filename=FIXTURE) emit("cpuinfo_nr_cpus", _ci.nr_cpus) emit("cpuinfo_nr_sockets", _ci.nr_sockets) emit("cpuinfo_nr_cores", _ci.nr_cores) emit("cpuinfo_sockets", ",".join(_ci.sockets)) # __getitem__ lowercases its key, so the lookup is case-insensitive while # keys() reports the stored (already lowercased) form. Both are asserted: # a parser that stored the raw case would still answer this lookup. emit("cpuinfo_model_name", _ci["Model Name"]) emit("cpuinfo_vendor", _ci["vendor_id"]) emit("cpuinfo_cpu_cores", _ci["cpu cores"]) emit("cpuinfo_keys", ",".join(sorted(_ci.keys()))) # `core id' is SKIPPED by the parser (it is per-CPU and would only ever hold # the last CPU's value); `processor' is consumed as the CPU counter. Their # absence from keys() is a property of the parser, so it is asserted rather # than left implied by the sorted list above. emit("cpuinfo_has_core_id", "yes" if "core id" in _ci.keys() else "no") emit("cpuinfo_has_processor", "yes" if "processor" in _ci.keys() else "no") except Exception as exc: # noqa: BLE001 err("cpuinfo_error", exc) # ---- pidstat / pidstatus, over a staged /proc-shaped tree ------------------- # # THE COMM FIELD IS THE WHOLE REASON THIS IS STAGED. /proc//stat's second # field is the executable name in parentheses and it may contain SPACES and # CLOSING PARENS, which is why the parser splits on ') ' and rejoins rather than # using str.split(). No live process on this image is named that way, so the # only way to assert the split is to write the pathological name into a file. _ST = os.path.join(STAGE, "4242") _COMM = "we ird name" _STAT = ("4242 (%s) S 4241 4242 4242 0 -1 4194560 100 0 0 0 11 22 0 0 20 0 3 0 " "987654 12345678 900 18446744073709551615 1 2 3 4 5 6 7 8 9 10 11 12 " "13 0 5 99\n" % _COMM) try: os.makedirs(_ST, exist_ok=True) with open(os.path.join(_ST, "stat"), "w") as _f: _f.write(_STAT) with open(os.path.join(_ST, "status"), "w") as _f: _f.write("Name:\t%s\nState:\tS (sleeping)\nTgid:\t4242\nPid:\t4242\n" "PPid:\t4241\nThreads:\t3\nVmRSS:\t3600 kB\n" "Cpus_allowed:\tf\nnot a field line\n" % _COMM) _ps = procfs.pidstat(4242, basedir=STAGE) emit("staged_comm", _ps["comm"]) emit("staged_state", _ps["state"]) emit("staged_ppid", _ps["ppid"]) emit("staged_num_threads", _ps["num_threads"]) emit("staged_starttime", _ps["starttime"]) # A numeric field arrives as an int and comm as a str: the parser int()s what # it can and keeps the rest. Asserting the TYPE is what says the dictionary # is usable for arithmetic rather than a bag of strings. emit("staged_ppid_type", type(_ps["ppid"]).__name__) emit("staged_comm_type", type(_ps["comm"]).__name__) # The mapping protocol the class hand-rolls: `in', has_key(), keys(). emit("staged_contains_comm", "yes" if "comm" in _ps else "no") emit("staged_has_key_state", "yes" if _ps.has_key("state") else "no") emit("staged_nr_keys", len(_ps.keys())) # is_bound_to_cpu() reads the `flags' field against PF_THREAD_BOUND # (0x04000000). The staged flags value is 4194560 = 0x400100, which does not # carry that bit, so the answer is False -- a computed expectation, not an # observation. emit("staged_bound_to_cpu", "yes" if _ps.is_bound_to_cpu() else "no") emit("staged_flags", _ps["flags"]) except Exception as exc: # noqa: BLE001 err("pidstat_staged_error", exc) try: _pst = procfs.pidstatus(4242, basedir=STAGE) emit("staged_status_name", _pst["Name"]) emit("staged_status_state", _pst["State"]) emit("staged_status_ppid", _pst["PPid"]) emit("staged_status_threads", _pst["Threads"]) emit("staged_status_vmrss", _pst["VmRSS"]) emit("staged_status_ppid_type", type(_pst["PPid"]).__name__) # A line with no colon is dropped rather than crashing the parse; `Name:' # with a value containing a colon would be split on the FIRST colon only -- # neither is exercised by a live /proc, both are by this file. emit("staged_status_dropped_junk", "yes" if "not a field line" not in _pst.keys() else "no") except Exception as exc: # noqa: BLE001 err("pidstatus_staged_error", exc) # ---- process(), over the staged tree --------------------------------------- # # `process' is lazy: it holds a pid and materialises `stat'/`status' on first # access. Asserting that the object exists BEFORE any file is read is what says # the laziness is real -- a class that loaded eagerly would raise in __init__ for # a pid that has no files, and the absence case below would then be describing a # different failure. try: _pr = procfs.process(4242, basedir=STAGE) emit("staged_process_pid", _pr.pid) emit("staged_process_lazy", "yes" if not hasattr(_pr, "stat") else "no") emit("staged_process_stat_comm", _pr["stat"]["comm"]) emit("staged_process_status_name", _pr["status"]["Name"]) emit("staged_process_cached", "yes" if hasattr(_pr, "stat") else "no") # process_cmdline() falls back to the stat comm when cmdline is empty. Driven # here rather than live because the fallback is the branch that needs a # process whose cmdline is unreadable, and the staged pid is exactly that. emit("staged_process_cmdline_fallback", procfs.process_cmdline(_pr)) except Exception as exc: # noqa: BLE001 err("process_staged_error", exc) # ---- pidstats over the staged tree ----------------------------------------- try: _pss = procfs.pidstats(basedir=STAGE) emit("staged_pidstats_keys", ",".join(str(k) for k in sorted(_pss.keys()))) emit("staged_pidstats_contains", "yes" if 4242 in _pss else "no") emit("staged_find_by_name", ",".join(str(p) for p in _pss.find_by_name(_COMM))) # find_by_name truncates its argument to 15 characters, because that is what # the kernel stores in comm. The staged name is 12 characters, so this is the # untruncated arm; asserting it keeps the truncating arm below honest. emit("staged_find_by_name_miss", ",".join(str(p) for p in _pss.find_by_name("no-such-comm"))) except Exception as exc: # noqa: BLE001 err("pidstats_staged_error", exc) # ---- THE ONE INPUT THIS PARSER GETS WRONG, RECORDED AS MEASURED BEHAVIOUR --- # # `pidstat.load' recovers comm by splitting the line on the literal two # characters `) ' and rejoining: `line.split(") ")' then `fields[0].split(" (") # + fields[1].split()'. That handles a comm containing SPACES, which is the case # it was written for. It does NOT handle a comm containing `) ' -- the split # yields three parts, `fields[1]' is the middle of the name rather than the # fields, and every numeric key after comm is absent, so a caller reading # `ppid' gets KeyError rather than a wrong number. # # This is asserted, not merely noted, and it is asserted as a KeyError: it is # upstream behaviour at this SRCREV, it is not caused by the port, and pinning # it makes a future upstream fix visible here as a red instead of passing # unnoticed. Nothing is filed for it -- we file nothing against third-party # public repositories (docs/conventions/no-public-upstream-reports.md). _PST2 = os.path.join(STAGE, "4243") _COMM2 = "odd) comm" try: os.makedirs(_PST2, exist_ok=True) with open(os.path.join(_PST2, "stat"), "w") as _f: _f.write(_STAT.replace("4242 (%s)" % _COMM, "4243 (%s)" % _COMM2, 1)) _bad = procfs.pidstat(4243, basedir=STAGE) emit("paren_comm", _bad["comm"]) try: _bad["ppid"] emit("paren_ppid", "no-raise") except KeyError: emit("paren_ppid", "KeyError") except Exception as exc: # noqa: BLE001 emit("paren_ppid", type(exc).__name__) except Exception as exc: # noqa: BLE001 err("paren_error", exc) # ============================================================================ # SECTION 3 -- THE ABSENCE PATHS. # ============================================================================ # # The ticket asks for two of them, and they are genuinely different: # # 1. AN UNUSED PID MUST RAISE, and raise the documented exception, rather than # returning a half-built object a caller would then read garbage out of. # pidstat.__init__ catches FileNotFoundError only to re-raise it -- the # comment in the source says "propagate the error to the user to handle" -- # so the exception TYPE is the assertion. Anything else (an IOError, an # empty object, a None) is a different contract. # # 2. A /proc FILE THIS KERNEL DOES NOT PROVIDE MUST GIVE THE DOCUMENTED EMPTY # RESULT. Only two places in this package document one, and both are # asserted: `process.load_cmdline' catches FileNotFoundError and sets # cmdline to None, and `interrupts.parse_affinity' catches IOError and # returns [0]. Everywhere else -- cmdline(), interrupts(), cpuinfo() -- # propagates, which is recorded in the suite header as the package's actual # contract rather than smoothed over here. # An unused pid, found rather than assumed: scanning upward from a high number # until /proc has no such directory. A hardcoded pid would eventually collide. _UNUSED = 0 for _cand in range(400000, 400200): if not os.path.exists("/proc/%d" % _cand): _UNUSED = _cand break emit("unused_pid", _UNUSED) emit("unused_pid_absent", "yes" if not os.path.exists("/proc/%d" % _UNUSED) else "no") try: procfs.pidstat(_UNUSED) emit("absent_pidstat", "no-raise") except FileNotFoundError: emit("absent_pidstat", "FileNotFoundError") except Exception as exc: # noqa: BLE001 emit("absent_pidstat", type(exc).__name__) try: procfs.pidstatus(_UNUSED) emit("absent_pidstatus", "no-raise") except FileNotFoundError: emit("absent_pidstatus", "FileNotFoundError") except Exception as exc: # noqa: BLE001 emit("absent_pidstatus", type(exc).__name__) # process() itself must NOT raise -- it is lazy -- and the raise must come from # the first access. Two keys, because a class that raised in __init__ would make # the second one unreachable and the difference is the contract. try: _absent = procfs.process(_UNUSED) emit("absent_process_ctor", "no-raise") try: _absent["stat"] emit("absent_process_stat", "no-raise") except FileNotFoundError: emit("absent_process_stat", "FileNotFoundError") except Exception as exc: # noqa: BLE001 emit("absent_process_stat", type(exc).__name__) # ...and the documented EMPTY RESULT, on the same object: an unreadable # cmdline is None, not an exception and not an empty list. try: _c = _absent["cmdline"] emit("absent_process_cmdline", "None" if _c is None else repr(_c)) except Exception as exc: # noqa: BLE001 err("absent_process_cmdline_error", exc) except Exception as exc: # noqa: BLE001 emit("absent_process_ctor", type(exc).__name__) # ============================================================================ # SECTION 4 -- LIVE /proc. EVERYTHING ABOVE HAS ALREADY BEEN PRINTED. # ============================================================================ # # Each group is caught on its own, so one file this kernel does not implement # costs its own key and not the rest of the section. _PID = os.getpid() _PPID = os.getppid() emit("own_pid", _PID) emit("own_ppid", _PPID) # THE FILE, READ DIRECTLY, BEFORE THE LIBRARY IS ASKED. This is the control the # cross-checks below compare against: without it a green `live_comm_match' would # only say the parser agrees with itself. _raw_comm = _raw_state = _raw_ppid = "" try: with open("/proc/%d/stat" % _PID) as _f: _raw = _f.readline().strip() _head, _, _tail = _raw.partition(") ") _raw_comm = _head.split(" (", 1)[1] _rf = _tail.split() _raw_state, _raw_ppid = _rf[0], _rf[1] emit("raw_comm", _raw_comm) emit("raw_state", _raw_state) emit("raw_ppid", _raw_ppid) except Exception as exc: # noqa: BLE001 err("raw_stat_error", exc) try: _live = procfs.pidstat(_PID) emit("live_comm", _live["comm"]) emit("live_state", _live["state"]) emit("live_ppid", _live["ppid"]) emit("live_pid_field", _live["pid"]) emit("live_pid_matches", "yes" if _live["pid"] == _PID else "no") # THE CROSS-CHECK, one key per field: the parser's answer against the bytes. emit("live_comm_match", "yes" if _live["comm"] == _raw_comm else "no") emit("live_state_match", "yes" if _live["state"] == _raw_state else "no") emit("live_ppid_match", "yes" if str(_live["ppid"]) == _raw_ppid else "no") emit("live_ppid_is_ppid", "yes" if _live["ppid"] == _PPID else "no") emit("live_nr_keys", len(_live.keys())) emit("live_flags_type", type(_live["flags"]).__name__) except Exception as exc: # noqa: BLE001 err("pidstat_live_error", exc) try: _lst = procfs.pidstatus(_PID) emit("live_status_name", _lst["Name"]) emit("live_status_pid", _lst["Pid"]) emit("live_status_ppid", _lst["PPid"]) emit("live_status_pid_matches", "yes" if _lst["Pid"] == _PID else "no") emit("live_status_ppid_matches", "yes" if _lst["PPid"] == _PPID else "no") # /proc//status truncates Name to the same 15 characters comm holds, so # the two views of one process must agree. A disagreement means one of the # two parsers is reading the wrong file for this pid. emit("live_status_name_matches_comm", "yes" if _lst["Name"] == _raw_comm else "no") emit("live_status_nr_keys", len(_lst.keys())) except Exception as exc: # noqa: BLE001 err("pidstatus_live_error", exc) # THE WHOLE-/proc WALK. This is the expensive call in the package -- it lists # /proc and builds a `process' for every numeric entry -- and it is what the # ticket names first. It is asserted to contain THIS process and its parent, # which are the two pids this probe can name with certainty. try: _all = procfs.pidstats() emit("pidstats_count", len(_all.keys())) emit("pidstats_has_self", "yes" if _PID in _all else "no") emit("pidstats_has_parent", "yes" if _PPID in _all else "no") emit("pidstats_has_init", "yes" if 1 in _all else "no") emit("pidstats_has_unused", "yes" if _UNUSED in _all else "no") # Non-numeric entries in /proc (self, thread-self, meminfo, ...) are skipped # by the int() guard in reload(). A key that is not an int would mean the # guard has stopped working and every caller indexing by pid would break. emit("pidstats_all_int", "yes" if all(isinstance(k, int) for k in _all.keys()) else "no") # ...and the walk's own view of this process must agree with the direct read. emit("pidstats_self_comm", _all[_PID]["stat"]["comm"]) emit("pidstats_self_comm_match", "yes" if _all[_PID]["stat"]["comm"] == _raw_comm else "no") emit("pidstats_self_state_match", "yes" if _all[_PID]["stat"]["state"] == _raw_state else "no") emit("pidstats_self_ppid_match", "yes" if str(_all[_PID]["stat"]["ppid"]) == _raw_ppid else "no") # find_by_name over the live table, using this process's own comm. It is the # documented lookup and the one pflags uses; a table that held the pids but # could not find one by name would be useless to every caller. emit("pidstats_find_self", "yes" if _PID in _all.find_by_name(_raw_comm) else "no") except Exception as exc: # noqa: BLE001 err("pidstats_live_error", exc) # process() over the live tree: cmdline, environ, status and stat, which is the # set the ticket names. cmdline and environ are the two that read files the # staged tree could not stand in for -- load_cmdline() and load_environ() ignore # `basedir' and hardcode /proc//, which is recorded in the suite header. try: _me = procfs.process(_PID) _cl = _me["cmdline"] emit("live_cmdline_type", type(_cl).__name__) emit("live_cmdline_len", len(_cl) if _cl is not None else -1) emit("live_cmdline_0", _cl[0] if _cl else "") # argv[0] is this interpreter and argv[1] is the fixture path this probe was # handed. Asserting the SECOND element is what says the whole vector was # split on NUL rather than only its head being readable. emit("live_cmdline_has_fixture", "yes" if _cl and FIXTURE in _cl else "no") emit("live_stat_comm", _me["stat"]["comm"]) emit("live_status_name_via_process", _me["status"]["Name"]) emit("live_process_cmdline_helper", procfs.process_cmdline(_me)) except Exception as exc: # noqa: BLE001 err("process_live_error", exc) try: _me2 = procfs.process(_PID) _env = _me2["environ"] emit("live_environ_type", type(_env).__name__) emit("live_environ_has_marker", "yes" if _env.get("BST_PROCFS_MARKER") == "brintos-1981" else "no") emit("live_environ_marker_value", _env.get("BST_PROCFS_MARKER", "")) emit("live_environ_nonempty", "yes" if len(_env) > 0 else "no") except Exception as exc: # noqa: BLE001 err("environ_live_error", exc) # /proc//cmdline read directly, as the control for live_cmdline_* above. try: with open("/proc/%d/cmdline" % _PID) as _f: _rawcl = _f.readline().strip().split("\0") while _rawcl and _rawcl[-1] == "": _rawcl.pop() emit("raw_cmdline_len", len(_rawcl)) emit("raw_cmdline_0", _rawcl[0] if _rawcl else "") except Exception as exc: # noqa: BLE001 err("raw_cmdline_error", exc) # ---- /proc/cmdline --------------------------------------------------------- # # The KERNEL command line, which is a different file from the per-process one # above and is parsed into a dict of `key=value' and bare booleans. What this # image's boot line contains is not knowable from a recipe phase, so what is # asserted is the SHAPE -- that it parsed, that every bare option is True, and # that the keys agree with the file read directly -- rather than any option name. try: _kc = procfs.cmdline() emit("kcmdline_nr_keys", len(_kc.keys())) emit("kcmdline_keys", ",".join(sorted(_kc.keys()))) emit("kcmdline_bare_are_true", "yes" if all(v is True or isinstance(v, str) for v in _kc.values()) else "no") except Exception as exc: # noqa: BLE001 err("kcmdline_error", exc) try: with open("/proc/cmdline") as _f: _rawk = _f.readline().strip().split() emit("raw_kcmdline_nr_options", len(_rawk)) _rawkk = sorted(set(o.split("=")[0] for o in _rawk)) emit("raw_kcmdline_keys", ",".join(_rawkk)) # The parser's keys against the file's own. A dict cannot hold a repeated # option twice, so the comparison is over the SET -- a boot line that passed # the same knob twice would otherwise look like a parser that lost one. emit("kcmdline_keys_match", "yes" if sorted(_kc.keys()) == _rawkk else "no") except Exception as exc: # noqa: BLE001 err("raw_kcmdline_error", exc) # ---- /proc/interrupts ------------------------------------------------------ # # The last live file, and the one least certain to exist on this arm. Its parser # needs a `CPU0 CPU1 ...' header line to learn nr_cpus before any entry can be # parsed, so a file with a different shape raises AttributeError rather than # returning an empty table -- which is a fact about the parser worth recording # whichever way this image comes out. try: _ir = procfs.interrupts() emit("interrupts_nr_cpus", _ir.nr_cpus) emit("interrupts_count", len(_ir.keys())) emit("interrupts_keys", ",".join(sorted(_ir.keys()))) _k = sorted(_ir.keys()) if _k: _e = _ir[_k[0]] emit("interrupts_first_key", _k[0]) emit("interrupts_first_fields", ",".join(sorted(_e.keys()))) emit("interrupts_first_cpu_len", len(_e.get("cpu", []))) # __getitem__ str()s its key, so an int and a str index the same entry. # That is the documented calling convention and both are used in the # package's own docstrings. try: emit("interrupts_int_key_ok", "yes" if _ir[int(_k[0])] is _e else "no") except (ValueError, KeyError): emit("interrupts_int_key_ok", "n/a-non-numeric") except Exception as exc: # noqa: BLE001 err("interrupts_error", exc) _raw_irq_entries = -1 _raw_irq_cpus = -1 try: with open("/proc/interrupts") as _f: _rawi = [l for l in _f.read().splitlines() if l.strip()] emit("raw_interrupts_lines", len(_rawi)) emit("raw_interrupts_header", _rawi[0].split()[0] if _rawi else "") if _rawi and _rawi[0].split()[0][:3] == "CPU": _raw_irq_cpus = len(_rawi[0].split()) _raw_irq_entries = len(_rawi) - 1 emit("raw_interrupts_cpus", _raw_irq_cpus) emit("raw_interrupts_entries", _raw_irq_entries) except Exception as exc: # noqa: BLE001 err("raw_interrupts_error", exc) # THE CROSS-CHECK, computed here rather than in the suite because only this # process saw both readings of a file whose contents change between reads: the # counters in /proc/interrupts move continuously, but the NUMBER of CPU columns # and the NUMBER of IRQ lines do not, which is why those are what is compared. try: emit("interrupts_nr_cpus_match", "yes" if _ir.nr_cpus == _raw_irq_cpus else "no") emit("interrupts_entries_match", "yes" if len(_ir.keys()) == _raw_irq_entries else "no") except Exception as exc: # noqa: BLE001 err("interrupts_match_error", exc) emit("done", "yes")