dawarich/spec/services/tracks/time_chunker_spec.rb

310 lines
11 KiB
Ruby
Raw Permalink Normal View History

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Tracks::TimeChunker do
let(:user) { create(:user) }
let(:chunker) { described_class.new(user, **options) }
let(:options) { {} }
describe '#initialize' do
it 'sets default values' do
expect(chunker.user).to eq(user)
expect(chunker.start_at).to be_nil
expect(chunker.end_at).to be_nil
expect(chunker.chunk_size).to eq(1.day)
expect(chunker.buffer_size).to eq(6.hours)
end
it 'accepts custom options' do
start_time = 1.week.ago
end_time = Time.current
2025-08-29 05:05:25 -04:00
custom_chunker = described_class.new(
user,
start_at: start_time,
end_at: end_time,
chunk_size: 2.days,
buffer_size: 2.hours
)
expect(custom_chunker.start_at).to eq(start_time)
expect(custom_chunker.end_at).to eq(end_time)
expect(custom_chunker.chunk_size).to eq(2.days)
expect(custom_chunker.buffer_size).to eq(2.hours)
end
end
describe '#call' do
context 'when user has no points' do
it 'returns empty array' do
expect(chunker.call).to eq([])
end
end
context 'when start_at is after end_at' do
let(:options) { { start_at: Time.current, end_at: 1.day.ago } }
it 'returns empty array' do
expect(chunker.call).to eq([])
end
end
context 'with user points' do
let!(:point1) { create(:point, user: user, timestamp: 3.days.ago.to_i) }
let!(:point2) { create(:point, user: user, timestamp: 2.days.ago.to_i) }
let!(:point3) { create(:point, user: user, timestamp: 1.day.ago.to_i) }
context 'with both start_at and end_at provided' do
let(:start_time) { 3.days.ago }
let(:end_time) { 1.day.ago }
let(:options) { { start_at: start_time, end_at: end_time } }
it 'creates chunks for the specified range' do
chunks = chunker.call
expect(chunks).not_to be_empty
expect(chunks.first[:start_time]).to be >= start_time
expect(chunks.last[:end_time]).to be <= end_time
end
it 'creates chunks with buffer zones' do
chunks = chunker.call
chunk = chunks.first
# Buffer zones should be at or beyond chunk boundaries (may be constrained by global boundaries)
expect(chunk[:buffer_start_time]).to be <= chunk[:start_time]
expect(chunk[:buffer_end_time]).to be >= chunk[:end_time]
2025-08-29 05:05:25 -04:00
# Verify buffer timestamps are consistent
expect(chunk[:buffer_start_timestamp]).to eq(chunk[:buffer_start_time].to_i)
expect(chunk[:buffer_end_timestamp]).to eq(chunk[:buffer_end_time].to_i)
end
it 'includes required chunk data structure' do
chunks = chunker.call
chunk = chunks.first
expect(chunk).to include(
:chunk_id,
:start_timestamp,
:end_timestamp,
:buffer_start_timestamp,
:buffer_end_timestamp,
:start_time,
:end_time,
:buffer_start_time,
:buffer_end_time
)
2025-08-29 05:05:25 -04:00
expect(chunk[:chunk_id]).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
end
end
context 'with only start_at provided' do
let(:start_time) { 2.days.ago }
let(:options) { { start_at: start_time } }
it 'creates chunks from start_at to current time' do
# Capture current time before running to avoid precision issues
end_time_before = Time.current
chunks = chunker.call
end_time_after = Time.current
expect(chunks).not_to be_empty
expect(chunks.first[:start_time]).to be >= start_time
# Allow for some time drift during test execution
expect(chunks.last[:end_time]).to be_between(end_time_before, end_time_after + 1.second)
end
end
context 'with only end_at provided' do
let(:options) { { end_at: 1.day.ago } }
it 'creates chunks from first point to end_at' do
chunks = chunker.call
expect(chunks).not_to be_empty
expect(chunks.first[:start_time]).to be >= Time.at(point1.timestamp)
expect(chunks.last[:end_time]).to be <= 1.day.ago
end
end
context 'with no time range provided' do
it 'creates chunks for full user point range' do
chunks = chunker.call
expect(chunks).not_to be_empty
expect(chunks.first[:start_time]).to be >= Time.at(point1.timestamp)
expect(chunks.last[:end_time]).to be <= Time.at(point3.timestamp)
end
end
context 'with custom chunk size' do
let(:options) { { chunk_size: 12.hours, start_at: 2.days.ago, end_at: Time.current } }
it 'creates smaller chunks' do
chunks = chunker.call
# Should create more chunks with smaller chunk size
expect(chunks.size).to be > 2
2025-08-29 05:05:25 -04:00
# Each chunk should be approximately 12 hours
chunk = chunks.first
duration = chunk[:end_time] - chunk[:start_time]
expect(duration).to be <= 12.hours
end
end
context 'with custom buffer size' do
let(:options) { { buffer_size: 1.hour, start_at: 2.days.ago, end_at: Time.current } }
it 'creates chunks with smaller buffer zones' do
chunks = chunker.call
chunk = chunks.first
buffer_start_diff = chunk[:start_time] - chunk[:buffer_start_time]
buffer_end_diff = chunk[:buffer_end_time] - chunk[:end_time]
2025-08-29 05:05:25 -04:00
expect(buffer_start_diff).to be <= 1.hour
expect(buffer_end_diff).to be <= 1.hour
end
end
end
context 'buffer zone boundary handling' do
let!(:point1) { create(:point, user: user, timestamp: 1.week.ago.to_i) }
let!(:point2) { create(:point, user: user, timestamp: Time.current.to_i) }
let(:options) { { start_at: 3.days.ago, end_at: Time.current } }
it 'does not extend buffers beyond global boundaries' do
chunks = chunker.call
chunk = chunks.first
expect(chunk[:buffer_start_time]).to be >= 3.days.ago
expect(chunk[:buffer_end_time]).to be <= Time.current
end
end
context 'chunk filtering based on points' do
let(:options) { { start_at: 1.week.ago, end_at: Time.current } }
context 'when chunk has no points in buffer range' do
# Create points only at the very end of the range
let!(:point) { create(:point, user: user, timestamp: 1.hour.ago.to_i) }
it 'filters out empty chunks' do
chunks = chunker.call
# Should only include chunks that actually have points
expect(chunks).not_to be_empty
chunks.each do |chunk|
# Verify each chunk has points in its buffer range
2025-08-29 05:05:25 -04:00
points_exist = user.points
.where(timestamp: chunk[:buffer_start_timestamp]..chunk[:buffer_end_timestamp])
.exists?
expect(points_exist).to be true
end
end
end
end
context 'timestamp consistency' do
let!(:point) { create(:point, user: user, timestamp: 1.day.ago.to_i) }
let(:options) { { start_at: 2.days.ago, end_at: Time.current } }
it 'maintains timestamp consistency between Time objects and integers' do
chunks = chunker.call
chunk = chunks.first
expect(chunk[:start_timestamp]).to eq(chunk[:start_time].to_i)
expect(chunk[:end_timestamp]).to eq(chunk[:end_time].to_i)
expect(chunk[:buffer_start_timestamp]).to eq(chunk[:buffer_start_time].to_i)
expect(chunk[:buffer_end_timestamp]).to eq(chunk[:buffer_end_time].to_i)
end
end
context 'edge cases' do
context 'when start_at equals end_at' do
let(:time_point) { 1.day.ago }
let(:options) { { start_at: time_point, end_at: time_point } }
it 'returns empty array' do
expect(chunker.call).to eq([])
end
end
context 'when user has only one point' do
let!(:point) { create(:point, user: user, timestamp: 1.day.ago.to_i) }
it 'creates appropriate chunks' do
chunks = chunker.call
# With only one point, start and end times are the same, so no chunks are created
# This is expected behavior as there's no time range to chunk
expect(chunks).to be_empty
end
end
context 'when time range is very small' do
let(:base_time) { 1.day.ago }
let(:options) { { start_at: base_time, end_at: base_time + 1.hour } }
let!(:point) { create(:point, user: user, timestamp: base_time.to_i) }
it 'creates at least one chunk' do
chunks = chunker.call
expect(chunks.size).to eq(1)
expect(chunks.first[:start_time]).to eq(base_time)
expect(chunks.first[:end_time]).to eq(base_time + 1.hour)
end
end
end
end
describe 'private methods' do
describe '#determine_time_range' do
let!(:point1) { create(:point, user: user, timestamp: 3.days.ago.to_i) }
let!(:point2) { create(:point, user: user, timestamp: 1.day.ago.to_i) }
it 'handles all time range scenarios correctly' do
test_start_time = 2.days.ago
test_end_time = Time.current
2025-08-29 05:05:25 -04:00
# Both provided
chunker_both = described_class.new(user, start_at: test_start_time, end_at: test_end_time)
result_both = chunker_both.send(:determine_time_range)
expect(result_both[0]).to be_within(1.second).of(test_start_time.to_time)
expect(result_both[1]).to be_within(1.second).of(test_end_time.to_time)
2025-08-29 05:05:25 -04:00
# Only start provided
chunker_start = described_class.new(user, start_at: test_start_time)
result_start = chunker_start.send(:determine_time_range)
expect(result_start[0]).to be_within(1.second).of(test_start_time.to_time)
expect(result_start[1]).to be_within(1.second).of(Time.current)
# Only end provided
chunker_end = described_class.new(user, end_at: test_end_time)
result_end = chunker_end.send(:determine_time_range)
expect(result_end[0]).to eq(Time.at(point1.timestamp))
expect(result_end[1]).to be_within(1.second).of(test_end_time.to_time)
# Neither provided
chunker_neither = described_class.new(user)
result_neither = chunker_neither.send(:determine_time_range)
expect(result_neither[0]).to eq(Time.at(point1.timestamp))
expect(result_neither[1]).to eq(Time.at(point2.timestamp))
end
context 'when user has no points and end_at is provided' do
let(:user_no_points) { create(:user) }
let(:chunker_no_points) { described_class.new(user_no_points, end_at: Time.current) }
it 'returns nil' do
expect(chunker_no_points.send(:determine_time_range)).to be_nil
end
end
end
end
2025-08-29 05:05:25 -04:00
end