Skip to content

Commit 218f55d

Browse files
authored
Improve image checker (#699)
* Improve image checker * Clean up code
1 parent 45e5c38 commit 218f55d

File tree

4 files changed

+231
-13
lines changed

4 files changed

+231
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ zppy.egg-info/
66
\#*
77

88
examples/example_compy.cfg
9+
min_case_summary.md
910
test_*_output
1011
tests/*.cfg.txt
1112
tests/integration/image_check_failures*

tests/integration/test_images.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import os
2+
from math import ceil
3+
from typing import Dict
4+
5+
import matplotlib.backends.backend_pdf
6+
import matplotlib.image as mpimg
7+
from mache import MachineInfo
8+
from matplotlib import pyplot as plt
9+
10+
from tests.integration.utils import Results, check_mismatched_images, get_expansions
11+
12+
V3_CASE_NAME = "v3.LR.historical_0051"
13+
V2_CASE_NAME = "v2.LR.historical_0201"
14+
15+
16+
# TODO: fix issue where blank plots generate after so many pages in the PDF
17+
def make_image_diff_grid(diff_subdir, pdf_name="image_diff_grid.pdf", rows_per_page=2):
18+
machine_info = MachineInfo()
19+
web_portal_base_path = machine_info.config.get("web_portal", "base_path")
20+
web_portal_base_url = machine_info.config.get("web_portal", "base_url")
21+
print(f"web_portal_base_path: {web_portal_base_path}")
22+
print(f"web_portal_base_url: {web_portal_base_url}")
23+
print(f"Making image diff grid for {diff_subdir}")
24+
25+
if not diff_subdir.startswith(web_portal_base_path):
26+
print(
27+
f"diff_subdir {diff_subdir} is not a subdir of web_portal_base_path: {web_portal_base_path}"
28+
)
29+
return
30+
pdf_path = f"{diff_subdir}/{pdf_name}"
31+
pdf = matplotlib.backends.backend_pdf.PdfPages(pdf_path)
32+
print(f"Saving to:\n{pdf_path}")
33+
web_subdir = diff_subdir.removeprefix(web_portal_base_path)
34+
print(f"Web page will be at:\n{web_portal_base_url}/{web_subdir}/{pdf_name}")
35+
36+
prefixes = []
37+
# print(f"Walking diff_subdir: {diff_subdir}")
38+
for root, _, files in os.walk(diff_subdir):
39+
# print(f"root: {root}")
40+
for file_name in files:
41+
# print(f"file_name: {file_name}")
42+
if file_name.endswith("_diff.png"):
43+
prefixes.append(f"{root}/{file_name.split('_diff.png')[0]}")
44+
rows = len(prefixes)
45+
if rows == 0:
46+
# No diffs to collect into a PDF
47+
return
48+
cols = 3 # actual, expected, diff
49+
print(f"Constructing a {rows}x{cols} grid of image diffs")
50+
51+
num_pages = ceil(rows / rows_per_page)
52+
for page in range(num_pages):
53+
fig, axes = plt.subplots(rows_per_page, cols)
54+
print(f"Page {page}")
55+
for i, ax_row in enumerate(axes):
56+
count = page * 3 + i
57+
if count > len(prefixes) - 1:
58+
break
59+
# We already know all the files are in `diff_subdir`; no need to repeat it.
60+
short_title = prefixes[count].removeprefix(diff_subdir)
61+
print(f"short_title {i}: {short_title}")
62+
ax_row[1].set_title(short_title, fontsize=6)
63+
img = mpimg.imread(f"{prefixes[count]}_actual.png")
64+
ax_row[0].imshow(img)
65+
ax_row[0].set_xticks([])
66+
ax_row[0].set_yticks([])
67+
img = mpimg.imread(f"{prefixes[count]}_expected.png")
68+
ax_row[1].imshow(img)
69+
ax_row[1].set_xticks([])
70+
ax_row[1].set_yticks([])
71+
img = mpimg.imread(f"{prefixes[count]}_diff.png")
72+
ax_row[2].imshow(img)
73+
ax_row[2].set_xticks([])
74+
ax_row[2].set_yticks([])
75+
fig.tight_layout()
76+
pdf.savefig(1)
77+
plt.close(fig)
78+
pdf.close()
79+
plt.close("all")
80+
print(f"Reminder:\n{web_portal_base_url}/{web_subdir}/{pdf_name}")
81+
82+
83+
def check_images(expansions, cfg_specifier, case_name, task, diff_dir_suffix=""):
84+
expected_dir = expansions["expected_dir"]
85+
user_www = expansions["user_www"]
86+
unique_id = expansions["unique_id"]
87+
actual_images_dir = (
88+
f"{user_www}zppy_weekly_{cfg_specifier}_www/{unique_id}/{case_name}/"
89+
)
90+
91+
# The expected_images_file lists all images we expect to compare.
92+
expected_images_file = f"{expected_dir}image_list_expected_{cfg_specifier}.txt"
93+
expected_images_dir = f"{expected_dir}expected_{cfg_specifier}"
94+
95+
# The directory to place differences in.
96+
diff_dir = (
97+
f"{actual_images_dir}image_check_failures_{cfg_specifier}{diff_dir_suffix}"
98+
)
99+
100+
test_results = check_mismatched_images(
101+
actual_images_dir,
102+
expected_images_file,
103+
expected_images_dir,
104+
diff_dir,
105+
task,
106+
)
107+
diff_subdir = f"{diff_dir}/{task}"
108+
# Write missing and mismatched images to files
109+
missing_images_file = f"{diff_subdir}/missing_images.txt"
110+
if os.path.exists(missing_images_file):
111+
os.remove(missing_images_file)
112+
for missing_image in test_results.file_list_missing:
113+
with open(missing_images_file, "a") as f:
114+
f.write(f"{missing_image}\n")
115+
mismatched_images_file = f"{diff_subdir}/mismatched_images.txt"
116+
if os.path.exists(mismatched_images_file):
117+
os.remove(mismatched_images_file)
118+
for mismatched_image in test_results.file_list_mismatched:
119+
with open(mismatched_images_file, "a") as f:
120+
f.write(f"{mismatched_image}\n")
121+
# Create image diff grid
122+
# make_image_diff_grid(diff_subdir)
123+
return test_results
124+
125+
126+
def construct_markdown_summary_table(
127+
test_results_dict: Dict[str, Results], output_file_path: str
128+
):
129+
with open(output_file_path, "w") as f:
130+
f.write("# Summary of test results\n\n")
131+
f.write(
132+
"Diff subdir is where to find the lists of missing/mismatched images, the image diff grid, and the individual diffs.\n"
133+
)
134+
f.write("Note image diff grids can not yet be constructed automatically.\n")
135+
f.write(
136+
"| Test name | Total images | Correct images | Missing images | Mismatched images | Diff subdir | \n"
137+
)
138+
f.write("| --- | --- | --- | --- | --- | --- | \n")
139+
for test_name, test_results in test_results_dict.items():
140+
f.write(
141+
f"| {test_name} | {test_results.image_count_total} | {test_results.image_count_correct} | {test_results.image_count_missing} | {test_results.image_count_mismatched} | {test_results.diff_dir}/{test_results.task} | \n"
142+
)
143+
144+
145+
def test_images():
146+
# To test a different branch, set this to True, and manually set the expansions.
147+
TEST_DIFFERENT_EXPANSIONS = False
148+
if TEST_DIFFERENT_EXPANSIONS:
149+
expansions = dict()
150+
# Example settings:
151+
expansions["expected_dir"] = "/lcrc/group/e3sm/public_html/zppy_test_resources/"
152+
expansions["user_www"] = (
153+
"/lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2/"
154+
)
155+
expansions["unique_id"] = "test_zppy_20250401"
156+
diff_dir_suffix = "_test_pr699_try6"
157+
else:
158+
expansions = get_expansions()
159+
diff_dir_suffix = ""
160+
test_results_dict: Dict[str, Results] = dict()
161+
for task in ["e3sm_diags", "mpas_analysis", "global_time_series", "ilamb"]:
162+
test_results = check_images(
163+
expansions,
164+
"comprehensive_v2",
165+
V2_CASE_NAME,
166+
task,
167+
diff_dir_suffix=diff_dir_suffix,
168+
)
169+
test_results_dict[f"comprehensive_v2_{task}"] = test_results
170+
for task in ["e3sm_diags", "mpas_analysis", "global_time_series", "ilamb"]:
171+
test_results = check_images(
172+
expansions,
173+
"comprehensive_v3",
174+
V3_CASE_NAME,
175+
task,
176+
diff_dir_suffix=diff_dir_suffix,
177+
)
178+
test_results_dict[f"comprehensive_v3_{task}"] = test_results
179+
for task in ["e3sm_diags", "global_time_series", "ilamb"]: # No mpas_analysis
180+
test_results = check_images(
181+
expansions,
182+
"bundles",
183+
V3_CASE_NAME,
184+
task,
185+
diff_dir_suffix=diff_dir_suffix,
186+
)
187+
test_results_dict[f"bundles_{task}"] = test_results
188+
md_path = "min_case_summary.md"
189+
construct_markdown_summary_table(test_results_dict, md_path)
190+
print(f"Copy output of {md_path} to a PR comment.")
191+
for tr in test_results_dict.values():
192+
assert tr.image_count_total == tr.image_count_correct

tests/integration/test_weekly.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def check_images(test_name, case_name, subdir):
2828
expected_images_file,
2929
expected_images_dir,
3030
diff_dir,
31-
[subdir],
31+
subdir,
3232
)
3333

3434

tests/integration/utils.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,31 @@
1212
# Image checking ##########################################################
1313

1414

15+
# Originally in https://github.com/E3SM-Project/zppy/pull/695
16+
# And https://github.com/E3SM-Project/zppy/pull/698
17+
class Results(object):
18+
def __init__(
19+
self,
20+
diff_dir: str,
21+
task: str,
22+
image_count_total: int,
23+
file_list_missing: List[str],
24+
file_list_mismatched: List[str],
25+
):
26+
if image_count_total == 0:
27+
raise ValueError(f"No images found for task {task} in {diff_dir}")
28+
self.diff_dir = diff_dir
29+
self.task = task
30+
self.image_count_total = image_count_total
31+
self.image_count_missing = len(file_list_missing)
32+
self.image_count_mismatched = len(file_list_mismatched)
33+
self.image_count_correct = (
34+
image_count_total - len(file_list_missing) - len(file_list_mismatched)
35+
)
36+
self.file_list_missing = sorted(file_list_missing)
37+
self.file_list_mismatched = sorted(file_list_mismatched)
38+
39+
1540
# Copied from E3SM Diags
1641
def compare_images(
1742
missing_images,
@@ -92,24 +117,24 @@ def compare_images(
92117

93118

94119
def check_mismatched_images(
95-
actual_images_dir,
96-
expected_images_file,
97-
expected_images_dir,
98-
diff_dir,
99-
subdirs_to_check,
100-
):
120+
actual_images_dir: str,
121+
expected_images_file: str,
122+
expected_images_dir: str,
123+
diff_dir: str,
124+
task: str,
125+
) -> Results:
101126
missing_images: List[str] = []
102127
mismatched_images: List[str] = []
103128

104129
counter = 0
130+
print(f"Opening expected images file {expected_images_file}")
105131
with open(expected_images_file) as f:
132+
print(f"Reading expected images file {expected_images_file}")
106133
for line in f:
107134
image_name = line.strip("./").strip("\n")
108135
proceed = False
109-
for subdir in subdirs_to_check:
110-
if image_name.startswith(subdir):
111-
proceed = True
112-
break
136+
if image_name.startswith(task):
137+
proceed = True
113138
if proceed:
114139
counter += 1
115140
if counter % 250 == 0:
@@ -142,6 +167,7 @@ def check_mismatched_images(
142167
print(
143168
f"Number of correct images: {counter - len(missing_images) - len(mismatched_images)}"
144169
)
170+
test_results = Results(diff_dir, task, counter, missing_images, mismatched_images)
145171

146172
# Make diff_dir readable
147173
if os.path.exists(diff_dir):
@@ -151,8 +177,7 @@ def check_mismatched_images(
151177
# That is, if we're in this case, we expect the following:
152178
assert len(missing_images) == counter
153179

154-
assert missing_images == []
155-
assert mismatched_images == []
180+
return test_results
156181

157182

158183
# Multi-machine testing ##########################################################

0 commit comments

Comments
 (0)