require_relative "test_helper" require "maptool/pmtiles_builder" require "tmpdir" require "fileutils" # Network calls (the protomaps build index, 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 fetch_build_url/ # fetch_manifest precisely so tests can swap those out. A fake `pmtiles` # CLI (PMTILES_BIN) stands in for the real go-pmtiles binary, 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"). 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 --bbox=... --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) @original_pmtiles_bin = ENV["PMTILES_BIN"] ENV["PMTILES_LOG"] = @log Maptool::PmtilesBuilder.reset! end def teardown FileUtils.remove_entry(@data_dir) FileUtils.remove_entry(@bin_dir) ENV["PMTILES_BIN"] = @original_pmtiles_bin ENV.delete("PMTILES_LOG") Maptool::PmtilesBuilder.reset! end def log_lines File.exist?(@log) ? File.readlines(@log, chomp: true) : [] end # planet.pmtiles always covers the whole world (z0-12); chunk-a/chunk-b # are disjoint small regions (z13+), matching Mapterhorn's real shape # (see download_urls.json) closely enough to exercise bbox_intersects? # and the extract-then-maybe-merge logic without a real manifest fetch. def fake_manifest [ { "name" => "planet.pmtiles", "url" => "https://example.com/planet.pmtiles", "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", "min_lon" => 17.0, "min_lat" => 48.0, "max_lon" => 17.5, "max_lat" => 48.5, "min_zoom" => 13, "max_zoom" => 15 }, { "name" => "chunk-b.pmtiles", "url" => "https://example.com/chunk-b.pmtiles", "min_lon" => 20.0, "min_lat" => 50.0, "max_lon" => 20.5, "max_lat" => 50.5, "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 def test_build_offline_writes_the_file_and_reports_done ENV["PMTILES_BIN"] = @pmtiles_ok thread = Maptool::PmtilesBuilder.build_offline( [1, 2, 3, 4], data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" }, ) thread.join assert_path_exists File.join(@data_dir, "offline.pmtiles") refute_path_exists File.join(@data_dir, "offline.pmtiles.tmp") assert_equal "done", Maptool::PmtilesBuilder.offline_status[:state] end def test_build_offline_reports_an_error_status_on_non_zero_exit ENV["PMTILES_BIN"] = @pmtiles_fail thread = Maptool::PmtilesBuilder.build_offline( [1, 2, 3, 4], data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" }, ) 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_the_binary_is_missing ENV["PMTILES_BIN"] = File.join(@bin_dir, "does-not-exist") thread = Maptool::PmtilesBuilder.build_offline( [1, 2, 3, 4], data_dir: @data_dir, fetch_build_url: -> { "https://example.com/fake.pmtiles" }, ) thread.join status = Maptool::PmtilesBuilder.offline_status assert_equal "error", status[:state] assert_match(/go-pmtiles installed/, status[:message]) end # fetch_build_url 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 ENV["PMTILES_BIN"] = @pmtiles_ok gate = Queue.new thread = Maptool::PmtilesBuilder.build_offline([1, 2, 3, 4], data_dir: @data_dir, fetch_build_url: -> { gate.pop }) assert_raises(Maptool::PmtilesBuilder::BuildError) do Maptool::PmtilesBuilder.build_offline([1, 2, 3, 4], data_dir: @data_dir, fetch_build_url: -> { "unused" }) end ensure gate.push("https://example.com/fake.pmtiles") thread&.join end # A bbox nowhere near chunk-a/chunk-b — 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 ENV["PMTILES_BIN"] = @pmtiles_ok thread = Maptool::PmtilesBuilder.build_terrain([0, 0, 1, 1], 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 # A bbox inside chunk-a's extent — planet.pmtiles (always) plus that one # chunk get extracted, then merged. def test_build_terrain_extracts_and_merges_a_single_intersecting_chunk ENV["PMTILES_BIN"] = @pmtiles_ok thread = Maptool::PmtilesBuilder.build_terrain([48.1, 17.1, 48.2, 17.2], 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 end # A bbox spanning both chunk-a and chunk-b — planet + both chunks, one # 3-way merge. def test_build_terrain_merges_multiple_intersecting_chunks ENV["PMTILES_BIN"] = @pmtiles_ok thread = Maptool::PmtilesBuilder.build_terrain([47.9, 16.9, 50.6, 20.6], 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 ENV["PMTILES_BIN"] = @pmtiles_ok thread = Maptool::PmtilesBuilder.build_terrain([0, 0, 1, 1], 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 ENV["PMTILES_BIN"] = @pmtiles_fail thread = Maptool::PmtilesBuilder.build_terrain([0, 0, 1, 1], 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 def test_build_terrain_raises_when_one_is_already_running ENV["PMTILES_BIN"] = @pmtiles_ok gate = Queue.new thread = Maptool::PmtilesBuilder.build_terrain([0, 0, 1, 1], data_dir: @data_dir, fetch_manifest: -> { gate.pop }) assert_raises(Maptool::PmtilesBuilder::BuildError) do Maptool::PmtilesBuilder.build_terrain([0, 0, 1, 1], data_dir: @data_dir, fetch_manifest: -> { "unused" }) end ensure gate.push(fake_manifest) thread&.join end end