Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CI misc improvements #296

Merged
merged 54 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
54182de
refactor to use f-strings
justinlaughlin Jul 23, 2024
2bdf1b7
fix some typos
justinlaughlin Jul 23, 2024
6646eaf
add requirements.txt
justinlaughlin Jul 23, 2024
f6b6fe4
add fail-fast:false to strategy so tests always complete
justinlaughlin Jul 23, 2024
9682049
update workflow to use requirements.txt
justinlaughlin Jul 23, 2024
3e48920
fix a couple more typos
justinlaughlin Jul 23, 2024
112ec9f
try adding macos-13 to see if it is more stable
justinlaughlin Jul 24, 2024
677bf57
removed .saved from name and fix a typo in an unused function
justinlaughlin Jul 24, 2024
5187edb
update name in cmakelists
justinlaughlin Jul 24, 2024
424d285
cleanup. move unused code to test_cmd.py
justinlaughlin Jul 24, 2024
7673158
update baseline_name
justinlaughlin Jul 24, 2024
03a6793
revert adding macos-13
justinlaughlin Jul 24, 2024
3fbf597
try forcing build on macos to see if it helps with stability
justinlaughlin Jul 24, 2024
2f05fce
(wip) implementing image diff
justinlaughlin Jul 24, 2024
f3a51d2
image diff implemented
justinlaughlin Jul 24, 2024
8f17c63
runner.os should be matrix.os
justinlaughlin Jul 24, 2024
741c23a
testing... remove macos-latest from cache step
justinlaughlin Jul 24, 2024
1853226
try adding image diff to ci driver
justinlaughlin Jul 24, 2024
d12eaa8
remove unnecessary (?) extra tar command in test GLVis (cmake/linux)
justinlaughlin Jul 24, 2024
6cb85f4
update ci script to upload image diff in tarball
justinlaughlin Jul 24, 2024
eaacb1b
update image diff so zoom/pan is syncronized
justinlaughlin Jul 24, 2024
7cdd418
for now, also generate image diff for 'zoom' tests so we know its wor…
justinlaughlin Jul 24, 2024
00a5413
A try to fix MacOS runners.
najlkin Jul 24, 2024
21a446f
Another try to fix MacOS runners.
najlkin Jul 24, 2024
a033fb4
A safer try for MacOS.
najlkin Jul 24, 2024
81df60b
Revert "A safer try for MacOS."
najlkin Jul 24, 2024
c8fc30d
Revert "Another try to fix MacOS runners."
najlkin Jul 24, 2024
6433a9d
Revert "A try to fix MacOS runners."
najlkin Jul 24, 2024
4497f4d
Fixed init of the main thread.
najlkin Jul 24, 2024
85360c7
add type hinting and only generate artifacts on test failure
justinlaughlin Jul 25, 2024
8be84a0
revert force build for macos
justinlaughlin Jul 25, 2024
e1e3bcd
typo
justinlaughlin Jul 25, 2024
3d63020
cleanup
justinlaughlin Jul 25, 2024
8629302
make sure exist_ok=True
justinlaughlin Jul 25, 2024
9cf8f95
change subplot titles
justinlaughlin Jul 25, 2024
73b28a1
use source= argument for generating html files
justinlaughlin Jul 25, 2024
caaf323
remove dir structure from tarball
justinlaughlin Jul 25, 2024
0d1407a
Merge branch 'master' into ci-misc-improvements
justinlaughlin Jul 25, 2024
2d5c224
fix tarball
justinlaughlin Jul 25, 2024
00c02c1
change behavior to always generate diff pngs. generate html on failures
justinlaughlin Jul 25, 2024
582fd25
fix path for tarball
justinlaughlin Jul 25, 2024
c3dfcdb
fix erroneous extra extension
justinlaughlin Jul 25, 2024
b56afa3
typo
justinlaughlin Jul 25, 2024
8073132
index bug
justinlaughlin Jul 25, 2024
3408a23
use old glvis/data hash for illustration
justinlaughlin Jul 25, 2024
11784ae
change subtitles to image_names
justinlaughlin Jul 25, 2024
e91ab56
revert the glvis/data hash even further for testing
justinlaughlin Jul 25, 2024
6eeda89
include_plotlyjs='cdn' makes html much smaller but requires internet
justinlaughlin Jul 25, 2024
1dea37d
since file is small now, always generate diff plot
justinlaughlin Jul 25, 2024
f2b0788
change glvis/data back to latest version
justinlaughlin Jul 25, 2024
92d9ec6
Merge branch 'master' into ci-misc-improvements
tzanio Aug 10, 2024
58cb4fe
New line
tzanio Aug 13, 2024
4b41c58
update data to most recent
justinlaughlin Aug 13, 2024
a6ff194
update changelog
justinlaughlin Aug 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ env:
jobs:
builds-and-tests:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
target: [dbg, opt]
Expand Down Expand Up @@ -230,7 +231,7 @@ jobs:
if: matrix.build-system == 'cmake'
run: |
python -m pip install --upgrade pip
pip install scikit-image
python -m pip install -r glvis/tests/requirements.txt

