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