Files
maptool/test/test_helper.rb
T
randy cd56ebe0a1
CI / check (push) Successful in 20s
CI / deploy (push) Successful in 1m52s
Add in-app regeneration of offline.pmtiles and terrain.pmtiles
A logged-in user can now trigger, from a new "Offline data" sidebar
panel, what was previously a fully manual go-pmtiles workflow
(docs/development.adoc): regenerate either file for the current map
viewport, no area-picker (same "visible area is the area" convention
the Overpass POI plugin already uses).

- lib/maptool/pmtiles_builder.rb: two independent background jobs
  (Thread + Mutex-guarded status Hash, same shape as app.rb's own GNSS
  poller), each writing to a .tmp path and atomically renaming into
  place so a build never serves a partially-written file mid-run.
  offline.pmtiles is a single `pmtiles extract` against Protomaps'
  public daily basemap build (byte-range extraction, not a full
  ~120GB download). terrain.pmtiles has no equivalent public
  Terrarium-encoded archive to extract from, so it fetches individual
  Terrarium tiles itself, packs them into an intermediate MBTiles
  (new sqlite3 gem), then the same `pmtiles convert`.
- 4 new app.rb routes (POST .../build, GET .../build-status) plus
  size/mtime added to the existing availability endpoints.
- public/js/offline_data.js: new sidebar panel, polling build-status
  the same self-rescheduling-setTimeout shape livetrack.js already
  uses for its own refresh loop.
- Both network-heavy pieces (the Protomaps build index, per-tile
  fetches) are injectable, so test/test_pmtiles_builder.rb exercises
  the actual build pipeline against a fake `pmtiles` binary without
  hitting a real network, matching this suite's existing
  keep-it-offline convention for external services.
2026-09-11 01:25:08 +02:00

96 lines
3.4 KiB
Ruby

ENV["RACK_ENV"] = "test"
ENV["LIVETRACK_WEBHOOK_SECRET"] ||= "test-webhook-secret"
require "fileutils"
require "tmpdir"
require "minitest/autorun"
require "rack"
require "json"
# A fresh throwaway directory per test run, not the real data/ — every
# GpxStore/LiveTrackStore write app.rb's routes make during the run lands
# here and is gone when the process exits, so running the suite never
# touches (or needs to clean up after itself in) real uploaded data.
ENV["MAPTOOL_DATA_DIR"] = Dir.mktmpdir("maptool-test-data")
Minitest.after_run { FileUtils.remove_entry(ENV["MAPTOOL_DATA_DIR"]) }
# A fixture plugin (test/fixtures/plugins/test-plugin), not the real
# plugins/ — keeps /api/plugins route tests offline/deterministic instead
# of actually running plugins/overpass-pois against the real Overpass API.
ENV["MAPTOOL_PLUGINS_DIR"] = File.join(__dir__, "fixtures", "plugins")
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require_relative "../app"
# Available in every test via Minitest::Test below — fixture_path/json_body
# are generic enough to be worth having everywhere, not just in app.rb's
# own HTTP-level tests (see AppTestHelper for those).
module TestHelpers
def fixture_path(name)
File.join(__dir__, "fixtures", name)
end
def json_body(response)
JSON.parse(response.body)
end
end
class Minitest::Test
include TestHelpers
end
# Only included where needed (test_app.rb) — drives app.rb's Sinatra routes
# through Rack::MockRequest, the same way every ad-hoc verification during
# development did.
module AppTestHelper
def app
Maptool::App
end
def request
@request ||= Rack::MockRequest.new(app)
end
# Sinatra's session is read from env["rack.session"] (populated by the
# Rack::Session::Cookie middleware on a real request) — setting it
# directly here is equivalent to already having a valid session cookie,
# without needing to drive the actual Gitea OAuth2 login flow.
def logged_in_env(user: "testuser")
{ "rack.session" => { user: user } }
end
# GPX_STORE/LIVETRACK_STORE are process-wide constants pointing at the
# one throwaway MAPTOOL_DATA_DIR (see above) — without this, uploads from
# one test would still be there (and show up in tree/list assertions) in
# the next one, since minitest runs a class's tests in one process.
def teardown
FileUtils.rm_rf(Dir.glob(File.join(Maptool::App::DATA_DIR, "gpx", "*")))
File.write(File.join(Maptool::App::DATA_DIR, "livetrack.json"), "[]")
Maptool::PmtilesBuilder.reset!
super
end
def multipart_body(boundary, filename, content, content_type: "application/octet-stream", field_name: "file")
"--#{boundary}\r\n" \
"Content-Disposition: form-data; name=\"#{field_name}\"; filename=\"#{filename}\"\r\n" \
"Content-Type: #{content_type}\r\n\r\n" \
"#{content}\r\n" \
"--#{boundary}--\r\n"
end
# POSTs a single file as a multipart/form-data upload; pass folder in
# `path`'s query string (e.g. "/api/gpx?folder=trips") the same way the
# real upload form does.
def post_file(path, filename:, content:, content_type: "application/octet-stream", env: {})
boundary = "TestBoundary#{rand(1_000_000)}"
body = multipart_body(boundary, filename, content, content_type: content_type)
request.post(
path,
input: body,
"CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}",
"CONTENT_LENGTH" => body.bytesize.to_s,
**env,
)
end
end