- name: setup Linux testing dependencies
if: matrix.build-system == 'cmake' && matrix.os == 'ubuntu-latest'
Expand All @@ -242,7 +243,6 @@ jobs:
run: |
cd glvis && cd build
xvfb-run -a ctest --verbose
tar czvf test_screenshots.tar.gz tests/test.*.png

- name: test GLVis (cmake/mac)
if: matrix.build-system == 'cmake' && matrix.os == 'macos-latest'
Expand All @@ -254,7 +254,7 @@ jobs:
if: always() && matrix.build-system == 'cmake' && matrix.os != 'windows-latest'
run: |
cd glvis && cd build
tar czvf test_screenshots.tar.gz tests/test.*.png
cd tests && tar czvf ../test_screenshots.tar.gz outputs

- name: upload test screenshots
if: always() && matrix.build-system == 'cmake' && matrix.os != 'windows-latest'
Expand Down
4 changes: 2 additions & 2 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ foreach(test_name IN LISTS stream_tests)
COMMAND ${CMAKE_COMMAND} -E make_directory
${CMAKE_CURRENT_SOURCE_DIR}/data/baselines/local
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_BINARY_DIR}/test.${test_name}.saved.png
${CMAKE_CURRENT_BINARY_DIR}/test.${test_name}.png
${CMAKE_CURRENT_SOURCE_DIR}/data/baselines/local
DEPENDS
${CMAKE_CURRENT_BINARY_DIR}/test.${test_name}.saved.png
${CMAKE_CURRENT_BINARY_DIR}/test.${test_name}.png
VERBATIM)

add_dependencies(rebaseline _rebaseline_stream_${test_name})
Expand Down
196 changes: 104 additions & 92 deletions tests/glvis_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,21 @@
import argparse
import sys
import os
from skimage.io import imread
import numpy as np
from base64 import b64encode
from skimage.io import imread, imsave
from skimage.metrics import structural_similarity
from skimage.color import rgb2gray, gray2rgb
from plotly.subplots import make_subplots
import plotly.graph_objects as go

def compare_images(
baseline_file: str,
output_file: str,
expect_fail: bool = False,
CUTOFF_SSIM: float = 0.999
) -> bool:

# Below are key commands that are passed to the -keys command-line argument for
# glvis in order to perform testing on raw mesh/grid function data (i.e. non-
# streams).
#
# Currently not in use.
test_cases = {
"magnify": "*****",
"axes1": "a",
"axes2": "aa",
"mesh1": "m",
"mesh2": "mm",
"cut_plane": "i",
"cut_plane_rotate": "iyyyy",
"cut_plane_rotate_back": "iyyyyYYYY",
"cut_plane_transl": "izzzz",
"cut_plane_transl_back": "izzzzZZZZ",
"orient2d_1": "R",
"orient2d_2": "RR",
"orient2d_3": "RRR",
"orient2d_4": "RRRR",
"orient2d_5": "RRRRR",
"orient2d_6": "RRRRRR",
"orient3d": "Rr",
"perspective": "j",
}

