When we first started caching the stats, generating them was a process
that took several minutes, so we never expired the cache.
However, there have been cases where we run into issues where the stats
shown on the screen were outdated. That's why we introduced a task to
manually expire the cache.
But now, generating the stats only takes a few seconds, so we can
automatically expire them every day, remove all the logic needed to
manually expire them, and get rid of most of the issues related to the
cache being outdated.
We're expiring them every day because it's the same day we were doing in
public stats (which we removed in commit 631b48f58), only we're using
`expires_at:` to set the expiration time, in order to simplify the code.
Note that, in the test, we're using `travel_to(time)` so the test passes
even when it starts an instant before midnight. We aren't using
`:with_frozen_time` because, in similar cases (although not in this
case, but I'm not sure whether that's intentional), `travel_to` shows
this error:
> Calling `travel_to` with a block, when we have previously already made
> a call to `travel_to`, can lead to confusing time stubbing.
255 lines
6.1 KiB
Ruby
255 lines
6.1 KiB
Ruby
require "rails_helper"
|
|
|
|
describe Statisticable do
|
|
before do
|
|
dummy_stats = Class.new do
|
|
include Statisticable
|
|
attr_accessor :total
|
|
stats_cache :total
|
|
|
|
def full_cache_key_for(key)
|
|
"dummy_stats/#{object_id}/#{key}"
|
|
end
|
|
|
|
def participants
|
|
User.all
|
|
end
|
|
alias_method :participants_from_original_table, :participants
|
|
|
|
def total_participants
|
|
User.count
|
|
end
|
|
|
|
def participation_date
|
|
Time.current
|
|
end
|
|
end
|
|
|
|
stub_const("DummyStats", dummy_stats)
|
|
end
|
|
|
|
let(:stats) { DummyStats.new(double(id: 1, class: double(table_name: ""))) }
|
|
|
|
describe "#gender?" do
|
|
context "No participants" do
|
|
it "is false" do
|
|
expect(stats.gender?).to be false
|
|
end
|
|
end
|
|
|
|
context "All participants have no defined gender" do
|
|
before { create(:user, gender: nil) }
|
|
|
|
it "is false" do
|
|
expect(stats.gender?).to be false
|
|
end
|
|
end
|
|
|
|
context "There's a male participant" do
|
|
before { create(:user, gender: "male") }
|
|
|
|
it "is true" do
|
|
expect(stats.gender?).to be true
|
|
end
|
|
end
|
|
|
|
context "There's a female participant" do
|
|
before { create(:user, gender: "female") }
|
|
|
|
it "is true" do
|
|
expect(stats.gender?).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#age?" do
|
|
context "No participants" do
|
|
it "is false" do
|
|
expect(stats.age?).to be false
|
|
end
|
|
end
|
|
|
|
context "All participants have no defined age" do
|
|
before { create(:user, date_of_birth: nil) }
|
|
|
|
it "is false" do
|
|
expect(stats.age?).to be false
|
|
end
|
|
end
|
|
|
|
context "All participants have impossible ages" do
|
|
before do
|
|
create(:user, date_of_birth: 3.seconds.ago)
|
|
create(:user, date_of_birth: 3000.years.ago)
|
|
end
|
|
|
|
it "is false" do
|
|
expect(stats.age?).to be false
|
|
end
|
|
end
|
|
|
|
context "There's a participant with a defined age" do
|
|
before { create(:user, date_of_birth: 30.years.ago) }
|
|
|
|
it "is true" do
|
|
expect(stats.age?).to be true
|
|
end
|
|
end
|
|
|
|
context "Partipation took place a long time ago" do
|
|
before do
|
|
create(:user, date_of_birth: 2000.years.ago)
|
|
allow(stats).to receive(:participation_date).and_return(1900.years.ago)
|
|
end
|
|
|
|
it "is true" do
|
|
expect(stats.age?).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#geozone?" do
|
|
context "No participants" do
|
|
it "is false" do
|
|
expect(stats.geozone?).to be false
|
|
end
|
|
end
|
|
|
|
context "All participants have no defined geozone" do
|
|
before { create(:user, geozone: nil) }
|
|
|
|
it "is false" do
|
|
expect(stats.geozone?).to be false
|
|
end
|
|
end
|
|
|
|
context "There's a participant with a defined geozone" do
|
|
before { create(:user, geozone: create(:geozone)) }
|
|
|
|
it "is true" do
|
|
expect(stats.geozone?).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#total_no_demographic_data" do
|
|
it "returns users with no defined gender" do
|
|
create(:user, gender: nil)
|
|
|
|
expect(stats.total_no_demographic_data).to be 1
|
|
end
|
|
|
|
it "returns users with no defined age" do
|
|
create(:user, gender: "female", date_of_birth: nil)
|
|
|
|
expect(stats.total_no_demographic_data).to be 1
|
|
end
|
|
|
|
it "returns users with no defined geozone" do
|
|
create(:user, gender: "female", geozone: nil)
|
|
|
|
expect(stats.total_no_demographic_data).to be 1
|
|
end
|
|
|
|
it "returns users with no defined gender, age nor geozone" do
|
|
create(:user, gender: nil, date_of_birth: nil, geozone: nil)
|
|
|
|
expect(stats.total_no_demographic_data).to be 1
|
|
end
|
|
|
|
it "doesn't return users with defined gender, age and geozone" do
|
|
create(:user, gender: "male", date_of_birth: 20.years.ago, geozone: create(:geozone))
|
|
|
|
expect(stats.total_no_demographic_data).to be 0
|
|
end
|
|
end
|
|
|
|
describe "#stats_methods" do
|
|
it "includes total participants" do
|
|
expect(stats.stats_methods).to include(:total_participants)
|
|
end
|
|
|
|
context "no gender stats" do
|
|
before { allow(stats).to receive(:gender?).and_return(false) }
|
|
|
|
it "doesn't include gender methods" do
|
|
expect(stats.stats_methods).not_to include(:total_male_participants)
|
|
end
|
|
end
|
|
|
|
context "no age stats" do
|
|
before { allow(stats).to receive(:age?).and_return(false) }
|
|
|
|
it "doesn't include age methods" do
|
|
expect(stats.stats_methods).not_to include(:participants_by_age)
|
|
end
|
|
end
|
|
|
|
context "no geozone stats" do
|
|
before { allow(stats).to receive(:geozone?).and_return(false) }
|
|
|
|
it "doesn't include age methods" do
|
|
expect(stats.stats_methods).not_to include(:participants_by_geozone)
|
|
end
|
|
end
|
|
|
|
context "all gender, age and geozone stats" do
|
|
before do
|
|
allow(stats).to receive_messages(gender?: true, age?: true, geozone?: true)
|
|
end
|
|
|
|
it "includes all stats methods" do
|
|
expect(stats.stats_methods).to include(:total_male_participants)
|
|
expect(stats.stats_methods).to include(:participants_by_age)
|
|
expect(stats.stats_methods).to include(:participants_by_geozone)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#generate" do
|
|
it "drops the temporary table after finishing" do
|
|
stats.generate
|
|
|
|
expect { stats.send(:participants_class).first }.to raise_exception(ActiveRecord::StatementInvalid)
|
|
end
|
|
|
|
it "can be executed twice without errors" do
|
|
stats.generate
|
|
|
|
expect { stats.generate }.not_to raise_exception
|
|
end
|
|
|
|
it "can be executed twice in parallel since it uses a transaction" do
|
|
other_stats = DummyStats.new(stats.resource)
|
|
|
|
[stats, other_stats].map do |stat|
|
|
Thread.new { stat.generate }
|
|
end.each(&:join)
|
|
end
|
|
end
|
|
|
|
describe "cache" do
|
|
it "expires the cache at the end of the day", :with_cache do
|
|
time = Time.current
|
|
|
|
travel_to(time) do
|
|
stats.total = 6
|
|
|
|
expect(stats.total).to eq 6
|
|
|
|
stats.total = 7
|
|
|
|
expect(stats.total).to eq 6
|
|
end
|
|
|
|
travel_to(time.end_of_day) do
|
|
expect(stats.total).to eq 6
|
|
end
|
|
|
|
travel_to(time.end_of_day + 1.second) do
|
|
expect(stats.total).to eq 7
|
|
end
|
|
end
|
|
end
|
|
end
|