web_tsunami 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a151d3928ef65c636d341b5c00c4aab1f51314f7bb809e845f1281782b172db
4
- data.tar.gz: '08597d6fcdb0157c1636033ebc92c2a0a0accd66d023ae6d609817f60d371756'
3
+ metadata.gz: feec552f2db4cf26a58dc9349313afd80bfd7da50f5143e3475f4c9cad497d6d
4
+ data.tar.gz: 54fafe2b562ba52ae68c12930ae8c33f80e3e6557b4e3d74cb5e3fa741a3ea9d
5
5
  SHA512:
6
- metadata.gz: 4f987ec8a711515c8bf5c8045b8fb074b8e4617af5c802e66cf0900b2e5df89eaeb286b78a529ef9508d7629ed365bbe73384bb722308a77dd26084d2ddce361
7
- data.tar.gz: e9e2e9d3c9c975b3f6f2b6ce3ea2657da4bd806819944b34ea7824621e85700637b340a2dab3388af74723d1391080e45da8b766488fbc9d1a41d0369bfcb521
6
+ metadata.gz: 6359c7e63dae34c480395d4f27f040b524fca85e1bb48422ab522ea8da899a39ec59613ff4ce3d7068b50f0c31e06b27f1087560d8506aa8fbf27a4df5e7b804
7
+ data.tar.gz: c967132a8f0ad8a9346683ed3dd84436e33d0a01275a73511aadd87a65bb45b84455aa89e54501e974b1d6d58b150b6dfb2597dd7db6f8a63df4dbafde2778bd
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # Changelog of Web Tsunami
2
2
 
3
- ## Unreleased
3
+ ## 0.3.0 (2024-02-01)
4
+
5
+ * Add session object to handle automatically cookies and CSRF tokens
6
+
7
+ ## 0.2.0 (2024-01-25)
8
+
9
+ * Add methods post, put, patch and delete
10
+ * Improve README with an advanced example
11
+
12
+ ## 0.1.0 (2024-01-19)
4
13
 
5
14
  * Fix non compatible changes of Typhoeus
6
15
 
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <img align="right" width="120px" src="./web_tsunami.png">
2
+
1
3
  # Web Tsunami
2
4
 
3
5
  Write tailor-made scenarios for load testing web apps
@@ -5,16 +7,31 @@ Write tailor-made scenarios for load testing web apps
5
7
  ## Why
6
8
 
7
9
  Many good tools already exist for a very long time such as ApacheBench and Siege.
8
- But, sometimes load testing a web app requires to write a custom scenario.
10
+ The goal is not to replace them.
11
+ But sometimes, load testing a web app requires to write a custom scenario.
12
+ My initial requirement was to send requests with unique parameters.
13
+ To the best of my knowledge, no tool could do this.
14
+
9
15
  The goal is to focus only on the scenario without thinking about forking, threads and non blocking IOs.
