A several-thousand-waypoint park4night area export re-parses the whole file with REXML on every toggle — ~2.4s for a 5k-waypoint/3.6MB sample, so tens of seconds for a 50k-waypoint one, likely the actual cause behind a "Could not load" report on a 41.5MB export. MapLibre already clusters the waypoint layer client-side, so the bottleneck was never rendering — cache the GeoJSON conversion alongside the source file (invalidated by mtime) instead of a bigger bbox/tiling rework.
126 lines
4.2 KiB
Ruby
126 lines
4.2 KiB
Ruby
require_relative "test_helper"
|
|
require "maptool/gpx_store"
|
|
require "stringio"
|
|
|
|
class GpxStoreTest < Minitest::Test
|
|
def setup
|
|
@store = Maptool::GpxStore.new(Dir.mktmpdir)
|
|
end
|
|
|
|
def test_save_returns_the_relative_path_and_lists_it_in_the_tree
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new("<gpx></gpx>"))
|
|
|
|
assert_equal "ride.gpx", path
|
|
assert_equal ["ride.gpx"], @store.tree["children"].map { |c| c["name"] }
|
|
end
|
|
|
|
def test_fit_extension_is_preserved_not_forced_to_gpx
|
|
path = @store.save(folder: "", filename: "activity.fit", io: StringIO.new("binary"))
|
|
assert_equal "activity.fit", path
|
|
end
|
|
|
|
def test_unrecognized_extension_gets_gpx_appended
|
|
path = @store.save(folder: "", filename: "notes.txt", io: StringIO.new("x"))
|
|
assert_equal "notes.txt.gpx", path
|
|
end
|
|
|
|
def test_colliding_filenames_are_disambiguated
|
|
@store.save(folder: "", filename: "ride.gpx", io: StringIO.new("a"))
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new("b"))
|
|
|
|
assert_equal "ride-1.gpx", path
|
|
end
|
|
|
|
def test_save_creates_the_target_folder
|
|
@store.save(folder: "trips/2026", filename: "ride.gpx", io: StringIO.new("x"))
|
|
|
|
folder = @store.tree["children"].find { |c| c["name"] == "trips" }["children"].find { |c| c["name"] == "2026" }
|
|
assert_equal ["ride.gpx"], folder["children"].map { |c| c["name"] }
|
|
end
|
|
|
|
def test_path_traversal_is_rejected
|
|
assert_raises(Maptool::GpxStore::InvalidPath) { @store.resolve_file("../../etc/passwd") }
|
|
assert_raises(Maptool::GpxStore::InvalidPath) { @store.save(folder: "../escape", filename: "x.gpx", io: StringIO.new("x")) }
|
|
end
|
|
|
|
def test_delete_file
|
|
@store.save(folder: "", filename: "ride.gpx", io: StringIO.new("x"))
|
|
@store.delete_file("ride.gpx")
|
|
|
|
assert_empty @store.tree["children"]
|
|
end
|
|
|
|
def test_delete_missing_file_raises_enoent
|
|
assert_raises(Errno::ENOENT) { @store.delete_file("nope.gpx") }
|
|
end
|
|
|
|
def test_delete_folder_refuses_the_store_root
|
|
assert_raises(Maptool::GpxStore::InvalidPath) { @store.delete_folder("") }
|
|
end
|
|
|
|
def test_move_relocates_and_disambiguates
|
|
@store.save(folder: "", filename: "ride.gpx", io: StringIO.new("a"))
|
|
@store.save(folder: "archive", filename: "ride.gpx", io: StringIO.new("b"))
|
|
|
|
new_path = @store.move(from: "ride.gpx", to_folder: "archive")
|
|
|
|
assert_equal "archive/ride-1.gpx", new_path
|
|
assert_empty @store.tree["children"].reject { |c| c["name"] == "archive" }
|
|
end
|
|
|
|
def test_geojson_is_cached_alongside_the_source_file
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
cache_path = @store.resolve_file(path) + ".geojson"
|
|
refute File.file?(cache_path)
|
|
|
|
first = @store.geojson(path)
|
|
assert File.file?(cache_path)
|
|
|
|
# Corrupt the cache directly to prove the second call reused it instead
|
|
# of re-parsing the (still-valid) source file.
|
|
File.write(cache_path, first.sub("waypoint", "cached-waypoint"))
|
|
assert_includes @store.geojson(path), "cached-waypoint"
|
|
end
|
|
|
|
def test_geojson_cache_is_invalidated_when_the_file_is_resaved
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
@store.geojson(path)
|
|
|
|
File.write(@store.resolve_file(path), sample_gpx.sub("Waypoint", "Renamed"))
|
|
FileUtils.touch(@store.resolve_file(path), mtime: Time.now + 1)
|
|
|
|
assert_includes @store.geojson(path), "Renamed"
|
|
end
|
|
|
|
def test_delete_file_removes_the_geojson_cache_too
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
@store.geojson(path)
|
|
cache_path = @store.resolve_file(path) + ".geojson"
|
|
|
|
@store.delete_file(path)
|
|
|
|
refute File.file?(cache_path)
|
|
end
|
|
|
|
def test_move_relocates_the_geojson_cache_too
|
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
|
@store.geojson(path)
|
|
|
|
new_path = @store.move(from: path, to_folder: "archive")
|
|
|
|
assert File.file?(@store.resolve_file(new_path) + ".geojson")
|
|
end
|
|
|
|
def test_geojson_raises_enoent_for_a_missing_file
|
|
assert_raises(Errno::ENOENT) { @store.geojson("nope.gpx") }
|
|
end
|
|
|
|
private
|
|
|
|
def sample_gpx
|
|
<<~GPX
|
|
<gpx><wpt lat="48.1" lon="17.1"><name>Waypoint</name></wpt></gpx>
|
|
GPX
|
|
end
|
|
end
|