lavin 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: 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