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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +17 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +6 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +72 -0
  9. data/Rakefile +6 -0
  10. data/bin/stormforge +18 -0
  11. data/examples/README.md +27 -0
  12. data/examples/Stormfile +1 -0
  13. data/examples/demo_case.rb +12 -0
  14. data/examples/file_upload.rb +28 -0
  15. data/examples/fixtures/requests.csv +2 -0
  16. data/examples/fixtures/users.csv +2 -0
  17. data/examples/simple_and_long.rb +12 -0
  18. data/examples/simple_and_short.rb +12 -0
  19. data/examples/test_case_definition_v1.rb +206 -0
  20. data/lib/core_ext/fixnum.rb +20 -0
  21. data/lib/stormforge.rb +39 -0
  22. data/lib/stormforge/client.rb +227 -0
  23. data/lib/stormforge/dsl.rb +4 -0
  24. data/lib/stormforge/dsl/test_case.rb +9 -0
  25. data/lib/stormforge/dsl/test_case/attribute_access.rb +19 -0
  26. data/lib/stormforge/dsl/test_case/cloud.rb +33 -0
  27. data/lib/stormforge/dsl/test_case/data_source.rb +49 -0
  28. data/lib/stormforge/dsl/test_case/data_source/file_fixture.rb +104 -0
  29. data/lib/stormforge/dsl/test_case/data_source/random_number.rb +64 -0
  30. data/lib/stormforge/dsl/test_case/data_source/random_string.rb +52 -0
  31. data/lib/stormforge/dsl/test_case/definition.rb +128 -0
  32. data/lib/stormforge/dsl/test_case/session.rb +77 -0
  33. data/lib/stormforge/registry.rb +23 -0
  34. data/lib/stormforge/version.rb +3 -0
  35. data/lib/thor/generators/init.rb +14 -0
  36. data/lib/thor/generators/templates/Stormfile +15 -0
  37. data/lib/thor/main.rb +79 -0
  38. data/lib/thor/storm_forge_base.rb +46 -0
  39. data/lib/thor/testcase.rb +23 -0
  40. data/lib/thor/testrun.rb +75 -0
  41. data/spec/client_spec.rb +4 -0
  42. data/spec/dsl/test_case/attribute_access_spec.rb +46 -0
  43. data/spec/dsl/test_case/cloud_spec.rb +15 -0
  44. data/spec/dsl/test_case/data_source/file_fixture_spec.rb +101 -0
  45. data/spec/dsl/test_case/data_source/random_number_spec.rb +51 -0
  46. data/spec/dsl/test_case/data_source/random_string_spec.rb +33 -0
  47. data/spec/dsl/test_case/data_source_spec.rb +12 -0
  48. data/spec/dsl/test_case/definition_spec.rb +107 -0
  49. data/spec/dsl/test_case/session_spec.rb +102 -0
  50. data/spec/dsl/test_case_integration_spec.rb +148 -0
  51. data/spec/dsl/test_case_spec.rb +8 -0
  52. data/spec/fixtures/ip_addresses.csv +1 -0
  53. data/spec/fixtures/slug.csv +1 -0
  54. data/spec/fixtures/users.csv +2 -0
  55. data/spec/registry_spec.rb +34 -0
  56. data/spec/spec_helper.rb +33 -0
  57. data/spec/stormforger_spec.rb +13 -0
  58. data/stormforge_ruby.gemspec +38 -0
  59. 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
+
@@ -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,4 @@
1
+ module StormForge
2
+ module Dsl
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ module StormForge
2
+ module Dsl
3
+ class TestCase
4
+ def self.define(*args, &block)
5
+ Definition.new(*args, &block)
6
+ end
7
+ end
8
+ end
9
+ 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
+