diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ae8da484 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/fixtures/* +/reports/* \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..a66b72cb --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +ruby '3.3.4' + +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + + +gem 'pry' +gem 'memory_profiler' +gem 'rspec-benchmark' +gem 'ruby-prof' +gem 'stackprof' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..41e7df1e --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,50 @@ +GEM + remote: https://rubygems.org/ + specs: + benchmark-malloc (0.2.0) + benchmark-perf (0.6.0) + benchmark-trend (0.4.0) + coderay (1.1.3) + diff-lcs (1.5.1) + memory_profiler (1.1.0) + method_source (1.1.0) + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.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.2) + 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) + ruby-prof (1.7.1) + stackprof (0.2.27) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + memory_profiler + pry + rspec-benchmark + ruby-prof + stackprof + +RUBY VERSION + ruby 3.3.4p94 + +BUNDLED WITH + 2.5.23 diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..56e335d3 --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +unzip: + gzip -dk fixtures/data_large.txt.gz + +prepare_data: + make unzip + head -n 200000 fixtures/data_large.txt > fixtures/data_smal.txt + head -n 50000 fixtures/data_large.txt > fixtures/data_smal_50000.txt + head -n 10000 fixtures/data_large.txt > fixtures/data_smal_10000.txt + +test: + ruby tests/reporter_test.rb + +perform_test: + rspec tests/reporter_spec.rb + +memeory_prof: + ./bin/report_builder.rb memeory_prof + +memeory_prof_to_file: + ./bin/report_builder.rb memeory_prof > reports/memeory_prof_report.txt + +stack_prof: + ./bin/report_builder.rb stack_prof + +open_stack_prof: + stackprof reports/stackprof.dump + +open_stack_prof_by_method: + stackprof reports/stackprof.dump --method $(T) + +open_stack_prof_by_img: + stackprof --graphviz reports/stackprof.dump > reports/graphviz.dot + dot -Tpng reports/graphviz.dot > reports/graphviz.png + +ruby_prof: + ./bin/report_builder.rb ruby_prof + +open_ruby_prof: + qcachegrind reports/$(T) + +all_reports: + make memeory_prof + make stack_prof + make ruby_prof + +run: + ./bin/runner + +benchmark: + ./bin/benchmark.rb + + diff --git a/bin/benchmark.rb b/bin/benchmark.rb new file mode 100755 index 00000000..abc64e0b --- /dev/null +++ b/bin/benchmark.rb @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby + +# frozen_string_literal: true + +require 'benchmark' +require_relative '../lib/profiler' + + +time = Benchmark.realtime do + work('fixtures/data_large.txt', 'result.json', { watcher_enable: true }) +end + + + +def printer(time, rows = 1000) + pp "Processing time from file: #{time.round(4)}" +end + +printer(time) + + diff --git a/bin/report_builder.rb b/bin/report_builder.rb new file mode 100755 index 00000000..90f8a6f4 --- /dev/null +++ b/bin/report_builder.rb @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby + +# frozen_string_literal: true + +require_relative '../lib/profiler' + +begin + prof_type = ARGV[0] + + available_types = %w[ + memeory_prof + stack_prof + ruby_prof + ] + +raise StandardError, "unknow profiler type: #{prof_type}" unless available_types.include?(prof_type) + + Profiler.make_report(prof_type) +end \ No newline at end of file diff --git a/bin/runner b/bin/runner new file mode 100755 index 00000000..547957e0 --- /dev/null +++ b/bin/runner @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require_relative "../lib/reporter" + +work('fixtures/data_large.txt', 'result.json', { watcher_enable: true }) \ No newline at end of file diff --git a/case-study-template.md b/case-study-template.md deleted file mode 100644 index c3279664..00000000 --- a/case-study-template.md +++ /dev/null @@ -1,55 +0,0 @@ -# Case-study оптимизации - -## Актуальная проблема -В нашем проекте возникла серьёзная проблема. - -Необходимо было обработать файл с данными, чуть больше ста мегабайт. - -У нас уже была программа на `ruby`, которая умела делать нужную обработку. - -Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. - -Я решил исправить эту проблему, оптимизировав эту программу. - -## Формирование метрики -Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* - -## Гарантия корректности работы оптимизированной программы -Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. - -## Feedback-Loop -Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* - -Вот как я построил `feedback_loop`: *как вы построили feedback_loop* - -## Вникаем в детали системы, чтобы найти главные точки роста -Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* - -Вот какие проблемы удалось найти и решить - -### Ваша находка №1 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -### Ваша находка №2 -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -### Ваша находка №X -- какой отчёт показал главную точку роста -- как вы решили её оптимизировать -- как изменилась метрика -- как изменился отчёт профилировщика - -## Результаты -В результате проделанной оптимизации наконец удалось обработать файл с данными. -Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* и уложиться в заданный бюджет. - -*Какими ещё результами можете поделиться* - -## Защита от регрессии производительности -Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..14d74ff8 --- /dev/null +++ b/case-study.md @@ -0,0 +1,119 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с данными, чуть больше ста мегабайт. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Порядок роста используемой помяти в начеле оптимизации + +2025-02-01 15:17:52 +0300: 25 MB +2025-02-01 15:17:53 +0300: 139 MB +2025-02-01 15:17:54 +0300: 147 MB +2025-02-01 15:17:56 +0300: 155 MB +2025-02-01 15:17:57 +0300: 166 MB +2025-02-01 15:17:59 +0300: 177 MB +2025-02-01 15:18:00 +0300: 186 MB +2025-02-01 15:18:02 +0300: 196 MB +2025-02-01 15:18:03 +0300: 205 MB +2025-02-01 15:18:05 +0300: 227 MB +2025-02-01 15:18:06 +0300: 237 MB +2025-02-01 15:18:08 +0300: 245 MB +2025-02-01 15:18:09 +0300: 256 MB +2025-02-01 15:18:10 +0300: 274 MB +2025-02-01 15:18:12 +0300: 290 MB + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: программа затрачивает на обработку больших файлов не больше `70MB` +Перед оптимизацией программа затрачивает на обработку файла в 50000 строк 335 MB + +## Гарантия корректности работы оптимизированной программы +Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: + +1. Сформировал короткие удобные алиаасы, что бы быстро получать обратную связь +2. Быстро запустил профилировщики(`make all_reports`) -> посмотрел отчеты, нашел точку роста -> внес изменения и снова запусти профилировщик +3. Создал отдельный вотчер, который запускается в отделном треде и проверяет количество используемой памяти(пишет лог), если оно привышает пороговое значение то выбрасывает исключение + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +1. отчет `memory prof` показал что основная точка роста которая потребляет много памяти это соединение двух массивов при обходе строк(на файле в 10000 строк потребление памяти 287 MB, общее потребление 440.21 MB) +2. Заменил `users + [parse_user(line)]` на `users << parse_user(line)` +3. Метрика изминилась - на файле 10000 общее потребление снизилось до 143.47 MB +4. Точка роста перестала быть проблеммойчя + +### Ваша находка №2 +1. Следующая точка роста `sessions.select { |session| session['user_id'] == user['id'] }` - потребляет памяти 104.07 MB на 10000 строк +2. Поскольку метод селект каждый раз создает новый массив, а старый оставляет без изминений, написал собственный метод фильтрации + +```ruby +class Array + def custom_filter! + i = 0 + acc = [] + while i < self.size + if yield(self[i]) + acc << self[i] + self.delete(self[i]) + next + end + i += 1 + end + acc + end +end +``` + +3. Алокация снизилась до 29.99 MB на 10000 строках +4. Данная проблемма перестала быть точкой роста + +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +### Ваша находка №3 +1. Отчет `stack_prof` показывает что теперь алоцированно много объектов и памяти при парсинге даты и разбитие строки метод `split` +2. Убрал парсинг даты так как она уже в нужном формате, `split` при обходе строк заменил на `line =~ /user/` +3. алокация на 10000 строк уменьшилась до 10 MB +4. Данная проблемма перестала быть точкой роста + +### Ваша находка №4 +1. отчет `memory prof` показал что теперь много памяти алоцируется при чтении файла и при создании промежуточных объектов пользователя +2. Сделал стриминговое чтение данных(читаю файл по строчно), и не создаю промежуточных объектов пользователя +3. Потребление памяти снизилось до 65.42 MB на 100_000 строк +4. Данная проблемма перестала быть точкой роста +- какой отчёт показал главную точку роста +- как вы решили её оптимизировать +- как изменилась метрика +- как изменился отчёт профилировщика + +### Ваша находка №5 +1. отчет `memory pro` и вотчер показывает что осноную память программа расходует когда накапливает данные и десеарилизует их затем в JSON(на большых данных 1340 MB) +2. Решил сделать программу полностью потоковой, то есть сразу писать в результирующий отчет и максимум накапливать данные по одному пользователю +3. Потребление памяти снизилось до константног - на полный отчет тратится всего 27 MB, так же увеличилась скорость 12.9!!!!! c за полный отчет +4. Данная проблемма перестала быть точкой роста и мы уложились в бюджет + + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с 1400 MB до 27 MB и уложиться в заданный бюджет. + +1. Увиличилась скорость выполнения по сравнению с первым заданием до 13 с не смотря на большое количество IO операций + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написал перформанс тест который проверяет что программа потребляет не более 70 MB памяти diff --git a/data_large.txt.gz b/data_large.txt.gz deleted file mode 100644 index 91c7e45e..00000000 Binary files a/data_large.txt.gz and /dev/null differ diff --git a/lib/process_watcher.rb b/lib/process_watcher.rb new file mode 100644 index 00000000..eae84783 --- /dev/null +++ b/lib/process_watcher.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class ProcessWatcher + LOG_FILE_PATH = 'fixtures/log.txt'.freeze + + attr_reader :pid, :limit + + class LongMemoryUsageError < StandardError; end + + def initialize(pid:, limit:) + @pid = pid + @limit = limit + end + + def watch + File.write(LOG_FILE_PATH, '') + f = File.open(LOG_FILE_PATH, 'w') + thread = Thread.new(pid, f) do |process_pid, file| + process = true + + while process + memory = "%d" % (`ps -o rss= -p #{process_pid}`.to_i / 1024) + f.write("#{Time.now}: #{memory} MB \n") + raise LongMemoryUsageError, "usage memory to long #{memory}" if memory.to_i > limit + # sleep(1) + end + rescue => e + file.close + raise e + end + thread.abort_on_exception = true + [thread, f] + end +end \ No newline at end of file diff --git a/lib/profiler.rb b/lib/profiler.rb new file mode 100644 index 00000000..5cfc0e67 --- /dev/null +++ b/lib/profiler.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'benchmark' +require 'memory_profiler' +require 'stackprof' +require 'ruby-prof' +require_relative 'reporter' + + +class Profiler + FIXTURE_PATH = 'fixtures/data_smal_50000.txt'.freeze + RESULT_PATH = 'fixtures/result.json'.freeze + REPORTS_DIR = 'reports' + + class << self + def make_report(reporter_type) + send(reporter_type) + end + + def memeory_prof + report = MemoryProfiler.report do + work(FIXTURE_PATH, RESULT_PATH) + end + report.pretty_print(scale_bytes: true) + end + + def stack_prof + StackProf.run(mode: :object, out: "#{REPORTS_DIR}/stackprof.dump", raw: true) do + work(FIXTURE_PATH, RESULT_PATH) + end + end + + def ruby_prof + RubyProf.measure_mode = RubyProf::MEMORY + result = RubyProf.profile do + work(FIXTURE_PATH, RESULT_PATH) + end + printer = RubyProf::CallTreePrinter.new(result) + printer.print(path: REPORTS_DIR, profile: 'profile') + end + end +end + diff --git a/lib/reporter.rb b/lib/reporter.rb new file mode 100644 index 00000000..a5371634 --- /dev/null +++ b/lib/reporter.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +# Deoptimized version of homework task + +require 'json' +require 'pry' +require 'date' +require_relative 'process_watcher' + + +def parse_user(user) + _, id, first_name, last_name, age = user.split(',') + { + id:, + first_name: , + last_name:, + age: age.strip!, + sessions_count: 0, + sessions_time: 0, + longest_session: 0, + browsers: [], + dates: [], + use_ie: false, + + } +end + +def parse_session(session) + _, user_id, session_id, browser, time, date = session.split(',') + { + user_id:, + session_id:, + browser:, + time:, + date: date.strip!, + } +end + +def collect_stats_from_user(user) + { 'sessionsCount' => user[:sessions_count], + 'totalTime' => "#{user[:sessions_time]} min.", + 'longestSession' => "#{user[:longest_session]} min.", + 'browsers' => user[:browsers].join(', '), + 'usedIE' => user[:use_ie], + 'alwaysUsedChrome' => user[:browsers].all? { |b| b.upcase =~ /CHROME/ }, + 'dates' => user[:dates].sort.reverse + } +end + +def work(source_path, report_file_path, options = {}) + GC.disable if options[:gc_disable] + # Запускаем наш вотчер + watcher, log_file = ProcessWatcher.new(pid: Process.pid, limit: 70).watch if options[:watcher_enable] + + + file_data = File.open(source_path, 'r') + File.write(report_file_path, '') + file = File.open(report_file_path, 'w') + file.write('{"usersStats":{') + + users_count = 0 + unique_browsers = [] + users_objects = {} + sessions_count = 0 + delimeter = ', ' + current_user_id = nil + + while true + line = file_data.gets + delimeter = nil unless line + if (line && current_user_id && line =~ /user/) || !line + user_data = users_objects[current_user_id] + key = "#{user_data[:first_name]} #{user_data[:last_name]}" + file.write("\"#{key}\":") + file.write("#{collect_stats_from_user(user_data).to_json}#{delimeter}") + users_objects.delete(current_user_id) + end + + break unless line + + if line =~ /user/ + users_count += 1 + user_attr = parse_user(line) + current_user_id = user_attr[:id] + users_objects[user_attr[:id]] ||= {} + users_objects[user_attr[:id]] = user_attr + else + sessions_count += 1 + sessions_attr = parse_session(line) + browser = sessions_attr[:browser].upcase + time = sessions_attr[:time].to_i + unique_browsers << browser unless unique_browsers.include?(browser) + user = users_objects[sessions_attr[:user_id]] + if user + user[:sessions_count] += 1 + user[:sessions_time] += time + user[:longest_session] = time if user[:longest_session] < time + user[:browsers] << browser + user[:browsers].sort! + user[:use_ie] = true if browser =~ /INTERNET EXPLORER/ + user[:dates] << sessions_attr[:date] + end + end + end + + file.write("}, ") + file.write("\"totalUsers\": #{users_count}, ") + file.write("\"uniqueBrowsersCount\": #{unique_browsers.size}, ") + file.write("\"totalSessions\": #{sessions_count}, ") + file.write("\"allBrowsers\": \"#{unique_browsers.sort.join(',')}\"}") + + puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) + + watcher.kill and log_file.close if watcher + file.close +end + + + diff --git a/task-2.rb b/task-2.rb deleted file mode 100644 index 34e09a3c..00000000 --- a/task-2.rb +++ /dev/null @@ -1,177 +0,0 @@ -# Deoptimized version of homework task - -require 'json' -require 'pry' -require 'date' -require 'minitest/autorun' - -class User - attr_reader :attributes, :sessions - - def initialize(attributes:, sessions:) - @attributes = attributes - @sessions = sessions - end -end - -def parse_user(user) - fields = user.split(',') - parsed_result = { - 'id' => fields[1], - 'first_name' => fields[2], - 'last_name' => fields[3], - 'age' => fields[4], - } -end - -def parse_session(session) - fields = session.split(',') - parsed_result = { - 'user_id' => fields[1], - 'session_id' => fields[2], - 'browser' => fields[3], - 'time' => fields[4], - 'date' => fields[5], - } -end - -def collect_stats_from_users(report, users_objects, &block) - users_objects.each do |user| - user_key = "#{user.attributes['first_name']}" + ' ' + "#{user.attributes['last_name']}" - report['usersStats'][user_key] ||= {} - report['usersStats'][user_key] = report['usersStats'][user_key].merge(block.call(user)) - end -end - -def work - file_lines = File.read('data.txt').split("\n") - - users = [] - sessions = [] - - file_lines.each do |line| - cols = line.split(',') - users = users + [parse_user(line)] if cols[0] == 'user' - sessions = sessions + [parse_session(line)] if cols[0] == 'session' - end - - # Отчёт в json - # - Сколько всего юзеров + - # - Сколько всего уникальных браузеров + - # - Сколько всего сессий + - # - Перечислить уникальные браузеры в алфавитном порядке через запятую и капсом + - # - # - По каждому пользователю - # - сколько всего сессий + - # - сколько всего времени + - # - самая длинная сессия + - # - браузеры через запятую + - # - Хоть раз использовал IE? + - # - Всегда использовал только Хром? + - # - даты сессий в порядке убывания через запятую + - - report = {} - - report[:totalUsers] = users.count - - # Подсчёт количества уникальных браузеров - uniqueBrowsers = [] - sessions.each do |session| - browser = session['browser'] - uniqueBrowsers += [browser] if uniqueBrowsers.all? { |b| b != browser } - end - - report['uniqueBrowsersCount'] = uniqueBrowsers.count - - report['totalSessions'] = sessions.count - - report['allBrowsers'] = - sessions - .map { |s| s['browser'] } - .map { |b| b.upcase } - .sort - .uniq - .join(',') - - # Статистика по пользователям - users_objects = [] - - users.each do |user| - attributes = user - user_sessions = sessions.select { |session| session['user_id'] == user['id'] } - user_object = User.new(attributes: attributes, sessions: user_sessions) - users_objects = users_objects + [user_object] - end - - report['usersStats'] = {} - - # Собираем количество сессий по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'sessionsCount' => user.sessions.count } - end - - # Собираем количество времени по пользователям - collect_stats_from_users(report, users_objects) do |user| - { 'totalTime' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.sum.to_s + ' min.' } - end - - # Выбираем самую длинную сессию пользователя - collect_stats_from_users(report, users_objects) do |user| - { 'longestSession' => user.sessions.map {|s| s['time']}.map {|t| t.to_i}.max.to_s + ' min.' } - end - - # Браузеры пользователя через запятую - collect_stats_from_users(report, users_objects) do |user| - { 'browsers' => user.sessions.map {|s| s['browser']}.map {|b| b.upcase}.sort.join(', ') } - end - - # Хоть раз использовал IE? - collect_stats_from_users(report, users_objects) do |user| - { 'usedIE' => user.sessions.map{|s| s['browser']}.any? { |b| b.upcase =~ /INTERNET EXPLORER/ } } - end - - # Всегда использовал только Chrome? - collect_stats_from_users(report, users_objects) do |user| - { 'alwaysUsedChrome' => user.sessions.map{|s| s['browser']}.all? { |b| b.upcase =~ /CHROME/ } } - end - - # Даты сессий через запятую в обратном порядке в формате iso8601 - collect_stats_from_users(report, users_objects) do |user| - { 'dates' => user.sessions.map{|s| s['date']}.map {|d| Date.parse(d)}.sort.reverse.map { |d| d.iso8601 } } - end - - File.write('result.json', "#{report.to_json}\n") - puts "MEMORY USAGE: %d MB" % (`ps -o rss= -p #{Process.pid}`.to_i / 1024) -end - -class TestMe < Minitest::Test - def setup - File.write('result.json', '') - File.write('data.txt', -'user,0,Leida,Cira,0 -session,0,0,Safari 29,87,2016-10-23 -session,0,1,Firefox 12,118,2017-02-27 -session,0,2,Internet Explorer 28,31,2017-03-28 -session,0,3,Internet Explorer 28,109,2016-09-15 -session,0,4,Safari 39,104,2017-09-27 -session,0,5,Internet Explorer 35,6,2016-09-01 -user,1,Palmer,Katrina,65 -session,1,0,Safari 17,12,2016-10-21 -session,1,1,Firefox 32,3,2016-12-20 -session,1,2,Chrome 6,59,2016-11-11 -session,1,3,Internet Explorer 10,28,2017-04-29 -session,1,4,Chrome 13,116,2016-12-28 -user,2,Gregory,Santos,86 -session,2,0,Chrome 35,6,2018-09-21 -session,2,1,Safari 49,85,2017-05-22 -session,2,2,Firefox 47,17,2018-02-02 -session,2,3,Chrome 20,84,2016-11-25 -') - end - - def test_result - work - expected_result = JSON.parse('{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}}') - assert_equal expected_result, JSON.parse(File.read('result.json')) - end -end diff --git a/data.txt b/tests/fixtures/data.txt similarity index 100% rename from data.txt rename to tests/fixtures/data.txt diff --git a/tests/fixtures/expected_json.json b/tests/fixtures/expected_json.json new file mode 100644 index 00000000..889aea0c --- /dev/null +++ b/tests/fixtures/expected_json.json @@ -0,0 +1 @@ +{"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}} \ No newline at end of file diff --git a/tests/reporter_spec.rb b/tests/reporter_spec.rb new file mode 100644 index 00000000..07d51f04 --- /dev/null +++ b/tests/reporter_spec.rb @@ -0,0 +1,22 @@ +require 'rspec-benchmark' +require_relative "../lib/reporter" + + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers +end + +describe 'Performance reporter' do + let(:file_path) { 'tests/fixtures/data.txt' } + let(:result_path) { 'tests/result.json' } + + after do + File.delete(result_path) + end + + it 'create report' do + expect { + work(file_path, result_path, { watcher_enable: true }) + }.to perform_allocation(70_000).bytes + end +end diff --git a/tests/reporter_test.rb b/tests/reporter_test.rb new file mode 100644 index 00000000..843bdeae --- /dev/null +++ b/tests/reporter_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require_relative '../lib/reporter' + + +class TestMe < Minitest::Test + def setup + @result_path = 'tests/result.json' + File.write(@result_path, '') + end + + def test_result + work('tests/fixtures/data.txt', @result_path, { watcher_enable: true }) + expected_result = JSON.parse(File.read('tests/fixtures/expected_json.json')) + result = JSON.parse(File.read(@result_path)) + assert_equal expected_result['usersStats'], result['usersStats'] + assert_equal expected_result['allBrowsers'], result['allBrowsers'] + assert_equal expected_result['totalUsers'], result['totalUsers'] + assert_equal expected_result['uniqueBrowsersCount'], result['uniqueBrowsersCount'] + assert_equal expected_result['totalSessions'], result['totalSessions'] + File.delete(@result_path) + end +end \ No newline at end of file