dawarich/spec/services/tracks/generator_spec.rb

260 lines
9.4 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
it 'properly handles point associations when cleaning existing tracks' do
# Create existing tracks with associated points
existing_track = create(:track, user: user)
existing_points = create_list(:point, 3, user: user, track: existing_track)
# Verify points are associated
expect(existing_points.map(&:reload).map(&:track_id)).to all(eq(existing_track.id))
# Run generator which should clean existing tracks and create new ones
generator.call
# Verify the old track is deleted
expect(Track.exists?(existing_track.id)).to be false
# Verify the points are no longer associated with the deleted track
expect(existing_points.map(&:reload).map(&:track_id)).to all(be_nil)
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
it 'properly handles point associations when cleaning daily tracks' do
# Create existing tracks with associated points for today
existing_track = create(:track, user: user, start_at: today.beginning_of_day)
existing_points = create_list(:point, 3, user: user, track: existing_track)
# Verify points are associated
expect(existing_points.map(&:reload).map(&:track_id)).to all(eq(existing_track.id))
# Run generator which should clean existing tracks for the day and create new ones
generator.call
# Verify the old track is deleted
expect(Track.exists?(existing_track.id)).to be false
# Verify the points are no longer associated with the deleted track
expect(existing_points.map(&:reload).map(&:track_id)).to all(be_nil)
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