disposable 0.0.9 → 0.1.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.
- checksums.yaml +4 -4
- data/.travis.yml +2 -5
- data/CHANGES.md +4 -0
- data/Gemfile +1 -1
- data/README.md +154 -1
- data/database.sqlite3 +0 -0
- data/disposable.gemspec +7 -7
- data/gemfiles/Gemfile.rails-3.0.lock +10 -8
- data/gemfiles/Gemfile.rails-3.2.lock +9 -7
- data/gemfiles/Gemfile.rails-4.0.lock +9 -7
- data/gemfiles/Gemfile.rails-4.1.lock +10 -8
- data/lib/disposable.rb +6 -7
- data/lib/disposable/callback.rb +174 -0
- data/lib/disposable/composition.rb +21 -58
- data/lib/disposable/expose.rb +49 -0
- data/lib/disposable/twin.rb +85 -38
- data/lib/disposable/twin/builder.rb +12 -30
- data/lib/disposable/twin/changed.rb +50 -0
- data/lib/disposable/twin/collection.rb +95 -0
- data/lib/disposable/twin/composition.rb +43 -15
- data/lib/disposable/twin/option.rb +1 -1
- data/lib/disposable/twin/persisted.rb +20 -0
- data/lib/disposable/twin/property_processor.rb +29 -0
- data/lib/disposable/twin/representer.rb +42 -14
- data/lib/disposable/twin/save.rb +19 -34
- data/lib/disposable/twin/schema.rb +31 -0
- data/lib/disposable/twin/setup.rb +38 -0
- data/lib/disposable/twin/sync.rb +114 -0
- data/lib/disposable/version.rb +1 -1
- data/test/api_semantics_test.rb +263 -0
- data/test/callback_group_test.rb +222 -0
- data/test/callbacks_test.rb +450 -0
- data/test/example.rb +40 -0
- data/test/expose_test.rb +92 -0
- data/test/persisted_test.rb +101 -0
- data/test/test_helper.rb +64 -0
- data/test/twin/benchmarking.rb +33 -0
- data/test/twin/builder_test.rb +32 -0
- data/test/twin/changed_test.rb +108 -0
- data/test/twin/collection_test.rb +223 -0
- data/test/twin/composition_test.rb +56 -25
- data/test/twin/expose_test.rb +73 -0
- data/test/twin/feature_test.rb +61 -0
- data/test/twin/from_test.rb +37 -0
- data/test/twin/inherit_test.rb +57 -0
- data/test/twin/option_test.rb +27 -0
- data/test/twin/readable_test.rb +57 -0
- data/test/twin/save_test.rb +192 -0
- data/test/twin/schema_test.rb +69 -0
- data/test/twin/setup_test.rb +139 -0
- data/test/twin/skip_unchanged_test.rb +64 -0
- data/test/twin/struct_test.rb +168 -0
- data/test/twin/sync_option_test.rb +228 -0
- data/test/twin/sync_test.rb +128 -0
- data/test/twin/twin_test.rb +49 -128
- data/test/twin/writeable_test.rb +56 -0
- metadata +106 -20
- data/STUFF +0 -4
- data/lib/disposable/twin/finders.rb +0 -29
- data/lib/disposable/twin/new.rb +0 -30
- data/lib/disposable/twin/save_.rb +0 -21
- data/test/composition_test.rb +0 -102
@@ -0,0 +1,64 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class SkipUnchangedTest < MiniTest::Spec
|
4
|
+
module Model
|
5
|
+
Song = Struct.new(:title, :composer)
|
6
|
+
Album = Struct.new(:name, :songs, :artist)
|
7
|
+
Artist = Struct.new(:name)
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
module Twin
|
12
|
+
class Album < Disposable::Twin
|
13
|
+
feature Setup
|
14
|
+
feature Sync
|
15
|
+
feature Sync::SkipUnchanged
|
16
|
+
|
17
|
+
property :name
|
18
|
+
|
19
|
+
collection :songs do
|
20
|
+
property :title
|
21
|
+
|
22
|
+
property :composer do
|
23
|
+
property :name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
property :artist do
|
28
|
+
property :name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
let (:song) { Model::Song.new() }
|
35
|
+
let (:composer) { Model::Artist.new(nil) }
|
36
|
+
let (:song_with_composer) { Model::Song.new("American Jesus", composer) }
|
37
|
+
let (:artist) { Model::Artist.new("Bad Religion") }
|
38
|
+
|
39
|
+
|
40
|
+
let (:album) { Model::Album.new("30 Years Live", [song, song_with_composer], artist) }
|
41
|
+
|
42
|
+
it do
|
43
|
+
twin = Twin::Album.new(album)
|
44
|
+
|
45
|
+
twin.songs[1].composer.name = "Greg Graffin"
|
46
|
+
twin.songs[0].title = "Resist Stance"
|
47
|
+
|
48
|
+
# raises exceptions when setters are called.
|
49
|
+
album.instance_eval { def name=; end }
|
50
|
+
artist.instance_eval { def name=; end }
|
51
|
+
song_with_composer.instance_eval { def title=; end }
|
52
|
+
|
53
|
+
twin.sync
|
54
|
+
|
55
|
+
# unchanged, and no exception raised.
|
56
|
+
album.name.must_equal "30 Years Live"
|
57
|
+
song_with_composer.title.must_equal "American Jesus"
|
58
|
+
artist.name.must_equal "Bad Religion"
|
59
|
+
|
60
|
+
# this actually got synced.
|
61
|
+
song_with_composer.composer.name.must_equal "Greg Graffin" # was nil.
|
62
|
+
song.title.must_equal "Resist Stance" # was nil.
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# require 'test_helper'
|
2
|
+
# require "representable/debug"
|
3
|
+
|
4
|
+
# require 'disposable/twin/struct'
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
# module Representable
|
9
|
+
# # The generic representer. Brings #to_hash and #from_hash to your object.
|
10
|
+
# # If you plan to write your own representer for a new media type, try to use this module (e.g., check how JSON reuses Hash's internal
|
11
|
+
# # architecture).
|
12
|
+
# module Object
|
13
|
+
# def self.included(base)
|
14
|
+
# base.class_eval do
|
15
|
+
# include Representable
|
16
|
+
# extend ClassMethods
|
17
|
+
# register_feature Representable::Object
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
|
21
|
+
|
22
|
+
# module ClassMethods
|
23
|
+
# def collection_representer_class
|
24
|
+
# Collection
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
|
28
|
+
# def from_object(data, options={}, binding_builder=Binding)
|
29
|
+
# update_properties_from(data, options, binding_builder)
|
30
|
+
# end
|
31
|
+
|
32
|
+
# # FIXME: remove me! only here to avoid AllowSymbols from Twin:Representer
|
33
|
+
# def update_properties_from(doc, options, format)
|
34
|
+
# representable_mapper(format, options).deserialize(doc, options)
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
# class TwinStructTest < MiniTest::Spec
|
44
|
+
# class Song < Disposable::Twin
|
45
|
+
# include Struct
|
46
|
+
# property :number, default: 1 # FIXME: this should be :default_if_nil so it becomes clear with a model.
|
47
|
+
# option :cool?
|
48
|
+
# end
|
49
|
+
|
50
|
+
# # empty hash
|
51
|
+
# it { Song.new({}).number.must_equal 1 }
|
52
|
+
# # model hash
|
53
|
+
# it { Song.new(number: 2).number.must_equal 2 }
|
54
|
+
|
55
|
+
# # with hash and options as one hash.
|
56
|
+
# it { Song.new(number: 3, cool?: true).cool?.must_equal true }
|
57
|
+
# it { Song.new(number: 3, cool?: true).number.must_equal 3 }
|
58
|
+
|
59
|
+
# # with model hash and options hash separated.
|
60
|
+
# it { Song.new({number: 3}, {cool?: true}).cool?.must_equal true }
|
61
|
+
# it { Song.new({number: 3}, {cool?: true}).number.must_equal 3 }
|
62
|
+
|
63
|
+
|
64
|
+
# describe "writing" do
|
65
|
+
# let (:song) { Song.new(model, {cool?: true}) }
|
66
|
+
# let (:model) { {number: 3} }
|
67
|
+
|
68
|
+
# # writer
|
69
|
+
# it do
|
70
|
+
# song.number = 9
|
71
|
+
# song.number.must_equal 9
|
72
|
+
# model[:number].must_equal 3
|
73
|
+
# end
|
74
|
+
|
75
|
+
# # writer with sync
|
76
|
+
# it do
|
77
|
+
# song.number = 9
|
78
|
+
# song.sync
|
79
|
+
|
80
|
+
# song.number.must_equal 9
|
81
|
+
# model["number"].must_equal 9
|
82
|
+
|
83
|
+
# song.send(:model).object_id.must_equal model.object_id
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
|
87
|
+
# end
|
88
|
+
|
89
|
+
|
90
|
+
# # Non-lazy initialization. This will copy all properties from the wrapped object to the twin when
|
91
|
+
# # instantiating the twin.
|
92
|
+
|
93
|
+
|
94
|
+
# class TwinWithNestedStructTest < MiniTest::Spec
|
95
|
+
# class Song < Disposable::Twin
|
96
|
+
# include Setup
|
97
|
+
# property :title
|
98
|
+
|
99
|
+
# property :options, twin: true do # don't call #to_hash, this is triggered in the twin's constructor.
|
100
|
+
# include Struct
|
101
|
+
# property :recorded
|
102
|
+
# property :released
|
103
|
+
|
104
|
+
# property :preferences, twin: true do
|
105
|
+
# include Struct
|
106
|
+
# property :show_image
|
107
|
+
# property :play_teaser
|
108
|
+
# end
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
|
112
|
+
# # FIXME: test with missing hash properties, e.g. without released and with released:false.
|
113
|
+
# let (:model) { OpenStruct.new(title: "Seed of Fear and Anger", options: {recorded: true, released: 1,
|
114
|
+
# preferences: {show_image: true, play_teaser: 2}}) }
|
115
|
+
|
116
|
+
# # public "hash" reader
|
117
|
+
# it { Song.new(model).options.recorded.must_equal true }
|
118
|
+
|
119
|
+
# # public "hash" writer
|
120
|
+
# it ("xxx") {
|
121
|
+
# song = Song.new(model)
|
122
|
+
|
123
|
+
# puts song.inspect
|
124
|
+
|
125
|
+
# # puts song.options.inspect
|
126
|
+
# # puts song.options.preferences.to_hash
|
127
|
+
# # raise
|
128
|
+
|
129
|
+
# song.options.recorded = "yo"
|
130
|
+
# song.options.recorded.must_equal "yo"
|
131
|
+
|
132
|
+
# song.options.preferences.show_image.must_equal true
|
133
|
+
# song.options.preferences.play_teaser.must_equal 2
|
134
|
+
|
135
|
+
# song.options.preferences.show_image= 9
|
136
|
+
|
137
|
+
|
138
|
+
# # song.extend(Disposable::Twin::Struct::Sync)
|
139
|
+
# song.sync # this is only called on the top model, e.g. in Reform#save.
|
140
|
+
|
141
|
+
# model.title.must_equal "Seed of Fear and Anger"
|
142
|
+
# model.options["recorded"].must_equal "yo"
|
143
|
+
# model.options["preferences"].must_equal({"show_image" => 9, "play_teaser"=>2})
|
144
|
+
# }
|
145
|
+
# end
|
146
|
+
|
147
|
+
|
148
|
+
|
149
|
+
# class SyncRepresenter < Representable::Decorator
|
150
|
+
# include Representable::Object
|
151
|
+
|
152
|
+
# property :title
|
153
|
+
# property :album, instance: lambda { |fragment, *| fragment } do
|
154
|
+
# property :name
|
155
|
+
# end
|
156
|
+
# end
|
157
|
+
|
158
|
+
# album = Struct.new(:name).new("Ass It Is")
|
159
|
+
|
160
|
+
# SyncRepresenter.new(obj = Struct.new(:title, :album).new).from_object(Struct.new(:title, :album).new("Eternal Scream", album))
|
161
|
+
|
162
|
+
# puts obj.title.inspect
|
163
|
+
# puts obj.inspect
|
164
|
+
# # reform
|
165
|
+
# # sync: twin.title = "Good Bye"
|
166
|
+
# # album.sync (copy attributes in nested form)
|
167
|
+
# # twin.name = "Matters"
|
168
|
+
# # save: twin.save (this will do twin.sync... does that call save on all nested twins, too, or do we still have to do that in reform?)
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# require 'test_helper'
|
2
|
+
# require "disposable/twin/changed"
|
3
|
+
|
4
|
+
# class SyncOptionTest < MiniTest::Spec
|
5
|
+
# module Model
|
6
|
+
# Song = Struct.new(:title, :composer)
|
7
|
+
# Album = Struct.new(:id, :name, :songs, :artist)
|
8
|
+
# Artist = Struct.new(:name, :hidden_taste)
|
9
|
+
# end
|
10
|
+
|
11
|
+
|
12
|
+
# module Twin
|
13
|
+
# class Album < Disposable::Twin
|
14
|
+
# feature Setup
|
15
|
+
# feature Sync
|
16
|
+
# # feature Changed
|
17
|
+
|
18
|
+
# property :id, sync: lambda { |value, options| nil }
|
19
|
+
# property :name, sync: lambda { |value, options| value == "Run For Cover" ? model.name= "processed+#{value}" : model.name= "#{value}+unprocessed" } # assign if album name is "Run For Cover".
|
20
|
+
|
21
|
+
# collection :songs ,
|
22
|
+
# sync: lambda { |value, options| } do # FIXME: this is called before the songs items where synced?
|
23
|
+
# property :title, sync: lambda { |value, options| value == "Empty Rooms" ? model.title= "++#{value}" : nil }
|
24
|
+
# end
|
25
|
+
|
26
|
+
# property :artist, sync: lambda { |twin, options| twin.model.hidden_taste = "Ska" } do
|
27
|
+
# property :name
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
# # sync does NOT call setter.
|
35
|
+
# describe ":sync allows you conditionals and is run in twin context" do
|
36
|
+
# let (:album) { Model::Album.new(1, "Corridors Of Power", [song, song_with_composer], artist) }
|
37
|
+
# let (:song) { Model::Song.new() }
|
38
|
+
# let (:song_with_composer) { Model::Song.new("American Jesus", composer) }
|
39
|
+
# let (:composer) { Model::Artist.new(nil) }
|
40
|
+
# let (:artist) { Model::Artist.new("Bad Religion") }
|
41
|
+
|
42
|
+
# let (:twin) { Twin::Album.new(album) }
|
43
|
+
|
44
|
+
# it do
|
45
|
+
# album.instance_exec { def id=(*); raise "don't call me!"; end }
|
46
|
+
# song.instance_exec { def title=(*); raise "don't call me!"; end }
|
47
|
+
|
48
|
+
# # triggers :sync.
|
49
|
+
# twin.name = "Run For Cover"
|
50
|
+
|
51
|
+
# twin.songs[1].title = "Empty Rooms" # song_with_composer. this will trigger the #title= writer.
|
52
|
+
# twin.artist.name = "Gary Moore"
|
53
|
+
|
54
|
+
# twin.sync # name is still "Corridors Of Power"
|
55
|
+
|
56
|
+
# # Album#id :sync was called, nothing happened.
|
57
|
+
# album.id.must_equal 1
|
58
|
+
# # Album#name :sync was called, first case.
|
59
|
+
# album.name.must_equal "processed+Run For Cover"
|
60
|
+
# # Song#title :sync called but returns nil.
|
61
|
+
# song.title.must_equal nil
|
62
|
+
# # Song#title :sync called and processed, first case.
|
63
|
+
# song_with_composer.title.must_equal "++Empty Rooms"
|
64
|
+
|
65
|
+
# # Artist#name :sync was called.
|
66
|
+
# artist.name.must_equal "Gary Moore"
|
67
|
+
# # Album#artist :sync was called.
|
68
|
+
# artist.hidden_taste.must_equal "Ska"
|
69
|
+
# end
|
70
|
+
|
71
|
+
# # trigger second case of Album#name to make sure conditionals work.
|
72
|
+
# it do
|
73
|
+
# twin.name = "This is not: Run For Cover" # triggers :sync, second case.
|
74
|
+
# twin.sync
|
75
|
+
# album.name.must_equal "This is not: Run For Cover+unprocessed"
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
|
80
|
+
|
81
|
+
# class SyncWithDynamicOptionsTest < MiniTest::Spec
|
82
|
+
# module Model
|
83
|
+
# Song = Struct.new(:title, :composer)
|
84
|
+
# Album = Struct.new(:id, :name, :songs, :artist)
|
85
|
+
# Artist = Struct.new(:name, :hidden_taste)
|
86
|
+
# end
|
87
|
+
|
88
|
+
|
89
|
+
# module Twin
|
90
|
+
# class Album < Disposable::Twin
|
91
|
+
# feature Setup
|
92
|
+
# feature Sync
|
93
|
+
# # feature Changed
|
94
|
+
|
95
|
+
# property :id, sync: true
|
96
|
+
# property :name, sync: true
|
97
|
+
|
98
|
+
# collection :songs ,
|
99
|
+
# sync: lambda { |value, options| } do # FIXME: this is called before the songs items where synced?
|
100
|
+
# property :title, sync: lambda { |value, options| value == "Empty Rooms" ? model.title= "++#{value}" : nil }
|
101
|
+
# end
|
102
|
+
|
103
|
+
# property :artist, sync: true do
|
104
|
+
# property :name
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
|
109
|
+
|
110
|
+
# # sync does NOT call setter.
|
111
|
+
# describe ":sync allows you conditionals and is run in twin context" do
|
112
|
+
# let (:album) { Model::Album.new(1, "Corridors Of Power", [song, song_with_composer], artist) }
|
113
|
+
# let (:song) { Model::Song.new() }
|
114
|
+
# let (:song_with_composer) { Model::Song.new("American Jesus", composer) }
|
115
|
+
# let (:composer) { Model::Artist.new(nil) }
|
116
|
+
# let (:artist) { Model::Artist.new("Bad Religion") }
|
117
|
+
|
118
|
+
# let (:twin) { Twin::Album.new(album) }
|
119
|
+
|
120
|
+
# it do
|
121
|
+
# album.instance_exec { def id=(*); raise "don't call me!"; end }
|
122
|
+
# song.instance_exec { def title=(*); raise "don't call me!"; end }
|
123
|
+
|
124
|
+
# # triggers :sync.
|
125
|
+
# twin.name = "Run For Cover"
|
126
|
+
|
127
|
+
# twin.songs[1].title = "Empty Rooms" # song_with_composer. this will trigger the #title= writer.
|
128
|
+
# twin.artist.name = "Gary Moore"
|
129
|
+
|
130
|
+
# twin.sync(
|
131
|
+
# id: lambda { |value, options| nil },
|
132
|
+
# name: lambda { |value, options| options.user_options[:twin].model.name= "processed+#{value}" },
|
133
|
+
|
134
|
+
# artist: lambda { |twin, options| twin.model.hidden_taste = "Ska" },
|
135
|
+
# )
|
136
|
+
|
137
|
+
# # Album#id :sync was called, nothing happened.
|
138
|
+
# album.id.must_equal 1
|
139
|
+
# # Album#name :sync was called, first case.
|
140
|
+
# album.name.must_equal "processed+Run For Cover"
|
141
|
+
# # Song#title :sync called but returns nil.
|
142
|
+
# song.title.must_equal nil
|
143
|
+
# # Song#title :sync called and processed, first case.
|
144
|
+
# #### song_with_composer.title.must_equal "++Empty Rooms"
|
145
|
+
|
146
|
+
# # Artist#name :sync was called.
|
147
|
+
# artist.name.must_equal "Gary Moore"
|
148
|
+
# # Album#artist :sync was called.
|
149
|
+
# artist.hidden_taste.must_equal "Ska"
|
150
|
+
# end
|
151
|
+
# end
|
152
|
+
# end
|
153
|
+
|
154
|
+
|
155
|
+
# class SyncWithOptionsAndSkipUnchangedTest < MiniTest::Spec
|
156
|
+
# module Model
|
157
|
+
# Song = Struct.new(:title, :composer)
|
158
|
+
# Album = Struct.new(:id, :name, :songs, :artist)
|
159
|
+
# Artist = Struct.new(:name)
|
160
|
+
# end
|
161
|
+
|
162
|
+
|
163
|
+
# module Twin
|
164
|
+
# class Album < Disposable::Twin
|
165
|
+
# feature Setup
|
166
|
+
# feature Sync
|
167
|
+
# feature Sync::SkipUnchanged
|
168
|
+
|
169
|
+
# property :id
|
170
|
+
# property :name
|
171
|
+
|
172
|
+
# collection :songs do # FIXME: this is called before the songs items where synced?
|
173
|
+
# property :title
|
174
|
+
# end
|
175
|
+
|
176
|
+
# # only execute this when changed.
|
177
|
+
# property :artist, sync: lambda { |artist_twin, options| model.artist.name = "#{artist_twin.name}+" } do
|
178
|
+
# property :name
|
179
|
+
# end
|
180
|
+
# end
|
181
|
+
# end
|
182
|
+
|
183
|
+
# let (:album) { Model::Album.new(1, "Corridors Of Power", [], artist) }
|
184
|
+
# # let (:song) { Model::Song.new() }
|
185
|
+
# # let (:song_with_composer) { Model::Song.new("American Jesus", composer) }
|
186
|
+
# # let (:composer) { Model::Artist.new(nil) }
|
187
|
+
# let (:artist) { Model::Artist.new("Bad Religion") }
|
188
|
+
|
189
|
+
# it do
|
190
|
+
# twin = Twin::Album.new(album)
|
191
|
+
|
192
|
+
# twin.artist.name.must_equal "Bad Religion"
|
193
|
+
# twin.sync
|
194
|
+
# twin.artist.name.must_equal "Bad Religion"
|
195
|
+
|
196
|
+
# twin.artist.name= "Greg Howe"
|
197
|
+
# twin.sync
|
198
|
+
# artist.name.must_equal "Greg Howe+"
|
199
|
+
# end
|
200
|
+
# end
|
201
|
+
|
202
|
+
# # :virtual wins over :sync
|
203
|
+
# # class SyncWithVirtualTest < MiniTest::Spec
|
204
|
+
# # Song = Struct.new(:title, :image, :band)
|
205
|
+
# # Band = Struct.new(:name)
|
206
|
+
|
207
|
+
# # let (:form) { HitForm.new(song) }
|
208
|
+
# # let (:song) { Song.new("Injection", Object, Band.new("Rise Against")) }
|
209
|
+
|
210
|
+
# # class HitForm < Disposable::Twin
|
211
|
+
# # include Sync::SkipUnchanged
|
212
|
+
# # register_feature Sync::SkipUnchanged
|
213
|
+
|
214
|
+
# # property :image, sync: lambda { |value, *| model.image = "processed via :sync: #{value}" }
|
215
|
+
# # property :band do
|
216
|
+
# # property :name, sync: lambda { |value, *| model.name = "band, processed: #{value}" }, virtual: true
|
217
|
+
# # end
|
218
|
+
# # end
|
219
|
+
|
220
|
+
# # it "abc" do
|
221
|
+
# # form.validate("image" => "Funny photo of Steve Harris", "band" => {"name" => "Iron Maiden"}).must_equal true
|
222
|
+
|
223
|
+
# # form.sync
|
224
|
+
# # song.image.must_equal "processed via :sync: Funny photo of Steve Harris"
|
225
|
+
# # song.band.name.must_equal "Rise Against"
|
226
|
+
# # end
|
227
|
+
# # end
|
228
|
+
|