load_test 0.1.0

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