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}" %>
  • + + 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 @@ -
  • Сервисы в автобусе:
  • - 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? %> +
  • Сервисы в автобусе:
  • + +<% 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 "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