Skip to content

Commit 4648d09

Browse files
krystophnyclaude
andauthored
fix: resolve CI segfaults in contour plotting functions (#402)
## Summary - Fixes critical CI segmentation faults in colored_contours and ascii_heatmap_demo examples - Replaces unsafe merge() function usage with conditional parameter forwarding in matplotlib wrapper - Adds comprehensive regression tests to prevent future memory safety issues ## Root Cause Analysis The segfaults were caused by improper usage of the `merge()` intrinsic function when forwarding optional parameters to underlying methods. When optional parameters were not present, merge() was still trying to pass zero-size allocated arrays, causing memory access violations. ## Technical Details **Fixed Functions:** - `contour_filled()` in fortplot_matplotlib.f90 (line 112) - `add_contour_filled()` in fortplot_matplotlib.f90 (line 437) **Memory Safety Improvements:** - Replace `merge(wp_levels, wp_levels, present(levels))` with conditional parameter forwarding - Prevent passing uninitialized zero-size arrays to underlying methods - Use explicit if/else chains to handle all parameter combinations safely ## Testing - ✅ colored_contours example runs without segfault - ✅ ascii_heatmap_demo example runs without segfault - ✅ pcolormesh_demo example continues to work correctly - ✅ Added targeted regression test `test_contour_memory_safety_regression.f90` - ✅ All existing tests continue to pass ## Impact This fix unblocks all PR merges that were prevented by CI failures. The examples now complete successfully and produce expected output files. 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8c7cd38 commit 4648d09

File tree

3 files changed

+187
-14
lines changed

3 files changed

+187
-14
lines changed

BACKLOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55

66
**🚨 CRITICAL: CI test suite failures - user-visible rendering broken**
7-
- [ ] #401: Fix - CI test-coverage failing with segfaults in colored_contours and ascii_heatmap_demo (blocks all PR merges)
87

98
**🚨 CRITICAL: User-visible PNG/PDF rendering regressions**
109

@@ -13,9 +12,10 @@
1312

1413
**Infrastructure & Documentation Issues (Lower Priority)**
1514
- [ ] #388: Fix - investigate test_mpeg_consolidated failure unrelated to ylabel rotation (test infrastructure)
15+
- [ ] #403: Refactor - reduce contour function complexity - functions exceed 50-line target
1616

1717
## DOING (Current Work)
18-
*Empty - ready for next issue*
18+
- [x] #401: Fix - CI test-coverage failing with segfaults in colored_contours and ascii_heatmap_demo (branch: fix-ci-segfaults-401)
1919

2020
## FUTURE SPRINTS - Systematic Restoration
2121

src/fortplot_matplotlib.f90

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,42 @@ subroutine contour_filled(x, y, z, levels, colormap, show_colorbar, label)
108108
allocate(wp_levels(0))
109109
end if
110110

111-
! Forward ALL parameters to underlying method using single call pattern
112-
call fig%add_contour_filled(wp_x, wp_y, wp_z, &
113-
levels=merge(wp_levels, wp_levels, present(levels)), &
114-
colormap=merge(colormap, "", present(colormap)), &
115-
show_colorbar=merge(show_colorbar, .false., present(show_colorbar)), &
116-
label=merge(label, "", present(label)))
111+
! Forward parameters to underlying method using conditional calls for memory safety
112+
if (present(levels) .and. present(colormap) .and. present(show_colorbar) .and. present(label)) then
113+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, &
114+
colormap=colormap, show_colorbar=show_colorbar, label=label)
115+
else if (present(levels) .and. present(colormap) .and. present(show_colorbar)) then
116+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, &
117+
colormap=colormap, show_colorbar=show_colorbar)
118+
else if (present(levels) .and. present(colormap) .and. present(label)) then
119+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, &
120+
colormap=colormap, label=label)
121+
else if (present(colormap) .and. present(show_colorbar) .and. present(label)) then
122+
call fig%add_contour_filled(wp_x, wp_y, wp_z, colormap=colormap, &
123+
show_colorbar=show_colorbar, label=label)
124+
else if (present(levels) .and. present(colormap)) then
125+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, colormap=colormap)
126+
else if (present(levels) .and. present(show_colorbar)) then
127+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, show_colorbar=show_colorbar)
128+
else if (present(levels) .and. present(label)) then
129+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, label=label)
130+
else if (present(colormap) .and. present(show_colorbar)) then
131+
call fig%add_contour_filled(wp_x, wp_y, wp_z, colormap=colormap, show_colorbar=show_colorbar)
132+
else if (present(colormap) .and. present(label)) then
133+
call fig%add_contour_filled(wp_x, wp_y, wp_z, colormap=colormap, label=label)
134+
else if (present(show_colorbar) .and. present(label)) then
135+
call fig%add_contour_filled(wp_x, wp_y, wp_z, show_colorbar=show_colorbar, label=label)
136+
else if (present(levels)) then
137+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels)
138+
else if (present(colormap)) then
139+
call fig%add_contour_filled(wp_x, wp_y, wp_z, colormap=colormap)
140+
else if (present(show_colorbar)) then
141+
call fig%add_contour_filled(wp_x, wp_y, wp_z, show_colorbar=show_colorbar)
142+
else if (present(label)) then
143+
call fig%add_contour_filled(wp_x, wp_y, wp_z, label=label)
144+
else
145+
call fig%add_contour_filled(wp_x, wp_y, wp_z)
146+
end if
117147

