stormforge-ruby 0.5.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
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.travis.yml +17 -0
- data/Gemfile +4 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +72 -0
- data/Rakefile +6 -0
- data/bin/stormforge +18 -0
- data/examples/README.md +27 -0
- data/examples/Stormfile +1 -0
- data/examples/demo_case.rb +12 -0
- data/examples/file_upload.rb +28 -0
- data/examples/fixtures/requests.csv +2 -0
- data/examples/fixtures/users.csv +2 -0
- data/examples/simple_and_long.rb +12 -0
- data/examples/simple_and_short.rb +12 -0
- data/examples/test_case_definition_v1.rb +206 -0
- data/lib/core_ext/fixnum.rb +20 -0
- data/lib/stormforge.rb +39 -0
- data/lib/stormforge/client.rb +227 -0
- data/lib/stormforge/dsl.rb +4 -0
- data/lib/stormforge/dsl/test_case.rb +9 -0
- data/lib/stormforge/dsl/test_case/attribute_access.rb +19 -0
- data/lib/stormforge/dsl/test_case/cloud.rb +33 -0
- data/lib/stormforge/dsl/test_case/data_source.rb +49 -0
- data/lib/stormforge/dsl/test_case/data_source/file_fixture.rb +104 -0
- data/lib/stormforge/dsl/test_case/data_source/random_number.rb +64 -0
- data/lib/stormforge/dsl/test_case/data_source/random_string.rb +52 -0
- data/lib/stormforge/dsl/test_case/definition.rb +128 -0
- data/lib/stormforge/dsl/test_case/session.rb +77 -0
- data/lib/stormforge/registry.rb +23 -0
- data/lib/stormforge/version.rb +3 -0
- data/lib/thor/generators/init.rb +14 -0
- data/lib/thor/generators/templates/Stormfile +15 -0
- data/lib/thor/main.rb +79 -0
- data/lib/thor/storm_forge_base.rb +46 -0
- data/lib/thor/testcase.rb +23 -0
- data/lib/thor/testrun.rb +75 -0
- data/spec/client_spec.rb +4 -0
- data/spec/dsl/test_case/attribute_access_spec.rb +46 -0
- data/spec/dsl/test_case/cloud_spec.rb +15 -0
- data/spec/dsl/test_case/data_source/file_fixture_spec.rb +101 -0
- data/spec/dsl/test_case/data_source/random_number_spec.rb +51 -0
- data/spec/dsl/test_case/data_source/random_string_spec.rb +33 -0
- data/spec/dsl/test_case/data_source_spec.rb +12 -0
- data/spec/dsl/test_case/definition_spec.rb +107 -0
- data/spec/dsl/test_case/session_spec.rb +102 -0
- data/spec/dsl/test_case_integration_spec.rb +148 -0
- data/spec/dsl/test_case_spec.rb +8 -0
- data/spec/fixtures/ip_addresses.csv +1 -0
- data/spec/fixtures/slug.csv +1 -0
- data/spec/fixtures/users.csv +2 -0
- data/spec/registry_spec.rb +34 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/stormforger_spec.rb +13 -0
- data/stormforge_ruby.gemspec +38 -0
- metadata +344 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
class StormForge::Dsl::TestCase::DataSource::RandomNumber
|
2
|
+
class InvalidRange < Exception; end;
|
3
|
+
|
4
|
+
include ActiveModel::Serializers::JSON
|
5
|
+
|
6
|
+
self.include_root_in_json = false
|
7
|
+
|
8
|
+
attr_reader :name, :range
|
9
|
+
|
10
|
+
def initialize(name, options)
|
11
|
+
@name = name
|
12
|
+
@sequence = 0
|
13
|
+
|
14
|
+
@range = ensure_range(options.fetch(:range, nil))
|
15
|
+
end
|
16
|
+
|
17
|
+
def next
|
18
|
+
args = {
|
19
|
+
t: "random_number",
|
20
|
+
n: name,
|
21
|
+
seq: (@sequence += 1)
|
22
|
+
}
|
23
|
+
|
24
|
+
StormForge::Dsl::TestCase::DataSource.build_marker("GENERATOR", args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def count
|
28
|
+
@sequence
|
29
|
+
end
|
30
|
+
|
31
|
+
def serializable_hash(*args)
|
32
|
+
{
|
33
|
+
name: name,
|
34
|
+
type: "random_number",
|
35
|
+
range: [range.begin, range.end]
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# TODO this is not so nice, to raise when called from #initialize
|
42
|
+
# This should be changed to validation errors.
|
43
|
+
def ensure_range(range)
|
44
|
+
unless range
|
45
|
+
raise InvalidRange.new("Range of RandomNumber cannot be nil!")
|
46
|
+
end
|
47
|
+
|
48
|
+
if range.instance_of?(Array) && range.size == 2
|
49
|
+
range = range[0]..range[1]
|
50
|
+
end
|
51
|
+
|
52
|
+
if range.instance_of?(Range)
|
53
|
+
unless range.begin.instance_of?(Fixnum) && range.end.instance_of?(Fixnum)
|
54
|
+
raise InvalidRange.new("Range of RandomNumber needs to be range of Fixnums, between 0 and 100000000")
|
55
|
+
end
|
56
|
+
unless range.begin > 0 && range.end <= 100_000_000
|
57
|
+
raise InvalidRange.new("Range of RandomNumber needs to be between 0 and 100000000")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
range
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class StormForge::Dsl::TestCase::DataSource::RandomString
|
2
|
+
include ActiveModel::Serializers::JSON
|
3
|
+
|
4
|
+
self.include_root_in_json = false
|
5
|
+
|
6
|
+
attr_reader :name, :length
|
7
|
+
|
8
|
+
def initialize(name, options)
|
9
|
+
@name = name
|
10
|
+
@sequence = 0
|
11
|
+
|
12
|
+
@length = ensure_length(options.fetch(:length, 1))
|
13
|
+
end
|
14
|
+
|
15
|
+
def next
|
16
|
+
args = {
|
17
|
+
t: "random_string",
|
18
|
+
n: name,
|
19
|
+
seq: (@sequence += 1)
|
20
|
+
}
|
21
|
+
|
22
|
+
StormForge::Dsl::TestCase::DataSource.build_marker("GENERATOR", args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def count
|
26
|
+
@sequence
|
27
|
+
end
|
28
|
+
|
29
|
+
def serializable_hash(*args)
|
30
|
+
{
|
31
|
+
name: name,
|
32
|
+
type: "random_string",
|
33
|
+
length: @length
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# TODO casting the length to an integer is not ideal.
|
41
|
+
# We should have validations here too, like for other
|
42
|
+
# DSL related objects.
|
43
|
+
def ensure_length(length)
|
44
|
+
length = length.to_i
|
45
|
+
|
46
|
+
return 1 if length < 1
|
47
|
+
return 100 if length > 100
|
48
|
+
|
49
|
+
length
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
class StormForge::Dsl::TestCase::Definition
|
2
|
+
include ActiveModel::Validations
|
3
|
+
include ActiveModel::Serializers::JSON
|
4
|
+
include ::StormForge::Dsl::TestCase::AttributeAccess
|
5
|
+
|
6
|
+
attr_reader :attributes
|
7
|
+
|
8
|
+
self.include_root_in_json = true
|
9
|
+
|
10
|
+
validates_presence_of :title
|
11
|
+
validates_presence_of :version
|
12
|
+
|
13
|
+
validates :targets, :length => {
|
14
|
+
minimum: 1,
|
15
|
+
too_short: "at least one target is required"
|
16
|
+
}
|
17
|
+
validates :arrival_phases, :length => {
|
18
|
+
minimum: 1,
|
19
|
+
too_short: "at least one arrival phase is required"
|
20
|
+
}
|
21
|
+
validates :sessions, :length => {
|
22
|
+
minimum: 1,
|
23
|
+
too_short: "at least one session is required"
|
24
|
+
}
|
25
|
+
|
26
|
+
class SessionProbabilityValidator < ActiveModel::EachValidator
|
27
|
+
def validate_each(record, attr, value)
|
28
|
+
total_probability = value.values.map(&:probability).sum
|
29
|
+
|
30
|
+
record.errors.add attr, "the total probability of all sessions need to be 100%" unless total_probability == 100
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
validates :sessions, :session_probability => true
|
35
|
+
|
36
|
+
|
37
|
+
def initialize(slug, callable=nil, &block)
|
38
|
+
@slug = slug
|
39
|
+
|
40
|
+
@attributes = {
|
41
|
+
arrival_phases: [],
|
42
|
+
sessions: {},
|
43
|
+
data_sources: nil
|
44
|
+
}
|
45
|
+
instance_eval(&(callable || block))
|
46
|
+
end
|
47
|
+
|
48
|
+
# TODO this feels messy. is this really the only way to serialize complex attributes?
|
49
|
+
def read_attribute_for_serialization(key)
|
50
|
+
case key
|
51
|
+
when :sessions
|
52
|
+
@attributes[:sessions].each_with_object({}) do |(session_name, session), hash|
|
53
|
+
hash[session_name] = session.serializable_hash
|
54
|
+
end
|
55
|
+
when :cloud
|
56
|
+
@attributes[:cloud].serializable_hash
|
57
|
+
when :data_sources
|
58
|
+
@attributes[:data_sources].serializable_hash if @attributes[:data_sources]
|
59
|
+
else
|
60
|
+
super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def as_json(*args)
|
65
|
+
hash = super
|
66
|
+
hash["dsl_version"] = self.dsl_version.to_s
|
67
|
+
hash["slug"] = slug
|
68
|
+
hash
|
69
|
+
end
|
70
|
+
|
71
|
+
def targets(*targets)
|
72
|
+
return @attributes[:targets] if targets.length == 0
|
73
|
+
@attributes[:targets] = targets.flatten
|
74
|
+
end
|
75
|
+
|
76
|
+
ARRIVAL_PHASE_OPTIONS = {
|
77
|
+
warmup: false,
|
78
|
+
|
79
|
+
duration: nil,
|
80
|
+
duration_unit: :second,
|
81
|
+
|
82
|
+
rate: nil,
|
83
|
+
rate_unit: :second
|
84
|
+
}.freeze
|
85
|
+
|
86
|
+
def arrival_phase(options)
|
87
|
+
@attributes[:arrival_phases] << ARRIVAL_PHASE_OPTIONS.each_with_object({}) do |(option, default), phase_options|
|
88
|
+
phase_options[option] = options.fetch(option, default)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def session(name, probability, callable=nil, &block)
|
93
|
+
@attributes[:sessions][name] = StormForge::Dsl::TestCase::Session.new(name, probability, @attributes[:data_sources], &(callable || block))
|
94
|
+
end
|
95
|
+
|
96
|
+
def available_attributes
|
97
|
+
[
|
98
|
+
:title,
|
99
|
+
:description,
|
100
|
+
:version,
|
101
|
+
:targets,
|
102
|
+
:arrival_phases,
|
103
|
+
:sessions,
|
104
|
+
:cloud,
|
105
|
+
:data_sources
|
106
|
+
]
|
107
|
+
end
|
108
|
+
|
109
|
+
def cloud(provider, callable=nil, &block)
|
110
|
+
@attributes[:cloud] = StormForge::Dsl::TestCase::Cloud.new(provider, &(callable || block))
|
111
|
+
end
|
112
|
+
|
113
|
+
def data_sources(callable=nil, &block)
|
114
|
+
if (callable || block)
|
115
|
+
@attributes[:data_sources] = StormForge::Dsl::TestCase::DataSource.new(&(callable || block))
|
116
|
+
else
|
117
|
+
@attributes[:data_sources]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def slug
|
122
|
+
@slug.to_s.downcase.gsub(/[^a-z0-9\-_]/, "_").gsub(/_+/, "_")
|
123
|
+
end
|
124
|
+
|
125
|
+
def dsl_version
|
126
|
+
:v1
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
class StormForge::Dsl::TestCase::Session
|
2
|
+
include ActiveModel::Validations
|
3
|
+
include ActiveModel::Serializers::JSON
|
4
|
+
include ::StormForge::Dsl::TestCase::AttributeAccess
|
5
|
+
|
6
|
+
self.include_root_in_json = false
|
7
|
+
|
8
|
+
attr_reader :attributes, :data_sources
|
9
|
+
|
10
|
+
validates :name, presence: true
|
11
|
+
validates :probability, :inclusion => 1..100, :numericality => true
|
12
|
+
validates :steps, :length => {
|
13
|
+
minimum: 1,
|
14
|
+
too_short: "at least one step is required"
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize(name, probability, data_sources, callable=nil, &block)
|
18
|
+
@attributes = {
|
19
|
+
name: name,
|
20
|
+
probability: probability,
|
21
|
+
steps: []
|
22
|
+
}
|
23
|
+
@data_sources = data_sources
|
24
|
+
|
25
|
+
instance_eval(&(callable || block))
|
26
|
+
end
|
27
|
+
|
28
|
+
def available_attributes
|
29
|
+
[
|
30
|
+
:name,
|
31
|
+
:probability,
|
32
|
+
:description,
|
33
|
+
:steps
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
[:head, :get, :post, :delete, :put, :options, :patch].each do |request_method|
|
38
|
+
define_method(request_method) do |path, options={}|
|
39
|
+
request(request_method, path, options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def wait(seconds)
|
44
|
+
@attributes[:steps] << {
|
45
|
+
action: :wait,
|
46
|
+
duration: seconds
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def pick(what)
|
51
|
+
raise "DataSource #{what} not defined!" unless @data_sources.sources[what]
|
52
|
+
@data_sources.sources[what].next
|
53
|
+
end
|
54
|
+
|
55
|
+
def serializable_hash(*args)
|
56
|
+
hash = super
|
57
|
+
hash[:data_sources_usage] = if data_sources.present?
|
58
|
+
data_sources.sources.each_with_object({}) do |(name, source), memo|
|
59
|
+
memo[name] = source.count
|
60
|
+
end
|
61
|
+
else
|
62
|
+
{}
|
63
|
+
end
|
64
|
+
hash
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def request(method, path, options={})
|
70
|
+
@attributes[:steps] << {
|
71
|
+
action: :request,
|
72
|
+
method: method.to_sym,
|
73
|
+
path: path,
|
74
|
+
options: options
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class StormForge::Registry
|
2
|
+
include Enumerable
|
3
|
+
extend Forwardable
|
4
|
+
def_delegator :test_cases, :each
|
5
|
+
def_delegator :test_cases, :each_pair
|
6
|
+
def_delegator :test_cases, :[]
|
7
|
+
def_delegator :test_cases, :[], :get
|
8
|
+
|
9
|
+
def define(slug, callable=nil, &block)
|
10
|
+
test_cases[slug] = StormForge::Dsl::TestCase.define(slug, callable || block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset!
|
14
|
+
@test_cases = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def test_cases
|
21
|
+
@test_cases ||= {}
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class StormForge::Thor::Init < Thor::Group
|
2
|
+
include Thor::Actions
|
3
|
+
|
4
|
+
argument :path, :default => "."
|
5
|
+
|
6
|
+
def self.source_root
|
7
|
+
File.join(File.dirname(__FILE__), "templates")
|
8
|
+
end
|
9
|
+
|
10
|
+
def stormfile
|
11
|
+
copy_file "Stormfile", File.join(path, "Stormfile")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# uncomment this line, to load all test cases in 'test_cases/*'
|
2
|
+
# Dir[File.join(File.dirname(__FILE__), "test_cases/*.rb")].each { |test_case| load test_case }
|
3
|
+
|
4
|
+
StormForge.define_case :getting_started do
|
5
|
+
title "My first test case"
|
6
|
+
version "0.1"
|
7
|
+
description "Simple and short test to examine test run process."
|
8
|
+
targets "stage.example.com"
|
9
|
+
|
10
|
+
arrival_phase duration: 5.minutes, rate: 1.per_second
|
11
|
+
|
12
|
+
session "hit start page", 100.percent do
|
13
|
+
get "/"
|
14
|
+
end
|
15
|
+
end
|
data/lib/thor/main.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require "io/console"
|
2
|
+
|
3
|
+
class StormForge::Thor::Main < StormForge::Thor::Base
|
4
|
+
class_option :authfile, type: :string, desc: "StormForger authentication file", default: "~/.stormforger"
|
5
|
+
class_option :stormfile, type: :string, desc: "Stormfile location", default: "./Stormfile"
|
6
|
+
class_option :development, type: :boolean, desc: "run in development mode"
|
7
|
+
class_option :skip_ssl_verify, type: :boolean, desc: "skip SSL verification", default: false
|
8
|
+
|
9
|
+
register StormForge::Thor::Init, "init", "init [PATH]", "Initialize new Stormfile"
|
10
|
+
|
11
|
+
desc "version", "Display version information"
|
12
|
+
def version
|
13
|
+
say "stormforge-ruby version #{StormForge::VERSION}"
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "auth", "Aquire an API token"
|
17
|
+
method_option :email, type: :string, desc: 'Your email'
|
18
|
+
method_option :password, type: :string, desc: 'Your password'
|
19
|
+
def auth
|
20
|
+
error "WARNING! Disabling SSL certificate verification is not recommended. Proceed with caution!" if options[:skip_ssl_verify]
|
21
|
+
email, password = options[:email], options[:password]
|
22
|
+
|
23
|
+
email = ask("Please enter your email:") unless email
|
24
|
+
raise Thor::Error, "You must enter a value for that field." if email.empty?
|
25
|
+
|
26
|
+
# TODO: replace with ask("...", :echo => false) if thor gets a new release
|
27
|
+
print "Please enter your password: "
|
28
|
+
password = STDIN.noecho(&:gets).chomp unless password
|
29
|
+
print "\n"
|
30
|
+
|
31
|
+
raise Thor::Error, "You must enter a value for that field." if password.empty?
|
32
|
+
|
33
|
+
say "Attempting to aquire API token..."
|
34
|
+
|
35
|
+
token = StormForge::Client.new(dev_mode: options[:development], skip_ssl_verify: options[:skip_ssl_verify]).aquire_api_token(email, password)
|
36
|
+
raise Thor::Error, "Authentication error!" unless token
|
37
|
+
|
38
|
+
say "Your API authentication is: #{token}", :yellow
|
39
|
+
unless options[:authfile] == "-"
|
40
|
+
say "Writing/Updating credentials to #{authfile}"
|
41
|
+
File.open(authfile, "w+") do |file|
|
42
|
+
file.puts({ email: email, authentication_token: token, created_at: Time.now }.to_json)
|
43
|
+
end
|
44
|
+
File.chmod(0600, authfile)
|
45
|
+
end
|
46
|
+
rescue Faraday::Error::ConnectionFailed => e
|
47
|
+
p e.message
|
48
|
+
raise Thor::Error, "StormForger API not available :-/ (connection failed)"
|
49
|
+
rescue Faraday::Error::ClientError => e
|
50
|
+
if e.response[:status] == 401
|
51
|
+
handle_authentication_error(e)
|
52
|
+
else
|
53
|
+
raise e
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "ping", "Ping StormForger API"
|
58
|
+
def ping
|
59
|
+
result = client.ping
|
60
|
+
say "pong" if result.status < 399
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "curl", "print curl command template with authentication"
|
64
|
+
def curl
|
65
|
+
say "curl --user '#{authentication["email"]}:#{authentication["authentication_token"]}' -X GET #{client.url_base}"
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "httpie", "print HTTPie command template with authentication"
|
69
|
+
def httpie
|
70
|
+
say "http --auth '#{authentication["email"]}:#{authentication["authentication_token"]}' GET #{client.url_base}"
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "testrun SUBCOMMAND ...ARGS", "Manage test runs"
|
74
|
+
subcommand "testrun", StormForge::Thor::Testrun
|
75
|
+
|
76
|
+
desc "testcase SUBCOMMAND ...ARGS", "Manage test cases"
|
77
|
+
subcommand "testcase", StormForge::Thor::Testcase
|
78
|
+
end
|
79
|
+
|