Add a persisted test suite
`bundle exec rake test` (minitest, a default gem but pinned in Gemfile for reproducibility) now covers what ad-hoc scripts kept re-verifying by hand during development: Gpx/Fit/TrackStats parsing, GpxStore/LiveTrackStore file/TTL logic, LiveTrackClient's payload parsing, and app.rb's routes end to end via Rack::MockRequest. MAPTOOL_DATA_DIR (new, app.rb) lets the suite point GPX_STORE/ LIVETRACK_STORE at a throwaway directory instead of real data/ — without it every run would leave uploaded test files behind. Each lib file's old `if __FILE__ == $0` self-check is now redundant with real tests covering the same ground, so removed rather than kept as a second copy of the same assertions. Wired into ci.yml so it isn't just a local convenience.
This commit is contained in:
1 parent
a3c31ace06
commit
cd4eef342b
19 files changed
+706
-174
No files matched your search
@@ -172,67 +172,3 @@ module Maptool
|
||||
private_class_method :invalid?
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
require "tmpdir"
|
||||
|
||||
# A hand-built minimal FIT file: header + one definition message (local
|
||||
# type 0, global mesg 20 "record", fields position_lat/position_long,
|
||||
# both sint32) + two data messages + a throwaway CRC. Exercises the
|
||||
# header/definition/data framing without external fixtures.
|
||||
def le32(n)
|
||||
[n].pack("l<")
|
||||
end
|
||||
|
||||
def build_fit(records)
|
||||
definition = [0, 0].pack("CC") + [20].pack("S<") + [2].pack("C") +
|
||||
[0, 4, 0x85].pack("C3") + [1, 4, 0x85].pack("C3")
|
||||
definition_header = [0b01000000].pack("C")
|
||||
|
||||
data = records.map do |lat, lon|
|
||||
[0b00000000].pack("C") + le32(lat) + le32(lon)
|
||||
end.join
|
||||
|
||||
body = definition_header + definition + data
|
||||
header = [12, 16, 0, 0].pack("C2S<") + le32(body.bytesize) + ".FIT"
|
||||
header + body + "\x00\x00"
|
||||
end
|
||||
|
||||
Dir.mktmpdir do |dir|
|
||||
path = File.join(dir, "test.fit")
|
||||
File.binwrite(path, build_fit([[100_000_000, 200_000_000], [100_000_100, 200_000_100]]))
|
||||
|
||||
geojson = Maptool::Fit.to_geojson(path)
|
||||
coords = geojson["features"][0]["geometry"]["coordinates"][0]
|
||||
raise "expected 2 points" unless coords.size == 2
|
||||
expected_lat = 100_000_000 * Maptool::Fit::SEMICIRCLE_TO_DEGREES
|
||||
expected_lon = 200_000_000 * Maptool::Fit::SEMICIRCLE_TO_DEGREES
|
||||
raise "bad coordinate: #{coords[0]}" unless (coords[0][1] - expected_lat).abs < 1e-9 && (coords[0][0] - expected_lon).abs < 1e-9
|
||||
raise "expected kind track" unless Maptool::Fit.kinds(path) == ["track"]
|
||||
|
||||
empty_path = File.join(dir, "empty.fit")
|
||||
File.binwrite(empty_path, build_fit([]))
|
||||
raise "expected no features for a pointless file" unless Maptool::Fit.to_geojson(empty_path)["features"].empty?
|
||||
raise "expected no kinds for a pointless file" unless Maptool::Fit.kinds(empty_path) == []
|
||||
|
||||
raise "expected ParseError for garbage" rescue nil
|
||||
begin
|
||||
Maptool::Fit.to_geojson(File.join(dir, "nonexistent.fit"))
|
||||
raise "expected an error reading a missing file"
|
||||
rescue Errno::ENOENT
|
||||
# fine — this app only ever calls to_geojson with a path it already
|
||||
# knows exists
|
||||
end
|
||||
|
||||
garbage_path = File.join(dir, "garbage.fit")
|
||||
File.binwrite(garbage_path, "not a fit file at all")
|
||||
begin
|
||||
Maptool::Fit.to_geojson(garbage_path)
|
||||
raise "expected ParseError for garbage input"
|
||||
rescue Maptool::Fit::ParseError
|
||||
# expected
|
||||
end
|
||||
end
|
||||
|
||||
puts "ok"
|
||||
end
|
||||
@@ -126,39 +126,3 @@ module Maptool
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
client = Maptool::LiveTrackClient.new
|
||||
|
||||
fake_page = <<~HTML
|
||||
<html><body>
|
||||
<script>self.__next_f.push([1,"6:[\\"$\\",\\"div\\",null,{}]"])</script>
|
||||
<script>self.__next_f.push([1,"9:{\\"session\\":{\\"sessionName\\":\\"Test Ride\\",\\"start\\":\\"2026-01-01T00:00:00Z\\",\\"end\\":\\"2026-01-01T01:00:00Z\\"}}"])</script>
|
||||
<script>self.__next_f.push([1,"10:{\\"unrelated\\":true,\\"nested\\":{\\"data\\":{\\"pages\\":[{\\"trackPoints\\":[{\\"position\\":{\\"lat\\":48.1,\\"lon\\":17.2},\\"dateTime\\":\\"2026-01-01T00:00:00Z\\",\\"totalDistanceMeters\\":\\"$undefined\\"},{\\"position\\":{\\"lat\\":48.2,\\"lon\\":17.3},\\"dateTime\\":\\"2026-01-01T00:01:00Z\\",\\"activityType\\":\\"CYCLING\\",\\"totalDistanceMeters\\":5000,\\"totalDurationSecs\\":600}]}]}}}"])</script>
|
||||
</body></html>
|
||||
HTML
|
||||
|
||||
payloads = client.send(:parse_payloads, fake_page)
|
||||
points = client.send(:search, payloads) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) }
|
||||
raise "expected 2 points" unless points&.size == 2
|
||||
raise "expected first point lat 48.1" unless points[0]["position"]["lat"] == 48.1
|
||||
|
||||
session = client.send(:search, payloads) { |n| n if n["sessionName"] }
|
||||
raise "expected session name" unless session["sessionName"] == "Test Ride"
|
||||
|
||||
result = Maptool::LiveTrackClient::Result.new(points: client.send(:clean, points), session: client.send(:clean, session))
|
||||
summary = Maptool::LiveTrackClient.summarize(result)
|
||||
raise "expected $undefined cleaned to nil" unless result.points[0]["totalDistanceMeters"].nil?
|
||||
raise "expected summary from last point" unless summary["activity_type"] == "CYCLING" && summary["distance_meters"] == 5000
|
||||
raise "expected session name in summary" unless summary["session_name"] == "Test Ride"
|
||||
raise "expected inactive: session has an end" if summary["active"]
|
||||
|
||||
ongoing_session = session.merge("end" => nil)
|
||||
ongoing_summary = Maptool::LiveTrackClient.summarize(Maptool::LiveTrackClient::Result.new(points: result.points, session: ongoing_session))
|
||||
raise "expected active: session has no end" unless ongoing_summary["active"]
|
||||
|
||||
no_points = client.send(:search, client.send(:parse_payloads, "<html></html>")) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) }
|
||||
raise "expected nil when no trackPoints present" unless no_points.nil?
|
||||
|
||||
puts "ok"
|
||||
end
|
||||
@@ -88,44 +88,3 @@ module Maptool
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
require "tmpdir"
|
||||
|
||||
Dir.mktmpdir do |dir|
|
||||
store = Maptool::LiveTrackStore.new(File.join(dir, "livetrack.json"))
|
||||
raise "expected empty list" unless store.list == []
|
||||
|
||||
store.add("abc123", "tok-abc")
|
||||
store.add("def456", "tok-def")
|
||||
raise "expected 2 sessions" unless store.list.size == 2
|
||||
raise "expected newest first" unless store.list.first["id"] == "def456"
|
||||
raise "expected token stored" unless store.find("abc123")["token"] == "tok-abc"
|
||||
raise "expected unknown id to be nil" unless store.find("nope").nil?
|
||||
raise "expected no summary yet" unless store.find("abc123")["summary"].nil?
|
||||
|
||||
store.add("abc123", "tok-abc-2")
|
||||
raise "expected re-added session moved to front, not duplicated" \
|
||||
unless store.list.map { |s| s["id"] } == %w[abc123 def456]
|
||||
raise "expected token updated on re-add" unless store.find("abc123")["token"] == "tok-abc-2"
|
||||
|
||||
store.set_summary("abc123", { "session_name" => "Test Ride" })
|
||||
raise "expected summary stored" unless store.find("abc123")["summary"] == { "session_name" => "Test Ride" }
|
||||
store.add("abc123", "tok-abc-3")
|
||||
raise "expected summary to carry over across re-add" \
|
||||
unless store.find("abc123")["summary"] == { "session_name" => "Test Ride" }
|
||||
|
||||
# Manually age an unsummarized session past the TTL and confirm it gets
|
||||
# pruned, while a summarized one (however old) survives.
|
||||
raw = JSON.parse(File.read(File.join(dir, "livetrack.json")))
|
||||
raw.find { |s| s["id"] == "def456" }["received_at"] = (Time.now.utc - 49 * 60 * 60).iso8601
|
||||
raw.find { |s| s["id"] == "abc123" }["received_at"] = (Time.now.utc - 1000 * 60 * 60).iso8601
|
||||
File.write(File.join(dir, "livetrack.json"), JSON.generate(raw))
|
||||
|
||||
reloaded = Maptool::LiveTrackStore.new(File.join(dir, "livetrack.json"))
|
||||
raise "expected old unsummarized session pruned" if reloaded.list.any? { |s| s["id"] == "def456" }
|
||||
raise "expected old but summarized session kept" unless reloaded.list.any? { |s| s["id"] == "abc123" }
|
||||
end
|
||||
|
||||
puts "ok"
|
||||
end
|
||||
@@ -37,30 +37,3 @@ module Maptool
|
||||
private_class_method :haversine
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
# A right-angle path with a known leg length (via the equirectangular
|
||||
# approximation, accurate at small scale) plus a climb-then-descend
|
||||
# elevation profile, so both distance and elevation-gain have a value to
|
||||
# check against instead of just "did it run".
|
||||
square = [
|
||||
[17.0, 48.0, 100],
|
||||
[17.001, 48.0, 150], # +50m gain
|
||||
[17.001, 48.001, 120], # descent, no gain added
|
||||
]
|
||||
stats = Maptool::TrackStats.compute(square, times: [Time.utc(2026, 1, 1, 0, 0, 0), nil, Time.utc(2026, 1, 1, 0, 10, 0)])
|
||||
|
||||
raise "expected nonzero distance" unless stats["distance_meters"] > 100
|
||||
raise "expected elevation gain 50" unless stats["elevation_gain_meters"] == 50.0
|
||||
raise "expected duration 600s" unless stats["duration_secs"] == 600
|
||||
raise "expected point count 3" unless stats["point_count"] == 3
|
||||
|
||||
flat = Maptool::TrackStats.compute([[17.0, 48.0], [17.0, 48.0]], times: [])
|
||||
raise "expected nil elevation gain without elevation data" unless flat["elevation_gain_meters"].nil?
|
||||
raise "expected nil duration without times" unless flat["duration_secs"].nil?
|
||||
|
||||
single = Maptool::TrackStats.compute([[17.0, 48.0]])
|
||||
raise "expected zero distance for a single point" unless single["distance_meters"] == 0.0
|
||||
|
||||
puts "ok"
|
||||
end
|
||||
Reference in new issue
Block a user