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,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,3 @@
1
+ module StormForge
2
+ VERSION = "0.5.0"
3
+ 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
@@ -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
+