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
28 changes: 28 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
run_tests:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.allow_failure }}
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -47,6 +48,18 @@ jobs:
arch: aarch64
version: '1.12'
allow_failure: true
# Julia 1.13 is the current stable release; test it explicitly on Linux
# so a regression there is visible independently of the '1' alias.
- os: ubuntu-latest
arch: x64
version: '1.13'
allow_failure: false
# Track the upcoming release so breakage is caught early, without
# blocking CI when an unreleased Julia is temporarily broken.
- os: ubuntu-latest
arch: x64
version: 'pre'
allow_failure: true
steps:
- uses: actions/checkout@v7
- uses: julia-actions/setup-julia@v3
Expand All @@ -57,7 +70,22 @@ jobs:
with:
include-matrix: false
- uses: julia-actions/julia-buildpkg@latest
# Cache the GMT topography tiles that the tests download (see
# test/topo_prefetch.jl), so PR runs do not depend on the GMT data
# server. The scheduled run skips the cache on purpose, so that the
# download itself is still tested regularly.
- uses: actions/cache/restore@v4
id: gmt-cache
if: github.event_name != 'schedule'
with:
path: ~/.gmt
key: gmt-topo-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('test/topo_prefetch.jl') }}
- uses: julia-actions/julia-runtest@latest
- uses: actions/cache/save@v4
if: always() && github.event_name != 'schedule' && steps.gmt-cache.outputs.cache-hit != 'true'
with:
path: ~/.gmt
key: gmt-topo-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('test/topo_prefetch.jl') }}
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v7
with:
Expand Down
66 changes: 57 additions & 9 deletions ext/GMT_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ using GeophysicalModelGenerator: lonlatdepth_grid, GeoData, UTMData, km, remove_

println("Loading GMT routines within GMG")

# Official GMT data server mirrors, see https://www.generic-mapping-tools.org/mirrors.
# `import_topo` cycles through these if a download fails, so that a single
# mirror being offline does not break the download (or the tests that check it).
# Ordered by measured response; "singapore" is omitted as it was unreachable.
const GMT_DATA_SERVERS = ["oceania", "australia", "sdsc-opentopography", "noaa", "brasil", "china"]


"""
Topo = import_topo(limits; file::String="@earth_relief_01m", maxattempts=5)
Expand Down Expand Up @@ -75,20 +81,62 @@ function import_topo(limits; file::String = "@earth_relief_01m", maxattempts = 5
limits[1:2] = sort(limits[1:2])
end

# Download topo file - add a few attempts to do so
# Download topo file. If the GMT data server is unreachable, GMT stops
# auto-downloading and reports "Remote download is currently deactivated".
#
# First try the server the user/GMT is already configured with, so that the
# normal case does not touch any global state. Only if that fails do we fall
# back to the official mirrors (https://www.generic-mapping-tools.org/mirrors),
# so a single server being offline does not fail the download. The download
# itself is still exercised -- only *which* mirror serves it changes.
#
# Note: `gmtset` changes GMT's *global* configuration (it writes a gmt.conf in
# the working directory), so switching mirrors is visible to anything else
# using GMT concurrently. We therefore restore the original setting afterwards
# and only switch when a download has actually failed.
local G
attempt = 0
while attempt < maxattempts
local last_err = nothing

# `gmtset` writes/updates a gmt.conf in the working directory. Remember whether
# one already existed so we can leave the directory as we found it.
conf_file = joinpath(pwd(), "gmt.conf")
had_conf = isfile(conf_file)
conf_backup = had_conf ? read(conf_file) : nothing

try
for attempt in 1:maxattempts
if attempt > 1
# Previous attempt failed -> try the next mirror in the list
server = GMT_DATA_SERVERS[mod1(attempt - 1, length(GMT_DATA_SERVERS))]
gmtset(GMT_DATA_SERVER = server)
@info "Retrying GMT topography download from mirror \"$server\""
end
try
G = gmtread(file, limits = limits, grid = true)
break
catch e
last_err = e
@warn "Failed downloading GMT topography ($file) on attempt $attempt/$maxattempts"
if attempt < maxattempts
sleep(5) # brief pause before trying the next mirror
end
end
end
finally
# Restore the original configuration, so our mirror choice does not leak
# into other code (or other tests) that use GMT afterwards.
try
G = gmtread(file, limits = limits, grid = true)
break
if had_conf
conf_backup !== nothing && write(conf_file, conf_backup)
elseif isfile(conf_file)
rm(conf_file, force = true)
end
catch
@warn "Failed downloading GMT topography on attempt $attempt/$maxattempts"
sleep(5) # wait a few sec
end
attempt += 1
end
(@isdefined G) || error("Could not download GMT topography data")
if !(@isdefined G)
error("Could not download GMT topography \"$file\" after $maxattempts attempts (mirrors tried: $(GMT_DATA_SERVERS)). Last error: $last_err")
end

# Transfer to GeoData
nx, ny = size(G.z, 2), size(G.z, 1)
Expand Down
17 changes: 17 additions & 0 deletions ext/Gmsh_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Gmsh_utils

using GridapGmsh, StaticArrays
using GridapGmsh: gmsh

import GeophysicalModelGenerator: import_Gmsh, FEData, CartData

Expand All @@ -14,6 +15,22 @@ Reads a Gmsh file and returns a `FEData` object with info about the mesh. `tag_n
"""
function import_Gmsh(fname::String)

