Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
57e5177
Add shared test fixtures and split py/tests into unit and integration
Manik-Khajuria-5 Jul 31, 2026
c46708d
Add a visdom_server fixture for tests that need real HTTP
Manik-Khajuria-5 Jul 31, 2026
5c5cc9f
Test window create, read, close and ordering over HTTP
Manik-Khajuria-5 Jul 31, 2026
ddb31b2
Test every pane type created through /events
Manik-Khajuria-5 Jul 31, 2026
4c74855
Test env creation, fork, save, delete and reload over HTTP
Manik-Khajuria-5 Jul 31, 2026
b726f59
Test env id escaping, error routes and awkward content
Manik-Khajuria-5 Jul 31, 2026
a7b61f2
Stop a closed window's index being handed to the next one
Manik-Khajuria-5 Jul 31, 2026
f1ada52
Reject malformed requests with 400 instead of a bare assert
Manik-Khajuria-5 Jul 31, 2026
4f12152
Merge branch 'dev' into TestingP1-1
Manik-Khajuria-5 Jul 31, 2026
0cadbbe
Merge branch 'TestingP1-1' into TestingP2
Manik-Khajuria-5 Jul 31, 2026
adb8c75
Drive the HTTP tests from VisdomHTTPTestCase again
Manik-Khajuria-5 Jul 31, 2026
662202a
Name the two ids in the window-replacement test
Manik-Khajuria-5 Jul 31, 2026
336c990
Document when a test is a TestCase and when it is a function
Manik-Khajuria-5 Jul 31, 2026
9e165ac
Split the update-path tests into text/media and plot files
Manik-Khajuria-5 Aug 1, 2026
a7b0f92
Land the memory cap tests as plain pytest functions
Manik-Khajuria-5 Aug 1, 2026
fcf82c1
Answer 200 instead of 500 on categorical x-values and short updates
Manik-Khajuria-5 Aug 1, 2026
ed8c796
Cap plot_history growth the way image history is capped
Manik-Khajuria-5 Aug 1, 2026
bf2cec3
Land the build verification tests under integration
Manik-Khajuria-5 Aug 1, 2026
e363d1f
Rescue the t-SNE unit tests into the tracked suite
Manik-Khajuria-5 Aug 1, 2026
59e706b
Cover the shared utility helpers
Manik-Khajuria-5 Aug 1, 2026
cad5892
Write environments atomically so an interrupted save keeps the old one
Manik-Khajuria-5 Aug 1, 2026
0a36686
Pin down LazyEnvData's mapping and deep-copy behaviour
Manik-Khajuria-5 Aug 1, 2026
cf71eed
Move the manual visual check script out of the test suite
Manik-Khajuria-5 Aug 1, 2026
fbceede
Cover socket open, registration and close
Manik-Khajuria-5 Aug 1, 2026
0a3fc06
Let a source socket save layouts instead of raising
Manik-Khajuria-5 Aug 1, 2026
07c1958
Send the closed pane data to sources instead of None
Manik-Khajuria-5 Aug 1, 2026
82ef6cf
Guard the embeddings pop and the plot history frame index
Manik-Khajuria-5 Aug 1, 2026
c616548
Cover every socket command dispatched by on_message
Manik-Khajuria-5 Aug 1, 2026
c74bf4f
Let an unnamed delete through the empty-data shortcut
Manik-Khajuria-5 Aug 1, 2026
92d8b9a
Merge branch 'dev' into Testing-5
Manik-Khajuria-5 Aug 2, 2026
0c503f5
test: drop redundant test_ prefix from files under py/tests
Manik-Khajuria-5 Aug 2, 2026
0604e19
Hand the polling source socket the request it opens with
Manik-Khajuria-5 Aug 2, 2026
f0b4d38
Cover the polling transport and its parity with sockets
Manik-Khajuria-5 Aug 2, 2026
311073a
Compare login credentials in constant time
Manik-Khajuria-5 Aug 2, 2026
d8f4164
Reject writes over HTTP while the server is readonly
Manik-Khajuria-5 Aug 2, 2026
da0b3ed
Cover login, authorization and readonly enforcement
Manik-Khajuria-5 Aug 2, 2026
b65af76
Show error details only when debug logging is on
Manik-Khajuria-5 Aug 2, 2026
24623b2
skip comparison panes whose base window carries no plottable content
Manik-Khajuria-5 Aug 3, 2026
6db2224
cover environment comparison merging, numbering and the legend pane
Manik-Khajuria-5 Aug 3, 2026
a4fd262
cover environment upload, fork, save and raw window data
Manik-Khajuria-5 Aug 3, 2026
a6b1313
cover the server command line and the asset download table
Manik-Khajuria-5 Aug 3, 2026
ceb06bf
Apply the tick step option when it is the only axis option given
Manik-Khajuria-5 Aug 4, 2026
df10040
Silence the label normalization warning on non-finite input
Manik-Khajuria-5 Aug 4, 2026
cce13be
Cover the client curve and metric helpers
Manik-Khajuria-5 Aug 4, 2026
5a36d4b
Cover the client layout, marker and option helpers
Manik-Khajuria-5 Aug 4, 2026
55a2725
Cover the client shape, hierarchy and network payloads
Manik-Khajuria-5 Aug 5, 2026
fad8e18
Cover the client media payloads and fix the image grid tile offset
Manik-Khajuria-5 Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 88 additions & 8 deletions .agents/context/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ pytest -m "not server" # skip tests that need a live server (CI
```

Config lives in `pyproject.toml` (`[tool.pytest.ini_options]`): discovery is scoped to
`py/tests/` and `pythonpath = ["py"]` makes `import visdom` work without an editable install.
Experimental `test_*.py` scripts in the repo root (and `test/`) are intentionally out of scope.
`py/tests/`, and `pythonpath = ["py", "py/tests"]` makes both `import visdom` and
`import testutils` work without an editable install. Because discovery is scoped by `testpaths`,
experimental `test_*.py` scripts in the repo root (and `test/`) stay out of scope; `testutils/` is
excluded via `norecursedirs` so helpers are importable but never collected.

## Run E2E / Visual Tests (Cypress)

Expand All @@ -38,12 +40,90 @@ Always use port `8098` and `-env_path /tmp` for isolation.

## Writing Python Tests

- Place in `py/tests/`, name files `test_*.py`, classes `Test*` (unittest `TestCase` or plain
pytest functions both work — pytest auto-discovers unittest).
- Keep them hermetic (no running server). A test that genuinely needs a live server must be
marked `@pytest.mark.server` so CI can deselect it.
- Start with simple hermetic tests (e.g., `test_smoke.py`) and add focused unit tests for
server/window/env lifecycle code as coverage grows.
### Where a test goes

```
py/tests/
conftest.py shared fixtures, auto-loaded by pytest
testutils/ importable helpers (fakes, payload builders, HTTP base class)
unit/ pure logic: no Application, no I/O beyond tmp_path
integration/ in-process Application, real HTTP, or handler dispatch
```

`py/tests/` has **no** `__init__.py` on purpose — `setup.py` runs `find_packages(where="py")`,
so a package there would ship a top-level `tests` distribution to users. `testutils/` is a
package and is reachable because `py/tests` is on `pythonpath`.

Everything under `py/tests/` must be **hermetic and collectable**: no externally launched
server, no browser, no assertion a human has to make. A script that needs a live server and is
judged by looking at the UI goes in `example/manual/` instead — see
`example/manual/visual_check.py`. Pixel correctness is Playwright's and Cypress's job, not
pytest's.

- Name a file after what it covers — `integration/window_types.py`, not
`integration/test_window_types.py`. The `unit/` and `integration/` directories already say these
are tests, so the filename does not repeat it; `python_files = ["*.py"]` in `pyproject.toml`
collects them, and `norecursedirs` keeps `testutils/` importable but uncollected. Test
*functions* and `Test*` classes still need their usual prefixes.
- **Which style you use depends on whether the test needs HTTP.**

| Test needs | Write | Why |
|---|---|---|
| no `Application`, or a handler object | plain `def test_*()` functions | fixtures and `parametrize` both work |
| a real HTTP round trip | a `VisdomHTTPTestCase` subclass | `tornado.testing.AsyncHTTPTestCase` is a `unittest.TestCase`, and that is what starts the app |

**pytest cannot inject fixtures into `TestCase` methods** — `def test_x(self, app)` fails, and
only autouse fixtures reach them. `@pytest.mark.parametrize` does not work on them either; use
a small `_assert_*` helper called from several one-line test methods instead. A module-level
`pytestmark = pytest.mark.integration` **does** apply to `TestCase` classes, so always set one.
- Keep them hermetic. A test that needs an **externally launched** server must be marked
`@pytest.mark.server` so CI can deselect it; nothing in the tracked suite needs one today.

### Shared fixtures (`py/tests/conftest.py`)

| Fixture | Gives you |
|---|---|
| `env_path` | disposable environment directory |
| `store` / `spy_store` | `JSONStore` / one that records backend calls |
| `app` / `app_factory` | `Application` on a temp `env_path`; factory for reload assertions |
| `handler` / `app_handler` | duck-typed handler, standalone or sharing an `Application`'s state |
| `fake_socket` | records `write_message`; `.commands()` and `.last(cmd)` for assertions |
| `offline_client` | `Visdom(send=False)` — never opens a connection |
| `capture_send` | runs a client call and returns the payload it would have sent |

`reset_warn_once` is autouse: `shared_utils.warn_once` dedupes against a module-level set, so
without it a warning raised by one test silently suppresses the same warning in another.

### HTTP tests

Subclass `testutils.VisdomHTTPTestCase`. It starts the app in-process on an ephemeral port, gives
every test a fresh `env_path` that is cleaned up in `tearDown`, and provides `post_json`,
`create_window`, `create_text_window`, `update`, `close_window`, `win_exists`, `get_win_data`,
`get_envs`, `save` and `panes`, on top of `AsyncHTTPTestCase`'s own `fetch`. Override the
`app_kwargs` class attribute to vary server configuration:

```python
class TestReadonlyRoutes(VisdomHTTPTestCase):
app_kwargs = {"readonly": True}
```

`AsyncHTTPTestCase` already runs the `Application` in-process on its own `IOLoop`, driving each
request through `io_loop.run_sync`. **Do not replace it with a background thread, a hand-rolled
`asyncio` loop, or an out-of-process server** — none of that buys anything, and it was tried and
reverted. The `TestCase` style is the accepted cost of using it.

Because fixtures cannot reach these tests, anything shared goes on the class: `self.env_path` for
the temp directory, and a small base class between `VisdomHTTPTestCase` and your test classes for
helpers several of them need (see `WindowTypeTestCase` in `integration/window_types.py`).
Need a second `Application` over the same directory, for a reload assertion? Construct it directly
with `Application(port=8097, env_path=self.env_path)` — `app_factory` is not available here.

### Markers

`unit`, `integration`, `slow`, and `server` are registered in `pyproject.toml`. Set
`pytestmark = pytest.mark.unit` or `pytest.mark.integration` at the top of every new file; it
works for both plain functions and `TestCase` classes. Many older files predate this and carry no
marker, so `-m integration` currently under-selects.

## CI

Expand Down
31 changes: 31 additions & 0 deletions example/manual/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!-- Copyright 2017-present, The Visdom Authors
All rights reserved.

This source code is licensed under the license found in the
LICENSE file in the root directory of this source tree. -->

# Manual checks

Scripts here drive a **running** visdom server and are judged by looking at the
browser. They are not tests: pytest does not collect them (`testpaths` is
`py/tests`), and nothing in CI runs them.

Automated checks belong elsewhere:

| Question | Where it is answered |
|---|---|
| Does the server behave correctly? | `py/tests/` (pytest) |
| Does the UI still render the same pixels? | `playwright/`, `cypress/` |
| Does this *look* right to a person? | here |

## `visual_check.py`

Creates one window of each visualization type in the `visual_check` environment,
then prints a checklist to walk through in the browser. Useful after a
dependency bump or a change to the plotting payloads.

```bash
visdom -port 8097 -env_path /tmp # in one shell
python example/manual/visual_check.py
# then open http://localhost:8097/env/visual_check
```
Loading