diff --git a/.gitignore b/.gitignore
index 59c74047..2eec9120 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
/tmp
/log
/public
+/.idea
\ No newline at end of file
diff --git a/.rspec b/.rspec
new file mode 100644
index 00000000..c99d2e73
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--require spec_helper
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 00000000..89c7595f
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,182 @@
+plugins:
+ - rubocop-minitest
+ - rubocop-performance
+ - rubocop-rspec
+ - rubocop-rails
+
+AllCops:
+ SuggestExtensions: false
+ NewCops: enable
+ TargetRubyVersion: 3.4
+ Exclude:
+ - 'vendor/**/*'
+ - 'db/schema.rb'
+ - 'db/migrate/*'
+ - 'bin/*'
+ - 'node_modules/**/*'
+ - 'tmp/**/*'
+ - 'log/**/*'
+
+# ОСНОВНЫЕ СТИЛЕВЫЕ ПРАВИЛА
+
+# Предпочитаем ДВОЙНЫЕ кавычки
+Style/StringLiterals:
+ Enabled: true
+ EnforcedStyle: double_quotes
+
+Style/StringLiteralsInInterpolation:
+ Enabled: true
+ EnforcedStyle: double_quotes
+
+# Отключаем требование документации
+Style/Documentation:
+ Enabled: false
+
+# Разрешаем не-ASCII комментарии (русские комментарии)
+Style/AsciiComments:
+ Enabled: false
+
+# МЕТРИКИ И ДЛИНЫ
+
+Layout/LineLength:
+ Max: 200
+ AllowURI: true
+ AllowHeredoc: true
+
+Metrics/MethodLength:
+ Max: 100
+ CountAsOne: ['array', 'hash', 'heredoc']
+
+Metrics/ClassLength:
+ Max: 200
+ CountAsOne: ['array', 'hash', 'heredoc']
+
+Metrics/ModuleLength:
+ Max: 200
+
+Metrics/BlockLength:
+ Max: 100
+ CountAsOne: ['array', 'hash', 'heredoc']
+ Exclude:
+ - 'spec/**/*'
+ - 'config/routes.rb'
+
+Metrics/AbcSize:
+ Max: 20
+
+Metrics/CyclomaticComplexity:
+ Max: 15
+
+Metrics/PerceivedComplexity:
+ Max: 15
+
+# LAYOUT И ФОРМАТИРОВАНИЕ
+
+Layout/EmptyLinesAroundAttributeAccessor:
+ Enabled: true
+
+Layout/SpaceAroundMethodCallOperator:
+ Enabled: true
+
+Layout/MultilineMethodCallIndentation:
+ Enabled: true
+ EnforcedStyle: indented
+
+# RAILS-СПЕЦИФИЧНЫЕ ПРАВИЛА
+
+Rails/FilePath:
+ Enabled: false
+
+Rails/UnknownEnv:
+ Environments:
+ - production
+ - development
+ - test
+
+Rails/SkipsModelValidations:
+ Enabled: false # Для bulk import операций
+
+# RSPEC-СПЕЦИФИЧНЫЕ ПРАВИЛА
+
+RSpec/ExampleLength:
+ Max: 20
+
+RSpec/MultipleExpectations:
+ Max: 5
+
+RSpec/NestedGroups:
+ Max: 4
+
+RSpec/DescribeClass:
+ Enabled: false
+
+# PERFORMANCE ПРАВИЛА
+
+Performance/StringReplacement:
+ Enabled: true
+
+Performance/RedundantBlockCall:
+ Enabled: true
+
+Performance/StringInclude:
+ Enabled: true
+
+# БЕЗОПАСНОСТЬ
+
+Security/Open:
+ Enabled: true
+
+Security/YAMLLoad:
+ Enabled: true
+
+# ИМЕНОВАНИЕ
+
+Naming/PredicatePrefix:
+ Enabled: true
+ AllowedMethods:
+ - is_a?
+ - has_key?
+
+Naming/MemoizedInstanceVariableName:
+ Enabled: false
+
+# STYLE ПРАВИЛА
+
+Style/HashEachMethods:
+ Enabled: true
+
+Style/HashTransformKeys:
+ Enabled: true
+
+Style/HashTransformValues:
+ Enabled: true
+
+Style/RedundantRegexpEscape:
+ Enabled: false # Может конфликтовать с некоторыми регексами
+
+Style/FrozenStringLiteralComment:
+ Enabled: true
+ EnforcedStyle: always
+
+Style/SymbolArray:
+ Enabled: true
+ EnforcedStyle: brackets
+
+Style/WordArray:
+ Enabled: true
+ EnforcedStyle: brackets
+ Exclude:
+ - 'app/models/**/*'
+
+# Разрешаем trailing запятые
+Style/TrailingCommaInArguments:
+ Enabled: true
+ EnforcedStyleForMultiline: comma
+
+Style/TrailingCommaInArrayLiteral:
+ Enabled: true
+ EnforcedStyleForMultiline: comma
+
+Style/TrailingCommaInHashLiteral:
+ Enabled: true
+ EnforcedStyleForMultiline: comma
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
new file mode 100644
index 00000000..7f609f6d
--- /dev/null
+++ b/.rubocop_todo.yml
@@ -0,0 +1,7 @@
+# This configuration was generated by
+# `rubocop --auto-gen-config`
+# and is meant to be a temporary file to ease migration to stricter rules.
+# Remove or comment out rules as you fix the violations.
+
+# Uncomment below if you have many violations and want to fix them gradually
+# inherit_from: .rubocop_todo.yml
diff --git a/.ruby-gemset b/.ruby-gemset
new file mode 100644
index 00000000..823585cd
--- /dev/null
+++ b/.ruby-gemset
@@ -0,0 +1 @@
+optimization
\ No newline at end of file
diff --git a/.ruby-version b/.ruby-version
index 47b322c9..f9892605 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.4.1
+3.4.4
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..3e1ce0e7
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,25 @@
+{
+ "ruby.useLanguageServer": true,
+ "ruby.format": "rubocop",
+ "ruby.lint": {
+ "rubocop": true,
+ "ruby": false
+ },
+ "ruby.rubocop.executePath": "bundle exec rubocop",
+ "ruby.rubocop.configFilePath": ".rubocop.yml",
+ "editor.formatOnSave": true,
+ "editor.formatOnPaste": true,
+ "files.trimTrailingWhitespace": true,
+ "files.insertFinalNewline": true,
+ "files.trimFinalNewlines": true,
+ "[ruby]": {
+ "editor.defaultFormatter": "rebornix.Ruby",
+ "editor.formatOnSave": true,
+ "editor.rulers": [200],
+ "editor.tabSize": 2,
+ "editor.insertSpaces": true
+ },
+ "emmet.includeLanguages": {
+ "erb": "html"
+ }
+}
diff --git a/Gemfile b/Gemfile
index 34074dfd..35f7f110 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,14 +1,54 @@
-source 'https://rubygems.org'
+# frozen_string_literal: true
+
+source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
-ruby file: '.ruby-version'
+ruby file: ".ruby-version"
+
+# Core Rails gems
+gem "bootsnap"
+gem "listen"
+gem "pg"
+gem "puma"
+gem "rails", "~> 8.0.1"
+gem "sprockets-rails"
-gem 'rails', '~> 8.0.1'
-gem 'pg'
-gem 'puma'
-gem 'listen'
-gem 'bootsnap'
-gem 'rack-mini-profiler'
+# Core application dependencies
+gem "activerecord-import"
+gem "oj"
+gem "ruby-progressbar"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
-gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+
+group :development do
+ # Database monitoring and optimization
+ gem "bullet"
+ gem "pghero"
+ gem "pg_query", ">= 2"
+ gem "strong_migrations"
+end
+
+group :development, :test do
+ # Code quality and linting
+ gem "rubocop", require: false
+ gem "rubocop-minitest", require: false
+ gem "rubocop-performance", require: false
+ gem "rubocop-rails", require: false
+ gem "rubocop-rspec", require: false
+
+ # Profiling tools
+ gem "memory_profiler", require: false
+ gem "pry"
+ gem "rack-mini-profiler", require: false
+ gem "ruby-prof", require: false
+ gem "stackprof", require: false
+ gem "test-prof", "~> 1.4.4"
+ gem "vernier", "~> 1.7", require: false
+end
+
+group :test do
+ gem "profile-viewer", require: false
+ gem "rspec-rails"
+ gem "rspec-sqlimit"
+end
diff --git a/Gemfile.lock b/Gemfile.lock
index a9ddd818..17a97d29 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -53,6 +53,8 @@ GEM
activemodel (= 8.0.1)
activesupport (= 8.0.1)
timeout (>= 0.4.0)
+ activerecord-import (2.2.0)
+ activerecord (>= 4.2)
activestorage (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
@@ -72,21 +74,30 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
+ ast (2.4.3)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
bootsnap (1.18.4)
msgpack (~> 1.2)
builder (3.3.0)
+ bullet (8.0.8)
+ activesupport (>= 3.0.0)
+ uniform_notifier (~> 1.11)
+ coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
crass (1.0.6)
date (3.4.1)
+ diff-lcs (1.6.2)
drb (2.2.1)
erubi (1.13.1)
- ffi (1.17.1-arm64-darwin)
+ ffi (1.17.2-arm64-darwin)
globalid (1.2.1)
activesupport (>= 6.1)
+ google-protobuf (4.32.0-arm64-darwin)
+ bigdecimal
+ rake (>= 13)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
@@ -94,6 +105,9 @@ GEM
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
+ json (2.13.2)
+ language_server-protocol (3.17.0.5)
+ lint_roller (1.1.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
@@ -107,6 +121,8 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
+ memory_profiler (1.1.0)
+ method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.25.4)
msgpack (1.8.0)
@@ -122,10 +138,30 @@ GEM
nio4r (2.7.4)
nokogiri (1.18.2-arm64-darwin)
racc (~> 1.4)
+ oj (3.16.11)
+ bigdecimal (>= 3.0)
+ ostruct (>= 0.2)
+ optparse (0.6.0)
+ ostruct (0.6.3)
+ parallel (1.27.0)
+ parser (3.3.9.0)
+ ast (~> 2.4.1)
+ racc
pg (1.5.9)
+ pg_query (6.1.0)
+ google-protobuf (>= 3.25.3)
+ pghero (3.7.0)
+ activerecord (>= 7.1)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
+ prism (1.4.0)
+ profile-viewer (0.0.5)
+ optparse
+ webrick
+ pry (0.15.2)
+ coderay (~> 1.1)
+ method_source (~> 1.0)
psych (5.2.3)
date
stringio
@@ -133,7 +169,7 @@ GEM
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.9)
- rack-mini-profiler (3.3.1)
+ rack-mini-profiler (4.0.1)
rack (>= 1.2.0)
rack-session (2.1.0)
base64 (>= 0.1.0)
@@ -171,22 +207,100 @@ GEM
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
+ rainbow (3.1.1)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rdoc (6.12.0)
psych (>= 4.0.0)
+ regexp_parser (2.11.2)
reline (0.6.0)
io-console (~> 0.5)
+ rspec (3.13.1)
+ rspec-core (~> 3.13.0)
+ rspec-expectations (~> 3.13.0)
+ rspec-mocks (~> 3.13.0)
+ rspec-core (3.13.5)
+ rspec-support (~> 3.13.0)
+ rspec-expectations (3.13.5)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-mocks (3.13.5)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-rails (8.0.2)
+ actionpack (>= 7.2)
+ activesupport (>= 7.2)
+ railties (>= 7.2)
+ rspec-core (~> 3.13)
+ rspec-expectations (~> 3.13)
+ rspec-mocks (~> 3.13)
+ rspec-support (~> 3.13)
+ rspec-sqlimit (1.0.0)
+ activerecord (>= 4.2.0)
+ rspec (~> 3.0)
+ rspec-support (3.13.4)
+ rubocop (1.79.2)
+ json (~> 2.3)
+ language_server-protocol (~> 3.17.0.2)
+ lint_roller (~> 1.1.0)
+ parallel (~> 1.10)
+ parser (>= 3.3.0.2)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 2.9.3, < 3.0)
+ rubocop-ast (>= 1.46.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 4.0)
+ rubocop-ast (1.46.0)
+ parser (>= 3.3.7.2)
+ prism (~> 1.4)
+ rubocop-minitest (0.38.1)
+ lint_roller (~> 1.1)
+ rubocop (>= 1.75.0, < 2.0)
+ rubocop-ast (>= 1.38.0, < 2.0)
+ rubocop-performance (1.25.0)
+ lint_roller (~> 1.1)
+ rubocop (>= 1.75.0, < 2.0)
+ rubocop-ast (>= 1.38.0, < 2.0)
+ rubocop-rails (2.33.3)
+ activesupport (>= 4.2.0)
+ lint_roller (~> 1.1)
+ rack (>= 1.1)
+ rubocop (>= 1.75.0, < 2.0)
+ rubocop-ast (>= 1.44.0, < 2.0)
+ rubocop-rspec (3.6.0)
+ lint_roller (~> 1.1)
+ rubocop (~> 1.72, >= 1.72.1)
+ ruby-prof (1.7.2)
+ base64
+ ruby-progressbar (1.13.0)
securerandom (0.4.1)
+ sprockets (4.2.2)
+ concurrent-ruby (~> 1.0)
+ logger
+ rack (>= 2.2.4, < 4)
+ sprockets-rails (3.5.2)
+ actionpack (>= 6.1)
+ activesupport (>= 6.1)
+ sprockets (>= 3.0.0)
+ stackprof (0.2.27)
stringio (3.1.2)
+ strong_migrations (2.5.0)
+ activerecord (>= 7.1)
+ test-prof (1.4.4)
thor (1.3.2)
timeout (0.4.3)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
+ unicode-display_width (3.1.5)
+ unicode-emoji (~> 4.0, >= 4.0.4)
+ unicode-emoji (4.0.4)
+ uniform_notifier (1.17.0)
uri (1.0.2)
useragent (0.16.11)
+ vernier (1.8.0)
+ webrick (1.9.1)
websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0)
@@ -197,16 +311,38 @@ PLATFORMS
arm64-darwin-24
DEPENDENCIES
+ activerecord-import
bootsnap
+ bullet
listen
+ memory_profiler
+ oj
pg
+ pg_query (>= 2)
+ pghero
+ profile-viewer
+ pry
puma
rack-mini-profiler
rails (~> 8.0.1)
+ rspec-rails
+ rspec-sqlimit
+ rubocop
+ rubocop-minitest
+ rubocop-performance
+ rubocop-rails
+ rubocop-rspec
+ ruby-prof
+ ruby-progressbar
+ sprockets-rails
+ stackprof
+ strong_migrations
+ test-prof (~> 1.4.4)
tzinfo-data
+ vernier (~> 1.7)
RUBY VERSION
- ruby 3.4.1p0
+ ruby 3.4.4p34
BUNDLED WITH
2.6.2
diff --git a/Rakefile b/Rakefile
index e85f9139..d2a78aa2 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-require_relative 'config/application'
+require_relative "config/application"
Rails.application.load_tasks
diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb
index d6726972..9aec2305 100644
--- a/app/channels/application_cable/channel.rb
+++ b/app/channels/application_cable/channel.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
index 0ff5442f..8d6c2a1b 100644
--- a/app/channels/application_cable/connection.rb
+++ b/app/channels/application_cable/connection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 09705d12..7944f9f9 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class ApplicationController < ActionController::Base
end
diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb
index acb38be2..1490d713 100644
--- a/app/controllers/trips_controller.rb
+++ b/app/controllers/trips_controller.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class TripsController < ApplicationController
def index
- @from = City.find_by_name!(params[:from])
- @to = City.find_by_name!(params[:to])
+ @from = City.find_by!(name: params[:from])
+ @to = City.find_by!(name: params[:to])
@trips = Trip.where(from: @from, to: @to).order(:start_time)
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index de6be794..15b06f0f 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
module ApplicationHelper
end
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
index a009ace5..d92ffddc 100644
--- a/app/jobs/application_job.rb
+++ b/app/jobs/application_job.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class ApplicationJob < ActiveJob::Base
end
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 286b2239..5cc63a0c 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class ApplicationMailer < ActionMailer::Base
- default from: 'from@example.com'
- layout 'mailer'
+ default from: "from@example.com"
+ layout "mailer"
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 10a4cba8..71fbba5b 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
diff --git a/app/models/bus.rb b/app/models/bus.rb
index 1dcc54cb..7b96d9b9 100644
--- a/app/models/bus.rb
+++ b/app/models/bus.rb
@@ -1,16 +1,7 @@
+# frozen_string_literal: true
+
class Bus < ApplicationRecord
- MODELS = [
- 'Икарус',
- 'Мерседес',
- 'Сканиа',
- 'Буханка',
- 'УАЗ',
- 'Спринтер',
- 'ГАЗ',
- 'ПАЗ',
- 'Вольво',
- 'Газель',
- ].freeze
+ MODELS = %w[Икарус Мерседес Сканиа Буханка УАЗ Спринтер ГАЗ ПАЗ Вольво Газель].freeze
has_many :trips
has_and_belongs_to_many :services, join_table: :buses_services
diff --git a/app/models/city.rb b/app/models/city.rb
index 19ec7f36..fd2ef235 100644
--- a/app/models/city.rb
+++ b/app/models/city.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
class City < ApplicationRecord
validates :name, presence: true, uniqueness: true
validate :name_has_no_spaces
def name_has_no_spaces
- errors.add(:name, "has spaces") if name.include?(' ')
+ errors.add(:name, "has spaces") if name.include?(" ")
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 9cbb2a32..707b4993 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
class Service < ApplicationRecord
SERVICES = [
- 'WiFi',
- 'Туалет',
- 'Работающий туалет',
- 'Ремни безопасности',
- 'Кондиционер общий',
- 'Кондиционер Индивидуальный',
- 'Телевизор общий',
- 'Телевизор индивидуальный',
- 'Стюардесса',
- 'Можно не печатать билет',
+ "WiFi",
+ "Туалет",
+ "Работающий туалет",
+ "Ремни безопасности",
+ "Кондиционер общий",
+ "Кондиционер Индивидуальный",
+ "Телевизор общий",
+ "Телевизор индивидуальный",
+ "Стюардесса",
+ "Можно не печатать билет",
].freeze
has_and_belongs_to_many :buses, join_table: :buses_services
diff --git a/app/models/trip.rb b/app/models/trip.rb
index 9d63dfff..d9369f9b 100644
--- a/app/models/trip.rb
+++ b/app/models/trip.rb
@@ -1,15 +1,13 @@
+# frozen_string_literal: true
+
class Trip < ApplicationRecord
HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/
- belongs_to :from, class_name: 'City'
- belongs_to :to, class_name: 'City'
+ belongs_to :from, class_name: "City"
+ belongs_to :to, class_name: "City"
belongs_to :bus
- validates :from, presence: true
- validates :to, presence: true
- validates :bus, presence: true
-
- validates :start_time, format: { with: HHMM_REGEXP, message: 'Invalid time' }
+ validates :start_time, format: { with: HHMM_REGEXP, message: "Invalid time" }
validates :duration_minutes, presence: true
validates :duration_minutes, numericality: { greater_than: 0 }
validates :price_cents, presence: true
diff --git a/app/services/json_trip_loader.rb b/app/services/json_trip_loader.rb
new file mode 100644
index 00000000..6ce55453
--- /dev/null
+++ b/app/services/json_trip_loader.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require "benchmark"
+require "ruby-progressbar"
+
+class JsonTripLoader
+ def load!(path)
+ json = JSON.parse(File.read(path))
+
+ old_logger = ActiveRecord::Base.logger
+ old_verbose =
+ ActiveRecord::Base.respond_to?(:verbose_query_logs) ? ActiveRecord::Base.verbose_query_logs : nil
+
+ time = Benchmark.realtime do
+ # тихий режим
+ ActiveRecord::Base.logger = nil
+ ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=)
+
+ ActiveRecord::Base.transaction do
+ ActiveRecord::Base.connection.execute <<~SQL.squish
+ TRUNCATE TABLE
+ buses_services,
+ trips,
+ buses,
+ services,
+ cities
+ RESTART IDENTITY
+ CASCADE;
+ SQL
+
+ city_id_cache = {}
+ service_id_cache = {}
+ bus_cache = {}
+ trips = []
+
+ progress_bar = ProgressBar.create(
+ title: "Loading trips",
+ total: json.size,
+ format: "%t: |%B| %p%% %e",
+ )
+
+ json.each do |trip|
+ from_name = trip["from"]
+ to_name = trip["to"]
+
+ from_id = city_id_cache[from_name] ||= City.find_or_create_by(name: from_name).id
+ to_id = city_id_cache[to_name] ||= City.find_or_create_by(name: to_name).id
+
+ service_ids = trip["bus"]["services"].map do |name|
+ service_id_cache[name] ||= Service.find_or_create_by(name:).id
+ end
+
+ number = trip["bus"]["number"]
+ model = trip["bus"]["model"]
+ bus = bus_cache[number] ||= Bus.find_or_create_by(number:)
+ bus.model = model if bus.model != model
+ bus.service_ids = service_ids if bus.service_ids != service_ids
+ bus.save if bus.changed?
+
+ # Используем массив - самый быстрый вариант
+ trips << [
+ from_id,
+ to_id,
+ bus.id,
+ trip["start_time"],
+ trip["duration_minutes"],
+ trip["price_cents"],
+ ]
+
+ progress_bar.increment
+ end
+
+ # Указываем колонки явно при импорте массивов
+ Trip.import(
+ [:from_id, :to_id, :bus_id, :start_time, :duration_minutes, :price_cents],
+ trips,
+ validate: false,
+ )
+ end
+ ensure
+ ActiveRecord::Base.logger = old_logger
+ ActiveRecord::Base.verbose_query_logs = old_verbose if ActiveRecord::Base.respond_to?(:verbose_query_logs=)
+ end
+
+ Rails.logger.debug { "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" }
+ end
+end
diff --git a/app/services/json_trip_loader_ai.rb b/app/services/json_trip_loader_ai.rb
new file mode 100644
index 00000000..43811240
--- /dev/null
+++ b/app/services/json_trip_loader_ai.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require "benchmark"
+require "ruby-progressbar"
+
+class JsonTripLoaderAi
+ BATCH_SIZE = 5_000
+
+ # Если нет модели для join-таблицы — объявим локально.
+ unless defined?(BusService)
+ class BusService < ApplicationRecord
+ self.table_name = "buses_services"
+ end
+ end
+
+ def load!(path)
+ json = JSON.parse(File.read(path))
+
+ old_logger = ActiveRecord::Base.logger
+ old_verbose =
+ ActiveRecord::Base.respond_to?(:verbose_query_logs) ? ActiveRecord::Base.verbose_query_logs : nil
+
+ time = Benchmark.realtime do
+ # Тихий режим логгера, чтобы не тратить время на форматирование логов
+ ActiveRecord::Base.logger = nil
+ ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=)
+
+ ActiveRecord::Base.transaction do
+ # Быстрый сброс
+ ActiveRecord::Base.connection.execute <<~SQL.squish
+ TRUNCATE TABLE
+ buses_services,
+ trips,
+ buses,
+ services,
+ cities
+ RESTART IDENTITY
+ CASCADE;
+ SQL
+
+ # 1) Собираем уникальные сущности из JSON
+ city_names = Set.new
+ service_names = Set.new
+ bus_number_to_model = {} # number => model
+ bus_number_to_service_names = Hash.new { |h, k| h[k] = Set.new }
+
+ json.each do |trip|
+ from_name = trip["from"]
+ to_name = trip["to"]
+ city_names << from_name
+ city_names << to_name
+
+ number = trip["bus"]["number"]
+ model = trip["bus"]["model"]
+ bus_number_to_model[number] = model
+
+ (trip["bus"]["services"] || []).each do |sname|
+ service_names << sname
+ bus_number_to_service_names[number] << sname
+ end
+ end
+
+ # 2) Вставляем города/сервисы одним махом (ignore duplicates)
+ if city_names.any?
+ City.import(
+ city_names.map { |n| City.new(name: n) },
+ on_duplicate_key_ignore: true,
+ validate: false,
+ )
+ end
+
+ if service_names.any?
+ Service.import(
+ service_names.map { |n| Service.new(name: n) },
+ on_duplicate_key_ignore: true,
+ validate: false,
+ )
+ end
+
+ # 3) Получаем маппинги name -> id одним запросом на каждую таблицу
+ city_id_by_name = City.where(name: city_names.to_a).pluck(:name, :id).to_h
+ service_id_by_name = Service.where(name: service_names.to_a).pluck(:name, :id).to_h
+
+ # 4) Upsert автобусов (обновляем model при конфликте по number)
+ if bus_number_to_model.any?
+ # Через activerecord-import с on_duplicate_key_update
+ Bus.import(
+ bus_number_to_model.map { |number, model| Bus.new(number: number, model: model) },
+ on_duplicate_key_update: [:model],
+ validate: false,
+ )
+ end
+
+ # 5) Получаем маппинг автобусов number -> id одним запросом
+ bus_id_by_number = Bus.where(number: bus_number_to_model.keys).pluck(:number, :id).to_h
+
+ # 6) Пакетно вставляем связи bus↔service в join-таблицу (ignore duplicates)
+ join_rows = []
+ bus_number_to_service_names.each do |number, sset|
+ bus_id = bus_id_by_number[number]
+ sset.each do |sname|
+ sid = service_id_by_name[sname]
+ next unless bus_id && sid
+
+ join_rows << { bus_id: bus_id, service_id: sid }
+ end
+ end
+
+ if join_rows.any?
+ # Через модель BusService — activerecord-import игнорирует дубликаты
+ BusService.import(
+ join_rows,
+ on_duplicate_key_ignore: true,
+ validate: false,
+ )
+ end
+
+ # 7) Формируем и заливаем trips батчами
+ trips_buffer = []
+ trips_buffer.reserve([json.size, BATCH_SIZE].min) if trips_buffer.respond_to?(:reserve)
+
+ flush_trips = lambda do
+ next if trips_buffer.empty?
+
+ Trip.import(trips_buffer, validate: true, batch_size: BATCH_SIZE)
+ trips_buffer.clear
+ end
+
+ # прогрессбар на количество записей
+ progress_bar = ProgressBar.create(
+ title: "Loading trips",
+ total: json.size,
+ format: "%t: |%B| %p%% %e",
+ )
+
+ json.each do |t|
+ trips_buffer << Trip.new(
+ from_id: city_id_by_name[t["from"]],
+ to_id: city_id_by_name[t["to"]],
+ bus_id: bus_id_by_number[t["bus"]["number"]],
+ start_time: t["start_time"],
+ duration_minutes: t["duration_minutes"],
+ price_cents: t["price_cents"],
+ )
+ flush_trips.call if trips_buffer.size >= BATCH_SIZE
+
+ progress_bar.increment
+ end
+ flush_trips.call
+ end
+ ensure
+ ActiveRecord::Base.logger = old_logger
+ ActiveRecord::Base.verbose_query_logs = old_verbose if ActiveRecord::Base.respond_to?(:verbose_query_logs=)
+ end
+
+ Rails.logger.debug { "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" }
+ end
+end
diff --git a/app/services/json_trip_loader_stream.rb b/app/services/json_trip_loader_stream.rb
new file mode 100644
index 00000000..f652abe2
--- /dev/null
+++ b/app/services/json_trip_loader_stream.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+require "benchmark"
+require "ruby-progressbar"
+require "oj" # Быстрый JSON парсер
+
+class JsonTripLoaderStream
+ BATCH_SIZE = 5000
+
+ def load!(path)
+ old_logger = ActiveRecord::Base.logger
+ old_verbose = ActiveRecord::Base.respond_to?(:verbose_query_logs) ? ActiveRecord::Base.verbose_query_logs : nil
+
+ time = Benchmark.realtime do
+ ActiveRecord::Base.logger = nil
+ ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=)
+
+ ActiveRecord::Base.transaction do
+ clear_database
+
+ # Кэши для уникальных значений
+ @city_cache = {}
+ @service_cache = {}
+ @bus_cache = {}
+ @trips_batch = []
+
+ # Получаем размер файла для прогресс-бара (приблизительно)
+ file_size = File.size(path)
+ progress_bar = ProgressBar.create(
+ title: "Loading trips",
+ total: file_size,
+ format: "%t: |%B| %p%% %e",
+ )
+
+ # Потоковое чтение и парсинг
+ stream_parse_file(path, progress_bar)
+
+ # Импортируем последний батч
+ import_trips_batch if @trips_batch.any?
+ end
+ ensure
+ ActiveRecord::Base.logger = old_logger
+ ActiveRecord::Base.verbose_query_logs = old_verbose if ActiveRecord::Base.respond_to?(:verbose_query_logs=)
+ end
+
+ Rails.logger.debug { "JsonTripLoader.load!: #{time.round(3)}s" }
+ end
+
+ private
+
+ def clear_database
+ ActiveRecord::Base.connection.execute <<~SQL.squish
+ TRUNCATE TABLE
+ buses_services,
+ trips,
+ buses,
+ services,
+ cities
+ RESTART IDENTITY
+ CASCADE;
+ SQL
+ end
+
+ def stream_parse_file(path, progress_bar)
+ File.open(path, "r") do |file|
+ nesting = 0
+ str = +""
+ bytes_read = 0
+ in_array = false
+
+ until file.eof?
+ ch = file.read(1)
+ bytes_read += 1
+
+ # Обновляем прогресс каждые 10KB
+ progress_bar.progress = bytes_read if (bytes_read % 10_000).zero?
+
+ case ch
+ when "["
+ in_array = true if nesting.zero?
+ str << ch if nesting.positive?
+ when "]"
+ # Конец корневого массива
+ if nesting.zero?
+ in_array = false
+ next
+ end
+ str << ch
+ when "{"
+ nesting += 1
+ str << ch
+ when "}"
+ str << ch
+ nesting -= 1
+
+ # Если закончился объект верхнего уровня (trip)
+ if nesting.zero? && in_array
+ begin
+ trip = Oj.load(str)
+ process_trip(trip)
+ rescue Oj::ParseError => e
+ Rails.logger.debug { "Parse error: #{e.message}" }
+ Rails.logger.debug { "String: #{str[0..100]}..." } if str.length > 100
+ raise
+ end
+ str = +""
+ end
+ when ","
+ # Запятая между объектами - игнорируем если на верхнем уровне
+ next if nesting.zero?
+
+ str << ch
+ when /\s/
+ # Пробелы - игнорируем если на верхнем уровне
+ next if nesting.zero?
+
+ str << ch
+ else
+ str << ch if nesting.positive?
+ end
+ end
+
+ progress_bar.finish
+ end
+ end
+
+ def process_trip(trip)
+ from_id = get_or_create_city_id(trip["from"])
+ to_id = get_or_create_city_id(trip["to"])
+ bus_id = get_or_create_bus_id(trip["bus"])
+
+ @trips_batch << [
+ from_id,
+ to_id,
+ bus_id,
+ trip["start_time"],
+ trip["duration_minutes"],
+ trip["price_cents"],
+ ]
+
+ # Импортируем батч когда набрался нужный размер
+ return unless @trips_batch.size >= BATCH_SIZE
+
+ import_trips_batch
+ @trips_batch.clear
+ end
+
+ def get_or_create_city_id(name)
+ @city_cache[name] ||= City.find_or_create_by(name: name).id
+ end
+
+ def get_or_create_bus_id(bus_data)
+ number = bus_data["number"]
+
+ @bus_cache[number] ||= begin
+ bus = Bus.find_or_create_by(number: number)
+ bus.model = bus_data["model"] if bus.model != bus_data["model"]
+
+ service_ids = bus_data["services"].map { |name| get_or_create_service_id(name) }
+ bus.service_ids = service_ids if bus.service_ids != service_ids
+
+ bus.save if bus.changed?
+ bus.id
+ end
+ end
+
+ def get_or_create_service_id(name)
+ @service_cache[name] ||= Service.find_or_create_by(name: name).id
+ end
+
+ def import_trips_batch
+ Trip.import(
+ [:from_id, :to_id, :bus_id, :start_time, :duration_minutes, :price_cents],
+ @trips_batch,
+ validate: false,
+ )
+ end
+end
diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb
index a60bce41..79bc535a 100644
--- a/app/views/trips/index.html.erb
+++ b/app/views/trips/index.html.erb
@@ -5,7 +5,7 @@
<%= "В расписании #{@trips.count} рейсов" %>
-<% @trips.each do |trip| %>
+<% @trips.preload([bus: [:services]]).each do |trip| %>
<%= render "trip", trip: trip %>
<% if trip.bus.services.present? %>
diff --git a/bin/rails b/bin/rails
index 5badb2fd..7a8ff81e 100755
--- a/bin/rails
+++ b/bin/rails
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
begin
- load File.expand_path('../spring', __FILE__)
+ load File.expand_path('spring', __dir__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
diff --git a/bin/rake b/bin/rake
index d87d5f57..0ba8c48c 100755
--- a/bin/rake
+++ b/bin/rake
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
begin
- load File.expand_path('../spring', __FILE__)
+ load File.expand_path('spring', __dir__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
diff --git a/bin/spring b/bin/spring
index fb2ec2eb..991bd4ef 100755
--- a/bin/spring
+++ b/bin/spring
@@ -8,7 +8,7 @@ unless defined?(Spring)
require 'bundler'
lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
- spring = lockfile.specs.detect { |spec| spec.name == "spring" }
+ spring = lockfile.specs.detect { |spec| spec.name == 'spring' }
if spring
Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
gem 'spring', spring.version
diff --git a/bin/yarn b/bin/yarn
index 460dd565..d3627c34 100755
--- a/bin/yarn
+++ b/bin/yarn
@@ -1,11 +1,9 @@
#!/usr/bin/env ruby
APP_ROOT = File.expand_path('..', __dir__)
Dir.chdir(APP_ROOT) do
- begin
- exec "yarnpkg", *ARGV
- rescue Errno::ENOENT
- $stderr.puts "Yarn executable was not detected in the system."
- $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
- exit 1
- end
+ exec 'yarnpkg', *ARGV
+rescue Errno::ENOENT
+ warn 'Yarn executable was not detected in the system.'
+ warn 'Download Yarn at https://yarnpkg.com/en/docs/install'
+ exit 1
end
diff --git a/case-study.md b/case-study.md
new file mode 100644
index 00000000..fb01c28d
--- /dev/null
+++ b/case-study.md
@@ -0,0 +1,55 @@
+## Шаги выстраивания оптимизации
+### Начало
+- установил гемы с настройками
+- запустил импорт на разных объемах, убедился в том, что все работает медленно
+- посмотрел на код и хотел на всикду порефакторить, но решил искать гемами точки роста
+
+### pghero
+- зашел в него и увидел сообщения о нехватке индексов
+- увидел много транзакций `SELECT`, `INSERT` c большим количеством запусков(Calls)
+
+### rspec-sqlimit
+- вынес импорт в отдельный сервис, чтобы проще было тестировать
+- создал тест для сервиса и увидел 14777 запроса при импорте файла `small.json`
+
+### stackprof
+- установил гем и написал скрипт для профилирования
+```
+==================================
+ Mode: wall(1000)
+ Samples: 4075 (0.02% miss rate)
+ GC: 271 (6.65%)
+==================================
+ TOTAL (pct) SAMPLES (pct) FRAME
+ 693 (17.0%) 693 (17.0%) String#sub
+ 493 (12.1%) 493 (12.1%) PG::Connection#exec_params
+ 462 (11.3%) 462 (11.3%) PG::Connection#exec_prepared
+ 360 (8.8%) 360 (8.8%) Regexp#match?
+ 305 (7.5%) 305 (7.5%) Thread::Backtrace::Location#to_s
+ 199 (4.9%) 199 (4.9%) (sweeping)
+ 130 (3.2%) 130 (3.2%) PG::Connection#exec
+ 113 (2.8%) 113 (2.8%) String#include?
+ 1627 (39.9%) 76 (1.9%) ActiveSupport::BacktraceCleaner#clean_frame
+ 72 (1.8%) 72 (1.8%) (marking)
+ 66 (1.6%) 66 (1.6%) #.[]
+ 58 (1.4%) 58 (1.4%) IO#write
+ 3744 (91.9%) 48 (1.2%) Array#each
+ 33 (0.8%) 33 (0.8%) IO#stat
+ 1745 (42.8%) 29 (0.7%) Thread.each_caller_location
+ 301 (7.4%) 27 (0.7%) Array#any?
+ 168 (4.1%) 25 (0.6%) Class#new
+ 1962 (48.1%) 18 (0.4%) ActiveRecord::LogSubscriber#sql
+ 18 (0.4%) 18 (0.4%) Process.clock_gettime
+ 14 (0.3%) 14 (0.3%) Regexp#===
+ 260 (6.4%) 14 (0.3%) Rails::BacktraceCleaner#initialize
+ 35 (0.9%) 11 (0.3%) Hash#fetch
+ 39 (1.0%) 10 (0.2%) Arel::Visitors::Visitor#visit
+ 17 (0.4%) 10 (0.2%) Array#inspect
+ 9 (0.2%) 9 (0.2%) block in redefine
+ 10 (0.2%) 8 (0.2%) ActiveSupport::IsolatedExecutionState.state
+ 18 (0.4%) 8 (0.2%) ActiveSupport::IsolatedExecutionState.[]
+ 700 (17.2%) 7 (0.2%) ActiveSupport::Callbacks::CallTemplate::ObjectCall#make_lambda
+ 7 (0.2%) 7 (0.2%) String#start_with?
+ 14 (0.3%) 7 (0.2%) ActiveSupport::LogSubscriber#color
+```
+
diff --git a/config.ru b/config.ru
index f7ba0b52..bff88d60 100644
--- a/config.ru
+++ b/config.ru
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
# This file is used by Rack-based servers to start the application.
-require_relative 'config/environment'
+require_relative "config/environment"
run Rails.application
diff --git a/config/application.rb b/config/application.rb
index f714e62d..50f4cf7d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,6 +1,8 @@
-require_relative 'boot'
+# frozen_string_literal: true
-require 'rails/all'
+require_relative "boot"
+
+require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
diff --git a/config/boot.rb b/config/boot.rb
index b9e460ce..aef6d031 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,4 +1,6 @@
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+# frozen_string_literal: true
-require 'bundler/setup' # Set up gems listed in the Gemfile.
-require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+require "bundler/setup" # Set up gems listed in the Gemfile.
+require "bootsnap/setup" # Speed up boot time by caching expensive operations.
diff --git a/config/environment.rb b/config/environment.rb
index 426333bb..7df99e89 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
# Load the Rails application.
-require_relative 'application'
+require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index bc3f8142..24e46a99 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -14,12 +16,12 @@
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
- if Rails.root.join('tmp', 'caching-dev.txt').exist?
+ if Rails.root.join("tmp", "caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.cache_store = :memory_store
config.public_file_server.headers = {
- 'Cache-Control' => "public, max-age=#{2.days.to_i}"
+ "Cache-Control" => "public, max-age=#{2.days.to_i}",
}
else
config.action_controller.perform_caching = false
@@ -58,4 +60,44 @@
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+
+ config.after_initialize do
+ Bullet.enable = true # Включает работу Bullet
+ Bullet.alert = false # Показывает всплывающее JS-окно в браузере при обнаружении проблемы
+ Bullet.bullet_logger = true # Записывает предупреждения в log/bullet.log
+ Bullet.console = true # Выводит предупреждения в консоль браузера (console.log)
+ Bullet.rails_logger = true # Записывает предупреждения в Rails логи
+ Bullet.add_footer = true # Добавляет предупреждение внизу HTML-страницы
+
+ # Bullet.sentry = true # Отправка уведомлений в Sentry
+ # Bullet.xmpp = { # Отправка уведомлений через XMPP (Jabber)
+ # :account => 'bullets_account@jabber.org', # Аккаунт для отправки
+ # :password => 'bullets_password_for_jabber', # Пароль аккаунта
+ # :receiver => 'your_account@jabber.org', # Кому отправлять
+ # :show_online_status => true # Показывать статус онлайн
+ # }
+ # Bullet.honeybadger = true # Отправка уведомлений в Honeybadger
+ # Bullet.bugsnag = true # Отправка уведомлений в Bugsnag
+ # Bullet.appsignal = true # Отправка уведомлений в AppSignal
+ # Bullet.airbrake = true # Отправка уведомлений в Airbrake
+ # Bullet.rollbar = true # Отправка уведомлений в Rollbar
+
+ Bullet.skip_html_injection = false # Если true — не будет встраивать HTML-предупреждения в ответ
+ Bullet.skip_http_headers = false # Если true — не будет добавлять HTTP-заголовки с предупреждениями
+
+ # Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ]
+ # Отображать только указанные библиотеки/модули в stacktrace
+
+ # Bullet.stacktrace_excludes = [ 'their_gem', 'their_middleware', ['my_file.rb', 'my_method'], ['my_file.rb', 16..20] ]
+ # Исключать указанные файлы, методы или диапазоны строк из stacktrace
+
+ # Bullet.slack = { webhook_url: 'http://some.slack.url', channel: '#default', username: 'notifier' }
+ # Отправка уведомлений в Slack через Webhook
+
+ # Bullet.opentelemetry = true # Отправка уведомлений в OpenTelemetry
+
+ Bullet.raise = false # Если true — будет выбрасывать исключение при проблемах (удобно в тестах)
+ Bullet.always_append_html_body = false # Если true — всегда вставлять HTML с предупреждениями даже без
+ Bullet.skip_user_in_notification = false # Если true — не добавлять инфо о пользователе в уведомления (актуально в multi-user)
+ end
end
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 613d8289..39f3b0e0 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -20,7 +22,7 @@
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
- config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+ config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
@@ -54,7 +56,7 @@
config.log_level = :debug
# Prepend all log lines with the following tags.
- config.log_tags = [ :request_id ]
+ config.log_tags = [:request_id]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
@@ -77,14 +79,14 @@
config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed.
- config.log_formatter = ::Logger::Formatter.new
+ config.log_formatter = Logger::Formatter.new
# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
- logger = ActiveSupport::Logger.new(STDOUT)
+ logger = ActiveSupport::Logger.new($stdout)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 0a38fd3c..f98f7779 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -15,7 +17,7 @@
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
- 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
+ "Cache-Control" => "public, max-age=#{1.hour.to_i}",
}
# Show full error reports and disable caching.
diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb
index 89d2efab..6d56e439 100644
--- a/config/initializers/application_controller_renderer.rb
+++ b/config/initializers/application_controller_renderer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# ActiveSupport::Reloader.to_prepare do
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 49a8c192..833a302e 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
index 59385cdf..4b63f289 100644
--- a/config/initializers/backtrace_silencers.rb
+++ b/config/initializers/backtrace_silencers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index d3bcaa5e..e3c96496 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
index 5a6a32d3..ee8dff9c 100644
--- a/config/initializers/cookies_serializer.rb
+++ b/config/initializers/cookies_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Specify a serializer for the signed and encrypted cookie jars.
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
index 4a994e1e..7a4f47b4 100644
--- a/config/initializers/filter_parameter_logging.rb
+++ b/config/initializers/filter_parameter_logging.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index ac033bf9..dc847422 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index dc189968..be6fedc5 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Add new mime types for use in respond_to blocks:
diff --git a/config/initializers/rack_mini_profiler.rb b/config/initializers/rack_mini_profiler.rb
new file mode 100644
index 00000000..27aa0b36
--- /dev/null
+++ b/config/initializers/rack_mini_profiler.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+unless Rails.env.test?
+ require "memory_profiler"
+ require "rack-mini-profiler"
+ require "stackprof"
+
+ Rack::MiniProfiler.config.authorization_mode = :allow_all
+ Rack::MiniProfiler.config.position = "left"
+
+ # Do not let rack-mini-profiler disable caching
+ Rack::MiniProfiler.config.disable_caching = false
+ Rack::MiniProfiler.config.show_total_sql_count = true
+
+ # Если initializer грузится поздно — инициализируем вручную
+ Rack::MiniProfilerRails.initialize!(Rails.application)
+
+ Rack::MiniProfiler.config.enable_advanced_debugging_tools = true
+ Rack::MiniProfiler.config.start_hidden = false
+ Rack::MiniProfiler.config.backtrace_threshold_ms = 2 # захватывать бэктрейсы у медленных блоков
+end
diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb
new file mode 100644
index 00000000..0b4b2dc4
--- /dev/null
+++ b/config/initializers/strong_migrations.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+# Mark existing migrations as safe
+StrongMigrations.start_after = 20_250_816_103_323
+
+# Set timeouts for migrations
+# If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user
+StrongMigrations.lock_timeout = 10.seconds
+StrongMigrations.statement_timeout = 1.hour
+
+# Analyze tables after indexes are added
+# Outdated statistics can sometimes hurt performance
+StrongMigrations.auto_analyze = true
+
+# Set the version of the production database
+# so the right checks are run in development
+# StrongMigrations.target_version = 10
+
+# Add custom checks
+# StrongMigrations.add_check do |method, args|
+# if method == :add_index && args[0].to_s == "users"
+# stop! "No more indexes on the users table"
+# end
+# end
+
+# Remove invalid indexes when rerunning migrations
+# StrongMigrations.remove_invalid_indexes = true
+
+# Make some operations safe by default
+# See https://github.com/ankane/strong_migrations#safe-by-default
+# StrongMigrations.safe_by_default = true
diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb
index bbfc3961..2f3c0db4 100644
--- a/config/initializers/wrap_parameters.rb
+++ b/config/initializers/wrap_parameters.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# This file contains settings for ActionController::ParamsWrapper which
diff --git a/config/puma.rb b/config/puma.rb
index a5eccf81..c938cdc3 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -1,19 +1,21 @@
+# frozen_string_literal: true
+
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
-threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
threads threads_count, threads_count
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
-port ENV.fetch("PORT") { 3000 }
+port ENV.fetch("PORT", 3000)
# Specifies the `environment` that Puma will run in.
#
-environment ENV.fetch("RAILS_ENV") { "development" }
+environment ENV.fetch("RAILS_ENV", "development")
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked webserver processes. If using threads and workers together
diff --git a/config/routes.rb b/config/routes.rb
index 0bbefa7a..3c406493 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,8 @@
+# frozen_string_literal: true
+
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get "автобусы/:from/:to" => "trips#index"
+
+ mount PgHero::Engine, at: "pghero"
end
diff --git a/config/spring.rb b/config/spring.rb
index 9fa7863f..12efcb38 100644
--- a/config/spring.rb
+++ b/config/spring.rb
@@ -1,6 +1,8 @@
-%w[
- .ruby-version
- .rbenv-vars
- tmp/restart.txt
- tmp/caching-dev.txt
+# frozen_string_literal: true
+
+[
+ ".ruby-version",
+ ".rbenv-vars",
+ "tmp/restart.txt",
+ "tmp/caching-dev.txt",
].each { |path| Spring.watch(path) }
diff --git a/db/migrate/20250816103837_create_pghero_space_stats.rb b/db/migrate/20250816103837_create_pghero_space_stats.rb
new file mode 100644
index 00000000..a9c10a3a
--- /dev/null
+++ b/db/migrate/20250816103837_create_pghero_space_stats.rb
@@ -0,0 +1,13 @@
+class CreatePgheroSpaceStats < ActiveRecord::Migration[8.0]
+ def change
+ create_table :pghero_space_stats do |t|
+ t.text :database
+ t.text :schema
+ t.text :relation
+ t.integer :size, limit: 8
+ t.timestamp :captured_at
+ end
+
+ add_index :pghero_space_stats, %i[database captured_at]
+ end
+end
diff --git a/db/migrate/20250816103847_create_pghero_query_stats.rb b/db/migrate/20250816103847_create_pghero_query_stats.rb
new file mode 100644
index 00000000..5881a639
--- /dev/null
+++ b/db/migrate/20250816103847_create_pghero_query_stats.rb
@@ -0,0 +1,15 @@
+class CreatePgheroQueryStats < ActiveRecord::Migration[8.0]
+ def change
+ create_table :pghero_query_stats do |t|
+ t.text :database
+ t.text :user
+ t.text :query
+ t.integer :query_hash, limit: 8
+ t.float :total_time
+ t.integer :calls, limit: 8
+ t.timestamp :captured_at
+ end
+
+ add_index :pghero_query_stats, %i[database captured_at]
+ end
+end
diff --git a/db/migrate/20250816140257_add_indexes.rb b/db/migrate/20250816140257_add_indexes.rb
new file mode 100644
index 00000000..962c8aa8
--- /dev/null
+++ b/db/migrate/20250816140257_add_indexes.rb
@@ -0,0 +1,14 @@
+class AddIndexes < ActiveRecord::Migration[8.0]
+ disable_ddl_transaction!
+
+ def change
+ add_index :buses, :number,
+ algorithm: :concurrently
+
+ add_index :trips, [:from_id, :to_id],
+ algorithm: :concurrently
+
+ add_index :buses_services, :bus_id,
+ algorithm: :concurrently
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f6921e45..296f5c67 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2,33 +2,55 @@
# 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_08_16_140257) 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"
t.string "model"
+ t.index ["number"], name: "index_buses_on_number"
end
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|
t.string "name"
end
+ create_table "pghero_query_stats", force: :cascade do |t|
+ t.text "database"
+ t.text "user"
+ t.text "query"
+ t.bigint "query_hash"
+ t.float "total_time"
+ t.bigint "calls"
+ t.datetime "captured_at", precision: nil
+ t.index ["database", "captured_at"], name: "index_pghero_query_stats_on_database_and_captured_at"
+ end
+
+ create_table "pghero_space_stats", force: :cascade do |t|
+ t.text "database"
+ t.text "schema"
+ t.text "relation"
+ t.bigint "size"
+ t.datetime "captured_at", precision: nil
+ t.index ["database", "captured_at"], name: "index_pghero_space_stats_on_database_and_captured_at"
+ end
+
create_table "services", force: :cascade do |t|
t.string "name"
end
@@ -40,6 +62,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_id"
end
-
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 1beea2ac..ebd18895 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake
index 540fe871..9a1e7acb 100644
--- a/lib/tasks/utils.rake
+++ b/lib/tasks/utils.rake
@@ -1,34 +1,6 @@
-# Наивная загрузка данных из json-файла в БД
-# rake reload_json[fixtures/small.json]
-task :reload_json, [:file_name] => :environment do |_task, args|
- json = JSON.parse(File.read(args.file_name))
+# frozen_string_literal: true
- 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
- 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'],
- )
- end
- end
+# rake 'reload_json[JsonTripLoader, fixtures/small.json]'
+task :reload_json, [:loader, :file_name] => :environment do |_task, args|
+ Object.const_get(args.loader || "JsonTripLoader").new.load!(args.file_name)
end
diff --git a/script/stackprof/flamegraph.rb b/script/stackprof/flamegraph.rb
new file mode 100644
index 00000000..8fbac676
--- /dev/null
+++ b/script/stackprof/flamegraph.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "stackprof"
+require_relative "../../config/environment"
+
+profile = StackProf.run(mode: :wall, raw: true) do
+ Object.const_get(ARGV[0] || "JsonTripLoader").new.load!(ARGV[1] || "fixtures/small.json")
+end
+
+File.write("tmp/reports/stackprof/flamegraph.json", Oj.dump(profile))
diff --git a/script/stackprof/table.rb b/script/stackprof/table.rb
new file mode 100644
index 00000000..8c44f9c8
--- /dev/null
+++ b/script/stackprof/table.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require "stackprof"
+require_relative "../../config/environment"
+
+StackProf.run(mode: :wall, out: "tmp/reports/stackprof/table.dump") do
+ Object.const_get(ARGV[0] || "JsonTripLoader").new.load!(ARGV[1] || "fixtures/small.json")
+end
diff --git a/script/vernier.rb b/script/vernier.rb
new file mode 100644
index 00000000..b964eb18
--- /dev/null
+++ b/script/vernier.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require "vernier"
+require_relative "../config/environment"
+
+Vernier.run(out: "tmp/reports/vernier.json") do
+ Object.const_get(ARGV[0] || "JsonTripLoader").new.load!(ARGV[1] || "fixtures/small.json")
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 00000000..1e899ef3
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require "spec_helper"
+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 }
+
+# Ensures that the test database schema matches the current schema file.
+# If there are pending migrations it will invoke `db:test:prepare` to
+# recreate the test database by loading the schema.
+# 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
+RSpec.configure do |config|
+ # 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/8-0/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/services/json_trip_loader_spec.rb b/spec/services/json_trip_loader_spec.rb
new file mode 100644
index 00000000..67426f12
--- /dev/null
+++ b/spec/services/json_trip_loader_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+require "rspec/sqlimit"
+
+RSpec.describe JsonTripLoader do
+ let(:path) { Rails.root.join("fixtures/large.json") }
+
+ it "limits SELECT/INSERT/DELETE/UPDATE separately" do
+ # SELECT
+ expect do
+ described_class.new.load!(path.to_s)
+ end.not_to exceed_query_limit(1).with(/\bSELECT\b/i)
+
+ # INSERT (включая UPSERT — это INSERT ... ON CONFLICT)
+ expect do
+ described_class.new.load!(path.to_s)
+ end.not_to exceed_query_limit(10).with(/\bINSERT\b/i)
+
+ # DELETE
+ expect do
+ described_class.new.load!(path.to_s)
+ end.not_to exceed_query_limit(1).with(/\bDELETE\b/i)
+
+ # UPDATE — должно быть 0, если всё через upsert_all и нет отдельных update
+ expect do
+ described_class.new.load!(path.to_s)
+ end.not_to exceed_query_limit(0).with(/\bUPDATE\b/i)
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 00000000..409c64b6
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+# 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.
+ # # 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
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
index d19212ab..c05709af 100644
--- a/test/application_system_test_case.rb
+++ b/test/application_system_test_case.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 3ab84e3d..47447b6f 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,10 +1,14 @@
-ENV['RAILS_ENV'] ||= 'test'
-require_relative '../config/environment'
-require 'rails/test_help'
+# frozen_string_literal: true
-class ActiveSupport::TestCase
- # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
- fixtures :all
+ENV["RAILS_ENV"] ||= "test"
+require_relative "../config/environment"
+require "rails/test_help"
- # Add more helper methods to be used by all tests here...
+module ActiveSupport
+ class TestCase
+ # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
+ fixtures :all
+
+ # Add more helper methods to be used by all tests here...
+ end
end