Skip to content

Fix latent bugs in helpers and add unit tests - #3574

Merged
mkoura merged 27 commits into
masterfrom
helpers_fixes
Jul 30, 2026
Merged

Fix latent bugs in helpers and add unit tests#3574
mkoura merged 27 commits into
masterfrom
helpers_fixes

Conversation

@mkoura

@mkoura mkoura commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Review of cardano_node_tests/utils/helpers.py uncovered several latent bugs and
hardening opportunities. Each issue is fixed in its own commit, and new unit tests
in framework_tests/ pin the fixed behavior. The tests don't depend on
project-specific binaries or a running cluster.

Bug fixes

  • get_pool_param: str.capitalize() lowercased the rest of the key, so
    "rewardAccount" was looked up as "spsRewardaccount" and the lookup failed on
    node 10.6.0+ params. Slicing-based transform now preserves camelCase and handles
    degenerate keys ("", "sps") without raising.
  • check_dir_arg_keep: the expanduser() result was discarded, so ~ paths
    were returned unexpanded and testnet_cleanup_info silently globbed nothing
    (reported balance 0). Error message also misattributed check_dir_arg.
  • decode_bech32 / encode_bech32: input was interpolated into a
    shell=True command line; a quote in the input broke the command or allowed
    injection. Input is now passed via stdin without a shell. No shell=True
    callers remain in the repo.
  • tool_has: IndexError on error message without a colon, and splitting on
    the first colon anywhere could report a missing CLI capability as present when
    the command string contained a colon (e.g. --opt=a:b).
  • environ: restoring a variable that was unset before entering (and deleted
    inside the block) raised KeyError from the finally clause, masking the real
    in-flight exception.
  • get_vcs_link: a calling path without cardano_node_tests silently
    produced a one-character URL; now raises ValueError.
  • run_command: string commands are tokenized with shlex (quoted arguments
    survive), list items are str()-converted (accepts Path), and all exec
    failures (missing executable, missing workdir, non-executable file) are
    normalized to RuntimeError. New stdin_data parameter used by the bech32
    helpers. Ignored failures are logged at debug level.
  • get_current_commit: git rev-parse now runs in the repo containing the
    module instead of CWD, so tests that change directories can't report a foreign
    repo commit. Resolves the standing TODO.
  • is_in_interval: negative reference value produced an inverted (empty)
    interval.
  • start_cluster / stop script call sites: script paths and args are passed
    to run_command as argv lists instead of strings that get re-tokenized.

Refactors and docs

  • random.choices in get_rand_str, hashlib.file_digest in checksum
    (unused blocksize param dropped), json.dump in write_json, deduplicated
    calling-frame lookup (_get_calling_frame).
  • Missing docstrings added, Raises/Args sections and caching semantics
    documented, contextmanager return hints use tp.Generator, bare generic hints
    parameterized.

Tests

  • New framework_tests/test_helpers.py (89 tests) and
    framework_tests/test_cluster_nodes.py. Subprocess tests use sys.executable
    or POSIX shell builtins; helpers wrapping external tools monkeypatch
    run_command; functools.cache/lru_cache state is cleared around tests.
  • 13 of the tests fail when run against the pre-fix code, one per behavioral fix.
  • Full framework suite: 222 passed, 1 pre-existing xfail. Lint clean.

mkoura added 26 commits July 30, 2026 15:53
str.capitalize() lowercases the rest of the string, so a key like
"rewardAccount" was translated to "spsRewardaccount" instead of
"spsRewardAccount" and the lookup failed on node 10.6.0+ params.
Uppercase only the first character instead. Use slicing so that
empty or too-short keys keep returning None instead of raising
IndexError.
The conditional `orig_path.expanduser()` call discarded its result,
so paths starting with ~ were returned unexpanded. Expand the user
home directly when constructing the returned path.
decode_bech32 and encode_bech32 interpolated their input into a
shell command line inside single quotes. Input containing a quote
would break the command or allow injection. Pass the input via
stdin and invoke the bech32 tool directly without a shell.

Add stdin_data parameter to run_command and document its
arguments.
When code running inside the environ context manager deleted a
variable that was not set before entering, the restore step raised
KeyError on `del os.environ[key]`. Use pop with a default
instead.
The check_dir_arg_keep error message referred to check_dir_arg,
misattributing the failing argparse option in user-facing output.
Indexing the result of split(":", maxsplit=1) with [1] raises
IndexError when the error message contains no colon. Use [-1] which
falls back to the whole message, and drop the now unneeded err_str
preassignment.
When the calling file path did not contain "cardano_node_tests",
str.find returned -1 and the resulting GitHub URL was silently
built from the last character of the path. Raise ValueError
instead.
Naive str.split() broke quoted arguments containing spaces. Use
shlex.split() for shell-like tokenization. Also convert list items
to str so callers can pass e.g. Path objects without crashing the
" ".join() used for logging.
With a negative num2 the computed interval was inverted
(_min > _max) and the check always returned False. Take the
absolute value of the fraction so the interval stays valid.
Single call instead of a generator loop with an unused loop
variable.
Replace the manual block-read loop with hashlib.file_digest
(Python 3.11+). Drop the blocksize parameter - no caller used it.
Use json.dump instead of building the whole string with json.dumps
before writing it.
The command argument is already a string.
get_current_line_str and get_vcs_link contained an identical block
for obtaining the caller's frame. Extract it into a
_get_calling_frame helper that raises ValueError when the frame is
unavailable.
Add docstrings to get_current_commit and get_line_str_from_frame,
and clarify that run_command stdout may contain merged stderr.
Use Generator return type for contextmanager functions (Iterator
annotation is deprecated by type checkers), parameterize bare
Iterable/Generator/dict/list hints, and reflect that run_command
accepts non-str list items.
git rev-parse ran in the current working directory, so when tests
changed CWD the reported commit could come from a different repo
(or fail). Run it in the directory of this source file instead.
Resolves the standing TODO.
start_cluster joined the script path and its arguments into one
string that run_command then re-tokenized with shlex, which would
raise on args containing quotes and split args containing spaces.
Pass the list directly.
Parameterize the remaining bare Iterable in flatten's ltypes hint,
document the actual filename#L<lineno> format, and note that an
empty GIT_REVISION falls through to git.
Cover the helpers touched by the preceding fixes: get_pool_param
key translation, check_dir_arg_keep ~ expansion, environ restore,
run_command tokenization/stdin/error handling, bech32 helper
invocation, tool_has error parsing, is_in_interval with negative
reference, and the remaining pure helpers.

