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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/bin/lavin +61 -0
- data/lib/lavin/client.rb +80 -0
- data/lib/lavin/error.rb +5 -0
- data/lib/lavin/http_client.rb +43 -0
- data/lib/lavin/runner.rb +143 -0
- data/lib/lavin/statistics.rb +153 -0
- data/lib/lavin/stats.rb +31 -0
- data/lib/lavin/step.rb +28 -0
- data/lib/lavin/user.rb +36 -0
- data/lib/lavin/user_config.rb +68 -0
- data/lib/lavin/version.rb +5 -0
- data/lib/lavin/web_server.rb +95 -0
- data/lib/lavin/worker.rb +84 -0
- data/lib/lavin.rb +6 -0
- data.tar.gz.sig +1 -0
- metadata +152 -0
- metadata.gz.sig +0 -0
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
|
data/lib/lavin/client.rb
ADDED
@@ -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
|
data/lib/lavin/error.rb
ADDED
@@ -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
|
data/lib/lavin/runner.rb
ADDED
@@ -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
|
data/lib/lavin/stats.rb
ADDED
@@ -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,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
|
data/lib/lavin/worker.rb
ADDED
@@ -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
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
|