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,46 @@
1
+ class StormForge::Thor::Base < Thor
2
+ private
3
+
4
+ def client
5
+ error "WARNING! Disabling SSL certificate verification is not recommended. Proceed with caution!" if options[:skip_ssl_verify]
6
+
7
+ @client ||= StormForge::Client.new({
8
+ user: authentication["email"],
9
+ token: authentication["authentication_token"],
10
+ dev_mode: dev_mode?,
11
+ skip_ssl_verify: options[:skip_ssl_verify]
12
+ })
13
+ end
14
+
15
+ def require_stormfile!
16
+ stormfile = File.expand_path(options[:stormfile])
17
+ raise Thor::Error, "Stormfile at '#{options[:stormfile]}' not found!" unless File.exists? stormfile
18
+ load options[:stormfile]
19
+ end
20
+
21
+ def handle_authentication_error(exception)
22
+ raise Thor::Error, "Authentication error, check your credentials or run 'stormforge auth' again!"
23
+ end
24
+
25
+ def authentication
26
+ return @authentication if @authentication
27
+
28
+ raise Thor::Error, "No authentication file found" unless File.exists? authfile
29
+
30
+ auth = JSON.parse(File.read(authfile))
31
+
32
+ raise Thor::Error, "Authentication file invalid. Use 'stormforger auth' to generate a new one." unless auth["email"].present? && auth["authentication_token"].present?
33
+
34
+ @authentication = auth
35
+ end
36
+
37
+ def authfile
38
+ path = File.expand_path(options[:authfile])
39
+ path += ".dev" if dev_mode?
40
+ path
41
+ end
42
+
43
+ def dev_mode?
44
+ !!options[:development]
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ class StormForge::Thor::Testcase < StormForge::Thor::Base
2
+ desc "update NAME", "Update test case NAME"
3
+ def update(name)
4
+ require_stormfile!
5
+
6
+ test_case = StormForge.test_case(name.to_sym)
7
+
8
+ raise Thor::Error, "Test case '#{name}' unknown!" unless test_case
9
+
10
+ client.create_or_update(test_case)
11
+ rescue Faraday::Error::ConnectionFailed
12
+ raise Thor::Error, "StormForger API not available :-/ (connection failed)"
13
+ end
14
+
15
+ desc "list", "List all defined test cases"
16
+ def list
17
+ require_stormfile!
18
+
19
+ StormForge.registry.each_pair do |name, test_case|
20
+ puts name
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,75 @@
1
+ class StormForge::Thor::Testrun < StormForge::Thor::Base
2
+ desc "list [TEST_CASE]", "List test runs"
3
+ def list(test_case_slug=nil)
4
+ require_stormfile!
5
+
6
+ test_runs = client.list_test_runs(test_case_slug)["test_runs"]
7
+ test_case = test_case_slug.present? ? "test case '#{test_case_slug}'" : "all test cases"
8
+ puts "Test Run ID & Description for #{test_case}"
9
+ test_runs.each do |test_run|
10
+ puts "#{test_run["id"].to_s} (#{test_run["state"]}): #{test_run["description"] || '-'}"
11
+ end
12
+ rescue Faraday::Error::ConnectionFailed
13
+ raise Thor::Error, "StormForger API not available :-/ (connection failed)"
14
+ rescue Faraday::Error::ResourceNotFound
15
+ raise Thor::Error, "Test case '#{test_case_slug}' not found!"
16
+ rescue Faraday::Error::ClientError => e
17
+ if e.response[:status] == 401
18
+ handle_authentication_error(e)
19
+ else
20
+ raise e
21
+ end
22
+ end
23
+
24
+ desc "create TEST_CASE [DESCRIPTION]", "Create a test run"
25
+ def create(test_case_slug, description="")
26
+ require_stormfile!
27
+
28
+ p client.create_test_run(test_case_slug, description)
29
+ rescue Faraday::Error::ConnectionFailed
30
+ raise Thor::Error, "StormForger API not available :-/ (connection failed)"
31
+ rescue Faraday::Error::ResourceNotFound
32
+ raise Thor::Error, "Test Run could not be created, test case '#{test_case_slug}' not found!"
33
+ end
34
+
35
+ desc "show TEST_RUN_ID", "Show a test run"
36
+ def show(test_run_id)
37
+ require_stormfile!
38
+
39
+ test_run = client.fetch_test_run(test_run_id)
40
+ pp test_run
41
+ rescue Faraday::Error::ConnectionFailed
42
+ raise Thor::Error, "StormForger API not available :-/ (connection failed)"
43
+ rescue Faraday::Error::ResourceNotFound
44
+ raise Thor::Error, "Test Run #{test_run_id} not found!"
45
+ end
46
+
47
+ desc "stats TEST_RUN_ID", "Get stats (WIP)"
48
+ method_option :stream, type: :boolean, default: true, desc: "WIP: Stream"
49
+ method_option :limit, type: :numeric, default: 10, desc: "Limit of stats per batch"
50
+ def stats(test_run_id, since=0)
51
+ require_stormfile!
52
+
53
+ client.test_run_log(test_run_id, since, options[:limit], options[:stream]) do |stats|
54
+ puts stats.to_json
55
+ end
56
+ end
57
+
58
+ desc "abort TEST_RUN_ID", "Abort a test run"
59
+ def abort(test_run_id)
60
+ require_stormfile!
61
+
62
+ test_run = client.abort_test_run(test_run_id)
63
+ puts test_run.inspect
64
+ rescue Faraday::Error::ConnectionFailed
65
+ raise Thor::Error, "StormForger API not available :-/ (connection failed)"
66
+ rescue Faraday::Error::ResourceNotFound
67
+ raise Thor::Error, "Test Run #{test_run_id} not found"
68
+ rescue Faraday::Error::ClientError => e
69
+ if e.response[:status] == 422
70
+ raise Thor::Error, "Test Run #{test_run_id} cannot be aborted!"
71
+ else
72
+ raise e
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ require "spec_helper"
2
+
3
+ describe StormForge::Client do
4
+ end
@@ -0,0 +1,46 @@
1
+ require "spec_helper"
2
+
3
+ class Foo
4
+ include StormForge::Dsl::TestCase::AttributeAccess
5
+
6
+ def initialize
7
+ @attributes = {}
8
+ end
9
+
10
+ def available_attributes
11
+ [:title]
12
+ end
13
+ end
14
+
15
+ describe StormForge::Dsl::TestCase::AttributeAccess do
16
+ it "should tell if it responds to a given attribute" do
17
+ foo = Foo.new
18
+ foo.respond_to?(:title).should == true
19
+ end
20
+
21
+ it "should be able to set the configured attribute's values (DSL style)" do
22
+ foo = Foo.new
23
+ foo.title "new title"
24
+ foo.title.should == "new title"
25
+ end
26
+
27
+ it "should be able to set the configured attribute's values via setter" do
28
+ foo = Foo.new
29
+ foo.title = "new title"
30
+ foo.title.should == "new title"
31
+ end
32
+
33
+ it "should delegate to super if no specified attribute matches" do
34
+ foo = Foo.new
35
+ expect do
36
+ foo.something
37
+ end.to raise_error(NoMethodError)
38
+ end
39
+
40
+ it "should not allow more than one argument to generated attribute accessors" do
41
+ foo = Foo.new
42
+ expect do
43
+ foo.title "new title", "meh?"
44
+ end.to raise_error(ArgumentError)
45
+ end
46
+ end
@@ -0,0 +1,15 @@
1
+ require "spec_helper"
2
+
3
+ describe StormForge::Dsl::TestCase::Cloud do
4
+ describe "Validations" do
5
+ subject { StormForge::Dsl::TestCase::Cloud.new :aws, Proc.new {} }
6
+ it { should ensure_inclusion_of(:provider).in_array(StormForge::Dsl::TestCase::Cloud.supported_cloud_providers) }
7
+ end
8
+
9
+ it "should have security groups" do
10
+ cloud = StormForge::Dsl::TestCase::Cloud.new :aws do
11
+ security_groups "sg-12345", "sg-abcdef"
12
+ end
13
+ cloud.security_groups.should include("sg-12345", "sg-abcdef")
14
+ end
15
+ end
@@ -0,0 +1,101 @@
1
+ require "spec_helper"
2
+
3
+ describe StormForge::Dsl::TestCase::DataSource::FileFixture do
4
+ subject { StormForge::Dsl::TestCase::DataSource::FileFixture }
5
+
6
+ describe "Validations" do
7
+ it "should require a name" do
8
+ users = subject.new(nil, {})
9
+ users.valid?
10
+ users.errors[:name].should be_present
11
+ end
12
+
13
+ it "should default order to :sequentially" do
14
+ users = subject.new("a name", {})
15
+ users.order.should == :sequentially
16
+ end
17
+
18
+ it "should require an existing source" do
19
+ users = subject.new("users", source: "NOT-EXISTING")
20
+ users.valid?
21
+ users.errors[:source].should be_present
22
+ end
23
+
24
+ it "should actually check for an existing source" do
25
+ users = subject.new("users", source: File.join(FIXTURE_PATH, "users.csv"))
26
+ users.valid?
27
+ users.errors[:source].should be_blank
28
+ end
29
+ end
30
+
31
+ describe "#delimiter" do
32
+ it "should have a delimiter" do
33
+ subject.new("a name", delimiter: "!").delimiter.should == "!"
34
+ end
35
+
36
+ it "should have a default delimiter" do
37
+ subject.new("a name", {}).delimiter.should == ";"
38
+ end
39
+ end
40
+
41
+ # TODO this spec feels messy, refactor it!
42
+ it "should calculate a md5_hash from the source file" do
43
+ file = "/foo/bar/fixture.csv"
44
+ File.should_receive(:exist?).with(file).and_return(true)
45
+
46
+ file_fixture = subject.new("blub", source: file)
47
+
48
+ md5_file_double = double()
49
+ md5_file_double.should_receive(:hexdigest).and_return("md5")
50
+ ::Digest::MD5.should_receive(:file).with(file).and_return(md5_file_double)
51
+
52
+ file_fixture.md5_hash.should == "md5"
53
+ end
54
+
55
+ # TODO this spec feels messy, refactor it!
56
+ it "#as_json" do
57
+ file = "/foo/bar/fixture.csv"
58
+ file_fixture = subject.new("blub", source: file)
59
+ file_fixture.should_receive(:md5_hash).and_return("md5_hash")
60
+
61
+ json = file_fixture.as_json
62
+
63
+ json[:source] = File.basename(file)
64
+ json[:md5_hash] = "md5"
65
+ end
66
+
67
+ describe "#next" do
68
+ context "no fields given" do
69
+ it "should return a marker and start with a sequence number of 1" do
70
+ subject.new("requests", {}).next.should == "${FILEFIXTURE|n=requests-seq=1}"
71
+ end
72
+
73
+ it "should increment the sequence on each call to #next" do
74
+ users = subject.new("users", {})
75
+ users.next.should == "${FILEFIXTURE|n=users-seq=1}"
76
+ users.next.should == "${FILEFIXTURE|n=users-seq=2}"
77
+ end
78
+ end
79
+
80
+ context "fields given" do
81
+ it "should increment the sequence on each call to #next" do
82
+ users = subject.new("users", fields: [:id, :email, :password])
83
+ users.next.email.should == "${FILEFIXTURE|f=email-n=users-seq=1}"
84
+ users.next.id.should == "${FILEFIXTURE|f=id-n=users-seq=2}"
85
+ end
86
+ end
87
+
88
+ it "should raise NoMethodError on proxy when invalid field is given" do
89
+ users = subject.new("users", fields: [:id, :email, :password])
90
+ expect { users.next.i_do_not_exist}.to raise_error(NoMethodError)
91
+ end
92
+
93
+ it "should be a good ruby citizen and implement respond_to_missing" do
94
+ users = subject.new("users", fields: [:id, :email, :password])
95
+ # users.next.send(:respond_to_missing?, :foo).should == false
96
+ users.next.send(:respond_to_missing?, :id).should == true
97
+ end
98
+ end
99
+
100
+ end
101
+
@@ -0,0 +1,51 @@
1
+ require "spec_helper"
2
+
3
+ describe StormForge::Dsl::TestCase::DataSource::RandomNumber do
4
+ subject { StormForge::Dsl::TestCase::DataSource::RandomNumber }
5
+
6
+ it "should return a marker and start with a sequence number of 1" do
7
+ subject.new("number", range: 1..10).next.should == "${GENERATOR|t=random_number-n=number-seq=1}"
8
+ end
9
+
10
+ it "should increment the sequence on each call to #next" do
11
+ foo = subject.new("another_number", range: 23..42)
12
+ foo.next.should == "${GENERATOR|t=random_number-n=another_number-seq=1}"
13
+ foo.next.should == "${GENERATOR|t=random_number-n=another_number-seq=2}"
14
+ end
15
+
16
+ it "should know how often #next was called" do
17
+ ref_token = subject.new("ref_token", range: 10..42)
18
+ 2.times { ref_token.next }
19
+ ref_token.count.should == 2
20
+ end
21
+
22
+ it "should work with an array for range too" do
23
+ subject.new("number", range: [23, 42]).range.should == (23..42)
24
+ end
25
+
26
+ describe "Invalid range options" do
27
+ it "should not accept missing range" do
28
+ expect {
29
+ subject.new("numer_without_range", range: nil)
30
+ }.to raise_error(StormForge::Dsl::TestCase::DataSource::RandomNumber::InvalidRange)
31
+ end
32
+
33
+ it "should not accept non-numerical range" do
34
+ expect {
35
+ subject.new("numer_with_invalid", range: "a".."z")
36
+ }.to raise_error(StormForge::Dsl::TestCase::DataSource::RandomNumber::InvalidRange)
37
+ end
38
+
39
+ it "should not accept range beginning below 0" do
40
+ expect {
41
+ subject.new("numer_with_invalid_range", range: -1..10)
42
+ }.to raise_error(StormForge::Dsl::TestCase::DataSource::RandomNumber::InvalidRange)
43
+ end
44
+
45
+ it "should not accept range beginning over 100_000_000" do
46
+ expect {
47
+ subject.new("numer_with_invalid_range", range: 1..100_000_001)
48
+ }.to raise_error(StormForge::Dsl::TestCase::DataSource::RandomNumber::InvalidRange)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+
3
+ describe StormForge::Dsl::TestCase::DataSource::RandomString do
4
+ subject { StormForge::Dsl::TestCase::DataSource::RandomString }
5
+
6
+ it "should return a marker and start with a sequence number of 1" do
7
+ subject.new("ref_token", length: 10).next.should == "${GENERATOR|t=random_string-n=ref_token-seq=1}"
8
+ end
9
+
10
+ it "should increment the sequence for every invocation of #next" do
11
+ ref_token = subject.new("ref_token", length: 10)
12
+ ref_token.next.should == "${GENERATOR|t=random_string-n=ref_token-seq=1}"
13
+ ref_token.next.should == "${GENERATOR|t=random_string-n=ref_token-seq=2}"
14
+ end
15
+
16
+ it "should know how often #next was called" do
17
+ ref_token = subject.new("ref_token", length: 10)
18
+ 2.times { ref_token.next }
19
+ ref_token.count.should == 2
20
+ end
21
+
22
+ it "should default to length of 1" do
23
+ subject.new("token_length_1", {}).length.should == 1
24
+ end
25
+
26
+ it "should limit the length to 100" do
27
+ subject.new("long_token", length: 101).length.should == 100
28
+ end
29
+
30
+ it "should ensure that length is a number" do
31
+ subject.new("ref_token", length: "meh").length.should == 1
32
+ end
33
+ end
@@ -0,0 +1,12 @@
1
+ require "spec_helper"
2
+
3
+ describe StormForge::Dsl::TestCase::DataSource do
4
+ describe "Usage" do
5
+ subject { StormForge::Dsl::TestCase::DataSource }
6
+
7
+ it "spec_name" do
8
+ subject.new Proc.new {}
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,107 @@
1
+ require "spec_helper"
2
+
3
+ describe StormForge::Dsl::TestCase::Definition do
4
+
5
+ describe "Validations" do
6
+
7
+ subject { StormForge::Dsl::TestCase::Definition.new :slug, Proc.new {} }
8
+ before(:each) { subject.valid? }
9
+
10
+ it "requires a title" do
11
+ subject.errors[:title].should be_present
12
+ end
13
+
14
+ it "requires a version" do
15
+ subject.errors[:version].should be_present
16
+ end
17
+
18
+ it "requires at least one target" do
19
+ subject.errors[:targets].should be_present
20
+ test_case = StormForge::Dsl::TestCase::Definition.new :slug do
21
+ targets "target1"
22
+ end
23
+ test_case.valid?
24
+ test_case.errors[:targets].should be_empty
25
+ end
26
+
27
+ it "requires at least one arrival phase" do
28
+ subject.errors[:arrival_phases].should be_present
29
+ end
30
+
31
+ it "requires at least one session" do
32
+ subject.errors[:sessions].should be_present
33
+ end
34
+
35
+ it "requires to have a total session probability of 100%" do
36
+ test_case = StormForge::Dsl::TestCase::Definition.new :slug do
37
+ session "a", 10, Proc.new {}
38
+ end
39
+ test_case.valid?
40
+ test_case.errors[:sessions].should be_present
41
+
42
+ test_case = StormForge::Dsl::TestCase::Definition.new :slug do
43
+ session "a", 100, Proc.new {}
44
+ end
45
+ test_case.valid?
46
+ test_case.errors[:sessions].should be_empty
47
+ end
48
+
49
+ end
50
+
51
+ it "should have a description" do
52
+ test_case = StormForge::Dsl::TestCase::Definition.new :slug do
53
+ description "test case description"
54
+ end
55
+
56
+ test_case.description.should == "test case description"
57
+ end
58
+
59
+ it "should have cloud configuration" do
60
+ expect {
61
+ StormForge::Dsl::TestCase::Definition.new :slug do
62
+ self.respond_to?(:cloud).should == true
63
+ cloud :provider, Proc.new {}
64
+ end
65
+ }.not_to raise_error
66
+ end
67
+
68
+ it "should serialize to JSON" do
69
+ test_case = StormForge::Dsl::TestCase::Definition.new :my_1st_case_title do
70
+ title "my 1st case title"
71
+ version "version"
72
+ targets "a1"
73
+ arrival_phase duration: 1.hour, rate: 100.per_second
74
+ session "GET /", 100.percent, Proc.new {}
75
+ end
76
+
77
+ # do a sanity check, to verify that the test_case is valid
78
+ test_case.valid?
79
+ test_case.errors.should be_empty
80
+
81
+ test_case.to_json(root: false).should match_json_expression({
82
+ dsl_version: :v1,
83
+ slug: "my_1st_case_title",
84
+ title: "my 1st case title",
85
+ version: "version",
86
+ targets: ["a1"],
87
+ arrival_phases: [
88
+ {
89
+ warmup: false,
90
+ duration: 3600,
91
+ duration_unit: :second,
92
+ rate: 100.0,
93
+ rate_unit: :second
94
+ }
95
+ ].ordered!,
96
+ data_sources: nil,
97
+ sessions: {
98
+ "GET /" => {
99
+ name: "GET /",
100
+ probability: 100,
101
+ steps: [],
102
+ data_sources_usage: {}
103
+ }
104
+ }
105
+ })
106
+ end
107
+ end