Skip to content

Commit d457bf8

Browse files
TST: cover the rocket and Monte Carlo plot branches (#1178)
rocket_plots.py sat at 73.9% and monte_carlo_plots.py at 63.1% because the fixtures never reached several drawing branches. For rocket_plots, add drawings for a rocket carrying one individual Fin rather than a fin set, one carrying a GenericSurface, one on a hybrid motor, one on a liquid motor, one on a ring cluster, and one whose nozzle sits behind its last aerodynamic surface in each coordinate system. Add the two validation refusals. For monte_carlo_plots, cover the ellipse branches that run when a results file is missing its apogee or its impact series, when it has neither, when the image path does not exist, and when a landing point and an image are drawn. Background-map fetching stays in test_monte_carlo_plots_background.py; everything added here runs with background=None and contacts no provider. No production code changes.
1 parent 3a75eac commit d457bf8

2 files changed

Lines changed: 240 additions & 0 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Unit tests for the rocket drawing helpers in ``rocketpy.plots.rocket_plots``."""
2+
3+
from unittest.mock import patch
4+
5+
import pytest
6+
7+
from rocketpy import LinearGenericSurface
8+
from rocketpy.mathutils.vector_matrix import Vector
9+
from rocketpy.motors.ring_cluster_motor import RingClusterMotor
10+
11+
REFERENCE_AREA = 0.005
12+
REFERENCE_LENGTH = 0.08
13+
14+
15+
@patch("matplotlib.pyplot.show")
16+
@pytest.mark.parametrize("plane", ["xz", "yz"])
17+
@pytest.mark.parametrize(
18+
"fin_fixture",
19+
["calisto_trapezoidal_fin", "calisto_elliptical_fin", "calisto_free_form_fin"],
20+
)
21+
def test_draw_a_rocket_carrying_one_fin( # pylint: disable=unused-argument
22+
mock_show, request, calisto_robust, plane, fin_fixture
23+
):
24+
"""A single ``Fin`` takes ``_draw_fin``, not the ``_draw_fins`` set branch.
25+
26+
That method rotates one fin out of its own coordinate system into the body
27+
frame, and it projects differently in each plane, so both are drawn here.
28+
"""
29+
fin = request.getfixturevalue(fin_fixture)
30+
calisto_robust.add_surfaces(fin, Vector([0, 0, -1.04956]))
31+
32+
assert calisto_robust.draw(plane=plane, filename=None) is None
33+
34+
35+
@patch("matplotlib.pyplot.show")
36+
@pytest.mark.parametrize("plane", ["xz", "yz"])
37+
def test_draw_a_rocket_carrying_a_generic_surface( # pylint: disable=unused-argument
38+
mock_show, calisto_robust, plane
39+
):
40+
"""A ``GenericSurface`` has no outline to trace, so it gets a scatter point.
41+
42+
``_draw_generic_surface`` is the only branch that reads the surface position
43+
by index rather than by attribute, and it picks a different index per plane.
44+
"""
45+
surface = LinearGenericSurface(
46+
reference_area=REFERENCE_AREA,
47+
reference_length=REFERENCE_LENGTH,
48+
coefficients={},
49+
name="Canard",
50+
)
51+
calisto_robust.add_surfaces(surface, Vector([0, 0, -0.5]))
52+
53+
assert calisto_robust.draw(plane=plane, filename=None) is None
54+
55+
56+
@patch("matplotlib.pyplot.show")
57+
def test_draw_a_rocket_with_a_hybrid_motor( # pylint: disable=unused-argument
58+
mock_show, calisto_hybrid_modded, calisto_nose_cone
59+
):
60+
"""The hybrid branch of ``_generate_motor_patches`` draws grains and tanks."""
61+
calisto_hybrid_modded.add_surfaces(calisto_nose_cone, 1.160)
62+
63+
assert calisto_hybrid_modded.draw(filename=None) is None
64+
65+
66+
@patch("matplotlib.pyplot.show")
67+
def test_draw_a_rocket_with_a_liquid_motor( # pylint: disable=unused-argument
68+
mock_show, calisto_liquid_modded, calisto_nose_cone
69+
):
70+
"""The liquid branch draws positioned tanks and no combustion chamber."""
71+
calisto_liquid_modded.add_surfaces(calisto_nose_cone, 1.160)
72+
73+
assert calisto_liquid_modded.draw(filename=None) is None
74+
75+
76+
@patch("matplotlib.pyplot.show")
77+
def test_draw_a_rocket_with_a_motor_cluster( # pylint: disable=unused-argument
78+
mock_show, calisto_motorless, cesaroni_m1670, calisto_nose_cone
79+
):
80+
"""A cluster repeats the grain patches around the ring.
81+
82+
Only the first offset keeps its legend entry, so the loop that relabels the
83+
rest runs solely when there is more than one motor.
84+
"""
85+
calisto_motorless.add_motor(
86+
RingClusterMotor(motor=cesaroni_m1670, number=3, radius=0.05),
87+
position=-1.373,
88+
)
89+
calisto_motorless.add_surfaces(calisto_nose_cone, 1.160)
90+
91+
assert calisto_motorless.draw(filename=None) is None
92+
93+
94+
@patch("matplotlib.pyplot.show")
95+
@pytest.mark.parametrize(
96+
"rocket_fixture,nose_position",
97+
[("calisto", 1.160), ("calisto_nose_to_tail", -1.160)],
98+
)
99+
def test_draw_a_rocket_whose_nozzle_sits_behind_its_last_surface( # pylint: disable=unused-argument
100+
mock_show, request, rocket_fixture, nose_position, calisto_nose_cone
101+
):
102+
"""``_draw_nozzle_tube`` only draws when the nozzle is past the last surface.
103+
104+
A rocket carrying nothing but a nose cone leaves that gap open, and the
105+
comparison flips with the coordinate system, so both orientations are drawn.
106+
"""
107+
rocket = request.getfixturevalue(rocket_fixture)
108+
rocket.add_surfaces(calisto_nose_cone, nose_position)
109+
110+
assert rocket.draw(filename=None) is None
111+
112+
113+
def test_draw_refuses_a_rocket_with_no_aerodynamic_surfaces(calisto_motorless):
114+
"""There is nothing to draw the body around without at least one surface."""
115+
with pytest.raises(ValueError, match="at least one aerodynamic surface"):
116+
calisto_motorless.draw(filename=None)
117+
118+
119+
def test_draw_refuses_a_plane_it_cannot_project_onto(calisto_robust):
120+
"""Only the two longitudinal planes are defined."""
121+
with pytest.raises(ValueError, match="must be 'xz' or 'yz'"):
122+
calisto_robust.draw(plane="xy", filename=None)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Unit tests for the ellipse plots in ``rocketpy.plots.monte_carlo_plots``.
2+
3+
Background-map fetching is covered separately in
4+
``test_monte_carlo_plots_background.py``. Everything here runs with
5+
``background=None`` so no tile provider is contacted.
6+
"""
7+
8+
from unittest.mock import patch
9+
10+
import numpy as np
11+
import pytest
12+
13+
from rocketpy.plots.monte_carlo_plots import _MonteCarloPlots
14+
from rocketpy.simulation import MonteCarlo
15+
16+
APOGEE = {"apogee_x": [100, 200, 300], "apogee_y": [100, 200, 300]}
17+
IMPACT = {"x_impact": [1000, 2000, 3000], "y_impact": [1000, 2000, 3000]}
18+
19+
20+
class MockMonteCarlo(MonteCarlo):
21+
"""A MonteCarlo carrying only the results the ellipse plots read."""
22+
23+
def __init__(self, results, filename="test"):
24+
# pylint: disable=super-init-not-called
25+
self.filename = filename
26+
self.results = results
27+
self.plots = _MonteCarloPlots(self)
28+
29+
30+
@pytest.fixture(name="square_image")
31+
def square_image_fixture(tmp_path):
32+
"""A small on-disk image for the ``image`` background path."""
33+
imageio = pytest.importorskip("imageio")
34+
path = tmp_path / "launch_site.png"
35+
imageio.imwrite(path, np.zeros((8, 8, 3), dtype=np.uint8))
36+
return str(path)
37+
38+
39+
@patch("matplotlib.pyplot.show")
40+
def test_ellipses_without_apogee_data_plots_the_impact_points(mock_show, caplog): # pylint: disable=unused-argument
41+
"""A results file may hold impacts and no apogees.
42+
43+
The apogee lookup is allowed to miss; the method warns and draws what is
44+
left rather than failing.
45+
"""
46+
monte_carlo = MockMonteCarlo(dict(IMPACT))
47+
48+
assert monte_carlo.plots.ellipses() is None
49+
assert "No apogee data found" in caplog.text
50+
51+
52+
@patch("matplotlib.pyplot.show")
53+
def test_ellipses_without_impact_data_plots_the_apogee_points(mock_show, caplog): # pylint: disable=unused-argument
54+
"""The mirror case: apogees recorded, impacts missing."""
55+
monte_carlo = MockMonteCarlo(dict(APOGEE))
56+
57+
assert monte_carlo.plots.ellipses() is None
58+
assert "No impact data found" in caplog.text
59+
60+
61+
def test_ellipses_refuses_results_with_neither_apogee_nor_impact():
62+
"""With both lookups missing there is nothing to draw an ellipse around."""
63+
monte_carlo = MockMonteCarlo({"t_final": [10, 11, 12]})
64+
65+
with pytest.raises(ValueError, match="No apogee or impact data found"):
66+
monte_carlo.plots.ellipses()
67+
68+
69+
def test_ellipses_reports_an_image_path_that_does_not_exist(tmp_path):
70+
"""The path is the user's input, so the failure names it rather than the read."""
71+
monte_carlo = MockMonteCarlo({**APOGEE, **IMPACT})
72+
73+
with pytest.raises(FileNotFoundError, match="image file was not found"):
74+
monte_carlo.plots.ellipses(image=str(tmp_path / "absent.png"))
75+
76+
77+
@patch("matplotlib.pyplot.show")
78+
def test_ellipses_draws_over_an_image_and_marks_the_actual_landing_point( # pylint: disable=unused-argument
79+
mock_show, square_image
80+
):
81+
"""``image`` and ``actual_landing_point`` are separate optional branches."""
82+
monte_carlo = MockMonteCarlo({**APOGEE, **IMPACT})
83+
84+
assert (
85+
monte_carlo.plots.ellipses(
86+
image=square_image, actual_landing_point=(1500, 1500)
87+
)
88+
is None
89+
)
90+
91+
92+
@patch("matplotlib.pyplot.show")
93+
def test_ellipses_comparison_without_apogee_data(mock_show, caplog): # pylint: disable=unused-argument
94+
"""The comparison reads four series at once, so one miss drops all four."""
95+
monte_carlo = MockMonteCarlo(dict(IMPACT))
96+
other = MockMonteCarlo(dict(IMPACT), filename="other")
97+
98+
assert monte_carlo.plots.ellipses_comparison(other) is None
99+
assert "No apogee data found" in caplog.text
100+
101+
102+
@patch("matplotlib.pyplot.show")
103+
def test_ellipses_comparison_without_impact_data(mock_show, caplog): # pylint: disable=unused-argument
104+
"""The mirror case for the impact series."""
105+
monte_carlo = MockMonteCarlo(dict(APOGEE))
106+
other = MockMonteCarlo(dict(APOGEE), filename="other")
107+
108+
assert monte_carlo.plots.ellipses_comparison(other) is None
109+
assert "No impact data found" in caplog.text
110+
111+
112+
@patch("matplotlib.pyplot.show")
113+
def test_ellipses_comparison_draws_over_an_image(mock_show, square_image): # pylint: disable=unused-argument
114+
"""The comparison takes the same ``image`` branch as ``ellipses``."""
115+
monte_carlo = MockMonteCarlo({**APOGEE, **IMPACT})
116+
other = MockMonteCarlo({**APOGEE, **IMPACT}, filename="other")
117+
118+
assert monte_carlo.plots.ellipses_comparison(other, image=square_image) is None

0 commit comments

Comments
 (0)