zactor 0.0.4 → 0.0.5

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.
data/Gemfile CHANGED
@@ -1,17 +1,13 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  group :development do
4
- gem "rspec", ">= 2"
4
+ gem "rspec", "~> 2.3.0"
5
5
  gem "yard", "~> 0.6.0"
6
6
  gem "bundler", "~> 1.0.0"
7
+ gem "jeweler", "~> 1.5.2"
7
8
  gem "rcov", ">= 0"
8
9
  gem "bluecloth"
9
10
  gem "rr"
10
- gem "guard-rspec"
11
- gem "growl"
12
- gem "ruby-debug19"
13
- gem "em-spec", :git => "https://github.com/mloughran/em-spec.git", :branch => 'rspec2'
14
11
  end
15
12
 
16
-
17
13
  gemspec
data/Gemfile.lock CHANGED
@@ -1,80 +1,33 @@
1
- GIT
2
- remote: https://github.com/mloughran/em-spec.git
3
- revision: fc888b289a424fa93848ffdac3dc4b24fdd34950
4
- branch: rspec2
5
- specs:
6
- em-spec (0.2.2)
7
-
8
1
  PATH
9
2
  remote: .
10
3
  specs:
11
- zactor (0.0.4)
4
+ zactor (0.0.5)
12
5
  activesupport (> 0.1)
13
- bson (> 0.1)
14
- bson_ext (> 0.1)
15
- em-zeromq (> 0.1)
16
- ffi (> 0.1)
17
- ffi-rzmq (> 0.1)
18
- ruby-interface (> 0)
6
+ i18n (> 0.1)
19
7
 
20
8
  GEM
21
9
  remote: http://rubygems.org/
22
10
  specs:
23
11
  activesupport (3.0.7)
24
- archive-tar-minitar (0.5.2)
25
12
  bluecloth (2.1.0)
26
- bson (1.2.4)
27
- bson_ext (1.2.4)
28
- columnize (0.3.2)
29
- configuration (1.2.0)
30
13
  diff-lcs (1.1.2)
31
- em-zeromq (0.2.1)
32
- eventmachine (>= 1.0.0.beta.3)
33
- ffi-rzmq (>= 0.7.2)
34
- eventmachine (1.0.0.beta.3)
35
- ffi (1.0.7)
36
- rake (>= 0.8.7)
37
- ffi-rzmq (0.7.2)
38
- growl (1.0.3)
39
- guard (0.3.0)
40
- open_gem (~> 1.4.2)
41
- thor (~> 0.14.6)
42
- guard-rspec (0.2.0)
43
- guard (>= 0.2.2)
14
+ git (1.2.5)
44
15
  i18n (0.5.0)
45
- launchy (0.3.7)
46
- configuration (>= 0.0.5)
47
- rake (>= 0.8.1)
48
- linecache19 (0.5.11)
49
- ruby_core_source (>= 0.1.4)
50
- open_gem (1.4.2)
51
- launchy (~> 0.3.5)
16
+ jeweler (1.5.2)
17
+ bundler (~> 1.0.0)
18
+ git (>= 1.2.5)
19
+ rake
52
20
  rake (0.8.7)
53
21
  rcov (0.9.9)
54
22
  rr (1.0.2)
55
- rspec (2.5.0)
56
- rspec-core (~> 2.5.0)
57
- rspec-expectations (~> 2.5.0)
58
- rspec-mocks (~> 2.5.0)
59
- rspec-core (2.5.1)
60
- rspec-expectations (2.5.0)
23
+ rspec (2.3.0)
24
+ rspec-core (~> 2.3.0)
25
+ rspec-expectations (~> 2.3.0)
26
+ rspec-mocks (~> 2.3.0)
27
+ rspec-core (2.3.1)
28
+ rspec-expectations (2.3.0)
61
29
  diff-lcs (~> 1.1.2)