10
16
  Fortunately there is [Typhoeus](https://github.com/typhoeus/typhoeus) to send parallel HTTP requests.
11
17
 
18
+ ## How
19
+
12
20
  Web Tsunami is a tiny class that forks every seconds and sends as many requests as expected.
21
+ It provide the methods `get`, `post`, `put`, `patch` and `delete`.
22
+ They all accept the same arguments : `get(url, options = {}, &block)`.
23
+ The `options` is given to Typhoeus as is.
24
+ It can contain headers and the request body.
25
+ See [Typhoeus usage](https://github.com/typhoeus/typhoeus/#usage) for more details.
13
26
 
14
- ## Example
27
+ ## Examples
28
+
29
+ Let's start with a very trivial scenario and I will show you an advanced one after :
15
30
 
16
31
  ```ruby
17
- class Example < WebTsunami::Scenario
32
+ require "web_tsunami"
33
+
34
+ class SearchTsunami < WebTsunami::Scenario
18
35
  def run
19
36
  get("http://site.example") do
20
37
  # Block is executed once the response has been received
@@ -22,7 +39,7 @@ class Example < WebTsunami::Scenario
22
39
  get("http://site.example/search?query=stress+test") do |response|
23
40
  # Do whatever you need with the response object or ignore it
24
41
  sleep(10)
25
- get("http://site.example/search?query=stress+test&page=2") do
42
+ get("http://site.example/search?query=stress+test&page=#{rand(100)}") do
26
43
  sleep(5)
27
44
  get("http://site.example/stress/test")
28
45
  end
@@ -33,11 +50,40 @@ end
33
50
 
34
51
  # Simulates 100 concurrent visitors every second for 10 minutes
35
52
  # It's a total of 60K unique visitors for an average of 23'220 rpm.
36
- Example.start(concurrency: 100, duration: 60 * 10)
53
+ SearchTsunami.start(concurrency: 100, duration: 60 * 10)
54
+ ```
55
+
56
+ In this scenario, a visitor comes on the index page, then search for _stress test_, then go on a random page of the search result, and finally found the stress test page.
57
+ It introduces a unique parameters which is the page number.
58
+ It's nice, but it could have almost be done with Siege.
59
+ Let me show you a more realistic scenario.
60
+
61
+ ```ruby
62
+ require "web_tsunami"
63
+
64
+ class SessionTsunami < WebTsunami::Scenario
65
+ def run
66
+ # The session object stores cookies and automatically submit CSRF token with forms
67
+ session = WebTsunami::Session.new(self, "https://site.example")
68
+ session.get("/") do
69
+ session.get("/account/new") do
70
+ # An authenticity_token param is automatically added by the session
71
+ session.post("/account", body: {account: "#{rand(1000000)}@email.test", password: "password"}}) do |response|
72
+ # The session stores the Set-Cookie header and will provide it to the next requests
73
+ session.get("/dashboard") do # Redirection after registration
74
+ # And so on
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ SessionTsunami.start(concurrency: 100, duration: 60 * 10)
37
83
  ```
38
84
 
39
- In this example, the same requests are always sent.
40
- But you can provide dynamic query strings, use variables and some randomness.
85
+ This is more realistic because it handles CSRF tokens and cookies.
86
+ Thus the scenario can submit forms and behaves a little bit more like a real visitor.
41
87
 
42
88
  ## Output and result
43
89
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebTsunami
4
+ # Scenario is the class that handle all the parallel requests.
5
+ class Scenario
6
+ attr_reader :concurrency
7
+
8
+ def self.start(options)
9
+ options[:duration].times { fork { new(options[:concurrency]).start } and sleep(1) }
10
+ Process.wait
11
+ end
12
+
13
+ def initialize(concurrency)
14
+ @concurrency = concurrency
15
+ end
16
+
17
+ def requests
18
+ @requests ||= Typhoeus::Hydra.new
19
+ end
20
+
21
+ def get(url, options = {}, &block)
22
+ request(:get, url, options, &block)
23
+ end
24
+
25
+ def post(url, options = {}, &block)
26
+ request(:post, url, options, &block)
27
+ end
28
+
29
+ def put(url, options = {}, &block)
30
+ request(:put, url, options, &block)
31
+ end
32
+
33
+ def patch(url, options = {}, &block)
34
+ request(:patch, url, options, &block)
35
+ end
36
+
37
+ def delete(url, options = {}, &block)
38
+ request(:delete, url, options, &block)
39
+ end
40
+
41
+ def request(method, url, options, &block)
42
+ req = Typhoeus::Request.new(url, {method: method}.merge(options))
43
+ requests.queue(req)
44
+ req.on_complete do |response|
45
+ if response.timed_out?
46
+ puts "Timeout #{url}"
47
+ elsif response.code == 0
48
+ puts "#{response.return_message} #{response.request.options[:method]} #{url}"
49
+ elsif !response.success? && ![302, 303].include?(response.code)
50
+ puts "#{response.code} #{response.request.options[:method]} #{url}"
51
+ end
52
+ block.call(response) if block
53
+ end
54
+ end
55
+
56
+ def start
57
+ concurrency.times { run }
58
+ requests.run
59
+ end
60
+
61
+ def run
62
+ raise NotImplementedError
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebTsunami
4
+ # Session is a helper class to be used inside a scenario.
5
+ # It's purpose is to avoid low level manipulations to handle cookies and CSRF tokens automatically.
6
+ class Session
7
+ attr_reader :scenario, :root_url
8
+
9
+ attr_reader :cookies, :last_response
10
+
11
+ def initialize(scenario, root_url)
12
+ @scenario = scenario
13
+ @root_url = root_url
14
+ @cookies = {}
15
+ @last_response = nil
16
+ end
17
+
18
+ def get(path, options = {}, &block)
19
+ url = File.join(root_url, path)
20
+ inject_headers(default_headers, options)
21
+ scenario.get(url, options) do |response|
22
+ @last_response = response
23
+ save_cookies(response)
24
+ block&.call(response)
25
+ end
26
+ end
27
+
28
+ def post(path, options = {}, &block)
29
+ url = File.join(root_url, path)
30
+ inject_headers(default_post_headers, options)
31
+ inject_csrf_token(options)
32
+ scenario.post(url, options) do |response|
33
+ @last_response = response
34
+ save_cookies(response)
35
+ block&.call(response)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def default_headers
42
+ {
43
+ "Origin" => last_response && request_origin_header(last_response.request),
44
+ "Cookie" => cookies.map { |(k,v)| "#{k}=#{v}" }.join(" "),
45
+ }
46
+ end
47
+
48
+ def default_post_headers
49
+ default_headers.merge("Content-Type" => "application/x-www-form-urlencoded;charset=UTF-8")
50
+ end
51
+
52
+ def request_origin_header(request)
53
+ return "null" unless request
54
+ uri = URI(request.base_url.to_s)
55
+ if [80, 443].include?(uri.port)
56
+ "#{uri.scheme}://#{uri.host}"
57
+ else
58
+ "#{uri.scheme}://#{uri.host}:#{uri.port}"
59
+ end
60
+ end
61
+
62
+ CSRF_REGEX = /<meta name="csrf-token" content="([^"]+)"/
63
+
64
+ def extract_csrf_token(html)
65
+ html.match(CSRF_REGEX)[1]
66
+ end
67
+
68
+ def save_cookies(response)
69
+ return unless header = response.headers["Set-Cookie"]
70
+ Array(header).each do |cookie|
71
+ name, value = cookie.split(" ", 2)[0].split("=")
72
+ @cookies[name] = value
73
+ end
74
+ end
75
+
76
+ def inject_headers(headers, options)
77
+ options[:headers] = headers.merge(options[:headers] || {})
78
+ end
79
+
80
+ def inject_csrf_token(options)
81
+ if options[:body].is_a?(Hash) && last_response
82
+ options[:body] = {authenticity_token: extract_csrf_token(last_response.body)}.merge(options[:body])
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,3 +1,3 @@
1
1
  module WebTsunami
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/web_tsunami.rb CHANGED
@@ -1,52 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
- require 'typhoeus'
4
-
5
- module WebTsunami
6
- class Scenario
7
-
8
- attr_reader :concurrency
9
-
10
- def self.start(options)
11
- options[:duration].times { fork { new(options[:concurrency]).start } and sleep(1) }
12
- Process.wait
13
- end
14
-
15
- def initialize(concurrency)
16
- @sleeps = {}
17
- @concurrency = concurrency
18
- end
19
-
20
- def requests
21
- @requests ||= Typhoeus::Hydra.new
22
- end
23
-
24
- def get(url, &block)
25
- requests.queue(req = Typhoeus::Request.new(url, request_options))
26
- req.on_complete do |response|
27
- if response.timed_out?
28
- puts "Timeout #{url}"
29
- elsif response.code == 0
30
- puts "#{response.return_message} #{url}"
31
- elsif !response.success? && response.code != 302
32
- puts "#{response.code} #{url}"
33
- end
34
- block.call(response) if block
35
- end
36
- end
37
-
38
- def start
39
- concurrency.times { run }
40
- requests.run
41
- end
42
-
43
- def run
44
- raise NotImplementedError
45
- end
46
-
47
- def request_options
48
- {}
49
- end
50
-
51
- end
52
- end
3
+ require "typhoeus"
4
+ require "web_tsunami/scenario"
5
+ require "web_tsunami/session"
data/web_tsunami.png ADDED
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web_tsunami
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexis Bernard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-19 00:00:00.000000000 Z
11
+ date: 2024-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typhoeus
@@ -36,10 +36,12 @@ files:
36
36
  - Gemfile.lock
