Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snapshot testing #793

Merged
merged 48 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e981e3b
Add docs for CSS variables
darrenburns Sep 15, 2022
74764ae
Merge branch 'css' of github.com:Textualize/textual into css
darrenburns Sep 15, 2022
2f01453
Merge branch 'css' of github.com:Textualize/textual into css
darrenburns Sep 15, 2022
e8acba5
Merge branch 'css' of github.com:Textualize/textual into css
darrenburns Sep 15, 2022
d619dae
Snapshot testing progress
darrenburns Sep 16, 2022
13b27e1
Updates
darrenburns Sep 20, 2022
7428a16
Merge branch 'css' of github.com:willmcgugan/textual into snapshot
darrenburns Sep 20, 2022
0cd5c0a
Merge branch 'css' of github.com:willmcgugan/textual into snapshot
darrenburns Sep 20, 2022
c9cdb97
Fix off by one
darrenburns Sep 20, 2022
a871e27
Update pre-commit-config to exclude snapshot tests
darrenburns Sep 20, 2022
c82a143
Snapshot tests output directory ignored
darrenburns Sep 20, 2022
8f64e36
Small tidy up
darrenburns Sep 20, 2022
4a4eeb5
Most different snapshots first
darrenburns Sep 20, 2022
cdd2c01
Fix type imports from pytest
darrenburns Sep 20, 2022
8639981
Remove unused pytest hookimpl
darrenburns Sep 20, 2022
0978eb5
Remove snapshot_report from git
darrenburns Sep 20, 2022
e3a917e
Fix guide example
darrenburns Sep 20, 2022
5e1c47b
Remove unused pytest marker declaration
darrenburns Sep 20, 2022
29da6ba
Use type hints and add docstring to snap_compare
darrenburns Sep 20, 2022
2bed87f
Run black on snapshot_tests/conftest
darrenburns Sep 20, 2022
0ce222d
Remove redundant info from docstring
darrenburns Sep 20, 2022
c54c844
Update usages of approx
darrenburns Sep 20, 2022
d2f8ad5
Add Makefile target for snapshot update
darrenburns Sep 20, 2022
7e72cf1
Update snapshots
darrenburns Sep 20, 2022
26fb890
Skip artifact creation/upload on Windows
darrenburns Sep 20, 2022
8e09c02
Adding windows snapshot testing conditional on github actions
darrenburns Sep 20, 2022
17f2e3e
Intentionally trigger test failure to test artifact upload
darrenburns Sep 20, 2022
da05b66
Intentionally trigger test failure to test artifact upload
darrenburns Sep 20, 2022
9606d43
Intentionally trigger test failure to test artifact upload
darrenburns Sep 20, 2022
49abcba
Upload snapshot report when build fails
darrenburns Sep 20, 2022
56b7ec7
Change write mode
darrenburns Sep 20, 2022
6a63ffb
Make output dir if not exists
darrenburns Sep 20, 2022
93fab61
Merge branch 'css' of github.com:willmcgugan/textual into snapshot
darrenburns Sep 20, 2022
4a46257
Revert center layout snapshot to make it pass
darrenburns Sep 20, 2022
1a65895
Reenable artifacts on windows
darrenburns Sep 21, 2022
644922c
Write snapshot file as utf-8
darrenburns Sep 21, 2022
bc16e16
Merge branch 'css' of github.com:Textualize/textual into snapshot
darrenburns Sep 21, 2022
53ac7b4
Force legacy windows False on the app
darrenburns Sep 21, 2022
04a513f
Add debug info
darrenburns Sep 21, 2022
f88a13f
Fix snapshot
darrenburns Sep 21, 2022
8db7a07
Force legacy_windows to False in Textual
darrenburns Sep 21, 2022
cbe91fd
Ensure legacy_windows is False on Console instance which exports scre…
darrenburns Sep 22, 2022
b5540ca
Add toggle for overlay mode
darrenburns Sep 22, 2022
1486e14
Merge branch 'snapshot' of github.com:Textualize/textual into snapshot
darrenburns Sep 22, 2022
ee0eed9
Remove some unused CSS in snapshot report
darrenburns Sep 22, 2022
cc352fa
Formatting
darrenburns Sep 22, 2022
9e2500a
Quick internal guide to snapshot testing
darrenburns Sep 22, 2022
0c0f5cd
Tidying up
darrenburns Sep 22, 2022
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
6 changes: 6 additions & 0 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,9 @@ jobs:
run: |
source $VENV
python e2e_tests/sandbox_basic_test.py basic 2.0
- name: Upload snapshot report
if: always()
uses: actions/upload-artifact@v3
with:
name: snapshot-report-textual
path: tests/snapshot_tests/output/snapshot_report.html
Comment on lines +46 to +51
Copy link
Member Author

Choose a reason for hiding this comment

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

This runs on both failure and success. On success, there's no snapshot report, but the step still succeeds.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,6 @@ venv.bak/

# mypy
.mypy_cache/

# Snapshot testing report output directory
tests/snapshot_tests/output
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ repos:
hooks:
- id: black
exclude: ^tests/
exclude: ^tests/snapshot_tests
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ test:
pytest --cov-report term-missing --cov=textual tests/ -vv
unit-test:
pytest --cov-report term-missing --cov=textual tests/ -vv -m "not integration_test"
test-snapshot-update:
pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update
Comment on lines +5 to +6
Copy link
Member Author

Choose a reason for hiding this comment

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

Since this runs all tests, it'll update all snapshots.

typecheck:
mypy src/textual
format:
Expand All @@ -14,4 +16,3 @@ docs-build:
mkdocs build
docs-deploy:
mkdocs gh-deploy

43 changes: 43 additions & 0 deletions notes/snapshot_testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Snapshot Testing
Copy link
Member Author