62
- rspec-mocks (2.5.0)
63
- ruby-debug-base19 (0.11.24)
64
- columnize (>= 0.3.1)
65
- linecache19 (>= 0.5.11)
66
- ruby_core_source (>= 0.1.4)
67
- ruby-debug19 (0.11.6)
68
- columnize (>= 0.3.1)
69
- linecache19 (>= 0.5.11)
70
- ruby-debug-base19 (>= 0.11.19)
71
- ruby-interface (0.0.2)
72
- activesupport (> 0.1)
73
- i18n (> 0.1)
74
- ruby-interface
75
- ruby_core_source (0.1.4)
76
- archive-tar-minitar (>= 0.5.2)
77
- thor (0.14.6)
30
+ rspec-mocks (2.3.0)
78
31
  yard (0.6.5)
79
32
 
80
33
  PLATFORMS
@@ -83,12 +36,9 @@ PLATFORMS
83
36
  DEPENDENCIES
84
37
  bluecloth
85
38
  bundler (~> 1.0.0)
86
- em-spec!
87
- growl
88
- guard-rspec
39
+ jeweler (~> 1.5.2)
89
40
  rcov
90
41
  rr
91
- rspec (>= 2)
92
- ruby-debug19
42
+ rspec (~> 2.3.0)
93
43
  yard (~> 0.6.0)
94
44
  zactor!
data/README.md CHANGED
@@ -1,167 +1,93 @@
1
- Что это
2
- =======
3
- Zactor позволяет любой руби-объект научить общаться с другими объектами посредством отправки и получения сообщений. При этом неважно где именно расположен объект, в том же процессе, в соседнем, или на другой физической машине.
1
+ RubyInterface
2
+ =============
4
3
 
5
- Zactor использует zmq как бекенд для сообщений, eventmachine и em-zeromq для асинхронной работы с zmq, RubyInterface для описания себя, BSON для сериализации данных.
4
+ Простенький патерн определения интерфейсов в руби. В противовес стандартным миксинам, для каждого интерфейса создается свой класс и соответсвенно экземпляр класса для каждого объекта с интерфейсом.
6
5
 
7
- Каждый zactor-объект имеет свой identity, который генерируется автоматически, либо задается вручную и постоянен. Совокупность identity, host и port на которых был рожден этот объект, это достаточная информация для того что бы отправить этому объекту сообщение из другого объекта. Выглядит примерно так:
8
-
9
- zactor.actor =>
10
- {"identity"=>"actor.2154247120-0.0.0.0:8000", "host"=>"0.0.0.0:8000"}
11
-
12
- Ипользование
13
- ============
14
-
15
- Для начала нужно стартануть Zactor.
16
-
17
- Zactor.start 8000
18
-
19
- Процесс забиндится на 0.0.0.0:8000, через эту точку будет происходить общение между zactor-процессами. Стартоваться должен в запущенном EM-контексте.
20
-
21
- В каждый zactor-активный класс нужно делать include Zactor, после чего у класса и его экземпляров для доступа к функциями Zactor появится метод zactor. После создания объекта нужно выполнить zactor.init
22
-
23
- class A
24
- include Zactor
25
-
26
- def initialize
27
- zactor.init
28
- end
29
- end
30
-
31
- Для отправки сообщений другому объекту нам нужно знать его идентификатор. Идентификатор можно получить тремя способами:
32
-
33
- * Непосрдественной передачей. При инициализации или в любом другом месте, это исключительно внутренняя логика приложения. Идентификатор объекта можно получить вызвав zactor.actor
34
- * При получении сообщения. В сообщении всегда содержится информация об отправителе
35
- * Если объект имеет заранее известный identity, то мы можем получить его полный идентификатор вызвав Zactor.get_actor с identity и хостом, на котором он запущен
36
-
37
- actor = Zactor.get_actor "broker", :host => "0.0.0.0:8001"
38
-
39
- Получив идентификатор можно отправлять ему сообщения
40
-
41
- zactor.send_request actor, :show_me, :boobs
42
-
43
-
44
- Каждый класс может определять какие именно ивенты он может получать и что с ними делать
45
-
46
- include Zactor
47
-
48
- zactor do
49
- event(:show_me) do |o, msg, what|
50
- case what
51
- when :boobs
52
- do_it
53
- else
54
- do_smth_else
6
+ module Tree
7
+ extend RubyInterface
8
+ interface :tree do
9
+ include Enumerable
10
+ attr_accessor :parent
11
+
12
+ def childs
13
+ @childs ||= []
55
14
  end
