data/hiking_trails.geojson now generates automatically as the last step of every offline.pmtiles build (PmtilesBuilder.extract_hiking_trails, best-effort — a failure here doesn't fail the whole build), merged across all selected countries with cross-border dedup by OSM relation id (HikingTrails.extract_merged). Served at GET /tiles/hiking_trails.geojson and GET /api/hiking-trails, fetched client-side only once the overlay is actually toggled on. hiking_trails.js now prefers this local file whenever it exists, regardless of which base map (online/offline) is active — same "prefer local when available" story as HillshadeControl already uses for terrain. Offline click hit-tests the real vector layer directly (queryRenderedFeatures) instead of querying Overpass — instant, no network. Coordinates are rounded to 5dp to keep the combined file smaller; colour tag values are normalized against a known-safe set before ever reaching a style paint property. Verified extract_merged (new: combines multiple countries' extracts, dedupes overlapping relations) against two real cached country PBFs on brick directly, not just fixtures.
650 lines
26 KiB
Ruby
650 lines
26 KiB
Ruby
require_relative "test_helper"
|
|
require "maptool/pmtiles_builder"
|
|
require "tmpdir"
|
|
require "fileutils"
|
|
|
|
# Network calls (Geofabrik downloads/HEADs, the mapterhorn manifest) are
|
|
# never exercised here — same "keep this offline/deterministic" reasoning
|
|
# test_helper.rb already gives for /api/plugins staying off the real
|
|
# Overpass API — build_offline/build_terrain take download_extract/
|
|
# fetch_manifest precisely so tests can swap those out. Fake `pmtiles`,
|
|
# `osmium`, and `java` binaries (PMTILES_BIN/OSMIUM_BIN/JAVA_BIN) stand in
|
|
# for the real tools, logging each invocation's subcommand/first arg to
|
|
# PMTILES_LOG so tests can assert on what actually got called (e.g. "was
|
|
# merge invoked, and with what").
|
|
#
|
|
# Country codes below are real ones from the vendored
|
|
# lib/maptool/data/europe_countries.geojson (Slovakia, Iceland, Spain) —
|
|
# chosen so their real bboxes fall cleanly inside/outside the fake
|
|
# manifest's chunk extents below, without needing a second fake geometry
|
|
# source.
|
|
class PmtilesBuilderTest < Minitest::Test
|
|
def setup
|
|
@data_dir = Dir.mktmpdir("maptool-pmtiles-test-data")
|
|
@bin_dir = Dir.mktmpdir("maptool-pmtiles-test-bin")
|
|
@log = File.join(@bin_dir, "pmtiles.log")
|
|
|
|
# Output path: $3 for `extract SOURCE OUTPUT --region=... --maxzoom=...`
|
|
# and `convert INPUT OUTPUT` alike; the *last* arg for `merge INPUT_1
|
|
# [INPUT_2 ...] OUTPUT`, since its input count varies.
|
|
@pmtiles_ok = File.join(@bin_dir, "pmtiles-ok")
|
|
File.write(@pmtiles_ok, <<~SH)
|
|
#!/bin/sh
|
|
echo "$1 $2" >> "$PMTILES_LOG"
|
|
case "$1" in
|
|
merge)
|
|
for last; do :; done
|
|
echo fake > "$last"
|
|
;;
|
|
*)
|
|
echo fake > "$3"
|
|
;;
|
|
esac
|
|
SH
|
|
File.chmod(0o755, @pmtiles_ok)
|
|
|
|
@pmtiles_fail = File.join(@bin_dir, "pmtiles-fail")
|
|
File.write(@pmtiles_fail, "#!/bin/sh\necho 'boom' >&2\nexit 1\n")
|
|
File.chmod(0o755, @pmtiles_fail)
|
|
|
|
# Mimics go-pmtiles' real extract failure mode: hundreds of progress
|
|
# lines before the actual error — see command_output's tail-truncation.
|
|
@pmtiles_fail_noisy = File.join(@bin_dir, "pmtiles-fail-noisy")
|
|
File.write(@pmtiles_fail_noisy, <<~SH)
|
|
#!/bin/sh
|
|
i=0
|
|
while [ "$i" -lt 200 ]; do
|
|
echo "fetching $i dirs, $i chunks, 1 requests" >&2
|
|
i=$((i + 1))
|
|
done
|
|
echo "stream error: stream ID 9; INTERNAL_ERROR" >&2
|
|
exit 1
|
|
SH
|
|
File.chmod(0o755, @pmtiles_fail_noisy)
|
|
|
|
# osmium merge PBF1 PBF2 ... -o OUTPUT — output is always "-o"'s next
|
|
# arg, unlike pmtiles' varying-position output above.
|
|
@osmium_ok = File.join(@bin_dir, "osmium-ok")
|
|
File.write(@osmium_ok, <<~SH)
|
|
#!/bin/sh
|
|
echo "osmium $1" >> "$PMTILES_LOG"
|
|
prev=""
|
|
for arg; do
|
|
if [ "$prev" = "-o" ]; then echo fake > "$arg"; fi
|
|
prev="$arg"
|
|
done
|
|
SH
|
|
File.chmod(0o755, @osmium_ok)
|
|
|
|
# Same shape as osmium-ok, but fails on tags-filter specifically (the
|
|
# first subcommand HikingTrails.extract runs) while still succeeding
|
|
# on merge — lets a single-country build (which never calls merge)
|
|
# exercise "hiking-trails extraction failed" without also breaking
|
|
# offline.pmtiles' own osmium use in a multi-country test.
|
|
@osmium_fail_tags_filter = File.join(@bin_dir, "osmium-fail-tags-filter")
|
|
File.write(@osmium_fail_tags_filter, <<~SH)
|
|
#!/bin/sh
|
|
echo "osmium $1" >> "$PMTILES_LOG"
|
|
if [ "$1" = "tags-filter" ]; then
|
|
echo "boom" >&2
|
|
exit 1
|
|
fi
|
|
prev=""
|
|
for arg; do
|
|
if [ "$prev" = "-o" ]; then echo fake > "$arg"; fi
|
|
prev="$arg"
|
|
done
|
|
SH
|
|
File.chmod(0o755, @osmium_fail_tags_filter)
|
|
|
|
# Stands in for `java -jar planetiler.jar --osm-path=... --output=OUT`
|
|
# — PLANETILER_JAR itself is irrelevant here (this fake ignores -jar's
|
|
# argument entirely), only JAVA_BIN needs pointing at it.
|
|
@java_ok = File.join(@bin_dir, "java-ok")
|
|
File.write(@java_ok, <<~SH)
|
|
#!/bin/sh
|
|
echo "planetiler ${3%%=*}" >> "$PMTILES_LOG"
|
|
for arg; do
|
|
case "$arg" in
|
|
--output=*) echo fake > "${arg#--output=}" ;;
|
|
esac
|
|
done
|
|
SH
|
|
File.chmod(0o755, @java_ok)
|
|
|
|
@java_fail = File.join(@bin_dir, "java-fail")
|
|
File.write(@java_fail, "#!/bin/sh\necho 'boom' >&2\nexit 1\n")
|
|
File.chmod(0o755, @java_fail)
|
|
|
|
@original_pmtiles_bin = ENV["PMTILES_BIN"]
|
|
@original_osmium_bin = ENV["OSMIUM_BIN"]
|
|
@original_java_bin = ENV["JAVA_BIN"]
|
|
ENV["PMTILES_LOG"] = @log
|
|
ENV["OSMIUM_BIN"] = @osmium_ok
|
|
ENV["JAVA_BIN"] = @java_ok
|
|
Maptool::PmtilesBuilder.reset!
|
|
end
|
|
|
|
def teardown
|
|
FileUtils.remove_entry(@data_dir)
|
|
FileUtils.remove_entry(@bin_dir)
|
|
ENV["PMTILES_BIN"] = @original_pmtiles_bin
|
|
ENV["OSMIUM_BIN"] = @original_osmium_bin
|
|
ENV["JAVA_BIN"] = @original_java_bin
|
|
ENV.delete("PMTILES_LOG")
|
|
Maptool::PmtilesBuilder.reset!
|
|
end
|
|
|
|
# download_extract stub: writes a marker file (same "fake" content the
|
|
# pmtiles/osmium/java fixtures use) instead of hitting Geofabrik, and
|
|
# returns a fixed Last-Modified so sidecar-meta assertions are stable.
|
|
# `calls` (an Array, shared across builds if the caller wants it) records
|
|
# every URL actually downloaded, for asserting the per-country cache
|
|
# skipped (or didn't skip) a re-download.
|
|
def fake_download(last_modified: "Wed, 10 Sep 2026 08:00:01 GMT", calls: nil)
|
|
->(url, dest) {
|
|
calls << url if calls
|
|
File.write(dest, "fake")
|
|
last_modified
|
|
}
|
|
end
|
|
|
|
def log_lines
|
|
File.exist?(@log) ? File.readlines(@log, chomp: true) : []
|
|
end
|
|
|
|
def select_regions(*codes)
|
|
Maptool::PmtilesBuilder.set_selected_regions(@data_dir, codes)
|
|
end
|
|
|
|
# chunk-a covers Slovakia's real extent, chunk-b covers Iceland's —
|
|
# Spain falls in neither, for the "no chunk intersects" case.
|
|
def fake_manifest(chunk_a_md5: "md5-a")
|
|
[
|
|
{ "name" => "planet.pmtiles", "url" => "https://example.com/planet.pmtiles", "md5sum" => "md5-planet", "min_lon" => -180.0, "min_lat" => -85.0, "max_lon" => 180.0, "max_lat" => 85.0, "min_zoom" => 0, "max_zoom" => 12 },
|
|
{ "name" => "chunk-a.pmtiles", "url" => "https://example.com/chunk-a.pmtiles", "md5sum" => chunk_a_md5, "min_lon" => 16.0, "min_lat" => 47.0, "max_lon" => 23.0, "max_lat" => 50.0, "min_zoom" => 13, "max_zoom" => 15 },
|
|
{ "name" => "chunk-b.pmtiles", "url" => "https://example.com/chunk-b.pmtiles", "md5sum" => "md5-b", "min_lon" => -25.0, "min_lat" => 63.0, "max_lon" => -13.0, "max_lat" => 67.0, "min_zoom" => 13, "max_zoom" => 15 },
|
|
]
|
|
end
|
|
|
|
def test_offline_and_terrain_status_start_idle
|
|
assert_equal({ state: "idle" }, Maptool::PmtilesBuilder.offline_status)
|
|
assert_equal({ state: "idle" }, Maptool::PmtilesBuilder.terrain_status)
|
|
end
|
|
|
|
# --- region selection ---
|
|
|
|
def test_available_regions_includes_known_countries
|
|
codes = Maptool::PmtilesBuilder.available_regions.map { |r| r[:code] }
|
|
assert_includes codes, "SK"
|
|
assert_includes codes, "IS"
|
|
end
|
|
|
|
def test_selected_regions_defaults_to_empty
|
|
assert_empty Maptool::PmtilesBuilder.selected_regions(@data_dir)
|
|
end
|
|
|
|
def test_set_selected_regions_round_trips
|
|
select_regions("SK", "AT")
|
|
assert_equal ["SK", "AT"], Maptool::PmtilesBuilder.selected_regions(@data_dir)
|
|
end
|
|
|
|
def test_set_selected_regions_rejects_an_unknown_code
|
|
assert_raises(Maptool::PmtilesBuilder::BuildError) { select_regions("SK", "XX") }
|
|
end
|
|
|
|
# --- offline.pmtiles ---
|
|
|
|
def test_build_offline_raises_with_no_selection
|
|
assert_raises(Maptool::PmtilesBuilder::BuildError) do
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
|
end
|
|
end
|
|
|
|
def test_build_offline_writes_the_file_and_reports_done
|
|
select_regions("SK")
|
|
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
|
thread.join
|
|
|
|
assert_path_exists File.join(@data_dir, "offline.pmtiles")
|
|
refute_path_exists File.join(@data_dir, "offline.tmp.pmtiles")
|
|
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
|
|
assert_equal ["planetiler --osm-path", "osmium tags-filter", "osmium add-locations-to-ways", "osmium cat"], log_lines
|
|
assert_equal(
|
|
{ "sources" => [{ "code" => "SK", "slug" => "europe/slovakia", "last_modified" => "Wed, 10 Sep 2026 08:00:01 GMT" }] },
|
|
Maptool::PmtilesBuilder.offline_meta(@data_dir),
|
|
)
|
|
end
|
|
|
|
def test_build_offline_also_writes_a_hiking_trails_geojson
|
|
select_regions("SK")
|
|
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
|
thread.join
|
|
|
|
# The fake osmium binary doesn't produce real OPL output, just
|
|
# confirms the file lands and is valid (empty) GeoJSON — the real
|
|
# parsing logic is covered by test_hiking_trails.rb against real OPL
|
|
# fixtures.
|
|
path = File.join(@data_dir, "hiking_trails.geojson")
|
|
assert_path_exists path
|
|
assert_equal({ "type" => "FeatureCollection", "features" => [] }, JSON.parse(File.read(path)))
|
|
end
|
|
|
|
# The actual reasoning: hiking trails are a bonus overlay bolted onto the
|
|
# existing offline.pmtiles build, not the core map — an osmium hiccup on
|
|
# this one step shouldn't take down an otherwise-successful build.
|
|
def test_build_offline_still_succeeds_when_hiking_trails_extraction_fails
|
|
select_regions("SK")
|
|
ENV["OSMIUM_BIN"] = @osmium_fail_tags_filter
|
|
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
|
thread.join
|
|
|
|
assert_path_exists File.join(@data_dir, "offline.pmtiles")
|
|
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
|
|
refute_path_exists File.join(@data_dir, "hiking_trails.geojson")
|
|
refute_path_exists "#{File.join(@data_dir, "hiking_trails.geojson")}.tmp"
|
|
end
|
|
|
|
# Two countries selected — osmium merge should run before planetiler,
|
|
# each downloaded source recorded in the sidecar meta.
|
|
def test_build_offline_merges_multiple_selected_countries
|
|
select_regions("SK", "AT")
|
|
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
|
thread.join
|
|
|
|
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
|
|
assert_equal(
|
|
[
|
|
"osmium merge", "planetiler --osm-path",
|
|
"osmium tags-filter", "osmium add-locations-to-ways", "osmium cat", # SK's own hiking-trails extraction
|
|
"osmium tags-filter", "osmium add-locations-to-ways", "osmium cat", # AT's own
|
|
],
|
|
log_lines,
|
|
)
|
|
codes = Maptool::PmtilesBuilder.offline_meta(@data_dir)["sources"].map { |s| s["code"] }
|
|
assert_equal %w[SK AT], codes
|
|
end
|
|
|
|
# The actual bug report this covers: hitting "Apply" after tweaking the
|
|
# region selection used to re-download every selected country's .osm.pbf
|
|
# from scratch, even ones already fetched by an earlier build.
|
|
def test_build_offline_skips_redownloading_an_unchanged_cached_country
|
|
select_regions("SK")
|
|
calls = []
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download(calls: calls)).join
|
|
assert_equal 1, calls.size
|
|
|
|
Maptool::PmtilesBuilder.build_offline(
|
|
data_dir: @data_dir,
|
|
download_extract: fake_download(calls: calls),
|
|
fetch_last_modified: ->(_url) { "Wed, 10 Sep 2026 08:00:01 GMT" },
|
|
).join
|
|
|
|
assert_equal 1, calls.size
|
|
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
|
|
end
|
|
|
|
def test_build_offline_redownloads_a_country_whose_extract_changed
|
|
select_regions("SK")
|
|
calls = []
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download(calls: calls)).join
|
|
|
|
Maptool::PmtilesBuilder.build_offline(
|
|
data_dir: @data_dir,
|
|
download_extract: fake_download(calls: calls, last_modified: "Thu, 11 Sep 2026 08:00:01 GMT"),
|
|
fetch_last_modified: ->(_url) { "Thu, 11 Sep 2026 08:00:01 GMT" },
|
|
).join
|
|
|
|
assert_equal 2, calls.size
|
|
end
|
|
|
|
# A cache-freshness check that itself fails (Geofabrik unreachable) should
|
|
# fall back to re-downloading rather than aborting the build.
|
|
def test_build_offline_redownloads_when_the_cache_check_fails
|
|
select_regions("SK")
|
|
calls = []
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download(calls: calls)).join
|
|
|
|
fetch_last_modified = ->(_url) { raise Maptool::PmtilesBuilder::BuildError, "could not reach geofabrik" }
|
|
Maptool::PmtilesBuilder.build_offline(
|
|
data_dir: @data_dir,
|
|
download_extract: fake_download(calls: calls),
|
|
fetch_last_modified: fetch_last_modified,
|
|
).join
|
|
|
|
assert_equal 2, calls.size
|
|
assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state]
|
|
end
|
|
|
|
def test_build_offline_only_downloads_a_newly_added_country
|
|
select_regions("SK")
|
|
calls = []
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download(calls: calls)).join
|
|
|
|
select_regions("SK", "AT")
|
|
Maptool::PmtilesBuilder.build_offline(
|
|
data_dir: @data_dir,
|
|
download_extract: fake_download(calls: calls),
|
|
fetch_last_modified: ->(_url) { "Wed, 10 Sep 2026 08:00:01 GMT" },
|
|
).join
|
|
|
|
assert_equal 1, calls.count { |url| url.include?("slovakia") }
|
|
assert_equal 1, calls.count { |url| url.include?("austria") }
|
|
end
|
|
|
|
# offline_status[:step] should say which country is in flight right now
|
|
# and which of the current build's selection are already done/still
|
|
# pending — not just "running since X".
|
|
def test_build_offline_reports_progress_step_while_running
|
|
select_regions("SK", "AT")
|
|
started = Queue.new
|
|
gate = Queue.new
|
|
download_extract = ->(url, dest) {
|
|
started.push(url)
|
|
gate.pop
|
|
File.write(dest, "fake")
|
|
"Wed, 10 Sep 2026 08:00:01 GMT"
|
|
}
|
|
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: download_extract)
|
|
|
|
started.pop
|
|
assert_equal "fetching SK (1/2) — pending: AT", Maptool::PmtilesBuilder.offline_status[:step]
|
|
gate.push(:go)
|
|
|
|
started.pop
|
|
assert_equal "fetching AT (2/2) — done: SK", Maptool::PmtilesBuilder.offline_status[:step]
|
|
gate.push(:go)
|
|
|
|
thread.join
|
|
assert_nil Maptool::PmtilesBuilder.offline_status[:step]
|
|
end
|
|
|
|
def test_delete_offline_data_removes_the_file_meta_and_cache
|
|
select_regions("SK")
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
|
assert_path_exists File.join(@data_dir, "offline.pmtiles")
|
|
assert_path_exists File.join(@data_dir, "geofabrik_cache", "SK.osm.pbf")
|
|
|
|
Maptool::PmtilesBuilder.delete_offline_data(@data_dir)
|
|
|
|
refute_path_exists File.join(@data_dir, "offline.pmtiles")
|
|
refute_path_exists File.join(@data_dir, "geofabrik_cache")
|
|
assert_nil Maptool::PmtilesBuilder.offline_meta(@data_dir)
|
|
end
|
|
|
|
def test_delete_offline_data_is_a_noop_when_nothing_was_built
|
|
Maptool::PmtilesBuilder.delete_offline_data(@data_dir) # should not raise
|
|
end
|
|
|
|
def test_delete_offline_data_raises_while_a_build_is_running
|
|
select_regions("SK")
|
|
gate = Queue.new
|
|
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: ->(_url, _dest) { gate.pop })
|
|
|
|
assert_raises(Maptool::PmtilesBuilder::BuildError) { Maptool::PmtilesBuilder.delete_offline_data(@data_dir) }
|
|
ensure
|
|
gate.push("unused")
|
|
thread&.join
|
|
end
|
|
|
|
def test_delete_cached_extract_removes_only_that_country
|
|
select_regions("SK", "AT")
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
|
|
|
Maptool::PmtilesBuilder.delete_cached_extract(@data_dir, "SK")
|
|
|
|
refute_path_exists File.join(@data_dir, "geofabrik_cache", "SK.osm.pbf")
|
|
assert_path_exists File.join(@data_dir, "geofabrik_cache", "AT.osm.pbf")
|
|
end
|
|
|
|
def test_delete_terrain_data_removes_the_file_and_meta
|
|
select_regions("ES")
|
|
ENV["PMTILES_BIN"] = @pmtiles_ok
|
|
thread = Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest })
|
|
thread.join
|
|
assert_path_exists File.join(@data_dir, "terrain.pmtiles")
|
|
|
|
Maptool::PmtilesBuilder.delete_terrain_data(@data_dir)
|
|
|
|
refute_path_exists File.join(@data_dir, "terrain.pmtiles")
|
|
assert_nil Maptool::PmtilesBuilder.terrain_meta(@data_dir)
|
|
end
|
|
|
|
def test_cached_extracts_is_empty_before_any_download
|
|
assert_empty Maptool::PmtilesBuilder.cached_extracts(@data_dir)
|
|
end
|
|
|
|
# Deliberately still selected — cached_extracts reflects the cache on
|
|
# disk, not the current selection or the last build's own meta (see
|
|
# cached_extracts' own comment for why those three can all disagree).
|
|
def test_cached_extracts_lists_what_was_downloaded
|
|
select_regions("SK", "AT")
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
|
|
|
extracts = Maptool::PmtilesBuilder.cached_extracts(@data_dir)
|
|
assert_equal %w[AT SK], extracts.map { |e| e["code"] }
|
|
assert_equal [4, 4], extracts.map { |e| e["size"] } # "fake".bytesize
|
|
assert_equal ["Wed, 10 Sep 2026 08:00:01 GMT"] * 2, extracts.map { |e| e["last_modified"] }
|
|
end
|
|
|
|
def test_offline_meta_is_nil_before_any_build
|
|
assert_nil Maptool::PmtilesBuilder.offline_meta(@data_dir)
|
|
end
|
|
|
|
def test_offline_update_available_is_nil_before_any_build
|
|
assert_nil Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: ->(_url) { "unused" })
|
|
end
|
|
|
|
def test_offline_update_available_is_false_when_unchanged
|
|
select_regions("SK")
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
|
|
|
refute Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: ->(_url) { "Wed, 10 Sep 2026 08:00:01 GMT" })
|
|
end
|
|
|
|
def test_offline_update_available_is_true_when_a_newer_extract_exists
|
|
select_regions("SK")
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
|
|
|
assert Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: ->(_url) { "Thu, 11 Sep 2026 08:00:01 GMT" })
|
|
end
|
|
|
|
# Same recorded source, but the selection has since grown — recorded
|
|
# sources no longer match what the current selection would need.
|
|
def test_offline_update_available_is_true_when_the_selection_grew
|
|
select_regions("SK")
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
|
|
|
select_regions("SK", "AT")
|
|
assert Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: ->(_url) { "Wed, 10 Sep 2026 08:00:01 GMT" })
|
|
end
|
|
|
|
def test_offline_update_available_is_nil_when_geofabrik_is_unreachable
|
|
select_regions("SK")
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download).join
|
|
|
|
fetch_last_modified = ->(_url) { raise Maptool::PmtilesBuilder::BuildError, "could not reach geofabrik" }
|
|
assert_nil Maptool::PmtilesBuilder.offline_update_available?(@data_dir, fetch_last_modified: fetch_last_modified)
|
|
end
|
|
|
|
def test_build_offline_reports_an_error_status_on_non_zero_exit
|
|
select_regions("SK")
|
|
ENV["JAVA_BIN"] = @java_fail
|
|
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
|
thread.join
|
|
|
|
refute_path_exists File.join(@data_dir, "offline.pmtiles")
|
|
status = Maptool::PmtilesBuilder.offline_status
|
|
assert_equal "error", status[:state]
|
|
assert_match(/exited with status/, status[:message])
|
|
end
|
|
|
|
def test_build_offline_reports_a_clear_error_when_planetiler_is_missing
|
|
select_regions("SK")
|
|
ENV["JAVA_BIN"] = File.join(@bin_dir, "does-not-exist")
|
|
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
|
thread.join
|
|
|
|
status = Maptool::PmtilesBuilder.offline_status
|
|
assert_equal "error", status[:state]
|
|
assert_match(/Java and planetiler\.jar installed/, status[:message])
|
|
end
|
|
|
|
# download_extract blocks on a Queue so the build is deterministically
|
|
# still "running" when the second call happens, rather than racing a real
|
|
# one to see which state wins.
|
|
def test_build_offline_raises_when_one_is_already_running
|
|
select_regions("SK")
|
|
gate = Queue.new
|
|
thread = Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: ->(_url, _dest) { gate.pop })
|
|
|
|
assert_raises(Maptool::PmtilesBuilder::BuildError) do
|
|
Maptool::PmtilesBuilder.build_offline(data_dir: @data_dir, download_extract: fake_download)
|
|
end
|
|
ensure
|
|
gate.push("Wed, 10 Sep 2026 08:00:01 GMT")
|
|
thread&.join
|
|
end
|
|
|
|
# --- terrain.pmtiles ---
|
|
|
|
def test_build_terrain_raises_with_no_selection
|
|
assert_raises(Maptool::PmtilesBuilder::BuildError) do
|
|
Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest })
|
|
end
|
|
end
|
|
|
|
# Spain intersects neither fake chunk — only planet.pmtiles matches, so
|
|
# this should be a single extract straight to the final path, no merge.
|
|
def test_build_terrain_extracts_only_planet_when_no_chunk_intersects
|
|
select_regions("ES")
|
|
ENV["PMTILES_BIN"] = @pmtiles_ok
|
|
thread = Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest })
|
|
thread.join
|
|
|
|
assert_path_exists File.join(@data_dir, "terrain.pmtiles")
|
|
assert_equal "done", Maptool::PmtilesBuilder.terrain_status[:state]
|
|
assert_equal ["extract https://example.com/planet.pmtiles"], log_lines
|
|
end
|
|
|
|
# Slovakia intersects chunk-a only — planet.pmtiles (always) plus that
|
|
# one chunk get extracted, then merged.
|
|
def test_build_terrain_extracts_and_merges_a_single_intersecting_chunk
|
|
select_regions("SK")
|
|
ENV["PMTILES_BIN"] = @pmtiles_ok
|
|
thread = Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest })
|
|
thread.join
|
|
|
|
assert_path_exists File.join(@data_dir, "terrain.pmtiles")
|
|
assert_equal "done", Maptool::PmtilesBuilder.terrain_status[:state]
|
|
extracted = log_lines.select { |line| line.start_with?("extract ") }
|
|
merged = log_lines.select { |line| line.start_with?("merge ") }
|
|
assert_equal ["extract https://example.com/planet.pmtiles", "extract https://example.com/chunk-a.pmtiles"], extracted
|
|
assert_equal 1, merged.size
|
|
assert_equal(
|
|
{ "sources" => [{ "name" => "planet.pmtiles", "md5sum" => "md5-planet" }, { "name" => "chunk-a.pmtiles", "md5sum" => "md5-a" }] },
|
|
Maptool::PmtilesBuilder.terrain_meta(@data_dir),
|
|
)
|
|
end
|
|
|
|
def test_terrain_meta_is_nil_before_any_build
|
|
assert_nil Maptool::PmtilesBuilder.terrain_meta(@data_dir)
|
|
end
|
|
|
|
def test_terrain_update_available_is_false_when_unchanged
|
|
select_regions("SK")
|
|
ENV["PMTILES_BIN"] = @pmtiles_ok
|
|
Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest }).join
|
|
|
|
refute Maptool::PmtilesBuilder.terrain_update_available?(@data_dir, fetch_manifest: -> { fake_manifest })
|
|
end
|
|
|
|
# Same selection, but chunk-a's md5sum has since changed upstream.
|
|
def test_terrain_update_available_is_true_when_a_source_changed
|
|
select_regions("SK")
|
|
ENV["PMTILES_BIN"] = @pmtiles_ok
|
|
Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest }).join
|
|
|
|
assert Maptool::PmtilesBuilder.terrain_update_available?(@data_dir, fetch_manifest: -> { fake_manifest(chunk_a_md5: "md5-a-v2") })
|
|
end
|
|
|
|
# Same recorded sources, but the selection has since grown to also cover
|
|
# chunk-b — recorded sources no longer match what the current selection
|
|
# would need.
|
|
def test_terrain_update_available_is_true_when_the_selection_grew
|
|
select_regions("SK")
|
|
ENV["PMTILES_BIN"] = @pmtiles_ok
|
|
Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest }).join
|
|
|
|
select_regions("SK", "IS")
|
|
assert Maptool::PmtilesBuilder.terrain_update_available?(@data_dir, fetch_manifest: -> { fake_manifest })
|
|
end
|
|
|
|
# Slovakia + Iceland together intersect both fake chunks — planet + both
|
|
# chunks, one 3-way merge.
|
|
def test_build_terrain_merges_multiple_intersecting_chunks
|
|
select_regions("SK", "IS")
|
|
ENV["PMTILES_BIN"] = @pmtiles_ok
|
|
thread = Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest })
|
|
thread.join
|
|
|
|
assert_equal "done", Maptool::PmtilesBuilder.terrain_status[:state]
|
|
extracted = log_lines.count { |line| line.start_with?("extract ") }
|
|
assert_equal 3, extracted # planet + chunk-a + chunk-b
|
|
assert_equal 1, log_lines.count { |line| line.start_with?("merge ") }
|
|
end
|
|
|
|
def test_build_terrain_reports_a_clear_error_when_the_manifest_is_missing_planet
|
|
select_regions("ES")
|
|
ENV["PMTILES_BIN"] = @pmtiles_ok
|
|
thread = Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest.reject { |i| i["name"] == "planet.pmtiles" } })
|
|
thread.join
|
|
|
|
status = Maptool::PmtilesBuilder.terrain_status
|
|
assert_equal "error", status[:state]
|
|
assert_match(/missing planet\.pmtiles/, status[:message])
|
|
end
|
|
|
|
def test_build_terrain_reports_an_error_status_on_non_zero_exit
|
|
select_regions("ES")
|
|
ENV["PMTILES_BIN"] = @pmtiles_fail
|
|
thread = Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest })
|
|
thread.join
|
|
|
|
refute_path_exists File.join(@data_dir, "terrain.pmtiles")
|
|
status = Maptool::PmtilesBuilder.terrain_status
|
|
assert_equal "error", status[:state]
|
|
assert_match(/exited with status/, status[:message])
|
|
end
|
|
|
|
# The real trigger for this: a Mapterhorn extract that failed after ~7000
|
|
# progress lines, producing a 500k+ character status[:message] that was
|
|
# unreadable in the web UI.
|
|
def test_build_terrain_truncates_a_noisy_command_failure
|
|
select_regions("ES")
|
|
ENV["PMTILES_BIN"] = @pmtiles_fail_noisy
|
|
thread = Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { fake_manifest })
|
|
thread.join
|
|
|
|
status = Maptool::PmtilesBuilder.terrain_status
|
|
assert_equal "error", status[:state]
|
|
assert_match(/\(\d+ earlier lines omitted\)/, status[:message])
|
|
assert_match(/stream error: stream ID 9/, status[:message])
|
|
assert_operator status[:message].length, :<, 5000
|
|
end
|
|
|
|
def test_build_terrain_raises_when_one_is_already_running
|
|
select_regions("ES")
|
|
ENV["PMTILES_BIN"] = @pmtiles_ok
|
|
gate = Queue.new
|
|
thread = Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { gate.pop })
|
|
|
|
assert_raises(Maptool::PmtilesBuilder::BuildError) do
|
|
Maptool::PmtilesBuilder.build_terrain(data_dir: @data_dir, fetch_manifest: -> { "unused" })
|
|
end
|
|
ensure
|
|
gate.push(fake_manifest)
|
|
thread&.join
|
|
end
|
|
end
|