mirage 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +50 -0
- data/README.md +93 -0
- data/bin/mirage +54 -0
- data/features/checking_for_requests.feature +74 -0
- data/features/clearing_requests_and_responses.feature +81 -0
- data/features/client/checking_for_requests.feature +20 -0
- data/features/client/clearing_responses.feature +75 -0
- data/features/client/getting_responses.feature +30 -0
- data/features/client/mirage_client.feature +36 -0
- data/features/client/peeking.feature +32 -0
- data/features/client/setting_responses.feature +91 -0
- data/features/client/snapshotting.feature +34 -0
- data/features/command_line_iterface.feature +39 -0
- data/features/default_responses.feature +91 -0
- data/features/file_hosting.feature +8 -0
- data/features/logging.feature +7 -0
- data/features/peeking_at_response.feature +24 -0
- data/features/resources/test.zip +0 -0
- data/features/response_templates.feature +45 -0
- data/features/root_responses.feature +47 -0
- data/features/setting_responses.feature +40 -0
- data/features/setting_responses_with_a_delay.feature +10 -0
- data/features/setting_responses_with_pattern_matching.feature +72 -0
- data/features/snapshotting.feature +25 -0
- data/features/step_definitions/my_steps.rb +127 -0
- data/features/support/env.rb +89 -0
- data/features/web_user_interface.feature +39 -0
- data/full_build.sh +100 -0
- data/lib/config.ru +5 -0
- data/lib/mirage.rb +14 -0
- data/lib/mirage/client.rb +140 -0
- data/lib/mirage/core.rb +206 -0
- data/lib/mirage/util.rb +40 -0
- data/lib/mirage/web.rb +65 -0
- data/lib/start_mirage.rb +15 -0
- data/lib/view/mirage/index.xhtml +23 -0
- data/mirage.gemspec +40 -0
- data/rakefile +50 -0
- metadata +199 -0
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm --create default@mirage
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
mirage (0.1.1)
|
5
|
+
mechanize (>= 1.0.0)
|
6
|
+
rack (~> 1.1.0)
|
7
|
+
ramaze (>= 2011.01.30)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
builder (3.0.0)
|
13
|
+
cucumber (0.10.2)
|
14
|
+
builder (>= 2.1.2)
|
15
|
+
diff-lcs (>= 1.1.2)
|
16
|
+
gherkin (>= 2.3.5)
|
17
|
+
json (>= 1.4.6)
|
18
|
+
term-ansicolor (>= 1.0.5)
|
19
|
+
diff-lcs (1.1.2)
|
20
|
+
gherkin (2.3.5)
|
21
|
+
json (>= 1.4.6)
|
22
|
+
innate (2011.01)
|
23
|
+
rack (>= 1.1.0)
|
24
|
+
json (1.5.1)
|
25
|
+
mechanize (1.0.0)
|
26
|
+
nokogiri (>= 1.2.1)
|
27
|
+
nokogiri (1.4.4)
|
28
|
+
rack (1.1.2)
|
29
|
+
rake (0.8.7)
|
30
|
+
ramaze (2011.01.30)
|
31
|
+
innate (>= 2010.03)
|
32
|
+
rspec (2.5.0)
|
33
|
+
rspec-core (~> 2.5.0)
|
34
|
+
rspec-expectations (~> 2.5.0)
|
35
|
+
rspec-mocks (~> 2.5.0)
|
36
|
+
rspec-core (2.5.1)
|
37
|
+
rspec-expectations (2.5.0)
|
38
|
+
diff-lcs (~> 1.1.2)
|
39
|
+
rspec-mocks (2.5.0)
|
40
|
+
term-ansicolor (1.0.5)
|
41
|
+
|
42
|
+
PLATFORMS
|
43
|
+
ruby
|
44
|
+
|
45
|
+
DEPENDENCIES
|
46
|
+
bundler
|
47
|
+
cucumber
|
48
|
+
mirage!
|
49
|
+
rake
|
50
|
+
rspec
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
Mirage
|
2
|
+
======
|
3
|
+
Mirage is an application for hosting responses to fool your applications into thinking that they are talking to real endpoints whilst you are developing them.
|
4
|
+
Its accessible via HTTP has a RESful interface so is easy to interact with.
|
5
|
+
|
6
|
+
Below are a few instructions telling you how to use Mirage. They are definately enough to get you up and running, but if you want to know everything you
|
7
|
+
can do then have a look at the feature files for a complete low down. I know I know, it sounds a bit corney to say that a project's documentation is its tests. But they are written using
|
8
|
+
cucumber and a lot of effort has been put in to try and make these things readable... promise!
|
9
|
+
|
10
|
+
Any ideas/improvements or feedback you have are greatly appreciated.
|
11
|
+
|
12
|
+
I hope that its useful.
|
13
|
+
|
14
|
+
Leon
|
15
|
+
|
16
|
+
P.s. Currently Mirage only runs on Linux; Rubies 1.8, 1.9 and JRuby. I plan to add support for Windows and MacOsX very soon.
|
17
|
+
|
18
|
+
Installation & Running
|
19
|
+
----------------------
|
20
|
+
|
21
|
+
`gem install mirage`
|
22
|
+
`mirage start`
|
23
|
+
|
24
|
+
That's it, its running, your done... No seriously, go to http://localhost:7001/mirage and you see that its running.
|
25
|
+
|
26
|
+
There are also a [few options](https://github.com/Ladtech/sandbox/blob/master/mirage/features/setting_responses_with_a_delay.feature) that let you configure things like what port mirage is started on.
|
27
|
+
|
28
|
+
Usage:
|
29
|
+
------
|
30
|
+
The examples below assume that you are running mirage with the default settings.
|
31
|
+
|
32
|
+
###Set
|
33
|
+
Example:
|
34
|
+
http://localhost:7001/mirage/set/my_endpoint?response=hello
|
35
|
+
|
36
|
+
By hitting this url, you have just put a response on mirage. Your endpoint, the bit after 'http://localhost:7001/mirage/set', can be anything you like for example 'anything/you/like'. In return for
|
37
|
+
doing this Mirage will return you a response id. You can use this id to various things with this response.
|
38
|
+
|
39
|
+
###Get
|
40
|
+
How do I get my response back back?
|
41
|
+
|
42
|
+
Example:
|
43
|
+
http://localhost:7001/mirage/get/my_endpoint
|
44
|
+
|
45
|
+
All you need to do is 'get/the/endpoint' and Mirage will serve which ever response has been set.
|
46
|
+
|
47
|
+
When it comes to getting and setting responses, Mirage lets you do quite a lot:
|
48
|
+
|
49
|
+
* [Setting responses](https://github.com/lashd/Mirage/blob/master/features/setting_responses.feature) - The basics
|
50
|
+
|
51
|
+
* [Associate responses on an endpoint with a pattern](https://github.com/lashd/Mirage/blob/master/features/setting_responses_with_pattern_matching.feature) - This lets you simulate different behaviour depending on the request that is sent to Mirage
|
52
|
+
|
53
|
+
* [Introduce a delay](https://github.com/Ladtech/sandbox/blob/master/mirage/features/setting_responses_with_a_delay.feature) - Make things a bit more realistic and make your application wait.
|
54
|
+
|
55
|
+
* [Templatise responses](https://github.com/lashd/Mirage/blob/master/features/response_templates.feature) - Substitute values found in a request back in to the response.
|
56
|
+
|
57
|
+
* [File hosting](https://github.com/lashd/Mirage/blob/master/features/file_hosting.feature) - As well as text based responses, Mirage can also host files
|
58
|
+
|
59
|
+
* [Root responses](https://github.com/lashd/Mirage/blob/master/features/root_responses.feature) - Want default a response for all urls under an endpoint? this is how.
|
60
|
+
|
61
|
+
* [Default responses](https://github.com/lashd/Mirage/blob/master/features/default_responses.feature) - Prime on or after startup with a bunch of default responses
|
62
|
+
|
63
|
+
|
64
|
+
###Check
|
65
|
+
If you want to see what data was sent when a response is triggered you can use the response's unique id to get that data back. This can be useful as it lets you test that your application is sending the right data.
|
66
|
+
Example:
|
67
|
+
http://localhost:7001/mirage/check/response_id
|
68
|
+
|
69
|
+
###Peek
|
70
|
+
If you want to see a what a response is set as then you can [peek](https://github.com/lashd/Mirage/blob/master/features/peeking_at_response.feature) at it using the unique id you got when setting the response.
|
71
|
+
Peeking at response will return allow you to see what a response is set to without causing a request to be tracked.
|
72
|
+
|
73
|
+
Example:
|
74
|
+
http://localhost:7001/mirage/peek/your_response_id
|
75
|
+
###Snapshot and Rollback
|
76
|
+
Once you have set up Mirage just as you want it, you can snapshot its state. This lets you roll it back to that state when ever you want to.
|
77
|
+
|
78
|
+
Example:
|
79
|
+
http://localhost:7001/mirage/snapshot
|
80
|
+
http://localhost:7001/mirage/rollback
|
81
|
+
|
82
|
+
###Ruby Client
|
83
|
+
You can use whatever you like interact with Mirage but if you are using Ruby and you and have other things to do, then you can use
|
84
|
+
the client that you get when you install Mirage.
|
85
|
+
|
86
|
+
Example:
|
87
|
+
require 'rubygems
|
88
|
+
require 'mirage'
|
89
|
+
Mirage::Client.new.set('greeting', :response=>'hello')`
|
90
|
+
|
91
|
+
Your response is now waiting for you at: http://localhost:7001/mirage/get/greeting :)
|
92
|
+
The client has methods that let allow you to fully interact with mirage. [So go ahead and check it out](https://github.com/lashd/Mirage/tree/master/features/client).
|
93
|
+
|
data/bin/mirage
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/../lib")
|
4
|
+
require 'mirage'
|
5
|
+
include Mirage::Util
|
6
|
+
RUBY_CMD = RUBY_PLATFORM == 'JAVA' ? 'jruby' : 'ruby'
|
7
|
+
|
8
|
+
|
9
|
+
def start_mirage(args, mirage)
|
10
|
+
puts "Starting Mirage"
|
11
|
+
system "(#{RUBY_CMD} #{File.dirname(__FILE__)}/../lib/start_mirage.rb #{args.join(' ')}) > /dev/null 2>&1 &"
|
12
|
+
wait_until do
|
13
|
+
mirage.running?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def mirage_process_ids
|
18
|
+
mirage_cmdline_files = Dir['/proc/*/cmdline'].find_all { |cmdline_file| File.read(cmdline_file) =~ /Mirage Server|start_mirage/ }
|
19
|
+
mirage_cmdline_files.collect{|mirage_cmdline_file| File.read("#{File.dirname(mirage_cmdline_file)}/stat").split(' ').first.to_i }
|
20
|
+
end
|
21
|
+
|
22
|
+
if ARGV.include?('start')
|
23
|
+
|
24
|
+
options = parse_options(ARGV)
|
25
|
+
mirage_client = Mirage::Client.new "http://localhost:#{options[:port]}/mirage"
|
26
|
+
|
27
|
+
if mirage_client.running?
|
28
|
+
puts "Mirage already running"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
|
32
|
+
start_mirage(ARGV, mirage_client)
|
33
|
+
begin
|
34
|
+
mirage_client.load_defaults
|
35
|
+
rescue Mirage::InternalServerException => e
|
36
|
+
puts "WARN: #{e.message}"
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
elsif ARGV.include?('stop')
|
41
|
+
puts "Stoping Mirage"
|
42
|
+
begin
|
43
|
+
mirage_process_ids.each do |id|
|
44
|
+
puts "killing #{id}"
|
45
|
+
Process.kill(9, id)
|
46
|
+
end
|
47
|
+
|
48
|
+
rescue
|
49
|
+
puts 'Mirage is not running'
|
50
|
+
end
|
51
|
+
else
|
52
|
+
parse_options ['--help']
|
53
|
+
exit 1
|
54
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
Feature: After a response has been served from Mirage, the content of the request that triggered it can be retrieved. This is useful
|
2
|
+
for testing that the correct information was sent to the endpint that the MockServer is stubbing,
|
3
|
+
|
4
|
+
On setting a response, a unique id is returned which can be used to look up the last request made to get that response.
|
5
|
+
|
6
|
+
If the the response is reset then the same id is returned in order to make it easier to keep track of.
|
7
|
+
|
8
|
+
Responses hosted on the same endpoint but with a pattern are considered unique and so get their own ID.
|
9
|
+
|
10
|
+
If the request body contains content this is stored. Otherwise it is the query string that is stored.
|
11
|
+
|
12
|
+
If a response is 'peeked' this does not count as a request that should be stored.
|
13
|
+
|
14
|
+
Background: There is a response already on Mirage
|
15
|
+
Given I hit 'http://localhost:7001/mirage/set/greeting' with parameters:
|
16
|
+
| response | Hello |
|
17
|
+
|
18
|
+
|
19
|
+
Scenario: Querying a response that was triggered by a request that had content in the body
|
20
|
+
Given I hit 'http://localhost:7001/mirage/get/greeting' with request body:
|
21
|
+
"""
|
22
|
+
Hello MockServer
|
23
|
+
"""
|
24
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
25
|
+
Then 'Hello MockServer' should be returned
|
26
|
+
|
27
|
+
|
28
|
+
Scenario: Querying a response that was triggered by a request with a query string
|
29
|
+
Given I hit 'http://localhost:7001/mirage/get/greeting' with parameters:
|
30
|
+
| surname | Davis |
|
31
|
+
| firstname | Leon |
|
32
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
33
|
+
Then 'surname=Davis&firstname=Leon' should be returned
|
34
|
+
|
35
|
+
|
36
|
+
Scenario: Querying a response that has not been served yet
|
37
|
+
Given I hit 'http://localhost:7001/mirage/check/1'
|
38
|
+
Then a 404 should be returned
|
39
|
+
|
40
|
+
|
41
|
+
Scenario: A response is peeked at
|
42
|
+
Given I hit 'http://localhost:7001/mirage/get/greeting' with request body:
|
43
|
+
"""
|
44
|
+
Hello
|
45
|
+
"""
|
46
|
+
And I hit 'http://localhost:7001/mirage/peek/1'
|
47
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
48
|
+
Then 'Hello' should be returned
|
49
|
+
|
50
|
+
|
51
|
+
Scenario: A default response and one for the same endpoint with a pattern are set
|
52
|
+
When I hit 'http://localhost:7001/mirage/set/greeting' with parameters:
|
53
|
+
| response | Hello who ever you are |
|
54
|
+
Then '1' should be returned
|
55
|
+
|
56
|
+
When I hit 'http://localhost:7001/mirage/set/greeting' with parameters:
|
57
|
+
| response | Hello Leon |
|
58
|
+
| pattern | Leon |
|
59
|
+
Then '3' should be returned
|
60
|
+
|
61
|
+
When I hit 'http://localhost:7001/mirage/get/greeting' with request body:
|
62
|
+
"""
|
63
|
+
My name is Joel
|
64
|
+
"""
|
65
|
+
And I hit 'http://localhost:7001/mirage/get/greeting' with request body:
|
66
|
+
"""
|
67
|
+
My name is Leon
|
68
|
+
"""
|
69
|
+
And I hit 'http://localhost:7001/mirage/check/1'
|
70
|
+
Then 'My name is Joel' should be returned
|
71
|
+
And I hit 'http://localhost:7001/mirage/check/3'
|
72
|
+
Then 'My name is Leon' should be returned
|
73
|
+
|
74
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
Feature: Responses and requests can be cleared.
|
2
|
+
Clearing a response clears any associated request information that may have been stored.
|
3
|
+
|
4
|
+
Usage:
|
5
|
+
${mirage_url}/clear - Clear all responses and requests
|
6
|
+
${mirage_url}/clear/requests - Clear all requests
|
7
|
+
${mirage_url}/clear/response_id - Clear a requests and response for a particular response
|
8
|
+
${mirage_url}/clear/request/response_id - Clear request for a particular response
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
Background: The MockServer has already got a response for greeting and leaving on it.
|
13
|
+
Given I post to 'http://localhost:7001/mirage/set/greeting' with parameters:
|
14
|
+
| response | Hello |
|
15
|
+
|
16
|
+
And I post to 'http://localhost:7001/mirage/set/leaving' with parameters:
|
17
|
+
| response | Goodbye |
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
Scenario: Clearing all responses
|
22
|
+
Given I hit 'http://localhost:7001/mirage/clear'
|
23
|
+
|
24
|
+
When I hit 'http://localhost:7001/mirage/get/greeting'
|
25
|
+
Then a 404 should be returned
|
26
|
+
|
27
|
+
When I hit 'http://localhost:7001/mirage/get/leaving'
|
28
|
+
Then a 404 should be returned
|
29
|
+
|
30
|
+
|
31
|
+
Scenario: Clearing a particular response
|
32
|
+
Given I hit 'http://localhost:7001/mirage/clear/1'
|
33
|
+
|
34
|
+
When I hit 'http://localhost:7001/mirage/get/greeting'
|
35
|
+
Then a 404 should be returned
|
36
|
+
|
37
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
38
|
+
Then a 404 should be returned
|
39
|
+
|
40
|
+
When I hit 'http://localhost:7001/mirage/get/leaving'
|
41
|
+
Then 'Goodbye' should be returned
|
42
|
+
|
43
|
+
|
44
|
+
Scenario: Clearing all requests
|
45
|
+
Given I hit 'http://localhost:7001/mirage/get/greeting' with request body:
|
46
|
+
"""
|
47
|
+
Say 'Hello' to me
|
48
|
+
"""
|
49
|
+
And I hit 'http://localhost:7001/mirage/get/leaving' with request body:
|
50
|
+
"""
|
51
|
+
Say 'Goodbye' to me
|
52
|
+
"""
|
53
|
+
And I hit 'http://localhost:7001/mirage/clear'
|
54
|
+
|
55
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
56
|
+
Then a 404 should be returned
|
57
|
+
|
58
|
+
When I hit 'http://localhost:7001/mirage/check/2'
|
59
|
+
Then a 404 should be returned
|
60
|
+
|
61
|
+
|
62
|
+
Scenario: Clearing a stored request request for a prticular response
|
63
|
+
Given I hit 'http://localhost:7001/mirage/get/leaving' with request body:
|
64
|
+
"""
|
65
|
+
See you later
|
66
|
+
"""
|
67
|
+
|
68
|
+
When I hit 'http://localhost:7001/mirage/clear/request/2'
|
69
|
+
Then I hit 'http://localhost:7001/mirage/check/2'
|
70
|
+
And a 404 should be returned
|
71
|
+
|
72
|
+
|
73
|
+
Scenario: Querying a response that has been cleared
|
74
|
+
Given I hit 'http://localhost:7001/mirage/get/greeting' with request body:
|
75
|
+
"""
|
76
|
+
greet me :)
|
77
|
+
"""
|
78
|
+
And I hit 'http://localhost:7001/mirage/clear/1'
|
79
|
+
|
80
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
81
|
+
Then a 404 should be returned
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Feature: Requests made to the Mirage Server can be tracked using the Mirage client
|
2
|
+
|
3
|
+
Background:
|
4
|
+
Given the following gems are required to run the Mirage client test code:
|
5
|
+
"""
|
6
|
+
require 'rubygems'
|
7
|
+
require 'rspec'
|
8
|
+
require 'mirage'
|
9
|
+
"""
|
10
|
+
|
11
|
+
Scenario: The MockServer returns a response
|
12
|
+
Given I hit 'http://localhost:7001/mirage/set/greeting' with parameters:
|
13
|
+
| response | Hello |
|
14
|
+
|
15
|
+
When I hit 'http://localhost:7001/mirage/get/greeting' with parameters:
|
16
|
+
| name | leon |
|
17
|
+
Then I run
|
18
|
+
"""
|
19
|
+
Mirage::Client.new.check(1).should == 'name=leon'
|
20
|
+
"""
|
@@ -0,0 +1,75 @@
|
|
1
|
+
Feature: The client can be used for clearing responses from Mirage
|
2
|
+
|
3
|
+
Background:
|
4
|
+
Given the following gems are required to run the Mirage client test code:
|
5
|
+
"""
|
6
|
+
require 'rubygems'
|
7
|
+
require 'rspec'
|
8
|
+
require 'mirage'
|
9
|
+
"""
|
10
|
+
And I hit 'http://localhost:7001/mirage/set/greeting' with parameters:
|
11
|
+
| response | Hello |
|
12
|
+
|
13
|
+
And I hit 'http://localhost:7001/mirage/get/greeting' with request body:
|
14
|
+
"""
|
15
|
+
Hello there
|
16
|
+
"""
|
17
|
+
And I hit 'http://localhost:7001/mirage/set/leaving' with parameters:
|
18
|
+
| response | Goodbye |
|
19
|
+
|
20
|
+
And I hit 'http://localhost:7001/mirage/get/greeting' with request body:
|
21
|
+
"""
|
22
|
+
I'm going
|
23
|
+
"""
|
24
|
+
|
25
|
+
Scenario: Clearing everything
|
26
|
+
When I run
|
27
|
+
"""
|
28
|
+
Mirage::Client.new.clear
|
29
|
+
"""
|
30
|
+
And I hit 'http://localhost:7001/mirage/get/greeting'
|
31
|
+
Then a 404 should be returned
|
32
|
+
|
33
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
34
|
+
Then a 404 should be returned
|
35
|
+
|
36
|
+
And I hit 'http://localhost:7001/mirage/get/leaving'
|
37
|
+
Then a 404 should be returned
|
38
|
+
|
39
|
+
When I hit 'http://localhost:7001/mirage/check/2'
|
40
|
+
Then a 404 should be returned
|
41
|
+
|
42
|
+
|
43
|
+
Scenario: Clearing all requests
|
44
|
+
When I run
|
45
|
+
"""
|
46
|
+
Mirage::Client.new.clear :requests
|
47
|
+
"""
|
48
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
49
|
+
Then a 404 should be returned
|
50
|
+
|
51
|
+
When I hit 'http://localhost:7001/mirage/check/2'
|
52
|
+
Then a 404 should be returned
|
53
|
+
|
54
|
+
Scenario: Clearning a response
|
55
|
+
Given I run
|
56
|
+
"""
|
57
|
+
Mirage::Client.new.clear 1
|
58
|
+
"""
|
59
|
+
When I hit 'http://localhost:7001/mirage/get/greeting'
|
60
|
+
Then a 404 should be returned
|
61
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
62
|
+
Then a 404 should be returned
|
63
|
+
|
64
|
+
Scenario: Clearning a request
|
65
|
+
Given I run
|
66
|
+
"""
|
67
|
+
Mirage::Client.new.clear :request => 1
|
68
|
+
"""
|
69
|
+
When I hit 'http://localhost:7001/mirage/check/1'
|
70
|
+
Then a 404 should be returned
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
|