mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
224 lines
7.8 KiB
Ruby
224 lines
7.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe Tracks::Generator do
|
|
let(:user) { create(:user) }
|
|
let(:safe_settings) { user.safe_settings }
|
|
|
|
before do
|
|
allow(user).to receive(:safe_settings).and_return(safe_settings)
|
|
end
|
|
|
|
describe '#call' do
|
|
context 'with bulk mode' do
|
|
let(:generator) { described_class.new(user, mode: :bulk) }
|
|
|
|
context 'with sufficient points' do
|
|
let!(:points) { create_points_around(user: user, count: 5, base_lat: 20.0) }
|
|
|
|
it 'generates tracks from all points' do
|
|
expect { generator.call }.to change(Track, :count).by(1)
|
|
end
|
|
|
|
it 'cleans existing tracks' do
|
|
existing_track = create(:track, user: user)
|
|
generator.call
|
|
expect(Track.exists?(existing_track.id)).to be false
|
|
end
|
|
|
|
it 'associates points with created tracks' do
|
|
generator.call
|
|
expect(points.map(&:reload).map(&:track)).to all(be_present)
|
|
end
|
|
end
|
|
|
|
context 'with insufficient points' do
|
|
let!(:points) { create_points_around(user: user, count: 1, base_lat: 20.0) }
|
|
|
|
it 'does not create tracks' do
|
|
expect { generator.call }.not_to change(Track, :count)
|
|
end
|
|
end
|
|
|
|
context 'with time range' do
|
|
let!(:old_points) { create_points_around(user: user, count: 3, base_lat: 20.0, timestamp: 2.days.ago.to_i) }
|
|
let!(:new_points) { create_points_around(user: user, count: 3, base_lat: 21.0, timestamp: 1.day.ago.to_i) }
|
|
|
|
it 'only processes points within range' do
|
|
generator = described_class.new(
|
|
user,
|
|
start_at: 1.day.ago.beginning_of_day,
|
|
end_at: 1.day.ago.end_of_day,
|
|
mode: :bulk
|
|
)
|
|
|
|
generator.call
|
|
track = Track.last
|
|
expect(track.points.count).to eq(3)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with incremental mode' do
|
|
let(:generator) { described_class.new(user, mode: :incremental) }
|
|
|
|
context 'with untracked points' do
|
|
let!(:points) { create_points_around(user: user, count: 3, base_lat: 22.0, track_id: nil) }
|
|
|
|
it 'processes untracked points' do
|
|
expect { generator.call }.to change(Track, :count).by(1)
|
|
end
|
|
|
|
it 'associates points with created tracks' do
|
|
generator.call
|
|
expect(points.map(&:reload).map(&:track)).to all(be_present)
|
|
end
|
|
end
|
|
|
|
context 'with end_at specified' do
|
|
let!(:early_points) { create_points_around(user: user, count: 2, base_lat: 23.0, timestamp: 2.hours.ago.to_i) }
|
|
let!(:late_points) { create_points_around(user: user, count: 2, base_lat: 24.0, timestamp: 1.hour.ago.to_i) }
|
|
|
|
it 'only processes points up to end_at' do
|
|
generator = described_class.new(user, end_at: 1.5.hours.ago, mode: :incremental)
|
|
generator.call
|
|
|
|
expect(Track.count).to eq(1)
|
|
expect(Track.first.points.count).to eq(2)
|
|
end
|
|
end
|
|
|
|
context 'without existing tracks' do
|
|
let!(:points) { create_points_around(user: user, count: 3, base_lat: 25.0) }
|
|
|
|
it 'does not clean existing tracks' do
|
|
existing_track = create(:track, user: user)
|
|
generator.call
|
|
expect(Track.exists?(existing_track.id)).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with daily mode' do
|
|
let(:today) { Date.current }
|
|
let(:generator) { described_class.new(user, start_at: today, mode: :daily) }
|
|
|
|
let!(:today_points) { create_points_around(user: user, count: 3, base_lat: 26.0, timestamp: today.beginning_of_day.to_i) }
|
|
let!(:yesterday_points) { create_points_around(user: user, count: 3, base_lat: 27.0, timestamp: 1.day.ago.to_i) }
|
|
|
|
it 'only processes points from specified day' do
|
|
generator.call
|
|
track = Track.last
|
|
expect(track.points.count).to eq(3)
|
|
end
|
|
|
|
it 'cleans existing tracks for the day' do
|
|
existing_track = create(:track, user: user, start_at: today.beginning_of_day)
|
|
generator.call
|
|
expect(Track.exists?(existing_track.id)).to be false
|
|
end
|
|
end
|
|
|
|
context 'with empty points' do
|
|
let(:generator) { described_class.new(user, mode: :bulk) }
|
|
|
|
it 'does not create tracks' do
|
|
expect { generator.call }.not_to change(Track, :count)
|
|
end
|
|
end
|
|
|
|
context 'with threshold configuration' do
|
|
let(:generator) { described_class.new(user, mode: :bulk) }
|
|
|
|
before do
|
|
allow(safe_settings).to receive(:meters_between_routes).and_return(1000)
|
|
allow(safe_settings).to receive(:minutes_between_routes).and_return(90)
|
|
end
|
|
|
|
it 'uses configured thresholds' do
|
|
expect(generator.send(:distance_threshold_meters)).to eq(1000)
|
|
expect(generator.send(:time_threshold_minutes)).to eq(90)
|
|
end
|
|
end
|
|
|
|
context 'with invalid mode' do
|
|
it 'raises argument error' do
|
|
expect do
|
|
described_class.new(user, mode: :invalid).call
|
|
end.to raise_error(ArgumentError, /Unknown mode/)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'segmentation behavior' do
|
|
let(:generator) { described_class.new(user, mode: :bulk) }
|
|
|
|
context 'with points exceeding time threshold' do
|
|
let!(:points) do
|
|
[
|
|
create_points_around(user: user, count: 1, base_lat: 29.0, timestamp: 90.minutes.ago.to_i),
|
|
create_points_around(user: user, count: 1, base_lat: 29.0, timestamp: 60.minutes.ago.to_i),
|
|
# Gap exceeds threshold 👇👇👇
|
|
create_points_around(user: user, count: 1, base_lat: 29.0, timestamp: 10.minutes.ago.to_i),
|
|
create_points_around(user: user, count: 1, base_lat: 29.0, timestamp: Time.current.to_i)
|
|
]
|
|
end
|
|
|
|
before do
|
|
allow(safe_settings).to receive(:minutes_between_routes).and_return(45)
|
|
end
|
|
|
|
it 'creates separate tracks for segments' do
|
|
expect { generator.call }.to change(Track, :count).by(2)
|
|
end
|
|
end
|
|
|
|
context 'with points exceeding distance threshold' do
|
|
let!(:points) do
|
|
[
|
|
create_points_around(user: user, count: 2, base_lat: 29.0, timestamp: 20.minutes.ago.to_i),
|
|
create_points_around(user: user, count: 2, base_lat: 29.0, timestamp: 15.minutes.ago.to_i),
|
|
# Large distance jump 👇👇👇
|
|
create_points_around(user: user, count: 2, base_lat: 28.0, timestamp: 10.minutes.ago.to_i),
|
|
create_points_around(user: user, count: 1, base_lat: 28.0, timestamp: Time.current.to_i)
|
|
]
|
|
end
|
|
|
|
before do
|
|
allow(safe_settings).to receive(:meters_between_routes).and_return(200)
|
|
end
|
|
|
|
it 'creates separate tracks for segments' do
|
|
expect { generator.call }.to change(Track, :count).by(2)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'deterministic behavior' do
|
|
let!(:points) { create_points_around(user: user, count: 10, base_lat: 28.0) }
|
|
|
|
it 'produces same results for bulk and incremental modes' do
|
|
# Generate tracks in bulk mode
|
|
bulk_generator = described_class.new(user, mode: :bulk)
|
|
bulk_generator.call
|
|
bulk_tracks = user.tracks.order(:start_at).to_a
|
|
|
|
# Clear tracks and generate incrementally
|
|
user.tracks.destroy_all
|
|
incremental_generator = described_class.new(user, mode: :incremental)
|
|
incremental_generator.call
|
|
incremental_tracks = user.tracks.order(:start_at).to_a
|
|
|
|
# Should have same number of tracks
|
|
expect(incremental_tracks.size).to eq(bulk_tracks.size)
|
|
|
|
# Should have same track boundaries (allowing for small timing differences)
|
|
bulk_tracks.zip(incremental_tracks).each do |bulk_track, incremental_track|
|
|
expect(incremental_track.start_at).to be_within(1.second).of(bulk_track.start_at)
|
|
expect(incremental_track.end_at).to be_within(1.second).of(bulk_track.end_at)
|
|
expect(incremental_track.distance).to be_within(10).of(bulk_track.distance)
|
|
end
|
|
end
|
|
end
|
|
end
|