rflow 1.0.0a1 → 1.0.0a2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/Gemfile +0 -1
- data/NOTES +0 -13
- data/README.md +6 -1
- data/bin/rflow +2 -9
- data/example/basic_config.rb +1 -33
- data/example/basic_extensions.rb +0 -98
- data/example/http_config.rb +2 -3
- data/example/http_extensions.rb +6 -63
- data/lib/rflow.rb +31 -39
- data/lib/rflow/child_process.rb +112 -0
- data/lib/rflow/component.rb +77 -148
- data/lib/rflow/component/port.rb +38 -41
- data/lib/rflow/components.rb +4 -8
- data/lib/rflow/components/clock.rb +49 -0
- data/lib/rflow/components/integer.rb +39 -0
- data/lib/rflow/components/raw.rb +10 -6
- data/lib/rflow/components/replicate.rb +20 -0
- data/lib/rflow/components/ruby_proc_filter.rb +27 -0
- data/lib/rflow/configuration.rb +105 -184
- data/lib/rflow/configuration/component.rb +1 -4
- data/lib/rflow/configuration/connection.rb +11 -16
- data/lib/rflow/configuration/port.rb +3 -5
- data/lib/rflow/configuration/ruby_dsl.rb +105 -119
- data/lib/rflow/configuration/setting.rb +19 -25
- data/lib/rflow/configuration/shard.rb +1 -3
- data/lib/rflow/connection.rb +47 -10
- data/lib/rflow/connections.rb +0 -1
- data/lib/rflow/connections/zmq_connection.rb +34 -38
- data/lib/rflow/daemon_process.rb +155 -0
- data/lib/rflow/logger.rb +41 -25
- data/lib/rflow/master.rb +23 -105
- data/lib/rflow/message.rb +78 -108
- data/lib/rflow/pid_file.rb +37 -37
- data/lib/rflow/shard.rb +33 -100
- data/lib/rflow/version.rb +2 -2
- data/rflow.gemspec +2 -2
- data/schema/tick.avsc +10 -0
- data/spec/fixtures/config_ints.rb +4 -40
- data/spec/fixtures/config_shards.rb +1 -2
- data/spec/fixtures/extensions_ints.rb +0 -98
- data/spec/rflow/component/port_spec.rb +61 -0
- data/spec/rflow/components/clock_spec.rb +72 -0
- data/spec/rflow/configuration/ruby_dsl_spec.rb +150 -0
- data/spec/rflow/configuration_spec.rb +54 -0
- data/spec/rflow/forward_to_input_port_spec.rb +48 -0
- data/spec/rflow/forward_to_output_port_spec.rb +40 -0
- data/spec/rflow/logger_spec.rb +48 -0
- data/spec/rflow/message/data/raw_spec.rb +29 -0
- data/spec/rflow/message/data_spec.rb +58 -0
- data/spec/rflow/message_spec.rb +154 -0
- data/spec/rflow_spec.rb +94 -124
- data/spec/spec_helper.rb +8 -12
- metadata +46 -22
- data/lib/rflow/components/raw/extensions.rb +0 -18
- data/lib/rflow/port.rb +0 -4
- data/lib/rflow/util.rb +0 -19
- data/spec/rflow_component_port_spec.rb +0 -58
- data/spec/rflow_configuration_ruby_dsl_spec.rb +0 -148
- data/spec/rflow_configuration_spec.rb +0 -73
- data/spec/rflow_message_data_raw.rb +0 -26
- data/spec/rflow_message_data_spec.rb +0 -60
- data/spec/rflow_message_spec.rb +0 -182
- data/spec/schema_spec.rb +0 -28
- data/temp.rb +0 -295
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class RFlow
|
4
|
+
module Components
|
5
|
+
describe Clock do
|
6
|
+
before(:each) do
|
7
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
8
|
+
Configuration.migrate_database
|
9
|
+
end
|
10
|
+
let(:config) do
|
11
|
+
RFlow::Configuration::Component.new.tap do |c|
|
12
|
+
c.output_ports << RFlow::Configuration::OutputPort.new(name: 'tick_port')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
let(:message_connection) { RFlow::MessageCollectingConnection.new }
|
16
|
+
|
17
|
+
def clock(args = {})
|
18
|
+
Clock.new(config).tap do |c|
|
19
|
+
c.configure! args
|
20
|
+
c.tick_port.connect!
|
21
|
+
c.tick_port.add_connection nil, message_connection
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def messages; message_connection.messages; end
|
26
|
+
|
27
|
+
it 'defaults configuration nicely' do
|
28
|
+
clock.tap do |c|
|
29
|
+
c.clock_name.should == 'Clock'
|
30
|
+
c.tick_interval.should == 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'supports name overrides' do
|
35
|
+
clock('name' => 'testname').tap do |c|
|
36
|
+
c.clock_name.should == 'testname'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'supports interval overrides for floats' do
|
41
|
+
clock('tick_interval' => 1.5).tap do |c|
|
42
|
+
c.tick_interval.should == 1.5
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'supports interval overrides for strings' do
|
47
|
+
clock('tick_interval' => '1.5').tap do |c|
|
48
|
+
c.tick_interval.should == 1.5
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should register a timer' do
|
53
|
+
EventMachine::PeriodicTimer.should_receive(:new).with(1)
|
54
|
+
clock.run!
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should generate a tick message when asked' do
|
58
|
+
clock.tap do |c|
|
59
|
+
now = Integer(Time.now.to_f * 1000)
|
60
|
+
messages.should be_empty
|
61
|
+
c.tick
|
62
|
+
messages.should have(1).message
|
63
|
+
messages.first.tap do |m|
|
64
|
+
m.data_type_name.should == 'RFlow::Message::Clock::Tick'
|
65
|
+
m.data.name.should == 'Clock'
|
66
|
+
m.data.timestamp.should >= now
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rflow/configuration'
|
3
|
+
|
4
|
+
class RFlow
|
5
|
+
class Configuration
|
6
|
+
describe RubyDSL do
|
7
|
+
before(:each) do
|
8
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
9
|
+
Configuration.migrate_database
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should correctly process an empty DSL" do
|
13
|
+
described_class.configure {}
|
14
|
+
|
15
|
+
Shard.should have(0).shards
|
16
|
+
Component.should have(0).components
|
17
|
+
Port.should have(0).ports
|
18
|
+
Connection.should have(0).connections
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should correctly process a component declaration" do
|
22
|
+
described_class.configure do |c|
|
23
|
+
c.component 'boom', 'town', 'opt1' => 'OPT1', 'opt2' => 'OPT2'
|
24
|
+
end
|
25
|
+
|
26
|
+
Shard.should have(1).shard
|
27
|
+
Component.should have(1).component
|
28
|
+
Port.should have(0).ports
|
29
|
+
Connection.should have(0).connections
|
30
|
+
|
31
|
+
Component.first.tap do |c|
|
32
|
+
c.name.should == 'boom'
|
33
|
+
c.specification.should == 'town'
|
34
|
+
c.options.should == {'opt1' => 'OPT1', 'opt2' => 'OPT2'}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should correctly process a connect declaration" do
|
39
|
+
described_class.configure do |c|
|
40
|
+
c.component 'first', 'First'
|
41
|
+
c.component 'second', 'Second'
|
42
|
+
c.connect 'first#out' => 'second#in'
|
43
|
+
c.connect 'first#out' => 'second#in[inkey]'
|
44
|
+
c.connect 'first#out[outkey]' => 'second#in'
|
45
|
+
c.connect 'first#out[outkey]' => 'second#in[inkey]'
|
46
|
+
end
|
47
|
+
|
48
|
+
Shard.should have(1).shard
|
49
|
+
Component.should have(2).components
|
50
|
+
Port.should have(2).ports
|
51
|
+
Connection.should have(4).connections
|
52
|
+
|
53
|
+
first_component = Component.where(name: 'first').first.tap do |component|
|
54
|
+
component.specification.should == 'First'
|
55
|
+
component.should have(0).input_ports
|
56
|
+
component.should have(1).output_port
|
57
|
+
component.output_ports.first.name.should == 'out'
|
58
|
+
|
59
|
+
component.output_ports.first.should have(4).connections
|
60
|
+
component.output_ports.first.connections.tap do |connections|
|
61
|
+
connections[0].input_port_key.should be_nil
|
62
|
+
connections[0].output_port_key.should be_nil
|
63
|
+
connections[1].input_port_key.should == 'inkey'
|
64
|
+
connections[1].output_port_key.should be_nil
|
65
|
+
connections[2].input_port_key.should be_nil
|
66
|
+
connections[2].output_port_key.should == 'outkey'
|
67
|
+
connections[3].input_port_key.should == 'inkey'
|
68
|
+
connections[3].output_port_key.should == 'outkey'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Component.where(name: 'second').first.tap do |component|
|
73
|
+
component.specification.should == 'Second'
|
74
|
+
component.should have(1).input_port
|
75
|
+
component.input_ports.first.name.should == 'in'
|
76
|
+
component.should have(0).output_ports
|
77
|
+
|
78
|
+
component.input_ports.first.should have(4).connections
|
79
|
+
component.input_ports.first.connections.should == first_component.output_ports.first.connections
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should correctly process shard declarations" do
|
84
|
+
described_class.configure do |c|
|
85
|
+
c.component 'first', 'First', :opt1 => 'opt1'
|
86
|
+
|
87
|
+
c.shard "s1", :process => 2 do |s|
|
88
|
+
s.component 'second', 'Second', :opt1 => 'opt1', "opt2" => "opt2"
|
89
|
+
end
|
90
|
+
|
91
|
+
c.shard "s2", :type => :process, :count => 10 do |s|
|
92
|
+
s.component 'third', 'Third'
|
93
|
+
s.component 'fourth', 'Fourth'
|
94
|
+
end
|
95
|
+
|
96
|
+
c.shard "s-ignored", :type => :process, :count => 10 do
|
97
|
+
# ignored because there are no components
|
98
|
+
end
|
99
|
+
|
100
|
+
c.component 'fifth', 'Fifth'
|
101
|
+
|
102
|
+
c.connect 'first#out' => 'second#in'
|
103
|
+
c.connect 'second#out[outkey]' => 'third#in[inkey]'
|
104
|
+
c.connect 'second#out' => 'third#in2'
|
105
|
+
c.connect 'third#out' => 'fourth#in'
|
106
|
+
c.connect 'third#out' => 'fifth#in'
|
107
|
+
end
|
108
|
+
|
109
|
+
Shard.should have(3).shards
|
110
|
+
Component.should have(5).components
|
111
|
+
Port.should have(8).ports
|
112
|
+
Connection.should have(5).connections
|
113
|
+
|
114
|
+
Shard.all.tap do |shards|
|
115
|
+
shards.map(&:name).should == ['DEFAULT', 's1', 's2']
|
116
|
+
shards.first.components.all.map(&:name).should == ['first', 'fifth']
|
117
|
+
shards.second.components.all.map(&:name).should == ['second']
|
118
|
+
shards.third.components.all.map(&:name).should == ['third', 'fourth']
|
119
|
+
end
|
120
|
+
|
121
|
+
Port.all.map(&:name).should == ['out', 'in', 'out', 'in', 'in2', 'out', 'in', 'in']
|
122
|
+
|
123
|
+
Connection.all.map(&:name).should ==
|
124
|
+
['first#out=>second#in',
|
125
|
+
'second#out[outkey]=>third#in[inkey]',
|
126
|
+
'second#out=>third#in2',
|
127
|
+
'third#out=>fourth#in',
|
128
|
+
'third#out=>fifth#in']
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should not allow two components with the same name" do
|
132
|
+
expect {
|
133
|
+
described_class.configure do |c|
|
134
|
+
c.component 'first', 'First'
|
135
|
+
c.component 'first', 'First'
|
136
|
+
end
|
137
|
+
}.to raise_error(ActiveRecord::RecordInvalid)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should not allow two shards with the same name" do
|
141
|
+
expect {
|
142
|
+
described_class.configure do |c|
|
143
|
+
c.shard("s1", :process => 2) {|c| c.component 'x', 'y' }
|
144
|
+
c.shard("s1", :process => 2) {|c| c.component 'z', 'q' }
|
145
|
+
end
|
146
|
+
}.to raise_error
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rflow/configuration'
|
3
|
+
|
4
|
+
class RFlow
|
5
|
+
describe Configuration do
|
6
|
+
describe '.add_available_data_type' do
|
7
|
+
context 'if passed a data_serialization that is not avro' do
|
8
|
+
it "should throw an exception" do
|
9
|
+
expect { Configuration.add_available_data_type('A', 'boom', 'schema') }.to raise_error(
|
10
|
+
ArgumentError, "Data serialization_type must be 'avro' for 'A'")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should not update the available_data_types" do
|
14
|
+
expect {
|
15
|
+
Configuration.add_available_data_type('A', 'boom', 'schema') rescue nil
|
16
|
+
}.not_to change { Configuration.available_data_types.size }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "Data Extensions" do
|
22
|
+
describe ".add_available_data_extension" do
|
23
|
+
context 'if passed a non-module data extension' do
|
24
|
+
it "should throw an exception" do
|
25
|
+
expect {
|
26
|
+
Configuration.add_available_data_extension('data_type', 'NOTAMODULE')
|
27
|
+
}.to raise_error(ArgumentError, "Invalid data extension NOTAMODULE for data_type. Only Ruby Modules allowed")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "if passed a valid Module as a data extension" do
|
32
|
+
it "should update the available_data_extensions" do
|
33
|
+
expect {
|
34
|
+
Configuration.add_available_data_extension('data_type', Module.new)
|
35
|
+
}.to change { Configuration.available_data_extensions['data_type'].size }.by(1)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should perform simple 'prefix'-based inheritance for extensions" do
|
41
|
+
Configuration.add_available_data_extension('A', A = Module.new)
|
42
|
+
Configuration.add_available_data_extension('A::B', B = Module.new)
|
43
|
+
Configuration.add_available_data_extension('A::B::C', C = Module.new)
|
44
|
+
Configuration.add_available_data_extension('A::B::C::D', D = Module.new)
|
45
|
+
|
46
|
+
Configuration.available_data_extensions['A'].should == [A]
|
47
|
+
Configuration.available_data_extensions['A::B'].should == [A, B]
|
48
|
+
Configuration.available_data_extensions['A::B::C'].should == [A, B, C]
|
49
|
+
Configuration.available_data_extensions['A::B::C::D'].should == [A, B, C, D]
|
50
|
+
Configuration.available_data_extensions['A::B::C::D::E'].should == [A, B, C, D]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class RFlow
|
4
|
+
describe ForwardToInputPort do
|
5
|
+
before(:each) do
|
6
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
7
|
+
Configuration.migrate_database
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:filtered_message_connection) { RFlow::MessageCollectingConnection.new }
|
11
|
+
let(:dropped_message_connection) { RFlow::MessageCollectingConnection.new }
|
12
|
+
|
13
|
+
let(:generator) do
|
14
|
+
config = RFlow::Configuration::Component.new.tap do |c|
|
15
|
+
c.output_ports << RFlow::Configuration::OutputPort.new(name: 'out')
|
16
|
+
end
|
17
|
+
RFlow::Components::GenerateIntegerSequence.new(config).tap do |c|
|
18
|
+
c.configure! config.options
|
19
|
+
c.out.add_connection nil, ForwardToInputPort.new(ruby_proc_filter, 'in', nil)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:ruby_proc_filter) do
|
24
|
+
config = RFlow::Configuration::Component.new.tap do |c|
|
25
|
+
c.input_ports << RFlow::Configuration::InputPort.new(name: 'in')
|
26
|
+
['filtered', 'dropped'].each {|p| c.output_ports << RFlow::Configuration::OutputPort.new(name: p) }
|
27
|
+
c.options = {'filter_proc_string' => 'message.data.data_object % 2 == 0'}
|
28
|
+
end
|
29
|
+
RFlow::Components::RubyProcFilter.new(config).tap do |c|
|
30
|
+
c.configure! config.options
|
31
|
+
c.filtered.add_connection nil, filtered_message_connection
|
32
|
+
c.dropped.add_connection nil, dropped_message_connection
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def filtered_messages; filtered_message_connection.messages; end
|
37
|
+
def dropped_messages; dropped_message_connection.messages; end
|
38
|
+
|
39
|
+
it 'foo' do
|
40
|
+
5.times { generator.generate }
|
41
|
+
filtered_messages.should have(3).messages
|
42
|
+
filtered_messages.map(&:data).map(&:data_object).should == [0, 2, 4]
|
43
|
+
dropped_messages.should have(2).messages
|
44
|
+
dropped_messages.map(&:data).map(&:data_object).should == [1, 3]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class RFlow
|
4
|
+
describe ForwardToOutputPort do
|
5
|
+
before(:each) do
|
6
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
7
|
+
Configuration.migrate_database
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:message_connection) { RFlow::MessageCollectingConnection.new }
|
11
|
+
|
12
|
+
let(:generator) do
|
13
|
+
config = RFlow::Configuration::Component.new.tap do |c|
|
14
|
+
c.output_ports << RFlow::Configuration::OutputPort.new(name: 'out')
|
15
|
+
end
|
16
|
+
RFlow::Components::GenerateIntegerSequence.new(config).tap do |c|
|
17
|
+
c.configure! config.options
|
18
|
+
c.out.add_connection nil, ForwardToOutputPort.new(ruby_proc_filter, 'filtered')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:ruby_proc_filter) do
|
23
|
+
config = RFlow::Configuration::Component.new.tap do |c|
|
24
|
+
c.output_ports << RFlow::Configuration::OutputPort.new(name: 'filtered')
|
25
|
+
c.options = {'filter_proc_string' => 'message % 2 == 0'}
|
26
|
+
end
|
27
|
+
RFlow::Components::RubyProcFilter.new(config).tap do |c|
|
28
|
+
c.configure! config.options
|
29
|
+
c.filtered.add_connection nil, message_connection
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def messages; message_connection.messages; end
|
34
|
+
|
35
|
+
it 'should place the messages on the output port, regardless of the filter' do
|
36
|
+
5.times { generator.generate }
|
37
|
+
messages.map(&:data).map(&:data_object).should == [0, 1, 2, 3, 4]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'open3'
|
3
|
+
require 'rflow'
|
4
|
+
|
5
|
+
class RFlow
|
6
|
+
describe Logger do
|
7
|
+
let(:log_file_path) { File.join(@temp_directory_path, 'logfile') }
|
8
|
+
let(:logger_config) do
|
9
|
+
{'rflow.log_file_path' => log_file_path,
|
10
|
+
'rflow.log_level' => 'DEBUG'}
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize_logger
|
14
|
+
@logger = described_class.new(logger_config)
|
15
|
+
end
|
16
|
+
let(:logger) { @logger }
|
17
|
+
|
18
|
+
before(:each) { initialize_logger }
|
19
|
+
|
20
|
+
it "should initialize correctly" do
|
21
|
+
File.exist?(log_file_path).should be true
|
22
|
+
|
23
|
+
logger.error "TESTTESTTEST"
|
24
|
+
File.read(log_file_path).should match(/TESTTESTTEST/)
|
25
|
+
|
26
|
+
logger.close
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should reopen correctly" do
|
30
|
+
moved_path = log_file_path + '.old'
|
31
|
+
|
32
|
+
File.exist?(log_file_path).should be true
|
33
|
+
File.exist?(moved_path).should be false
|
34
|
+
|
35
|
+
File.rename log_file_path, moved_path
|
36
|
+
|
37
|
+
logger.reopen
|
38
|
+
|
39
|
+
logger.error "TESTTESTTEST"
|
40
|
+
File.read(log_file_path).should match(/TESTTESTTEST/)
|
41
|
+
File.read(moved_path).should_not match(/TESTTESTTEST/)
|
42
|
+
|
43
|
+
logger.close
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should toggle log level"
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rflow/components/raw'
|
3
|
+
|
4
|
+
class RFlow
|
5
|
+
class Message
|
6
|
+
class Data
|
7
|
+
describe 'Raw Avro Schema' do
|
8
|
+
let(:schema) { Configuration.available_data_types['RFlow::Message::Data::Raw']['avro'] }
|
9
|
+
|
10
|
+
it "should load the schema" do
|
11
|
+
schema.should_not be_nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should encode and decode an object" do
|
15
|
+
raw = {'raw' => Array.new(256) { rand(256) }.pack('c*')}
|
16
|
+
|
17
|
+
expect { encode_avro(schema, raw) }.to_not raise_error
|
18
|
+
encoded = encode_avro(schema, raw)
|
19
|
+
|
20
|
+
expect { decode_avro(schema, encoded) }.to_not raise_error
|
21
|
+
decoded = decode_avro(schema, encoded)
|
22
|
+
|
23
|
+
decoded.should == raw
|
24
|
+
decoded['raw'].should == raw['raw']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|