37
37
  - LICENSE.txt
38
38
  - README.md
39
- - example.rb
40
39
  - lib/web_tsunami.rb
40
+ - lib/web_tsunami/scenario.rb
41
+ - lib/web_tsunami/session.rb
41
42
  - lib/web_tsunami/version.rb
42
43
  - web_tsunami.gemspec
44
+ - web_tsunami.png
43
45
  homepage: https://github.com/BaseSecrete/web_tsunami
44
46
  licenses:
45
47
  - MIT
data/example.rb DELETED
@@ -1,28 +0,0 @@
1
- $LOAD_PATH << File.dirname(__FILE__)
2
-
3
- require 'web_tsunami'
4
-
5
- # Triggers the following requests concurently:
6
- # http://www.google.com
7
- # http://www.google.com/search?q=ruby
8
- # http://www.google.com/search?q=ruby&start=10
9
-
10
- class GoogleTsunami < WebTsunami::Scenario
11
- def run
12
- get('http://www.google.com') do
13
- puts 'http://www.google.com'
14
- get('http://www.google.com/search?q=ruby') do
15
- puts 'http://www.google.com/search?q=ruby'
16
- get('http://www.google.com/search?q=ruby&start=10') do
17
- puts 'http://www.google.com/search?q=ruby&start=10'
18
- end
19
- end
20
- end
21
- end
22
- end
23
-
24
- # Set concurrency and duration in seconds and start your script.
25
- # These numbers are voluntary low because I don't want any trouble with Google.
26
- # But don't hesitate to set a higher concurrency and a duration of almost 5 minutes
27
- # in order to get a reliable benchmark.
28
- GoogleTsunami.start(concurrency: 2, duration: 10)