screenshot_keys = "Sq"
screenshot_file = "GLVis_s01.png"

cutoff_ssim = 0.999

def compare_images(baseline_file, output_file, expect_fail=False):
# Try to open output image
output_img = imread(output_file)
if output_img is None:
Expand All @@ -62,7 +42,7 @@ def compare_images(baseline_file, output_file, expect_fail=False):
# Compare images with SSIM metrics. For two exactly-equal images, SSIM=1.0.
# We set a cutoff of 0.999 to account for possible differences in rendering.
ssim = structural_similarity(baseline_img, output_img, channel_axis=2)
if ssim < cutoff_ssim:
if ssim < CUTOFF_SSIM:
if expect_fail:
print("[PASS] Differences were detected in the control case.")
else:
Expand All @@ -72,92 +52,120 @@ def compare_images(baseline_file, output_file, expect_fail=False):
print("[FAIL] Differences were not detected in the control case.")
else:
print("[PASS] Images match.")
print(" actual ssim = {}, cutoff = {}".format(ssim, cutoff_ssim))
return ssim >= cutoff_ssim if not expect_fail else ssim < cutoff_ssim

# Function to test a given glvis command with a variety of key-based commands.
# Not currently in use.
def test_case(exec_path, exec_args, baseline, t_group, t_name, cmd):
print("Testing {0}:{1}...".format(t_group, t_name))
full_screenshot_cmd = cmd + screenshot_keys
cmd = "{0} {1} -k \"{2}\"".format(exec_path, exec_args, full_screenshot_cmd)
print("Exec: {}".format(cmd))
ret = os.system(cmd + " > /dev/null 2>&1")
if ret != 0:
print("[FAIL] GLVis exited with error code {}.".format(ret))
return False
if not os.path.exists(t_group):
os.mkdir(t_group)
output_name = "{0}/{1}.png".format(t_group, t_name)
print(f" actual ssim = {ssim}, cutoff = {CUTOFF_SSIM}")
return ssim >= CUTOFF_SSIM if not expect_fail else ssim < CUTOFF_SSIM

def color_distance(I1: np.array, I2: np.array) -> dict[str, np.array]:
"""
L2-norm in rgb space. There are better ways but this is probably good enough.
"""
NORM_CONSTANT = (3*(255**2))**0.5 # max distance
l2norm = lambda x: np.linalg.norm(x, ord=2, axis=2)
delta = l2norm(I2.astype(int)-I1.astype(int)) / NORM_CONSTANT # output is NxM [0,1]
# now we scale to [0,255] and cast as uint8 so it is a "proper" image
Idiff_abs = (delta * 255).astype(np.uint8)
# get relative version
Idiff_rel = (Idiff_abs / Idiff_abs.max() * 255).astype(np.uint8)
return {'abs': Idiff_abs,
'rel': Idiff_rel,}

def generate_image_diffs(
image1_filename: str,
image2_filename: str,
absdiff_filename: str,
reldiff_filename: str,
) -> None:
# Images are read as NxMx3 [uint8] arrays from [0,255]
I1 = imread(image1_filename)
I2 = imread(image2_filename)
# Get the image diffs (abs and rel)
Idiffs = color_distance(I1, I2) # output is NxM [0,1]
# Save 3-channel image to file
imsave(absdiff_filename, gray2rgb(Idiffs['abs']))
imsave(reldiff_filename, gray2rgb(Idiffs['rel']))

# For the source= argument in plotly
def _get_image_src(filename):
with open(filename, "rb") as f:
image_bytes = b64encode(f.read()).decode()
return f"data:image/png;base64,{image_bytes}"

