Skip to content

Commit 1dc63ab

Browse files
authored
Feature: Test Coverage (#2567)
- coveralls token has been silently broken, and we self-host a webpage anyway. This adds the coverage report and badge to `trimesh.org/coverage` - adds a test for `line_line` that somehow didn't have any coverage, which is now obvious because this PR adds a coverage report 😄. - applies the deprecation for `widget.py` that moves it into `examples` and out of the core library.
2 parents 791a694 + 2956901 commit 1dc63ab

11 files changed

Lines changed: 348 additions & 320 deletions

File tree

.github/workflows/release.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,6 @@ jobs:
8888
- name: Checkout trimesh
8989
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
9090
- name: Build Images And Docs
91-
env:
92-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
9391
run: |
9492
make tests # build docker images and run unit tests
9593
make publish-docker # push images to docker hub

Makefile

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ VERSION := $(shell $(PYTHON) trimesh/version.py)
1414
# save the git short hash for tags
1515
GIT_SHA := $(shell git rev-parse --short HEAD)
1616

17-
# for coverage reports
18-
GIT_SHA_FULL := $(shell git rev-parse HEAD)
19-
GIT_REPO := "mikedh/trimesh"
20-
2117
# the name of the docker images
2218
NAME=trimesh/trimesh
2319
REPO=docker.io
@@ -66,7 +62,6 @@ tests: ## Run unit tests inside docker images.
6662
docker build \
6763
$(DOCKER_BUILD_ARGS) \
6864
--target tests \
69-
--secret id=codecov_token,env=CODECOV_TOKEN \
7065
.
7166

7267
# run the `ty` type checker and print a per-file diagnostic-count table

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[![trimesh](https://trimesh.org/_static/images/logotype-a.svg)](http://trimesh.org)
22

33
-----------
4-
[![Github Actions](https://github.com/mikedh/trimesh/workflows/Release%20Trimesh/badge.svg)](https://github.com/mikedh/trimesh/actions) [![codecov](https://codecov.io/gh/mikedh/trimesh/branch/main/graph/badge.svg?token=4PVRQXyl2h)](https://codecov.io/gh/mikedh/trimesh) [![Docker Image Version (latest by date)](https://img.shields.io/docker/v/trimesh/trimesh?label=docker&sort=semver)](https://hub.docker.com/r/trimesh/trimesh/tags) [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh)
4+
[![Github Actions](https://github.com/mikedh/trimesh/workflows/Release%20Trimesh/badge.svg)](https://github.com/mikedh/trimesh/actions) [![coverage](https://trimesh.org/coverage/badge.svg)](https://trimesh.org/coverage/) [![Docker Image Version (latest by date)](https://img.shields.io/docker/v/trimesh/trimesh?label=docker&sort=semver)](https://hub.docker.com/r/trimesh/trimesh/tags) [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh)
55

66

77
Trimesh is a pure Python 3.10+ library for loading and using [triangular meshes](https://en.wikipedia.org/wiki/Triangle_mesh) with an emphasis on watertight surfaces. The goal of the library is to provide a full featured and well tested Trimesh object which allows for easy manipulation and analysis, in the style of the Polygon object in the [Shapely library](https://github.com/Toblerity/Shapely).

docs/Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ TEMPLATESDIR = templates
1212
GENDIR = generate
1313
CONTENT = content
1414
STATICDIR = static
15+
# where the test-coverage report is published in the built site
16+
COVERAGEDIR = $(BUILDDIR)/html/coverage
1517

1618

1719
example_notebooks := $(wildcard ../examples/*.ipynb)
@@ -26,6 +28,15 @@ html: conf.py $(CONTENT)/index.rst trimesh.rst README.rst $(example_rsts) exampl
2628
echo "trimesh.org" > "$(BUILDDIR)/html/CNAME"
2729
touch "$(BUILDDIR)/html/.nojekyll"
2830
cp "$(STATICDIR)/favicon.ico" "$(BUILDDIR)/html/favicon.ico" || true
31+
# render the styled coverage report + badge into the built site at /coverage/
32+
# run coverage from the repo root so its config and `trimesh` source resolve
33+
if [ -f ../.coverage ]; then \
34+
( cd .. && coverage html --data-file=.coverage -d "docs/$(COVERAGEDIR)" ) && \
35+
$(PYTHON) coverage_badge.py \
36+
"$$( cd .. && coverage report --format=total --precision=1 --data-file=.coverage )" \
37+
"$(COVERAGEDIR)/badge.svg" && \
38+
sed -i -E 's#<link rel="icon"[^>]*>#<link rel="icon" href="../favicon.ico">#' "$(COVERAGEDIR)"/*.html ; \
39+
else echo "no ../.coverage data file — skipping coverage report" ; fi
2940

3041
.deps: requirements.txt
3142
$(PIP) install -r requirements.txt

docs/coverage_badge.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Render a self-hosted SVG coverage badge from a percentage
3+
"""
4+
5+
import sys
6+
7+
# (lower-bound percent, hex color) checked high-to-low
8+
COLORS = [
9+
(90, "#4c1"), # brightgreen
10+
(75, "#97ca00"), # green
11+
(50, "#dfb317"), # yellow
12+
(0, "#e05d44"), # red
13+
]
14+
15+
# shields-style template
16+
TEMPLATE = """<svg xmlns="http://www.w3.org/2000/svg" width="{total}" height="20" role="img" aria-label="coverage: {pct}">
17+
<linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>
18+
<clipPath id="r"><rect width="{total}" height="20" rx="3" fill="#fff"/></clipPath>
19+
<g clip-path="url(#r)">
20+
<rect width="{left}" height="20" fill="#555"/>
21+
<rect x="{left}" width="{right}" height="20" fill="{color}"/>
22+
<rect width="{total}" height="20" fill="url(#s)"/>
23+
</g>
24+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="110" text-rendering="geometricPrecision">
25+
<text x="{lx}" y="150" transform="scale(.1)" fill="#fff">coverage</text>
26+
<text x="{rx}" y="150" transform="scale(.1)" fill="#fff">{pct}</text>
27+
</g>
28+
</svg>
29+
"""
30+
31+
32+
def color_for(percent):
33+
# first threshold the percent meets or exceeds
34+
for bound, hexcolor in COLORS:
35+
if percent >= bound:
36+
return hexcolor
37+
return COLORS[-1][1]
38+
39+
40+
def render(total):
41+
# `total` is the string coverage already rounded, e.g. "86.9"
42+
pct = f"{total}%"
43+
# match shields.io geometry: 20px tall, ~7px per glyph at 11px Verdana
44+
# plus equal padding so this sits flush with the other README badges
45+
left = 61 # width of the "coverage" label, same as shields
46+
right = 7 * len(pct) + 12
47+
total_width = left + right
48+
return TEMPLATE.format(
49+
total=total_width,
50+
left=left,
51+
right=right,
52+
color=color_for(float(total)),
53+
pct=pct,
54+
lx=left * 5,
55+
rx=left * 10 + right * 5,
56+
)
57+
58+
59+
if __name__ == "__main__":
60+
total, target = sys.argv[1], sys.argv[2]
61+
with open(target, "w") as f:
62+
f.write(render(total))

docs/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ jinja2==3.1.6
1111
matplotlib==3.10.8
1212
nbconvert==7.17.1
1313

14+
# render the test-coverage report into the site
15+
coverage==7.14.1
16+

docs/static/coverage.css

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* coverage.css — appended to coverage.py's report via `[tool.coverage.html] extra_css`.
3+
*
4+
* Goal: make the standalone coverage report look like it belongs in the trimesh
5+
* Furo docs. We import Furo's compiled stylesheet (one dir up on the deployed
6+
* site) purely to pull in its CSS variables, font stacks and automatic light/dark
7+
* — Furo switches on `@media (prefers-color-scheme:dark){body:not([data-theme=light])}`
8+
* and coverage's <body> has no `data-theme`, so it follows the OS like the docs do.
9+
*
10+
* Colors reference Furo variables only — no hardcoded hex — so light/dark stays
11+
* in sync with the docs; the one media query below is layout-only (responsive).
12+
*/
13+
14+
@import "../_static/styles/furo.css";
15+
16+
body {
17+
background: var(--color-background-primary);
18+
color: var(--color-foreground-primary);
19+
font-family: var(--font-stack);
20+
}
21+
22+
a, a.nav {
23+
color: var(--color-link);
24+
}
25+
26+
/* ---- header ---------------------------------------------------------- */
27+
header {
28+
background: var(--color-background-secondary);
29+
border-bottom: 1px solid var(--color-background-border);
30+
}
31+
32+
header h1 {
33+
font-weight: 600;
34+
}
35+
36+
.pc_cov {
37+
color: var(--color-brand-primary);
38+
font-weight: 700;
39+
}
40+
41+
header p.text, footer .content {
42+
color: var(--color-foreground-muted);
43+
}
44+
45+
/* nav tabs (Files / Functions / Classes) styled like Furo buttons */
46+
header h2 a.button {
47+
background: var(--color-background-secondary);
48+
border: 1px solid var(--color-background-border);
49+
border-radius: 0.25em;
50+
color: var(--color-foreground-secondary);
51+
}
52+
header h2 a.button:hover {
53+
color: var(--color-brand-primary);
54+
border-color: var(--color-foreground-border);
55+
}
56+
header h2 a.button.current {
57+
background: var(--color-background-primary);
58+
border-color: var(--color-brand-primary);
59+
color: var(--color-foreground-primary);
60+
}
61+
62+
/* show run/mis/exc/par toggle buttons — neutral chrome, coverage keeps its
63+
* semantic backgrounds when a category is active (.show_*) */
64+
header button {
65+
background: var(--color-background-primary);
66+
border: 1px solid var(--color-background-border);
67+
border-radius: 0.25em;
68+
color: var(--color-foreground-secondary);
69+
}
70+
71+
/* ---- filter box ------------------------------------------------------ */
72+
#filter_container #filter {
73+
background: var(--color-background-primary);
74+
border: 1px solid var(--color-background-border);
75+
border-radius: 0.25em;
76+
color: var(--color-foreground-primary);
77+
}
78+
#filter_container #filter:focus {
79+
border-color: var(--color-brand-primary);
80+
}
81+
#filter_container label {
82+
color: var(--color-foreground-muted);
83+
}
84+
85+
/* keyboard-shortcut key caps */
86+
kbd {
87+
background: var(--color-inline-code-background);
88+
border: 1px solid var(--color-background-border);
89+
border-radius: 0.25em;
90+
color: var(--color-foreground-primary);
91+
font-family: var(--font-stack--monospace);
92+
}
93+
94+
#help_panel {
95+
background: var(--color-background-secondary);
96+
border: 1px solid var(--color-background-border);
97+
color: var(--color-foreground-primary);
98+
}
99+
100+
/* ---- index table ----------------------------------------------------- */
101+
#index {
102+
font-family: var(--font-stack--monospace);
103+
}
104+
105+
/* numeric columns: right aligned, tabular figures so digits line up and each
106+
* header sits squarely over its column */
107+
#index td, #index th {
108+
border-bottom: 1px solid var(--color-background-border);
109+
text-align: right;
110+
font-variant-numeric: tabular-nums;
111+
}
112+
#index td.name, #index th.name {
113+
text-align: left;
114+
font-family: var(--font-stack);
115+
}
116+
117+
#index th {
118+
color: var(--color-foreground-secondary);
119+
border-color: var(--color-background-border);
120+
}
121+
#index th:hover,
122+
#index th[aria-sort="ascending"],
123+
#index th[aria-sort="descending"] {
124+
background: var(--color-background-hover);
125+
}
126+
#index th .arrows {
127+
color: var(--color-foreground-muted);
128+
}
129+
130+
#index td.name a {
131+
color: inherit;
132+
}
133+
#index tr.region:hover {
134+
background: var(--color-background-hover);
135+
}
136+
#index tr.region:hover td.name {
137+
color: var(--color-brand-primary);
138+
}
139+
#index tr.total td {
140+
border-top: 1px solid var(--color-foreground-border);
141+
}
142+
143+
/* ---- source view ----------------------------------------------------- */
144+
#source {
145+
font-family: var(--font-stack--monospace);
146+
}
147+
/* Furo's bare `p{margin}` would gap every code line — id specificity wins */
148+
#source p {
149+
margin: 0;
150+
}
151+
#source p .n, #source p .n a {
152+
color: var(--color-foreground-muted);
153+
}
154+
#source p .t .key {
155+
color: var(--color-brand-primary);
156+
}
157+
#source p .t .com {
158+
color: var(--color-foreground-muted);
159+
}
160+
161+
#scroll_marker {
162+
background: var(--color-background-primary);
163+
border-left: 1px solid var(--color-background-border);
164+
}
165+
#scroll_marker .marker {
166+
background: var(--color-background-border);
167+
}
168+
169+
/* ---- layout: centered Furo-style column, responsive ------------------ */
170+
/* coverage hard-codes a 3.5rem left margin and floats the filter, so the
171+
* stock page is left-jammed and desktop-only. center a max-width column,
172+
* make the table fill it (so the title block and table share a width and
173+
* left edge), and collapse to a usable table on phones. */
174+
:root {
175+
--cov-width: 52rem;
176+
--cov-pad: 2rem;
177+
}
178+
179+
header .content,
180+
main#index,
181+
main#source,
182+
footer .content {
183+
max-width: var(--cov-width);
184+
margin-left: auto;
185+
margin-right: auto;
186+
box-sizing: border-box;
187+
}
188+
189+
header .content { padding-left: var(--cov-pad); padding-right: var(--cov-pad); }
190+
main#index { margin-top: 1rem; padding: 0 var(--cov-pad); }
191+
footer { margin-left: 0; margin-right: 0; }
192+
footer .content { padding: 0 var(--cov-pad); }
193+
/* keep room for the floated line-number gutter */
194+
main#source { padding-left: calc(var(--cov-pad) + 3.5rem); padding-right: var(--cov-pad); }
195+
196+
/* table fills the column so its width + left edge match the title block */
197+
#index table.index { width: 100%; margin: 0; }
198+
199+
@media (max-width: 700px) {
200+
/* collapse the side margin to the screen edge */
201+
:root { --cov-pad: 0.75rem; }
202+
/* the centering auto-margins leave a stray left offset here — pin flush */
203+
header .content, main#index, main#source, footer .content {
204+
margin-left: 0;
205+
margin-right: 0;
206+
}
207+
/* stack the filter instead of floating it off-screen */
208+
#filter_container { float: none; margin: 0.5rem 0 0; }
209+
#filter_container #filter { width: 100%; box-sizing: border-box; }
210+
/* a 5-column table can't fit a phone — drop the low-value columns and
211+
* keep File / missing / coverage% */
212+
#index th.spacer, #index td.spacer,
213+
#index th#statements, #index tr > td:nth-child(3),
214+
#index th#excluded, #index tr > td:nth-child(5) { display: none; }
215+
#index td.name, #index th.name { min-width: 0; }
216+
}

