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,20 @@
|
|
1
|
+
require "active_support/core_ext/numeric/time"
|
2
|
+
|
3
|
+
class Fixnum
|
4
|
+
def percent
|
5
|
+
self
|
6
|
+
end
|
7
|
+
|
8
|
+
def method_missing(method, *args, &block)
|
9
|
+
if unit = method.to_s.scan(/^per_(second|minute|hour)$/).flatten.first
|
10
|
+
self / 1.send(unit.to_sym).to_f
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to_missing?(method, include_private = false)
|
17
|
+
method =~ /^per_(second|minute|hour)$/
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
data/lib/stormforge.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "core_ext/fixnum"
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext"
|
5
|
+
class String
|
6
|
+
alias_method :undent, :strip_heredoc
|
7
|
+
end
|
8
|
+
require "active_model"
|
9
|
+
|
10
|
+
module StormForge
|
11
|
+
end
|
12
|
+
|
13
|
+
require "stormforge/version"
|
14
|
+
|
15
|
+
require "stormforge/client"
|
16
|
+
require "stormforge/registry"
|
17
|
+
|
18
|
+
require "stormforge/dsl"
|
19
|
+
require "stormforge/dsl/test_case"
|
20
|
+
require "stormforge/dsl/test_case/attribute_access"
|
21
|
+
require "stormforge/dsl/test_case/definition"
|
22
|
+
require "stormforge/dsl/test_case/data_source"
|
23
|
+
require "stormforge/dsl/test_case/data_source/file_fixture"
|
24
|
+
require "stormforge/dsl/test_case/data_source/random_number"
|
25
|
+
require "stormforge/dsl/test_case/data_source/random_string"
|
26
|
+
require "stormforge/dsl/test_case/session"
|
27
|
+
require "stormforge/dsl/test_case/cloud"
|
28
|
+
|
29
|
+
module StormForge
|
30
|
+
class << self
|
31
|
+
extend Forwardable
|
32
|
+
def_delegator :registry, :define, :define_case
|
33
|
+
def_delegator :registry, :get, :test_case
|
34
|
+
|
35
|
+
def registry
|
36
|
+
@registry ||= StormForge::Registry.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require "faraday"
|
2
|
+
require "faraday_middleware"
|
3
|
+
|
4
|
+
class StormForge::Client
|
5
|
+
|
6
|
+
attr_reader :email, :token, :url_base
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
@email = options[:user] || options[:email]
|
10
|
+
@token = options[:token]
|
11
|
+
|
12
|
+
@debug = options[:debug]
|
13
|
+
@logger = options.fetch(:logger, logger)
|
14
|
+
|
15
|
+
@skip_ssl_verification = options.fetch(:skip_ssl_verify, false)
|
16
|
+
|
17
|
+
@url_base = if options[:dev_mode]
|
18
|
+
"http://localhost:3000/api"
|
19
|
+
else
|
20
|
+
"https://api.stormforger.com"
|
21
|
+
end
|
22
|
+
@url_base = options[:url_base] if options[:url_base]
|
23
|
+
end
|
24
|
+
|
25
|
+
def connection
|
26
|
+
@connection ||= Faraday.new(url: @url_base, ssl: {
|
27
|
+
verify: !@skip_ssl_verification}) do |faraday|
|
28
|
+
faraday.basic_auth(email, token)
|
29
|
+
|
30
|
+
faraday.headers["User-Agent"] = "stormforge-ruby (#{StormForge::VERSION})"
|
31
|
+
faraday.headers["Content-Type"] = "application/json"
|
32
|
+
|
33
|
+
faraday.request :json
|
34
|
+
faraday.request :multipart
|
35
|
+
|
36
|
+
faraday.response :json, :content_type => /\bjson$/
|
37
|
+
|
38
|
+
faraday.use Faraday::Response::Logger, logger
|
39
|
+
faraday.use Faraday::Response::RaiseError
|
40
|
+
|
41
|
+
faraday.adapter Faraday.default_adapter
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_run_log(test_run_id, since, limit=10, stream=true, &block)
|
46
|
+
include_start = true
|
47
|
+
while (response = fetch_test_run_log(test_run_id, since, limit, include_start)).body["stats"].present? || stream
|
48
|
+
include_start = false
|
49
|
+
since = response.body["last"] || since
|
50
|
+
|
51
|
+
sleep 1 if response.body["stats"].empty?
|
52
|
+
|
53
|
+
response.body["stats"].each do |stats|
|
54
|
+
yield stats
|
55
|
+
end
|
56
|
+
|
57
|
+
break if response.body["end_reached"]
|
58
|
+
end
|
59
|
+
rescue Interrupt
|
60
|
+
STDERR.puts "aborting..."
|
61
|
+
rescue Faraday::Error::ResourceNotFound
|
62
|
+
raise Thor::Error, "Test Run #{test_run_id} not found!"
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch_test_run_log(test_run_id, since, limit, include_start)
|
66
|
+
connection.get("#{test_run_endpoint(test_run_id)}/log?since=#{since}&limit=#{limit}&include_start=#{include_start}")
|
67
|
+
end
|
68
|
+
|
69
|
+
def ping
|
70
|
+
connection.get("/")
|
71
|
+
end
|
72
|
+
|
73
|
+
def aquire_api_token(email, password)
|
74
|
+
response = connection.get authentication_endpoint, email: email, password: password
|
75
|
+
return unless response.status == 200
|
76
|
+
response.body["authentication_token"]
|
77
|
+
end
|
78
|
+
|
79
|
+
def create_or_update(test_case)
|
80
|
+
handle_fixtures(test_case)
|
81
|
+
|
82
|
+
handle_test_case(test_case)
|
83
|
+
end
|
84
|
+
|
85
|
+
def handle_fixtures(test_case)
|
86
|
+
return unless test_case.data_sources
|
87
|
+
|
88
|
+
test_case.data_sources.file_fixture_sources.map do |fixture_name, fixture|
|
89
|
+
logger.info "Fixture #{fixture_name} checking (#{fixture.md5_hash})"
|
90
|
+
next if fixture_exists? fixture.md5_hash
|
91
|
+
logger.info "Fixture #{fixture_name} uploading"
|
92
|
+
|
93
|
+
fixture_upload(fixture)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def handle_test_case(test_case)
|
98
|
+
if existing_test_case = fetch_test_case(test_case.slug)
|
99
|
+
test_case_id = existing_test_case["test_cases"][0]["id"]
|
100
|
+
end
|
101
|
+
|
102
|
+
payload = {
|
103
|
+
test_case: {
|
104
|
+
json_definition: test_case.as_json(root: false),
|
105
|
+
dsl_version: test_case.dsl_version || :v1,
|
106
|
+
slug: test_case.slug,
|
107
|
+
title: test_case.title
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
unless existing_test_case.present?
|
112
|
+
logger.info "Test Case: Creating new test case"
|
113
|
+
|
114
|
+
connection.post do |request|
|
115
|
+
request.url test_case_endpoint
|
116
|
+
request.headers["Content-Type"] = "application/json"
|
117
|
+
request.body = payload.to_json
|
118
|
+
end
|
119
|
+
else
|
120
|
+
logger.info "Test Case: Updating case #{test_case.slug}"
|
121
|
+
|
122
|
+
payload[:id] = test_case_id
|
123
|
+
|
124
|
+
connection.put do |request|
|
125
|
+
request.url test_case_endpoint(test_case.slug)
|
126
|
+
request.headers["Content-Type"] = "application/json"
|
127
|
+
request.body = payload.to_json
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
rescue Faraday::Error::ClientError => e
|
132
|
+
puts e.response
|
133
|
+
end
|
134
|
+
|
135
|
+
def fetch_test_case(slug)
|
136
|
+
connection.get(test_case_endpoint(slug)).body
|
137
|
+
rescue Faraday::Error::ResourceNotFound
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
|
141
|
+
def create_test_run(test_case_slug, description="")
|
142
|
+
connection.post(test_run_endpoint(nil), {
|
143
|
+
test_case: test_case_slug,
|
144
|
+
test_run: { description: description }
|
145
|
+
}).body
|
146
|
+
end
|
147
|
+
|
148
|
+
def fetch_test_run(id)
|
149
|
+
response = connection.get(test_run_endpoint(id)).body
|
150
|
+
|
151
|
+
return unless response
|
152
|
+
|
153
|
+
response["test_runs"][0]
|
154
|
+
rescue Faraday::Error::ResourceNotFound
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
|
158
|
+
def list_test_runs(test_case_slug=nil)
|
159
|
+
connection.get(test_run_endpoint, test_case: test_case_slug).body
|
160
|
+
end
|
161
|
+
|
162
|
+
def abort_test_run(id)
|
163
|
+
connection.post(test_run_abort_endpoint(id)).body
|
164
|
+
end
|
165
|
+
|
166
|
+
def fixture_upload(fixture)
|
167
|
+
logger.info "Uploading file #{fixture.name.to_s}..."
|
168
|
+
|
169
|
+
payload = { fixture: {
|
170
|
+
name: fixture.name.to_s,
|
171
|
+
file: Faraday::UploadIO.new(fixture.source, "text/plain")
|
172
|
+
}}
|
173
|
+
|
174
|
+
connection.post do |request|
|
175
|
+
request.url fixture_endpoint
|
176
|
+
request.headers["Content-Type"] = "multipart/form-data"
|
177
|
+
request.body = payload
|
178
|
+
end
|
179
|
+
|
180
|
+
rescue Exception => e
|
181
|
+
puts "Error while uploading '#{fixture.name.to_s}' ('#{fixture.source}')"
|
182
|
+
puts e.message
|
183
|
+
end
|
184
|
+
|
185
|
+
def fixture_exists?(md5_hash)
|
186
|
+
endpoint = "#{fixture_endpoint}/by_md5/#{md5_hash}.json"
|
187
|
+
connection.get(endpoint).status == 200
|
188
|
+
rescue Faraday::Error::ResourceNotFound
|
189
|
+
false
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def test_case_endpoint(slug=nil)
|
197
|
+
"#{url_base}/test_cases#{slug ? "/#{slug}" : ""}"
|
198
|
+
end
|
199
|
+
|
200
|
+
def test_run_endpoint(id=nil)
|
201
|
+
"#{url_base}/test_runs/#{id}"
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_run_abort_endpoint(id)
|
205
|
+
"#{url_base}/test_runs/#{id}/abort"
|
206
|
+
end
|
207
|
+
|
208
|
+
def fixture_endpoint
|
209
|
+
"#{url_base}/fixtures"
|
210
|
+
end
|
211
|
+
|
212
|
+
def authentication_endpoint
|
213
|
+
"#{url_base}/auth/token"
|
214
|
+
end
|
215
|
+
|
216
|
+
def resource_exists?(endpoint)
|
217
|
+
get(endpoint).response_code == 200
|
218
|
+
end
|
219
|
+
|
220
|
+
def logger
|
221
|
+
return @logger if @logger
|
222
|
+
|
223
|
+
@logger = Logger.new(STDOUT)
|
224
|
+
@logger.level = Logger::WARN
|
225
|
+
@logger
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module StormForge::Dsl::TestCase::AttributeAccess
|
2
|
+
def method_missing(meth, *args, &block)
|
3
|
+
if available_attributes.include? meth
|
4
|
+
case args.size
|
5
|
+
when 0 then @attributes[meth]
|
6
|
+
when 1 then @attributes[meth] = args.first
|
7
|
+
else raise ArgumentError
|
8
|
+
end
|
9
|
+
elsif available_attributes.map {|a| "#{a}=".to_sym }.include? meth
|
10
|
+
@attributes[meth.to_s.split("=").first.to_sym] = args.first
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to_missing?(meth, *args, &blk)
|
17
|
+
available_attributes.include? meth
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class StormForge::Dsl::TestCase::Cloud
|
2
|
+
include ActiveModel::Serializers::JSON
|
3
|
+
include ActiveModel::Validations
|
4
|
+
include ::StormForge::Dsl::TestCase::AttributeAccess
|
5
|
+
|
6
|
+
self.include_root_in_json = false
|
7
|
+
|
8
|
+
attr_reader :attributes
|
9
|
+
|
10
|
+
validates :provider, :inclusion => { in: Proc.new { supported_cloud_providers } }
|
11
|
+
|
12
|
+
def initialize(provider, callable=nil, &block)
|
13
|
+
@attributes = {}
|
14
|
+
|
15
|
+
@attributes[:provider] = provider.to_sym
|
16
|
+
@attributes[:security_groups] = []
|
17
|
+
|
18
|
+
instance_eval(&(callable || block))
|
19
|
+
end
|
20
|
+
|
21
|
+
def security_groups(*groups)
|
22
|
+
@attributes[:security_groups] += groups
|
23
|
+
end
|
24
|
+
alias_method :security_group, :security_groups
|
25
|
+
|
26
|
+
def available_attributes
|
27
|
+
[ :provider ]
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.supported_cloud_providers
|
31
|
+
[ :aws ]
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class StormForge::Dsl::TestCase::DataSource
|
2
|
+
|
3
|
+
class << self
|
4
|
+
def build_marker(base_type, args)
|
5
|
+
"${#{base_type}|#{args_to_param(args)}}"
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def args_to_param(args)
|
11
|
+
args.each_pair.map { |key, value| "#{key}=#{value}" }.join("-")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(callable=nil, &block)
|
16
|
+
@data_sources = {}
|
17
|
+
|
18
|
+
instance_eval(&(callable || block))
|
19
|
+
end
|
20
|
+
|
21
|
+
def random_string(name, options)
|
22
|
+
@data_sources[name] = RandomString.new(name, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def random_number(name, options)
|
26
|
+
@data_sources[name] = RandomNumber.new(name, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def file(name, options)
|
30
|
+
@data_sources[name] = FileFixture.new(name, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def file_fixture_sources
|
34
|
+
@data_sources.each_with_object({}) do |(name, source), memo|
|
35
|
+
memo[name] = source if source.instance_of? FileFixture
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def sources
|
40
|
+
@data_sources
|
41
|
+
end
|
42
|
+
|
43
|
+
def serializable_hash(*args)
|
44
|
+
@data_sources.each_with_object({}) do |(name, data_source), hash|
|
45
|
+
hash[name] = data_source
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
class StormForge::Dsl::TestCase::DataSource::FileFixture
|
2
|
+
|
3
|
+
class ItemProxy
|
4
|
+
attr_reader :base_args, :fields
|
5
|
+
|
6
|
+
def initialize(base_args, fields)
|
7
|
+
@base_args = base_args
|
8
|
+
@fields = fields
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(meth, *args, &blk)
|
12
|
+
if fields.include? meth
|
13
|
+
StormForge::Dsl::TestCase::DataSource.build_marker("FILEFIXTURE", {
|
14
|
+
f: meth
|
15
|
+
}.merge(base_args))
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def respond_to_missing?(meth, include_private = false)
|
22
|
+
fields.include?(meth) || super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
include ActiveModel::Validations
|
28
|
+
include ActiveModel::Serializers::JSON
|
29
|
+
include ::StormForge::Dsl::TestCase::AttributeAccess
|
30
|
+
|
31
|
+
self.include_root_in_json = false
|
32
|
+
|
33
|
+
attr_reader :attributes
|
34
|
+
|
35
|
+
validates :name, presence: true
|
36
|
+
validates :source, presence: true
|
37
|
+
validates :order, inclusion: { in: [:randomly, :sequentially] }
|
38
|
+
|
39
|
+
class FixtureSourceValidator < ActiveModel::Validator
|
40
|
+
def validate(record)
|
41
|
+
if record.source.nil?
|
42
|
+
record.errors[:source] << "Source file must be set"
|
43
|
+
elsif !File.exist?(record.source)
|
44
|
+
record.errors[:source] << "Source file must exist"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
validates_with FixtureSourceValidator
|
50
|
+
|
51
|
+
def initialize(name, options)
|
52
|
+
@attributes = {
|
53
|
+
name: name
|
54
|
+
}.merge(options)
|
55
|
+
|
56
|
+
unless @attributes[:simple_file]
|
57
|
+
@attributes[:order] = :sequentially unless @attributes[:order]
|
58
|
+
@attributes[:delimiter] = ";" unless @attributes[:delimiter]
|
59
|
+
end
|
60
|
+
|
61
|
+
@sequence = 0
|
62
|
+
end
|
63
|
+
|
64
|
+
def available_attributes
|
65
|
+
[
|
66
|
+
:name,
|
67
|
+
:source,
|
68
|
+
:fields,
|
69
|
+
:delimiter,
|
70
|
+
:order,
|
71
|
+
:simple_file
|
72
|
+
]
|
73
|
+
end
|
74
|
+
|
75
|
+
def md5_hash
|
76
|
+
::Digest::MD5.file(self.source).hexdigest if File.exist? self.source
|
77
|
+
end
|
78
|
+
|
79
|
+
def as_json(*args)
|
80
|
+
hash = super
|
81
|
+
hash[:type] = "file_fixture"
|
82
|
+
hash[:md5_hash] = self.md5_hash
|
83
|
+
hash[:source] = File.basename(self.source)
|
84
|
+
hash
|
85
|
+
end
|
86
|
+
|
87
|
+
def next
|
88
|
+
args = {
|
89
|
+
n: name,
|
90
|
+
seq: (@sequence += 1)
|
91
|
+
}
|
92
|
+
|
93
|
+
if fields.present?
|
94
|
+
ItemProxy.new(args, fields)
|
95
|
+
else
|
96
|
+
StormForge::Dsl::TestCase::DataSource.build_marker("FILEFIXTURE", args)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def count
|
101
|
+
@sequence
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|