disposable 0.0.9 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|