lavin 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: ed47d43b5e5f91e1af2c3e319fa03d3588aee9826215394038359e559773a173
4
+ data.tar.gz: 17261c42f46a7c4e1ecff28adbd92434c5ec66ac28d4c208d6195a55bf49f69d
5
+ SHA512:
6
+ metadata.gz: 747a493c85dd8fe99c5da92d9fc8380248f80a7b0483bfe42ba3f48fc8a06c89ecc45dfa02f424304b75d28709dfd7c35bc9137a658b22cb449dab91a4bb13ff
7
+ data.tar.gz: 304de7aa069cb83a61f3530bec34df37349e4572cdc4eb23ec20b86daa4a75606f207248f7b4efe5f3338d8f4166ba94c71ee33e856bc22e6cb36720585627cf
checksums.yaml.gz.sig ADDED
Binary file
data/bin/lavin ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.expand_path("../lib", __dir__)
4
+ require 'lavin'
5
+ require 'optionparser'
6
+
7
+ module Lavin
8
+ class Script
9
+ def self.run(*files, web_ui: true)
10
+ exit_with_usage(msg: "No file or directory given") if files.empty?
11
+ files.keep_if { |file| File.exists? file }
12
+ new(*files, web_ui:).run
13
+ end
14
+
15
+ def self.usage
16
+ puts "\nUsage: #{$PROGRAM_NAME} PATH_TO_DIRECTORY_OR_FILE [*PATH_TO_DIRECTORY_OR_FILE]"
17
+ end
18
+
19
+ def self.exit_with_usage(msg: nil, status: 1)
20
+ puts msg if msg
21
+ usage
22
+ exit(status)
23
+ end
24
+
25
+ attr_reader :files, :web_ui
26
+
27
+ def initialize(*files, web_ui: true)
28
+ @files = files
29
+ @web_ui = web_ui
30
+ end
31
+
32
+ def run
33
+ files.each do |file|
34
+ if Dir.exists? file
35
+ $LOAD_PATH << file unless $LOAD_PATH.include? file
36
+ Dir.children(file).each { |file| require file }
37
+ else
38
+ $LOAD_PATH << "." unless $LOAD_PATH.include? "."
39
+ require file
40
+ end
41
+ end
42
+
43
+ if web_ui
44
+ require 'lavin/web_server'
45
+ Lavin::WebServer.run!
46
+ else
47
+ runner = Lavin::Runner.new
48
+ runner.start
49
+ runner.wait
50
+ Statistics.show
51
+ end
52
+ end
53
+ end
54
+
55
+ if (ARGV & ["-h", "--help"]).size.positive?
56
+ Script.exit_with_usage(status: 0)
57
+ else
58
+ web_ui = ARGV.delete("--no-web").nil?
59
+ Script.run(*ARGV, web_ui:)
60
+ end
61
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async/http/internet'
4
+
5
+ module Lavin
6
+ class Client
7
+ class Error < Lavin::Error; end
8
+ class NoCurrentAsyncTaskError < Error
9
+ def initialize(msg = nil)
10
+ super(msg || "Trying to create a client outside of an Async task")
11
+ end
12
+ end
13
+
14
+ DEFAULT_HEADERS = {
15
+ 'User-Agent'=> 'LavinLoadTest',
16
+ 'Accept' => '*/*',
17
+ }.freeze
18
+
19
+ attr_reader :internet, :base_url
20
+ attr_accessor :request_count, :cookie
21
+
22
+ def initialize(base_url = nil)
23
+ raise NoCurrentAsyncTaskError unless Async::Task.current?
24
+
25
+ @internet = Async::HTTP::Internet.new
26
+ @base_url = base_url
27
+ @request_count = 0
28
+ @cookie = nil
29
+ end
30
+
31
+ def close
32
+ internet.close
33
+ end
34
+
35
+ def request(method, url:, headers:, body: nil)
36
+ url, headers, body = rewrite_request(url:, headers:, body:)
37
+
38
+ start_time = Time.now
39
+ response = internet.send(method, url, headers, body)
40
+ duration = Time.now - start_time
41
+
42
+ status, headers, body = process(response)
43
+
44
+ Statistics.register_request(method:, url:, status:, duration:)
45
+
46
+ {status:, headers:, body: body}
47
+ end
48
+
49
+ private
50
+
51
+ def rewrite_request(url:, headers:, body:)
52
+ headers = DEFAULT_HEADERS.merge(headers || {})
53
+ headers["Cookie"] = cookie if cookie
54
+
55
+ if body.is_a? Hash
56
+ body = JSON.dump(body)
57
+ headers["Content-Type"] = "application/json"
58
+ end
59
+
60
+ url = File.join(base_url, url) if base_url && !url.start_with?(/https?:/)
61
+
62
+ [url, headers, body]
63
+ end
64
+
65
+ def process(response)
66
+ status = response.status
67
+ headers = response.headers
68
+ body = response.read
69
+ save_cookie(headers)
70
+ self.request_count += 1
71
+
72
+ [status, headers, body]
73
+ end
74
+
75
+ def save_cookie(headers)
76
+ cookie = headers['Set-Cookie'] || headers['SET-COOKIE'] || headers['set-cookie']
77
+ self.cookie = cookie if cookie
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lavin
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lavin/client'
4
+
5
+ module Lavin
6
+ module HttpClient
7
+ attr_reader :client
8
+ attr_writer :index
9
+
10
+ def initialize(**kwargs)
11
+ super(**kwargs)
12
+ @client = Client.new(config[:base_url])
13
+ end
14
+
15
+ def cleanup
16
+ client&.close
17
+ end
18
+
19
+ def get(url, headers: {})
20
+ client.request(:get, url:, headers:)
21
+ end
22
+
23
+ def head(url, headers: {})
24
+ client.request(:head, url:, headers:)
25
+ end
26
+
27
+ def post(url, headers: {}, body: nil)
28
+ client.request(:post, url:, headers:, body:)
29
+ end
30
+
31
+ def put(url, headers: {}, body: nil)
32
+ client.request(:put, url:, headers:, body:)
33
+ end
34
+
35
+ def patch(url, headers: {}, body: nil)
36
+ client.request(:patch, url:, headers:, body:)
37
+ end
38
+
39
+ def delete(url, headers: {})
40
+ client.request(:delete, url:, headers:)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+ require 'lavin/user'
5
+ require 'lavin/statistics'
6
+
7
+ module Lavin
8
+ class Runner
9
+ class Entry
10
+ class DepletedError < Lavin::Error
11
+ def initialize(msg = nil)
12
+ super(msg || "Depleted")
13
+ end
14
+ end
15
+
16
+ attr_reader :persona, :name
17
+ attr_accessor :count
18
+
19
+ def initialize(persona)
20
+ @persona = persona
21
+ @name = persona.config[:name]
22
+ @count = persona.user_count
23
+ end
24
+
25
+ def present?
26
+ count.positive?
27
+ end
28
+
29
+ def get
30
+ raise DepletedError unless present?
31
+
32
+ self.count -= 1
33
+ persona
34
+ end
35
+ end
36
+
37
+ attr_reader :personas, :total_users, :remaining
38
+ attr_accessor :spawned_users, :index
39
+
40
+ class << self
41
+ attr_accessor :thread
42
+
43
+ def yield
44
+ Async::Task.current.yield if Async::Task.current?
45
+ end
46
+
47
+ def start
48
+ new.start
49
+ end
50
+
51
+ def start_async
52
+ self.thread = Thread.new do
53
+ new.start
54
+ stop
55
+ exit
56
+ end
57
+ rescue StandardError => error
58
+ puts "Failed to run in thread: #{error.message}"
59
+ thread.join
60
+ thread = nil
61
+ raise
62
+ end
63
+
64
+ def stop
65
+ Statistics.stop
66
+ thread&.kill
67
+ self.thread = nil
68
+ end
69
+
70
+ def running?
71
+ return false unless thread
72
+ return true if %w[run sleep].include? thread.status
73
+
74
+ stop
75
+ false
76
+ end
77
+ end
78
+
79
+ def initialize(personas: nil)
80
+ @personas = personas || User.personas
81
+ @remaining = @personas.map { |persona| Entry.new(persona) }
82
+ @total_users = @remaining.sum(&:count)
83
+ @spawned_users = 0
84
+ @index = -1
85
+ end
86
+
87
+ def start
88
+ Statistics.meassure { create_async_task }
89
+ rescue StandardError => error
90
+ puts "Failed to run tasks: #{error.message}"
91
+ raise
92
+ ensure
93
+ stop
94
+ end
95
+
96
+ def create_async_task
97
+ @task = Async(annotation: "Main") do |task|
98
+ spawn(count: total_users) do |persona|
99
+ next unless persona
100
+
101
+ user_index = spawned_users
102
+ annotation = "User: #{persona.name} ##{user_index}"
103
+ task.async(annotation:) do |user_task|
104
+ user = persona.new(user_index:, task: user_task)
105
+ user.run
106
+ ensure
107
+ user.cleanup
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def wait
114
+ @task&.wait
115
+ end
116
+
117
+ def stop
118
+ @task&.stop
119
+ @task&.wait
120
+ end
121
+
122
+ def spawn(count: 1)
123
+ count.times do
124
+ persona = next_persona
125
+ break unless persona
126
+
127
+ self.spawned_users += 1
128
+ yield persona
129
+ end
130
+ end
131
+
132
+ def next_persona
133
+ self.index += 1
134
+
135
+ entry = remaining[index % remaining.size]
136
+ if entry.present?
137
+ entry.get
138
+ elsif remaining.any?(&:present?)
139
+ next_persona
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lavin/stats'
4
+
5
+ module Lavin
6
+ class Statistics
7
+ class << self
8
+ # FIXME: make thread safe
9
+
10
+ def meassure
11
+ reset
12
+ data[:start] = Time.now
13
+ yield.tap { data[:duration] = Time.now - data[:start] }
14
+ end
15
+
16
+ def reset
17
+ self.data = new_data
18
+ end
19
+
20
+ def stop
21
+ return if data.frozen?
22
+
23
+ data[:duration] = (Time.now - data[:start])
24
+ data.freeze
25
+ end
26
+
27
+ def total_requests
28
+ data[:total_requests]
29
+ end
30
+
31
+ def duration
32
+ return data[:duration] if data[:duration]
33
+
34
+ Time.now - data[:start] if data[:start]
35
+ end
36
+
37
+ def register_request(method:, url:, status:, duration:)
38
+ data[:total_requests] += 1
39
+ key = [method, url]
40
+ result = {status:, duration:}
41
+ result[:failure] = true if status > 499
42
+ data[:requests][key] << result
43
+ end
44
+
45
+ def register_step
46
+ # TODO
47
+ end
48
+
49
+ def stats
50
+ reset unless data
51
+ time = Time.now
52
+
53
+ requests = data[:requests].map do |(method, url), requests|
54
+ durations = []
55
+ statuses = []
56
+ requests.each do |request|
57
+ durations << request[:duration]
58
+ statuses << request[:status]
59
+ end
60
+ min_duration = durations.min
61
+ max_duration = durations.max
62
+ avg_duration = durations.empty? ? 0 : (durations.sum / durations.size)
63
+
64
+ {
65
+ method: method.to_s.upcase,
66
+ url: url,
67
+ requests: requests.size,
68
+ statuses: statuses.tally,
69
+ avg_duration: avg_duration,
70
+ min_duration: min_duration,
71
+ max_duration: max_duration,
72
+ }
73
+ end
74
+
75
+ Stats.new(
76
+ duration: duration,
77
+ total_requests: total_requests,
78
+ rate: duration ? format("%.2f", total_requests / duration) : 0,
79
+ requests: requests
80
+ ).tap do |stats|
81
+ # FIXME remove!
82
+ puts "Calculated stats in #{Time.now - time}s"
83
+ end
84
+ end
85
+
86
+ def show
87
+ values = stats
88
+
89
+ show_summary(values)
90
+
91
+ show_table(values) do |request_values|
92
+ format(
93
+ "%-6<method>s %-100<url>s %6<requests>d %12<avg_duration>fs %12<min_duration>fs %12<max_duration>fs",
94
+ **request_values
95
+ )
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def data
102
+ synchronize { @data ||= new_data }
103
+ end
104
+
105
+ def data=(values)
106
+ synchronize { @data = values }
107
+ end
108
+
109
+ def synchronize(&block)
110
+ @mutex ||= Thread::Mutex.new
111
+ @mutex.synchronize(&block)
112
+ end
113
+
114
+ def new_data
115
+ {
116
+ start: nil,
117
+ duration: nil,
118
+ total_requests: 0,
119
+ steps: [],
120
+ requests: Hash.new { |h, k| h[k] = [] }
121
+ }
122
+ end
123
+
124
+ def show_summary(values)
125
+ puts <<~RESULT
126
+
127
+ Lavin results:
128
+ Test ran for #{values.duration}s
129
+ Total number of requests: #{values.total_requests}
130
+ Rate: #{format("%.2f", values.total_requests/values.duration)} rps
131
+
132
+ RESULT
133
+ end
134
+
135
+ def show_table(values)
136
+ puts format(
137
+ "%-6<method>s %-100<url>s %-6<requests>s %12<avg_duration>s %12<min_duration>s %12<max_duration>s",
138
+ method: "Method",
139
+ url: "URL",
140
+ requests: "Requests",
141
+ avg_duration: "Avg duration",
142
+ min_duration: "Min duration",
143
+ max_duration: "Max duration"
144
+ )
145
+
146
+ divider = "-" * 156
147
+ puts divider
148
+ values.each_request { |request_values| puts yield request_values }
149
+ puts divider
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lavin
4
+ class Stats
5
+ attr_reader :duration, :total_requests, :rate, :requests
6
+
7
+ def initialize(duration:, total_requests:, rate:, requests: [])
8
+ @duration = duration
9
+ @total_requests = total_requests
10
+ @rate = rate
11
+ @requests = requests
12
+ end
13
+
14
+ def empty?
15
+ requests.empty?
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ duration:,
21
+ total_requests:,
22
+ rate:,
23
+ requests:
24
+ }
25
+ end
26
+
27
+ def each_request(&block)
28
+ requests.each(&block)
29
+ end
30
+ end
31
+ end
data/lib/lavin/step.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lavin
4
+ class Step
5
+ attr_reader :user, :block, :repeat
6
+
7
+ def initialize(repeat: 1, &block)
8
+ @repeat = repeat
9
+ @block = block
10
+ end
11
+
12
+ def run(context: nil)
13
+ repeat.times do
14
+ call(context:)
15
+ Runner.yield
16
+ end
17
+ end
18
+
19
+ def call(context:)
20
+ context.instance_exec(&block)
21
+ # Report Success!
22
+ rescue => error
23
+ puts "Caught an error - #{error.class}: #{error.message}"
24
+ puts error.backtrace
25
+ # Report Failure!
26
+ end
27
+ end
28
+ end
data/lib/lavin/user.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "lavin/user_config"
5
+ require "lavin/worker"
6
+ require "lavin/http_client"
7
+
8
+ module Lavin
9
+ class User
10
+ def self.inherited(subclass)
11
+ super
12
+ subclass.include UserConfig
13
+ subclass.include Worker
14
+ subclass.include HttpClient
15
+ all_personas << subclass
16
+ end
17
+
18
+ def self.all_personas
19
+ @all_personas ||= Set.new
20
+ end
21
+
22
+ def self.personas
23
+ all_personas.select(&:enabled?)
24
+ end
25
+
26
+ attr_reader :user_index
27
+
28
+ def initialize(**options)
29
+ @user_index = options.delete(:user_index)
30
+ end
31
+
32
+ def user_name
33
+ "#{name}##{user_index}"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lavin
4
+ module UserConfig
5
+ DEFAULT = {
6
+ enabled: true,
7
+ user_count: 1,
8
+ iterations: 1,
9
+ base_url: nil
10
+ }.freeze
11
+
12
+ module ClassMethods
13
+ def config
14
+ @config ||= DEFAULT.dup
15
+ end
16
+
17
+ DEFAULT.each_key do |name|
18
+ define_method(name) do |value = :no_value_given|
19
+ current = config.fetch(name) # Make sure the key exist!
20
+ if value == :no_value_given
21
+ current
22
+ else
23
+ config[name] = value
24
+ end
25
+ end
26
+ end
27
+
28
+ def name(value = :no_value_given)
29
+ if value == :no_value_given
30
+ @name ||= to_s
31
+ else
32
+ @name = value
33
+ end
34
+ end
35
+
36
+ def description(value = :no_value_given)
37
+ if value == :no_value_given
38
+ @description ||= ""
39
+ else
40
+ @description = value
41
+ end
42
+ end
43
+
44
+ def enabled?
45
+ !!config[:enabled]
46
+ end
47
+ end
48
+
49
+ def self.included(base)
50
+ base.extend(ClassMethods)
51
+ end
52
+
53
+ attr_reader :config
54
+
55
+ def initialize(**kwargs)
56
+ @config = self.class.config
57
+ super(**kwargs)
58
+ end
59
+
60
+ def name
61
+ self.class.name
62
+ end
63
+
64
+ def description
65
+ self.class.description
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lavin
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require 'lavin/user'
5
+ require 'lavin/statistics'
6
+
7
+ module Lavin
8
+ class WebServer < Sinatra::Base
9
+ set :views, File.expand_path("../../views", __dir__)
10
+ set :port, 1080
11
+
12
+ not_found do
13
+ erb :not_found, status: 404
14
+ end
15
+
16
+ error do
17
+ erb :server_error, status: 500, locals: {error: env['sinatra.error']}
18
+ end
19
+
20
+ helpers do
21
+ def input_type_for(value)
22
+ case value
23
+ when Numeric
24
+ "number"
25
+ when TrueClass, FalseClass
26
+ "checkbox"
27
+ else
28
+ "text"
29
+ end
30
+ end
31
+ end
32
+
33
+ get '/' do
34
+ erb :index, locals: { personas: Lavin::User.all_personas }
35
+ end
36
+
37
+ post '/start' do
38
+ Statistics.reset
39
+ Lavin::Runner.start_async
40
+ redirect to('/statistics')
41
+ end
42
+
43
+ get '/statistics' do
44
+ stats = Statistics.stats
45
+ running = Lavin::Runner.running?
46
+ if stats.empty? && !running
47
+ redirect to('/')
48
+ else
49
+ erb :statistics, locals: {stats:, running:}
50
+ end
51
+ end
52
+
53
+ get '/edit' do
54
+ persona = find_persona
55
+ raise Sinatra::NotFound unless persona
56
+
57
+ erb :edit, locals: {persona:}
58
+ end
59
+
60
+ post '/update_config' do
61
+ persona = find_persona
62
+ raise Sinatra::NotFound unless persona
63
+
64
+ persona.config.each do |key, old_value|
65
+ bool = [true, false].include? old_value
66
+ next unless bool || params.key?(key.to_s)
67
+
68
+ new_value = rewrite_config_value(params[key.to_s], old_value)
69
+ persona.send(key, new_value)
70
+ end
71
+
72
+ redirect to('/')
73
+ end
74
+
75
+ def find_persona(name = nil)
76
+ name ||= params['persona']
77
+ Lavin::User.all_personas.find do |persona|
78
+ persona.name == name
79
+ end
80
+ end
81
+
82
+ def rewrite_config_value(new_value, old_value)
83
+ case old_value
84
+ when TrueClass, FalseClass
85
+ !!new_value
86
+ when Integer
87
+ new_value.to_i
88
+ when Float
89
+ new_value.to_f
90
+ else
91
+ new_value.to_s
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lavin/step'
4
+
5
+ module Lavin
6
+ module Worker
7
+ module ClassMethods
8
+ def before(&block)
9
+ return @before unless block
10
+
11
+ @before = block
12
+ end
13
+
14
+ def after(&block)
15
+ return @after unless block
16
+
17
+ @after = block
18
+ end
19
+
20
+ def steps
21
+ @steps ||= []
22
+ end
23
+
24
+ def step(**options, &block)
25
+ steps << Step.new(**options, &block)
26
+ end
27
+ end
28
+
29
+ def self.included(base)
30
+ base.extend(ClassMethods)
31
+ end
32
+
33
+ attr_writer :index
34
+
35
+ def initialize(**kwargs)
36
+ @task = kwargs.delete(:task)
37
+ super(**kwargs)
38
+ end
39
+
40
+ def run
41
+ self.class.before.call.then { Runner.yield } if self.class.before
42
+
43
+ run_step until finished?
44
+
45
+ self.class.after.call.then { Runner.yield } if self.class.after
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :task
51
+
52
+ def sleep(seconds)
53
+ task.sleep seconds
54
+ end
55
+
56
+ def run_step
57
+ current_step = steps[step_index]
58
+ self.index += 1
59
+ current_step&.run(context: self)
60
+ end
61
+
62
+ def steps
63
+ self.class.steps
64
+ end
65
+
66
+ def index
67
+ @index ||= 0
68
+ end
69
+
70
+ def step_index
71
+ index % steps.size
72
+ end
73
+
74
+ def iteration
75
+ @iteration ||= 0
76
+ end
77
+
78
+ def finished?
79
+ return false if config[:iterations].negative?
80
+
81
+ (index / steps.size) >= config[:iterations]
82
+ end
83
+ end
84
+ end
data/lib/lavin.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'debug'
4
+ require 'lavin/version'
5
+ require 'lavin/error'
6
+ require 'lavin/runner'
data.tar.gz.sig ADDED
@@ -0,0 +1 @@
1
+ w���r�We��sk�8ƚ�J�A��sl��� k*�z4��\��w�x
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lavin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sammy Henningsson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDVjCCAj6gAwIBAgIBATANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9zYW1t
14
+ eS5oZW5uaW5nc3Nvbi9EQz1oZXkvREM9Y29tMB4XDTIyMDEzMTIwNTQxMloXDTI0
15
+ MDEzMTIwNTQxMlowKjEoMCYGA1UEAwwfc2FtbXkuaGVubmluZ3Nzb24vREM9aGV5
16
+ L0RDPWNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK+SDC1mfyhu
17
+ cJ6Va21rIHUGscEtQrdvyBqxFG1s2TgPMAv4RbqwdJVPa7kjtbCzslADlUE1oru2
18
+ C+rcJsMtVGX02ukMIPHT1OjTyy0/EMqLqSy3WeRI8APyDSxCVbe+h5BMf3zZnYfd
19
+ dR6AeG7ln09T1P/tX+9lTMc+I+DW1fUlQgY48CNUayvtJR61svXvXMrhLhi29SQi
20
+ g1qmH6Zoe22/JgH+m2JksPndY5Ep3gqfDc6Imwu2vGvmGErJD63FB0XQ/wb4WVH4
21
+ l7sHQSTfKDp8SImCt1xqNgIyjw578ZG2geGLoncuxgDrbQ/UFIJ11lDZd4vLevMh
22
+ nIxTSJpPr2cCAwEAAaOBhjCBgzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNV
23
+ HQ4EFgQUukjj1Cd2ea6IOHDLZe0ymzs2jWkwJAYDVR0RBB0wG4EZc2FtbXkuaGVu
24
+ bmluZ3Nzb25AaGV5LmNvbTAkBgNVHRIEHTAbgRlzYW1teS5oZW5uaW5nc3NvbkBo
25
+ ZXkuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQCNUrjx1+L5W9R5O5MF3fnnNIyxOXGa
26
+ qzqpmXpTd7pHUGvSzVxYM/NIkUHSSPyxLAG+RYMPJHjDOSzhLmOUbgpfOsQ8sqUP
27
+ plABBC03e/+oDwFwlHefonil+sp/AOT9TYZJQbShjKt3/X+VGzjtujJZwvKgiiP0
28
+ Ht5Q4dvW1ZqUZgvXdYM1LnCX7I3WjoRyhOwdSlaEz5gnD+KYewHiiByK1Jv2n0PG
29
+ DVzXaUnsmwP+jQ1PkDa5q8ibBzMd2c6Hmm87UDqPxZtML0bF9SjrpbyLMjwtXaMA
30
+ WDPp0ajpdUZ9GPHsrVNYXiOfQIqcmlmpYVsH1o7vuneUIcIDMrnMDChh
31
+ -----END CERTIFICATE-----
32
+ date: 2022-10-30 00:00:00.000000000 Z
33
+ dependencies:
34
+ - !ruby/object:Gem::Dependency
35
+ name: async
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.2'
48
+ - !ruby/object:Gem::Dependency
49
+ name: async-http
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.59'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.59'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sinatra
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ type: :runtime
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: debug
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.6'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.6'
90
+ - !ruby/object:Gem::Dependency
91
+ name: standard
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.16'
97
+ type: :development
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.16'
104
+ description:
105
+ email:
106
+ - sammy.henningsson@hemnet.se
107
+ executables:
108
+ - lavin
109
+ extensions: []
110
+ extra_rdoc_files: []
111
+ files:
112
+ - bin/lavin
113
+ - lib/lavin.rb
114
+ - lib/lavin/client.rb
115
+ - lib/lavin/error.rb
116
+ - lib/lavin/http_client.rb
117
+ - lib/lavin/runner.rb
118
+ - lib/lavin/statistics.rb
119
+ - lib/lavin/stats.rb
120
+ - lib/lavin/step.rb
121
+ - lib/lavin/user.rb
122
+ - lib/lavin/user_config.rb
123
+ - lib/lavin/version.rb
124
+ - lib/lavin/web_server.rb
125
+ - lib/lavin/worker.rb
126
+ homepage: https://github.com/sammyhenningsson/lavin
127
+ licenses:
128
+ - MIT
129
+ metadata:
130
+ homepage_uri: https://github.com/sammyhenningsson/lavin
131
+ source_code_uri: https://github.com/sammyhenningsson/lavin
132
+ changelog_uri: https://github.com/sammyhenningsson/lavin/CHANGELOG
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: 3.0.3
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.3.7
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: A framework for loadtesting sites.
152
+ test_files: []
metadata.gz.sig ADDED
Binary file