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,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
+