def image_comparison_plot(
image_filenames: list[str],
image_names: list[str], # for subtitles
output_filename: str,
):
"""
Illustrate results as an interactive plotly figure (html)
"""
assert len(image_filenames) == len(image_names)
n = len(image_filenames)
fig = make_subplots(rows=1, cols=n,
shared_xaxes=True,
shared_yaxes=True,
subplot_titles=image_names)
for idx, filename in enumerate(image_filenames):
fig.add_trace(go.Image(source=_get_image_src(filename)), 1, idx+1)
fig.update_xaxes(matches='x', showticklabels=False, showgrid=False, zeroline=False)
fig.update_yaxes(matches='y', showticklabels=False, showgrid=False, zeroline=False)
fig.write_html(output_filename, include_plotlyjs='cdn')

def test_stream(
exec_path: str,
exec_args: str,
save_file: str,
baseline: str
) -> bool:

ret = os.system("mv {0} {1}".format(screenshot_file, output_name))
if ret != 0:
print("[FAIL] Could not move output image: exit code {}.".format(ret))
return False

if baseline:
baseline_name = "{0}/test.{1}.png".format(baseline, test_name)
return compare_images(baseline_name, output_name)
else:
print("[IGNORE] No baseline exists to compare against.")
return True

def test_stream(exec_path, exec_args, save_file, baseline):
if exec_args is None:
exec_args = ""
test_name = os.path.basename(save_file)
print("Testing {}...".format(save_file))
print(f"Testing {save_file}...")
test_name = os.path.basename(save_file).replace(".saved", "") # e.g. "ex3"
output_dir = f"outputs/{test_name}"
os.makedirs(output_dir, exist_ok=True)

# Create new stream file with command to screenshot and close
stream_data = None
with open(save_file) as in_f:
stream_data = in_f.read()

output_name = "test.{}.png".format(test_name)
output_name_fail = "test.fail.{}.png".format(test_name)
output_name = f"{output_dir}/test.nominal.{test_name}.png"
output_name_fail = f"{output_dir}/test.zoom.{test_name}.png"
absdiff_name = f"{output_dir}/test.nominal.absdiff.{test_name}.png"
reldiff_name = f"{output_dir}/test.nominal.reldiff.{test_name}.png"
tmp_file = "test.saved"
with open(tmp_file, 'w') as out_f:
out_f.write(stream_data)
out_f.write("\nwindow_size 800 600")
out_f.write("\nscreenshot {}".format(output_name))
out_f.write(f"\nscreenshot {output_name}")
# Zooming in should create some difference in the images
out_f.write("\nkeys *")
out_f.write("\nscreenshot {}".format(output_name_fail))
out_f.write(f"\nscreenshot {output_name_fail}")
out_f.write("\nkeys q")

# Run GLVis with modified stream file
cmd = "{0} {1} -saved {2}".format(exec_path, exec_args, tmp_file)
print("Exec: {}".format(cmd))
cmd = f"{exec_path} {exec_args} -saved {tmp_file}"
print(f"Exec: {cmd}")
ret = os.system(cmd)
if ret != 0:
print("[FAIL] GLVis exited with error code {}.".format(ret))
print(f"[FAIL] GLVis exited with error code {ret}.")
return False

if baseline:
baseline_name = "{0}/test.{1}.png".format(baseline, test_name)
baseline_name = f"{baseline}/test.{test_name}.saved.png"
test_baseline = compare_images(baseline_name, output_name)
test_control = compare_images(baseline_name, output_name_fail,
expect_fail=True)
generate_image_diffs(baseline_name, output_name, absdiff_name, reldiff_name)
# Generate an interactive html plot, only if the test fails
# if not test_baseline:
image_comparison_plot([baseline_name, output_name, reldiff_name],
["Baseline", "Test Output", "Normalized Diff"],
reldiff_name.replace(".png", ".html"))
test_control = compare_images(baseline_name, output_name_fail, expect_fail=True)
return (test_baseline and test_control)
else:
print("[IGNORE] No baseline exists to compare against.")
return True

def test_cmd(exec_path, exec_args, tgroup, baseline):
try:
os.remove(screenshot_file)
except OSError:
pass
all_tests_passed = True
for testname, cmds in test_cases.items():
result = test_case(exec_path, exec_args, baseline, tgroup, testname, cmds)
all_tests_passed = all_tests_passed and result