56
- end
57
- end
58
-
59
- Рассмотрим пример банального ping-pong
60
-
61
- class A
62
- include Zactor
63
-
64
- def initialize
65
- zactor.init
66
- ping Zactor.get_actor("b")
67
- end
68
-
69
- def ping(actor)
70
- puts "Ping!"
71
- zactor.send_request actor, :ping do |res|
72
- puts res
15
+
16
+ def each(&blk)
17
+ blk.call(owner)
18
+ childs.each { |v| v.tree.each(&blk) }
73
19
  end
74
- end
75
- end
76
-
77
-
78
- class B
79
- include Zactor
80
-
81
- zactor do
82
- identity "b"
83
20
 
84
- event(:ping) do |o, msg|
85
- msg.reply "Pong!"
21
+ def set_parent(parent)
22
+ parent.tree.childs << owner
23
+ @parent = parent
86
24
  end
87
25
  end
88
-
89
- def initialize
90
- zactor.init
91
- end
92
-
93
26
  end
94
27
 
95
- EM.run do
96
- Zactor.start 8000
97
-
98
- a = A.new
99
- b = B.new
28
+ class A
29
+ include Tree
100
30
  end
101
-
102
- A посылает сообщение :ping для B, а B отвечает "Pong!"
103
-
104
- В коллбэк определенный в event передается объект получившый сообщение, объект сообщения ({Zactor::Message}) и далее переданные в запросе аргументы (если они есть). У {Zactor::Message} есть два основных метода: sender, возвращающий идентификатор отправителя и reply, который посылает ответ на запрос.
105
-
106
- Важный момент, identity должно задаваться ДО zactor.init и после этого не может меняться.
107
-
108
- ZMQ
109
- ===
110
-
111
- При Zactor.start стартует брокер, по одному на каждый процесс, через него проходят все сообщения данного процесса, принимает сообщения через SUB-сокет, отправляет через PUB. SUB подписан на все сообщения. Каждый zactor-объект создает по паре сокетов, PUB подключается к SUB-брокера, а SUB к PUB-брокера. SUB подписывается на сообщения содержащие его identity.
112
-
113
- ![ZMQ](images/zmq1.png)
114
-
115
- Рассмотрим жизнь сообщения на примере с ping-ping. В случае с b в том же процессе:
116
-
117
- <div class=wsd wsd_style="default"><pre>
118
- A[PUB]->Broker[SUB]: Посылаем запрос :ping
119
- Broker[SUB]->Broker[PUB]: Перебрасываем запрос в PUB сокет
120
- Broker[PUB]->B[SUB]: Передаем получателю сообщение
121
- B[PUB]->Broker[SUB]: Отправляем ответ "Pong!"
122
- Broker[SUB]->Broker[PUB]: Перебрасываем запрос в PUB сокет
123
- Broker[PUB]->A[SUB]: Отправитель получает ответ
124
- </pre></div><script type="text/javascript" src="http://www.websequencediagrams.com/service.js"></script>
125
31
 
126
- В случае с b в другом процессе:
32
+ При разработке интерфейса не нужно задумываться о конфликтах имен переменных, методов, можно делать все что угодно. Аргументом к методу interface передается название метода, по которому этот интерфейс будет доступен.
127
33
 
128
- <div class=wsd wsd_style="default"><pre>
129
- A[PUB for App2]->App2 Broker[SUB]: Посылаем запрос :ping
130
- App2 Broker[SUB]->App2 Broker[PUB]: Перебрасываем запрос в PUB сокет
131
- App2 Broker[PUB]->B[SUB]: Передаем получателю сообщение
132
- B[PUB for App1]->App1 Broker[SUB]: Отправляем ответ "Pong!"
133
- App1 Broker[SUB]->App1 Broker[PUB]: Перебрасываем запрос в PUB сокет
134
- App1 Broker[PUB]->A[SUB]: Отправитель получает ответ
135
- </pre></div><script type="text/javascript" src="http://www.websequencediagrams.com/service.js"></script>
34
+ a = A.new
35
+ b = A.new
136
36
 
