Skip to content

test: convert the last unittest classes onto pytest fixtures (PyTest - 11) - #1694

Open
Manik-Khajuria-5 wants to merge 57 commits into
fossasia:devfrom
Manik-Khajuria-5:convert-02-units
Open

test: convert the last unittest classes onto pytest fixtures (PyTest - 11)#1694
Manik-Khajuria-5 wants to merge 57 commits into
fossasia:devfrom
Manik-Khajuria-5:convert-02-units

Conversation

@Manik-Khajuria-5

@Manik-Khajuria-5 Manik-Khajuria-5 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Description

Converts the six remaining unittest.TestCase files to plain pytest functions on the shared fixtures.

No production code changes, no behaviour changes, no new tests. py/tests/unit/now has no TestCase classes; every remaining one is an HTTP test in integration/.

Fixes : #1695

Motivation and Context

A TestCase method cannot receive a pytest fixture, so each class re-implemented setup conftest.py already provides.

How Has This Been Tested?

Python 3.12.13, pytest -m "not server".

  • 1,515 tests pass
  • black --check py clean under 23.1.0, the version pinned in

Types of changes

  • Code refactor or cleanup (changes to existing code for improved readability or performance)

Checklist:

  • I adapted the version number under py/visdom/VERSION not applicable, test-only change
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly `.agents/conte the plain-function rule.

Summary by Sourcery

Refactor tests to use pytest fixtures and expand coverage around server, client, storage, and CI behavior while keeping production functionality unchanged.

Enhancements:

  • Tighten server robustness and security with readonly checks on HTTP routes, safer env file writes, improved socket and layout handling, and constant-time login credential comparison.
  • Add new server defaults and options for plot history caps and error-page detail, and refine image, slider, and environment comparison behavior without changing public APIs.

Build:

  • Adjust packaging to exclude tests from the distributed package and ensure static assets and coverage configuration are correctly wired.

CI:

  • Split Python test workflow into fast unit gate and full matrix job, and run pytest with coverage reporting in CI.

Documentation:

  • Expand testing documentation and agent context to describe pytest markers, test layout, and fast feedback loops for contributors.

Tests:

  • Convert remaining unittest-based tests to pytest functions with shared fixtures and reorganize tests into unit and integration suites.
  • Add extensive new unit and integration tests for client plotting, media handling, metrics, server handlers, sockets, storage, environment lifecycle, and t-SNE helpers.
  • Introduce manual visual regression script and update testing documentation and context guidance for test placement and markers.

Adds conftest.py, a testutils package, and unit tests for window().
AsyncHTTPTestCase is a unittest.TestCase, and pytest will not inject
fixtures into those. Run the Application on a background event loop
instead and talk to it with requests, which is already a runtime
dependency, so HTTP tests can be plain functions.
Lands the window lifecycle file as plain pytest functions on the
visdom_server fixture, dropping the per-file setUp block and the
temp directory it never cleaned up.
Collapses the repeated plot-trace and opts assertions into parametrized
tables, so each case reports separately instead of hiding behind the
first failure.
The reload case now builds its second Application from the app_factory
fixture, which shares env_path with visdom_server, instead of hand-rolling
one against a leaked temp directory.
The update-into-a-missing-env case asserted 'status is 200 or 500',
which could not fail. It now pins the actual behaviour: 200 with
'win does not exist'.
register_window took the index from len(env), so closing any window but
the last produced a duplicate index and the pane order went undefined
after a reload. Use max(i) + 1, which is what the undo path already does.
Three validation checks used a bare assert, so an invalid request came
back as a 500 and, under python -O, was skipped entirely: forking an
unknown env then raised KeyError and a named trace update read past the
data it was given. Raise HTTPError(400) like the other checks do.
Places the two test files dev added into the new layout, and points the
new storage-wiring test at env_payload: it called the local _env helper
this branch had already replaced, which merged cleanly and then failed.
Brings in dev along with the layout placement and the env_payload fix
made on the PR-1 branch. D1 and D2 both survive the auto-merge of
server_utils.py and web_handlers.py.
Replaces the background event loop and requests session with the
AsyncHTTPTestCase base class from PR-1, which already runs the app
in-process on an ephemeral port. The four integration files become
TestCase subclasses; pytest still collects and marks them.
first/second read as two windows; they are the id returned by each call,
which is the same id both times. Also assert only one pane exists.
The guidance still said VisdomHTTPTestCase was scheduled for replacement
by a background-loop fixture. Record the opposite, and the consequences
for anyone writing an HTTP test: no fixtures, no parametrize, share via
a base class.
Both halves now run off VisdomHTTPTestCase instead of their own fixture block.
The three cap tables collapse onto parametrize; the file was untracked until now.
Guard the missing-point check behind a numeric test, and stop indexing data and
traces past their length when an update supplies fewer entries than the plot.
It was the one history pane that grew without bound.
Removing a heatmap names no trace and posts no data, so the shortcut for
opts-only updates returned before the delete branch and left the plot up.
write_error rendered the exception, its traceback and request.__dict__
whenever tornado's debug flag was set, and app.py set that flag
unconditionally -- so every deployment served its own source paths and
request internals on any 500, and error.html's production branch was
unreachable. The flag also turned on autoreload for every server as a
side effect.

Replaces it with a show_error_details setting driven by the root logging
level, so -logging_level DEBUG brings the detail back and nothing else
changes with it.
TestClientMessageShapes built its client with Visdom(send=False) but left
use_incoming_socket at its default, so every one of its six tests opened an
incoming socket and waited out the connect timeout: 6.30s each, 37.8s of the
file's 37.99s and of the whole suite's 43s.

Those six drive no Application and no HTTP, so they belong in unit/ on the
offline_client fixture rather than here; they move to unit/client_content.py.
The two AsyncHTTPTestCase classes stay TestCases, as the HTTP round trip
requires, and lose their duplicated setUp/get_app/post_json to
VisdomHTTPTestCase. The readonly class expresses its server through the
app_kwargs hook the base class already provides.

The file now runs in 0.17s instead of 37.99s.
Adds unit/client_content.py: text, properties, table, embeddings and its
event handlers, learning_curve, update_window_opts, the window and env writes,
and the four methods that parse the server's reply rather than returning it.
The experiment message-shape tests move here from integration/.

The reply parsers cannot use capture_send -- with send=False, _send returns a
(msg, endpoint) tuple and json.loads chokes on it -- so they patch _send with
a canned reply instead.

Fixes get_env_list, which was documented and typed as returning a list of env
names but returned the current env's pane dict. _send defaults a missing eid
to the client's env, and /env_state answers with one env's windows whenever it
is given an eid, returning the env list only when it is not. A new
default_eid=False keyword lets a caller opt out of that defaulting; every
route that wants the current env is unaffected.

Two behaviours are pinned rather than changed: properties validates none of
the five property types it documents, and embeddings raises on a client with
no incoming socket unless opts.register_embedding_events is False.
Eight files carried no module-level marker, so -m unit and -m integration
selected 1165 of 1428 tests between them and 263 were in neither bucket. With
the markers backfilled the two selections now sum to the whole suite, which is
the property the new CI split depends on. unit/server_utils.py was also
missing the license header AGENTS.md requires.

python-tests.yml gains a unit job that runs in about a second and gates the
3.12/3.13 matrix, so an obvious break fails before three torch installs
happen. The matrix job now runs with --cov-fail-under=80 against a measured
84%; the gap is deliberate headroom so an unrelated PR does not go red on
rounding. The flags live in the workflow rather than in pyproject's addopts,
because a floor is meaningless on the single-file runs done while writing a
test and addopts would make every local pytest hard-require pytest-cov.

Documents the marker meanings, the bucket-sum invariant and the socket
timeout that made the suite slow, so the next person does not reintroduce it.
Drops --cov-fail-under from the workflow. CI still runs --cov=visdom
--cov-report=term-missing, so the number is on every run and the next PR can
pick a threshold from real data instead of guessing one.

Choosing that threshold is its own decision and does not belong bundled with
the suite work. The candidates are not equivalent: 84 is an exact ratchet with
no slack for the fact that CI has torch and av installed where a dev machine
may not, and 100 is unreachable without either covering the sklearn logger --
whose autolog() monkey-patches every estimator with no un-patch API -- or
pragma-ing out roughly 500 lines, which would make the number meaningless.

The docs now say coverage is reported rather than enforced, and record what a
future floor has to account for.

@sourcery-ai sourcery-ai 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.

Sorry @Manik-Khajuria-5, your pull request is larger than the review limit of 150000 diff characters

@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors the test suite and server utilities to fully migrate Python unit tests to pytest with shared fixtures, add extensive unit and integration coverage for client and server behavior (including plots, media, embeddings, sockets, env lifecycle, storage, and t-SNE), tighten server correctness and robustness (read-only enforcement, auth, error handling, caps on histories, atomic env writes), adjust pytest and CI configuration, and document the new testing strategy.

Sequence diagram for readonly HTTP write rejection

sequenceDiagram
    actor Client
    participant PostHandler
    participant check_auth
    participant check_readonly
    participant reject_readonly

    Client->>PostHandler: post()
    activate PostHandler
    PostHandler->>check_auth: _check_auth(handler)
    activate check_auth
    check_auth-->>PostHandler: auth ok
    deactivate check_auth
    PostHandler->>check_readonly: _check_readonly(handler)
    activate check_readonly
    alt [handler.readonly]
        check_readonly->>reject_readonly: reject_readonly(handler)
        activate reject_readonly
        reject_readonly-->>Client: 403 {success: False, error: ...}
        deactivate reject_readonly
    else [not handler.readonly]
        check_readonly-->>PostHandler: proceed
        PostHandler-->>Client: state-changing response
    end
    deactivate check_readonly
    deactivate PostHandler
Loading

File-Level Changes

Change Details Files
Converted remaining unittest-based tests to pytest-style functions with shared fixtures, reorganised tests into unit/integration directories, and updated pytest/CI configuration and docs to enforce a hermetic, fast, marker-based test suite.
  • Replaced unittest.TestCase-based tests with pytest functions using shared fixtures in conftest.py for applications, JSONStore backends, fake handlers/sockets, and offline clients.
  • Split tests into py/tests/unit/ and py/tests/integration/, updated AGENTS.md and .agents/context/testing.md to document where tests belong and to require module-level pytestmark markers (unit/integration/server).
  • Adjusted pyproject.toml to collect all *.py under py/tests, added pythonpath entry for py/tests so testutils can be imported, and excluded testutils from discovery via norecursedirs.
  • Introduced a new fast unit-only CI job (pytest -m unit) and wired the existing matrix job to run pytest -m "not server" with coverage flags only in CI.
  • Moved smoke and socket_setup tests into py/tests/unit and marked them as unit to participate in the new gate.
py/tests/conftest.py
.agents/context/testing.md
AGENTS.md
py/tests/unit/smoke.py
py/tests/unit/socket_setup.py
pyproject.toml
.github/workflows/python-tests.yml
setup.py
Expanded test coverage for the Python client’s plotting, media, metrics, helpers, and content APIs using an offline Visdom client and a capture_send fixture, and moved experiment client message tests into unit coverage.
  • Added unit tests for the client’s line/scatter/bar/histogram/box/surf/contour/dual-axis and grid plots to assert payload structure, update semantics, and validation behavior.
  • Added unit tests for image/audio/video/svg payloads (including edge cases like float ranges, normalize behaviour, image grids, heatmaps, slider updates, and caption/markup handling).
  • Added unit tests for client helpers (_axisformat, _opts2layout, _normalize_labels, marker/line colour/size checks, dashCheck, _decode_binary_arrays, _assert_opts) to pin their contracts and mutation behavior.
  • Added unit tests for metrics helpers and plotting APIs (ROC, PR, confusion matrix) to validate numeric correctness, input modes (raw vs precomputed), and default titles/legends.
  • Moved the old TestClientMessageShapes (experiment/log_metrics) from the integration test into a new unit test module that uses the offline_client and capture_send fixtures to assert experiment-related message payloads and error handling.
py/tests/unit/plots.py
py/tests/unit/client_payloads_media.py
py/tests/unit/client_payloads_graph.py
py/tests/unit/client_helpers.py
py/tests/unit/client_metrics.py
py/tests/unit/client_content.py
Added unit tests for server-side utilities (shared_utils, server_utils, window building, t-SNE helpers, memory caps) and refactored helpers to improve robustness (e.g., safe NaN handling, categorical axes, error messages).
  • Introduced unit tests for shared_utils (_sanitize_nans, NanSafeEncoder, warn_once, _is_missing_value, get_visdom_path, ensure_dir_exists, get_rand_id, _coerce_image_slider_index) and ensured warn_once can be safely used across tests via an autouse reset fixture.
  • Added unit tests for server_utils helpers (escape_eid, extract_eid, hash_password, stringify, recursive_order) to pin their output formats and constraints.
  • Added unit tests for window() and update_window() (including different pane types, slider/history metadata, opts/layout propagation, and legend updates).
  • Added unit tests for UpdateHandler caps on text, embeddings history, image history, and plot history, and wired a max_plot_history setting through defaults, Application, and BaseHandler.
  • Added unit tests for t-SNE helpers (_get_perplexity, _normalize_tsne, backend selection for openTSNE vs bhtsne) using module reloads under patched sys.modules.
  • Refined UpdateHandler.update to use a shared _is_missing_value helper to safely skip numeric gaps in plot updates (instead of math.isnan on arbitrary types), and to enforce better error messages and index handling for plot updates and image sliders.
py/visdom/utils/shared_utils.py
py/visdom/server/handlers/web_handlers.py
py/visdom/server/defaults.py
py/visdom/server/app.py
py/visdom/server/handlers/base_handlers.py
py/tests/unit/shared_utils.py
py/tests/unit/server_utils.py
py/tests/unit/window_builder.py
py/tests/unit/memory_caps.py
py/tests/unit/tsne.py
py/tests/unit/image_slider.py
Strengthened server correctness and robustness for HTTP handlers, sockets, and storage (auth, readonly mode, error pages, env comparisons, JSONStore durability, polling parity), and added targeted integration tests to lock these behaviors.
  • Made BaseHandler.write_error honor a new show_error_details flag instead of Tornado’s debug, and wired Application to set it based on the root logger’s level; added tests to ensure production error pages hide tracebacks and requests while debug mode shows them.
  • Introduced a check_readonly decorator and reject_readonly helper to enforce readonly mode on HTTP write handlers, and ensured mixed read/write endpoints like /win_data handle readonly writes explicitly; added integration tests to verify readonly enforcement for HTTP and sockets.
  • Hardened web_handlers for fork_env (HTTPError with safe reason), env_state (/env_state distinction between env list and env contents via default_eid), and image slider / plot updates (named trace validation, heatmap deletion path, embeddings pop safety).
  • Refactored auth (LoginHandler) to compare username and password in constant time using hmac.compare_digest and to ensure both halves are always checked; added integration tests for login page, cookie issuance, authenticated vs unauthenticated routes, and constant-time comparison behavior.
  • Reworked AnySocketHandlerOrWrapper.on_message for close/undo/delete_env/save/save_all/save_layouts/layout updates/forward_to_vis/pop_embeddings/echo parity across WebSocket and polling transports; added integration tests that drive the full command set through real sockets and polling wrappers.
  • Improved JSONStore durability by adding an _atomic_write helper and using it for serialize_env and save_undo so a crash mid-write cannot corrupt env files; added unit tests that simulate interrupted writes and stranded temp files.
  • Fixed compare_envs to be more defensive (skip malformed panes, support empty content, avoid mutating source envs, handle unnamed/untitled panes) and added integration tests that exercise plot/image comparison, numbering, show_all behavior, and legend rendering.
  • Improved socket_handlers (AnySocketHandlerOrWrapper and VisSocketWrapper) for layout broadcasting, polling monitor, correct env deletion event emission, and listener initialization; added integration tests for socket lifecycle, polling parity, and message queue behavior.
  • Added integration tests for env/window lifecycle, storage wiring (ensuring JSONStore is used rather than direct file I/O), and edge cases (env id escaping, malformed requests, error routes, window ordering).
py/visdom/server/handlers/web_handlers.py
py/visdom/server/handlers/socket_handlers.py
py/visdom/utils/server_utils.py
py/visdom/data_model/json_store.py
py/visdom/server/app.py
py/visdom/server/handlers/base_handlers.py
py/tests/integration/auth.py
py/tests/integration/polling_parity.py
py/tests/integration/socket_commands.py
py/tests/integration/storage_wiring.py
py/tests/integration/window_lifecycle.py
py/tests/integration/environment_lifecycle.py
py/tests/integration/env_transfer.py
py/tests/integration/update_plots.py
py/tests/integration/update_text_media.py
py/tests/integration/window_types.py
py/tests/integration/edge_cases.py
py/tests/integration/socket_lifecycle.py
py/tests/integration/build_verification.py
py/tests/testutils/*.py
Adjusted packaging to avoid shipping tests and clarified test-only paths and ignores.
  • Updated setup.py to exclude tests packages (tests, tests.*) from distribution via find_packages(where="py", exclude=[...]).
  • Ensured py/tests has no init.py so it does not become a package and get shipped; documented this in context/testing.md and AGENTS.md.
  • Documented and enforced that testutils is importable but excluded from pytest discovery via norecursedirs, preventing helper modules from being collected as tests.
  • Updated .gitignore (no-op in the diff snippet but present) to keep build/test artifacts from being tracked if applicable.
setup.py
.agents/context/testing.md
AGENTS.md
.gitignore

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

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.

Add a Python test suite (pytest) for the server and client

1 participant