Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 60 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI
on:
push:
branches:
- main
tags: ['*']
pull_request:
workflow_dispatch:
concurrency:
# skip intermediate builds: always, cancel intermediate builds: only if it is a pull request build
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 120
permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created
actions: write
contents: read
strategy:
fail-fast: false
matrix:
version:
- 'lts' # 1.10
- '1.11'
- '1.12'
- '1' # latest release
os:
- ubuntu-latest
- macos-latest
- windows-latest
steps:
- uses: actions/checkout@v5
- uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.version }}
- uses: julia-actions/cache@v2

# GLMakie needs OpenGL. On Linux, we use a virtual display and the GUI tests are
# required to run. On Windows we install a software OpenGL driver (Mesa); there and
# on macOS the GUI tests are run if an OpenGL context can be created.
- name: Install virtual display (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb libgl1 libglx-mesa0 mesa-utils
- name: Install Mesa OpenGL (Windows)
if: runner.os == 'Windows'
uses: ssciwr/setup-mesa-dist-win@v2

- name: Disable GLMakie precompilation (macOS)
# the GLMakie precompile workload needs OpenGL, which the macOS runners don't have
if: runner.os == 'macOS'
run: printf '[GLMakie]\nprecompile_workload = false\n' > LocalPreferences.toml

- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
with:
prefix: ${{ runner.os == 'Linux' && 'xvfb-run -s ''-screen 0 1024x768x24''' || '' }}
env:
ADA_GUI_TESTS: ${{ runner.os == 'Linux' && 'true' || 'auto' }}
15 changes: 8 additions & 7 deletions src/AdA_VizPickTool_V1.jl
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function start_AdA_Picker(;data=nothing)
############################################################
# LOAD PROFILE DATA
@async begin
fn = fetch(Threads.@spawn pick_file(""))
fn = fetch(Threads.@spawn PICK_FILE[](""))

data = load(fn,"Profile") # we need to make this more foolproof, I don't think we can rely on people calling hteir profile structure Profile
println(fn*" loaded")
Expand Down Expand Up @@ -362,7 +362,7 @@ function start_AdA_Picker(;data=nothing)
elseif s == "Load Picks..."
# load picks from a text file, these will be modifyable
@async begin
fn = fetch(Threads.@spawn pick_file(""))
fn = fetch(Threads.@spawn PICK_FILE[](""))
println(fn)


Expand Down Expand Up @@ -410,7 +410,7 @@ function start_AdA_Picker(;data=nothing)
elseif s == "Load Picks (not modifyable)..."
# load the picks as point data, these will be treated in a similar way as e.g. the seismicity data
@async begin
fn = fetch(Threads.@spawn pick_file(""))
fn = fetch(Threads.@spawn PICK_FILE[](""))

# test plot in ax1
#lines!(ax1, [0,100],[0,-200], color = :red,linewidth = 3)
Expand Down Expand Up @@ -476,19 +476,20 @@ function start_AdA_Picker(;data=nothing)
lat_pick = interp_linear_lat(x_pick)
lon_pick = interp_linear_lon(x_pick)

picks = (x = x_pick, depth = y_pick, lat = lat_pick, lon = lon_pick)
# use a new name here: `picks` is the (global) Observable used for picking
pick_data = (x = x_pick, depth = y_pick, lat = lat_pick, lon = lon_pick)
pick_info = (user_name = pick_name.stored_string[], date = now(), units = (x = "km", depth = "km", lat = "deg", lon = "deg"))
profile_info = (start_lonlat = data.start_lonlat, end_lonlat = data.end_lonlat)

@async begin
fn_save = fetch(Threads.@spawn save_file("")) # open native file dialog and choose a filename
fn_save = fetch(Threads.@spawn SAVE_FILE[]("")) # open native file dialog and choose a filename
filetype = split(fn_save,".")[end] # get the ending
# save depending on file ending
if filetype == "jld2"
# file should contain: profile information, picker information, picks, lat and lon of the picked points

# save as jld2 file
jldsave(fn_save; picks=picks, pick_info=pick_info, profile_info=profile_info)
jldsave(fn_save; picks=pick_data, pick_info=pick_info, profile_info=profile_info)
println("... "*fn_save*" saved")
elseif filetype == "csv"
println("Saving as csv is not implemented yet")
Expand All @@ -503,7 +504,7 @@ function start_AdA_Picker(;data=nothing)
elseif s == "Save Screenshot..."
# save a screen shot of the makie window
@async begin
fn_screen = fetch(Threads.@spawn save_file(""))
fn_screen = fetch(Threads.@spawn SAVE_FILE[](""))
save(fn_screen, fig, px_per_unit = 2)
println(fn_screen*" saved")
end
Expand Down
7 changes: 6 additions & 1 deletion src/FileIO/fileIO_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ using FileIO
using NativeFileDialog
using JLD2

# The GUI opens the native file dialogs through these Refs, so that the tests can
# replace them by functions that return a fixed filename (there is no user on CI)
const PICK_FILE = Ref{Function}(pick_file)
const SAVE_FILE = Ref{Function}(save_file)

# function for fileIO menu response
function menu_fileIO_response(s)
if s == "Load Profile..."
@async begin
filename = fetch(Threads.@spawn pick_file(""))
filename = fetch(Threads.@spawn PICK_FILE[](""))
println(filename)
#data = load_GMG(filename) # load the profile data, this did not work properly
data = load(filename,"Profile")
Expand Down
57 changes: 54 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,57 @@
using AdriaArrayPicker
using AdriaArrayGeometryPicker
using GLMakie
using Test

@testset "AdriaArrayPicker.jl" begin
# Write your tests here.
include("synthetic_profile.jl")

# The GUI tests need OpenGL. By default they only run if a GLMakie window can be created,
# which is not the case on most headless machines. Set ADA_GUI_TESTS=true to enforce them
# (e.g. on CI with a virtual display) or ADA_GUI_TESTS=false to skip them.
function opengl_available()
try
screen = GLMakie.Screen(visible = false)
close(screen)
return true
catch err
@warn "Cannot create an OpenGL context" exception = err
return false
end
end

const GUI_TESTS = let setting = lowercase(get(ENV, "ADA_GUI_TESTS", "auto"))
setting == "auto" ? opengl_available() : parse(Bool, setting)
end

@testset "AdriaArrayGeometryPicker.jl" begin
@testset "Package" begin
@test isdefined(AdriaArrayGeometryPicker, :start_AdA_Picker)
@test :start_AdA_Picker in names(AdriaArrayGeometryPicker)
@test isfile(joinpath(pkgdir(AdriaArrayGeometryPicker), "assets", "AdA_Picker_logo_tr.png"))
end

@testset "Synthetic profile" begin
# checks that the profile has the structure the GUI relies on
dir = mktempdir()
fn = save_synthetic_profile(joinpath(dir, "profile.jld2"))
P = load(fn, "Profile")
@test P.vertical
@test P.start_lonlat == START_LONLAT && P.end_lonlat == END_LONLAT
@test keys(P.VolData.fields)[end-1:end] == (:FlatCrossSection, :x_profile)
@test haskey(P.SurfData, :Topography) && haskey(P.SurfData.Topography.fields, :x_profile)
@test haskey(P.PointData.Seismicity.fields, :depth_proj)
end

@testset "Controls" begin
fig = Figure()
menu = AdriaArrayGeometryPicker.set_fileIO_menu!(fig, fig[1, 1])
@test menu isa Menu
@test ["Load Profile...", "Load Picks...", "Save Picks...", "Save Screenshot..."] ⊆ menu.options[]
end

if GUI_TESTS
include("test_gui.jl")
else
@warn "Skipping GUI tests (no OpenGL available)"
@test_skip "GUI"
end
end
40 changes: 40 additions & 0 deletions test/synthetic_profile.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Creates a small synthetic vertical profile with GeophysicalModelGenerator, containing
# the same kind of data the GUI expects: volume data (tomographies), surface data
# (topography + Moho) and point data (seismicity)
using GeophysicalModelGenerator, JLD2

const START_LONLAT = (11.0, 45.0)
const END_LONLAT = (19.0, 45.0)

function create_synthetic_profile(; npoints = 200)
# volume data
lon, lat, z = lonlatdepth_grid(10:0.5:20, 40:0.5:50, -300:10:0)
dVs = @. 0.5 * sin(lon / 2) * cos(z / 50)
dVp = @. 2 * dVs
Vol = GeoData(lon, lat, z, (dVs = dVs, dVp = dVp))

# surface data
lon2, lat2, z2 = lonlatdepth_grid(10:0.5:20, 40:0.5:50, 0)
topo = @. 1.0 * sin(lon2)
moho = @. -35 + 3 * cos(lat2)
Topo = GeoData(lon2, lat2, z2 .+ topo, (Topography = topo * km,))
Moho = GeoData(lon2, lat2, z2 .+ moho, (MohoDepth = moho * km,))

# point data, located close to the profile
elon = collect(range(10.5, 19.5, length = npoints))
elat = 45.0 .+ 0.2 .* sin.(1:npoints)
ez = -100.0 .* (0.5 .+ 0.5 .* cos.(1:npoints))
Seis = GeoData(elon, elat, ez, (Magnitude = 3.0 .+ sin.(1:npoints),))

Profile = ProfileData(start_lonlat = START_LONLAT, end_lonlat = END_LONLAT)
extract_ProfileData!(Profile, Vol, (Topography = Topo, Moho = Moho), (Seismicity = Seis,);
DimsVolCross = (50, 40), Depth_extent = (-300, 0))

return Profile
end

# the GUI loads the variable `Profile` from the jld2 file
function save_synthetic_profile(filename)
jldsave(filename; Profile = create_synthetic_profile())
return filename
end
125 changes: 125 additions & 0 deletions test/test_gui.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Drives the GUI through its menus, the way a user would. The native file dialogs are
# replaced by functions returning a fixed filename, see `PICK_FILE`/`SAVE_FILE`.
using AdriaArrayGeometryPicker, GLMakie, JLD2, Test

const AGP = AdriaArrayGeometryPicker

# menu callbacks run asynchronously, so we have to wait for them to finish
function wait_until(f; timeout = 300)
status = timedwait(f, timeout; pollint = 0.1)
return status == :ok
end

find_blocks(fig, T) = filter(b -> b isa T, fig.content)
find_plots(ax, T) = filter(p -> p isa T, ax.scene.plots)

function select!(menu, option)
menu.selection[] = option
return nothing
end

@testset "GUI" begin
GLMakie.activate!(visible = false) # don't pop up windows during the tests

dir = mktempdir()
profile_file = save_synthetic_profile(joinpath(dir, "test_profile.jld2"))
picks_file = joinpath(dir, "picks.jld2")
screen_file = joinpath(dir, "screenshot.png")

fig = start_AdA_Picker()
@test fig isa Figure

menus = find_blocks(fig, Menu)
main_menu = only(filter(m -> "Load Profile..." in m.options[], menus))

@testset "Load profile" begin
AGP.PICK_FILE[] = _ -> profile_file
select!(main_menu, "Load Profile...")

# loading is done once the colormap menu (the last control) has its callback
loaded() = any(m -> :roma in m.options[] && !isempty(GLMakie.Observables.listeners(m.selection)), find_blocks(fig, Menu))
@test wait_until(loaded)
@test any(ax -> ax.title[] == "test_profile.jld2", find_blocks(fig, Axis))

# data axis = the one with the heatmap
ax = only(filter(ax -> !isempty(find_plots(ax, Heatmap)), find_blocks(fig, Axis)))
@test ax === AGP.ax1
@test length(find_plots(ax, Scatter)) == 2 # picks + seismicity

# one toggle per surface (only Moho, topography is plotted separately) and point data set
# (+ the picking and compare toggles)
@test length(find_blocks(fig, Toggle)) == 2 + 1 + 1

# the field menu contains the volume data, without the helper fields
field_menu = only(filter(m -> :dVs in m.options[], find_blocks(fig, Menu)))
@test field_menu.options[] == [:dVs, :dVp]
end

ax = AGP.ax1
hm = only(find_plots(ax, Heatmap))

@testset "Change field and colormap" begin
field_menu = only(filter(m -> :dVs in m.options[], find_blocks(fig, Menu)))
dVs_max = maximum(filter(!isnan, hm[3][]))
select!(field_menu, :dVp)
@test maximum(filter(!isnan, hm[3][])) ≈ 2 * dVs_max

cmap_menu = only(filter(m -> :roma in m.options[], find_blocks(fig, Menu)))
select!(cmap_menu, :roma)
@test hm.colormap[] == Reverse(:roma)
end

@testset "Save and load picks" begin
textbox = only(filter(t -> t.placeholder[] == "User name", find_blocks(fig, Textbox)))
textbox.stored_string[] = "CI"

# add picks, as done by the mouse callbacks
x_picks = [100.0, 300.0, 500.0]
depth_picks = [-40.0, -80.0, -120.0]
AGP.picks[] = [Point3f(x, d, 1000) for (x, d) in zip(x_picks, depth_picks)]

AGP.SAVE_FILE[] = _ -> picks_file
select!(main_menu, "Save Picks...")
@test wait_until(() -> isfile(picks_file))

saved = wait_until(() -> try
load(picks_file); true
catch
false
end, timeout = 30)
@test saved
data = load(picks_file)
@test data["picks"].x ≈ x_picks
@test data["picks"].depth ≈ depth_picks
# the profile runs along a latitude, so lat is constant and lon increases
@test all(data["picks"].lat .≈ 45.0)
@test issorted(data["picks"].lon) && all(11.0 .<= data["picks"].lon .<= 19.0)
@test data["pick_info"].user_name == "CI"
@test data["profile_info"].start_lonlat == (11.0, 45.0)
@test data["profile_info"].end_lonlat == (19.0, 45.0)

# picking has to keep working after saving
@test AGP.picks isa Observable

# load them back as modifiable picks
AGP.picks[] = Point3f[]
AGP.PICK_FILE[] = _ -> picks_file
select!(main_menu, "Load Picks...")
@test wait_until(() -> length(AGP.picks[]) == 3)
@test [p[1] for p in AGP.picks[]] ≈ x_picks
@test [p[2] for p in AGP.picks[]] ≈ depth_picks

# and as fixed picks for comparison, which adds two lines to the profile
nlines = length(find_plots(ax, Lines))
select!(main_menu, "Load Picks (not modifyable)...")
@test wait_until(() -> length(find_plots(ax, Lines)) == nlines + 2)
end

@testset "Save screenshot" begin
AGP.SAVE_FILE[] = _ -> screen_file
select!(main_menu, "Save Screenshot...")
@test wait_until(() -> isfile(screen_file) && filesize(screen_file) > 0)
end

GLMakie.closeall()
end
Loading