Skip to content

[change] Add test_connection_command_timeout_with_output#1433

Draft
asmodehn wants to merge 1 commit into
openwisp:masterfrom
asmodehn:output_lost_on_command_timeout
Draft

[change] Add test_connection_command_timeout_with_output#1433
asmodehn wants to merge 1 commit into
openwisp:masterfrom
asmodehn:output_lost_on_command_timeout

Conversation

@asmodehn

Copy link
Copy Markdown
Member

Checklist

Reference to Existing Issue

Demonstrate #1424 and (TODO) propose a solution

Description of Changes

  • Added a test to demonstrate the issue
  • Add helpers to drain stdout / stderr from the paramiko channel, whether the command completes or not
  • Add CommandTimeoutWithPartialOutput Exception (containing the actual output - seems necessary for fallback logic with sysupgrade disconnection)
  • reraise original socket.timeout exception with traceback
  • Add channel.close() on timeout

@asmodehn asmodehn self-assigned this Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

SSH timeout test coverage was expanded. The existing timeout test now verifies both command execution and timeout log messages. A new test covers commands that emit stdout and stderr before timing out, checking the logging sequence and asserting that the timeout exception contains the combined output.

Estimated code review effort: 2 (Simple) | ~10 minutes


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 2 warnings)

Check name Status Explanation Resolution
Bug Fixes ❌ Error The PR adds only a timing-based test; exec_command() still times out before reading stdout/stderr and CommandTimeoutException has no output, so the bug isn't fixed and the test won't pass. Implement timeout handling that drains partial output and stores it on the exception, or mark the test xfail until that code lands.
General Rules ❌ Error The new timeout-with-output test asserts ctx.exception.output, but exec_command() raises before stdout/stderr are read and CommandTimeoutException has no such field. Add timeout-output handling (or mark the test xfail) by draining stdout/stderr on timeout and storing partial output in the exception.
Features ⚠️ Warning #1424 exists, but I found no maintainer acceptance proof, no docs update for the feature, and this PR only adds a test. Add evidence of maintainer acceptance (label/project/approval), document the feature, and land the implementation or mark the test xfail until then.
Changes ⚠️ Warning The new timeout-output test asserts ctx.exception.output and stdout/stderr logs, but CommandTimeoutException has no output field and exec_command() raises before draining output. Either add the timeout-with-partial-output implementation (and exception payload) or mark this test expected failure until that behavior lands.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is descriptive, matches the added timeout-with-output test, and uses a valid required type prefix.
Description check ✅ Passed The description covers the checklist, issue reference, and change summary, though the issue line is not in the exact required format.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_controller/connection/tests/test_ssh.py`:
- Around line 88-111: Update test_connection_command_timeout_with_output and the
underlying exec_command timeout handling consistently: either implement timeout
output capture by reading stdout/stderr, logging the captured stdout, and
attaching the combined output to CommandTimeoutException, or remove those
assertions and mark the test as an expected failure until support is
implemented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 33a2d97b-4fcf-42f7-8e12-1c9f01a203df

📥 Commits

Reviewing files that changed from the base of the PR and between 08a0821 and 35e53d3.

📒 Files selected for processing (1)
  • openwisp_controller/connection/tests/test_ssh.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Avoid unnecessary blank lines inside function and method bodies
Be careful with authentication, authorization, queryset filtering, serializers, admin behavior, cache invalidation, signals, Celery tasks, and websocket updates in Django code
Preserve validation around templates, VPN/PKI material, SSH credentials, device commands, uploaded files, URLs, and subnet/IP data
Write comments and docstrings only when they explain why code is shaped a certain way, placing them before the relevant code block instead of scattering them inside it

Files:

  • openwisp_controller/connection/tests/test_ssh.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework.

Files:

  • openwisp_controller/connection/tests/test_ssh.py
🧠 Learnings (8)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/connection/tests/test_ssh.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/connection/tests/test_ssh.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/connection/tests/test_ssh.py
📚 Learning: 2026-06-07T12:07:08.468Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_admin.py:2335-2335
Timestamp: 2026-06-07T12:07:08.468Z
Learning: In this project’s Python test suite (files under openwisp_controller/**/tests/), don’t require or request prose/inline comments that document the breakdown of query-count changes (e.g., assertions around template/DB query counts in helpers like _verify_template_queries). Treat query-count assertions as volatile implementation details that change frequently; review should focus on whether the test asserts the expected behavior, not on explaining the specific query-count deltas in comments.

Applied to files:

  • openwisp_controller/connection/tests/test_ssh.py
📚 Learning: 2026-06-07T12:07:24.608Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/pki/tests/test_api.py:155-155
Timestamp: 2026-06-07T12:07:24.608Z
Learning: When reviewing Python test files in this repository, avoid recommending inline comments that explain or justify `assertNumQueries` (Django query count) expectations. Query counts can change frequently as implementations evolve, and inline explanations add maintenance burden; the expected count should be understandable without added comment blocks.

Applied to files:

  • openwisp_controller/connection/tests/test_ssh.py
📚 Learning: 2026-06-25T12:20:18.414Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/base/models.py:571-572
Timestamp: 2026-06-25T12:20:18.414Z
Learning: When writing or reviewing tests that override pagination behavior via OpenWispPagination.paginate_queryset(), patch `view.pagination_page_size` (not `page_size`). The method uses `getattr(view, "pagination_page_size", self.page_size)`, so tests must set the attribute on the view to affect pagination. If the view class does not define `pagination_page_size`, using `unittest.mock.patch(..., create=True)` is intentional and correct because the attribute may not exist until patched.

Applied to files:

  • openwisp_controller/connection/tests/test_ssh.py
📚 Learning: 2026-06-07T12:07:25.164Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_config.py:864-865
Timestamp: 2026-06-07T12:07:25.164Z
Learning: When reviewing this repo’s Python test suite, treat changes to the *expected* query count in `assertNumQueries(...)` calls as routine test maintenance. If a PR updates the numeric argument (e.g., in `test_config.py`, `test_api.py`, `test_admin.py`, `test_pki.py`) and the test remains consistent with the feature changes, reviewers should not flag the increased number as a performance regression that requires investigation solely because the count went up; instead, focus on whether the update is intentional and the surrounding test/code changes justify the revised expectation.

Applied to files:

  • openwisp_controller/connection/tests/test_ssh.py
📚 Learning: 2026-06-25T12:20:45.387Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/tests/test_api.py:916-932
Timestamp: 2026-06-25T12:20:45.387Z
Learning: When reviewing API pagination behavior in openwisp-controller, assume `OpenWispPagination.paginate_queryset()` allows a per-view page-size override via `getattr(view, "pagination_page_size", self.page_size)` (so `view.pagination_page_size`, if present, should affect pagination). In Python tests, it is valid to patch `pagination_page_size` on a view class even if the attribute isn’t declared on the class by default, by using `unittest.mock.patch.object(..., "pagination_page_size", ..., create=True)` so the override is available for the pagination logic during the test.

Applied to files:

  • openwisp_controller/connection/tests/test_ssh.py
🔇 Additional comments (1)
openwisp_controller/connection/tests/test_ssh.py (1)

83-87: LGTM!

Comment on lines +88 to 111
@mock.patch.object(ssh_logger, "info")
@mock.patch.object(ssh_logger, "debug")
def test_connection_command_timeout_with_output(self, mocked_debug, mocked_info):
ckey = self._create_credentials_with_key(port=self.ssh_server.port)
dc = self._create_device_connection(credentials=ckey)
dc.connector_instance.connect()
with self.assertRaises(Exception) as ctx:
# timeout of 0.0 is a special case in paramiko -> we check for 0.01 instead
dc.connector_instance.exec_command(
"echo fake_output && echo fake_error 1>&2 && sleep 1", timeout=0.01
)
log_message = "Command timed out after 0.01 seconds."
mocked_info.assert_has_calls(
[
mock.call(
"Executing command: echo fake_output &&"
" echo fake_error 1>&2 && sleep 1"
),
mock.call("fake_output\n"),
mock.call(log_message),
]
)
self.assertEqual(ctx.exception.output, "fake_output\nfake_error\n")
self.assertEqual(str(ctx.exception), log_message)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that CommandTimeoutException has no `output` attribute and exec_command
# does not set one on the timeout path.

# Check the exception class definition
cat -n openwisp_controller/connection/connectors/exceptions.py

# Check the timeout path in exec_command - look for where output is read
# relative to where CommandTimeoutException is raised
sed -n '172,232p' openwisp_controller/connection/connectors/ssh.py

# Search for any place where `output` is set on a CommandTimeoutException instance
rg -n 'CommandTimeoutException' --type py -C3

Repository: openwisp/openwisp-controller

Length of output: 3100


This test will fail as written. exec_command() raises CommandTimeoutException(log_message) before reading stdout, and CommandTimeoutException has no output field, so both ctx.exception.output and the fake_output log assertion are unsupported. Add the timeout-output handling in this PR or mark the test expected failure until the implementation lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/connection/tests/test_ssh.py` around lines 88 - 111,
Update test_connection_command_timeout_with_output and the underlying
exec_command timeout handling consistently: either implement timeout output
capture by reading stdout/stderr, logging the captured stdout, and attaching the
combined output to CommandTimeoutException, or remove those assertions and mark
the test as an expected failure until support is implemented.

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.

1 participant