stormforge-ruby 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|