virtualbox 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|