virtualbox 0.1.1 → 0.2.0
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/.gitignore +2 -1
- data/.yardopts +3 -0
- data/Rakefile +15 -1
- data/Readme.md +3 -0
- data/TODO +0 -3
- data/VERSION +1 -1
- data/docs/GettingStarted.md +196 -0
- data/lib/virtualbox/abstract_model/attributable.rb +15 -16
- data/lib/virtualbox/abstract_model/dirty.rb +13 -2
- data/lib/virtualbox/abstract_model/relatable.rb +59 -10
- data/lib/virtualbox/abstract_model.rb +23 -1
- data/lib/virtualbox/attached_device.rb +144 -19
- data/lib/virtualbox/command.rb +11 -2
- data/lib/virtualbox/dvd.rb +52 -4
- data/lib/virtualbox/exceptions.rb +13 -0
- data/lib/virtualbox/hard_drive.rb +37 -8
- data/lib/virtualbox/image.rb +37 -0
- data/lib/virtualbox/nic.rb +1 -1
- data/lib/virtualbox/proxies/collection.rb +29 -0
- data/lib/virtualbox/storage_controller.rb +11 -0
- data/lib/virtualbox/vm.rb +105 -5
- data/lib/virtualbox.rb +2 -1
- data/test/virtualbox/abstract_model/dirty_test.rb +17 -0
- data/test/virtualbox/abstract_model/relatable_test.rb +49 -1
- data/test/virtualbox/abstract_model_test.rb +32 -1
- data/test/virtualbox/attached_device_test.rb +184 -2
- data/test/virtualbox/command_test.rb +36 -0
- data/test/virtualbox/dvd_test.rb +43 -0
- data/test/virtualbox/hard_drive_test.rb +52 -1
- data/test/virtualbox/image_test.rb +79 -0
- data/test/virtualbox/nic_test.rb +10 -0
- data/test/virtualbox/proxies/collection_test.rb +45 -0
- data/test/virtualbox/storage_controller_test.rb +12 -0
- data/test/virtualbox/vm_test.rb +118 -10
- data/virtualbox.gemspec +8 -3
- metadata +8 -3
- data/lib/virtualbox/errors.rb +0 -7
data/lib/virtualbox/command.rb
CHANGED
@@ -14,6 +14,13 @@ module VirtualBox
|
|
14
14
|
@@vboxmanage = "VBoxManage"
|
15
15
|
|
16
16
|
class <<self
|
17
|
+
# Returns true if the last run command was a success. Obviously this
|
18
|
+
# will introduce all sorts of thread-safe problems. Those will have to
|
19
|
+
# be addressed another time.
|
20
|
+
def success?
|
21
|
+
$?.to_i == 0
|
22
|
+
end
|
23
|
+
|
17
24
|
# Sets the path to VBoxManage, which is required for this gem to
|
18
25
|
# work.
|
19
26
|
def vboxmanage=(path)
|
@@ -22,7 +29,9 @@ module VirtualBox
|
|
22
29
|
|
23
30
|
# Runs a VBoxManage command and returns the output.
|
24
31
|
def vboxmanage(command)
|
25
|
-
execute("#{@@vboxmanage} #{command}")
|
32
|
+
result = execute("#{@@vboxmanage} #{command}")
|
33
|
+
raise Exceptions::CommandFailedException.new(result) if !Command.success?
|
34
|
+
result
|
26
35
|
end
|
27
36
|
|
28
37
|
# Runs a command and returns a boolean result showing
|
@@ -30,7 +39,7 @@ module VirtualBox
|
|
30
39
|
# exit code.
|
31
40
|
def test(command)
|
32
41
|
execute(command)
|
33
|
-
|
42
|
+
success?
|
34
43
|
end
|
35
44
|
|
36
45
|
# Runs a command and returns the STDOUT result. The reason this is
|
data/lib/virtualbox/dvd.rb
CHANGED
@@ -9,6 +9,15 @@ module VirtualBox
|
|
9
9
|
#
|
10
10
|
# DVD.all
|
11
11
|
#
|
12
|
+
# # Empty Drives
|
13
|
+
#
|
14
|
+
# Sometimes it is useful to have an empty drive. This is the case where you
|
15
|
+
# may have a DVD drive but it has no disk in it. To create an {AttachedDevice},
|
16
|
+
# an image _must_ be specified, and an empty drive is a simple option. Creating
|
17
|
+
# an empty drive is simple:
|
18
|
+
#
|
19
|
+
# DVD.empty_drive
|
20
|
+
#
|
12
21
|
class DVD < Image
|
13
22
|
class <<self
|
14
23
|
# Returns an array of all available DVDs as DVD objects
|
@@ -16,16 +25,55 @@ module VirtualBox
|
|
16
25
|
raw = Command.vboxmanage("list dvds")
|
17
26
|
parse_raw(raw)
|
18
27
|
end
|
28
|
+
|
29
|
+
# Returns an empty drive. This is useful for creating new
|
30
|
+
# or modifyingn existing {AttachedDevice} objects and
|
31
|
+
# attaching an empty drive to them.
|
32
|
+
#
|
33
|
+
# @return [DVD]
|
34
|
+
def empty_drive
|
35
|
+
new(:empty_drive)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(*args)
|
40
|
+
if args.length == 1 && args[0] == :empty_drive
|
41
|
+
@empty_drive = true
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Override of {Image#empty_drive?}. This will only be true if
|
48
|
+
# the DVD was created with {DVD.empty_drive}.
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
def empty_drive?
|
52
|
+
@empty_drive || false
|
53
|
+
end
|
54
|
+
|
55
|
+
# Override of {Image#image_type}.
|
56
|
+
def image_type
|
57
|
+
"dvddrive"
|
19
58
|
end
|
20
59
|
|
21
60
|
# Deletes the DVD from VBox managed list and also from disk.
|
22
61
|
# This method will fail if the disk is currently mounted to any
|
23
|
-
# virtual machine.
|
62
|
+
# virtual machine. This method also does nothing for empty drives
|
63
|
+
# (see {DVD.empty_drive}) and will return false automatically in
|
64
|
+
# that case.
|
24
65
|
#
|
25
|
-
# @
|
26
|
-
|
66
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
67
|
+
# will be raised if the command failed.
|
68
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
69
|
+
def destroy(raise_errors=false)
|
70
|
+
return false if empty_drive?
|
71
|
+
|
27
72
|
Command.vboxmanage("closemedium dvd #{uuid} --delete")
|
28
|
-
|
73
|
+
true
|
74
|
+
rescue Exceptions::CommandFailedException
|
75
|
+
raise if raise_errors
|
76
|
+
false
|
29
77
|
end
|
30
78
|
end
|
31
79
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module VirtualBox
|
2
|
+
# Gem specific exceptions will reside under this namespace for easy
|
3
|
+
# documentation and searching.
|
4
|
+
module Exceptions
|
5
|
+
class Exception < ::Exception; end
|
6
|
+
|
7
|
+
class CommandFailedException < Exception; end
|
8
|
+
class InvalidObjectException < Exception; end
|
9
|
+
class InvalidRelationshipObjectException < Exception; end
|
10
|
+
class NonSettableRelationshipException < Exception; end
|
11
|
+
class NoParentException < Exception; end
|
12
|
+
end
|
13
|
+
end
|
@@ -120,24 +120,44 @@ module VirtualBox
|
|
120
120
|
# single filename, since VirtualBox will place it in the hard
|
121
121
|
# drives folder.
|
122
122
|
# @param [String] format The format to convert to.
|
123
|
-
# @
|
124
|
-
|
123
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
124
|
+
# will be raised if the command failed.
|
125
|
+
# @return [HardDrive] The new, cloned hard drive, or nil on failure.
|
126
|
+
def clone(outputfile, format="VDI", raise_errors=false)
|
125
127
|
raw = Command.vboxmanage("clonehd #{uuid} #{Command.shell_escape(outputfile)} --format #{format} --remember")
|
126
128
|
return nil unless raw =~ /UUID: (.+?)$/
|
127
129
|
|
128
130
|
self.class.find($1.to_s)
|
131
|
+
rescue Exceptions::CommandFailedException
|
132
|
+
raise if raise_errors
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
|
136
|
+
# Override of {Image#image_type}.
|
137
|
+
def image_type
|
138
|
+
"hdd"
|
129
139
|
end
|
130
140
|
|
131
141
|
# Creates a new hard drive.
|
132
142
|
#
|
133
143
|
# **This method should NEVER be called. Call {#save} instead.**
|
134
|
-
|
144
|
+
#
|
145
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
146
|
+
# will be raised if the command failed.
|
147
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
148
|
+
def create(raise_errors=false)
|
135
149
|
raw = Command.vboxmanage("createhd --filename #{location} --size #{size} --format #{read_attribute(:format)} --remember")
|
136
150
|
return nil unless raw =~ /UUID: (.+?)$/
|
137
151
|
|
138
152
|
# Just replace our attributes with the newly created ones. This also
|
139
153
|
# will set new_record to false.
|
140
154
|
populate_attributes(self.class.find($1.to_s).attributes)
|
155
|
+
|
156
|
+
# Return the success of the command
|
157
|
+
true
|
158
|
+
rescue Exceptions::CommandFailedException
|
159
|
+
raise if raise_errors
|
160
|
+
false
|
141
161
|
end
|
142
162
|
|
143
163
|
# Saves the hard drive object. If the hard drive is new,
|
@@ -146,10 +166,14 @@ module VirtualBox
|
|
146
166
|
#
|
147
167
|
# Currently, **saving existing hard drives does nothing**.
|
148
168
|
# This is a limitation of VirtualBox, rather than the library itself.
|
149
|
-
|
169
|
+
#
|
170
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
171
|
+
# will be raised if the command failed.
|
172
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
173
|
+
def save(raise_errors=false)
|
150
174
|
if new_record?
|
151
175
|
# Create a new hard drive
|
152
|
-
create
|
176
|
+
create(raise_errors)
|
153
177
|
else
|
154
178
|
super
|
155
179
|
end
|
@@ -160,10 +184,15 @@ module VirtualBox
|
|
160
184
|
#
|
161
185
|
# **This operation is not reversable.**
|
162
186
|
#
|
163
|
-
# @
|
164
|
-
|
187
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
188
|
+
# will be raised if the command failed.
|
189
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
190
|
+
def destroy(raise_errors=false)
|
165
191
|
Command.vboxmanage("closemedium disk #{uuid} --delete")
|
166
|
-
|
192
|
+
true
|
193
|
+
rescue Exceptions::CommandFailedException
|
194
|
+
raise if raise_errors
|
195
|
+
false
|
167
196
|
end
|
168
197
|
end
|
169
198
|
end
|
data/lib/virtualbox/image.rb
CHANGED
@@ -72,6 +72,7 @@ module VirtualBox
|
|
72
72
|
#
|
73
73
|
# @return [Array<Image>]
|
74
74
|
def populate_relationship(caller, data)
|
75
|
+
return DVD.empty_drive if data[:medium] == "emptydrive"
|
75
76
|
return nil if data[:uuid].nil?
|
76
77
|
|
77
78
|
subclasses.each do |subclass|
|
@@ -80,6 +81,22 @@ module VirtualBox
|
|
80
81
|
matching = subclass.all.find { |obj| obj.uuid == data[:uuid] }
|
81
82
|
return matching unless matching.nil?
|
82
83
|
end
|
84
|
+
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# Sets an image onto a relationship and/or removes it from a
|
89
|
+
# relationship. This method is automatically called by {Relatable}.
|
90
|
+
#
|
91
|
+
# **This method typically won't be used except internally.**
|
92
|
+
#
|
93
|
+
# @return [Image]
|
94
|
+
def set_relationship(caller, old_value, new_value)
|
95
|
+
# We don't actually destroy any images using this method,
|
96
|
+
# so just return the new value as long as its a valid object
|
97
|
+
raise Exceptions::InvalidRelationshipObjectException.new if new_value && !new_value.is_a?(Image)
|
98
|
+
|
99
|
+
return new_value
|
83
100
|
end
|
84
101
|
end
|
85
102
|
|
@@ -90,5 +107,25 @@ module VirtualBox
|
|
90
107
|
|
91
108
|
populate_attributes(info) if info
|
92
109
|
end
|
110
|
+
|
111
|
+
# The image type as a string for the virtualbox command line. This
|
112
|
+
# method should be overridden by any subclass and is expected to
|
113
|
+
# return the type which is used in command line parameters for
|
114
|
+
# attaching to storage controllers.
|
115
|
+
#
|
116
|
+
# @return [String]
|
117
|
+
def image_type
|
118
|
+
raise "This must be implemented by any subclasses"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns boolean showing if empty drive or not. This method should be
|
122
|
+
# overriden by any subclass and is expected to return true of false
|
123
|
+
# showing if this image represents an empty drive of whatever type
|
124
|
+
# the subclass is.
|
125
|
+
#
|
126
|
+
# @return [Boolean]
|
127
|
+
def empty_drive?
|
128
|
+
false
|
129
|
+
end
|
93
130
|
end
|
94
131
|
end
|
data/lib/virtualbox/nic.rb
CHANGED
@@ -142,7 +142,7 @@ module VirtualBox
|
|
142
142
|
# called on {#save}.
|
143
143
|
#
|
144
144
|
# **This method typically won't be used except internally.**
|
145
|
-
def save_attribute(key, value, vmname)
|
145
|
+
def save_attribute(key, value, vmname)
|
146
146
|
Command.vboxmanage("modifyvm #{vmname} --#{key}#{@index} #{Command.shell_escape(value)}")
|
147
147
|
super
|
148
148
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module VirtualBox
|
2
|
+
module Proxies
|
3
|
+
# A relationship which can be described as a collection, which
|
4
|
+
# is a set of items.
|
5
|
+
class Collection < Array
|
6
|
+
def initialize(parent)
|
7
|
+
super()
|
8
|
+
|
9
|
+
@parent = parent
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(item)
|
13
|
+
item.added_to_relationship(@parent) if item.respond_to?(:added_to_relationship)
|
14
|
+
push(item)
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear
|
18
|
+
each do |item|
|
19
|
+
delete(item)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(item)
|
24
|
+
return unless super
|
25
|
+
item.removed_from_relationship(@parent) if item.respond_to?(:removed_from_relationship)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -73,6 +73,17 @@ module VirtualBox
|
|
73
73
|
def destroy_relationship(caller, data, *args)
|
74
74
|
data.each { |v| v.destroy(*args) }
|
75
75
|
end
|
76
|
+
|
77
|
+
# Saves the relationship. This simply calls {#save} on every
|
78
|
+
# member of the relationship.
|
79
|
+
#
|
80
|
+
# **This method typically won't be used except internally.**
|
81
|
+
def save_relationship(caller, data)
|
82
|
+
# Just call save on each nic with the VM
|
83
|
+
data.each do |sc|
|
84
|
+
sc.save
|
85
|
+
end
|
86
|
+
end
|
76
87
|
end
|
77
88
|
|
78
89
|
# Since storage controllers still can't be created from scratch,
|
data/lib/virtualbox/vm.rb
CHANGED
@@ -226,7 +226,7 @@ module VirtualBox
|
|
226
226
|
# Saves the virtual machine if modified. This method saves any modified
|
227
227
|
# attributes of the virtual machine. If any related attributes were saved
|
228
228
|
# as well (such as storage controllers), those will be saved, too.
|
229
|
-
def save
|
229
|
+
def save(raise_errors=false)
|
230
230
|
# Make sure we save the new name first if that was changed, or
|
231
231
|
# we'll get some inconsistencies later
|
232
232
|
if name_changed?
|
@@ -234,7 +234,12 @@ module VirtualBox
|
|
234
234
|
@original_name = name
|
235
235
|
end
|
236
236
|
|
237
|
-
super
|
237
|
+
super()
|
238
|
+
|
239
|
+
true
|
240
|
+
rescue Exceptions::CommandFailedException
|
241
|
+
raise if raise_errors
|
242
|
+
return false
|
238
243
|
end
|
239
244
|
|
240
245
|
# Saves a single attribute of the virtual machine. This should **not**
|
@@ -246,6 +251,42 @@ module VirtualBox
|
|
246
251
|
super
|
247
252
|
end
|
248
253
|
|
254
|
+
# Exports a virtual machine. The virtual machine will be exported
|
255
|
+
# to the specified OVF file name. This directory will also have the
|
256
|
+
# `mf` file which contains the file checksums and also the virtual
|
257
|
+
# drives of the machine.
|
258
|
+
#
|
259
|
+
# Export also supports an additional options hash which can contain
|
260
|
+
# information that will be embedded with the virtual machine. View
|
261
|
+
# below for more information on the available options.
|
262
|
+
#
|
263
|
+
# This method will block until the export is complete, which takes about
|
264
|
+
# 60 to 90 seconds on my 2.2 GHz 2009 model MacBook Pro.
|
265
|
+
#
|
266
|
+
# @param [String] filename The file (not directory) to save the exported
|
267
|
+
# OVF file. This directory will also receive the checksum file and
|
268
|
+
# virtual disks.
|
269
|
+
# @option options [String] :product (nil) The name of the product
|
270
|
+
# @option options [String] :producturl (nil) The URL of the product
|
271
|
+
# @option options [String] :vendor (nil) The name of the vendor
|
272
|
+
# @option options [String] :vendorurl (nil) The URL for the vendor
|
273
|
+
# @option options [String] :version (nil) The version information
|
274
|
+
# @option options [String] :eula (nil) License text
|
275
|
+
# @option options [String] :eulafile (nil) License file
|
276
|
+
def export(filename, options={}, raise_error=false)
|
277
|
+
options = options.inject([]) do |acc, kv|
|
278
|
+
acc.push("--#{kv[0]} #{Command.shell_escape(kv[1])}")
|
279
|
+
end
|
280
|
+
|
281
|
+
options.unshift("--vsys 0") unless options.empty?
|
282
|
+
|
283
|
+
raw = Command.vboxmanage("export #{@original_name} -o #{Command.shell_escape(filename)} #{options.join(" ")}".strip)
|
284
|
+
true
|
285
|
+
rescue Exceptions::CommandFailedException
|
286
|
+
raise if raise_error
|
287
|
+
false
|
288
|
+
end
|
289
|
+
|
249
290
|
# Starts the virtual machine. The virtual machine can be started in a
|
250
291
|
# variety of modes:
|
251
292
|
#
|
@@ -255,15 +296,74 @@ module VirtualBox
|
|
255
296
|
#
|
256
297
|
# All modes will start their processes and return almost immediately.
|
257
298
|
# Both the GUI and headless mode will not block the ruby process.
|
258
|
-
|
299
|
+
#
|
300
|
+
# @param [Symbol] mode Described above.
|
301
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
302
|
+
# will be raised if the command failed.
|
303
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
304
|
+
def start(mode=:gui, raise_errors=false)
|
259
305
|
Command.vboxmanage("startvm #{@original_name} --type #{mode}")
|
306
|
+
true
|
307
|
+
rescue Exceptions::CommandFailedException
|
308
|
+
raise if raise_errors
|
309
|
+
false
|
260
310
|
end
|
261
311
|
|
262
312
|
# Stops the VM by directly calling "poweroff." Immediately halts the
|
263
313
|
# virtual machine without saving state. This could result in a loss
|
264
314
|
# of data.
|
265
|
-
|
266
|
-
|
315
|
+
#
|
316
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
317
|
+
# will be raised if the command failed.
|
318
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
319
|
+
def stop(raise_errors=false)
|
320
|
+
control(:poweroff, raise_errors)
|
321
|
+
end
|
322
|
+
|
323
|
+
# Pauses the VM, putting it on hold temporarily. The VM can be resumed
|
324
|
+
# again by calling {#resume}
|
325
|
+
#
|
326
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
327
|
+
# will be raised if the command failed.
|
328
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
329
|
+
def pause(raise_errors=false)
|
330
|
+
control(:pause, raise_errors)
|
331
|
+
end
|
332
|
+
|
333
|
+
# Resume a paused VM.
|
334
|
+
#
|
335
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
336
|
+
# will be raised if the command failed.
|
337
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
338
|
+
def resume(raise_errors=false)
|
339
|
+
control(:resume, raise_errors)
|
340
|
+
end
|
341
|
+
|
342
|
+
# Saves the state of a VM and stops it. The VM can be resumed
|
343
|
+
# again by calling "start" again.
|
344
|
+
#
|
345
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
346
|
+
# will be raised if the command failed.
|
347
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
348
|
+
def save_state(raise_errors=false)
|
349
|
+
control(:savestate, raise_errors)
|
350
|
+
end
|
351
|
+
|
352
|
+
# Controls the virtual machine. This method is used by {#stop},
|
353
|
+
# {#pause}, {#resume}, and {#save_state} to control the virtual machine.
|
354
|
+
# Typically, you won't ever have to call this method and should
|
355
|
+
# instead call those.
|
356
|
+
#
|
357
|
+
# @param [String] command The command to run on controlvm
|
358
|
+
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
359
|
+
# will be raised if the command failed.
|
360
|
+
# @return [Boolean] True if command was successful, false otherwise.
|
361
|
+
def control(command, raise_errors=false)
|
362
|
+
Command.vboxmanage("controlvm #{@original_name} #{command}")
|
363
|
+
true
|
364
|
+
rescue Exceptions::CommandFailedException
|
365
|
+
raise if raise_errors
|
366
|
+
false
|
267
367
|
end
|
268
368
|
|
269
369
|
# Destroys the virtual machine. This method also removes all attached
|
data/lib/virtualbox.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
$:.unshift(File.expand_path(File.dirname(__FILE__)))
|
2
|
-
require 'virtualbox/
|
2
|
+
require 'virtualbox/exceptions'
|
3
3
|
require 'virtualbox/command'
|
4
4
|
require 'virtualbox/abstract_model'
|
5
|
+
require 'virtualbox/proxies/collection'
|
5
6
|
require 'virtualbox/image'
|
6
7
|
require 'virtualbox/attached_device'
|
7
8
|
require 'virtualbox/dvd'
|
@@ -43,6 +43,14 @@ class DirtyTest < Test::Unit::TestCase
|
|
43
43
|
assert !@model.changed?
|
44
44
|
end
|
45
45
|
|
46
|
+
should "be able to clear dirty state on entire model" do
|
47
|
+
@model.foo = "changed"
|
48
|
+
@model.bar = "changed"
|
49
|
+
assert @model.changed?
|
50
|
+
@model.clear_dirty!
|
51
|
+
assert !@model.changed?
|
52
|
+
end
|
53
|
+
|
46
54
|
should "show changes on specific field" do
|
47
55
|
assert !@model.changed?
|
48
56
|
@model.foo = "my value"
|
@@ -51,6 +59,11 @@ class DirtyTest < Test::Unit::TestCase
|
|
51
59
|
assert_equal "foo", @model.foo_was
|
52
60
|
end
|
53
61
|
|
62
|
+
should "return nil for field_was if its not changed" do
|
63
|
+
assert !@model.foo_changed?
|
64
|
+
assert_nil @model.foo_was
|
65
|
+
end
|
66
|
+
|
54
67
|
should "show changes for the whole model" do
|
55
68
|
assert !@model.changed?
|
56
69
|
@model.foo = "foo2"
|
@@ -62,5 +75,9 @@ class DirtyTest < Test::Unit::TestCase
|
|
62
75
|
assert_equal ["foo", "foo2"], changes[:foo]
|
63
76
|
assert_equal ["bar", "bar2"], changes[:bar]
|
64
77
|
end
|
78
|
+
|
79
|
+
should "still forward non-dirty magic methods up method_missing" do
|
80
|
+
assert_raises(NoMethodError) { @model.foobarbaz }
|
81
|
+
end
|
65
82
|
end
|
66
83
|
end
|
@@ -6,7 +6,10 @@ class RelatableTest < Test::Unit::TestCase
|
|
6
6
|
"FOO"
|
7
7
|
end
|
8
8
|
end
|
9
|
-
class BarRelatee
|
9
|
+
class BarRelatee
|
10
|
+
def self.set_relationship(caller, old_value, new_value)
|
11
|
+
end
|
12
|
+
end
|
10
13
|
|
11
14
|
class RelatableModel
|
12
15
|
include VirtualBox::AbstractModel::Relatable
|
@@ -19,6 +22,37 @@ class RelatableTest < Test::Unit::TestCase
|
|
19
22
|
@data = {}
|
20
23
|
end
|
21
24
|
|
25
|
+
context "setting a relationship" do
|
26
|
+
setup do
|
27
|
+
@model = RelatableModel.new
|
28
|
+
end
|
29
|
+
|
30
|
+
should "have a magic method relationship= which calls set_relationship" do
|
31
|
+
@model.expects(:set_relationship).with(:foos, "FOOS!")
|
32
|
+
@model.foos = "FOOS!"
|
33
|
+
end
|
34
|
+
|
35
|
+
should "raise a NonSettableRelationshipException if relationship can't be set" do
|
36
|
+
assert_raises(VirtualBox::Exceptions::NonSettableRelationshipException) {
|
37
|
+
@model.foos = "FOOS!"
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
should "call set_relationship on the relationship class" do
|
42
|
+
BarRelatee.expects(:populate_relationship).returns("foo")
|
43
|
+
@model.populate_relationships({})
|
44
|
+
|
45
|
+
BarRelatee.expects(:set_relationship).with(@model, "foo", "bars")
|
46
|
+
assert_nothing_raised { @model.bars = "bars" }
|
47
|
+
end
|
48
|
+
|
49
|
+
should "set the result of set_relationship as the new relationship data" do
|
50
|
+
BarRelatee.stubs(:set_relationship).returns("hello")
|
51
|
+
@model.bars = "zoo"
|
52
|
+
assert_equal "hello", @model.bars
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
22
56
|
context "subclasses" do
|
23
57
|
class SubRelatableModel < RelatableModel
|
24
58
|
relationship :bars, Relatee
|
@@ -122,6 +156,20 @@ class RelatableTest < Test::Unit::TestCase
|
|
122
156
|
end
|
123
157
|
end
|
124
158
|
|
159
|
+
context "checking for relationships" do
|
160
|
+
setup do
|
161
|
+
@model = RelatableModel.new
|
162
|
+
end
|
163
|
+
|
164
|
+
should "return true for existing relationships" do
|
165
|
+
assert @model.has_relationship?(:foos)
|
166
|
+
end
|
167
|
+
|
168
|
+
should "return false for nonexistent relationships" do
|
169
|
+
assert !@model.has_relationship?(:bazs)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
125
173
|
context "populating relationships" do
|
126
174
|
setup do
|
127
175
|
@model = RelatableModel.new
|
@@ -1,7 +1,12 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), '..', 'test_helper')
|
2
2
|
|
3
3
|
class AbstractModelTest < Test::Unit::TestCase
|
4
|
-
class Foo
|
4
|
+
class Foo
|
5
|
+
def self.set_relationship(caller, old_value, new_value)
|
6
|
+
new_value
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
5
10
|
class Bar; end
|
6
11
|
|
7
12
|
class FakeModel < VirtualBox::AbstractModel
|
@@ -30,6 +35,20 @@ class AbstractModelTest < Test::Unit::TestCase
|
|
30
35
|
@model.save
|
31
36
|
assert !@model.new_record?
|
32
37
|
end
|
38
|
+
|
39
|
+
should "become a new record again if new_record! is called" do
|
40
|
+
assert @model.new_record?
|
41
|
+
@model.save
|
42
|
+
assert !@model.new_record?
|
43
|
+
@model.new_record!
|
44
|
+
assert @model.new_record?
|
45
|
+
end
|
46
|
+
|
47
|
+
should "become an existing record if existing_record! is called" do
|
48
|
+
assert @model.new_record?
|
49
|
+
@model.existing_record!
|
50
|
+
assert !@model.new_record?
|
51
|
+
end
|
33
52
|
end
|
34
53
|
|
35
54
|
context "subclasses" do
|
@@ -120,6 +139,18 @@ class AbstractModelTest < Test::Unit::TestCase
|
|
120
139
|
end
|
121
140
|
end
|
122
141
|
|
142
|
+
context "integrating relatable" do
|
143
|
+
setup do
|
144
|
+
@model = FakeModel.new
|
145
|
+
end
|
146
|
+
|
147
|
+
should "set dirty state when a relationship is set" do
|
148
|
+
assert !@model.changed?
|
149
|
+
@model.foos = "foo"
|
150
|
+
assert @model.changed?
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
123
154
|
context "integrating attributable and dirty" do
|
124
155
|
setup do
|
125
156
|
@model = FakeModel.new
|