load_test 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5a2e834da8e9476bffe2860fbcdf90231913a6cede61abda5c82416846469b8a
4
+ data.tar.gz: 3fcc964680d5156a44fcf4cd14b4fae48af527ca60b229c1d1fa991db80993a8
5
+ SHA512:
6
+ metadata.gz: 0facea8a7bc6cbd28faa6611791c5188fbd4c7dd50147502164b1ab9d6b586da96e3c8bd59cca734c0e3862e210da47ef8bf7febbff914714325936d72ea670d
7
+ data.tar.gz: da2cf8d638641d39e949467668fcd742daf251803538a53a2cd0477794317a9c63f16d05c218579664cbb9693abbda8e9fe624a1a7cfe3843eb0e64438d54234
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 3.0
data/CHANGELOG.md ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in load_test.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "standard", "~> 1.3"
data/Gemfile.lock ADDED
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ load_test (0.1.0)
5
+ async (~> 2.0)
6
+ timers (~> 4.3)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.4.2)
12
+ async (2.0.3)
13
+ console (~> 1.10)
14
+ io-event (~> 1.0.0)
15
+ timers (~> 4.1)
16
+ console (1.15.3)
17
+ fiber-local
18
+ fiber-local (1.0.0)
19
+ io-event (1.0.9)
20
+ json (2.6.2)
21
+ parallel (1.22.1)
22
+ parser (3.1.2.1)
23
+ ast (~> 2.4.1)
24
+ rainbow (3.1.1)
25
+ rake (13.0.6)
26
+ regexp_parser (2.5.0)
27
+ rexml (3.2.5)
28
+ rubocop (1.33.0)
29
+ json (~> 2.3)
30
+ parallel (~> 1.10)
31
+ parser (>= 3.1.0.0)
32
+ rainbow (>= 2.2.2, < 4.0)
33
+ regexp_parser (>= 1.8, < 3.0)
34
+ rexml (>= 3.2.5, < 4.0)
35
+ rubocop-ast (>= 1.19.1, < 2.0)
36
+ ruby-progressbar (~> 1.7)
37
+ unicode-display_width (>= 1.4.0, < 3.0)
38
+ rubocop-ast (1.21.0)
39
+ parser (>= 3.1.1.0)
40
+ rubocop-performance (1.14.3)
41
+ rubocop (>= 1.7.0, < 2.0)
42
+ rubocop-ast (>= 0.4.0)
43
+ ruby-progressbar (1.11.0)
44
+ standard (1.15.0)
45
+ rubocop (= 1.33.0)
46
+ rubocop-performance (= 1.14.3)
47
+ timers (4.3.3)
48
+ unicode-display_width (2.2.0)
49
+
50
+ PLATFORMS
51
+ x86_64-darwin-20
52
+
53
+ DEPENDENCIES
54
+ load_test!
55
+ rake (~> 13.0)
56
+ standard (~> 1.3)
57
+
58
+ BUNDLED WITH
59
+ 2.3.7
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 kekemoto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # Load Test for Rubyist
2
+
3
+ ```ruby
4
+ require "load_test"
5
+ require "net/http" # This is standard library.
6
+ require "benchmark" # This is standard library.
7
+
8
+ result = LoadTest.run(rpm: 2000, limit_time: 5) do |result|
9
+ uri = URI.parse("http://localhost")
10
+ time = Benchmark.realtime { Net::HTTP.get_response(uri) }
11
+ result << { name: uri.to_s, value: time }
12
+ end
13
+
14
+ stat = LoadTest.stat(result)
15
+ LoadTest.output_stdout(stat)
16
+
17
+ # => name | average | median | percentile_80 | percentile_90 | count | error_count
18
+ # => http://localhost | 1.78 | 2.01 | 3.01 | 3.02 | 34 | 0
19
+ ```
20
+
21
+ ## Feature
22
+
23
+ - Write in ruby.
24
+ - It's a library, not a command, for simplicity.
25
+ - No need to learn scenario files.
26
+ - Just run the ruby file.
27
+ - Loop and branch at will.
28
+ - Parallel execution, serial execution, deferred execution, whatever you want.
29
+ - You can use it without HTTP.
30
+ - It's flexible yet simple.
31
+ - Efficient load testing with multiple processes and asynchronous IO with the {Async gem}[https://github.com/socketry/async].
32
+
33
+ ## Installation
34
+
35
+ Add this line to your application's Gemfile:
36
+
37
+ ```
38
+ gem 'load_test'
39
+ ```
40
+
41
+ And then execute:
42
+
43
+ $ bundle install
44
+
45
+ Or install it yourself as:
46
+
47
+ $ gem install load_test
48
+
49
+ ## API Document
50
+
51
+ TODO
52
+
53
+ ## Example: error handling
54
+
55
+ ```ruby
56
+ require "load_test"
57
+ require "net/http"
58
+ require "benchmark"
59
+
60
+ result = LoadTest.run(rpm: 2000, limit_time: 5) do |result|
61
+ uri = URI.parse("http://localhost")
62
+ error = nil
63
+ time = Benchmark.realtime do
64
+ response = Net::HTTP.get_response(uri)
65
+ error = response if response.code != "200"
66
+ end
67
+
68
+ if error
69
+ result << { name: uri.to_s, error: error }
70
+ else
71
+ result << { name: uri.to_s, value: time }
72
+ end
73
+ end
74
+
75
+ stat = LoadTest.stat(result)
76
+ LoadTest.output_stdout(stat)
77
+ ```
78
+
79
+ ## Example: serial execution
80
+
81
+ ```ruby
82
+ require "load_test"
83
+ require "net/http"
84
+ require "benchmark"
85
+
86
+ result_a = LoadTest.run(rpm: 2000, limit_time: 5) do |result|
87
+ uri = URI.parse("http://localhost/a")
88
+ time = Benchmark.realtime { Net::HTTP.get_response(uri) }
89
+ result << { name: uri.to_s, value: time }
90
+ end
91
+
92
+ result_b = LoadTest.run(rpm: 2000, limit_time: 5) do |result|
93
+ uri = URI.parse("http://localhost/b")
94
+ time = Benchmark.realtime { Net::HTTP.get_response(uri) }
95
+ result << { name: uri.to_s, value: time }
96
+ end
97
+
98
+ # Concatenate the results.
99
+ stat = LoadTest.stat(result_a + result_b)
100
+ LoadTest.output_stdout(stat)
101
+ ```
102
+
103
+ ## Example: parallel execution
104
+
105
+ ```ruby
106
+ require "load_test"
107
+ require "net/http"
108
+ require "benchmark"
109
+
110
+ future_a = LoadTest::Future.new do
111
+ LoadTest.run(rpm: 2000, limit_time: 5) do |result|
112
+ uri = URI.parse("http://localhost/a")
113
+ time = Benchmark.realtime { Net::HTTP.get_response(uri) }
114
+ result << { name: uri.to_s, value: time }
115
+ end.to_a
116
+ end
117
+
118
+ future_b = LoadTest::Future.new do
119
+ LoadTest.run(rpm: 2000, limit_time: 5) do |result|
120
+ uri = URI.parse("http://localhost/b")
121
+ time = Benchmark.realtime { Net::HTTP.get_response(uri) }
122
+ result << { name: uri.to_s, value: time }
123
+ end.to_a
124
+ end
125
+
126
+ result_a = future_a.take
127
+ result_b = future_b.take
128
+
129
+ stat = LoadTest.stat(result_a + result_b)
130
+ LoadTest.output_stdout(stat)
131
+ ```
132
+
133
+ ## Example: options for execution.
134
+
135
+ ```ruby
136
+ require "load_test"
137
+ require "net/http"
138
+ require "benchmark"
139
+
140
+ result = LoadTest.run_custom(concurrent: 1, process_size: 1, repeat: 1, interval: 0) do |result|
141
+ uri = URI.parse("http://localhost")
142
+ time = Benchmark.realtime { Net::HTTP.get_response(uri) }
143
+ result << { name: uri.to_s, value: time }
144
+ end
145
+
146
+ stat = LoadTest.stat(result)
147
+ LoadTest.output_stdout(stat)
148
+ ```
149
+
150
+ ## Example: custom stat
151
+
152
+ ```ruby
153
+ require "load_test"
154
+
155
+ result = LoadTest.run(rpm: 2000, limit_count: 10) do |result|
156
+ result << {name: :rand, value: rand(1..10)}
157
+ end
158
+
159
+ stat = LoadTest.stat(result)
160
+ LoadTest.output_stdout(stat)
161
+ ```
162
+
163
+ ```ruby
164
+ require "load_test"
165
+
166
+ result = LoadTest.run(rpm: 2000, limit_count: 10) do |result|
167
+ result << rand(1..10)
168
+ end
169
+
170
+ stat = LoadTest::Stat.start do |stat|
171
+ result.each do |value|
172
+ stat.add(name: :rand, value: value)
173
+ end
174
+ end
175
+ LoadTest.output_stdout(stat)
176
+ ```
177
+
178
+ ## Example: custom output format
179
+
180
+ ```ruby
181
+ require "load_test"
182
+ require "csv"
183
+
184
+ result = LoadTest.run(rpm: 2000, limit_count: 10) do |result|
185
+ result << {name: :rand, value: rand(1..10)}
186
+ end
187
+
188
+ stat = LoadTest.stat(result)
189
+
190
+ CSV.open("./result.csv", "wb") do |csv|
191
+ csv << ["name", "average", "median", "90 percentile", "99 percentile", "error_count"]
192
+ stat.each do
193
+ csv << [_1.name, _1.average, _1.median, _1.percentile(90), _1.percentile(99), _1.error_count]
194
+ end
195
+ end
196
+ ```
197
+
198
+ # Example: More freedom to stat!
199
+
200
+ `result` is just an [Enumerator](https://docs.ruby-lang.org/en/master/Enumerator.html).
201
+
202
+ ```ruby
203
+ require "load_test"
204
+
205
+ result = LoadTest.run(rpm: 2000, limit_count: 10) do |result|
206
+ result << rand(1..10)
207
+ end
208
+
209
+ result = result.to_a
210
+ min, max = result.minmax
211
+ total = result.sum
212
+ puts "total: #{total}, min: #{min}, max: #{max}"
213
+ pp result
214
+ ```
215
+
216
+ ## Note
217
+
218
+ LoadTest is run using a [process](https://docs.ruby-lang.org/en/master/Process.html#method-c-fork), so be careful with the scope.
219
+
220
+ ```ruby
221
+ require "load_test"
222
+
223
+ # Bad
224
+ count = 0
225
+ LoadTest.run_custom(process_size: 2, concurrent: 1, repeat: 2) do
226
+ count += 1
227
+ pp count
228
+ end
229
+ # => 1
230
+ # => 1
231
+
232
+ pp count
233
+ # => 0
234
+ ```
235
+
236
+ ```ruby
237
+ require "load_test"
238
+
239
+ # Good
240
+ result = LoadTest.run_custom(process_size: 2, concurrent: 1, repeat: 2) do |result|
241
+ result << 1
242
+ end
243
+
244
+ pp result.to_a.size
245
+ # => 2
246
+ ```
247
+
248
+ ## Development
249
+
250
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
251
+
252
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
253
+
254
+ ## Contributing
255
+
256
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kekemoto/load_test.
257
+
258
+ ## License
259
+
260
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "standard/rake"
13
+
14
+ task default: %i[test standard]
@@ -0,0 +1,64 @@
1
+ # Make parallelism by processes manageable.
2
+ class LoadTest::Future
3
+ # process id
4
+ attr_reader :pid
5
+ # reader of IO.pipe
6
+ attr_reader :reader
7
+
8
+ class << self
9
+ # Get the first completed future.
10
+ def race(futures)
11
+ hash = futures.to_h { [_1.reader, _1] }
12
+ readers, = IO.select(futures.map(&:reader))
13
+ hash[readers[0]]
14
+ end
15
+
16
+ # Wait until all futures have completed.
17
+ def all(futures)
18
+ futures.each(&:wait)
19
+ end
20
+ end
21
+
22
+ # Execute the processing of the block asynchronously.
23
+ def initialize(&block)
24
+ reader, writer = IO.pipe
25
+
26
+ @pid = fork do
27
+ reader.close
28
+ begin
29
+ result = block.call
30
+ rescue => e
31
+ result = e
32
+ end
33
+ Marshal.dump(result, writer)
34
+ end
35
+ writer.close
36
+
37
+ @reader = reader
38
+ @is_done = false
39
+ end
40
+
41
+ # Wait for the process to complete.
42
+ def wait
43
+ return if @is_done
44
+ Process.waitpid(@pid)
45
+ @is_done = true
46
+ self
47
+ end
48
+
49
+ # Wait until the process is completed and return the result.
50
+ def take
51
+ wait
52
+ result = Marshal.load(@reader)
53
+ @reader.close
54
+ raise result if result.is_a?(Exception)
55
+ result
56
+ end
57
+
58
+ # Abort the future.
59
+ def abort
60
+ Process.kill(:INT, @pid)
61
+ @reader.close
62
+ @is_done = true
63
+ end
64
+ end
@@ -0,0 +1,61 @@
1
+ # Statistics are taken from the load test results.
2
+ class LoadTest::Stat
3
+ include Enumerable
4
+
5
+ def self.load(result) # :nodoc:
6
+ this = new
7
+ result.each do
8
+ this.add(_1)
9
+ end
10
+ this.finish
11
+ this
12
+ end
13
+
14
+ # Constructor of LoadTest::Stat
15
+ #
16
+ # block {|stat| ... }:: Data entry within the block. The argument is LoadTest::Stat.
17
+ # return:: LoadTest::Stat
18
+ def self.start(&block)
19
+ this = new
20
+ block.call this
21
+ this.finish
22
+ this
23
+ end
24
+
25
+ def initialize # :nodoc:
26
+ @rows = Hash.new { |h, k| h[k] = LoadTest::StatRow.new(k) }
27
+ end
28
+
29
+ # Enter data for aggregation.
30
+ #
31
+ # hash1 and hash2 are the same.
32
+ #
33
+ # {
34
+ # name: Aggregate field name,
35
+ # value: The value you want to stat, such as speed,
36
+ # error: If you get an error,
37
+ # }
38
+ def add(hash1 = nil, **hash2)
39
+ hash = hash1 || hash2
40
+
41
+ if hash.key?(:value)
42
+ @rows[hash[:name]].values << hash[:value]
43
+ end
44
+
45
+ if hash.key?(:error)
46
+ @rows[hash[:name]].errors << hash[:error]
47
+ end
48
+ end
49
+
50
+ # This method performs data relocation.
51
+ # Call when you have finished entering data.
52
+ # If you are using LoadTest::Stat.start or LoadTest::Stat.load, it will be done automatically.
53
+ def finish
54
+ @rows.values.each(&:finish)
55
+ end
56
+
57
+ # Repeat LoadTest::StatRow.
58
+ def each(...)
59
+ @rows.values.each(...)
60
+ end
61
+ end
@@ -0,0 +1,48 @@
1
+ # Statistics
2
+ class LoadTest::StatRow
3
+ # name
4
+ attr_accessor :name
5
+ # Data array for statistics.
6
+ attr_accessor :values
7
+ # An array of errors.
8
+ attr_accessor :errors
9
+
10
+ # Constructor
11
+ def initialize(name)
12
+ @name = name
13
+ @values = []
14
+ @errors = []
15
+ end
16
+
17
+ # This method performs data relocation.
18
+ # Call when you have finished entering data.
19
+ def finish
20
+ @values.sort!
21
+ end
22
+
23
+ # Calculate the average value.
24
+ def average
25
+ @values.sum / @values.size.to_f
26
+ end
27
+
28
+ # Calculate the percentile value.
29
+ def percentile(value)
30
+ index = (@values.size.pred * value / 100.0).round
31
+ @values[index]
32
+ end
33
+
34
+ # Calculate the median value.
35
+ def median
36
+ percentile(50)
37
+ end
38
+
39
+ # Number of data used for statistics.
40
+ def count
41
+ @values.size
42
+ end
43
+
44
+ # number of errors.
45
+ def error_count
46
+ @errors.size
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoadTest
4
+ VERSION = "0.1.0"
5
+ end
data/lib/load_test.rb ADDED
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+ require "async"
5
+
6
+ require_relative "load_test/version"
7
+ require_relative "load_test/future"
8
+ require_relative "load_test/stat"
9
+ require_relative "load_test/stat_row"
10
+
11
+ # load test
12
+ module LoadTest
13
+ module_function
14
+
15
+ # Run a load test.
16
+ #
17
+ # Note: Either limit_time or limit_count is required.
18
+ #
19
+ # rpm:: The number of times to execute the block per minute.
20
+ # limit_time:: After this number of seconds, the test ends.
21
+ # limit_count:: The test ends when the block executes limit_count times.
22
+ # process_size:: Number of processes to launch. Default is equal to the number of CPU cores.
23
+ # block {|result_sender| ... }:: This content is efficiently repeated in parallel. Argument is LoadTest::ResultSender.
24
+ # return:: Enumerator[https://docs.ruby-lang.org/en/master/Enumerator.html]
25
+ def run(rpm:, limit_time: nil, limit_count: nil, process_size: nil, &block)
26
+ if limit_time.nil? && limit_count.nil?
27
+ raise ArgumentError, "Either limit_time or limit_count is required."
28
+ end
29
+
30
+ process_size ||= Etc.nprocessors
31
+
32
+ # request per second (per process)
33
+ rps = (60 / rpm.to_f) * process_size
34
+
35
+ # Number of requests per process.
36
+ if limit_count
37
+ base_request_count = limit_count.div(process_size)
38
+ request_counts = Array.new(process_size, base_request_count)
39
+ (limit_count % process_size).times { request_counts[_1] += 1 }
40
+ else
41
+ request_counts = Array.new(process_size, nil)
42
+ end
43
+
44
+ reader, writer = IO.pipe
45
+ result_sender = LoadTest::ResultSender.new(writer)
46
+
47
+ request_counts.each do |request_count|
48
+ fork do
49
+ reader.close
50
+ error = nil
51
+
52
+ catch do |finish|
53
+ timers = Timers::Group.new
54
+
55
+ timers.after(limit_time) { throw finish } if limit_time
56
+
57
+ timers.every(rps) do
58
+ block.call(result_sender)
59
+ rescue => e
60
+ error = e
61
+ throw finish
62
+ end
63
+
64
+ if request_count
65
+ request_count.times { timers.wait }
66
+ else
67
+ loop { timers.wait }
68
+ end
69
+ end
70
+
71
+ raise error if error
72
+
73
+ ensure
74
+ writer.close
75
+ end
76
+ end
77
+
78
+ writer.close
79
+ Process.waitall
80
+
81
+ result_receiver(reader)
82
+ end
83
+
84
+ # It executes while finely controlling computational resources.
85
+ #
86
+ # process_size:: The number of times to do {Process.fork}[https://docs.ruby-lang.org/en/master/Process.html#method-c-fork].
87
+ # concurrent:: The meaning is close to the number of threads. It's strictly the number of Async[https://github.com/socketry/async].
88
+ # repeat:: number of times to repeat. If nil is specified, it will loop infinitely.
89
+ # block {|result_sender| ... }:: This content is efficiently repeated in parallel. Argument is LoadTest::ResultSender.
90
+ # return:: Enumerator[https://docs.ruby-lang.org/en/master/Enumerator.html]
91
+ def run_custom(process_size: nil, concurrent: 1, repeat: 1, interval: 0, &block)
92
+ process_size ||= Etc.nprocessors
93
+ reader, writer = IO.pipe
94
+ result_sender = LoadTest::ResultSender.new(writer)
95
+
96
+ process_size.times do
97
+ fork do
98
+ reader.close
99
+ Async do |task|
100
+ concurrent.times do
101
+ task.async do
102
+ if repeat
103
+ repeat.times do
104
+ block.call(result_sender)
105
+ sleep interval
106
+ end
107
+ else
108
+ loop do
109
+ block.call(result_sender)
110
+ sleep interval
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ ensure
117
+ writer.close
118
+ end
119
+ end
120
+
121
+ writer.close
122
+ Process.waitall
123
+
124
+ result_receiver(reader)
125
+ end
126
+
127
+ # Create an Enumerator that receives results from the child process.
128
+ #
129
+ # reader:: reader of IO.pipe
130
+ # return:: Enumerator
131
+ def result_receiver(reader) # :nodoc:
132
+ Enumerator.new do |y|
133
+ raise "This Enumerator can only be repeated once. hint: It may be better to use the Enumerator#to_a method to make it an array." if reader.closed?
134
+
135
+ loop do
136
+ break if reader.eof?
137
+ result = Marshal.load(reader)
138
+ raise result if result.is_a?(Exception)
139
+ y << result
140
+ end
141
+ ensure
142
+ reader.close
143
+ end
144
+ end
145
+
146
+ # Send result to parent process.
147
+ class ResultSender
148
+ # Constructor.
149
+ #
150
+ # writer:: writer of IO.pipe
151
+ def initialize(writer)
152
+ @writer = writer
153
+ end
154
+
155
+ # Send result to parent process.
156
+ #
157
+ # data:: Marshalable[https://docs.ruby-lang.org/en/master/Marshal.html] object.
158
+ def <<(data)
159
+ Marshal.dump(data, @writer)
160
+ end
161
+ end
162
+
163
+ # Statistics of load test results.
164
+ def stat(result)
165
+ LoadTest::Stat.load(result)
166
+ end
167
+
168
+ # Output statistics to stdout.
169
+ def output_stdout(stat, columns: [:name, :average, :median, :percentile_80, :percentile_90, :count, :error_count], decimal_place: 2)
170
+ columns.map!(&:to_s)
171
+ column_to_values = columns.to_h { [_1, [_1]] }
172
+
173
+ stat.to_a.sort_by(&:name).each do |stat_row|
174
+ columns.each do |column|
175
+ if /percentile/.match?(column)
176
+ percent = column.split("_")[1]
177
+ column_to_values[column] << stat_row.percentile(Integer(percent)).round(decimal_place).to_s
178
+ else
179
+ raise "Column '#{column}' does not exist." unless stat_row.respond_to?(column)
180
+ result = stat_row.send(column)
181
+ if result.is_a?(Numeric)
182
+ result = result.round(decimal_place)
183
+ end
184
+ column_to_values[column] << result.to_s
185
+ end
186
+ end
187
+ end
188
+
189
+ column_to_size = column_to_values.transform_values do |values|
190
+ values.max_by(&:size).size
191
+ end
192
+
193
+ column_to_values = column_to_values.transform_values { _1[1..] }
194
+
195
+ puts columns.map {
196
+ _1.ljust(column_to_size[_1])
197
+ }.join(" | ")
198
+
199
+ column_to_values.first[1].size.times do |index|
200
+ puts columns.map { |column|
201
+ column_to_values[column][index].rjust(column_to_size[column])
202
+ }.join(" | ")
203
+ end
204
+ end
205
+ end
data/load_test.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/load_test/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "load_test"
7
+ spec.version = LoadTest::VERSION
8
+ spec.authors = ["kekemoto"]
9
+ spec.email = ["kekemoto.hp@gmail.com"]
10
+
11
+ spec.summary = "Load Test for Rubyist."
12
+ spec.description = "A simple and easy load test library."
13
+ spec.homepage = "https://github.com/kekemoto/load_test"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1.0"
16
+
17
+ # spec.metadata["allowed_push_host"] = "https://github.com/kekemoto/load_test"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/kekemoto/load_test.git"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
27
+ end
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_dependency "async", "~> 2.0"
34
+ spec.add_dependency "timers", "~> 4.3"
35
+
36
+ # For more information and examples about making a new gem, check out our
37
+ # guide at: https://bundler.io/guides/creating_gem.html
38
+ end
data/sig/load_test.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module LoadTest
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: load_test
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - kekemoto
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-09-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: timers
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.3'
41
+ description: A simple and easy load test library.
42
+ email:
43
+ - kekemoto.hp@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".standard.yml"
49
+ - CHANGELOG.md
50
+ - Gemfile
51
+ - Gemfile.lock
52
+ - LICENSE.txt
53
+ - README.md
54
+ - Rakefile
55
+ - lib/load_test.rb
56
+ - lib/load_test/future.rb
57
+ - lib/load_test/stat.rb
58
+ - lib/load_test/stat_row.rb
59
+ - lib/load_test/version.rb
60
+ - load_test.gemspec
61
+ - sig/load_test.rbs
62
+ homepage: https://github.com/kekemoto/load_test
63
+ licenses:
64
+ - MIT
65
+ metadata:
66
+ homepage_uri: https://github.com/kekemoto/load_test
67
+ source_code_uri: https://github.com/kekemoto/load_test.git
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 3.1.0
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.3.7
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: Load Test for Rubyist.
87
+ test_files: []