rspeed 0.0.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/LICENSE +1 -1
  4. data/README.md +88 -5
  5. data/lib/generators/rspeed/install_generator.rb +13 -0
  6. data/lib/generators/rspeed/templates/lib/tasks/rspeed.rb +9 -0
  7. data/lib/rspeed.rb +9 -0
  8. data/lib/rspeed/env.rb +43 -0
  9. data/lib/rspeed/extension.rb +12 -0
  10. data/lib/rspeed/observer.rb +43 -0
  11. data/lib/rspeed/redis.rb +55 -0
  12. data/lib/rspeed/runner.rb +13 -0
  13. data/lib/rspeed/splitter.rb +130 -0
  14. data/lib/rspeed/variable.rb +31 -0
  15. data/lib/rspeed/version.rb +1 -1
  16. data/spec/common_helper.rb +10 -0
  17. data/spec/fixtures/1_spec.rb +9 -0
  18. data/spec/fixtures/2_spec.rb +5 -0
  19. data/spec/fixtures/empty.rb +4 -0
  20. data/spec/fixtures/new_spec.rb.csv +1 -0
  21. data/spec/models/rspeed/env/db_spec.rb +17 -0
  22. data/spec/models/rspeed/env/host_spec.rb +17 -0
  23. data/spec/models/rspeed/env/name_spec.rb +17 -0
  24. data/spec/models/rspeed/env/pipe_spec.rb +19 -0
  25. data/spec/models/rspeed/env/pipes_spec.rb +41 -0
  26. data/spec/models/rspeed/env/port_spec.rb +17 -0
  27. data/spec/models/rspeed/env/result_key_spec.rb +19 -0
  28. data/spec/models/rspeed/env/rspeed_spec.rb +43 -0
  29. data/spec/models/rspeed/env/tmp_spec.rb +19 -0
  30. data/spec/models/rspeed/observer/after_spec.rb +16 -0
  31. data/spec/models/rspeed/observer/after_suite_spec.rb +46 -0
  32. data/spec/models/rspeed/observer/before_spec.rb +15 -0
  33. data/spec/models/rspeed/observer/before_suite_spec.rb +37 -0
  34. data/spec/models/rspeed/redis/clean_pipes_flag_spec.rb +14 -0
  35. data/spec/models/rspeed/redis/client_spec.rb +7 -0
  36. data/spec/models/rspeed/redis/destroy_spec.rb +29 -0
  37. data/spec/models/rspeed/redis/get_spec.rb +9 -0
  38. data/spec/models/rspeed/redis/keys_spec.rb +29 -0
  39. data/spec/models/rspeed/redis/result_spec.rb +13 -0
  40. data/spec/models/rspeed/redis/set_spec.rb +9 -0
  41. data/spec/models/rspeed/redis/specs_finished_spec.rb +19 -0
  42. data/spec/models/rspeed/redis/specs_initiated_spec.rb +13 -0
  43. data/spec/models/rspeed/runner/run_spec.rb +30 -0
  44. data/spec/models/rspeed/splitter/actual_examples_spec.rb +26 -0
  45. data/spec/models/rspeed/splitter/append_spec.rb +35 -0
  46. data/spec/models/rspeed/splitter/diff_spec.rb +30 -0
  47. data/spec/models/rspeed/splitter/first_pipe_spec.rb +17 -0
  48. data/spec/models/rspeed/splitter/get_spec.rb +74 -0
  49. data/spec/models/rspeed/splitter/pipe_files_spec.rb +26 -0
  50. data/spec/models/rspeed/splitter/redundant_run_spec.rb +45 -0
  51. data/spec/models/rspeed/splitter/rename_spec.rb +16 -0
  52. data/spec/models/rspeed/splitter/split_spec.rb +89 -0
  53. data/spec/models/rspeed/variable/append_name_spec.rb +19 -0
  54. data/spec/models/rspeed/variable/csv_spec.rb +5 -0
  55. data/spec/models/rspeed/variable/default_partner_spec.rb +5 -0
  56. data/spec/models/rspeed/variable/key_spec.rb +15 -0
  57. data/spec/models/rspeed/variable/pipe_name_spec.rb +15 -0
  58. data/spec/models/rspeed/variable/pipes_pattern_spec.rb +5 -0
  59. data/spec/models/rspeed/variable/result_spec.rb +19 -0
  60. data/spec/models/rspeed/variable/tmp_spec.rb +15 -0
  61. data/spec/spec_helper.rb +27 -0
  62. data/spec/support/common.rb +5 -3
  63. data/spec/support/coverage.rb +14 -0
  64. data/spec/support/env_mock.rb +3 -0
  65. data/spec/support/fakeredis.rb +3 -0
  66. metadata +180 -23
  67. data/spec/rails_helper.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37630f2559997e8c9d0a8128030d60d58a47b419197bae4e5af03bd882961108
