rspeed 0.0.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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