diff --git a/Gemfile b/Gemfile index e20b1260..721e3e40 100644 --- a/Gemfile +++ b/Gemfile @@ -24,3 +24,10 @@ end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +gem 'perfolab', github: 'Tab10id/perfolab', branch: :temp_26 + +gem "activerecord-import", "~> 1.4" + +gem "rack-mini-profiler" +gem "oj", "~> 3.14" diff --git a/Gemfile.lock b/Gemfile.lock index fccf6f5f..75c13d47 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,15 @@ +GIT + remote: https://github.com/Tab10id/perfolab.git + revision: b75ed48af9aa69affc28a4034d47221a1690db26 + branch: temp_26 + specs: + perfolab (0.1.0) + memory_profiler (~> 1.0) + ruby-prof (~> 1.4, < 1.4.4) + stackprof (~> 0.2.0) + tabulo (~> 2.0) + zeitwerk (~> 2.6) + GEM remote: https://rubygems.org/ specs: @@ -33,6 +45,8 @@ GEM activemodel (= 5.2.3) activesupport (= 5.2.3) arel (>= 9.0) + activerecord-import (1.4.1) + activerecord (>= 4.2) activestorage (5.2.3) actionpack (= 5.2.3) activerecord (= 5.2.3) @@ -67,8 +81,11 @@ GEM mini_mime (>= 0.1.1) marcel (0.3.3) mimemagic (~> 0.3.2) + memory_profiler (1.0.1) method_source (0.9.2) - mimemagic (0.3.3) + mimemagic (0.3.10) + nokogiri (~> 1) + rake mini_mime (1.0.1) mini_portile2 (2.4.0) minitest (5.11.3) @@ -76,9 +93,12 @@ GEM nio4r (2.3.1) nokogiri (1.10.2) mini_portile2 (~> 2.4.0) + oj (3.14.2) pg (1.1.4) puma (3.12.1) rack (2.0.6) + rack-mini-profiler (3.0.0) + rack (>= 1.2.0) rack-test (1.1.0) rack (>= 1.0, < 3) rails (5.2.3) @@ -109,6 +129,7 @@ GEM rb-fsevent (0.10.3) rb-inotify (0.10.0) ffi (~> 1.0) + ruby-prof (1.4.3) ruby_dep (1.5.0) sprockets (3.7.2) concurrent-ruby (~> 1.0) @@ -117,10 +138,16 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + stackprof (0.2.24) + tabulo (2.8.2) + tty-screen (= 0.8.1) + unicode-display_width (~> 2.2) thor (0.20.3) thread_safe (0.3.6) + tty-screen (0.8.1) tzinfo (1.2.5) thread_safe (~> 0.1) + unicode-display_width (2.4.2) web-console (3.7.0) actionview (>= 5.0) activemodel (>= 5.0) @@ -129,16 +156,21 @@ GEM websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) + zeitwerk (2.6.7) PLATFORMS ruby DEPENDENCIES + activerecord-import (~> 1.4) bootsnap (>= 1.1.0) byebug listen (>= 3.0.5, < 3.2) + oj (~> 3.14) + perfolab! pg (>= 0.18, < 2.0) puma (~> 3.11) + rack-mini-profiler rails (~> 5.2.3) tzinfo-data web-console (>= 3.3.0) diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb index acb38be2..eb2dee12 100644 --- a/app/controllers/trips_controller.rb +++ b/app/controllers/trips_controller.rb @@ -2,6 +2,6 @@ class TripsController < ApplicationController def index @from = City.find_by_name!(params[:from]) @to = City.find_by_name!(params[:to]) - @trips = Trip.where(from: @from, to: @to).order(:start_time) + @trips = Trip.where(from: @from, to: @to).preload(bus: :services).order(:start_time) end end 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..08a8eb09 100644 --- a/app/views/trips/_trip.html.erb +++ b/app/views/trips/_trip.html.erb @@ -1,5 +1,13 @@ -
  • <%= "Отправление: #{trip.start_time}" %>
  • -
  • <%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
  • -
  • <%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
  • -
  • <%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
  • -
  • <%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
  • + \ No newline at end of file diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb index a60bce41..d123e89e 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -5,12 +5,4 @@ <%= "В расписании #{@trips.count} рейсов" %> -<% @trips.each do |trip| %> - - <%= render "delimiter" %> -<% end %> +<%= render partial: 'trip', collection: @trips, spacer_template: 'delimiter' %> diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..093b25cc --- /dev/null +++ b/case-study.md @@ -0,0 +1,316 @@ +# Оптимизация взаимодействия с БД + +## User story + +Тут должны быть описаны пользовательские истории которые определят целевые +значения метрик, но так как их нет, то я их придумаю. + +### Импорт данных + +Для проведения нагрузочного тестирования на тестовом стенде необходимо регулярно +импортировать различные наборы данных о расписании автобусов из больших +текстовых файлов, файлы получаются из внешних систем поэтому нет возможности +осуществлять накат предварительно сформированного sql-дампа. +Текущее решение не позволяет выполять эту операцию быстро из-за чего подготовка +тестовой среды происходит непозволительно долгое время чем блокирует всю +дальнейшую работу пайплайна. + +### Отображение расписания + +Система веб-аналитики показывает, что при просмотре расписаний на популярных +направлениях пользователи не дожидаются загрузки страницы и уходят с сайта. +Британские учёные доказали, что финансово успешные web-проекты должны +формировать основной документ страницы не больше чем за 300 миллисекунд. + +## Что сделать + +* Нужно оптимизировать механизм перезагрузки расписания из файла + * файл large.json должен обрабатывался не более минуты. +* Необходимо ускорить отображение расписаний + * страница автобусы/Самара/Москва должна открываться менее чем за 300мс + +# Оптимизация импорта + +## Основные инструменты для исследования + +* perfolab (чуть доведенный до ума фреймворк разработанный мной в рамках предыдущего задания) +* stackprof +* speedscope + +## Основные проблемы на начальном этапе + +* Отсутствие тестов. + * Нет возможности проводить оптимизацию без опасения что-либо сломать. +* Вся процедура выполняется в едином блоке кода без разбивки на методы. + * Сложно выяснить что именно вызывает основные проблемы. + +## Первичный анализ + +Замеры времени импорта показывют что оно растёт приблизительно линейно в +зависимости от размера файла, что несложно увидеть и при обычном чтении +оригинального скрипта. Инструменты показывают что большую часть времени +выполняются запросы на поиск и создание объектов. + +Уже на данном этапе очевидно что необходимо кардинально уменьшать количество +отправляемых в БД запросов. Внимательное изучение скрипта показывает что +от запросов на поиск данных в бд можно полностью избавиться, так как изначально +в БД нет никаких данных. Уменьшения же количества запросов на вставку данных +можно добиться с помощью массового создания объектов. + +Для удобства можно воспользоваться библиотекой activerecord-import. + +## Первые шаги + +* Код импорта был выделен в отдельный класс для удобства анализа и тестрования. +* Был написан минимальный тест на корректнось работы скрипта. +* Произведен первичный анализ скрипта на файле small.json + +| benchmark | Previous | Current | Diff % | +|--------------------------------|----------|---------|-----------------| +| total | | 3014ms | | + +## Выделение методов + +Хоть у нас и есть инсайдерская информация о частоте появления уникальных записей +различных моделей, всё же будет намного удобнее ориентироваться в результатах +профилировщиков если выделить создание разных объектов в отельные методы. +Как и ожидалось, выделение методов не оказало значительного влияния на время +работы импорта. + +| benchmark | Previous | Current | Diff % | +|--------------------------------|-----------|---------|----------------| +| total | 3014ms | 2976ms | -1% | + +Кроме того, сразу видно что 59% времени уходит на создание и обновление записей +об автобусах. + +## Оптимизация создания автобусов + +При внимательном изучении механизма создания автобусов становится очевидным +что выполняются множество лишних действий: +* лишнее обновление записи, имя модели выставляется уже после создания; +* обновление модели и услуг происходит каждый раз, даже если автобус уже есть в БД; +* поиск автобусов в БД, хотя их создание происходило в этом же скрипте. + +Результат исправления: + +| benchmark | Previous | Current | Diff % | +|--------------------------------|----------|---------|-----------------| +| total | 2976ms | 2628ms | -11% | + +## Оптимизация выставления связей между автобусами и услугами + +Результат улучшения оказался не столь существенным для файла small.json, +на обновление данных об услугах в автобусах всё ещё уходит около 45% времени, +это говорит о том, что нужно оптимизировать выставление связей между автобусами +и сервисами. +Оптимизировать это можно несколькими способами, но самым эффективным будет +массовое выставление связей уже после создания записей автобусов. + +Результат исправления: + +| benchmark | Previous | Current | Diff % | +|--------------------------------|----------|----------|-----------------| +| total | 2628ms | 1450ms | -44% | + +## Создание сервисов + +На поиск и создание сервисов уходит 34% времени. При этом заранее известно что +их ровно 10 (валидация на имя в модели и подсказка в Readme.md). +Вместо того чтобы осуществлять поиск и создание, можно было бы заранее создать +все 10 записей игнорируя данные из json. Но так как у нас есть требование на +полное отсутствие функциональных изменений, то отказываемся от этой идеи, так +как для файла example.json должно быть создано только 3 сервиса. +Так или иначе можно избавиться от необходимости поиска записей в БД. + +| benchmark | Previous | Current | Diff % | +|--------------------------------|----------|----------|-----------------| +| total | 1450ms | 885ms | -38% | + +## Импорт расписаний + +Наконец дошли до основной модели. Замеры показывают что 39% времени уходит на +создание записей модели Trip. +Так же как и с привязкой услуг к автобусам используем массовый импорт заранее +подготовленных данных. + +| benchmark | Previous | Current | Diff % | +|--------------------------------|----------|----------|-----------------| +| total | 885ms | 626ms | -29% | + +## Оптимизация создания городов + +Теперь 38% времени уходит на создание записей модели City. Так же как и ранее +убираем поиск сохраненных записей в БД. + +| benchmark | Previous | Current | Diff % | +|--------------------------------|----------|----------|-----------------| +| total | 626ms | 357ms | -43% | + +## Предварительные итоги + +Проверка иморта файла large.json показала что время импорта составляет около +12 секунд, что уже существенно ниже исходных требований. +На файле small.json stackprof показывает что около 50% времени уходит на +создание записей модели автобуса, однако на файлах medium.json и large.json +на данное действие уходит намного меньше времени в процентном отношении (24% +и 3,5% соответственно). + +На больших файла на первый план выходит уже работа массового импорта записей. +Причём много времени уходит на валидации записей. +Можно было бы отключить их, но строго говоря это действие является +функциональным изменением, так как оригинальный скрипт просто бы пропустил +навалидные записи. + +Дальнейший анализ будет производиться на файле medium.json, но уже после +выполнения второй части задания. Вероятно решение второй части зададания +несколько замедлит работу импорта, так как наверняка в рамках изменений +потребуется добавить индексы, что должно несколько замедлить добавление записей. + +# Ускорение отображения расписаний + +## Основные инструменты для исследования + +* rails log +* rack-mini-profiler + +## Первичный анализ + +Страница расписания рейсов из Самары в Москву генерируется около 2 секунд +в dev-окружении и около 1,2 секунды в production окружении. +При этом время на работу с БД относительно небольшое: +Production: Views: 934.6ms | ActiveRecord: 356.4ms +Development: Views: 1714.1ms | ActiveRecord: 242.4ms. +Вызывает вопросы почему время на ActiveRecord в Development-окружении +отображается как меньшее, пока решил отложить этот вопрос. +Очевидно тут стоило бы заняться в первую очередь вопросом генерации вьюх, +но так как тема задания касается оптимизации БД, то пока проигнорируем это. + +## Анализ с использованием rack-mini-profiler + +Для удобства решил выполнять основную работу по оптимизации в development. +Подключение rack-mini-profiler сразу же ухудшило показатели +Views: 5273.9ms | ActiveRecord: 737.0ms, тем не менее общая картина должна +быть той же. + +## Двойная проблема N+1 (автобусы и сервисы) + +Даже без установки bullet в логе видно огромное количество однотипных запросов. +Добавил preload на обе модели. +Время генерации страницы сократилось вдвое, при этом на работу ActiveRecord +теперь уходит всего 22ms (если верить логу rails) +Views: 2928.7ms | ActiveRecord: 22.0ms. +Предполагаю что уменьшение времени на вьюхи обусловлено меньшим временем для +формирования отчёта rack-mini-profiler, но это не точно, так как в production +время тоже значительно уменьшилось. +С отключенным rack-mini-profiler: +Production: Views: 345.8ms | ActiveRecord: 22.2ms +Development: Views: 984.0ms | ActiveRecord: 17.3ms + +## Оптимизация рендеринга + +Как бы ни хотелось продолжить, но в дальнейшей оптимизации запросов не вижу +смысла, можно было бы еще добавить индекс на start_time таблицы trips, +но это ускорило бы и без того быстрый запрос (на моём ноутбуке этот запрос +выполняется за 3мс) и в любом случае это не позволит добиться придуманного мной +целевого показателя. + +Поэтому единственным способом достижения цели является оптимизация реднеринга. +Используя механизм render collection удалось добиться формирования страницы +в production окружении в пределах 150-200 мс без использования кэширования, +что соответствует целевым показателям. + +# Оптимизация импорта: империя наносит ответный удар + +Снова придумываю пользовательскую историю. +Отдел продаж начал продавать функционалость позволяющую провести миграцию +с системы конкурентов. + +Объем данных в исходной системе может быть произвольным. +Формат данных для импорта тот же и не может быть изменён. + +## Что сделать + +Нужно оптимизировать механизм загрузки данных из файла таким образом, чтобы +потребление памяти не зависило от объёма данных. + +## Общий план действий + +Необходимо реализизовать потоковое чтение и импорт данных. +Основными проблемами является то, что данные нужно импортировать в несколько +таблиц, нет возможности продолжительное время "накапливать" данные для +последующего импорта. +До начала работ по оптимизации желательно уточнить фунциональные требования: +* необходимось сохранения вызова валидаций при создании объектов; +* необходимость выполнения всех действий в рамках единой транзакции. + +Для интереса я буду исходить из необходимости сохранения старого поведения, +поэтому вариант с импортом данных напрямую в PG из Readme.md будет +нецелесообразен, так как всё равно портребует создания объектов для запуска +валидаций. + +Текущая идея состоит в том, чтобы при потоковом чтении из исходного файла +формировать буфер записей определённого размера с последующим импортом +с помощью activerecord-import. + +## Потоковое чтение и импорт блоками + +Для реализации потокового чтения была использована библиотека oj. +Благодаря использованию метода `Oj.sc_parse` и написанию простого +вспомогательного класса удалось сохранить код практически в неизменном виде. +Кроме того была использована встроенная библиотека zlib для возможности +поточного чтения из gz-файла, тем самым избавляя от необходимости распаковки +файлов большого размера. + +Оверхед добавленный механизмом потокового чтения оказался незначительным. + +| benchmark | Previous | Current | Diff % | +|--------------------------------|-----------|---------|-----------------| +| total | 1498ms | 1531ms | 2% | + +## Оптимизация activerecord-import + +Инструменты показывают что большую часть времени скрипт производит импорт данных +с помощью библиотеки activerecord-import. +К сожалению возможности настройки параметров иморта доволько ограничены. +Большая часть времени уходит на валидации, но, как было сказано ранее, от них я +отказываться не плананирую. +Можно было бы на время импорта отключить часть валидаций в тех случаях когда мы +заранее уверены в корректности данных, но этим мне уже заниматься довольно +лениво, к тому же это действие вряд ли бы дало больше 5 процентов уменшения +времени работы скрипта (валидации занимаю 17% от полного времени работы). + +## Создание AR-объектов + +На создание объектов уходит 20% времени. Существенного улучшения результатов +можно добиться только если произвести полный отказ от ActiveRecord, +а следовательно и от встроенных валидаций. +По примерной оценке такая работа могда бы уменьшить время работы импорта +на 60-70%, но данная работа сделала бы скрипт менее поддерживаемым из-за +расхождения валидаций в скрипте импорта и реальных моделей. Вынесение же +валидаций в отдельный модуль не выглядит целесообразным. + +## Возвращаемся к импорту автобусов + +На создание автобусов уходит 26% времени. На текущий момент автобусы создаются +сразу же по одной записи. Можно было бы произвести массовый импорт автобусов +по аналогии с другими моделями, но это может создать проблемы в будущем. +Причина в том, что связь с автобусами явно указывается в модели Trip. +А следовательно записи модели Bus должны быть добавлены до записей модели Trip. +Как известно, на 1000 записей Trip приходится около 1 записи модели Bus, +следовательно для того чтобы массово сохранить хотя бы 10 записей модели Bus +на нужно было бы накопить 10000 записей модели Trip. + +От данного ограничения можно было бы избавиться если вручную выставить +идентификаторы модели Bus до фактического сохранения, но и тут есть проблема, +так как если в будущем кто-то настроит внешние ключи на колонки в БД, +то скрипт внезапно перестанет работать и придётся настраивать отключение +проверки внешних ключей. + +Таким образом, данную оптимизацию так же считаю нецелесообразной. + +## Результаты + +* large.json: 12 секунд +* 1M.json.gz: 2,5 минуты +* 10M.json.gz: предположительно 25-30 минут. diff --git a/fixtures/example.json b/fixtures/example.json index 510b4c2d..2ce50934 100644 --- a/fixtures/example.json +++ b/fixtures/example.json @@ -46,10 +46,10 @@ }, { "bus": { - "model": "Икарус", - "number": "123", + "model":"ГАЗ", + "number":"584", "services": [ - "Туалет", + "Кондиционер общий", "WiFi" ] }, diff --git a/lib/my_app/import.rb b/lib/my_app/import.rb new file mode 100644 index 00000000..7f423ef5 --- /dev/null +++ b/lib/my_app/import.rb @@ -0,0 +1,147 @@ +module MyApp + class Import + attr_reader :file_name, :batch_size + BusesService = Class.new(ActiveRecord::Base) + + class TripsJsonStreamHandler < ::Oj::ScHandler + def initialize(&block) + @root = true + @block = block + end + + def hash_start + {} + end + + def hash_set(h, k, v) + h[k] = v + end + + def array_start + if @root + @root = false + return + else + [] + end + end + + def array_append(a, v) + if a + a << v + else + @block.call(v) + end + end + end + + def initialize(file_name, batch_size: 1000) + @file_name = file_name + @batch_size = batch_size + @imported_buses = {} + @imported_services = {} + @imported_cities = {} + @imported_trips = [] + @buses_services = [] + end + + def call + ActiveRecord::Base.transaction do + db_clear + import + end + end + + private + + def import + parse do |trip| + from = find_or_create_city(trip['from']) + to = find_or_create_city(trip['to']) + service_names = trip['bus']['services'] + services = find_or_create_services(service_names) + bus_number = trip['bus']['number'] + bus_model = trip['bus']['model'] + bus = find_or_create_bus(bus_number, bus_model, services) + + start_time = trip['start_time'] + duration = trip['duration_minutes'] + price = trip['price_cents'] + add_trip(bus, duration, from, price, start_time, to) + end + BusesService.import(@buses_services) + Trip.import(@imported_trips) + end + + def db_clear + City.delete_all + Bus.delete_all + Service.delete_all + Trip.delete_all + ActiveRecord::Base.connection.execute('delete from buses_services;') + end + + def parse(&block) + io = + if File.extname(file_name).downcase == '.gz' + Zlib::GzipReader.open(file_name) + else + File.open(file_name, 'r') + end + Oj.sc_parse(TripsJsonStreamHandler.new(&block), io) + ensure + io.close + end + + def find_or_create_city(city_name) + @imported_cities[city_name] ||= City.create(name: city_name) + end + + def find_or_create_services(service_names) + service_names.map do |service| + find_or_create_service(service) + end + end + + def find_or_create_service(service_name) + @imported_services[service_name] ||= Service.create(name: service_name) + end + + def find_or_create_bus(bus_number, model, services) + bus = @imported_buses[bus_number] + unless bus + bus = Bus.create!(number: bus_number, model: model) + services.each do |service| + add_bus_service(bus, service) + end + @imported_buses[bus_number] = bus + end + bus + end + + def add_bus_service(bus, service) + @buses_services << { bus_id: bus.id, service_id: service.id } + if @buses_services.size == batch_size + BusesService.import(@buses_services) + @buses_services = [] + end + end + + def add_trip(bus, duration, from, price, start_time, to) + @imported_trips << + Trip.new( + from: from, + to: to, + bus: bus, + start_time: start_time, + duration_minutes: duration, + price_cents: price, + ) + + if @imported_trips.size == batch_size + Trip.import(@imported_trips) + @imported_trips = [] + end + end + end +end diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 540fe871..2b63a9bc 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,34 +1,7 @@ # Наивная загрузка данных из 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 Rails.root.join('lib/my_app/import') - ActiveRecord::Base.transaction do - City.delete_all - Bus.delete_all - Service.delete_all - Trip.delete_all - ActiveRecord::Base.connection.execute('delete from buses_services;') - - json.each do |trip| - from = City.find_or_create_by(name: trip['from']) - to = City.find_or_create_by(name: trip['to']) - services = [] - trip['bus']['services'].each do |service| - s = Service.find_or_create_by(name: service) - services << s - end - bus = Bus.find_or_create_by(number: trip['bus']['number']) - bus.update(model: trip['bus']['model'], services: services) - - Trip.create!( - from: from, - to: to, - bus: bus, - start_time: trip['start_time'], - duration_minutes: trip['duration_minutes'], - price_cents: trip['price_cents'], - ) - end - end + MyApp::Import.new(args.file_name, batch_size: ENV.fetch('BATCH_SIZE', 1000).to_i).call end diff --git a/loop.sh b/loop.sh new file mode 100755 index 00000000..a8a02ee9 --- /dev/null +++ b/loop.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +bundle exec rake test + +export RAILS_ENV=production +bundle exec ruby bin/rails runner perfolab/my_app/import_stand.rb \ No newline at end of file diff --git a/perfolab/my_app/import_stand.rb b/perfolab/my_app/import_stand.rb new file mode 100644 index 00000000..91ae06f6 --- /dev/null +++ b/perfolab/my_app/import_stand.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'my_app/import' +require 'perfolab' + +loop = + PerfoLab::Loop.new do |toolbox| + toolbox.add_tool( + :stackprof, + type: :stackprof, + config: { + raw: true + }, + runner_options: { + arguments: ['medium'] + } + ) + toolbox.add_tool( + :benchmark, + type: :benchmark, + runner_options: { + gc_disable: false, + warmup: 1, + arguments: ['medium'] + } + ) + end + +loop.analyze do |filename| + MyApp::Import.new(Rails.root.join('fixtures', "#{filename}.json")).call +end diff --git a/test/lib/my_app/import_test.rb b/test/lib/my_app/import_test.rb new file mode 100644 index 00000000..565b05b8 --- /dev/null +++ b/test/lib/my_app/import_test.rb @@ -0,0 +1,29 @@ +require 'test_helper' +require 'my_app/import' + +class TestMe < Minitest::Test + def test_result + MyApp::Import.new(Rails.root.join("fixtures", 'example.json')).call + assert_equal 10, Trip.count + assert_equal 2, Bus.count + assert_equal 3, Service.count + assert_equal 2, City.count + gaz = Bus.find_by!(number: '584', model: 'ГАЗ') + icarus = Bus.find_by!(number: '123', model: 'Икарус') + assert_equal ['Кондиционер общий', 'WiFi'], gaz.services.map(&:name) + assert_equal ['Туалет', 'WiFi'], icarus.services.map(&:name) + assert_equal 1, gaz.trips.size + gaz_trip = gaz.trips.first + gaz_trip_attributes = gaz_trip.attributes + assert_equal( + { + 'duration_minutes' => 315, + 'price_cents' => 969, + 'start_time' => '18:30', + }, + gaz_trip_attributes.slice('duration_minutes', 'price_cents', 'start_time') + ) + assert_equal 'Самара', gaz_trip.from.name + assert_equal 'Москва', gaz_trip.to.name + end +end \ No newline at end of file