diff --git a/.gitignore b/.gitignore
index 59c74047..7efd1f5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@
/tmp
/log
/public
+/vendor/*
+.byebug_history
+/fixtures/
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 00000000..8c037a6f
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,62 @@
+---
+AllCops:
+ DisplayCopNames: true
+ Exclude:
+ - "bin/**"
+ - "db/schema.rb"
+ - "db/migrate/**"
+ - "vendor/**/*"
+ - "node_modules/**/*"
+ NewCops: enable
+ TargetRailsVersion: 6
+ TargetRubyVersion: 3.2
+
+Layout/LineLength:
+ Enabled: false
+
+Metrics/AbcSize:
+ Enabled: false
+
+Metrics/ModuleLength:
+ Enabled: false
+
+Metrics/BlockLength:
+ Enabled: false
+
+Metrics/CyclomaticComplexity:
+ Enabled: false
+
+Metrics/ClassLength:
+ Enabled: false
+
+Metrics/MethodLength:
+ Enabled: false
+
+Metrics/PerceivedComplexity:
+ Enabled: false
+
+Rails:
+ Enabled: true
+
+Rails/UnknownEnv:
+ Environments:
+ - production
+ - development
+ - staging
+ - test
+
+require:
+ - rubocop-performance
+ - rubocop-rails
+
+Style/AsciiComments:
+ Enabled: false
+
+Style/ClassAndModuleChildren:
+ Enabled: false
+
+Style/Documentation:
+ Enabled: false
+
+Style/IfUnlessModifier:
+ Enabled: false
diff --git a/.ruby-version b/.ruby-version
index ec1cf33c..a0891f56 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.6.3
+3.3.4
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..a2c0d3e3
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+FROM ruby:3.3.4
+
+ENV NODE_VERSION 19.x
+
+RUN curl -sL https://deb.nodesource.com/setup_${NODE_VERSION} | bash -
+
+RUN apt-get update \
+ && apt-get install -y nodejs \
+ && rm -rf /var/lib/apt/lists/*
+
+ENV PROJECT_ROOT /app
+RUN mkdir -p ${PROJECT_ROOT}
+
+WORKDIR ${PROJECT_ROOT}
+
+ENV BUNDLE_APP_CONFIG ${PROJECT_ROOT}/bundle/config
+ENV GEM_HOME ${PROJECT_ROOT}/vendor/bundle
+ENV BUNDLE_PATH ${GEM_HOME}
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
index e20b1260..3dc5759d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,26 +1,52 @@
+# frozen_string_literal: true
+
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
-ruby '2.6.3'
+ruby '3.3.4'
-gem 'rails', '~> 5.2.3'
-gem 'pg', '>= 0.18', '< 2.0'
-gem 'puma', '~> 3.11'
-gem 'bootsnap', '>= 1.1.0', require: false
+gem 'activerecord-import'
+gem 'bootsnap', require: false
+gem 'flamegraph'
+gem 'json-stream'
+gem 'kaminari'
+gem 'meta_request'
+gem 'pg'
+gem 'puma'
+gem 'rack-mini-profiler'
+gem 'rails', '~> 7'
+gem 'rubocop-rails', require: false
+gem 'sprockets-rails'
+gem 'yajl-ruby', require: 'yajl'
+gem "pghero"
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
- gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
+ gem 'benchmark'
+ gem 'bullet'
+ gem 'byebug', platforms: %i[mri mingw x64_mingw]
+ gem 'memory_profiler'
+ gem 'rubocop-performance'
+ gem 'ruby-prof'
+ gem 'stackprof'
+ gem 'strong_migrations'
end
group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
- gem 'web-console', '>= 3.3.0'
- gem 'listen', '>= 3.0.5', '< 3.2'
+ gem 'listen'
+ gem 'web-console'
end
group :test do
+ gem 'minitest-power_assert'
+ gem 'capybara'
+ gem 'selenium-webdriver'
+ # Easy installation and use of web drivers to run system tests with browsers
+ gem 'webdrivers'
+ gem 'sqlite3'
+ gem 'rspec-benchmark'
end
# 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: %i[mingw mswin x64_mingw jruby]
diff --git a/Gemfile.lock b/Gemfile.lock
index fccf6f5f..40eb9ac0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,150 +1,403 @@
GEM
remote: https://rubygems.org/
specs:
- actioncable (5.2.3)
- actionpack (= 5.2.3)
+ actioncable (7.2.2.1)
+ actionpack (= 7.2.2.1)
+ activesupport (= 7.2.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailer (5.2.3)
- actionpack (= 5.2.3)
- actionview (= 5.2.3)
- activejob (= 5.2.3)
- mail (~> 2.5, >= 2.5.4)
- rails-dom-testing (~> 2.0)
- actionpack (5.2.3)
- actionview (= 5.2.3)
- activesupport (= 5.2.3)
- rack (~> 2.0)
+ zeitwerk (~> 2.6)
+ actionmailbox (7.2.2.1)
+ actionpack (= 7.2.2.1)
+ activejob (= 7.2.2.1)
+ activerecord (= 7.2.2.1)
+ activestorage (= 7.2.2.1)
+ activesupport (= 7.2.2.1)
+ mail (>= 2.8.0)
+ actionmailer (7.2.2.1)
+ actionpack (= 7.2.2.1)
+ actionview (= 7.2.2.1)
+ activejob (= 7.2.2.1)
+ activesupport (= 7.2.2.1)
+ mail (>= 2.8.0)
+ rails-dom-testing (~> 2.2)
+ actionpack (7.2.2.1)
+ actionview (= 7.2.2.1)
+ activesupport (= 7.2.2.1)
+ nokogiri (>= 1.8.5)
+ racc
+ rack (>= 2.2.4, < 3.2)
+ rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.2.3)
- activesupport (= 5.2.3)
+ rails-dom-testing (~> 2.2)
+ rails-html-sanitizer (~> 1.6)
+ useragent (~> 0.16)
+ actiontext (7.2.2.1)
+ actionpack (= 7.2.2.1)
+ activerecord (= 7.2.2.1)
+ activestorage (= 7.2.2.1)
+ activesupport (= 7.2.2.1)
+ globalid (>= 0.6.0)
+ nokogiri (>= 1.8.5)
+ actionview (7.2.2.1)
+ activesupport (= 7.2.2.1)
builder (~> 3.1)
- erubi (~> 1.4)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.0, >= 1.0.3)
- activejob (5.2.3)
- activesupport (= 5.2.3)
+ erubi (~> 1.11)
+ rails-dom-testing (~> 2.2)
+ rails-html-sanitizer (~> 1.6)
+ activejob (7.2.2.1)
+ activesupport (= 7.2.2.1)
globalid (>= 0.3.6)
- activemodel (5.2.3)
- activesupport (= 5.2.3)
- activerecord (5.2.3)
- activemodel (= 5.2.3)
- activesupport (= 5.2.3)
- arel (>= 9.0)
- activestorage (5.2.3)
- actionpack (= 5.2.3)
- activerecord (= 5.2.3)
- marcel (~> 0.3.1)
- activesupport (5.2.3)
- concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (>= 0.7, < 2)
- minitest (~> 5.1)
- tzinfo (~> 1.1)
- arel (9.0.0)
- bindex (0.6.0)
- bootsnap (1.4.2)
- msgpack (~> 1.0)
- builder (3.2.3)
- byebug (11.0.1)
- concurrent-ruby (1.1.5)
- crass (1.0.4)
- erubi (1.8.0)
- ffi (1.10.0)
- globalid (0.4.2)
- activesupport (>= 4.2.0)
- i18n (1.6.0)
+ activemodel (7.2.2.1)
+ activesupport (= 7.2.2.1)
+ activerecord (7.2.2.1)
+ activemodel (= 7.2.2.1)
+ activesupport (= 7.2.2.1)
+ timeout (>= 0.4.0)
+ activerecord-import (2.1.0)
+ activerecord (>= 4.2)
+ activestorage (7.2.2.1)
+ actionpack (= 7.2.2.1)
+ activejob (= 7.2.2.1)
+ activerecord (= 7.2.2.1)
+ activesupport (= 7.2.2.1)
+ marcel (~> 1.0)
+ activesupport (7.2.2.1)
+ base64
+ benchmark (>= 0.3)
+ bigdecimal
+ concurrent-ruby (~> 1.0, >= 1.3.1)
+ connection_pool (>= 2.2.5)
+ drb
+ i18n (>= 1.6, < 2)
+ logger (>= 1.4.2)
+ minitest (>= 5.1)
+ securerandom (>= 0.3)
+ tzinfo (~> 2.0, >= 2.0.5)
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
+ ast (2.4.2)
+ 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)
+ bindex (0.8.1)
+ bootsnap (1.18.4)
+ msgpack (~> 1.2)
+ builder (3.3.0)
+ bullet (8.0.1)
+ activesupport (>= 3.0.0)
+ uniform_notifier (~> 1.11)
+ byebug (11.1.3)
+ 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.5.1)
+ drb (2.2.1)
+ erubi (1.13.1)
+ ffi (1.17.1-aarch64-linux-gnu)
+ ffi (1.17.1-aarch64-linux-musl)
+ ffi (1.17.1-arm-linux-gnu)
+ ffi (1.17.1-arm-linux-musl)
+ ffi (1.17.1-arm64-darwin)
+ ffi (1.17.1-x86_64-darwin)
+ ffi (1.17.1-x86_64-linux-gnu)
+ ffi (1.17.1-x86_64-linux-musl)
+ flamegraph (0.9.5)
+ globalid (1.2.1)
+ activesupport (>= 6.1)
+ i18n (1.14.7)
concurrent-ruby (~> 1.0)
- listen (3.1.5)
- rb-fsevent (~> 0.9, >= 0.9.4)
- rb-inotify (~> 0.9, >= 0.9.7)
- ruby_dep (~> 1.2)
- loofah (2.2.3)
+ io-console (0.8.0)
+ irb (1.15.1)
+ pp (>= 0.6.0)
+ rdoc (>= 4.0.0)
+ reline (>= 0.4.2)
+ json (2.10.1)
+ json-stream (1.0.0)
+ kaminari (1.2.2)
+ activesupport (>= 4.1.0)
+ kaminari-actionview (= 1.2.2)
+ kaminari-activerecord (= 1.2.2)
+ kaminari-core (= 1.2.2)
+ kaminari-actionview (1.2.2)
+ actionview
+ kaminari-core (= 1.2.2)
+ kaminari-activerecord (1.2.2)
+ activerecord
+ kaminari-core (= 1.2.2)
+ kaminari-core (1.2.2)
+ language_server-protocol (3.17.0.4)
+ listen (3.9.0)
+ rb-fsevent (~> 0.10, >= 0.10.3)
+ rb-inotify (~> 0.9, >= 0.9.10)
+ logger (1.6.5)
+ loofah (2.24.0)
crass (~> 1.0.2)
- nokogiri (>= 1.5.9)
- mail (2.7.1)
+ nokogiri (>= 1.12.0)
+ mail (2.8.1)
mini_mime (>= 0.1.1)
- marcel (0.3.3)
- mimemagic (~> 0.3.2)
- method_source (0.9.2)
- mimemagic (0.3.3)
- mini_mime (1.0.1)
- mini_portile2 (2.4.0)
- minitest (5.11.3)
- msgpack (1.2.9)
- nio4r (2.3.1)
- nokogiri (1.10.2)
- mini_portile2 (~> 2.4.0)
- pg (1.1.4)
- puma (3.12.1)
- rack (2.0.6)
- rack-test (1.1.0)
- rack (>= 1.0, < 3)
- rails (5.2.3)
- actioncable (= 5.2.3)
- actionmailer (= 5.2.3)
- actionpack (= 5.2.3)
- actionview (= 5.2.3)
- activejob (= 5.2.3)
- activemodel (= 5.2.3)
- activerecord (= 5.2.3)
- activestorage (= 5.2.3)
- activesupport (= 5.2.3)
- bundler (>= 1.3.0)
- railties (= 5.2.3)
- sprockets-rails (>= 2.0.0)
- rails-dom-testing (2.0.3)
- activesupport (>= 4.2.0)
+ net-imap
+ net-pop
+ net-smtp
+ marcel (1.0.4)
+ matrix (0.4.2)
+ memory_profiler (1.1.0)
+ meta_request (0.8.5)
+ rack-contrib (>= 1.1, < 3)
+ railties (>= 3.0.0, < 9)
+ mini_mime (1.1.5)
+ minitest (5.25.4)
+ minitest-power_assert (0.3.1)
+ minitest
+ power_assert (>= 1.1)
+ msgpack (1.8.0)
+ net-imap (0.5.6)
+ date
+ net-protocol
+ net-pop (0.1.2)
+ net-protocol
+ net-protocol (0.2.2)
+ timeout
+ net-smtp (0.5.1)
+ net-protocol
+ nio4r (2.7.4)
+ nokogiri (1.18.2-aarch64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.18.2-aarch64-linux-musl)
+ racc (~> 1.4)
+ nokogiri (1.18.2-arm-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.18.2-arm-linux-musl)
+ racc (~> 1.4)
+ nokogiri (1.18.2-arm64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.18.2-x86_64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.18.2-x86_64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.18.2-x86_64-linux-musl)
+ racc (~> 1.4)
+ parallel (1.26.3)
+ parser (3.3.7.1)
+ ast (~> 2.4.1)
+ racc
+ pg (1.5.9)
+ pghero (3.6.1)
+ activerecord (>= 6.1)
+ power_assert (2.0.5)
+ 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)
+ rack (3.1.9)
+ rack-contrib (2.5.0)
+ rack (< 4)
+ rack-mini-profiler (3.3.1)
+ rack (>= 1.2.0)
+ rack-session (2.1.0)
+ base64 (>= 0.1.0)
+ rack (>= 3.0.0)
+ rack-test (2.2.0)
+ rack (>= 1.3)
+ rackup (2.2.1)
+ rack (>= 3)
+ rails (7.2.2.1)
+ actioncable (= 7.2.2.1)
+ actionmailbox (= 7.2.2.1)
+ actionmailer (= 7.2.2.1)
+ actionpack (= 7.2.2.1)
+ actiontext (= 7.2.2.1)
+ actionview (= 7.2.2.1)
+ activejob (= 7.2.2.1)
+ activemodel (= 7.2.2.1)
+ activerecord (= 7.2.2.1)
+ activestorage (= 7.2.2.1)
+ activesupport (= 7.2.2.1)
+ bundler (>= 1.15.0)
+ railties (= 7.2.2.1)
+ rails-dom-testing (2.2.0)
+ activesupport (>= 5.0.0)
+ minitest
nokogiri (>= 1.6)
- rails-html-sanitizer (1.0.4)
- loofah (~> 2.2, >= 2.2.2)
- railties (5.2.3)
- actionpack (= 5.2.3)
- activesupport (= 5.2.3)
- method_source
- rake (>= 0.8.7)
- thor (>= 0.19.0, < 2.0)
- rake (12.3.2)
- rb-fsevent (0.10.3)
- rb-inotify (0.10.0)
+ rails-html-sanitizer (1.6.2)
+ loofah (~> 2.21)
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
+ railties (7.2.2.1)
+ actionpack (= 7.2.2.1)
+ activesupport (= 7.2.2.1)
+ irb (~> 1.13)
+ rackup (>= 1.0.0)
+ 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)
- ruby_dep (1.5.0)
- sprockets (3.7.2)
+ rdoc (6.12.0)
+ psych (>= 4.0.0)
+ regexp_parser (2.10.0)
+ reline (0.6.0)
+ io-console (~> 0.5)
+ rexml (3.4.0)
+ 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-support (3.13.2)
+ rubocop (1.71.2)
+ json (~> 2.3)
+ language_server-protocol (>= 3.17.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.38.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 4.0)
+ rubocop-ast (1.38.0)
+ parser (>= 3.3.1.0)
+ rubocop-performance (1.23.1)
+ rubocop (>= 1.48.1, < 2.0)
+ rubocop-ast (>= 1.31.1, < 2.0)
+ rubocop-rails (2.29.1)
+ activesupport (>= 4.2.0)
+ rack (>= 1.1)
+ rubocop (>= 1.52.0, < 2.0)
+ rubocop-ast (>= 1.31.1, < 2.0)
+ ruby-prof (1.7.1)
+ ruby-progressbar (1.13.0)
+ rubyzip (2.4.1)
+ securerandom (0.4.1)
+ selenium-webdriver (4.10.0)
+ rexml (~> 3.2, >= 3.2.5)
+ rubyzip (>= 1.2.2, < 3.0)
+ websocket (~> 1.0)
+ sprockets (4.2.1)
concurrent-ruby (~> 1.0)
- rack (> 1, < 3)
- sprockets-rails (3.2.1)
- actionpack (>= 4.0)
- activesupport (>= 4.0)
+ rack (>= 2.2.4, < 4)
+ sprockets-rails (3.5.2)
+ actionpack (>= 6.1)
+ activesupport (>= 6.1)
sprockets (>= 3.0.0)
- thor (0.20.3)
- thread_safe (0.3.6)
- tzinfo (1.2.5)
- thread_safe (~> 0.1)
- web-console (3.7.0)
- actionview (>= 5.0)
- activemodel (>= 5.0)
+ sqlite3 (2.5.0-aarch64-linux-gnu)
+ sqlite3 (2.5.0-aarch64-linux-musl)
+ sqlite3 (2.5.0-arm-linux-gnu)
+ sqlite3 (2.5.0-arm-linux-musl)
+ sqlite3 (2.5.0-arm64-darwin)
+ sqlite3 (2.5.0-x86_64-darwin)
+ sqlite3 (2.5.0-x86_64-linux-gnu)
+ sqlite3 (2.5.0-x86_64-linux-musl)
+ stackprof (0.2.27)
+ 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)
+ unicode-display_width (3.1.4)
+ unicode-emoji (~> 4.0, >= 4.0.4)
+ unicode-emoji (4.0.4)
+ uniform_notifier (1.16.0)
+ useragent (0.16.11)
+ web-console (4.2.1)
+ actionview (>= 6.0.0)
+ activemodel (>= 6.0.0)
bindex (>= 0.4.0)
- railties (>= 5.0)
- websocket-driver (0.7.0)
+ railties (>= 6.0.0)
+ webdrivers (5.3.1)
+ nokogiri (~> 1.6)
+ rubyzip (>= 1.3.0)
+ selenium-webdriver (~> 4.0, < 4.11)
+ websocket (1.2.11)
+ websocket-driver (0.7.7)
+ base64
websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.3)
+ websocket-extensions (0.1.5)
+ xpath (3.2.0)
+ nokogiri (~> 1.8)
+ yajl-ruby (1.4.3)
+ zeitwerk (2.7.1)
PLATFORMS
- ruby
+ aarch64-linux-gnu
+ aarch64-linux-musl
+ arm-linux-gnu
+ arm-linux-musl
+ arm64-darwin
+ x86_64-darwin
+ x86_64-linux-gnu
+ x86_64-linux-musl
DEPENDENCIES
- bootsnap (>= 1.1.0)
+ activerecord-import
+ benchmark
+ bootsnap
+ bullet
byebug
- listen (>= 3.0.5, < 3.2)
- pg (>= 0.18, < 2.0)
- puma (~> 3.11)
- rails (~> 5.2.3)
+ capybara
+ flamegraph
+ json-stream
+ kaminari
+ listen
+ memory_profiler
+ meta_request
+ minitest-power_assert
+ pg
+ pghero
+ puma
+ rack-mini-profiler
+ rails (~> 7)
+ rspec-benchmark
+ rubocop-performance
+ rubocop-rails
+ ruby-prof
+ selenium-webdriver
+ sprockets-rails
+ sqlite3
+ stackprof
+ strong_migrations
tzinfo-data
- web-console (>= 3.3.0)
+ web-console
+ webdrivers
+ yajl-ruby
RUBY VERSION
- ruby 2.6.3p62
+ ruby 3.3.4p94
BUNDLED WITH
- 2.0.2
+ 2.5.11
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..4c12aa25
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,47 @@
+include make-compose.mk
+include make-services-app.mk
+
+
+backend:
+ rm -rf tmp/pids/server.pid
+ bundle exec rails s -p 3000 -b '0.0.0.0'
+
+setup:
+ make setup-app
+
+setup-app:
+ cp -n .env.example .env || true
+ bin/setup
+
+fixtures-load-json:
+ bin/rake utils:reload_json[fixtures/medium.json]
+
+clean:
+ bin/rails db:drop
+
+console:
+ bin/rails c
+
+db-reset:
+ bin/rails db:drop
+ bin/rails db:create
+ bin/rails db:schema:load
+ bin/rails db:migrate
+ bin/rake reload_json[fixtures/small.json]
+ bin/rails log:clear tmp:clear
+ bin/rails restart
+
+start:
+ bin/rails s
+
+linter-code-fix:
+ bundle exec rubocop -A
+
+test:
+ bin/rails test
+
+system-test:
+ bin/rails test:system
+
+
+.PHONY: test
\ No newline at end of file
diff --git a/Rakefile b/Rakefile
index e85f9139..488c551f 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,3 +1,5 @@
+# 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.
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..c5ece113 100644
--- a/app/controllers/trips_controller.rb
+++ b/app/controllers/trips_controller.rb
@@ -1,7 +1,14 @@
+# frozen_string_literal: true
+
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)
+ @from = City.find_by!(name: params[:from])
+ @to = City.find_by!(name: params[:to])
+ trips = Trip
+ .eager_load(bus: :services)
+ .where(from: @from, to: @to)
+
+ @trips_count = trips.size
+ @trips = trips.order(:start_time).page(params[:page]).per(20)
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..d84cb6e7 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 10a4cba8..68c1fef6 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,3 +1,9 @@
+# frozen_string_literal: true
+
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
+
+ def self.enum_from_array(array)
+ array.index_by(&:to_sym)
+ end
end
diff --git a/app/models/bus.rb b/app/models/bus.rb
index 1dcc54cb..e20e2ee7 100644
--- a/app/models/bus.rb
+++ b/app/models/bus.rb
@@ -1,20 +1,23 @@
+# frozen_string_literal: true
+
class Bus < ApplicationRecord
- MODELS = [
- 'Икарус',
- 'Мерседес',
- 'Сканиа',
- 'Буханка',
- 'УАЗ',
- 'Спринтер',
- 'ГАЗ',
- 'ПАЗ',
- 'Вольво',
- 'Газель',
+ MODELS = %w[
+ Икарус
+ Мерседес
+ Сканиа
+ Буханка
+ УАЗ
+ Спринтер
+ ГАЗ
+ ПАЗ
+ Вольво
+ Газель
].freeze
has_many :trips
has_and_belongs_to_many :services, join_table: :buses_services
+ enum :model, enum_from_array(MODELS)
+
validates :number, presence: true, uniqueness: true
- validates :model, inclusion: { in: MODELS }
end
diff --git a/app/models/city.rb b/app/models/city.rb
index 19ec7f36..19897544 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..e092e086 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Service < ApplicationRecord
SERVICES = [
'WiFi',
@@ -9,11 +11,11 @@ class Service < ApplicationRecord
'Телевизор общий',
'Телевизор индивидуальный',
'Стюардесса',
- 'Можно не печатать билет',
+ 'Можно не печатать билет'
].freeze
has_and_belongs_to_many :buses, join_table: :buses_services
validates :name, presence: true
- validates :name, inclusion: { in: SERVICES }
+ enum :name, enum_from_array(SERVICES)
end
diff --git a/app/models/trip.rb b/app/models/trip.rb
index 9d63dfff..d0bda301 100644
--- a/app/models/trip.rb
+++ b/app/models/trip.rb
@@ -1,14 +1,12 @@
+# frozen_string_literal: true
+
class Trip < ApplicationRecord
- HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/
+ 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 :bus
- validates :from, presence: true
- validates :to, presence: true
- validates :bus, presence: true
-
validates :start_time, format: { with: HHMM_REGEXP, message: 'Invalid time' }
validates :duration_minutes, presence: true
validates :duration_minutes, numericality: { greater_than: 0 }
@@ -25,8 +23,8 @@ def to_h
bus: {
number: bus.number,
model: bus.model,
- services: bus.services.map(&:name),
- },
+ services: bus.services.map(&:name)
+ }
}
end
end
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index e64170ee..128f140d 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -5,8 +5,8 @@
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
- <%= stylesheet_link_tag 'application', media: 'all' %>
- <%= javascript_include_tag 'application' %>
+ <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track": "reload" %>
+ <%= javascript_include_tag "application", "data-turbolinks-track": "reload" %>
diff --git a/app/views/trips/_service.html.erb b/app/views/trips/_service.html.erb
index 178ea8c0..58a0e32f 100644
--- a/app/views/trips/_service.html.erb
+++ b/app/views/trips/_service.html.erb
@@ -1 +1,4 @@
-<%= "#{service.name}" %>
+
+
+ - <%= "#{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
index fa1de9aa..58aad25e 100644
--- a/app/views/trips/_trip.html.erb
+++ b/app/views/trips/_trip.html.erb
@@ -3,3 +3,11 @@
<%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
<%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
<%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
+<% if trip.bus.services.present? %>
+ Сервисы в автобусе:
+
+ <%= render partial: "service", collection: trip.bus.services %>
+
+<% end %>
+
+<%= render "delimiter" %>
diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb
index a60bce41..256ff48d 100644
--- a/app/views/trips/index.html.erb
+++ b/app/views/trips/index.html.erb
@@ -2,15 +2,8 @@
<%= "Автобусы #{@from.name} – #{@to.name}" %>
- <%= "В расписании #{@trips.count} рейсов" %>
+ <%= "В расписании #{@trips_count} рейсов" %>
-
-<% @trips.each do |trip| %>
-
- <%= render "trip", trip: trip %>
- <% if trip.bus.services.present? %>
- <%= render "services", services: trip.bus.services %>
- <% end %>
-
- <%= render "delimiter" %>
-<% end %>
+<%= paginate @trips %>
+<%= render partial: "trip", collection: @trips%>
+<%= paginate @trips %>
diff --git a/bin/setup b/bin/setup
index f294207b..835e2ac9 100755
--- a/bin/setup
+++ b/bin/setup
@@ -29,7 +29,7 @@ chdir APP_ROOT do
system! 'bin/rails db:setup'
puts "\n== Loading data from fixtures/small.json =="
- system! 'bin/rake reload_json[fixtures/small.json]'
+ system! 'bin/rake utils:reload_json[fixtures/small.json]'
puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'
diff --git a/case-study.md b/case-study.md
new file mode 100644
index 00000000..113a37f0
--- /dev/null
+++ b/case-study.md
@@ -0,0 +1,56 @@
+# Case-study оптимизации
+
+## Актуальная проблема
+1. Импорт данных из json занимает большое время(на 1млн записей импорт длился 7 минут)
+2. Загрузка страници `автобусы/` занемает большое время, если данных много, на ней явно есть лишнии запросы(на начальном этпе ~55) и у сущностей ро которым идет поиск нет индексоа
+
+Я решил исправить эту проблему, оптимизировав эту программу.
+
+
+## Формирование метрики
+Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику:
+1. программа затрачивает на обработку больших файлов не больше 1 минуты
+2. При открытии страници проискодит не более 4 запросов
+Перед оптимизацией программа затрачивает на обработку файла в 50000 строк 335 MB
+
+## Гарантия корректности работы оптимизированной программы
+1. На писан юнит тест гарантирующий корректную работу рейк таски `UtilsTest`
+2. Написан системный тест который гарантирует сохранность структуры страници `TripsTest`
+
+
+## Вникаем в детали системы, чтобы найти главные точки роста
+Для того, чтобы найти "точки роста" для оптимизации я воспользовался:
+
+- pghero
+- rack-mini-profiler
+- benchmark
+- byebug
+
+
+Вот какие проблемы удалось найти и решить
+
+### Оптимизация загрузки данных
+С помощью `benchmark` замерял время работы
+
+1. На первом этапе было внесенно изменение позволяющее последовательно читать json файл а не весь целиком(json-stream)
+2. Затем что бы поиск сущностей для создагия Trip происходил быстрее добавил индексы автобусов и сервисов
+3. Так как импорт данных все равно не укладывался в бюджет, воспользовался библиотекой `ActiveRecord::Import` и стал агрегировать города, автобусы и сервисы, и затем их импортить одной транзакцией(данных сущностей на 1м записей находится в памяти не больше 100 обектов)
+4. Последним этапом перевел таску на стриминг данных в БД
+
+### Оптимизация страници
+Вявил с помощю гема `bullet` что на странице есть N+1 запрос
+rack-mini-profiler - показал что на странице есть много однотипных запросов(обращения к сервисам и автобусам)
+1. Зделал предзагрузку связанных даннях `.eager_load(bus: :services)`
+2. Добавил индекс для города на поле `name` проверил с помощью pghero что поиск города происходит по городу(визуализация explain)
+3. Добавил пагинацию - это позволяло не загружать на страницу сразу все сущности
+
+
+
+## Результаты
+1. В результате загрузка файла в 1m - длится 12 сек
+2. При загрузке страници происходит всего 3 запроса
+3. Загрузка занимает ~50 mls
+
+## Защита от регрессии производительности
+1. Перформ тест проверющий что таска выполняется не более 60 сек
+2. Тест контроллера - если будет n + 1 запрос тест упадет с ошибкой
\ No newline at end of file
diff --git a/config.ru b/config.ru
index f7ba0b52..842bccc3 100644
--- a/config.ru
+++ b/config.ru
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
diff --git a/config/application.rb b/config/application.rb
index 9c331097..3cb92f3c 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require_relative 'boot'
require 'rails/all'
diff --git a/config/boot.rb b/config/boot.rb
index b9e460ce..c04863fa 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/config/database.yml b/config/database.yml
index e116cfa6..cc42398b 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -15,11 +15,13 @@
# gem 'pg'
#
default: &default
- adapter: postgresql
encoding: unicode
- # For details on connection pooling, see Rails configuration guide
- # http://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+ adapter: postgresql
+ host: postgres
+ username: postgres
+ password:
development:
<<: *default
@@ -58,6 +60,7 @@ development:
test:
<<: *default
database: task-4_test
+ # adapter: sqlite3
# As with config/secrets.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
diff --git a/config/environment.rb b/config/environment.rb
index 426333bb..d5abe558 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Load the Rails application.
require_relative 'application'
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 1311e3e4..8543a99d 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,4 +1,15 @@
+# frozen_string_literal: true
+
Rails.application.configure do
+ config.after_initialize do
+ Bullet.enable = true
+ Bullet.alert = true
+ Bullet.bullet_logger = true
+ Bullet.console = true
+ Bullet.rails_logger = true
+ Bullet.add_footer = true
+ end
+
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded on
@@ -14,7 +25,7 @@
# 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
@@ -47,10 +58,10 @@
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
- config.assets.debug = true
+ # config.assets.debug = true
# Suppress logger output for asset requests.
- config.assets.quiet = true
+ # config.assets.quiet = true
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 613d8289..50fa05bb 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.
@@ -23,11 +25,11 @@
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress JavaScripts and CSS.
- config.assets.js_compressor = :uglifier
+ # config.assets.js_compressor = :uglifier
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
- config.assets.compile = false
+ # config.assets.compile = false
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
@@ -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)
+ if ENV['RAILS_LOG_TO_STDOUT'].present?
+ 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..d928ac16 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,4 +1,12 @@
+# frozen_string_literal: true
+
Rails.application.configure do
+ config.after_initialize do
+ Bullet.enable = true
+ Bullet.bullet_logger = true
+ Bullet.raise = true # raise an error if n+1 query occurs
+ end
+
# Settings specified here will take precedence over those in config/application.rb.
# The test environment is used exclusively to run your application's
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 4b828e80..c8ea76c7 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -1,14 +1,14 @@
-# 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.
-Rails.application.config.assets.version = '1.0'
+# frozen_string_literal: true
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
-Rails.application.config.assets.paths << Rails.root.join('node_modules')
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )
+
+Rails.application.config.assets.paths << Rails.root.join('app/assets/javascripts')
+Rails.application.config.assets.paths << Rails.root.join('app/assets/stylesheets')
+Rails.application.config.assets.precompile += %w[application.js application.css]
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/kaminari_config.rb b/config/initializers/kaminari_config.rb
new file mode 100644
index 00000000..a1950df8
--- /dev/null
+++ b/config/initializers/kaminari_config.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+Kaminari.configure do |config|
+ # config.default_per_page = 25
+ # config.max_per_page = nil
+ config.window = 2
+ # config.outer_window = 0
+ # config.left = 0
+ # config.right = 0
+ # config.page_method_name = :page
+ # config.param_name = :page
+ # config.max_pages = nil
+ # config.params_on_first_page = false
+end
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/strong_migrations.rb b/config/initializers/strong_migrations.rb
new file mode 100644
index 00000000..308870b1
--- /dev/null
+++ b/config/initializers/strong_migrations.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# Mark existing migrations as safe
+StrongMigrations.start_after = 20_250_209_144_006
+
+# 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
+
+# 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..d9a94f35 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 a2da6a7b..0d75bbe0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,5 +1,9 @@
+# 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 "/" => "statistics#index"
- get "автобусы/:from/:to" => "trips#index"
+ mount PgHero::Engine, at: "pghero"
+
+ get '/' => 'statistics#index'
+ get 'buses/:from/:to' => 'trips#index'
end
diff --git a/config/spring.rb b/config/spring.rb
index 9fa7863f..c5933e49 100644
--- a/config/spring.rb
+++ b/config/spring.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
%w[
.ruby-version
.rbenv-vars
diff --git a/db/migrate/20190330192820_create_cities.rb b/db/migrate/20190330192820_create_cities.rb
index 96f5b3e8..fcdfd0f3 100644
--- a/db/migrate/20190330192820_create_cities.rb
+++ b/db/migrate/20190330192820_create_cities.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateCities < ActiveRecord::Migration[5.2]
def change
create_table :cities do |t|
diff --git a/db/migrate/20190330192933_create_trips.rb b/db/migrate/20190330192933_create_trips.rb
index 46f19479..c86743d7 100644
--- a/db/migrate/20190330192933_create_trips.rb
+++ b/db/migrate/20190330192933_create_trips.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateTrips < ActiveRecord::Migration[5.2]
def change
create_table :trips do |t|
diff --git a/db/migrate/20190330193017_create_buses.rb b/db/migrate/20190330193017_create_buses.rb
index 274117ed..622af62d 100644
--- a/db/migrate/20190330193017_create_buses.rb
+++ b/db/migrate/20190330193017_create_buses.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateBuses < ActiveRecord::Migration[5.2]
def change
create_table :buses do |t|
diff --git a/db/migrate/20190330193027_create_services.rb b/db/migrate/20190330193027_create_services.rb
index 243473b7..71831dc0 100644
--- a/db/migrate/20190330193027_create_services.rb
+++ b/db/migrate/20190330193027_create_services.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateServices < ActiveRecord::Migration[5.2]
def change
create_table :services do |t|
diff --git a/db/migrate/20190330193044_create_buses_services.rb b/db/migrate/20190330193044_create_buses_services.rb
index 71da32bb..e8b0446e 100644
--- a/db/migrate/20190330193044_create_buses_services.rb
+++ b/db/migrate/20190330193044_create_buses_services.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CreateBusesServices < ActiveRecord::Migration[5.2]
def change
create_table :buses_services do |t|
diff --git a/db/migrate/20250209144520_add_index_number_column_from_buses.rb b/db/migrate/20250209144520_add_index_number_column_from_buses.rb
new file mode 100644
index 00000000..a0686e73
--- /dev/null
+++ b/db/migrate/20250209144520_add_index_number_column_from_buses.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddIndexNumberColumnFromBuses < ActiveRecord::Migration[7.2]
+ disable_ddl_transaction!
+
+ def change
+ add_index :buses, %i[number model], unique: true, algorithm: :concurrently
+ end
+end
diff --git a/db/migrate/20250209145721_add_index_from_cities.rb b/db/migrate/20250209145721_add_index_from_cities.rb
new file mode 100644
index 00000000..4f26742c
--- /dev/null
+++ b/db/migrate/20250209145721_add_index_from_cities.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddIndexFromCities < ActiveRecord::Migration[7.2]
+ disable_ddl_transaction!
+
+ def change
+ add_index :cities, :name, unique: true, algorithm: :concurrently
+ end
+end
diff --git a/db/migrate/20250209150005_add_index_from_services.rb b/db/migrate/20250209150005_add_index_from_services.rb
new file mode 100644
index 00000000..6e0a99a2
--- /dev/null
+++ b/db/migrate/20250209150005_add_index_from_services.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddIndexFromServices < ActiveRecord::Migration[7.2]
+ disable_ddl_transaction!
+
+ def change
+ add_index :services, :name, unique: true, algorithm: :concurrently
+ end
+end
diff --git a/db/migrate/20250209150200_add_index_from_trips.rb b/db/migrate/20250209150200_add_index_from_trips.rb
new file mode 100644
index 00000000..657e75d4
--- /dev/null
+++ b/db/migrate/20250209150200_add_index_from_trips.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddIndexFromTrips < ActiveRecord::Migration[7.2]
+ disable_ddl_transaction!
+
+ def change
+ add_index :trips, :from_id, algorithm: :concurrently
+ add_index :trips, :to_id, algorithm: :concurrently
+ add_index :trips, :bus_id, algorithm: :concurrently
+ end
+end
diff --git a/db/migrate/20250209215015_create_enum_from_bus.rb b/db/migrate/20250209215015_create_enum_from_bus.rb
new file mode 100644
index 00000000..284e6a1e
--- /dev/null
+++ b/db/migrate/20250209215015_create_enum_from_bus.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class CreateEnumFromBus < ActiveRecord::Migration[7.2]
+ def change
+ safety_assured do
+ create_enum :mode, Bus::MODELS
+ end
+ end
+end
diff --git a/db/migrate/20250209215912_create_enum_from_service.rb b/db/migrate/20250209215912_create_enum_from_service.rb
new file mode 100644
index 00000000..2cc41d9b
--- /dev/null
+++ b/db/migrate/20250209215912_create_enum_from_service.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class CreateEnumFromService < ActiveRecord::Migration[7.2]
+ def change
+ safety_assured do
+ create_enum :name, Service::SERVICES
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f6921e45..12c90c21 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2,22 +2,27 @@
# 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[7.2].define(version: 2025_02_12_061158) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
+ # Custom types defined in this database.
+ # Note that some types may not work with other database engines. Be careful if changing database.
+ create_enum "mode", ["Икарус", "Мерседес", "Сканиа", "Буханка", "УАЗ", "Спринтер", "ГАЗ", "ПАЗ", "Вольво", "Газель"]
+ create_enum "name", ["WiFi", "Туалет", "Работающий туалет", "Ремни безопасности", "Кондиционер общий", "Кондиционер Индивидуальный", "Телевизор общий", "Телевизор индивидуальный", "Стюардесса", "Можно не печатать билет"]
+
create_table "buses", force: :cascade do |t|
t.string "number"
t.string "model"
+ t.index ["number", "model"], name: "index_buses_on_number_and_model", unique: true
end
create_table "buses_services", force: :cascade do |t|
@@ -27,10 +32,12 @@
create_table "cities", force: :cascade do |t|
t.string "name"
+ t.index ["name"], name: "index_cities_on_name", unique: true
end
create_table "services", force: :cascade do |t|
t.string "name"
+ t.index ["name"], name: "index_services_on_name", unique: true
end
create_table "trips", force: :cascade do |t|
@@ -40,6 +47,8 @@
t.integer "duration_minutes"
t.integer "price_cents"
t.integer "bus_id"
+ t.index ["bus_id"], name: "index_trips_on_bus_id"
+ t.index ["from_id"], name: "index_trips_on_from_id"
+ t.index ["to_id"], name: "index_trips_on_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/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..b999a751
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,31 @@
+---
+version: "3.7"
+
+services:
+ postgres:
+ image: postgres:13-alpine
+ environment:
+ POSTGRES_HOST_AUTH_METHOD: trust
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ command: make backend
+ volumes:
+ - ~/.bash_history:/root/.bash_history
+ - .:/app
+ ports:
+ - "3000:3000"
+ depends_on:
+ - postgres
+ stdin_open: true
+ tty: true
+
+
+volumes:
+ pgdata:
\ No newline at end of file
diff --git a/fixtures/micro.json b/fixtures/micro.json
new file mode 100644
index 00000000..f9d20f7b
--- /dev/null
+++ b/fixtures/micro.json
@@ -0,0 +1,5 @@
+[
+ {"from":"Сочи","to":"Тула","start_time":"16:11","duration_minutes":83,"price_cents":23354,"bus":{"number":"229","model":"Икарус","services":["Ремни безопасности","Кондиционер общий","Кондиционер Индивидуальный","Телевизор индивидуальный","Стюардесса","Можно не печатать билет"]}},
+ {"from":"Самара","to":"Самара","start_time":"13:13","duration_minutes":572,"price_cents":83861,"bus":{"number":"912","model":"Вольво","services":[]}}
+]
+
diff --git a/lib/benchmark.rb b/lib/benchmark.rb
new file mode 100755
index 00000000..94126483
--- /dev/null
+++ b/lib/benchmark.rb
@@ -0,0 +1,17 @@
+#!/usr/bin/env ruby
+
+# frozen_string_literal: true
+
+require 'benchmark'
+
+path = ARGV[0]
+
+time = Benchmark.realtime do
+ system "bin/rake utils:reload_json[fixtures/#{path}.json]"
+end
+
+def printer(time)
+ puts "Processing time from file: #{time.round(4)}"
+end
+
+printer(time)
diff --git a/lib/importer.rb b/lib/importer.rb
new file mode 100644
index 00000000..2b50d752
--- /dev/null
+++ b/lib/importer.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+class Importer
+ TRUNCATE_SQL = <<~SQL.squish
+ TRUNCATE trips, services, cities, buses_services, buses CASCADE;
+ SQL
+
+ TRIPS_COMMAND = "copy trips (from_id, to_id, start_time, duration_minutes, price_cents, bus_id) from stdin with csv delimiter ';'"
+ CITIES_COMMAND = "copy cities (id, name) from stdin with csv delimiter ';'"
+ BUSES_COMMAND = "copy buses (id, number, model) from stdin with csv delimiter ';'"
+ BUSES_SERVICES_COMMAND = "copy buses_services (bus_id, service_id) from stdin with csv delimiter ';'"
+
+ @@city_by_name = {}
+ @@buses_by_number = {}
+ @@services = {}
+
+ def self.call(stream)
+ ActiveRecord::Base.connection.execute(TRUNCATE_SQL)
+ ActiveRecord::Base.transaction do
+ Service::SERVICES.each_with_index do |s, i|
+ @@services[s] ||= {}
+ @@services[s] = { id: i, name: s }
+ end
+
+ Service.import @@services.values
+
+ ActiveRecord::Base.connection.raw_connection.copy_data TRIPS_COMMAND do
+ raw = ActiveRecord::Base.connection.raw_connection
+ current_key = ''
+ nested_key = ''
+
+ current_obj = {}
+ nested_obj_lvl = 0
+ nested_arr_lvl = 0
+
+ parser = JSON::Stream::Parser.new
+ parser.start_object do
+ nested_obj_lvl += 1
+ end
+ parser.end_object do
+ nested_obj_lvl -= 1
+ if nested_obj_lvl.zero?
+ transaction(current_obj, raw)
+ current_obj = {}
+ end
+ end
+ parser.start_array { nested_arr_lvl += 1 }
+ parser.end_array { nested_arr_lvl -= 1 }
+
+ parser.key do |k|
+ if nested_obj_lvl.eql? 1
+ current_key = k
+ elsif nested_obj_lvl.eql? 2
+ nested_key = k
+ end
+ end
+
+ parser.value do |v|
+ if nested_arr_lvl.eql?(2) && nested_obj_lvl.eql?(2)
+ current_obj[current_key][nested_key] ||= []
+ current_obj[current_key][nested_key] << v
+ elsif nested_obj_lvl.eql? 2
+ current_obj[current_key] ||= {}
+ current_obj[current_key][nested_key] = v
+ elsif nested_obj_lvl.eql? 1
+ current_obj[current_key] = v
+ end
+ end
+
+ stream.each_char do |c|
+ parser << c
+ end
+ end
+
+ ActiveRecord::Base.connection.raw_connection.copy_data CITIES_COMMAND do
+ raw = ActiveRecord::Base.connection.raw_connection
+ @@city_by_name.each_value do |v|
+ raw.put_copy_data("#{v[:id]};#{v[:name]}\n")
+ end
+ end
+
+ ActiveRecord::Base.connection.raw_connection.copy_data BUSES_COMMAND do
+ raw_bus = ActiveRecord::Base.connection.raw_connection
+ @@buses_by_number.each_value do |v|
+ raw_bus.put_copy_data("#{v[:id]};#{v[:number]};#{v[:model]}\n")
+ end
+ end
+
+ ActiveRecord::Base.connection.raw_connection.copy_data BUSES_SERVICES_COMMAND do
+ raw_services = ActiveRecord::Base.connection.raw_connection
+ @@buses_by_number.each_value do |v|
+ next unless v[:services]
+
+ v[:services].each do |service|
+ raw_services.put_copy_data("#{v[:id]};#{@@services[service][:id]}\n")
+ end
+ end
+ end
+ end
+ end
+
+ def self.transaction(trip, connection)
+ from_obj = @@city_by_name[trip['from']]
+
+ unless from_obj
+ from_obj = { name: trip['from'], id: @@city_by_name.keys.size + 1 }
+ @@city_by_name[trip['from']] = from_obj
+ end
+
+ to_obj = @@city_by_name[trip['to']]
+
+ unless to_obj
+ to_obj = { name: trip['to'], id: @@city_by_name.keys.size + 1 }
+ @@city_by_name[trip['to']] = to_obj
+ end
+
+ bus_number = trip['bus']['number']
+
+ buses_obj = @@buses_by_number[bus_number]
+
+ unless buses_obj
+ buses_obj = { id: @@buses_by_number.keys.size + 1, number: bus_number, model: trip['bus']['model'],
+ services: trip['bus']['services'] }
+ @@buses_by_number[bus_number] = buses_obj
+ end
+
+ data = "#{from_obj[:id]};#{to_obj[:id]};#{trip['start_time']};#{trip['duration_minutes']};#{trip['price_cents']};#{buses_obj[:id]}\n"
+ connection.put_copy_data(data)
+ end
+end
diff --git a/lib/profiler.rb b/lib/profiler.rb
new file mode 100644
index 00000000..3ba10899
--- /dev/null
+++ b/lib/profiler.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'memory_profiler'
+require 'stackprof'
+require 'ruby-prof'
+
+class Profiler
+ REPORTS_DIR = 'reports'
+
+ class << self
+ def make_report(reporter_type)
+ send(reporter_type)
+ end
+
+ def memory_prof
+ report = MemoryProfiler.report do
+ system 'bin/rake utils:reload_json[fixtures/medium.json]'
+ end
+ report.pretty_print(scale_bytes: true)
+ end
+
+ def stack_prof
+ StackProf.run(mode: :object, out: "#{REPORTS_DIR}/stackprof.dump", raw: true) do
+ system 'bin/rake utils:reload_json[fixtures/medium.json]'
+ end
+ end
+
+ def ruby_prof
+ RubyProf.measure_mode = RubyProf::MEMORY
+ result = RubyProf.profile do
+ system 'bin/rake utils:reload_json[fixtures/medium.json]'
+ end
+ printer = RubyProf::CallTreePrinter.new(result)
+ printer.print(path: REPORTS_DIR, profile: 'profile')
+ end
+ end
+end
diff --git a/lib/reports_builder.rb b/lib/reports_builder.rb
new file mode 100755
index 00000000..89d39ad5
--- /dev/null
+++ b/lib/reports_builder.rb
@@ -0,0 +1,19 @@
+#!/usr/bin/env ruby
+
+# frozen_string_literal: true
+
+require_relative 'profiler'
+
+begin
+ prof_type = ARGV[0]
+
+ available_types = %w[
+ memory_prof
+ stack_prof
+ ruby_prof
+ ]
+
+ raise StandardError, "unknow profiler type: #{prof_type}" unless available_types.include?(prof_type)
+
+ Profiler.make_report(prof_type)
+end
diff --git a/lib/tasks/reports.rake b/lib/tasks/reports.rake
new file mode 100644
index 00000000..57969c22
--- /dev/null
+++ b/lib/tasks/reports.rake
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'profiler'
+
+namespace :reports do
+ task :build, [:reporter_type] => :environment do |_t, args|
+ reporter_type = args.reporter_type
+
+ available_types = %w[
+ memory_prof
+ stack_prof
+ ruby_prof
+ ]
+
+ abort "unknow profiler type: #{reporter_type}" unless available_types.include?(reporter_type)
+
+ Profiler.make_report(reporter_type)
+ end
+end
diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake
index 540fe871..cf033d2f 100644
--- a/lib/tasks/utils.rake
+++ b/lib/tasks/utils.rake
@@ -1,34 +1,15 @@
+# frozen_string_literal: true
+
# Наивная загрузка данных из json-файла в БД
# rake reload_json[fixtures/small.json]
-task :reload_json, [:file_name] => :environment do |_task, args|
- json = JSON.parse(File.read(args.file_name))
+require 'importer'
- ActiveRecord::Base.transaction do
- City.delete_all
- Bus.delete_all
- Service.delete_all
- Trip.delete_all
- ActiveRecord::Base.connection.execute('delete from buses_services;')
+namespace :utils do
+ task :reload_json, [:file_name] => :environment do |_task, args|
+ abort 'Send file path' unless args.file_name
- 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)
+ stream = File.open(args.file_name)
- Trip.create!(
- from: from,
- to: to,
- bus: bus,
- start_time: trip['start_time'],
- duration_minutes: trip['duration_minutes'],
- price_cents: trip['price_cents'],
- )
- end
+ Importer.call(stream)
end
end
diff --git a/make-compose.mk b/make-compose.mk
new file mode 100644
index 00000000..b5c0a923
--- /dev/null
+++ b/make-compose.mk
@@ -0,0 +1,25 @@
+compose:
+ docker-compose up -d
+
+compose-build:
+ docker-compose build
+
+compose-clear:
+ docker-compose down -v --remove-orphans || true
+
+compose-down:
+ docker-compose down || true
+
+compose-install:
+ docker-compose run --rm app make setup
+
+compose-logs:
+ docker-compose logs -f
+
+compose-restart:
+ docker-compose restart
+
+compose-stop:
+ docker-compose stop || true
+
+compose-setup: compose-down compose-build compose-install
\ No newline at end of file
diff --git a/make-services-app.mk b/make-services-app.mk
new file mode 100644
index 00000000..ec05fffd
--- /dev/null
+++ b/make-services-app.mk
@@ -0,0 +1,50 @@
+app-bash:
+ docker-compose run --rm app bash
+
+app-install-bundle:
+ docker-compose run --rm app bundle install --jobs $(shell nproc)
+
+app-update: app-update-bundle
+
+app-update-bundle:
+ docker-compose run --rm app bundle update --jobs $(shell nproc)
+
+app-debug:
+ docker attach --sig-proxy=false --detach-keys="ctrl-c" $(shell docker ps -q --filter publish=3000)
+
+app-rails-console:
+ docker-compose run --rm app make console
+
+app-rails:
+ docker-compose run --rm app bin/rails $(T)
+
+app-make:
+ docker-compose run --rm app make $(T)
+
+app-test:
+ docker-compose run --rm app make test
+
+app-fixtures-load:
+ docker-compose run --rm app make fixtures-load-json
+
+app-benchmark:
+ docker-compose run --rm app lib/benchmark.rb $(T)
+
+app-benchmark-small:
+ make app-benchmark T='small'
+
+app-benchmark-medium:
+ make app-benchmark T='medium'
+
+app-benchmark-large:
+ make app-benchmark T='large'
+
+app-report:
+ docker-compose run --rm app bin/rake reports:build[$(T)]
+
+app-lint-fix:
+ docker-compose run --rm app make linter-code-fix
+
+app-sistem-test:
+ docker-compose run --rm app bin/rails test:system
+
diff --git a/spec/importer_spec.rb b/spec/importer_spec.rb
new file mode 100644
index 00000000..c9de6a7e
--- /dev/null
+++ b/spec/importer_spec.rb
@@ -0,0 +1,13 @@
+require_relative 'rspec_helper'
+
+describe 'Performance reporter' do
+ let(:file_path) { 'fixtures/large.json' }
+ let(:time) { 1 }
+ let(:strem) { File.open(file_path) }
+
+ it 'create report' do
+ expect {
+ Importer.call(stream)
+ }.to perform_under(time).sec.warmup(60).times.sample(10).times
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 00000000..f557a75f
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,5 @@
+require 'rspec-benchmark'
+
+RSpec.configure do |config|
+ config.include RSpec::Benchmark::Matchers
+end
\ No newline at end of file
diff --git a/task-4_test b/task-4_test
new file mode 100644
index 00000000..840250c0
Binary files /dev/null and b/task-4_test differ
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
index d19212ab..e0ab8a4b 100644
--- a/test/application_system_test_case.rb
+++ b/test/application_system_test_case.rb
@@ -1,5 +1,10 @@
-require "test_helper"
+# frozen_string_literal: true
+
+require 'test_helper'
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
- driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
-end
+ driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] do |driver|
+ driver.add_argument('--no-sandbox')
+ driver.add_argument('--disable-gpu')
+ end
+end
\ No newline at end of file
diff --git a/test/controllers/trips_controller_test.rb b/test/controllers/trips_controller_test.rb
new file mode 100644
index 00000000..8a48eb32
--- /dev/null
+++ b/test/controllers/trips_controller_test.rb
@@ -0,0 +1,12 @@
+require 'test_helper'
+
+class TripsControllerTest < ActionDispatch::IntegrationTest
+ test '#index' do
+ from = cities(:moskow)
+ to = cities(:volgograd)
+
+ get "/buses/#{from.name}/#{to.name}"
+
+ assert_response :success
+ end
+end
\ No newline at end of file
diff --git a/test/fixtures/buses.yml b/test/fixtures/buses.yml
new file mode 100644
index 00000000..9b35c8cf
--- /dev/null
+++ b/test/fixtures/buses.yml
@@ -0,0 +1,4 @@
+ickarus:
+ number: 111
+ model: Икарус
+ services: split
\ No newline at end of file
diff --git a/test/fixtures/cities.yml b/test/fixtures/cities.yml
new file mode 100644
index 00000000..273e06ec
--- /dev/null
+++ b/test/fixtures/cities.yml
@@ -0,0 +1,8 @@
+DEFAULTS: &DEFAULTS
+ name: Moskow
+
+moskow:
+ <<: *DEFAULTS
+
+volgograd:
+ name: Volgograd
\ No newline at end of file
diff --git a/test/fixtures/files/data.json b/test/fixtures/files/data.json
new file mode 100644
index 00000000..c25dcc84
--- /dev/null
+++ b/test/fixtures/files/data.json
@@ -0,0 +1,21 @@
+[
+ {
+ "from":"Сочи",
+ "to":"Тула",
+ "start_time":"16:11",
+ "duration_minutes":83,
+ "price_cents":23354,
+ "bus":{
+ "number":"229",
+ "model":"Икарус",
+ "services":[
+ "Ремни безопасности",
+ "Кондиционер общий",
+ "Кондиционер Индивидуальный",
+ "Телевизор индивидуальный",
+ "Стюардесса",
+ "Можно не печатать билет"
+ ]
+ }
+ }
+]
\ No newline at end of file
diff --git a/test/fixtures/services.yml b/test/fixtures/services.yml
new file mode 100644
index 00000000..26dc7d4f
--- /dev/null
+++ b/test/fixtures/services.yml
@@ -0,0 +1,2 @@
+split:
+ name: Кондиционер общий
\ No newline at end of file
diff --git a/test/fixtures/trips.yml b/test/fixtures/trips.yml
new file mode 100644
index 00000000..5df16bb3
--- /dev/null
+++ b/test/fixtures/trips.yml
@@ -0,0 +1,7 @@
+moscow-volga:
+ from: moskow
+ to: volgograd
+ start_time: "12:00"
+ duration_minutes: 0
+ price_cents: 0
+ bus: ickarus
\ No newline at end of file
diff --git a/test/lib/tasks/utils_test.rb b/test/lib/tasks/utils_test.rb
new file mode 100644
index 00000000..0bdbf00a
--- /dev/null
+++ b/test/lib/tasks/utils_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+require 'rake'
+
+class UtilsTest < ActiveSupport::TestCase
+ def setup
+ Rails.application.load_tasks if Rake::Task.tasks.empty?
+ @path = 'test/fixtures/files/data.json'
+ @expected_result = {
+ from: 'Сочи',
+ to: 'Тула',
+ start_time: '16:11',
+ duration_minutes: 83,
+ price_cents: 23_354,
+ bus: {
+ number: '229',
+ model: 'Икарус',
+ services: [
+ 'Ремни безопасности',
+ 'Кондиционер общий',
+ 'Кондиционер Индивидуальный',
+ 'Телевизор индивидуальный',
+ 'Стюардесса',
+ 'Можно не печатать билет'
+ ]
+ }
+ }
+ end
+
+ test 'fixtures load' do
+ Rake::Task['utils:reload_json'].invoke(@path)
+
+ assert_equal Trip.last.to_h, @expected_result
+ end
+end
diff --git a/test/system/trips_test.rb b/test/system/trips_test.rb
new file mode 100644
index 00000000..fd40bb55
--- /dev/null
+++ b/test/system/trips_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'application_system_test_case'
+
+class TripsTest < ApplicationSystemTestCase
+ test 'visit index page' do
+ from = cities(:moskow)
+ to = cities(:volgograd)
+
+ visit "/buses/#{from.name}/#{to.name}"
+
+ page.assert_selector('h1', text: "Автобусы #{from.name} – #{to.name}")
+ page.assert_selector('h2', text: "В расписании 1 рейс")
+ page.assert_selector('li', text: "Отправление: 12:00")
+ page.assert_selector('li', text: "Прибытие: 12:00")
+ page.assert_selector('li', text: "В пути: 0ч. 0мин.")
+ page.assert_selector('li', text: "Цена: 0р. 0коп.")
+ page.assert_selector('li', text: "Сервисы в автобусе:")
+ page.assert_selector('li', text: "Кондиционер общий")
+ end
+end
\ No newline at end of file
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 3ab84e3d..3f273187 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,10 +1,14 @@
+# frozen_string_literal: true
+
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
-class ActiveSupport::TestCase
- # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
- fixtures :all
+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...
+ # Add more helper methods to be used by all tests here...
+ end
end