Skip to content

Commit 73cbe0a

Browse files
authored
Add plot doctests (#1542)
My understanding is that the doctests for plotting don't check the generated output and only look for the "-Graphics-" string, is that correct? Here's a proposal for tests that compare the generated Graphics* output with expected output. The tests in `test_plot_detail.py` are now augmented with about 125 plot-related doctests, with corresponding expected reference output in `test_plot_detail_ref/doc-*`. I hope this will give us a lot more confidence to refactor plotting code without breaking anything in the doc. These tests take about 10 seconds to run so having them separated out from the doctests will make development cycles faster. Couple notes: * I chose to store the test definitions in a yaml file `doc_tests.yaml` because with that number of tests a Python data structure is a bit unwieldy. The yaml file is leaner and I think easier to peruse and edit. * I obtained the 128 tests by scraping the output from running the current doctests and munging with sed, grep, etc.. Going forward maybe we can do something a little more robust - maybe from time-to-time run a python script tbd to extract them from the docstrings in the same way the existing doctests are extracted, compare to the existing list, and add new ones.
1 parent 189cf33 commit 73cbe0a

File tree

128 files changed

+293769
-10
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+293769
-10
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
#
2+
# These tests were automatically picked up from doc 2025-12-13
3+
# The doc-xxx numbers are simply sequential numbers in alphebatized list as of that date
4+
# The doc-xxx labels correspond to test output so shouldn't be changed
5+
# New tests can just be added at end of list, disregarding alphabetical order
6+
#
7+
8+
doc-001:
9+
expr: BarChart[{1, 4, 2}, ChartStyle -> {Red, Green, Blue}]
10+
doc-002:
11+
expr: BarChart[{1, 4, 2}]
12+
doc-003:
13+
expr: BarChart[{{1, 2, 3}, {2, 3, 4}}, ChartLabels -> {"a", "b", "c"}]
14+
doc-004:
15+
expr: BarChart[{{1, 2, 3}, {2, 3, 4}}]
16+
doc-005:
17+
expr: BarChart[{{1, 5}, {3, 4}}, ChartStyle -> {{EdgeForm[Thin], White}, {EdgeForm[Thick], White}}]
18+
doc-006:
19+
expr: DensityPlot[1 / x, {x, 0, 1}, {y, 0, 1}]
20+
doc-007:
21+
expr: DensityPlot[1/(x^2 + y^2 + 1), {x, -1, 1}, {y, -2,2}, Mesh->Full]
22+
doc-008:
23+
expr: DensityPlot[Sin[x y], {x, -2, 2}, {y, -2, 2}, Mesh->Full]
24+
doc-009:
25+
expr: DensityPlot[Sqrt[x * y], {x, -1, 1}, {y, -1, 1}]
26+
doc-010:
27+
expr: 'DensityPlot[x ^ 2 + 1 / y, {x, -1, 1}, {y, 1, 4}, ColorFunction -> (Blend[{Red, Green, Blue}, #]&)]'
28+
skip: pyodide
29+
doc-011:
30+
expr: DensityPlot[x ^ 2 + 1 / y, {x, -1, 1}, {y, 1, 4}]
31+
doc-012:
32+
expr: DensityPlot[x^2 y, {x, -1, 1}, {y, -1, 1}, Mesh->All]
33+
skip: pyodide
34+
doc-013:
35+
expr: DiscretePlot[2.5 Sqrt[k], {k, 100}]
36+
doc-014:
37+
expr: DiscretePlot[PrimePi[k], {k, 1, 100}]
38+
doc-015:
39+
expr: DiscretePlot[{Sin[Pi x/20], Cos[Pi x/20]}, {x, 0, 40}]
40+
doc-016:
41+
expr: Histogram[{3, 8, 10, 100, 1000, 500, 300, 200, 10, 20, 200, 100, 200, 300, 500}]
42+
doc-017:
43+
expr: Histogram[{{1, 2, 10, 5, 50, 20}, {90, 100, 101, 120, 80}}]
44+
doc-018:
45+
expr: ListLinePlot[Table[Cos[x], {x, -5, 5, 0.2}], Filling->Top]
46+
doc-019:
47+
expr: ListLinePlot[Table[Sin[x], {x, -5, 5, 0.2}], Filling->Axis]
48+
doc-020:
49+
expr: ListLinePlot[Table[Sin[x], {x, -5, 5, 0.2}], Filling->Bottom]
50+
doc-021:
51+
expr: ListLinePlot[Table[{n, n ^ 0.5}, {n, 10}]]
52+
doc-022:
53+
expr: ListLinePlot[list]
54+
skip: true # malformed test - depends on list
55+
doc-023:
56+
expr: ListLinePlot[{{-2, -1}, {-1, -1}, {1, 3}}, Filling->Axis]
57+
doc-024:
58+
expr: ListLogPlot[Table[Fibonacci[n], {n, 10}]]
59+
doc-025:
60+
expr: ListLogPlot[Table[n!, {n, 10}], Joined -> True]
61+
doc-026:
62+
expr: ListPlot[Prime[Range[30]]]
63+
doc-027:
64+
expr: ListPlot[Table[ElementData[z, "AtomicWeight"], {z, 118}]]
65+
doc-028:
66+
expr: ListPlot[Table[n ^ 2 / 8, {n, 30}]]
67+
doc-029:
68+
expr: ListPlot[Table[n ^ 2, {n, 10}], Joined->True]
69+
doc-030:
70+
expr: ListPlot[Table[n ^ 2, {n, 30}], Filling->Axis]
71+
doc-031:
72+
expr: ListPlot[Table[n ^ 2, {n, 30}], Joined->True]
73+
doc-032:
74+
expr: ListPlot[ToCharacterCode["plot this string"], Filling -> Axis]
75+
doc-033:
76+
expr: ListStepPlot[{1, 1, 2, 3, 5, 8, 13, 21}, Joined->False]
77+
doc-034:
78+
expr: ListStepPlot[{1, 1, 2, 3, 5, 8, 13, 21}]
79+
doc-035:
80+
expr: ListStepPlot[{{1, 1}, {3, 2}, {4, 5}, {5, 8}, {6, 13}, {7, 21}}, Filling->Axis]
81+
doc-036:
82+
expr: LogPlot[x^x, {x, 1, 5}]
83+
doc-037:
84+
expr: LogPlot[{10^x, Factorial[x], Subfactorial[x]}, {x, 0, 25}, PlotPoints->26]
85+
doc-038:
86+
expr: LogPlot[{x^x, Exp[x], x!}, {x, 1, 5}]
87+
doc-039:
88+
expr: NumberLinePlot[Prime[Range[10]]]
89+
doc-040:
90+
expr: NumberLinePlot[Table[x^2, {x, 10}]]
91+
doc-041:
92+
expr: ParametricPlot[ {LegendreP[7, x], LegendreP[5, x]}, {x, -1, 1}]
93+
doc-042:
94+
expr: ParametricPlot[{Cos[u] / u, Sin[u] / u}, {u, 0, 50}, PlotRange->0.5]
95+
doc-043:
96+
expr: ParametricPlot[{Sin[u], Cos[3 u]}, {u, 0, 2 Pi}]
97+
doc-044:
98+
expr: ParametricPlot[{{Sin[u], Cos[u]},{0.6 Sin[u], 0.6 Cos[u]}, {0.2 Sin[u], 0.2 Cos[u]}}, {u, 0, 2 Pi}, PlotRange->1, AspectRatio->1]
99+
doc-045:
100+
expr: PieChart[{1, -1, 3}]
101+
doc-046:
102+
expr: PieChart[{30, 20, 10}, ChartLabels -> {Dogs, Cats, Fish}]
103+
doc-047:
104+
expr: PieChart[{8, 16, 2}, SectorOrigin -> {Automatic, 1.5}]
105+
doc-048:
106+
expr: PieChart[{{10, 20, 30}, {15, 22, 30}}, ChartLabels -> {A, B, C}]
107+
doc-049:
108+
expr: PieChart[{{10, 20, 30}, {15, 22, 30}}, SectorSpacing -> None]
109+
doc-050:
110+
expr: PieChart[{{10, 20, 30}, {15, 22, 30}}]
111+
doc-051:
112+
expr: Plot3D[Exp[x] Cos[y], {x, -2, 1}, {y, -Pi, 2 Pi}]
113+
doc-052:
114+
expr: Plot3D[Log[x + y^2], {x, -1, 1}, {y, -1, 1}]
115+
skip: pyodide # abort exceeded iteration limit
116+
# skipping following due to significant numerical diff btw macos and ubuntu
117+
# i think the test is numerically unstable because of the / (x y)
118+
doc-053:
119+
expr: Plot3D[Sin[x y] /(x y), {x, -3, 3}, {y, -3, 3}, Mesh->All]
120+
skip: true
121+
doc-054:
122+
expr: Plot3D[Sin[x y], {x, -2, 2}, {y, -2, 2}, Mesh->Full]
123+
doc-055:
124+
expr: Plot3D[Sin[y + Sin[3 x]], {x, -2, 2}, {y, -2, 2}, PlotPoints->20]
125+
doc-056:
126+
expr: Plot3D[x / (x ^ 2 + y ^ 2 + 1), {x, -2, 2}, {y, -2, 2}, Mesh->None]
127+
doc-057:
128+
expr: Plot3D[x ^ 2 + 1 / y, {x, -1, 1}, {y, 1, 4}]
129+
doc-058:
130+
expr: Plot3D[{x^2 + y^2, -x^2 - y^2}, {x, -2, 2}, {y, -2, 2}, BoxRatios-> Automatic, Mesh->None]
131+
doc-059:
132+
expr: Plot[3, {x, 0, 1}]
133+
skip: pyodide # pyodide emits Real 3 where other platforms emit Integer 3
134+
doc-060:
135+
expr: Plot[Abs[x], {x, -4, 4}]
136+
doc-061:
137+
expr: Plot[AiryAiPrime[x], {x, -10, 10}]
138+
doc-062:
139+
expr: Plot[AiryAi[x], {x, -10, 10}]
140+
doc-063:
141+
expr: Plot[AiryBiPrime[x], {x, -10, 2}]
142+
doc-064:
143+
expr: Plot[AiryBi[x], {x, -10, 2}]
144+
doc-065:
145+
expr: Plot[AngerJ[1, x], {x, -10, 10}]
146+
doc-066:
147+
expr: Plot[BesselI[0, x], {x, 0, 5}]
148+
doc-067:
149+
expr: Plot[BesselJ[0, x], {x, 0, 10}]
150+
doc-068:
151+
expr: Plot[BesselK[0, x], {x, 0, 5}]
152+
doc-069:
153+
expr: Plot[BesselY[0, x], {x, 0, 10}]
154+
doc-070:
155+
expr: Plot[EllipticE[m], {m, -2, 2}]
156+
doc-071:
157+
expr: Plot[EllipticK[n], {n, -1, 1}]
158+
doc-072:
159+
expr: Plot[Erf[x], {x, -2, 2}]
160+
doc-073:
161+
expr: Plot[Erfc[x], {x, -2, 2}]
162+
doc-074:
163+
expr: Plot[Evaluate[Table[x^y, {y, 1, 5}]], {x, -1.5, 1.5}, AspectRatio -> 1]
164+
doc-075:
165+
expr: Plot[Exp[x], {x, 0, 3}]
166+
doc-076:
167+
expr: Plot[Gudermannian[x], {x, -10, 10}]
168+
doc-077:
169+
expr: Plot[Hypergeometric1F1[1, 2, x], {x, -5, 5}]
170+
doc-078:
171+
expr: Plot[Hypergeometric2F1[1/3, 1/3, 2/3, x], {x, -1, 1}]
172+
doc-079:
173+
expr: Plot[HypergeometricPFQ[{1, 1}, {3, 3, 3}, x], {x, -30, 30}]
174+
doc-080:
175+
expr: Plot[HypergeometricU[3, 2, x], {x, 0.5, 10}]
176+
skip: true # hits iteration limit
177+
doc-081:
178+
expr: Plot[InverseErf[x], {x, -1, 1}]
179+
doc-082:
180+
expr: Plot[InverseGudermannian[x], {x, -2 Pi, 2 Pi}]
181+
doc-083:
182+
expr: Plot[KelvinBei[x], {x, 0, 10}]
183+
doc-084:
184+
expr: Plot[KelvinBer[x], {x, 0, 10}]
185+
doc-085:
186+
expr: Plot[KelvinKei[x], {x, 0, 10}]
187+
doc-086:
188+
expr: Plot[KelvinKer[x], {x, 0, 10}]
189+
doc-087:
190+
expr: Plot[LambertW[x], {x, -1/E, E}]
191+
doc-088:
192+
expr: Plot[LerchPhi[x, 1, 2], {x, -1, 1}]
193+
doc-089:
194+
expr: Plot[Log[x], {x, 0, 5}, MaxRecursion->0]
195+
doc-090:
196+
expr: Plot[Log[x], {x, 0, 5}]
197+
doc-091:
198+
expr: Plot[LucasL[1/2, x], {x, -5, 5}]
199+
doc-092:
200+
expr: Plot[Piecewise[{{Log[x], x > 0}, {x*-0.5, x < 0}}], {x, -1, 1}]
201+
skip: true # hits iteration limit
202+
doc-093:
203+
expr: Plot[PolyLog[2,x], {x, -20, 1}]
204+
doc-094:
205+
expr: Plot[ProductLog[x], {x, -1/E, E}]
206+
doc-095:
207+
expr: Plot[Sin[Cos[x^2]],{x,-4,4}, PlotPoints->22]
208+
doc-096:
209+
expr: Plot[Sin[Cos[x^2]],{x,-4,4}, PlotRange -> All]
210+
doc-097:
211+
expr: Plot[Sin[Cos[x^2]],{x,-4,4},Mesh->All]
212+
doc-098:
213+
expr: Plot[Sin[x], {x, -Pi, Pi}]
214+
doc-099:
215+
expr: Plot[Sin[x], {x, 0, 10}, ImageSize -> Small]
216+
doc-100:
217+
expr: Plot[Sin[x], {x, 0, 2 Pi}, Background -> LightBlue]
218+
doc-101:
219+
expr: Plot[Sin[x], {x, 0, 2 Pi}]
220+
doc-102:
221+
expr: Plot[Sin[x], {x, 0, 4 Pi}, PlotRange->{{0, 4 Pi}, {0, 1.5}}]
222+
doc-103:
223+
expr: Plot[Sin[x], {x,0,4 Pi}, Mesh->Full]
224+
doc-104:
225+
expr: Plot[SphericalBesselJ[1, x], {x, 0.1, 10}]
226+
doc-105:
227+
expr: Plot[SphericalBesselY[1, x], {x, 0, 10}]
228+
doc-106:
229+
expr: Plot[Sqrt[a^2], {a, -2, 2}]
230+
doc-107:
231+
expr: Plot[StruveH[0, x], {x, 0, 10}]
232+
doc-108:
233+
expr: Plot[StruveL[0, x], {x, 0, 5}]
234+
doc-109:
235+
expr: Plot[Tan[x], {x, -6, 6}, Mesh->Full]
236+
doc-110:
237+
expr: Plot[Tan[x], {x, 0, 6}, Mesh->All, PlotRange->{{-1, 5}, {0, 15}}, MaxRecursion->10]
238+
doc-111:
239+
expr: Plot[UnitStep[x], {x, -4, 4}]
240+
doc-112:
241+
expr: Plot[WeberE[1, x], {x, -10, 10}]
242+
doc-113:
243+
expr: Plot[Zeta[z], {z, -20, 10}]
244+
doc-114:
245+
expr: Plot[f[x], {x, -8, 6}]
246+
doc-115:
247+
expr: Plot[x^2, {x, -1, 1}, MaxRecursion->5, Mesh->All]
248+
doc-116:
249+
expr: Plot[{Cos[a], Re[E^(I a)]}, {a, 0, 2 Pi}]
250+
doc-117:
251+
expr: Plot[{Gamma[x], x!}, {x, 0, 4}]
252+
doc-118:
253+
expr: Plot[{Hypergeometric1F1[1/2, Sqrt[2], x], Hypergeometric1F1[1/2, Sqrt[3], x], Hypergeometric1F1[1/2, Sqrt[5], x]}, {x, -4, 4}]
254+
doc-119:
255+
expr: Plot[{Hypergeometric1F1[Sqrt[2], b, 1], Hypergeometric1F1[Sqrt[5], b, 1], Hypergeometric1F1[Sqrt[7], b, 1]}, {b, -3, 3}]
256+
doc-120:
257+
expr: Plot[{Hypergeometric1F1[Sqrt[3], Sqrt[2], z], -0.01}, {z, -10, -2}]
258+
doc-121:
259+
expr: Plot[{Sin[a], Im[E^(I a)]}, {a, 0, 2 Pi}]
260+
doc-122:
261+
expr: Plot[{Sin[x], Cos[x], x / 3}, {x, -Pi, Pi}, Background -> RGBColor[0.5, .5, .5, 0.1]]
262+
doc-123:
263+
expr: Plot[{Sin[x], Cos[x], x / 3}, {x, -Pi, Pi}]
264+
doc-124:
265+
expr: Plot[{Sin[x], Cos[x], x ^ 2}, {x, -1, 1}]
266+
doc-125:
267+
expr: PolarPlot[Abs[Cos[5t]], {t, 0, Pi}]
268+
doc-126:
269+
expr: PolarPlot[Cos[5t], {t, 0, Pi}]
270+
doc-127:
271+
expr: PolarPlot[Sqrt[t], {t, 0, 16 Pi}]
272+
doc-128:
273+
expr: PolarPlot[{1, 1 + Sin[20 t] / 5}, {t, 0, 2 Pi}]

test/builtin/drawing/manage.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import argparse
2+
3+
import test_plot_detail
4+
5+
parser = argparse.ArgumentParser(description="manage plot tests")
6+
parser.add_argument(
7+
"--generate", action="store_true", help="generate test reference files"
8+
)
9+
parser.add_argument(
10+
"--check-docs", action="store_true", help="compare doc tests with docs"
11+
)
12+
args = parser.parse_args()
13+
14+
15+
if args.generate:
16+
test_plot_detail.make_ref_files()
17+
18+
if args.check_docs:
19+
print("TBD")

test/builtin/drawing/test_plot_detail.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,24 @@
5252
"""
5353

5454
import os
55+
import pathlib
5556
import subprocess
5657

57-
# couple tests depend on ths
58+
import yaml
59+
60+
# couple tests depend on this
5861
try:
5962
import skimage
6063
except:
6164
skimage = None
6265

66+
# check if pyoidide so we can skip some there
67+
try:
68+
import pyodide
69+
except:
70+
pyodide = None
71+
72+
6373
from test.helper import session
6474

6575
import mathics.builtin.drawing.plot as plot
@@ -112,12 +122,18 @@
112122
("plot3d", "Plot3D[x y,{x,-2,2},{y,-2,2}]", opt3, True),
113123
]
114124

125+
115126
# compute reference dir, which is this file minus .py plus _ref
116127
path, _ = os.path.splitext(__file__)
117128
ref_dir = path + "_ref"
118129
print(f"ref_dir {ref_dir}")
119130

120131

132+
doc_tests_fn = pathlib.Path(__file__).resolve().parent / "doc_tests.yaml"
133+
with open(doc_tests_fn) as r:
134+
doc_tests = yaml.safe_load(r)
135+
136+
121137
def one_test(name, str_expr, vec, opt, act_dir="/tmp"):
122138
# update name and set use_vectorized_plot depending on
123139
# whether vectorized test
@@ -129,11 +145,13 @@ def one_test(name, str_expr, vec, opt, act_dir="/tmp"):
129145

130146
# update name and splice in options depending on
131147
# whether default or with-options test
132-
if opt:
148+
if opt is None:
149+
name += "-def"
150+
elif opt is ...:
151+
pass
152+
else:
133153
name += "-opt"
134154
str_expr = f"{str_expr[:-1]}, {opt}]"
135-
else:
136-
name += "-def"
137155

138156
print(f"=== running {name} {str_expr}")
139157

@@ -187,18 +205,37 @@ def test_all(act_dir="/tmp", opt=None):
187205
opt = opt if use_opt else None
188206
one_test(name, str_expr, True, opt, act_dir)
189207

208+
# several of these tests failed on pyodide due to apparent differences
209+
# in numpy (and/or the blas library backing it) between pyodide and other platforms
210+
# including numerical instability, different data types (integer vs real)
211+
# the tests above seem so far to be ok on pyodide, but generally they are
212+
# simpler than these doc_tests
213+
if not pyodide:
214+
for name, info in doc_tests.items():
215+
skip = info.get("skip", False)
216+
if isinstance(skip, str):
217+
skip = {
218+
"pyodide": pyodide, # skip if on pyodide
219+
"skimage": not skimage, # skip if no skimage
220+
}[skip]
221+
if not skip:
222+
one_test(name, info["expr"], False, ..., act_dir)
223+
else:
224+
print(f"skipping {name}")
225+
226+
227+
# reference files can be generated by pointing saved actual
228+
# output at reference dir instead of /tmp
229+
def make_ref_files():
230+
test_all(ref_dir)
231+
190232

191233
if __name__ == "__main__":
192-
# reference files can be generated by pointing saved actual
193-
# output at reference dir instead of /tmp
194-
def make_ref_files():
195-
test_all(ref_dir)
196234

197235
def run_tests():
198236
try:
199237
test_all()
200238
except AssertionError:
201239
print("FAIL")
202240

203-
make_ref_files()
204-
# run_tests()
241+
run_tests()

0 commit comments

Comments
 (0)