helpers/Dockerfile

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,6 @@ COPY --chown=499 trimesh ./trimesh/
8080
COPY --chown=499 models ./models/
8181
COPY --chown=499 pyproject.toml .
8282

83-
# codecov looks at the git history
84-
COPY --chown=499 ./.git ./.git/
85-
8683
USER root
8784
RUN trimesh-setup --install=test,gmsh,gltf_validator,llvmpipe,binvox,blender,build
8885
USER user
@@ -100,20 +97,14 @@ RUN ruff check trimesh && \
10097

10198
# run pytest wrapped with xvfb for simple viewer tests
10299
# print more columns so the short summary is usable
100+
# `--cov` leaves a `.coverage` data file the docs build turns into a report
103101
RUN COLUMNS=240 xvfb-run pytest \
104102
--cov=trimesh \
105103
--beartype-packages=trimesh \
106104
-p no:ALL_DEPENDENCIES \
107105
-p no:INCLUDE_RENDERING \
108106
-p no:cacheprovider tests
109107

110-
111-
# use BuildKit secrets to access codecov token securely
112-
RUN --mount=type=secret,id=codecov_token \
113-
curl -Os https://uploader.codecov.io/latest/linux/codecov && \
114-
chmod +x codecov && \
115-
./codecov -t $(cat /run/secrets/codecov_token || echo "")
116-
117108
################################
118109
### Build Sphinx Docs
119110
FROM output AS build_docs
@@ -130,6 +121,10 @@ COPY --chown=499 examples ./examples/
130121
COPY --chown=499 models ./models/
131122
COPY --chown=499 trimesh ./trimesh/
132123

124+
# pull the coverage data from the tests stage so the docs build can render
125+
# it into a styled report next to `trimesh` (same path pytest measured)
126+
COPY --chown=499 --from=tests /home/user/.coverage /home/user/.coverage
127+
133128
# autodoc walks trimesh modules that import optional deps
134129
RUN pip install /home/user[easy,recommend]
135130

0 commit comments

Comments
 (0)