stormforge-ruby 0.5.0

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