omf_ec 6.0.0.pre.4 → 6.0.0.pre.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,222 @@
1
+ require 'hashie'
2
+ require 'singleton'
3
+
4
+ module OmfEc
5
+ #
6
+ # This class defines an Experiment Property, and also holds all of the
7
+ # Experiment Properties defined for a given experiment.
8
+ # Most of this implementation is re-used from OMF 5.4
9
+ #
10
+ class ExperimentProperty
11
+
12
+ # Contains all the experiment properties
13
+ @@properties = Hashie::Mash.new
14
+
15
+ # Holds all observers on any Experiment Property creation
16
+ @@creation_observers = []
17
+
18
+ #
19
+ # Returns a given property
20
+ # - name =nameof the property to return
21
+ #
22
+ # [Return] a property
23
+ #
24
+ def self.[](name)
25
+ p = @@properties[name.to_s.to_sym]
26
+ if p.nil?
27
+ raise OEDLCommandException.new(name,
28
+ "Unknown experiment property '#{name}'\n\tKnown properties are "+
29
+ "'#{ExperimentProperty.names.join(', ')}'")
30
+ end
31
+ return p
32
+ end
33
+
34
+ def self.[]=(name, val)
35
+ p = ExperimentProperty[name.to_sym]
36
+ p.set(val)
37
+ end
38
+
39
+ def self.length; @@properties.length end
40
+
41
+ # Minitest needs to be able to turn this Class into a string, this is
42
+ # normally done through the default 'method_missing' of the Classe
43
+ # but we redefined that... so to run minitest we need to explicitly
44
+ # define 'to_str' for this Class
45
+ def self.to_str; "ExperimentProperty" end
46
+
47
+ #
48
+ # Handles missing method, allows to access an existing Experiment
49
+ # Property with the syntax 'propcontext.propname'
50
+ #
51
+ def self.method_missing(name, args = nil)
52
+ name = name.to_s
53
+ if setter = (name[-1] == ?=)
54
+ name.chop!
55
+ end
56
+ p = ExperimentProperty[name.to_sym]
57
+ if setter
58
+ p.set(args)
59
+ else
60
+ return p
61
+ end
62
+ end
63
+
64
+ # Iterate over all Experiment Properties. The block
65
+ # will be called with the respective property as single
66
+ # argument
67
+ #
68
+ # - sort_names = if 'true' sort the properties (default: true)
69
+ # - &block = the block of commands to call
70
+ #
71
+ def self.each(sort_names = false, &block)
72
+ names = @@properties.keys
73
+ names = names.sort_by {|sym| sym.to_s} if (sort_names)
74
+ names.each { |n| block.call(@@properties[n]) }
75
+ end
76
+
77
+ # Return an existing Experiment Property, or create a new one
78
+ #
79
+ # - name = name of the property to create/return
80
+ # - value = value to assign to this property
81
+ # - description = short string description for this property
82
+ #
83
+ # [Return] an Experiment Property
84
+ #
85
+ def self.create(name, value = nil, description = nil)
86
+ name = name.to_s
87
+ # http://stackoverflow.com/questions/4378670/what-is-a-ruby-regex-to-match-a-function-name
88
+ if /[@$"]/ =~ name.to_sym.inspect
89
+ raise OEDLCommandException.new("ExperimentProperty.create",
90
+ "Cannot create property '#{name}', its name is not a valid Ruby name")
91
+ end
92
+ p = nil
93
+ name = name.to_sym
94
+ if (p = @@properties[name]) != nil
95
+ p.set(value) if value != nil
96
+ p.description = description if description != nil
97
+ else
98
+ p = ExperimentProperty.new(name, value, description)
99
+ @@properties[name] = p
100
+ # Let the observers know that we created a new property
101
+ @@creation_observers.each { |proc| proc.call(:create, p) }
102
+ end
103
+ return p
104
+ end
105
+
106
+ #
107
+ # Return the names of the all defined Experiment Properties
108
+ #
109
+ # [Return] an Array with the names of all defined Experiment Properties
110
+ #
111
+ def self.names() return @@properties.keys end
112
+
113
+ # Add an observer for any creation of a new Experiment Property
114
+ #
115
+ # - proc = block to execute when a new Experiment Property is created
116
+ #
117
+ def self.add_observer(&proc) @@creation_observers << proc end
118
+
119
+ attr_reader :name, :value, :id
120
+ attr_accessor :description
121
+
122
+ private :initialize
123
+
124
+ #
125
+ # Create a new Experiment Property
126
+ #
127
+ # - name = name of the property to create/return
128
+ # - value = value to assign to this property
129
+ # - description = short string description for this property
130
+ #
131
+ def initialize(name, value = nil, description = nil)
132
+ @name = name.to_s
133
+ @description = description
134
+ @change_observers = Array.new
135
+ set(value)
136
+ end
137
+
138
+ #
139
+ # Add a block of command to the list of actions to do
140
+ # when this property is being changed
141
+ #
142
+ # - &block = the block of command to add
143
+ #
144
+ def on_change (&block)
145
+ debug "Somebody bound to me"
146
+ @change_observers << block
147
+ end
148
+
149
+ #
150
+ # Update the value of this Experiment Property
151
+ #
152
+ # - value = new value for this property
153
+ #
154
+ def set(value)
155
+ @value = value
156
+ info "#{name} = #{value.inspect} (#{value.class})"
157
+ @change_observers.each { |proc| proc.call(value) }
158
+ end
159
+
160
+ # Implicit conversion to String (required for + operator)
161
+ def to_str() @value.to_s end
162
+
163
+ # Explicit conversion to String
164
+ alias_method :to_s, :to_str
165
+
166
+ # Division operator for Integer and Float properties
167
+ def /(right)
168
+ if @value.kind_of?(Integer) || @value.kind_of?(Float)
169
+ return (@value / right)
170
+ else
171
+ raise OEDLCommandException.new("/", "Illegal operation, "+
172
+ "the value of Experiment Property '#{@name}' is not numerical "+
173
+ "(current value is of type #{value.class})")
174
+ end
175
+ end
176
+
177
+ # Multiplication operator for Integer and Float properties
178
+ def *(right)
179
+ if @value.kind_of?(Integer) || @value.kind_of?(Float)
180
+ return (@value * right)
181
+ else
182
+ raise OEDLCommandException.new("*", "Illegal operation, "+
183
+ "the value of Experiment Property '#{@name}' is not numerical "+
184
+ "(current value is of type #{value.class})")
185
+ end
186
+ end
187
+
188
+ # Substraction operator for Integer and Float properties
189
+ def -(right)
190
+ if @value.kind_of?(Integer) || @value.kind_of?(Float)
191
+ return (@value - right)
192
+ else
193
+ raise OEDLCommandException.new("-", "Illegal operation, "+
194
+ "the value of Experiment Property '#{@name}' is not numerical "+
195
+ "(current value is of type #{value.class})")
196
+ end
197
+ end
198
+
199
+ # Addition operator for Integer, Float, and String properties
200
+ def +(right)
201
+ if @value.kind_of?(Integer) || @value.kind_of?(Float) || @value.kind_of?(String)
202
+ return (@value + right)
203
+ else
204
+ raise OEDLCommandException.new("+", "Illegal operation, "+
205
+ "The value of Experiment Property '#{@name}' does not support addition "+
206
+ "(current value is of type #{value.class})")
207
+ end
208
+ end
209
+
210
+ # Explicit Coercion for Integer, Float, and String properties
211
+ # (allow property to be on the right-hand of an operator such as +)
212
+ def coerce(other)
213
+ if @value.kind_of?(Integer) || @value.kind_of?(Float) || @value.kind_of?(String)
214
+ return other, @value
215
+ else
216
+ raise OEDLCommandException.new("coercion", "Illegal operation, "+
217
+ "The value of Experiment Property '#{@name}' cannot be coerced to allow "+
218
+ " the requested operation (current value is of type #{value.class})")
219
+ end
220
+ end
221
+ end
222
+ end
data/lib/omf_ec/group.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'securerandom'
2
+ require 'monitor'
2
3
 
3
4
  module OmfEc
4
5
  # Group instance used in experiment script
@@ -9,12 +10,15 @@ module OmfEc
9
10
  # @!attribute members [Array] holding members to be added to group
10
11
  # @!attribute apps [Array] holding applications to be added to group
11
12
  class Group
13
+ include MonitorMixin
14
+
12
15
  attr_accessor :name, :id, :net_ifs, :members, :app_contexts
16
+ attr_reader :topic
13
17
 
14
18
  # @param [String] name name of the group
15
19
  # @param [Hash] opts
16
20
  # @option opts [Boolean] :unique Should the group be unique or not, default is true
17
- def initialize(name, opts = {})
21
+ def initialize(name, opts = {}, &block)
18
22
  @opts = {unique: true}.merge!(opts)
19
23
  self.name = name
20
24
  self.id = @opts[:unique] ? SecureRandom.uuid : self.name
@@ -22,57 +26,45 @@ module OmfEc
22
26
  self.net_ifs = []
23
27
  self.members = []
24
28
  self.app_contexts = []
25
- end
26
-
27
- # Add existing resources to the group
28
- #
29
- # Resources to be added could be a list of resources, groups, or the mixture of both.
30
- def add_resource(*names)
31
- names.each do |name|
32
- # resource to add is a group
33
- if OmfEc.exp.groups.any? { |v| v.name == name }
34
- self.add_resource(*group(name).members.uniq)
35
- else
36
- OmfEc.comm.subscribe(name) do |m|
37
- unless m.error?
38
- # resource with uid: name is available
39
- unless OmfEc.exp.state.any? { |v| v[:uid] == name }
40
- OmfEc.exp.state << { uid: name }
41
- end
42
-
43
- r = OmfEc.comm.get_topic(name)
44
29
 
45
- r.on_message lambda {|m| m.operation == :inform && m.read_content('inform_type') == 'STATUS' && m.context_id.nil? } do |i|
46
- r = OmfEc.exp.state.find { |v| v[:uid] == i.read_property(:uid) }
47
- unless r.nil?
48
- i.each_property do |p|
49
- key = p.attr('key').to_sym
50
- r[key] = i.read_property(key)
51
- end
52
- end
53
- Experiment.instance.process_events
54
- end
30
+ @resource_topics = {}
55
31
 
56
- # Receive failed inform message
57
- r.on_message lambda {|m| m.operation == :inform && m.read_content('inform_type') == 'FAILED' && m.context_id.nil? } do |i|
58
- warn "RC reports failure: '#{i.read_content("reason")}'"
59
- end
32
+ OmfEc.subscribe_and_monitor(id, self, &block)
33
+ super()
34
+ end
60
35
 
61
- c = OmfEc.comm.configure_message(self.id) do |m|
62
- m.property(:membership, self.id)
63
- end
36
+ def associate_topic(topic)
37
+ self.synchronize do
38
+ @topic = topic
39
+ end
40
+ end
64
41
 
65
- c.publish name
42
+ def associate_resource_topic(name, res_topic)
43
+ self.synchronize do
44
+ @resource_topics[name] = res_topic
45
+ end
46
+ end
66
47
 
67
- c.on_inform_status do |i|
68
- r = OmfEc.exp.state.find { |v| v[:uid] == name }
69
- r[:membership] = i.read_property(:membership)
70
- Experiment.instance.process_events
71
- end
48
+ def resource_topic(name)
49
+ @resource_topics[name]
50
+ end
72
51
 
73
- c.on_inform_failed do |i|
74
- warn "RC reports failure: '#{i.read_content("reason")}'"
75
- end
52
+ # Add existing resources to the group
53
+ #
54
+ # Resources to be added could be a list of resources, groups, or the mixture of both.
55
+ def add_resource(*names)
56
+ self.synchronize do
57
+ # Recording membership first, used for ALL_UP event
58
+ names.each do |name|
59
+ g = OmfEc.experiment.group(name)
60
+ if g # resource to add is a group
61
+ @members += g.members
62
+ self.add_resource(*g.members.uniq)
63
+ else
64
+ @members << name.to_s
65
+ OmfEc.subscribe_and_monitor(name) do |res|
66
+ info "Config #{name} to join #{self.name}"
67
+ res.configure(membership: self.id)
76
68
  end
77
69
  end
78
70
  end
@@ -84,73 +76,31 @@ module OmfEc
84
76
  # @param [String] name
85
77
  # @param [Hash] opts to be used to create new resources
86
78
  def create_resource(name, opts, &block)
87
-
88
- # Make a deep copy of opts in case it contains structures of structures
89
- begin
90
- opts = Marshal.load ( Marshal.dump(opts.merge(hrn: name)))
91
- rescue Exception => e
92
- raise "#{e.message} - Could not deep copy opts: '#{opts.inspect}'"
93
- end
94
-
95
- # Naming convention of child resource group
96
- resource_group_name = "#{self.id}_#{opts[:type]}"
97
-
98
-
99
- unless OmfEc.exp.sub_groups.include?(resource_group_name)
100
- OmfEc.exp.sub_groups << resource_group_name
101
-
102
- rg = OmfEc.comm.get_topic(resource_group_name)
103
- # Receive status inform message
104
- rg.on_message lambda {|m| m.operation == :inform && m.read_content('inform_type') == 'STATUS' && m.context_id.nil? } do |i|
105
- r = OmfEc.exp.state.find { |v| v[:uid] == i.read_property(:uid) }
106
- unless r.nil?
107
- if i.read_property("status_type") == 'APP_EVENT'
108
- info "APP_EVENT #{i.read_property('event')} "+
109
- "from app #{i.read_property("app")} - msg: #{i.read_property("msg")}"
110
- end
111
- i.each_property do |p|
112
- r[p.attr('key').to_sym] = p.content.ducktype
113
- end
114
- end
115
- Experiment.instance.process_events
116
- end
117
-
118
- # Receive failed inform message
119
- rg.on_message lambda {|m| m.operation == :inform && m.read_content('inform_type') == 'FAILED' && m.context_id.nil? } do |i|
120
- warn "RC reports failure: '#{i.read_content("reason")}'"
79
+ self.synchronize do
80
+ raise ArgumentError, "Option :type is required for creating resource" if opts[:type].nil?
81
+
82
+ # Make a deep copy of opts in case it contains structures of structures
83
+ begin
84
+ opts = Marshal.load ( Marshal.dump(opts.merge(hrn: name)))
85
+ rescue Exception => e
86
+ raise "#{e.message} - Could not deep copy opts: '#{opts.inspect}'"
121
87
  end
122
- end
123
-
124
- # We create another group topic for new resouce
125
- OmfEc.comm.subscribe(resource_group_name, create_if_non_existent: true) do |m|
126
- unless m.error?
127
88
 
128
- c = OmfEc.comm.create_message(self.id) do |m|
129
- m.property(:membership, resource_group_name)
130
- opts.each_pair do |k, v|
131
- m.property(k, v)
132
- end
133
- end
89
+ # Naming convention of child resource group
90
+ resource_group_name = "#{self.id}_#{opts[:type].to_s}"
134
91
 
135
- c.publish self.id
136
-
137
- c.on_inform_created do |i|
138
- info "#{opts[:type]} #{i.resource_id} created"
139
- OmfEc.exp.state << { uid: i.resource_id, type: opts[:type], hrn: name, membership: [resource_group_name]}
140
- block.call if block
141
- Experiment.instance.process_events
142
- end
143
-
144
- c.on_inform_failed do |i|
145
- warn "RC reports failure: '#{i.read_content("reason")}'"
146
- end
92
+ OmfEc.subscribe_and_monitor(resource_group_name) do |res_group|
93
+ associate_resource_topic(opts[:type].to_s, res_group)
94
+ # Send create message to group
95
+ r_type = opts.delete(:type)
96
+ @topic.create(r_type, opts.merge(membership: resource_group_name))
147
97
  end
148
98
  end
149
99
  end
150
100
 
151
101
  # @return [OmfEc::Context::GroupContext]
152
102
  def resources
153
- OmfEc::Context::GroupContext.new(group: self.id)
103
+ OmfEc::Context::GroupContext.new(group: self)
154
104
  end
155
105
 
156
106
  include OmfEc::Backward::Group
@@ -1,3 +1,3 @@
1
1
  module OmfEc
2
- VERSION = "6.0.0.pre.4"
2
+ VERSION = "6.0.0.pre.5"
3
3
  end
data/lib/omf_ec.rb CHANGED
@@ -5,6 +5,7 @@ require 'omf_ec/backward/app_definition'
5
5
  require 'omf_ec/backward/default_events'
6
6
  require 'omf_ec/backward/core_ext/array'
7
7
  require "omf_ec/version"
8
+ require "omf_ec/experiment_property"
8
9
  require "omf_ec/experiment"
9
10
  require "omf_ec/group"
10
11
  require "omf_ec/app_definition"
@@ -22,17 +23,68 @@ module OmfEc
22
23
 
23
24
  alias_method :exp, :experiment
24
25
 
25
- # Experiment's communicator instance
26
- # @return [OmfCommon::Comm]
27
- def communicator
28
- Experiment.instance.comm
29
- end
30
-
31
26
  # Full path of lib directory
32
27
  def lib_root
33
28
  File.expand_path("../..", "#{__FILE__}/lib")
34
29
  end
35
30
 
36
- alias_method :comm, :communicator
31
+ def register_default_callback(topic)
32
+ topic.on_creation_failed do |msg|
33
+ warn "RC reports creation.failed: '#{msg[:reason]}'"
34
+ debug msg
35
+ end
36
+
37
+ topic.on_error do |msg|
38
+ warn "RC reports error: '#{msg[:reason]}'"
39
+ debug msg
40
+ end
41
+
42
+ topic.on_warn do |msg|
43
+ warn "RC reports warning: '#{msg[:reason]}'"
44
+ debug msg
45
+ end
46
+
47
+ topic.on_creation_ok do |msg|
48
+ debug "Received CREATION.OK via #{topic.id}"
49
+ info "Resource #{msg[:res_id]} created"
50
+
51
+ OmfEc.experiment.add_or_update_resource_state(msg[:uid], msg.properties)
52
+
53
+ OmfEc.experiment.process_events
54
+ end
55
+
56
+ topic.on_status do |msg|
57
+ props = []
58
+ msg.each_property { |k, v| props << "#{k}: #{v}" }
59
+ debug "#{topic.id} >> inform: #{props.join(", ")}"
60
+
61
+ if msg[:status_type] == 'APP_EVENT'
62
+ info "APP_EVENT #{msg[:event]} from app #{msg[:app]} - msg: #{msg[:msg]}"
63
+ end
64
+
65
+ OmfEc.experiment.add_or_update_resource_state(msg[:uid], msg.properties)
66
+ OmfEc.experiment.process_events
67
+ end
68
+ end
69
+
70
+ #TODO: Could we find a better name for this method?
71
+ def subscribe_and_monitor(topic_id, context_obj = nil, &block)
72
+ topic = OmfCommon::Comm::Topic[topic_id]
73
+ if topic.nil?
74
+ OmfCommon.comm.subscribe(topic_id) do |topic|
75
+ if topic.error?
76
+ error "Failed to subscribe #{topic_id}"
77
+ else
78
+ info "Subscribed to #{topic_id}"
79
+ context_obj.associate_topic(topic) if context_obj
80
+ block.call(context_obj || topic) if block
81
+ register_default_callback(topic)
82
+ end
83
+ end
84
+ else
85
+ context_obj.associate_topic(topic) if context_obj
86
+ block.call(context_obj || topic) if block
87
+ end
88
+ end
37
89
  end
38
90
  end
data/omf_ec.gemspec CHANGED
@@ -7,9 +7,11 @@ Gem::Specification.new do |s|
7
7
  s.version = OmfEc::VERSION
8
8
  s.authors = ["NICTA"]
9
9
  s.email = ["omf-user@lists.nicta.com.au"]
10
- s.homepage = "https://www.mytestbed.net"
10
+ s.homepage = "http://omf.mytestbed.net"
11
11
  s.summary = %q{OMF experiment controller}
12
12
  s.description = %q{Experiment controller of OMF, a generic framework for controlling and managing networking testbeds.}
13
+ s.required_ruby_version = '>= 1.9.3'
14
+ s.license = 'MIT'
13
15
 
14
16
  s.rubyforge_project = "omf_ec"
15
17
 
@@ -21,5 +23,5 @@ Gem::Specification.new do |s|
21
23
  # specify any dependencies here; for example:
22
24
  s.add_development_dependency "minitest", "~> 3.2"
23
25
  s.add_runtime_dependency "omf_common", "~> 6.0.0.pre"
24
- s.add_runtime_dependency "gli", "~> 2.4.1"
26
+ s.add_runtime_dependency "gli", "~> 2.5.3"
25
27
  end
@@ -0,0 +1,53 @@
1
+ require 'test_helper'
2
+ require 'omf_ec/dsl'
3
+
4
+ describe OmfEc::DSL do
5
+ describe "when included" do
6
+
7
+ include OmfEc::DSL
8
+
9
+ it "must respond to after and every" do
10
+ respond_to?(:after).must_equal true
11
+ respond_to?(:every).must_equal true
12
+ end
13
+
14
+ it "must respond to def_property" do
15
+ @exp = MiniTest::Mock.new
16
+ @exp.expect(:add_property, true, [String, Object, String])
17
+
18
+ OmfEc.stub :experiment, @exp do
19
+ def_property('name', 'default', 'testing')
20
+ end
21
+ end
22
+
23
+ it "must respond to def_application" do
24
+ block = proc { 1 }
25
+ def_application('bob', &block).must_equal 1
26
+ OmfEc.experiment.app_definitions.key?('bob').must_equal true
27
+ end
28
+
29
+ it "must respond to def_group" do
30
+ block = proc { 1 }
31
+ OmfEc.stub :subscribe_and_monitor, true do
32
+ def_group('bob', &block).must_be_kind_of OmfEc::Group
33
+ end
34
+ end
35
+
36
+ it "must respond to all_groups iterator" do
37
+ block = proc { 1 }
38
+ all_groups(&block)
39
+ end
40
+
41
+ it "must respond to all_groups?" do
42
+ OmfEc.stub :subscribe_and_monitor, true do
43
+ OmfEc.experiment.stub :groups, [] do
44
+ all_groups? { true }.must_equal false
45
+ end
46
+ def_group('bob')
47
+ all_groups? { |g| g.name == 'bob' }.must_equal true
48
+ end
49
+ end
50
+ end
51
+
52
+ end
53
+
@@ -0,0 +1,81 @@
1
+ require 'test_helper'
2
+ require 'omf_ec/experiment_property'
3
+ require 'omf_ec/dsl'
4
+ include OmfEc::DSL
5
+
6
+ describe OmfEc::ExperimentProperty do
7
+
8
+ describe "when a new ExperimentProperty is created" do
9
+ it "must raise an error if it is given an invalid name" do
10
+ created_properties = 0
11
+ # Test only a few common invalid name patterns
12
+ %w(12 a=b a/b 1a .a a.b a?b a!b !a ?a #a $a @a %a).each do |name|
13
+ begin
14
+ OmfEc::ExperimentProperty.create(name)
15
+ created_properties = created_properties + 1
16
+ rescue Exception => ex
17
+ ex.must_be_kind_of OEDLCommandException
18
+ end
19
+ end
20
+ created_properties.must_equal 0
21
+ end
22
+
23
+ it "must not create a new property if one already exist with the same name" do
24
+ size_before = OmfEc::ExperimentProperty.length
25
+ OmfEc::ExperimentProperty.create('bar','a')
26
+ OmfEc::ExperimentProperty[:bar].value.must_equal 'a'
27
+ OmfEc::ExperimentProperty.create('bar','b')
28
+ OmfEc::ExperimentProperty[:bar].value.must_equal 'b'
29
+ OmfEc::ExperimentProperty.length.must_equal (size_before + 1)
30
+ end
31
+
32
+ it "must return a properly set ExperimentProperty object" do
33
+ size_before = OmfEc::ExperimentProperty.length
34
+ OmfEc::ExperimentProperty.create('foo', 1, 'abc')
35
+ OmfEc::ExperimentProperty[:foo].name.must_equal 'foo'
36
+ OmfEc::ExperimentProperty[:foo].value.must_equal 1
37
+ OmfEc::ExperimentProperty[:foo].description.must_equal 'abc'
38
+ OmfEc::ExperimentProperty.length.must_equal (size_before + 1)
39
+ end
40
+
41
+ it "must inform all of its observers when its value changes" do
42
+ value = 2
43
+ foobar = OmfEc::ExperimentProperty.create('foobar',1)
44
+ foobar.on_change { |v| v.must_equal value }
45
+ foobar.on_change { |v| (v*2).must_equal value*2 }
46
+ OmfEc::ExperimentProperty[:foobar] = value
47
+ OmfEc::ExperimentProperty[:foobar].value.must_equal value
48
+ end
49
+ end
50
+
51
+ describe "when a the Class ExperimentProperty is creating a new property" do
52
+ it "must inform all of its observers" do
53
+ size_before = OmfEc::ExperimentProperty.length
54
+ OmfEc::ExperimentProperty.add_observer do |c,p|
55
+ p.name.must_equal 'barfoo'
56
+ p.value.must_equal 123
57
+ p.description.must_equal 'abc'
58
+ end
59
+ OmfEc::ExperimentProperty.create('barfoo', 123, 'abc')
60
+ OmfEc::ExperimentProperty.length.must_equal (size_before + 1)
61
+ end
62
+ end
63
+
64
+ describe "when an operation involves an ExperimentProperty" do
65
+ it "must return the expected result" do
66
+ OmfEc::ExperimentProperty[:foo] = 2
67
+ (OmfEc::ExperimentProperty[:foo] + 1).must_equal 3
68
+ (1 + OmfEc::ExperimentProperty[:foo]).must_equal 3
69
+ (OmfEc::ExperimentProperty[:foo] - 1).must_equal 1
70
+ (1 - OmfEc::ExperimentProperty[:foo]).must_equal -1
71
+ (OmfEc::ExperimentProperty[:foo] * 2).must_equal 4
72
+ (2 * OmfEc::ExperimentProperty[:foo]).must_equal 4
73
+ (OmfEc::ExperimentProperty[:foo] / 1).must_equal 2
74
+ (2 / OmfEc::ExperimentProperty[:foo]).must_equal 1
75
+ OmfEc::ExperimentProperty[:bar] = 'a'
76
+ (OmfEc::ExperimentProperty[:bar] + "b").must_equal 'ab'
77
+ ('b' + OmfEc::ExperimentProperty[:bar]).must_equal 'ba'
78
+ end
79
+ end
80
+
81
+ end