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