137
- Балансировка
138
- ============
139
-
140
- Так как это ZMQ, мы можем очень просто изменить тип получения сообщения. Например, добавив балансер. Теперь можно запускать процессы со ссылкой на этот балансер.
141
-
142
- Zactor.start :balancer => "0.0.0.0:4000"
143
-
144
- У нас получится примерно следующая схема:
145
-
146
- ![ZMQ](images/zmq2.png)
147
-
148
- Теперь наш ping можно отправлять в балансер, а отвечать будет один из подключенных воркеров.
149
-
150
- ping Zactor.get_actor("b", :host => "0.0.0.0:4000")
37
+ a.tree.set_parent b
38
+ b.tree.childs # => [a]
39
+ b.tree.map { |o| o } # => [b, a]
151
40
 
41
+ А при использовании методов относящихся к интерфейсу мы явно видим к какому же интерфейсу он относится. Всем профит!
152
42
 
153
- Протокол обмена
154
- ===============
43
+ В интерфейсе доступен метод owner, возвращающий родительский объект. У класса интерфейса есть <tt>interface_base</tt>, возвращающий класс, куда интерфейс был заинклужен.
155
44
 
45
+ Помимо инстанс метода, создается так же класс-метод. В него можно передать блок, который выполнится в скоупе класса интерфейса. Сам метод возвращает класс интерфейса.
156
46
 
157
- Perfomance
158
- ==========
47
+ module StateMachine
48
+ extend RubyInterface
49
+ interface :state_machine do
50
+ def self.state(name)
51
+ puts "New state #{name}"
52
+ end
53
+ end
54
+ end
159
55
 
160
- А хрен его знает, толком не мерялось ничего :)
56
+ class A
57
+ include StateMachine
58
+
59
+ state_machine do
60
+ state(:parked) # => New state parked
61
+ state(:idling) # => New state idling
62
+ end
63
+ end
64
+
65
+ При наследовании класса с интерфейсом, создается новый класс интерфейса и наследуется от предыдущего, т.е. повторяет иерархию класса, в который он включен.
161
66
 
162
- TODO
163
- ====
67
+ Если в блоке <tt>interface</tt> вызывается метод <tt>interfaced</tt>, то исполнение блока, передаваемого <tt>interfaced</tt>
68
+ происходит после добавления интерфейса в класс, в контексте этого класса.
164
69
 
165
- * Сделать событие отваливания объектов. Наверное, что-то вроде простого аналога link в эрланге.
166
- * Добавить таймауты для запросов с коллбэками. Сейчас они будут висеть бесконечно и засрут память.
167
- * Доступ к отправителю в колллбэке запроса. В случае с балансировкой он будет не тем же, кому мы посылали сообщение
70
+ Пример:
71
+
72
+ module A
73
+ extend RubyInterface
74
+ interface :int do
75
+ interfaced do
76
+ def baz
77
+ self.class.int_interface.foo
78
+ end
79
+ end
80
+
81
+ def self.foo
82
+ "bar"
83
+ end
84
+ end
85
+ end
86
+
87
+ class B
88
+ include A
89
+ end
90
+
91
+ B.new.baz # => "bar"
92
+
93
+ В каждом модуле может быть определено произвольное количество интерфейсов
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+
1
3
  require 'rubygems'
2
4
  require 'bundler'
3
5
  begin