4
- data.tar.gz: ce15fb8fe3bd8617819e7b9735a1300e31fdac020d6b907ebef9abedfe0bee52
3
+ metadata.gz: c2d2d14366ee70f0cf449f6fbf6bc6c7cedd57edc8497b646721afa4b098720a
4
+ data.tar.gz: 0ac5789e67ba474055620780a0311525bbe29c801b2c09344218888b33de55ac
5
5
  SHA512:
6
- metadata.gz: 2ee677e609df2a543b75c14259ab1d878fdbf6f853e00bffb196b14fc76fb0f4f026df9ea7c2ce9ec56744a497418b271dc6c5e5ccb053a62a6c110d3e794613
7
- data.tar.gz: 92f1c09d0f251d4b5e7bffed57120c3d9fa56b16278dd8b5de2dd225218d1993d62db5f3a57b0b30a8bd47bccaada5a90969c713bb63a45590bb3c4d3c10bea9
6
+ metadata.gz: 00adb24277015b707cfbd1d5ea0f2cd9b220701344654a107471ab6683ea4876830a34cb34262a42b425078a5dab85efaee16cb2252da1027b807a3409eb22ac
7
+ data.tar.gz: aa4932a206cd9a5ab93e4b4217099ebad1a110b8ee9d48c6c0dd9addaf9073d561249480cfe1f74137f484c27a649b05afc4720fde679abb825ef43ba59feb4a
@@ -1,3 +1,35 @@
1
+ ## master
2
+
3
+ - None;
4
+
5
+ ## v0.5.0
6
+
7
+ #### Fix
8
+
9
+ - Add env `RSPEED_NAME` to specify the application name and avoid result conflicts between multiple runs;
10
+ - No more depends on pipe sequence to generate ou aggregate the resul;
11
+ - rake `rspeed:install`;
12
+
13
+ #### Update
14
+
15
+ - The result of the pipes are no more saved on Redis. It's now calculated based on result key `rspeed`;
16
+
17
+ ## v0.4.0
18
+
19
+ - Now we make diff to discover removed and added examples;
20
+
21
+ ## v0.3.0
22
+
23
+ - Removed and added files now is considered on run;
24
+
25
+ ## v0.2.0
26
+
27
+ - Splits specs by examples over files;
28
+
29
+ ## v0.1.0
30
+
31
+ - First release;
32
+
1
33
  ## v0.0.1
2
34
 
3
35
  - RubyGems namespace reservation.
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Washington Botelho
3
+ Copyright (c) 2017-2020 Washington Botelho
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,11 +1,94 @@
1
1
  # RSpeed
2
2
 
