omf_ec 6.0.0.pre.4 → 6.0.0.pre.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.
@@ -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