The tests don't depend on external binaries - subprocess tests use
sys.executable and the bech32/tool_has tests monkeypatch
run_command.
Splitting on the first ":" selected the wrong segment when the
command string itself contained a colon, biasing the check toward
reporting the capability as present. Partition on the "`: "
delimiter that run_command actually emits.
A missing executable raised FileNotFoundError from Popen since the
shell invocation was removed, giving run_command two failure types.
Re-raise as RuntimeError naming the command, document raised
exceptions and string tokenization in the docstring, and log
ignored failures at debug level.
Document that get_current_commit is cached and anchored to this
repo, that _get_calling_frame raises ValueError when the frame is
unavailable, and reword the get_current_line_str NOTE to describe
the actual frame-depth mechanism instead of a vague warning.
The stop script path was passed to run_command as a string and
tokenized with shlex, so a state dir path containing a space or
quote would be mis-split. Pass a single-item list instead,
consistent with the start_cluster fix.
Add tests for start_cluster argv pass-through, run_in_bash
invocation, run_command failure with merged stderr, missing
executable error, empty GIT_REVISION fallback and environ restore
on exception. Clarify the module docstring on shell builtins.
Popen raises FileNotFoundError for a missing workdir too, so the
"Command not found" message could blame the wrong thing, and a
non-executable file escaped as PermissionError. Catch OSError and
re-raise as RuntimeError with neutral wording that covers all
cases.
@mkoura
mkoura requested a review from saratomaz as a code owner July 30, 2026 15:03
@mkoura
mkoura requested review from Copilot and removed request for saratomaz July 30, 2026 15:03
Comment thread cardano_node_tests/utils/helpers.py Dismissed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens cardano_node_tests.utils.helpers by fixing several latent bugs (notably around subprocess execution, env handling, and key transformations) and adds a new framework_tests/ unit-test suite that pins the corrected behavior without requiring a running cluster or project-specific binaries.

Changes:

  • Add unit tests for helpers and cluster_nodes under framework_tests/, relying on monkeypatching instead of external tools.
  • Refactor and harden helper utilities (safer bech32 invocation, improved run_command, bug fixes in environ, get_pool_param, is_in_interval, etc.).
  • Update cluster start/stop call sites to pass argv lists into run_command (avoids re-tokenization issues).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
framework_tests/test_helpers.py Adds broad unit coverage for helper utilities, including subprocess invocation semantics via monkeypatching.
framework_tests/test_cluster_nodes.py Adds a focused unit test ensuring start_cluster forwards argv lists to run_command.
cardano_node_tests/utils/helpers.py Implements the bug fixes/hardening (notably run_command, bech32 helpers, environ, tool_has, and key transformations).
cardano_node_tests/utils/cluster_nodes.py Updates start_cluster to call run_command with an argv list.
cardano_node_tests/cluster_management/manager.py Updates stop-script execution to use argv list calling convention.
cardano_node_tests/cluster_management/cluster_getter.py Updates stop-script execution to use argv list calling convention.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cardano_node_tests/utils/helpers.py
tool_has caught the RuntimeError that run_command now raises for
execution failures, so a missing or non-executable tool was
reported as having the capability. Raise the execution failure as
a CommandExecError subclass and re-raise it in tool_has, since
availability cannot be determined when the tool doesn't run at
all.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

cardano_node_tests/utils/helpers.py:147

  • For list-form commands, cmd_str is built with ' '.join(cmd), which can misrepresent argv when an argument contains spaces/quotes (e.g. it will look like multiple args). Since cmd_str is used in logs and error messages (and parsed by tool_has), consider using shlex.join(cmd) to produce an unambiguous, shell-escaped representation of the actual argv.
    else:
        cmd = [str(c) for c in command]
        cmd_str = " ".join(cmd)

cardano_node_tests/utils/helpers.py:415

  • get_pool_param still uses an unparameterized dict type for pool_params, while other updated helpers in this file use parameterized generics (e.g. dict[str, tp.Any]). Parameterizing this improves type clarity and matches the surrounding style.
def get_pool_param(key: str, *, pool_params: dict) -> tp.Any:

@mkoura
mkoura merged commit cfab881 into master Jul 30, 2026
4 checks passed
@mkoura
mkoura deleted the helpers_fixes branch July 30, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants