ws_discovery 0.0.1

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 (60) hide show
  1. data/Gemfile +2 -0
  2. data/History.rdoc +3 -0
  3. data/README.rdoc +77 -0
  4. data/Rakefile +16 -0
  5. data/lib/ws_discovery/core_ext/socket_patch.rb +16 -0
  6. data/lib/ws_discovery/error.rb +4 -0
  7. data/lib/ws_discovery/multicast_connection.rb +77 -0
  8. data/lib/ws_discovery/network_constants.rb +13 -0
  9. data/lib/ws_discovery/response.rb +95 -0
  10. data/lib/ws_discovery/searcher.rb +91 -0
  11. data/lib/ws_discovery/version.rb +3 -0
  12. data/lib/ws_discovery.rb +61 -0
  13. data/spec/spec_helper.rb +26 -0
  14. data/spec/ws_discovery/coverage/assets/0.7.1/application.css +1110 -0
  15. data/spec/ws_discovery/coverage/assets/0.7.1/application.js +626 -0
  16. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
  17. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
  18. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
  19. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
  20. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
  21. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
  22. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
  23. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
  24. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
  25. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
  26. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
  27. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
  28. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
  29. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
  30. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
  31. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
  32. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
  33. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
  34. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
  35. data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
  36. data/spec/ws_discovery/coverage/assets/0.7.1/favicon_green.png +0 -0
  37. data/spec/ws_discovery/coverage/assets/0.7.1/favicon_red.png +0 -0
  38. data/spec/ws_discovery/coverage/assets/0.7.1/favicon_yellow.png +0 -0
  39. data/spec/ws_discovery/coverage/assets/0.7.1/loading.gif +0 -0
  40. data/spec/ws_discovery/coverage/assets/0.7.1/magnify.png +0 -0
  41. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  42. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  43. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  44. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  45. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  46. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  47. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  48. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  49. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
  50. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  51. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
  52. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
  53. data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  54. data/spec/ws_discovery/coverage/index.html +72 -0
  55. data/spec/ws_discovery/multicast_connection_spec.rb +108 -0
  56. data/spec/ws_discovery/response_spec.rb +118 -0
  57. data/spec/ws_discovery/searcher_spec.rb +85 -0
  58. data/spec/ws_discovery_spec.rb +43 -0
  59. data/ws_discovery.gemspec +32 -0
  60. metadata +296 -0
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/History.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ === 0.0.1 / 2012-11-12
2
+
3
+ * Initial release.
data/README.rdoc ADDED
@@ -0,0 +1,77 @@
1
+ = ws_discovery
2
+
3
+ * {Homepage}[https://github.com/pelco-automation/ws-discovery]
4
+ * {WS-Discovery 1.0 Specification}[http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf]
5
+ * {SOAP-over-UDP Specification}[http://specs.xmlsoap.org/ws/2004/09/soap-over-udp/soap-over-udp.pdf]
6
+
7
+ == Description
8
+
9
+ This gem aims to provide the ability to search for WS-Discovery compatible target
10
+ services.
11
+
12
+ This uses EventMachine[http://github.com/eventmachine/eventmachine], so if
13
+ you're not already, getting familiar with its concepts will be helpful here.
14
+
15
+ == Features
16
+
17
+ * Search for WS-Discovery compatible target services.
18
+
19
+ == Examples
20
+
21
+ === WS-Discovery Searches
22
+
23
+ A WS-Discovery search simply sends the probe out to the multicast group and
24
+ listens for responses for a given (or default of 5 seconds) amount of time. The
25
+ return from this depends on if you're running it within an EventMachine reactor
26
+ or not. If not, it returns an Array of responses as WSDiscovery::Responses.
27
+ Take a look at the WSDiscovery#search docs for more on the options here.
28
+
29
+ require 'ws_discovery'
30
+
31
+ # Search for all devices (do a probe with Types left unspecified)
32
+ all_devices = WSDiscovery.search # this is default
33
+
34
+ # Search for devices of a specific Type
35
+ network_video_transmitters = WSDiscovery.search(
36
+ env_namespaces: { "xmlns:dn" => "http://www.onvif.org/ver10/network/wsdl" },
37
+ types: "dn:NetworkVideoTransmitter")
38
+
39
+ # These searches will return an Array of WSDiscovery::Responses. See the
40
+ # WSDiscovery::Response documentation for more information.
41
+
42
+ If you do the search inside of an EventMachine reactor, as the
43
+ WSDiscovery::Searcher receives and parses responses, it adds them to the accessor
44
+ #discovery_responses, which is an EventMachine::Channel. This lets you subscribe
45
+ to the responses and do what you want with them.
46
+
47
+ == Requirements
48
+
49
+ * Ruby
50
+ * 1.9.3
51
+ * Gems
52
+ * builder
53
+ * eventmachine
54
+ * log_switch
55
+ * nokogiri
56
+ * nori
57
+ * uuid
58
+ * Gems (development)
59
+ * bundler
60
+ * rake
61
+ * rspec
62
+ * simplecov
63
+ * simplecov-rcov
64
+ * yard
65
+
66
+ == Install
67
+
68
+ $ gem install ws_discovery
69
+
70
+ == THANKS
71
+
72
+ The initial core of this gem came from https://github.com/turboladen/upnp due to
73
+ the similarities in how SSDP and WS-Discovery searches are performed.
74
+
75
+ The WSDiscovery::Response class reuses parts of https://github.com/savonrb/savon.
76
+ It made sense to me that WSDiscovery::Responses would behave similarly to
77
+ Savon::SOAP::Responses.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'yard'
4
+
5
+ YARD::Rake::YardocTask.new
6
+ RSpec::Core::RakeTask.new
7
+
8
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
9
+ spec.pattern = 'spec/**/*_spec.rb'
10
+ spec.rcov = true
11
+ end
12
+
13
+ task default: :install
14
+
15
+ # Alias for rubygems-test
16
+ task test: :spec
@@ -0,0 +1,16 @@
1
+ require 'socket'
2
+
3
+ # Workaround for missing constants on Windows
4
+ module Socket::Constants
5
+ IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
6
+ IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
7
+ IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
8
+ IP_TTL = 4 unless defined? IP_TTL
9
+ end
10
+
11
+ class Socket
12
+ IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
13
+ IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
14
+ IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
15
+ IP_TTL = 4 unless defined? IP_TTL
16
+ end
@@ -0,0 +1,4 @@
1
+ module WSDiscovery
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,77 @@
1
+ require_relative 'core_ext/socket_patch'
2
+ require_relative 'network_constants'
3
+ require_relative 'error'
4
+ require 'ipaddr'
5
+ require 'socket'
6
+ require 'eventmachine'
7
+
8
+ module WSDiscovery
9
+ class MulticastConnection < EventMachine::Connection
10
+ include WSDiscovery::NetworkConstants
11
+
12
+ # @param [Fixnum] ttl The TTL value to use when opening the UDP socket
13
+ # required for WSDiscovery actions.
14
+ def initialize ttl=TTL
15
+ @ttl = ttl
16
+ @discovery_responses = EM::Channel.new
17
+
18
+ setup_multicast_socket
19
+ end
20
+
21
+ private
22
+
23
+ # Gets the IP and port from the peer that just sent data.
24
+ #
25
+ # @return [Array<String,Fixnum>] The IP and port.
26
+ def peer_info
27
+ peer_bytes = get_peername[2, 6].unpack("nC4")
28
+ port = peer_bytes.first.to_i
29
+ ip = peer_bytes[1, 4].join(".")
30
+
31
+ [ip, port]
32
+ end
33
+
34
+ # Sets Socket options to allow for multicasting. If ENV["RUBY_TESTING_ENV"]
35
+ # is equal to "testing", then it doesn't turn off multicast looping.
36
+ def setup_multicast_socket
37
+ set_membership(IPAddr.new(MULTICAST_IP).hton + IPAddr.new('0.0.0.0').hton)
38
+ set_multicast_ttl(@ttl)
39
+ set_ttl(@ttl)
40
+
41
+ unless ENV["RUBY_TESTING_ENV"] == "testing"
42
+ switch_multicast_loop :off
43
+ end
44
+ end
45
+
46
+ # @param [String] membership The network byte ordered String that represents
47
+ # the IP(s) that should join the membership group.
48
+ def set_membership(membership)
49
+ set_sock_opt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
50
+ end
51
+
52
+ # @param [Fixnum] ttl TTL to set IP_MULTICAST_TTL to.
53
+ def set_multicast_ttl(ttl)
54
+ set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, [ttl].pack('i'))
55
+ end
56
+
57
+ # @param [Fixnum] ttl TTL to set IP_TTL to.
58
+ def set_ttl(ttl)
59
+ set_sock_opt(Socket::IPPROTO_IP, Socket::IP_TTL, [ttl].pack('i'))
60
+ end
61
+
62
+ # @param [Symbol] on_off Turn on/off multicast looping. Supply :on or :off.
63
+ # @raise [WSDiscovery::Error] If invalid option is given.
64
+ def switch_multicast_loop(on_off)
65
+ hex_value = case on_off
66
+ when :on, "\001"
67
+ "\001"
68
+ when :off, "\000"
69
+ "\000"
70
+ else
71
+ raise WSDiscovery::Error, "Can't switch IP_MULTICAST_LOOP to '#{on_off}'"
72
+ end
73
+
74
+ set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, hex_value)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,13 @@
1
+ module WSDiscovery
2
+ module NetworkConstants
3
+
4
+ # Default multicast IP address
5
+ MULTICAST_IP = '239.255.255.250'
6
+
7
+ # Default multicast port
8
+ MULTICAST_PORT = 3702
9
+
10
+ # Default TTL
11
+ TTL = 1
12
+ end
13
+ end
@@ -0,0 +1,95 @@
1
+ require 'nokogiri'
2
+ require 'nori'
3
+ require_relative 'error'
4
+
5
+ Nori.configure do |config|
6
+ config.strip_namespaces = true
7
+ config.convert_tags_to { |tag| tag.snakecase.to_sym }
8
+ end
9
+
10
+ module WSDiscovery
11
+
12
+ # Represents the probe response.
13
+ class Response
14
+
15
+ attr_accessor :response
16
+
17
+ # @param [String] response Text of the response to a WSDiscovery probe.
18
+ def initialize(response)
19
+ @response = response
20
+ end
21
+
22
+ # Shortcut accessor for the SOAP response body Hash.
23
+ #
24
+ # @param [Symbol] key The key to access in the body Hash.
25
+ # @return [Hash,String] The accessed value.
26
+ def [](key)
27
+ body[key]
28
+ end
29
+
30
+ # Returns the SOAP response header as a Hash.
31
+ #
32
+ # @return [Hash] SOAP response header.
33
+ # @raise [WSDiscovery::Error] If unable to parse response.
34
+ def header
35
+ unless hash.has_key? :envelope
36
+ raise WSDiscovery::Error, "Unable to parse response body '#{to_xml}'"
37
+ end
38
+
39
+ hash[:envelope][:header]
40
+ end
41
+
42
+ # Returns the SOAP response body as a Hash.
43
+ #
44
+ # @return [Hash] SOAP response body.
45
+ # @raise [WSDiscovery::Error] If unable to parse response.
46
+ def body
47
+ unless hash.has_key? :envelope
48
+ raise WSDiscovery::Error, "Unable to parse response body '#{to_xml}'"
49
+ end
50
+
51
+ hash[:envelope][:body]
52
+ end
53
+
54
+ alias to_hash body
55
+
56
+ # Returns the complete SOAP response XML without normalization.
57
+ #
58
+ # @return [Hash] Complete SOAP response Hash.
59
+ def hash
60
+ @hash ||= Nori.parse(to_xml)
61
+ end
62
+
63
+ # Returns the SOAP response XML.
64
+ #
65
+ # @return [String] Raw SOAP response XML.
66
+ def to_xml
67
+ response
68
+ end
69
+
70
+ # Returns a Nokogiri::XML::Document for the SOAP response XML.
71
+ #
72
+ # @return [Nokogiri::XML::Document] Document for the SOAP response.
73
+ def doc
74
+ @doc ||= Nokogiri::XML(to_xml)
75
+ end
76
+
77
+ # Returns an Array of Nokogiri::XML::Node objects retrieved with the given +path+.
78
+ # Automatically adds all of the document's namespaces unless a +namespaces+ hash is provided.
79
+ #
80
+ # @param [String] path XPath to search.
81
+ # @param [Hash<String>] namespaces Namespaces to append.
82
+ def xpath(path, namespaces = nil)
83
+ doc.xpath(path, namespaces || xml_namespaces)
84
+ end
85
+
86
+ private
87
+
88
+ # XML Namespaces from the Document.
89
+ #
90
+ # @return [Hash] Namespaces from the Document.
91
+ def xml_namespaces
92
+ @xml_namespaces ||= doc.collect_namespaces
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,91 @@
1
+ require_relative 'multicast_connection'
2
+ require_relative 'response'
3
+ require 'builder'
4
+ require 'log_switch'
5
+ require 'uuid'
6
+
7
+ class WSDiscovery::Searcher < WSDiscovery::MulticastConnection
8
+ extend LogSwitch
9
+ self.logger.datetime_format = "%Y-%m-%d %H:%M:%S "
10
+
11
+ # @return [EventMachine::Channel] Provides subscribers with responses from
12
+ # their search request.
13
+ attr_reader :discovery_responses
14
+
15
+ # @param [Hash] options The options for the probe.
16
+ # @option options [Hash<String>] :env_namespaces Additional envelope namespaces.
17
+ # @option options [Hash<String>] :type_attributes Type attributes.
18
+ # @option options [String] :types Types.
19
+ # @option options [Hash<String>] :scope_attributes Scope attributes.
20
+ # @option options [String] :scopes Scopes.
21
+ # @option options [Fixnum] :ttl TTL for the probe.
22
+ def initialize(options={})
23
+ options[:ttl] ||= TTL
24
+
25
+ @search = probe(options)
26
+
27
+ super options[:ttl]
28
+ end
29
+
30
+ # This is the callback called by EventMachine when it receives data on the
31
+ # socket that's been opened for this connection. In this case, the method
32
+ # parses the probe matches into WSDiscovery::Responses and adds them to the
33
+ # appropriate EventMachine::Channel (provided as accessor methods). This
34
+ # effectively means that in each Channel, you get a WSDiscovery::Response
35
+ # for each response that comes in on the socket.
36
+ #
37
+ # @param [String] response The data received on this connection's socket.
38
+ def receive_data(response)
39
+ ip, port = peer_info
40
+ WSDiscovery::Searcher.log "<#{self.class}> Response from #{ip}:#{port}:\n#{response}\n"
41
+ parsed_response = parse(response)
42
+ @discovery_responses << parsed_response
43
+ end
44
+
45
+ # Converts the headers to a set of key-value pairs.
46
+ #
47
+ # @param [String] data The data to convert.
48
+ # @return [WSDiscovery::Response] The converted data.
49
+ def parse(data)
50
+ WSDiscovery::Response.new(data)
51
+ end
52
+
53
+ # Sends the probe that was built during init. Logs what was sent if the
54
+ # send was successful.
55
+ def post_init
56
+ if send_datagram(@search, MULTICAST_IP, MULTICAST_PORT) > 0
57
+ WSDiscovery::Searcher.log("Sent datagram search:\n#{@search}")
58
+ end
59
+ end
60
+
61
+ # Probe for target services supporting WS-Discovery.
62
+ #
63
+ # @param [Hash] options The options for the probe.
64
+ # @option options [Hash<String>] :env_namespaces Additional envelope namespaces.
65
+ # @option options [Hash<String>] :type_attributes Type attributes.
66
+ # @option options [String] :types Types.
67
+ # @option options [Hash<String>] :scope_attributes Scope attributes.
68
+ # @option options [String] :scopes Scopes.
69
+ # @return [String] Probe SOAP message.
70
+ def probe(options={})
71
+ namespaces = {
72
+ 'xmlns:a' => 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
73
+ 'xmlns:d' => 'http://schemas.xmlsoap.org/ws/2005/04/discovery',
74
+ 'xmlns:s' => 'http://www.w3.org/2003/05/soap-envelope'
75
+ }
76
+ namespaces.merge options[:env_namespaces] if options[:env_namespaces]
77
+
78
+ Builder::XmlMarkup.new.s(:Envelope, namespaces) do |xml|
79
+ xml.s(:Header) do |xml|
80
+ xml.a(:Action, 'http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe')
81
+ xml.a(:MessageID, "uuid:#{UUID.generate}")
82
+ xml.a(:To, 'urn:schemas-xmlsoap-org:ws:2005:04:discovery')
83
+ end
84
+
85
+ xml.s(:Body) do |xml|
86
+ xml.d(:Types, options[:type_attributes], options[:types])
87
+ xml.d(:Scopes, options[:scope_attributes], options[:scopes])
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module WSDiscovery
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'ws_discovery/core_ext/socket_patch'
2
+ require 'eventmachine'
3
+
4
+ require_relative 'ws_discovery/error'
5
+ require_relative 'ws_discovery/network_constants'
6
+ require_relative 'ws_discovery/searcher'
7
+
8
+ module WSDiscovery
9
+ include NetworkConstants
10
+
11
+ DEFAULT_WAIT_TIME = 5
12
+
13
+ # Opens a UDP socket on 0.0.0.0, on an ephemeral port, has WSDiscovery::Searcher
14
+ # build and send the search request, then receives the responses. The search
15
+ # will stop after +response_wait_time+.
16
+ #
17
+ # @param [Hash] options The options for the probe.
18
+ # @option options [Hash<String>] :env_namespaces Additional envelope namespaces.
19
+ # @option options [Hash<String>] :type_attributes Type attributes.
20
+ # @option options [String] :types Types.
21
+ # @option options [Hash<String>] :scope_attributes Scope attributes.
22
+ # @option options [String] :scopes Scopes.
23
+ # @return [Array<WSDiscovery::Response>,WSDiscovery::Searcher] Returns an
24
+ # Array of probe responses. If the reactor is already running this will return
25
+ # a WSDiscovery::Searcher which will make its accessors available so you can
26
+ # get responses in real time.
27
+ def self.search(options={})
28
+ response_wait_time = options[:response_wait_time] || DEFAULT_WAIT_TIME
29
+ responses = []
30
+
31
+ multicast_searcher = proc do
32
+ EM.open_datagram_socket('0.0.0.0', 0, WSDiscovery::Searcher, options)
33
+ end
34
+
35
+ if EM.reactor_running?
36
+ return multicast_searcher.call
37
+ else
38
+ EM.run do
39
+ ms = multicast_searcher.call
40
+
41
+ ms.discovery_responses.subscribe do |notification|
42
+ responses << notification
43
+ end
44
+
45
+ EM.add_timer(response_wait_time) { EM.stop }
46
+ trap_signals
47
+ end
48
+ end
49
+
50
+ responses.flatten
51
+ end
52
+
53
+ private
54
+
55
+ # Traps INT, TERM, and HUP signals and stops the reactor.
56
+ def self.trap_signals
57
+ trap('INT') { EM.stop }
58
+ trap('TERM') { EM.stop }
59
+ trap('HUP') { EM.stop } if RUBY_PLATFORM !~ /mswin|mingw/
60
+ end
61
+ end
@@ -0,0 +1,26 @@
1
+ require 'simplecov'
2
+ require 'simplecov-rcov'
3
+
4
+ class SimpleCov::Formatter::MergedFormatter
5
+ def format(result)
6
+ SimpleCov::Formatter::HTMLFormatter.new.format(result)
7
+ SimpleCov::Formatter::RcovFormatter.new.format(result)
8
+ end
9
+ end
10
+
11
+ SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
12
+
13
+ SimpleCov.start do
14
+ add_filter "/spec"
15
+ add_filter "/lib/deps"
16
+ end
17
+
18
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
19
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
20
+ require 'rspec'
21
+
22
+ # Requires supporting files with custom matchers and macros, etc,
23
+ # in ./support/ and its subdirectories.
24
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |file| require file }
25
+
26
+ ENV["RUBY_TESTING_ENV"] = "testing"