bulldog 0.0.1

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.
Files changed (77) hide show
  1. data/.gitignore +2 -0
  2. data/DESCRIPTION.txt +3 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +64 -0
  6. data/VERSION +1 -0
  7. data/bulldog.gemspec +157 -0
  8. data/lib/bulldog.rb +95 -0
  9. data/lib/bulldog/attachment.rb +49 -0
  10. data/lib/bulldog/attachment/base.rb +167 -0
  11. data/lib/bulldog/attachment/has_dimensions.rb +94 -0
  12. data/lib/bulldog/attachment/image.rb +63 -0
  13. data/lib/bulldog/attachment/maybe.rb +229 -0
  14. data/lib/bulldog/attachment/none.rb +37 -0
  15. data/lib/bulldog/attachment/pdf.rb +63 -0
  16. data/lib/bulldog/attachment/unknown.rb +11 -0
  17. data/lib/bulldog/attachment/video.rb +143 -0
  18. data/lib/bulldog/error.rb +5 -0
  19. data/lib/bulldog/has_attachment.rb +214 -0
  20. data/lib/bulldog/interpolation.rb +73 -0
  21. data/lib/bulldog/missing_file.rb +12 -0
  22. data/lib/bulldog/processor.rb +5 -0
  23. data/lib/bulldog/processor/argument_tree.rb +116 -0
  24. data/lib/bulldog/processor/base.rb +124 -0
  25. data/lib/bulldog/processor/ffmpeg.rb +172 -0
  26. data/lib/bulldog/processor/image_magick.rb +134 -0
  27. data/lib/bulldog/processor/one_shot.rb +19 -0
  28. data/lib/bulldog/reflection.rb +234 -0
  29. data/lib/bulldog/saved_file.rb +19 -0
  30. data/lib/bulldog/stream.rb +186 -0
  31. data/lib/bulldog/style.rb +38 -0
  32. data/lib/bulldog/style_set.rb +101 -0
  33. data/lib/bulldog/tempfile.rb +28 -0
  34. data/lib/bulldog/util.rb +92 -0
  35. data/lib/bulldog/validations.rb +68 -0
  36. data/lib/bulldog/vector2.rb +18 -0
  37. data/rails/init.rb +9 -0
  38. data/script/console +8 -0
  39. data/spec/data/empty.txt +0 -0
  40. data/spec/data/test.jpg +0 -0
  41. data/spec/data/test.mov +0 -0
  42. data/spec/data/test.pdf +0 -0
  43. data/spec/data/test.png +0 -0
  44. data/spec/data/test2.jpg +0 -0
  45. data/spec/helpers/image_creation.rb +8 -0
  46. data/spec/helpers/temporary_directory.rb +25 -0
  47. data/spec/helpers/temporary_models.rb +76 -0
  48. data/spec/helpers/temporary_values.rb +102 -0
  49. data/spec/helpers/test_upload_files.rb +108 -0
  50. data/spec/helpers/time_travel.rb +20 -0
  51. data/spec/integration/data/test.jpg +0 -0
  52. data/spec/integration/lifecycle_hooks_spec.rb +213 -0
  53. data/spec/integration/processing_image_attachments.rb +72 -0
  54. data/spec/integration/processing_video_attachments_spec.rb +82 -0
  55. data/spec/integration/saving_an_attachment_spec.rb +31 -0
  56. data/spec/matchers/file_operations.rb +159 -0
  57. data/spec/spec_helper.rb +76 -0
  58. data/spec/unit/attachment/base_spec.rb +311 -0
  59. data/spec/unit/attachment/image_spec.rb +128 -0
  60. data/spec/unit/attachment/maybe_spec.rb +126 -0
  61. data/spec/unit/attachment/pdf_spec.rb +137 -0
  62. data/spec/unit/attachment/video_spec.rb +176 -0
  63. data/spec/unit/attachment_spec.rb +61 -0
  64. data/spec/unit/has_attachment_spec.rb +700 -0
  65. data/spec/unit/interpolation_spec.rb +108 -0
  66. data/spec/unit/processor/argument_tree_spec.rb +159 -0
  67. data/spec/unit/processor/ffmpeg_spec.rb +467 -0
  68. data/spec/unit/processor/image_magick_spec.rb +260 -0
  69. data/spec/unit/processor/one_shot_spec.rb +70 -0
  70. data/spec/unit/reflection_spec.rb +338 -0
  71. data/spec/unit/stream_spec.rb +234 -0
  72. data/spec/unit/style_set_spec.rb +44 -0
  73. data/spec/unit/style_spec.rb +51 -0
  74. data/spec/unit/validations_spec.rb +491 -0
  75. data/spec/unit/vector2_spec.rb +27 -0
  76. data/tasks/bulldog_tasks.rake +4 -0
  77. metadata +193 -0
