diff --git a/Gemfile b/Gemfile
index 34074dfd..9a792c7a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,7 +8,19 @@ gem 'pg'
gem 'puma'
gem 'listen'
gem 'bootsnap'
-gem 'rack-mini-profiler'
+
+group :development, :test do
+ gem 'pghero'
+ gem 'bullet'
+ gem 'pg_query', '>= 2'
+ gem 'rspec-rails'
+ gem 'rspec-benchmark'
+ gem 'rack-mini-profiler'
+ gem 'ruby-prof'
+ gem 'memory_profiler'
+ gem 'capybara'
+ gem 'strong_migrations'
+end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
diff --git a/Gemfile.lock b/Gemfile.lock
index a9ddd818..e37b0af7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -72,21 +72,42 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
base64 (0.2.0)
benchmark (0.4.0)
+ benchmark-malloc (0.2.0)
+ benchmark-perf (0.6.0)
+ benchmark-trend (0.4.0)
bigdecimal (3.1.9)
bootsnap (1.18.4)
msgpack (~> 1.2)
builder (3.3.0)
+ bullet (8.0.1)
+ activesupport (>= 3.0.0)
+ uniform_notifier (~> 1.11)
+ capybara (3.40.0)
+ addressable
+ matrix
+ mini_mime (>= 0.1.3)
+ nokogiri (~> 1.11)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ regexp_parser (>= 1.5, < 3.0)
+ xpath (~> 3.2)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
crass (1.0.6)
date (3.4.1)
+ diff-lcs (1.6.0)
drb (2.2.1)
erubi (1.13.1)
ffi (1.17.1-arm64-darwin)
globalid (1.2.1)
activesupport (>= 6.1)
+ google-protobuf (4.30.0-arm64-darwin)
+ bigdecimal
+ rake (>= 13)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
@@ -107,6 +128,8 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
+ matrix (0.4.2)
+ memory_profiler (1.1.0)
mini_mime (1.1.5)
minitest (5.25.4)
msgpack (1.8.0)
@@ -123,12 +146,17 @@ GEM
nokogiri (1.18.2-arm64-darwin)
racc (~> 1.4)
pg (1.5.9)
+ pg_query (6.0.0)
+ google-protobuf (>= 3.25.3)
+ pghero (3.6.1)
+ activerecord (>= 6.1)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
psych (5.2.3)
date
stringio
+ public_suffix (6.0.1)
puma (6.6.0)
nio4r (~> 2.0)
racc (1.8.1)
@@ -177,32 +205,75 @@ GEM
ffi (~> 1.0)
rdoc (6.12.0)
psych (>= 4.0.0)
+ regexp_parser (2.10.0)
reline (0.6.0)
io-console (~> 0.5)
+ rspec (3.13.0)
+ rspec-core (~> 3.13.0)
+ rspec-expectations (~> 3.13.0)
+ rspec-mocks (~> 3.13.0)
+ rspec-benchmark (0.6.0)
+ benchmark-malloc (~> 0.2)
+ benchmark-perf (~> 0.6)
+ benchmark-trend (~> 0.4)
+ rspec (>= 3.0)
+ rspec-core (3.13.3)
+ rspec-support (~> 3.13.0)
+ rspec-expectations (3.13.3)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-mocks (3.13.2)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-rails (7.1.1)
+ actionpack (>= 7.0)
+ activesupport (>= 7.0)
+ railties (>= 7.0)
+ rspec-core (~> 3.13)
+ rspec-expectations (~> 3.13)
+ rspec-mocks (~> 3.13)
+ rspec-support (~> 3.13)
+ rspec-support (3.13.2)
+ ruby-prof (1.7.1)
securerandom (0.4.1)
stringio (3.1.2)
+ strong_migrations (2.2.0)
+ activerecord (>= 7)
thor (1.3.2)
timeout (0.4.3)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
+ uniform_notifier (1.16.0)
uri (1.0.2)
useragent (0.16.11)
websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
+ xpath (3.2.0)
+ nokogiri (~> 1.8)
zeitwerk (2.7.1)
PLATFORMS
+ arm64-darwin-21
arm64-darwin-24
DEPENDENCIES
bootsnap
+ bullet
+ capybara
listen
+ memory_profiler
pg
+ pg_query (>= 2)
+ pghero
puma
rack-mini-profiler
rails (~> 8.0.1)
+ rspec-benchmark
+ rspec-rails
+ ruby-prof
+ strong_migrations
tzinfo-data
RUBY VERSION
diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb
index acb38be2..1431925c 100644
--- a/app/controllers/trips_controller.rb
+++ b/app/controllers/trips_controller.rb
@@ -2,6 +2,6 @@ class TripsController < ApplicationController
def index
@from = City.find_by_name!(params[:from])
@to = City.find_by_name!(params[:to])
- @trips = Trip.where(from: @from, to: @to).order(:start_time)
+ @trips = Trip.includes(bus: :services).where(from: @from, to: @to).order(:start_time)
end
end
diff --git a/app/views/trips/_delimiter.html.erb b/app/views/trips/_delimiter.html.erb
deleted file mode 100644
index 3f845ad0..00000000
--- a/app/views/trips/_delimiter.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-====================================================
diff --git a/app/views/trips/_service.html.erb b/app/views/trips/_service.html.erb
deleted file mode 100644
index 178ea8c0..00000000
--- a/app/views/trips/_service.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-
<%= "#{service.name}" %>
diff --git a/app/views/trips/_services.html.erb b/app/views/trips/_services.html.erb
deleted file mode 100644
index 2de639fc..00000000
--- a/app/views/trips/_services.html.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-Сервисы в автобусе:
-
- <% services.each do |service| %>
- <%= render "service", service: service %>
- <% end %>
-
diff --git a/app/views/trips/_trip.html.erb b/app/views/trips/_trip.html.erb
deleted file mode 100644
index fa1de9aa..00000000
--- a/app/views/trips/_trip.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<%= "Отправление: #{trip.start_time}" %>
-<%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
-<%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
-<%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
-<%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb
index a60bce41..f0a7d663 100644
--- a/app/views/trips/index.html.erb
+++ b/app/views/trips/index.html.erb
@@ -7,10 +7,21 @@
<% @trips.each do |trip| %>
- <%= render "trip", trip: trip %>
- <% if trip.bus.services.present? %>
- <%= render "services", services: trip.bus.services %>
+ - <%= "Отправление: #{trip.start_time}" %>
+ - <%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
+ - <%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
+ - <%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
+ - <%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
+
+ <% services = trip.bus.services %>
+ <% if services.present? %>
+ - Сервисы в автобусе:
+
+ <% services.each do |service| %>
+ - <%= "#{service.name}" %>
+ <% end %>
+
<% end %>
- <%= render "delimiter" %>
+ ====================================================
<% end %>
diff --git a/benchmark/benchmark.rb b/benchmark/benchmark.rb
new file mode 100644
index 00000000..7cef9cfb
--- /dev/null
+++ b/benchmark/benchmark.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'benchmark'
+
+time = Benchmark.realtime do
+ system 'bin/rake utils:reload_json[fixtures/medium.json]'
+end
+
+puts "Finish in #{time.round(2)}"
diff --git a/case-study.md b/case-study.md
new file mode 100644
index 00000000..4f6ad5ca
--- /dev/null
+++ b/case-study.md
@@ -0,0 +1,154 @@
+# Case-study оптимизации
+
+## Case 1
+Метрика:
+
+`ruby benchmark/benchmark.rb`
+
+Время работы rake таски на medium.json
+Finish in 69.99
+
+## Бюджет
+Снизить время выполнения rake задачи до 10 секунд на файле large.json
+
+## Фиксация текущей метрики
+`bundle exec rspec spec/task/utils_performace_spec.rb:5`
+
+## Профилировщик
+Воспользовался ruby-profiler
+
+`ruby profiling/ruby_prof.rb`
+
+**Точки роста:**
+```
+99.66% 0.10% 24.91 0.02 0.00 24.89 7421 *ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction
+15.19 0.00 0.00 15.19 4265/4265 ActiveRecord::Validations#save
+```
+
+Создается множество отдельных операций к базе. Выполняются валидации, запуск транзакций и персист записей.
+
+**Решение**
+
+Пробую воспользоваться bulk_insert
+
+После оптимизации:
+Результат снизился до 8 секунд при загрузки данных из файла large.json
+
+## Case 2
+Метрика:
+Загрузка страницы( /автобусы/Самара/Москва) происходит ~ 6.8 секунд, посмотрел в логи + rack-mini-profiler
+
+## Бюджет
+Снизить загрузку страницы до 0.4 секунд
+
+**Точки роста**
+N+1 в логах
+Много раз рендеринг паршалов.
+
+**Решение**
+```ruby
+@trips = Trip.includes(bus: :services).where(from: @from, to: @to).order(:start_time)
+```
+
+Для каждого объекта Trip подгружаю объекты Bus и для каждого объекта Bus подгружаю объекты Service
+Также пока решил убрать паршелы и перенести во вьюху
+
+После оптимизации:
+Скорость загрузки страницы снизилась до 330ms ~ 0.3 секунды
+
+## Case 3
+
+Бюджет снизизить загрузку страницы до 0.1 секунды
+
+## Профилировщик
+Установил PGHero
+В дашборде на вкладке Overview не показал что нужно создать индексы
+
+`
+No long running queries
+Connections healthy 7
+Vacuuming healthy
+No columns near integer overflow
+No invalid indexes or constraints
+No duplicate indexes
+No suggested indexes
+No slow queries
+`
+
+На вкладке Queries отображает два запроса:
+
+1. Выполняется 30% от общего времени
+
+```ruby
+SELECT COUNT(*) FROM "trips" WHERE "trips"."from_id" = $1 AND "trips"."to_id" = $2
+```
+
+2. Выполняется 16% от общего времени
+
+```ruby
+SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = $1 AND "trips"."to_id" = $2 ORDER BY "trips"."start_time" ASC
+```
+Добавил в explain
+```ruby
+SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = 4 AND "trips"."to_id" = 2 ORDER BY "trips"."start_time" ASC
+```
+
+**Решение**
+Добавил композитный индекс по двум полям
+В результате:
+для запроса:
+
+```ruby
+SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = 4 AND "trips"."to_id" = 2 ORDER BY "trips"."start_time" ASC
+```
+
+```
+Sort (cost=953.40..955.84 rows=977 width=34)
+ Sort Key: start_time
+ -> Bitmap Heap Scan on trips (cost=14.31..904.88 rows=977 width=34)
+ Recheck Cond: ((from_id = 4) AND (to_id = 2))
+ -> Bitmap Index Scan on index_trips_on_from_id_and_to (cost=0.00..14.06 rows=977 width=0)
+ Index Cond: ((from_id = 4) AND (to_id = 2))
+```
+
+для запроса:
+```ruby
+SELECT COUNT(*) FROM "trips" WHERE "trips"."from_id" = 4 AND "trips"."to_id" = 2
+```
+```
+Aggregate (cost=26.27..26.29 rows=1 width=8)
+ -> Index Only Scan using index_trips_on_from_id_and_to on trips (cost=0.29..23.83 rows=977 width=0)
+ Index Cond: ((from_id = 4) AND (to_id = 2))
+```
+
+Скорость закрузки страницы снизилась от 144ms до 290 ms
+Думаю на большом объеме данных результат будет более наглядным
+
+## Case 4
+PGHero
+
+Беру запрос который на первом месте в разделе Queries
+
+37% от общего времени
+```ruby
+SELECT "buses_services".* FROM "buses_services" WHERE "buses_services"."bus_id" IN ($1, $2,......)
+```
+
+Explain отдал
+```
+CREATE INDEX CONCURRENTLY ON buses_services (bus_id)
+Rows: 4534
+Row progression: 4534, 5
+
+Row estimates
+- bus_id (=): 5
+
+Existing indexes
+- id PRIMARY
+```
+
+Добавляю индекс по bus_id
+
+Скорость загрузки стариницы осталась примерно на том же уровне иногда опускаясь до 129 ms
+Хотя PGHero показывает по прежнему 37% от общего времени выполнение запроса на получение сервисов, решил остановиться.
+В бюджет почти уложился(хотел до 0.1 сек)
diff --git a/config/environments/development.rb b/config/environments/development.rb
index bc3f8142..5a6fd0a9 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,4 +1,15 @@
Rails.application.configure do
+=begin
+ config.after_initialize do
+ Bullet.enable = true
+ Bullet.alert = false
+ Bullet.bullet_logger = false
+ Bullet.console = false
+ Bullet.rails_logger = true
+ Bullet.add_footer = true
+ end
+=end
+
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded on
diff --git a/config/routes.rb b/config/routes.rb
index 0bbefa7a..ed223a52 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,6 @@
Rails.application.routes.draw do
+ mount PgHero::Engine, at: 'pghero'
+
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
- get "автобусы/:from/:to" => "trips#index"
+ get "автобусы/:from/:to" => "trips#index", as: :trips
end
diff --git a/db/migrate/20250305221527_added_composite_index_to_trips.rb b/db/migrate/20250305221527_added_composite_index_to_trips.rb
new file mode 100644
index 00000000..c6e5f65f
--- /dev/null
+++ b/db/migrate/20250305221527_added_composite_index_to_trips.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddedCompositeIndexToTrips < ActiveRecord::Migration[8.0]
+ disable_ddl_transaction!
+
+ def change
+ add_index :trips, [:from_id, :to_id], name: 'index_trips_on_from_id_and_to', algorithm: :concurrently
+ end
+end
diff --git a/db/migrate/20250306175758_add_index_to_buses_services.rb b/db/migrate/20250306175758_add_index_to_buses_services.rb
new file mode 100644
index 00000000..dc892e54
--- /dev/null
+++ b/db/migrate/20250306175758_add_index_to_buses_services.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddIndexToBusesServices < ActiveRecord::Migration[8.0]
+ disable_ddl_transaction!
+
+ def change
+ add_index :buses_services, :bus_id, algorithm: :concurrently
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f6921e45..975faca7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2,18 +2,18 @@
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
-# Note that this schema.rb definition is the authoritative source for your
-# database schema. If you need to create the application database on another
-# system, you should be using db:schema:load, not running all the migrations
-# from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_03_30_193044) do
-
+ActiveRecord::Schema[8.0].define(version: 2025_03_06_175758) do
# These are extensions that must be enabled in order to support this database
- enable_extension "plpgsql"
+ enable_extension "pg_catalog.plpgsql"
+ enable_extension "pg_stat_statements"
create_table "buses", force: :cascade do |t|
t.string "number"
@@ -23,6 +23,7 @@
create_table "buses_services", force: :cascade do |t|
t.integer "bus_id"
t.integer "service_id"
+ t.index ["bus_id"], name: "index_buses_services_on_bus_id"
end
create_table "cities", force: :cascade do |t|
@@ -40,6 +41,6 @@
t.integer "duration_minutes"
t.integer "price_cents"
t.integer "bus_id"
+ t.index ["from_id", "to_id"], name: "index_trips_on_from_id_and_to"
end
-
end
diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake
index 540fe871..7525b841 100644
--- a/lib/tasks/utils.rake
+++ b/lib/tasks/utils.rake
@@ -1,34 +1,67 @@
-# Наивная загрузка данных из json-файла в БД
-# rake reload_json[fixtures/small.json]
-task :reload_json, [:file_name] => :environment do |_task, args|
- json = JSON.parse(File.read(args.file_name))
-
- ActiveRecord::Base.transaction do
- City.delete_all
- Bus.delete_all
- Service.delete_all
- Trip.delete_all
- ActiveRecord::Base.connection.execute('delete from buses_services;')
-
- json.each do |trip|
- from = City.find_or_create_by(name: trip['from'])
- to = City.find_or_create_by(name: trip['to'])
- services = []
- trip['bus']['services'].each do |service|
- s = Service.find_or_create_by(name: service)
- services << s
+# bin/rake 'utils:reload_json[fixtures/large.json]'
+
+namespace :utils do
+ task :reload_json, [:file_name] => :environment do |_task, args|
+ json = JSON.parse(File.read(args.file_name))
+
+ ActiveRecord::Base.transaction do
+ ActiveRecord::Base.connection.execute('TRUNCATE cities, buses, services, trips, buses_services RESTART IDENTITY;')
+
+ cities = Set.new
+ allowed_services = Set.new(::Service::SERVICES)
+ current_services = Set.new
+ buses_params = Set.new
+ trips_params = []
+ bus_service_relations = Set.new
+ allowed_buses_models = ::Bus::MODELS.to_h { |model| [model, true] }
+
+ json.each do |trip|
+ cities.add(trip['from'].delete(' '))
+ cities.add(trip['to'].delete(' '))
+ trip['bus']['services'].each { |service| current_services.add(service) }
+ buses_params.add(trip['bus']['number'] => trip['bus']['model'])
end
- bus = Bus.find_or_create_by(number: trip['bus']['number'])
- bus.update(model: trip['bus']['model'], services: services)
-
- Trip.create!(
- from: from,
- to: to,
- bus: bus,
- start_time: trip['start_time'],
- duration_minutes: trip['duration_minutes'],
- price_cents: trip['price_cents'],
+
+ City.insert_all(cities.map { |city| { name: city } })
+ city_ids = City.pluck(:name, :id).to_h
+
+ valid_services = allowed_services & current_services
+ Service.insert_all(valid_services.map { |service| { name: service } })
+ service_ids = Service.pluck(:name, :id).to_h
+
+ Bus.insert_all(
+ buses_params.map do |params|
+ key, value = params.first
+ { number: key, model: allowed_buses_models[value] ? value : nil }
+ end
)
+ bus_ids = Bus.pluck(:number, :id).to_h
+
+ json.each do |trip|
+ from_id = city_ids[trip['from'].delete(' ')]
+ to_id = city_ids[trip['to'].delete(' ')]
+ bus_id = bus_ids[trip['bus']['number']]
+
+ trip['bus']['services'].each do |service|
+ service_id = service_ids[service]
+ bus_service_relations.add([bus_id, service_id]) if service_id
+ end
+
+ trips_params << {
+ from_id: from_id,
+ to_id: to_id,
+ bus_id: bus_id,
+ start_time: trip['start_time'],
+ duration_minutes: trip['duration_minutes'],
+ price_cents: trip['price_cents']
+ }
+ end
+
+ Trip.insert_all(trips_params)
+
+ values = bus_service_relations.map { |bus_id, service_id| "(#{bus_id}, #{service_id})" }.join(', ')
+
+ ActiveRecord::Base.connection.execute("INSERT INTO buses_services (bus_id, service_id) VALUES #{values}")
end
end
end
diff --git a/spec/feature/trips_index_spec.rb b/spec/feature/trips_index_spec.rb
new file mode 100644
index 00000000..f4ff5921
--- /dev/null
+++ b/spec/feature/trips_index_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Trips Index', type: :feature do
+ describe 'GET trips#index' do
+ subject(:make_request) { visit(trips_path(from: 'Самара', to: 'Москва'))}
+
+ before(:all) do
+ system 'RAILS_ENV=test bin/rake utils:reload_json[fixtures/large.json]'
+ end
+
+ after(:all) do
+ ActiveRecord::Base.connection.execute('TRUNCATE cities, buses, services, trips, buses_services RESTART IDENTITY;')
+ end
+
+ context 'when all content present' do
+ it 'displays trip details', :aggregate_failures do
+ make_request
+ expect(page).to have_content('Автобусы Самара – Москва')
+ expect(page).to have_content('В расписании 1004 рейсов')
+ end
+ end
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 00000000..6335579e
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,80 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'spec_helper'
+require 'capybara/rspec'
+
+ENV['RAILS_ENV'] ||= 'test'
+
+require_relative '../config/environment'
+# Prevent database truncation if the environment is production
+abort('The Rails environment is running in production mode!') if Rails.env.production?
+# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file
+# that will avoid rails generators crashing because migrations haven't been run yet
+# return unless Rails.env.test?
+require 'rspec/rails'
+# Add additional requires below this line. Rails is not loaded until this point!
+
+# Requires supporting ruby files with custom matchers and macros, etc, in
+# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
+# run as spec files by default. This means that files in spec/support that end
+# in _spec.rb will both be required and run as specs, causing the specs to be
+# run twice. It is recommended that you do not name files matching this glob to
+# end with _spec.rb. You can configure this pattern with the --pattern
+# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
+#
+# The following line is provided for convenience purposes. It has the downside
+# of increasing the boot-up time by auto-requiring all files in the support
+# directory. Alternatively, in the individual `*_spec.rb` files, manually
+# require only the support files necessary.
+#
+# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
+
+# Checks for pending migrations and applies them before tests are run.
+# If you are not using ActiveRecord, you can remove these lines.
+begin
+ ActiveRecord::Migration.maintain_test_schema!
+rescue ActiveRecord::PendingMigrationError => e
+ abort e.to_s.strip
+end
+
+Capybara.configure do |config|
+ config.default_driver = :rack_test
+end
+
+RSpec.configure do |config|
+ config.include RSpec::Benchmark::Matchers
+
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ config.fixture_paths = [
+ Rails.root.join('spec/fixtures')
+ ]
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, remove the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = true
+
+ # You can uncomment this line to turn off ActiveRecord support entirely.
+ # config.use_active_record = false
+
+ # RSpec Rails uses metadata to mix in different behaviours to your tests,
+ # for example enabling you to call `get` and `post` in request specs. e.g.:
+ #
+ # RSpec.describe UsersController, type: :request do
+ # # ...
+ # end
+ #
+ # The different available types are documented in the features, such as in
+ # https://rspec.info/features/7-1/rspec-rails
+ #
+ # You can also this infer these behaviours automatically by location, e.g.
+ # /spec/models would pull in the same behaviour as `type: :model` but this
+ # behaviour is considered legacy and will be removed in a future version.
+ #
+ # To enable this behaviour uncomment the line below.
+ # config.infer_spec_type_from_file_location!
+
+ # Filter lines from Rails gems in backtraces.
+ config.filter_rails_from_backtrace!
+ # arbitrary gems may also be filtered via:
+ # config.filter_gems_from_backtrace("gem name")
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 00000000..327b58ea
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,94 @@
+# This file was generated by the `rails generate rspec:install` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+# The settings below are suggested to provide a good initial experience
+# with RSpec, but feel free to customize to your heart's content.
+=begin
+ # This allows you to limit a spec run to individual examples or groups
+ # you care about by tagging them with `:focus` metadata. When nothing
+ # is tagged with `:focus`, all examples get run. RSpec also provides
+ # aliases for `it`, `describe`, and `context` that include `:focus`
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ config.filter_run_when_matching :focus
+
+ # Allows RSpec to persist some state between runs in order to support
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # you configure your source control system to ignore this file.
+ config.example_status_persistence_file_path = "spec/examples.txt"
+
+ # Limits the available syntax to the non-monkey patched syntax that is
+ # recommended. For more details, see:
+ # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
+ config.disable_monkey_patching!
+
+ # Many RSpec users commonly either run the entire suite or an individual
+ # file, and it's useful to allow more verbose output when running an
+ # individual spec file.
+ if config.files_to_run.one?
+ # Use the documentation formatter for detailed output,
+ # unless a formatter has already been configured
+ # (e.g. via a command-line flag).
+ config.default_formatter = "doc"
+ end
+
+ # Print the 10 slowest examples and example groups at the
+ # end of the spec run, to help surface which specs are running
+ # particularly slow.
+ config.profile_examples = 10
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = :random
+
+ # Seed global randomization in this process using the `--seed` CLI option.
+ # Setting this allows you to use `--seed` to deterministically reproduce
+ # test failures related to randomization by passing the same `--seed` value
+ # as the one that triggered the failure.
+ Kernel.srand config.seed
+=end
+end
diff --git a/spec/task/utils_performace_spec.rb b/spec/task/utils_performace_spec.rb
new file mode 100644
index 00000000..74e5a8a7
--- /dev/null
+++ b/spec/task/utils_performace_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec.describe 'UtilsTaskPerformance' do
+ describe '.reload_json' do
+ context 'with file contained 100K trips' do
+ it 'performs under 10 seconds' do
+ expect { system 'bin/rake utils:reload_json[fixtures/large.json]' }.to perform_under(10).sec
+ end
+ end
+ end
+end