118148
deallocate(wp_x, wp_y, wp_z)
119149
if (allocated(wp_levels)) deallocate(wp_levels)
@@ -433,12 +463,42 @@ subroutine add_contour_filled(x, y, z, levels, colormap, show_colorbar, label)
433463
allocate(wp_levels(0))
434464
end if
435465

436-
! Forward ALL parameters to underlying method using single call pattern
437-
call fig%add_contour_filled(wp_x, wp_y, wp_z, &
438-
levels=merge(wp_levels, wp_levels, present(levels)), &
439-
colormap=merge(colormap, "", present(colormap)), &
440-
show_colorbar=merge(show_colorbar, .false., present(show_colorbar)), &
441-
label=merge(label, "", present(label)))
466+
! Forward parameters to underlying method using conditional calls for memory safety
467+
if (present(levels) .and. present(colormap) .and. present(show_colorbar) .and. present(label)) then
468+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, &
469+
colormap=colormap, show_colorbar=show_colorbar, label=label)
470+
else if (present(levels) .and. present(colormap) .and. present(show_colorbar)) then
471+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, &
472+
colormap=colormap, show_colorbar=show_colorbar)
473+
else if (present(levels) .and. present(colormap) .and. present(label)) then
474+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, &
475+
colormap=colormap, label=label)
476+
else if (present(colormap) .and. present(show_colorbar) .and. present(label)) then
477+
call fig%add_contour_filled(wp_x, wp_y, wp_z, colormap=colormap, &
478+
show_colorbar=show_colorbar, label=label)
479+
else if (present(levels) .and. present(colormap)) then
480+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, colormap=colormap)
481+
else if (present(levels) .and. present(show_colorbar)) then
482+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, show_colorbar=show_colorbar)
483+
else if (present(levels) .and. present(label)) then
484+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels, label=label)
485+
else if (present(colormap) .and. present(show_colorbar)) then
486+
call fig%add_contour_filled(wp_x, wp_y, wp_z, colormap=colormap, show_colorbar=show_colorbar)
487+
else if (present(colormap) .and. present(label)) then
488+
call fig%add_contour_filled(wp_x, wp_y, wp_z, colormap=colormap, label=label)
489+
else if (present(show_colorbar) .and. present(label)) then
490+
call fig%add_contour_filled(wp_x, wp_y, wp_z, show_colorbar=show_colorbar, label=label)
491+
else if (present(levels)) then
492+
call fig%add_contour_filled(wp_x, wp_y, wp_z, levels=wp_levels)
493+
else if (present(colormap)) then
494+
call fig%add_contour_filled(wp_x, wp_y, wp_z, colormap=colormap)
495+
else if (present(show_colorbar)) then
496+
call fig%add_contour_filled(wp_x, wp_y, wp_z, show_colorbar=show_colorbar)
497+
else if (present(label)) then
498+
call fig%add_contour_filled(wp_x, wp_y, wp_z, label=label)
499+
else
500+
call fig%add_contour_filled(wp_x, wp_y, wp_z)
501+
end if
442502