@@ -0,0 +1,128 @@
1
+ require 'spec_helper'
2
+
3
+ describe Attachment::Image do
4
+ use_model_class(:Thing,
5
+ :photo_file_name => :string,
6
+ :photo_width => :integer,
7
+ :photo_height => :integer,
8
+ :photo_aspect_ratio => :float,
9
+ :photo_dimensions => :string)
10
+
11
+ before do
12
+ Thing.has_attachment :photo do
13
+ style :double, :size => '80x60'
14
+ style :filled, :size => '60x60', :filled => true
15
+ style :unfilled, :size => '120x120'
16
+ default_style :double
17
+ end
18
+ @thing = Thing.new(:photo => test_image_file)
19
+ end
20
+
21
+ def run(command)
22
+ `#{command}`
23
+ $?.success? or
24
+ raise "command failed: #{command}"
25
+ end
26
+
27
+ describe "#dimensions" do
28
+ it "should return 1x1 if the style is missing" do
29
+ Thing.attachment_reflections[:photo].configure do
30
+ detect_type_by{:image}
31
+ end
32
+ @thing.save.should be_true
33
+ File.unlink(@thing.photo.path(:original))
34
+ @thing = Thing.find(@thing.id)
35
+ @thing.photo.is_a?(Attachment::Image) # sanity check
36
+ @thing.photo.stream.missing? # sanity check
37
+ @thing.photo.dimensions(:original).should == [1, 1]
38
+ end
39
+
40
+ it "should return the width and height of the default style if no style name is given" do
41
+ @thing.photo.dimensions.should == [80, 60]
42
+ end
43
+
44
+ it "should return the width and height of the given style" do
45
+ @thing.photo.dimensions(:original).should == [40, 30]
46
+ @thing.photo.dimensions(:double).should == [80, 60]
47
+ end
48
+
49
+ it "should return the calculated width according to style filledness" do
50
+ @thing.photo.dimensions(:filled).should == [60, 60]
51
+ @thing.photo.dimensions(:unfilled).should == [120, 90]
52
+ end
53
+
54
+ it "should honor the exif:Orientation header" do
55
+ path = create_image("#{temporary_directory}/test.jpg", :size => '40x30')
56
+ rotated_path = "#{temporary_directory}/rotated-test.jpg"
57
+ run "exif --create-exif --ifd=EXIF --tag=Orientation --set-value=4 --output=#{rotated_path} #{path}"
58
+ open(rotated_path) do |file|
59
+ @thing.photo = file
60
+ @thing.photo.dimensions(:original).should == [30, 40]
61
+ end
62
+ end
63
+
64
+ it "should only invoke identify once"
65
+ it "should log the result"
66
+ end
67
+
68
+ describe "#width" do
69
+ it "should return the width of the default style if no style name is given" do
70
+ @thing.photo.width.should == 80
71
+ end
72
+
73
+ it "should return the width of the given style" do
74
+ @thing.photo.width(:original).should == 40
75
+ @thing.photo.width(:double).should == 80
76
+ end
77
+ end
78
+
79
+ describe "#height" do
80
+ it "should return the height of the default style if no style name is given" do
81
+ @thing.photo.height.should == 60
82
+ end
83
+
84
+ it "should return the height of the given style" do
85
+ @thing.photo.height(:original).should == 30
86
+ @thing.photo.height(:double).should == 60
87
+ end
88
+ end
89
+
90
+ describe "#aspect_ratio" do
91
+ it "should return the aspect ratio of the default style if no style name is given" do
92
+ @thing.photo.aspect_ratio.should be_close(4.0/3, 1e-5)
93
+ end
94
+
95
+ it "should return the aspect ratio of the given style" do
96
+ @thing.photo.aspect_ratio(:original).should be_close(4.0/3, 1e-5)
97
+ @thing.photo.aspect_ratio(:filled).should be_close(1, 1e-5)
98
+ end
99
+ end
100
+
101
+ describe "storable attributes" do
102
+ it "should set the stored attributes on assignment" do
103
+ @thing.photo_width.should == 40
104
+ @thing.photo_height.should == 30
105
+ @thing.photo_aspect_ratio.should be_close(4.0/3, 1e-5)
106
+ @thing.photo_dimensions.should == '40x30'
107
+ end
108
+
109
+ describe "after roundtripping through the database" do
110
+ before do
111
+ @thing.save
112
+ @thing = Thing.find(@thing.id)
113
+ end
114
+
115
+ it "should restore the stored attributes" do
116
+ @thing.photo_width.should == 40
117
+ @thing.photo_height.should == 30
118
+ @thing.photo_aspect_ratio.should be_close(4.0/3, 1e-5)
119
+ @thing.photo_dimensions.should == '40x30'
120
+ end
121
+
122
+ it "should recalculate the dimensions correctly" do
123
+ @thing.photo.dimensions(:filled).should == [60, 60]
124
+ @thing.photo.dimensions(:unfilled).should == [120, 90]
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,126 @@
1
+ require 'spec_helper'
2
+
3
+ describe Attachment::Maybe do
4
+ use_model_class(:Thing, :photo_file_name => :string)
5
+
6
+ before do
7
+ spec = self
8
+ Thing.has_attachment :photo do
9
+ path "#{spec.temporary_directory}/photo.:style.:extension"
10
+ end
11
+ end
12
+
13
+ def configure_attachment(&block)
14
+ spec = self
15
+ Thing.has_attachment :photo do
16
+ instance_exec(spec, &block)
17
+ end
18
+ @thing = Thing.new
19
+ end
20
+
21
+ describe "#interpolate_path" do
22
+ it "should return the path that the given style name would be stored at" do
23
+ spec = self
24
+ configure_attachment do
25
+ path "#{spec.temporary_directory}/:attachment.:style.jpg"
26
+ end
27
+ @thing.photo.interpolate_path(:original).should == "#{temporary_directory}/photo.original.jpg"
28
+ end
29
+
30
+ it "should use the given interpolation parameters" do
31
+ spec = self
32
+ configure_attachment do
33
+ path "#{spec.temporary_directory}/:attachment.:style.jpg"
34
+ end
35
+ @thing.photo.interpolate_path(:original, :style => 'STYLE').should == "#{temporary_directory}/photo.STYLE.jpg"
36
+ end
37
+
38
+ it "should use the style's format attribute for the extension by default" do
39
+ spec = self
40
+ configure_attachment do
41
+ style :processed, :format => 'png'
42
+ path "#{spec.temporary_directory}/:attachment.:style.:extension"
43
+ end
44
+ @thing.photo.interpolate_path(:processed, :format => 'png').should == "#{temporary_directory}/photo.processed.png"
45
+ end
46
+
47
+ describe "for the original style" do
48
+ it "should support the :basename interpolation key if the basename is given" do
49
+ spec = self
50
+ configure_attachment do
51
+ path "#{spec.temporary_directory}/:attachment.:style/:basename"
52
+ end
53
+ @thing.photo.interpolate_path(:original, :basename => 'file.xyz').should == "#{temporary_directory}/photo.original/file.xyz"
54
+ end
55
+
56
+ it "should support the :extension interpolation key if the basename is given" do
57
+ spec = self
58
+ configure_attachment do
59
+ path "#{spec.temporary_directory}/:attachment.:style.:extension"
60
+ end
61
+ @thing.photo.interpolate_path(:original, :basename => 'file.xyz').should == "#{temporary_directory}/photo.original.xyz"
62
+ end
63
+
64
+ it "should support the :extension interpolation key if the extension is given" do
65
+ spec = self
66
+ configure_attachment do
67
+ path "#{spec.temporary_directory}/:attachment.:style.:extension"
68
+ end
69
+ @thing.photo.interpolate_path(:original, :extension => 'xyz').should == "#{temporary_directory}/photo.original.xyz"
70
+ end
71
+ end
72
+ end
73
+
74
+ describe "#interpolate_url" do
75
+ it "should return the url that the given style name would be found at" do
76
+ spec = self
77
+ configure_attachment do
78
+ url "/:attachment.:style.jpg"
79
+ end
80
+ @thing.photo.interpolate_url(:original).should == "/photo.original.jpg"
81
+ end
82
+
83
+ it "should use the given interpolation parameters" do
84
+ spec = self
85
+ configure_attachment do
86
+ url "/:attachment.:style.jpg"
87
+ end
88
+ @thing.photo.interpolate_url(:original, :style => 'STYLE').should == "/photo.STYLE.jpg"
89
+ end
90
+
91
+ it "should use the style's format attribute for the extension by default" do
92
+ spec = self
93
+ configure_attachment do
94
+ style :processed, :format => 'png'
95
+ url "/:attachment.:style.:extension"
96
+ end
97
+ @thing.photo.interpolate_url(:processed).should == "/photo.processed.png"
98
+ end
99
+
100
+ describe "for the original style" do
101
+ it "should support the :basename interpolation key if the basename is given" do
102
+ spec = self
103
+ configure_attachment do
104
+ url "/:attachment.:style/:basename"
105
+ end
106
+ @thing.photo.interpolate_url(:original, :basename => 'file.xyz').should == "/photo.original/file.xyz"
107
+ end
108
+
109
+ it "should support the :extension interpolation key if the basename is given" do
110
+ spec = self
111
+ configure_attachment do
112
+ url "/:attachment.:style.:extension"
113
+ end
114
+ @thing.photo.interpolate_url(:original, :basename => 'file.xyz').should == "/photo.original.xyz"
115
+ end
116
+
117
+ it "should support the :extension interpolation key if the extension is given" do
118
+ spec = self
119
+ configure_attachment do
120
+ url "/:attachment.:style.:extension"
121
+ end
122
+ @thing.photo.interpolate_url(:original, :extension => 'xyz').should == "/photo.original.xyz"
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,137 @@
1
+ require 'spec_helper'
2
+
3
+ describe Attachment::Pdf do
4
+ use_model_class(:Thing,
5
+ :attachment_file_name => :string,
6
+ :attachment_width => :integer,
7
+ :attachment_height => :integer,
8
+ :attachment_aspect_ratio => :float,
9
+ :attachment_dimensions => :string)
10
+
11
+ before do
12
+ Thing.has_attachment :attachment do
13
+ style :double, :size => '1224x1584'
14
+ style :filled, :size => '500x500', :filled => true
15
+ style :unfilled, :size => '1000x1000'
16
+ default_style :double
17
+ end
18
+ @thing = Thing.new(:attachment => test_file)
19
+ end
20
+
21
+ def test_file
22
+ path = "#{temporary_directory}/test.pdf"
23
+ FileUtils.cp("#{ROOT}/spec/data/test.pdf", path)
24
+ autoclose open(path)
25
+ end
26
+
27
+ def configure(&block)
28
+ Thing.attachment_reflections[:attachment].configure(&block)
29
+ end
30
+
31
+ describe "#process" do
32
+ it "should be processed with ImageMagick by default" do
33
+ context = nil
34
+ configure do
35
+ style :output
36
+ process :on => :event do
37
+ context = self
38
+ end
39
+ end
40
+
41
+ @thing.attachment.process(:event)
42
+ context.should be_a(Processor::ImageMagick)
43
+ end
44
+ end
45
+
46
+ describe "#dimensions" do
47
+ it "should return 1x1 if the style is missing" do
48
+ Thing.attachment_reflections[:attachment].configure do
49
+ detect_type_by{:pdf}
50
+ end
51
+ @thing.save.should be_true
52
+ File.unlink(@thing.attachment.path(:original))
53
+ @thing = Thing.find(@thing.id)
54
+ @thing.attachment.is_a?(Attachment::Pdf) # sanity check
55
+ @thing.attachment.stream.missing? # sanity check
56
+ @thing.attachment.dimensions(:original).should == [1, 1]
57
+ end
58
+
59
+ it "should return the width and height of the default style if no style name is given" do
60
+ @thing.attachment.dimensions.should == [1224, 1584]
61
+ end
62
+
63
+ it "should return the width and height of the given style" do
64
+ @thing.attachment.dimensions(:original).should == [612, 792]
65
+ @thing.attachment.dimensions(:double).should == [1224, 1584]
66
+ end
67
+
68
+ it "should return the calculated width according to style filledness" do
69
+ @thing.attachment.dimensions(:filled).should == [500, 500]
70
+ @thing.attachment.dimensions(:unfilled).should == [773, 1000]
71
+ end
72
+
73
+ it "should only invoke identify once"
74
+ it "should log the result"
75
+ end
76
+
77
+ describe "#width" do
78
+ it "should return the width of the default style if no style name is given" do
79
+ @thing.attachment.width.should == 1224
80
+ end
81
+
82
+ it "should return the width of the given style" do
83
+ @thing.attachment.width(:original).should == 612
84
+ @thing.attachment.width(:double).should == 1224
85
+ end
86
+ end
87
+
88
+ describe "#height" do
89
+ it "should return the height of the default style if no style name is given" do
90
+ @thing.attachment.height.should == 1584
91
+ end
92
+
93
+ it "should return the height of the given style" do
94
+ @thing.attachment.height(:original).should == 792
95
+ @thing.attachment.height(:double).should == 1584
96
+ end
97
+ end
98
+
99
+ describe "#aspect_ratio" do
100
+ it "should return the aspect ratio of the default style if no style name is given" do
101
+ @thing.attachment.aspect_ratio.should be_close(612.0/792, 1e-5)
102
+ end
103
+
104
+ it "should return the aspect ratio of the given style" do
105
+ @thing.attachment.aspect_ratio(:original).should be_close(612.0/792, 1e-5)
106
+ @thing.attachment.aspect_ratio(:filled).should be_close(1, 1e-5)
107
+ end
108
+ end
109
+
110
+ describe "storable attributes" do
111
+ it "should set the stored attributes on assignment" do
112
+ @thing.attachment_width.should == 612
113
+ @thing.attachment_height.should == 792
114
+ @thing.attachment_aspect_ratio.should be_close(612.0/792, 1e-5)
115
+ @thing.attachment_dimensions.should == '612x792'
116
+ end
117
+
118
+ describe "after roundtripping through the database" do
119
+ before do
120
+ @thing.save
121
+ @thing = Thing.find(@thing.id)
122
+ end
123
+
124
+ it "should restore the stored attributes" do
125
+ @thing.attachment_width.should == 612
126
+ @thing.attachment_height.should == 792
127
+ @thing.attachment_aspect_ratio.should be_close(612.0/792, 1e-5)
128
+ @thing.attachment_dimensions.should == '612x792'
129
+ end
130
+
131
+ it "should recalculate the dimensions correctly" do
132
+ @thing.attachment.dimensions(:filled).should == [500, 500]
133
+ @thing.attachment.dimensions(:unfilled).should == [773, 1000]
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,176 @@
1
+ require 'spec_helper'
2
+
3
+ describe Attachment::Video do
4
+ use_model_class(:Thing,
5
+ :video_file_name => :string,
6
+ :video_width => :integer,
7
+ :video_height => :integer,
8
+ :video_aspect_ratio => :float,
9
+ :video_dimensions => :string,
10
+ :video_duration => :string)
11
+
12
+ before do
13
+ Thing.has_attachment :video do
14
+ # original video is 640x480.
15
+ style :half, :size => '320x240'
16
+ style :filled, :size => '60x60', :filled => true
17
+ style :unfilled, :size => '120x120'
18
+ default_style :half
19
+ end
20
+ @thing = Thing.new(:video => test_file)
21
+ end
22
+
23
+ def test_file
24
+ path = "#{temporary_directory}/test.mov"
25
+ FileUtils.cp("#{ROOT}/spec/data/test.mov", path)
26
+ autoclose open(path)
27
+ end
28
+
29
+ def run(command)
30
+ `#{command}`
31
+ $?.success? or
32
+ raise "command failed: #{command}"
33
+ end
34
+
35
+ describe "#dimensions" do
36
+ it "should return 2x2 if the style is missing" do
37
+ Thing.attachment_reflections[:video].configure do
38
+ detect_type_by{:video}
39
+ end
40
+ @thing.save.should be_true
41
+ File.unlink(@thing.video.path(:original))
42
+ @thing = Thing.find(@thing.id)
43
+ @thing.video.is_a?(Attachment::Video) # sanity check
44
+ @thing.video.stream.missing? # sanity check
45
+ @thing.video.dimensions(:original).should == [2, 2]
46
+ end
47
+
48
+ it "should return the width and height of the default style if no style name is given" do
49
+ @thing.video.dimensions.should == [320, 240]
50
+ end
51
+
52
+ it "should return the width and height of the given style" do
53
+ @thing.video.dimensions(:original).should == [640, 480]
54
+ @thing.video.dimensions(:half).should == [320, 240]
55
+ end
56
+
57
+ it "should return the calculated width according to style filledness" do
58
+ @thing.video.dimensions(:filled).should == [60, 60]
59
+ @thing.video.dimensions(:unfilled).should == [120, 90]
60
+ end
61
+
62
+ it "should round calculated dimensions down to the nearest multiple of 2" do
63
+ # TODO: ick!
64
+ Thing.attachment_reflections[:video].styles[:filled][:size] = '59x59'
65
+ @thing.video.dimensions(:filled).should == [58, 58]
66
+ end
67
+
68
+ it "should only invoke ffmpeg once"
69
+ it "should log the result"
70
+ end
71
+
72
+ describe "#width" do
73
+ it "should return the width of the default style if no style name is given" do
74
+ @thing.video.width.should == 320
75
+ end
76
+
77
+ it "should return the width of the given style" do
78
+ @thing.video.width(:original).should == 640
79
+ @thing.video.width(:half).should == 320
80
+ end
81
+ end
82
+
83
+ describe "#height" do
84
+ it "should return the height of the default style if no style name is given" do
85
+ @thing.video.height.should == 240
86
+ end
87
+
88
+ it "should return the height of the given style" do
89
+ @thing.video.height(:original).should == 480
90
+ @thing.video.height(:half).should == 240
91
+ end
92
+ end
93
+
94
+ describe "#aspect_ratio" do
95
+ it "should return the aspect ratio of the default style if no style name is given" do
96
+ @thing.video.aspect_ratio.should be_close(4.0/3, 1e-5)
97
+ end
98
+
99
+ it "should return the aspect ratio of the given style" do
100
+ @thing.video.aspect_ratio(:original).should be_close(4.0/3, 1e-5)
101
+ @thing.video.aspect_ratio(:filled).should be_close(1, 1e-5)
102
+ end
103
+ end
104
+
105
+ describe "#duration" do
106
+ it "should return the duration of the original style if no style name is given" do
107
+ @thing.video.duration.should == 1.second
108
+ end
109
+
110
+ it "should return the duration of the original style if a style name is given" do
111
+ @thing.video.duration(:filled).should == 1.second
112
+ end
113
+
114
+ # TODO: make these work instead of the above
115
+ it "should return the duration of the default style if no style name is given"
116
+ it "should return the duration of the given style"
117
+ end
118
+
119
+ describe "#video_tracks" do
120
+ it "should return the video tracks of the original style if no style name is given" do
121
+ @thing.video.video_tracks.should have(1).video_track
122
+ @thing.video.video_tracks.first.dimensions.should == [320, 240]
123
+ end
124
+
125
+ it "should return the video tracks of the target style if a style name is given" do
126
+ @thing.video.video_tracks(:original).should have(1).video_track
127
+ @thing.video.video_tracks(:original).first.dimensions.should == [640, 480]
128
+
129
+ @thing.video.video_tracks(:filled).should have(1).video_track
130
+ @thing.video.video_tracks(:filled).first.dimensions.should == [60, 60]
131
+ end
132
+ end
133
+
134
+ describe "#audio_tracks" do
135
+ it "should return the audio tracks of the original style if no style name is given" do
136
+ @thing.video.video_tracks.should have(1).video_track
137
+ @thing.video.video_tracks.first.dimensions.should == [320, 240]
138
+ end
139
+
140
+ it "should return the audio tracks of the target style if a style name is given" do
141
+ @thing.video.video_tracks(:original).should have(1).video_track
142
+ @thing.video.video_tracks(:original).first.dimensions.should == [640, 480]
143
+
144
+ @thing.video.video_tracks(:filled).should have(1).video_track
145
+ @thing.video.video_tracks(:filled).first.dimensions.should == [60, 60]
146
+ end
147
+ end
148
+
149
+ describe "storable attributes" do
150
+ it "should set the stored attributes on assignment" do
151
+ @thing.video_width.should == 640
152
+ @thing.video_height.should == 480
153
+ @thing.video_aspect_ratio.should be_close(4.0/3, 1e-5)
154
+ @thing.video_dimensions.should == '640x480'
155
+ end
156
+
157
+ describe "after roundtripping through the database" do
158
+ before do
159
+ @thing.save
160
+ @thing = Thing.find(@thing.id)
161
+ end
162
+
163
+ it "should restore the stored attributes" do
164
+ @thing.video_width.should == 640
165
+ @thing.video_height.should == 480
166
+ @thing.video_aspect_ratio.should be_close(4.0/3, 1e-5)
167
+ @thing.video_dimensions.should == '640x480'
168
+ end
169
+
170
+ it "should recalculate the dimensions correctly" do
171
+ @thing.video.dimensions(:filled).should == [60, 60]
172
+ @thing.video.dimensions(:unfilled).should == [120, 90]
173
+ end
174
+ end
175
+ end
176
+ end