Choose a reason for hiding this comment

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

Internal docs



## What is snapshot testing?

Some tests that run for Textual are snapshot tests.
When you first run a snapshot test, a screenshot of an app is taken and saved to disk.
Next time you run it, another screenshot is taken and compared with the original one.

If the screenshots don't match, it means something has changed.
It's up to you to tell the test system whether that change is expected or not.

This allows us to easily catch regressions in how Textual outputs to the terminal.

Snapshot tests run alongside normal unit tests.

## How do I write a snapshot test?

1. Inject the `snap_compare` fixture into your test.
2. Pass in the path to the file which contains the Textual app.

```python
def test_grid_layout_basic_overflow(snap_compare):
assert snap_compare("docs/examples/guide/layout/grid_layout2.py")
```

`snap_compare` can take additional arguments such as `press`, which allows
you to simulate key presses etc.
See the signature of `snap_compare` for more info.

## A snapshot test failed, what do I do?

When a snapshot test fails, a report will be created on your machine, and you
can use this report to visually compare the output from your test with the historical output for that test.

This report will be visible at the bottom of the terminal after the `pytest` session completes,
or, if running in CI, it will be available as an artifact attached to the GitHub Actions summary.

If you're happy that the new output of the app is correct, you can run `pytest` with the
`--snapshot-update` flag. This flag will update the snapshots for any test that is executed in the run,
so to update a snapshot for a single test, run only that test.

With your snapshot on disk updated to match the new output, running the test again should result in a pass.
418 changes: 363 additions & 55 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ nanoid = "^2.0.0"
dev = ["aiohttp", "click", "msgpack"]

[tool.poetry.dev-dependencies]
pytest = "^6.2.3"
pytest = "^7.1.3"
black = "^22.3.0"
mypy = "^0.950"
pytest-cov = "^2.12.1"
Expand All @@ -48,13 +48,15 @@ pre-commit = "^2.13.0"
pytest-aiohttp = "^1.0.4"
time-machine = "^2.6.0"
Jinja2 = "<3.1.0"
syrupy = "^3.0.0"
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the snapshot testing library we're wrapping which handles associating pytest test functions with snapshots on disk.


[tool.black]
includes = "src"

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = "--strict-markers"
Copy link
Member Author

Choose a reason for hiding this comment

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

IMO this should be the default - if you use an undefined marker it'll fail fast - good for catching typos.

markers = [
"integration_test: marks tests as slow integration tests (deselect with '-m \"not integration_test\"')",
]
Expand Down
70 changes: 52 additions & 18 deletions src/textual/_doc.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
from __future__ import annotations

import os
import runpy
import shlex
from typing import TYPE_CHECKING, cast
from typing import Iterable

from textual._import_app import AppFail, import_app
from textual.app import App
from textual._import_app import import_app

if TYPE_CHECKING:
from textual.app import App

# This module defines our "Custom Fences", powered by SuperFences
# @link https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#custom-fences
def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str:
"""A superfences formatter to insert a SVG screenshot."""
"""A superfences formatter to insert an SVG screenshot."""

try:
cmd: list[str] = shlex.split(attrs["path"])
Expand All @@ -23,22 +21,15 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
press = [*_press.split(",")] if _press else ["_"]
title = attrs.get("title")

os.environ["COLUMNS"] = attrs.get("columns", "80")
os.environ["LINES"] = attrs.get("lines", "24")

print(f"screenshotting {path!r}")

cwd = os.getcwd()
try:
app = import_app(path)
app.run(
quit_after=5,
press=press or ["ctrl+c"],
headless=True,
screenshot=True,
screenshot_title=title,
rows = int(attrs.get("lines", 24))
columns = int(attrs.get("columns", 80))
svg = take_svg_screenshot(
None, path, press, title, terminal_size=(rows, columns)
)
svg = app._screenshot
finally:
os.chdir(cwd)

Expand All @@ -51,8 +42,51 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
traceback.print_exception(error)


def take_svg_screenshot(
app: App | None = None,
app_path: str | None = None,
press: Iterable[str] = ("_",),
title: str | None = None,
terminal_size: tuple[int, int] = (24, 80),
) -> str:
"""

Args:
app: An app instance. Must be supplied if app_path is not.
app_path: A path to an app. Must be supplied if app is not.
press: Key presses to run before taking screenshot. "_" is a short pause.
title: The terminal title in the output image.
terminal_size: A pair of integers (rows, columns), representing terminal size.

Returns:
str: An SVG string, showing the content of the terminal window at the time
the screenshot was taken.

"""
rows, columns = terminal_size

os.environ["COLUMNS"] = str(columns)
os.environ["LINES"] = str(rows)

if app is None:
app = import_app(app_path)

if title is None:
title = app.title

app.run(
quit_after=5,
press=press or ["ctrl+c"],
headless=True,
screenshot=True,
screenshot_title=title,
)
svg = app._screenshot
return svg


def rich(source, language, css_class, options, md, attrs, **kwargs) -> str:
"""A superfences formatter to insert a SVG screenshot."""
"""A superfences formatter to insert an SVG screenshot."""

import io

Expand Down
2 changes: 2 additions & 0 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def __init__(
markup=False,
highlight=False,
emoji=False,
legacy_windows=False,
)
self.error_console = Console(markup=False, stderr=True)
self.driver_class = driver_class or self.get_driver_class()
Expand Down Expand Up @@ -557,6 +558,7 @@ def export_screenshot(self, *, title: str | None = None) -> str:
force_terminal=True,
color_system="truecolor",
record=True,
legacy_windows=False,
)
screen_render = self.screen._compositor.render(full=True)
console.print(screen_render)
Expand Down
Empty file.
Loading