@@ -0,0 +1,3 @@
1
+ module RubyInterface
2
+ VERSION = "0.0.5"
3
+ end
@@ -0,0 +1,8 @@
1
+ class RubyInterfaceHandler < YARD::Handlers::Ruby::Base
2
+ handles method_call(:interface)
3
+
4
+ def process
5
+ parse_block(statement.last.last)
6
+ rescue YARD::Handlers::NamespaceMissingError
7
+ end
8
+ end
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'active_support/core_ext/class/attribute'
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ module RubyInterface
6
+ def interface(method_name, &interface_body)
7
+ mod_inst = self.const_set("#{method_name.to_s.camelize}InstanceMethods", Module.new)
8
+ mod_inst.module_eval <<-EOT, __FILE__, __LINE__ + 1
9
+ def #{method_name}
10
+ @#{method_name}_interface ||= self.class.#{method_name}_interface.new(self)
11
+ end
12
+ EOT
13
+
14
+
15
+ mod_class = self.const_set("#{method_name.to_s.camelize}ClassMethods", Module.new)
16
+ mod_class.module_eval <<-EOT, __FILE__, __LINE__ + 1
17
+ def #{method_name}(&blk)
18
+ self.#{method_name}_interface.class_eval(&blk) if blk
19
+ self.#{method_name}_interface
20
+ end
21
+
22
+ def inherited(subclass)
23
+ new_class = subclass.const_set("#{method_name.to_s.camelize}InterfaceClass", Class.new(self.#{method_name}_interface))
24
+ new_class.interface_base = subclass
25
+ subclass.#{method_name}_interface = new_class
26
+ super
27
+ end
28
+ EOT
29
+
30
+ interface_module = self
31
+
32
+ add_interface do |base|
33
+ base.send(:class_attribute, "#{method_name}_interface")
34
+ interface_class = base.const_set("#{method_name.to_s.camelize}InterfaceClass", Class.new(RubyInterface::InterfaceClass))
35
+ interface_class.interface_base = base
36
+ interface_class.class_eval(&interface_body) if interface_body
37
+ base.send("#{method_name}_interface=", interface_class)
38
+ base.extend mod_class
39
+ base.send :include, mod_inst
40
+ base.class_eval(&interface_class.interfaced) if interface_class.interfaced
41
+ end
42
+
43
+ interface_module.define_singleton_method(:included) do |base|
44
+ @_deps.each {|d| d.call(base)}
45
+ end
46
+ end
47
+
48
+ private
49
+ def add_interface &block
50
+ @_deps ||= []
51
+ @_deps << block
52
+ end
53
+
54
+ class InterfaceClass
55
+ class_attribute :interface_base
56
+ attr_accessor :owner
57
+ def initialize(owner)
58
+ @owner = owner
59
+ end
60
+
61
+ class << self
62
+ def interfaced(&block)
63
+ if block_given?
64
+ @_interfaced_block = block
65
+ else
66
+ @_interfaced_block
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,15 +1,15 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
- require "zactor/version"
3
+ require "ruby-interface/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = %q{zactor}
7
- s.version = Zactor::VERSION
8
- s.summary = "Zactor"
7
+ s.version = RubyInterface::VERSION
8
+ s.summary = "Ruby interface"
9
9
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
- s.authors = ["Andrew Rudenko"]
10
+ s.authors = ["Andrew Rudenko", "Nick Recobra"]
11
11
  s.date = %q{2011-03-24}
12
- s.description = %q{Zactor}
12
+ s.description = %q{Ruby interface}
13
13
  s.email = %q{ceo@prepor.ru}
14
14
 
15
15
  s.files = `git ls-files`.split("\n")
@@ -17,12 +17,6 @@ Gem::Specification.new do |s|
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
19
 
20
- s.add_dependency('ffi', ["> 0.1"])
21
- s.add_dependency('ruby-interface', ["> 0"])
22
- s.add_dependency('ffi-rzmq', ["> 0.1"])
23
- s.add_dependency('em-zeromq', ["> 0.1"])
24
- s.add_dependency('bson', ["> 0.1"])
25
- s.add_dependency('bson_ext', ["> 0.1"])
26
20
  s.add_dependency('activesupport', ["> 0.1"])
27
- end
28
-
21
+ s.add_dependency(%q<i18n>, ["> 0.1"])
22
+ end
@@ -0,0 +1,127 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe RubyInterface do
5
+ def make_interface
6
+ mod = Module.new
7
+ mod.extend(RubyInterface)
8
+ mod
9
+ end
10
+
11
+ describe "Создание интерфейса" do
12
+ let(:mod) { make_interface.tap { |v| v.interface(:test) } }
13
+
14
+ it "должен вызываться interfaced, если определен" do
15
+ @proc = proc {}
16
+ klass = Class.new
17
+ mod.interface :int do
18
+ interfaced(&@proc)
19
+ end
20
+ mock(klass).class_eval(&@proc)
21
+ mock(klass).class_eval(&@proc)
22
+ klass.send :include, mod
23
+ end
24
+
25
+ describe "Класс с интерфейсом" do
26
+ subject { klass }
27
+ let(:klass) { Class.new.tap { |v| v.send :include, mod } }
28
+ let(:interface_klass) { klass.test_interface }
29
+ its(:test) { should eq(interface_klass) }
30
+
31
+ it "может дополнять класс интерфейса" do
32
+ interface_klass.respond_to?(:test).should be_false
33
+ klass.test do
34
+ mattr_accessor :test
35
+ end
36
+ interface_klass.respond_to?(:test).should be_true
37
+ end
38
+
39
+ it "должен содержать класс интерфейса" do
40
+ klass::TestInterfaceClass.ancestors.should include(RubyInterface::InterfaceClass)
41
+ klass.test_interface.should eq(klass::TestInterfaceClass)
42
+ end
43
+
44
+ describe "Класс интерфейса" do
45
+ subject { interface_klass }
46
+ its(:interface_base) { should eq(klass) }
47
+
48
+ describe "Объект с интерфейсом" do
49
+ subject { obj }
50
+ let(:obj) { klass.new }
51
+ its(:test) { should be_kind_of(interface_klass)}
52
+ describe "Объект интерфейса" do
53
+ subject { interface_obj }
54
+ let(:interface_obj) { obj.test }
55
+ its(:owner) { should eq(obj) }
56
+ let(:mod) do
57
+ make_interface.tap do |v|
58
+ v.interface(:test) do
59
+ def foo
60
+ "bar"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ it "должен поддерживать методы определенные в интерфейсе" do
67
+ interface_obj.foo.should eq('bar')
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "при наследовании" do
73
+ let(:klass2) { Class.new(klass) }
74
+ it "должен создавать новый интерфейс-класс, наследуя старый" do
75
+ klass2.test.ancestors.should include(interface_klass)
76
+ end
77
+ it "новый интерфейс-класс должен ссылаться на новый класс" do
78
+ klass2.test.interface_base.should eq(klass2)
79
+ end
80
+ end
81
+ end
82
+
83
+
84
+ end
85
+ end
86
+
87
+ describe "Модуль с двумя интерфейсами" do
88
+ before(:each) do
89
+ @mod = Module.new
90
+ @mod.send :extend, RubyInterface
91
+ @mod.interface :first do
92
+ interfaced do
93
+ def baz
94
+ first.foo
95
+ end
96
+ end
97
+
98
+ def foo
99
+ "bar"
100
+ end
101
+ end
102
+
103
+ @mod.interface :second do
104
+ interfaced do
105
+ self.second_interface.foo
106
+ end
107
+
108
+ def self.foo
109
+ "baz"
110
+ end
111
+ end
112
+ end
113
+
114
+ it "не должно происходить ошибок при подключении модуля с двумя интерфейсами" do
115
+ b = Class.new
116
+ lambda { b.send :include, @mod }.should_not raise_error
117
+ end
118
+
119
+ it "должны появиться интерфейсы с методами" do
120
+ b = Class.new
121
+ b.send :include, @mod
122
+ bb = b.new
123
+ bb.baz.should eq("bar")
124
+ bb.second_interface.foo.should eq("baz")
125
+ end
126
+ end
127
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  $LOAD_PATH.unshift(File.dirname(__FILE__))
3
3
  require 'rspec'
4
- require 'zactor'
5
4
  require 'rr'
5
+ require 'ruby_interface'
6
6
 
7
7
  # Requires supporting files with custom matchers and macros, etc,
8
8
  # in ./support/ and its subdirectories.