443503
deallocate(wp_x, wp_y, wp_z)
444504
if (allocated(wp_levels)) deallocate(wp_levels)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
program test_contour_memory_safety_regression
2+
!! Test to prevent memory safety regressions in contour plotting
3+
!! Specifically tests the segfault issue fixed in Issue #401 related to merge() function
4+
use fortplot
5+
implicit none
6+
7+
call test_contour_filled_no_optional_args()
8+
call test_contour_filled_with_colormap_only()
9+
call test_add_contour_filled_no_optional_args()
10+
call test_add_contour_filled_with_colormap_only()
11+
12+
print *, "All contour memory safety regression tests passed!"
13+
14+
contains
15+
16+
subroutine test_contour_filled_no_optional_args()
17+
!! Test contour_filled with no optional arguments - this caused segfault in Issue #401
18+
real(wp), dimension(10) :: x_grid, y_grid
19+
real(wp), dimension(10,10) :: z_grid
20+
integer :: i, j
21+
22+
! Generate simple test data
23+
do i = 1, 10
24+
x_grid(i) = (i-1) * 0.5_wp
25+
y_grid(i) = (i-1) * 0.5_wp
26+
end do
27+
28+
do i = 1, 10
29+
do j = 1, 10
30+
z_grid(i,j) = sin(x_grid(i)) * cos(y_grid(j))
31+
end do
32+
end do
33+
34+
! This should NOT segfault - the merge() issue was here
35+
call figure()
36+
call contour_filled(x_grid, y_grid, z_grid)
37+
38+
print *, "✓ contour_filled with no optional args - PASSED"
39+
end subroutine test_contour_filled_no_optional_args
40+
41+
subroutine test_contour_filled_with_colormap_only()
42+
!! Test contour_filled with only colormap argument
43+
real(wp), dimension(5) :: x_grid, y_grid
44+
real(wp), dimension(5,5) :: z_grid
45+
integer :: i, j
46+
47+
! Generate simple test data
48+
do i = 1, 5
49+
x_grid(i) = (i-1) * 1.0_wp
50+
y_grid(i) = (i-1) * 1.0_wp
51+
end do
52+
53+
do i = 1, 5
54+
do j = 1, 5
55+
z_grid(i,j) = x_grid(i) + y_grid(j)
56+
end do
57+
end do
58+
59+
call figure()
60+
call contour_filled(x_grid, y_grid, z_grid, colormap="plasma")
61+
62+
print *, "✓ contour_filled with colormap only - PASSED"
63+
end subroutine test_contour_filled_with_colormap_only
64+
65+
subroutine test_add_contour_filled_no_optional_args()
66+
!! Test add_contour_filled with no optional arguments
67+
real(wp), dimension(8) :: x_grid, y_grid
68+
real(wp), dimension(8,8) :: z_grid
69+
integer :: i, j
70+
71+
! Generate simple test data
72+
do i = 1, 8
73+
x_grid(i) = (i-1) * 0.25_wp
74+
y_grid(i) = (i-1) * 0.25_wp
75+
end do
76+
77+
do i = 1, 8
78+
do j = 1, 8
79+
z_grid(i,j) = exp(-(x_grid(i)**2 + y_grid(j)**2))
80+
end do
81+
end do
82+
83+
call figure()
84+
call add_contour_filled(x_grid, y_grid, z_grid)
85+
86+
print *, "✓ add_contour_filled with no optional args - PASSED"
87+
end subroutine test_add_contour_filled_no_optional_args
88+
89+
subroutine test_add_contour_filled_with_colormap_only()
90+
!! Test add_contour_filled with only colormap argument
91+
real(wp), dimension(6) :: x_grid, y_grid
92+
real(wp), dimension(6,6) :: z_grid
93+
integer :: i, j
94+
95+
! Generate simple test data
96+
do i = 1, 6
97+
x_grid(i) = (i-1) * 0.4_wp
98+
y_grid(i) = (i-1) * 0.4_wp
99+
end do
100+
101+
do i = 1, 6
102+
do j = 1, 6
103+
z_grid(i,j) = x_grid(i)**2 - y_grid(j)**2
104+
end do
105+
end do
106+
107+
call figure()
108+
call add_contour_filled(x_grid, y_grid, z_grid, colormap="inferno")
109+
110+
print *, "✓ add_contour_filled with colormap only - PASSED"
111+
end subroutine test_add_contour_filled_with_colormap_only
112+
113+
end program test_contour_memory_safety_regression

0 commit comments

Comments
 (0)