if all_tests_passed:
print("All tests passed.")
else:
sys.exit(1)

if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-s", "--save_stream", help="Path to a GLVis saved stream file.")
Expand All @@ -166,9 +174,13 @@ def test_cmd(exec_path, exec_args, tgroup, baseline):
parser.add_argument("-n", "--group_name", help="Name of the test group.")
parser.add_argument("-b", "--baseline", help="Path to test baseline.")
args = parser.parse_args()

# Make a directory for storing test outputs
os.makedirs("outputs", exist_ok=True)
# Run tests
if args.save_stream is not None:
result = test_stream(args.exec_cmd, args.exec_args, args.save_stream, args.baseline)
if not result:
sys.exit(1)
else:
test_cmd(args.exec_cmd, args.exec_args, args.group_name, args.baseline)
raise Exception("--save_stream must be specified. test_cmd() is unused. Import from `test_cmd.py`")
3 changes: 3 additions & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
scikit-image
plotly
numpy >= 1.20.0, < 2.0.0
84 changes: 84 additions & 0 deletions tests/test_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright (c) 2010-2024, Lawrence Livermore National Security, LLC. Produced
# at the Lawrence Livermore National Laboratory. All Rights reserved. See files
# LICENSE and NOTICE for details. LLNL-CODE-443271.
#
# This file is part of the GLVis visualization tool and library. For more
# information and source code availability see https://glvis.org.
#
# GLVis is free software; you can redistribute it and/or modify it under the
# terms of the BSD-3 license. We welcome feedback and contributions, see file
# CONTRIBUTING.md for details.

"""
Code snippets to test glvis in the command-line. None of the code
contained here is currently being used.
"""

# Globals
screenshot_keys = "Sq"
screenshot_file = "GLVis_s01.png"

# Below are key commands that are passed to the -keys command-line argument for
# glvis in order to perform testing on raw mesh/grid function data (i.e. non-
# streams).
test_cases = {
"magnify": "*****",
"axes1": "a",
"axes2": "aa",
"mesh1": "m",
"mesh2": "mm",
"cut_plane": "i",
"cut_plane_rotate": "iyyyy",
"cut_plane_rotate_back": "iyyyyYYYY",
"cut_plane_transl": "izzzz",
"cut_plane_transl_back": "izzzzZZZZ",
"orient2d_1": "R",
"orient2d_2": "RR",
"orient2d_3": "RRR",
"orient2d_4": "RRRR",
"orient2d_5": "RRRRR",
"orient2d_6": "RRRRRR",
"orient3d": "Rr",
"perspective": "j",
}

# Function to test a given glvis command with a variety of key-based commands.
def test_case(exec_path, exec_args, baseline, t_group, t_name, cmd):
print(f"Testing {t_group}:{t_name}...")
full_screenshot_cmd = cmd + screenshot_keys
cmd = f"{exec_path} {exec_args} -k \"{full_screenshot_cmd}\""
print(f"Exec: {cmd}")
ret = os.system(cmd + " > /dev/null 2>&1")
if ret != 0:
print(f"[FAIL] GLVis exited with error code {ret}.")
return False
if not os.path.exists(t_group):
os.mkdir(t_group)
output_name = f"{t_group}/{t_name}.png"

ret = os.system(f"mv {screenshot_file} {output_name}")
if ret != 0:
print(f"[FAIL] Could not move output image: exit code {ret}.")
return False

if baseline:
baseline_name = f"{baseline}/test.{t_name}.png"
return compare_images(baseline_name, output_name)
else:
print("[IGNORE] No baseline exists to compare against.")
return True

def test_cmd(exec_path, exec_args, tgroup, baseline):
try:
os.remove(screenshot_file)
except OSError:
pass
all_tests_passed = True
for testname, cmds in test_cases.items():
result = test_case(exec_path, exec_args, baseline, tgroup, testname, cmds)
all_tests_passed = all_tests_passed and result

if all_tests_passed:
print("All tests passed.")
else:
sys.exit(1)
Loading