3
- [![Build Status](https://travis-ci.org/wbotelhos/normalizy.svg)](https://travis-ci.org/wbotelhos/normalizy)
4
- [![Gem Version](https://badge.fury.io/rb/normalizy.svg)](https://badge.fury.io/rb/normalizy)
5
- [![LiberPay](https://img.shields.io/badge/donate-%3C3-brightgreen.svg)](https://liberapay.com/wbotelhos)
3
+ [![CI](https://github.com/wbotelhos/rspeed/workflows/CI/badge.svg)](https://github.com/wbotelhos/rspeed/actions)
4
+ [![Gem Version](https://badge.fury.io/rb/rspeed.svg)](https://badge.fury.io/rb/rspeed)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/f312587b4f126bb13e85/maintainability)](https://codeclimate.com/github/wbotelhos/rspeed/maintainability)
6
+ [![Coverage](https://codecov.io/gh/wbotelhos/rspeed/branch/main/graph/badge.svg)](https://codecov.io/gh/wbotelhos/rspeed)
7
+ [![Sponsor](https://img.shields.io/badge/sponsor-%3C3-green)](https://www.patreon.com/wbotelhos)
6
8
 
9
+ RSpeed splits your specs to you run parallels tests.
7
10
 
11
+ ## Install
8
12
 
9
- ## Love it!
13
+ Add the following code on your `Gemfile` and run `bundle install`:
10
14
 
11
- Via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=X8HEP2878NDEG&item_name=rspeed) or [LiberPay](https://liberapay.com/wbotelhos). Thanks! (:
15
+ ```ruby
16
+ gem 'rspeed'
17
+ ```
18
+
19
+ ## Setup
20
+
21
+ We need to extract the rake that executes the split via `rspeed:run`.
22
+
23
+ ```ruby
24
+ rake rspeed:install
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ - `RSPEED_NAME`: You app name to avoid data override
30
+ - `RSPEED_PIPE`: Current pipe
31
+ - `RSPEED_PIPES`: Quantity of pipes
32
+ - `RSPEED_RESULT_KEY`: The key that keeps the final result of all pipes
33
+ - `RSPEED_TMP_KEY`: The temporary key that keeps the partial result of the pipes
34
+ - `RSPEED`: Enables RSpeed
35
+
36
+ ```sh
37
+ RSPEED=true RSPEED_NAME=authorizy RSPEED_PIPE=1 RSPEED_PIPES=3 bundle exec rake rspeed:run
38
+ ```
39
+
40
+ ## How it Works
41
+
42
+ ### First run
43
+
44
+ 1. Since we have no statistics on the first time, we run all specs and collect it;
45
+
46
+ ```json
47
+ { "file": "./spec/models/1_spec.rb", "time": 0.01 }
48
+ { "file": "./spec/models/2_spec.rb", "time": 0.02 }
49
+ { "file": "./spec/models/3_spec.rb", "time": 0.001 }
50
+ { "file": "./spec/models/4_spec.rb", "time": 1 }
51
+ ```
52
+
53
+ ### Second and next runs
54
+
55
+ 1. Previous statistics is balanced by times and distributed between pipes:
56
+
57
+ ```json
58
+ { "file": "./spec/models/4_spec.rb", "time": 1 }
59
+ ```
60
+
61
+ ```json
62
+ { "file": "./spec/models/2_spec.rb", "time": 0.02 }
63
+ ```
64
+
65
+ ```json
66
+ { "file": "./spec/models/3_spec.rb", "time": 0.001 }
67
+ { "file": "./spec/models/1_spec.rb", "time": 0.01 }
68
+ ```
69
+
70
+ 2. Run the current pipe `1`:
71
+
72
+ ```json
73
+ { "file": "./spec/models/4_spec.rb", "time": 1 }
74
+ ```
75
+
76
+ - Collects statistics and temporary save it;
77
+
78
+ 4. Run the current pipe `2`:
79
+
80
+ ```json
81
+ { "file": "./spec/models/2_spec.rb", "time": 0.02 }
82
+ ```
83
+
84
+ - Collects statistics and temporary save it;
85
+
86
+ 5. Run the current pipe `3`:
87
+
88
+ ```json
89
+ { "file": "./spec/models/3_spec.rb", "time": 0.001 }
90
+ { "file": "./spec/models/1_spec.rb", "time": 0.01 }
91
+ ```
92
+
93
+ - Collects statistics and temporary save it;
94
+ - Sum all the last statistics and save it for the next run;
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpeed
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ desc 'Creates RSpeed task'
8
+
9
+ def create_initializer
10
+ copy_file 'lib/tasks/rspeed.rake', 'lib/tasks/rspeed.rake'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspeed'
4
+
5
+ namespace :rspeed do
6
+ task run: :environment do
7
+ ::RSpeed::Runner.run ->(command) { sh command }
8
+ end
9
+ end
@@ -1,4 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpeed
4
+ require 'csv'
5
+
6
+ require 'rspeed/env'
7
+ require 'rspeed/extension'
8
+ require 'rspeed/observer'
9
+ require 'rspeed/redis'
10
+ require 'rspeed/runner'
11
+ require 'rspeed/splitter'
12
+ require 'rspeed/variable'
4
13
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpeed
4
+ module Env
5
+ module_function
6
+
7
+ def db
8
+ ENV['RSPEED_DB']&.to_i
9
+ end
10
+
11
+ def host
12
+ ENV['RSPEED_HOST']
13
+ end
14
+
15
+ def name
16
+ ENV['RSPEED_NAME']
17
+ end
18
+
19
+ def pipe
20
+ ENV.fetch('RSPEED_PIPE', 1).to_i
21
+ end
22
+
23
+ def pipes
24
+ RSpeed::Redis.result? ? ENV.fetch('RSPEED_PIPES', 1).to_i : 1
25
+ end
26
+
27
+ def port
28
+ ENV['RSPEED_PORT']&.to_i
29
+ end
30
+
31
+ def result_key
32
+ ENV.fetch('RESPEED_RESULT_KEY', RSpeed::Variable.result)
33
+ end
34
+
35
+ def rspeed
36
+ ENV['RSPEED'] == 'true'
37
+ end
38
+
39
+ def tmp_key
40
+ ENV.fetch('RESPEED_TMP_KEY', RSpeed::Variable.tmp)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ if RSpeed::Env.rspeed
4
+ require 'rspec'
5
+
6
+ RSpec.configure do |config|
7
+ config.before(:suite) { RSpeed::Observer.before_suite }
8
+ config.before { |example| RSpeed::Observer.before(example) }
9
+ config.after { |example| RSpeed::Observer.after(example) }
10
+ config.after(:suite) { RSpeed::Observer.after_suite }
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpeed
4
+ module Observer
5
+ module_function
6
+
7
+ def after(example)
8
+ file_path = example.metadata[:file_path]
9
+ line_number = example.metadata[:line_number]
10
+ spent_time = example.clock.now - example.metadata[:start_at]
11
+
12
+ File.open(RSpeed::Variable::CSV, 'a') do |file|
13
+ file.write("#{spent_time},#{file_path}:#{line_number}\n")
14
+ end
15
+ end
16
+
17
+ def after_suite(splitter = ::RSpeed::Splitter.new)
18
+ RSpeed::Redis.set(RSpeed::Variable.pipe_name, true)
19
+
20
+ splitter.append
21
+
22
+ return unless RSpeed::Redis.specs_finished?
23
+
24
+ splitter.rename
25
+
26
+ RSpeed::Redis.clean_pipes_flag
27
+ end
28
+
29
+ def before(example)
30
+ example.update_inherited_metadata(start_at: example.clock.now)
31
+ end
32
+
33
+ def before_suite
34
+ truncate_csv_file
35
+
36
+ RSpeed::Redis.destroy(RSpeed::Variable.tmp) unless RSpeed::Redis.specs_initiated?
37
+ end
38
+
39
+ def truncate_csv_file
40
+ File.open(RSpeed::Variable::CSV, 'w') { |file| file.truncate(0) }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpeed
4
+ module Redis
5
+ require 'redis'
6
+
7
+ module_function
8
+
9
+ def clean_pipes_flag
10
+ destroy(RSpeed::Variable::PIPES_PATTERN)
11
+ end
12
+
13
+ def client
14
+ @client ||= ::Redis.new(db: RSpeed::Env.db, host: RSpeed::Env.host, port: RSpeed::Env.port)
15
+ end
16
+
17
+ def destroy(pattern = RSpeed::Variable::DEFAULT_PATTERN)
18
+ keys(pattern).each { |key| client.del(key) }
19
+ end
20
+
21
+ def get(key)
22
+ client.get(key)
23
+ end
24
+
25
+ def keys(pattern = RSpeed::Variable::DEFAULT_PATTERN)
26
+ cursor = 0
27
+ result = []
28
+
29
+ loop do
30
+ cursor, results = client.scan(cursor, match: pattern)
31
+ result += results
32
+
33
+ break if cursor.to_i.zero?
34
+ end
35
+
36
+ result
37
+ end
38
+
39
+ def result?
40
+ keys(RSpeed::Env.result_key).any?
41
+ end
42
+
43
+ def set(key, value)
44
+ client.set(key, value)
45
+ end
46
+
47
+ def specs_finished?
48
+ RSpeed::Redis.keys(RSpeed::Variable::PIPES_PATTERN).size == RSpeed::Env.pipes
49
+ end
50
+
51
+ def specs_initiated?
52
+ RSpeed::Redis.keys(RSpeed::Variable::PIPES_PATTERN).any?
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpeed
4
+ module Runner
5
+ module_function
6
+
7
+ def run(shell, splitter: ::RSpeed::Splitter.new)
8
+ return if splitter.redundant_run?
9
+
10
+ shell.call(['bundle exec rspec', splitter.pipe_files].compact.join(' '))
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpeed
4
+ class Splitter
5
+ require 'json'
6
+
7
+ def initialize(specs_path: './spec/**/*_spec.rb')
8
+ @specs_path = specs_path
9
+ end
10
+
11
+ def actual_examples
12
+ @actual_examples ||= begin
13
+ [].tap do |examples|
14
+ Dir[@specs_path].sort.each do |file|
15
+ data = File.open(file).read
16
+ lines = data.split("\n")
17
+
18
+ lines&.each&.with_index do |item, index|
19
+ examples << "#{file}:#{index + 1}" if /^it/.match?(item.gsub(/\s+/, ''))
20
+ end
21
+ end
22
+
23
+ stream(:actual_examples, examples)
24
+ end
25
+ end
26
+ end
27
+
28
+ def append(files = CSV.read(RSpeed::Variable::CSV))
29
+ files.each do |time, file|
30
+ redis.lpush(RSpeed::Env.tmp_key, { file: file, time: time.to_f }.to_json)
31
+ end
32
+ end
33
+
34
+ def diff
35
+ actual_data = rspeed_data.select { |item| actual_examples.include?(item[:file]) }
36
+ added_data = added_examples.map { |item| { file: item, time: 0 } }
37
+
38
+ removed_examples # called just for stream for now
39
+
40
+ actual_data + added_data
41
+ end
42
+
43
+ def first_pipe?
44
+ RSpeed::Env.pipe == 1
45
+ end
46
+
47
+ def get(pattern)
48
+ @get ||= begin
49
+ return redis.lrange(pattern, 0, -1) if [RSpeed::Variable.result, RSpeed::Variable.tmp].include?(pattern)
50
+
51
+ RSpeed::Redis.keys(pattern).map { |key| ::JSON.parse(redis.get(key)) }
52
+ end
53
+ end
54
+
55
+ def pipe_files
56
+ return unless RSpeed::Redis.result?
57
+
58
+ split[RSpeed::Variable.key(RSpeed::Env.pipe)][:files].map { |item| item[:file] }.join(' ')
59
+ end
60
+
61
+ def redundant_run?
62
+ !first_pipe? && !exists?(RSpeed::Env.result_key)
63
+ end
64
+
65
+ def rename
66
+ redis.rename(RSpeed::Env.tmp_key, RSpeed::Env.result_key)
67
+ end
68
+
69
+ def split(data = diff)
70
+ json = {}
71
+
72
+ RSpeed::Env.pipes.times do |index|
73
+ json[RSpeed::Variable.key(index + 1)] ||= []
74
+ json[RSpeed::Variable.key(index + 1)] = { total: 0, files: [], number: index + 1 }
75
+ end
76
+
77
+ sorted_data = data.sort_by { |item| item[:time] }.reverse
78
+
79
+ sorted_data.each do |record|
80
+ selected_pipe_data = json.min_by { |pipe| pipe[1][:total] }
81
+ selected_pipe = json[RSpeed::Variable.key(selected_pipe_data[1][:number])]
82
+ time = record[:time].to_f
83
+
84
+ selected_pipe[:total] += time
85
+ selected_pipe[:files] << { file: record[:file], time: time }
86
+ end
87
+
88
+ json
89
+ end
90
+
91
+ private
92
+
93
+ def added_examples
94
+ @added_examples ||= begin
95
+ (actual_examples - rspeed_examples).tap { |examples| stream(:added_examples, examples) }
96
+ end
97
+ end
98
+
99
+ # TODO: exists? does not work: undefined method `>' for false:FalseClass
100
+ def exists?(key)
101
+ redis.keys.include?(key)
102
+ end
103
+
104
+ def redis
105
+ @redis ||= ::RSpeed::Redis.client
106
+ end
107
+
108
+ def removed_examples
109
+ @removed_examples ||= begin
110
+ (rspeed_examples - actual_examples).tap { |examples| stream(:removed_examples, examples) }
111
+ end
112
+ end
113
+
114
+ def removed_time
115
+ removed_examples.sum { |item| item[0].to_f }
116
+ end
117
+
118
+ def rspeed_data
119
+ @rspeed_data ||= get(RSpeed::Env.result_key).map { |item| JSON.parse(item, symbolize_names: true) }
120
+ end
121
+
122
+ def rspeed_examples
123
+ rspeed_data.map { |item| item[:file] }
124
+ end
125
+
126
+ def stream(type, data)
127
+ puts "PIPE: #{RSpeed::Env.pipe} with #{type}: #{data}"
128
+ end
129
+ end
130
+ end