# `GmshDiscreteModel` initializes and finalizes gmsh itself. That normally
# works, but on a freshly started worker process gmsh's global state is
# occasionally not in place yet and it fails with "Gmsh has not been
# initialized" (seen on Windows under the parallel test runner).
#
# Initializing here first makes that state explicit. gmsh reference-counts
# nothing, so we must NOT finalize afterwards: GmshDiscreteModel already
# finalizes the session it used, and a second finalize errors.
try
if gmsh.isInitialized() == 0
gmsh.initialize()
end
catch e
@warn "Could not initialize gmsh, letting GmshDiscreteModel handle it" exception = e
end

mesh = GmshDiscreteModel(fname, renumber = false, has_affine_map = false)

# Extract vertices
Expand Down
20 changes: 13 additions & 7 deletions src/IO.jl
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,25 @@ function download_data(url::String, local_filename = "temp.dat"; dir = pwd(), ma

#download remote file to a local temporary directory
file_ext = []
attempt = 0
while attempt < maxattempts
for attempt in 1:maxattempts
try
file_ext = Downloads.download(url, joinpath(dir, local_filename))
# A truncated transfer can still return a path, so only accept a
# download that actually produced a non-empty file.
if filesize(file_ext) == 0
error("downloaded file is empty")
end
break
catch
@warn "Failed downloading data on attempt $attempt/$maxattempts"
sleep(5) # wait a few sec
catch e
file_ext = []
@warn "Failed downloading $url on attempt $attempt/$maxattempts: $e"
if attempt < maxattempts
sleep(5 * attempt) # back off progressively
end
end
attempt += 1
end
if isempty(file_ext)
error("Could not download GMT topography data")
error("Could not download data from $url after $maxattempts attempts")
end

return file_ext
Expand Down
7 changes: 7 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
using GeophysicalModelGenerator
using ParallelTestRunner

# Download the topography tiles the tests need *before* spawning the parallel
# workers, so they are served from GMT's cache instead of being downloaded
# concurrently (which times out regularly in CI). See topo_prefetch.jl.
include("topo_prefetch.jl")

testsuite = find_tests(@__DIR__)
# Not a test file, only a helper
delete!(testsuite, "topo_prefetch")

# Add `using GeophysicalModelGenerator` to each test
for (name, expr) in testsuite
Expand Down
20 changes: 15 additions & 5 deletions test/test_paraview_collection.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
using Test
using GeophysicalModelGenerator, WriteVTK

# Check the structure of a `*.pvd` collection rather than its exact byte size:
# the file embeds absolute paths and OS-dependent separators, so `filesize` varies
# between machines/platforms and is not a portable assertion.
function check_pvd(fname, n_datasets)
isfile(fname) || return false
str = read(fname, String)
occursin("type=\"Collection\"", str) || return false
return count(_ -> true, eachmatch(r"<DataSet\b", str)) == n_datasets
end

@testset "Paraview collection" begin

x, y, z = 0:10, 1:6, 2:0.1:3
Expand All @@ -20,26 +31,25 @@ using GeophysicalModelGenerator, WriteVTK

make_paraview_collection(; dir = "./test_files", pvd_name = "test", file_extension = ".vti")
@test isfile("test.pvd")
@test filesize("test.pvd") == 317
@test check_pvd("test.pvd", 2)

make_paraview_collection(; dir = "./test_files", file_extension = ".vti")
@test isfile("full_simulation.pvd")
@test filesize("full_simulation.pvd") == 317
@test check_pvd("full_simulation.pvd", 2)

make_paraview_collection(; dir = "./test_files")
@test isfile("full_simulation.pvd")
#@test filesize("full_simulation.pvd") == 251


files = ["test_files/test_vti_1.vti", "test_files/test_vti_2.vti"]
time = ["1.0", "2.0"]
make_paraview_collection("test2", files, time)
@test isfile("test2.pvd")
@test filesize("test2.pvd") == 317
@test check_pvd("test2.pvd", 2)

make_paraview_collection(; pvd_name = "test3", files = files, time = time)
@test isfile("test3.pvd")
@test filesize("test3.pvd") == 317
@test check_pvd("test3.pvd", 2)

rm("test.pvd")
rm("full_simulation.pvd")
Expand Down
46 changes: 46 additions & 0 deletions test/topo_prefetch.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Warm the GMT topography cache before the parallel test workers start.
#
# Several tests (test_GMT, test_WaterFlow, the LaPalma tutorial) download
# topography tiles through GMT. Under ParallelTestRunner they run concurrently,
# so they all hit the GMT data server at the same time, and the transfers then
# regularly time out in CI ("Libcurl Error: Timeout was reached").
#
# Downloading the tiles here, one at a time and with generous retries, means the
# workers later find them in GMT's cache (~/.gmt/server) and never touch the
# network. The download itself is still exercised: it happens right here, and
# on CI the cache is bypassed for the scheduled run so it is tested regularly.
#
# NOTE: .github/workflows/CI.yml caches ~/.gmt keyed on the hash of this file,
# so changing the list below invalidates that cache automatically.

using GeophysicalModelGenerator, GMT

# (region, file) pairs, matching the calls made in the tests/tutorials
const TOPO_PREFETCH = [
([8.0, 9.0, 50.0, 51.0], "@earth_relief_01m"), # test_GMT
([6.5, 7.3, 50.2, 50.6], "@earth_relief_03s"), # test_WaterFlow
([-18.2, -17.5, 28.4, 29.0], "@earth_relief_15s.grd"), # LaPalma tutorial
]

function prefetch_topography()
gmt_cache = joinpath(get(ENV, "GMT_USERDIR", joinpath(homedir(), ".gmt")), "server")
n_before = isdir(gmt_cache) ? count(f -> endswith(f, ".nc"), collect(Iterators.flatten(map(t -> t[3], walkdir(gmt_cache))))) : 0

for (limits, file) in TOPO_PREFETCH
t = @elapsed try
# copy: import_topo may modify `limits` in place (negative longitudes)
import_topo(copy(limits); file = file, maxattempts = 8)
catch e
# Do not abort the test run: the test that needs this tile will try
# again itself and report a proper error if the server really is down.
@warn "Could not prefetch topography $file for $limits; the test will retry" exception = (e, catch_backtrace())
end
@info "Prefetched topography $file for $limits in $(round(t, digits = 1)) s"
end

n_after = isdir(gmt_cache) ? count(f -> endswith(f, ".nc"), collect(Iterators.flatten(map(t -> t[3], walkdir(gmt_cache))))) : 0
@info "GMT topography cache: $n_after tiles in $gmt_cache ($(n_after - n_before) newly downloaded)"
return nothing
end

prefetch_topography()
Loading