spotify 12.0.0 → 12.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,104 @@
1
+ # This file contains a tiny (!) DSL that wraps existing Spotify
2
+ # functions into versions that return Spotify::Pointers instead
3
+ # of the usual FFI::Pointers. The Spotify::Pointer automatically
4
+ # manages the underlying pointers reference count, which allows
5
+ # us to piggyback on the Ruby GC mechanism.
6
+
7
+ module Spotify
8
+ # Wraps the function `function` so that it always returns
9
+ # a Spotify::Pointer with correct refcount. Functions that
10
+ # contain the word `create` are assumed to start out with
11
+ # a refcount of `+1`.
12
+ #
13
+ # @note This method is removed at the bottom of this file.
14
+ #
15
+ # @param [#to_s] function
16
+ # @param [#to_s] return_type
17
+ # @raise [NoMethodError] if `function` is not defined
18
+ # @see Spotify::Pointer
19
+ def self.wrap_function(function, return_type)
20
+ method(function) # make sure it exists
21
+ define_singleton_method("#{function}!") do |*args, &block|
22
+ pointer = public_send(function, *args, &block)
23
+ Spotify::Pointer.new(pointer, return_type, function !~ /create/)
24
+ end
25
+ end
26
+
27
+ # @macro [attach] wrap_function
28
+ # Same as {Spotify}.`$1`, but wraps result in a {Spotify::Pointer}.
29
+ #
30
+ # @method $1!
31
+ # @return [Spotify::Pointer<$2>]
32
+ # @see #$1
33
+ wrap_function :session_user, :user
34
+ wrap_function :session_playlistcontainer, :playlistcontainer
35
+ wrap_function :session_inbox_create, :playlist
36
+ wrap_function :session_starred_create, :playlist
37
+ wrap_function :session_starred_for_user_create, :playlist
38
+ wrap_function :session_publishedcontainer_for_user_create, :playlistcontainer
39
+
40
+ wrap_function :track_artist, :artist
41
+ wrap_function :track_album, :album
42
+ wrap_function :localtrack_create, :track
43
+ wrap_function :track_get_playable, :track
44
+
45
+ wrap_function :album_artist, :artist
46
+
47
+ wrap_function :albumbrowse_create, :albumbrowse
48
+ wrap_function :albumbrowse_album, :album
49
+ wrap_function :albumbrowse_artist, :artist
50
+ wrap_function :albumbrowse_track, :track
51
+
52
+ wrap_function :artistbrowse_create, :artistbrowse
53
+ wrap_function :artistbrowse_artist, :artist
54
+ wrap_function :artistbrowse_track, :track
55
+ wrap_function :artistbrowse_album, :album
56
+ wrap_function :artistbrowse_similar_artist, :artist
57
+ wrap_function :artistbrowse_tophit_track, :track
58
+
59
+ wrap_function :image_create, :image
60
+ wrap_function :image_create_from_link, :image
61
+
62
+ wrap_function :link_as_track, :track
63
+ wrap_function :link_as_track_and_offset, :track
64
+ wrap_function :link_as_album, :album
65
+ wrap_function :link_as_artist, :artist
66
+ wrap_function :link_as_user, :user
67
+
68
+ wrap_function :link_create_from_string, :link
69
+ wrap_function :link_create_from_track, :link
70
+ wrap_function :link_create_from_album, :link
71
+ wrap_function :link_create_from_artist, :link
72
+ wrap_function :link_create_from_search, :link
73
+ wrap_function :link_create_from_playlist, :link
74
+ wrap_function :link_create_from_artist_portrait, :link
75
+ wrap_function :link_create_from_artistbrowse_portrait, :link
76
+ wrap_function :link_create_from_album_cover, :link
77
+ wrap_function :link_create_from_image, :link
78
+ wrap_function :link_create_from_user, :link
79
+
80
+ wrap_function :search_create, :search
81
+ wrap_function :search_track, :track
82
+ wrap_function :search_album, :album
83
+ wrap_function :search_artist, :artist
84
+
85
+ wrap_function :playlist_track, :track
86
+ wrap_function :playlist_track_creator, :user
87
+ wrap_function :playlist_owner, :user
88
+ wrap_function :playlist_create, :playlist
89
+
90
+ wrap_function :playlistcontainer_playlist, :playlist
91
+ wrap_function :playlistcontainer_add_new_playlist, :playlist
92
+ wrap_function :playlistcontainer_add_playlist, :playlist
93
+ wrap_function :playlistcontainer_owner, :user
94
+
95
+ wrap_function :toplistbrowse_create, :toplistbrowse
96
+ wrap_function :toplistbrowse_artist, :artist
97
+ wrap_function :toplistbrowse_album, :album
98
+ wrap_function :toplistbrowse_track, :track
99
+
100
+ wrap_function :inbox_post_tracks, :inbox
101
+
102
+ # Clean up
103
+ class << self; undef :wrap_function; end
104
+ end
@@ -0,0 +1,38 @@
1
+ module Spotify
2
+ # A custom data type for Spotify image IDs.
3
+ #
4
+ # It will convert strings to image ID pointers when handling
5
+ # values from Ruby to C, and it will convert pointers to Ruby
6
+ # strings when handling values from C to Ruby.
7
+ module ImageID
8
+ extend FFI::DataConverter
9
+ native_type FFI::Type::POINTER
10
+
11
+ # Given a string, convert it to an image ID pointer.
12
+ #
13
+ # @param [String] value image id as a string
14
+ # @param ctx
15
+ # @return [FFI::Pointer] pointer to the image ID
16
+ def self.to_native(value, ctx)
17
+ pointer = if value
18
+ if value.bytesize != 20
19
+ raise ArgumentError, "image id bytesize must be 20, was #{value.bytesize}"
20
+ end
21
+
22
+ pointer = FFI::MemoryPointer.new(:char, 20)
23
+ pointer.write_string(value.to_s)
24
+ end
25
+
26
+ super(pointer, ctx)
27
+ end
28
+
29
+ # Given a pointer, read a 20-byte image ID from it.
30
+ #
31
+ # @param [FFI::Pointer] value
32
+ # @param ctx
33
+ # @return [String, nil] the image ID as a string, or nil
34
+ def self.from_native(value, ctx)
35
+ value.read_string(20) unless value.null?
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,61 @@
1
+ require 'set'
2
+
3
+ module Spotify
4
+ # The Pointer is a kind of AutoPointer specially tailored for Spotify
5
+ # objects, that releases the raw pointer on GC.
6
+ class Pointer < FFI::AutoPointer
7
+ # Raised when #releaser_for is given an invalid type.
8
+ class InvalidTypeError < StandardError
9
+ end
10
+
11
+ class << self
12
+ # Create a proc that will accept a pointer of a given type and
13
+ # release it with the correct function if it’s not null.
14
+ #
15
+ # @raise [InvalidTypeError] when given an invalid type
16
+ # @param [Symbol] type
17
+ # @return [Proc]
18
+ def releaser_for(type)
19
+ unless Spotify.respond_to?(:"#{type}_release")
20
+ raise InvalidTypeError, "#{type} is not a valid Spotify type"
21
+ end
22
+
23
+ lambda do |pointer|
24
+ $stdout.puts "Spotify::#{type}_release(#{pointer})" if $DEBUG
25
+ Spotify.send(:"#{type}_release", pointer) unless pointer.null?
26
+ end
27
+ end
28
+
29
+ # Checks an object by pointer kind and type.
30
+ #
31
+ # @param [Object] object
32
+ # @param [Symbol] type
33
+ # @return [Boolean] true if object is a spotify pointer and of correct type
34
+ def typechecks?(object, type)
35
+ object.is_a?(Spotify::Pointer) && (object.type == type.to_s)
36
+ end
37
+ end
38
+
39
+ # @return [Symbol] type
40
+ attr_reader :type
41
+
42
+ # Initialize a Spotify pointer, which will automatically decrease
43
+ # the reference count of it’s pointer when garbage collected.
44
+ #
45
+ # @param [FFI::Pointer] pointer
46
+ # @param [#to_s] type session, link, etc
47
+ # @param [Boolean] add_ref will increase refcount by one if true
48
+ def initialize(pointer, type, add_ref)
49
+ super pointer, self.class.releaser_for(@type = type.to_s)
50
+
51
+ unless pointer.null?
52
+ Spotify.send(:"#{type}_add_ref", pointer)
53
+ end if add_ref
54
+ end
55
+
56
+ # @return [String] representation of the spotify pointer
57
+ def to_s
58
+ "<#{self.class} address=0x#{address.to_s(16)} type=#{type}>"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,45 @@
1
+ module Spotify
2
+ # A UTF-8 FFI type, making sure all strings are UTF8 in and out.
3
+ #
4
+ # Given an ingoing (ruby to C) string, it will make sure the string
5
+ # is in UTF-8 encoding. An outgoing (C to Ruby) will be assumed to
6
+ # actually be in UTF-8, and force-encoded as such.
7
+ module UTF8String
8
+ extend FFI::DataConverter
9
+ native_type FFI::Type::STRING
10
+
11
+ if "".respond_to?(:encode)
12
+ # Given a value, encodes it to UTF-8 no matter what.
13
+ #
14
+ # @note if the value is already in UTF-8, ruby does nothing
15
+ # @note if the given value is falsy, default behaviour is used
16
+ #
17
+ # @param [String] value
18
+ # @param ctx
19
+ # @return [String] value, but in UTF-8 if it wasn’t already
20
+ def self.to_native(value, ctx)
21
+ if value
22
+ value.encode('UTF-8')
23
+ else
24
+ super
25
+ end
26
+ end
27
+ end
28
+
29
+ if "".respond_to?(:force_encoding)
30
+ # Given an original string, assume it is in UTF-8.
31
+ #
32
+ # @note NO error checking is made, the string is just forced to UTF-8
33
+ # @param [String] value can be in any encoding
34
+ # @param ctx
35
+ # @return [String] value, but with UTF-8 encoding
36
+ def self.from_native(value, ctx)
37
+ if value
38
+ value.force_encoding('UTF-8')
39
+ else
40
+ super
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,4 +1,4 @@
1
1
  module Spotify
2
2
  # See README for versioning policy.
3
- VERSION = [12, 0, 0].join('.')
3
+ VERSION = [12, 0, 1].join('.')
4
4
  end
@@ -1,7 +1,10 @@
1
1
  # coding: utf-8
2
2
  require 'rubygems' # needed for 1.8, does not matter in 1.9
3
3
 
4
+ require 'ostruct'
5
+ require 'set'
4
6
  require 'rbgccxml'
7
+ require 'minitest/mock'
5
8
  require 'minitest/autorun'
6
9
 
7
10
  #
@@ -44,6 +47,19 @@ module C
44
47
  attach_function :strncpy, [ :pointer, :utf8_string, :size_t ], :utf8_string
45
48
  end
46
49
 
50
+ # Used for checking Spotify::Pointer things.
51
+ module Spotify
52
+ def bogus_add_ref(pointer)
53
+ end
54
+
55
+ def bogus_release(pointer)
56
+ end
57
+
58
+ # This may be called after our GC test. Randomly.
59
+ def garbage_release(pointer)
60
+ end
61
+ end
62
+
47
63
  #
48
64
  # Utility
49
65
  #
@@ -67,6 +83,16 @@ describe Spotify do
67
83
  end
68
84
  end
69
85
 
86
+ describe ".enum_value!" do
87
+ it "raises an error if given an invalid enum value" do
88
+ proc { Spotify.enum_value!(:moo, "error value") }.must_raise(ArgumentError)
89
+ end
90
+
91
+ it "gives back the enum value for that enum" do
92
+ Spotify.enum_value!(:ok, "error value").must_equal 0
93
+ end
94
+ end
95
+
70
96
  describe Spotify::SessionConfig do
71
97
  it "allows setting boolean values with bools" do
72
98
  subject = Spotify::SessionConfig.new
@@ -111,7 +137,129 @@ describe Spotify do
111
137
  end
112
138
  end
113
139
 
114
- describe "UTF8 string" do
140
+ describe "error wrapped functions" do
141
+ wrapped_methods = Spotify.attached_methods.find_all { |meth, info| info[:returns] == :error }
142
+ wrapped_methods.each do |meth, info|
143
+ it "raises an error if #{meth}! returns non-ok" do
144
+ Spotify.stub(meth, :bad_application_key) do
145
+ proc { Spotify.send("#{meth}!") }.must_raise(Spotify::Error, /BAD_APPLICATION_KEY/)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ describe "GC wrapped functions" do
152
+ gc_types = Set.new([:session, :track, :user, :playlistcontainer, :playlist, :link, :album, :artist, :search, :image, :albumbrowse, :artistbrowse, :toplistbrowse, :inbox])
153
+ wrapped_methods = Spotify.attached_methods.find_all { |meth, info| gc_types.member?(info[:returns]) }
154
+ wrapped_methods.each do |meth, info|
155
+ it "returns a Spotify::Pointer for #{meth}!" do
156
+ Spotify.stub(meth, lambda { FFI::Pointer.new(0) }) do
157
+ Spotify.send("#{meth}!").must_be_instance_of Spotify::Pointer
158
+ end
159
+ end
160
+ end
161
+
162
+ it "adds a ref to the pointer if required" do
163
+ session = FFI::Pointer.new(1)
164
+ ref_added = false
165
+
166
+ Spotify.stub(:session_user, FFI::Pointer.new(1)) do
167
+ Spotify.stub(:user_add_ref, proc { ref_added = true }) do
168
+ Spotify.session_user!(session)
169
+ end
170
+ end
171
+
172
+ ref_added.must_equal true
173
+ end
174
+
175
+ it "does not add a ref when the result is null" do
176
+ session = FFI::Pointer.new(1)
177
+ ref_added = false
178
+
179
+ Spotify.stub(:session_user, FFI::Pointer.new(0)) do
180
+ Spotify.stub(:user_add_ref, proc { ref_added = true }) do
181
+ Spotify.session_user!(session)
182
+ end
183
+ end
184
+
185
+ ref_added.must_equal false
186
+ end
187
+
188
+ it "does not add a ref when the result already has one" do
189
+ session = FFI::Pointer.new(1)
190
+ ref_added = false
191
+
192
+ Spotify.stub(:albumbrowse_create, FFI::Pointer.new(1)) do
193
+ Spotify.stub(:albumbrowse_add_ref, proc { ref_added = true }) do
194
+ # to avoid it trying to GC our fake pointer later, and cause a
195
+ # segfault in our tests
196
+ Spotify::Pointer.stub(:releaser_for, proc { proc {} }) do
197
+ Spotify.albumbrowse_create!(session)
198
+ end
199
+ end
200
+ end
201
+
202
+ ref_added.must_equal false
203
+ end
204
+ end
205
+
206
+ describe Spotify::Pointer do
207
+ describe ".new" do
208
+ it "adds a reference on the given pointer" do
209
+ ref_added = false
210
+
211
+ Spotify.stub(:bogus_add_ref, proc { ref_added = true }) do
212
+ Spotify::Pointer.new(FFI::Pointer.new(1), :bogus, true)
213
+ end
214
+
215
+ ref_added.must_equal true
216
+ end
217
+
218
+ it "does not add a reference on the given pointer if it is NULL" do
219
+ ref_added = false
220
+
221
+ Spotify.stub(:bogus_add_ref, proc { ref_added = true }) do
222
+ Spotify::Pointer.new(FFI::Pointer::NULL, :bogus, true)
223
+ end
224
+
225
+ ref_added.must_equal false
226
+ end
227
+
228
+ it "raises an error when given an invalid type" do
229
+ proc { Spotify::Pointer.new(FFI::Pointer.new(1), :really_bogus, true) }.
230
+ must_raise(Spotify::Pointer::InvalidTypeError, /invalid/)
231
+ end
232
+ end
233
+
234
+ describe ".typechecks?" do
235
+ it "typechecks a given spotify pointer" do
236
+ pointer = Spotify::Pointer.new(FFI::Pointer.new(1), :bogus, true)
237
+ bogus = OpenStruct.new(:type => :bogus)
238
+ Spotify::Pointer.typechecks?(bogus, :bogus).must_equal false
239
+ Spotify::Pointer.typechecks?(pointer, :link).must_equal false
240
+ Spotify::Pointer.typechecks?(pointer, :bogus).must_equal true
241
+ end
242
+ end
243
+
244
+ describe "garbage collection" do
245
+ let(:my_pointer) { FFI::Pointer.new(1) }
246
+
247
+ it "should work" do
248
+ gc_count = 0
249
+
250
+ Spotify.stub(:garbage_release, proc { gc_count += 1 }) do
251
+ 5.times { Spotify::Pointer.new(my_pointer, :garbage, false) }
252
+ 5.times { GC.start; sleep 0.01 }
253
+ end
254
+
255
+ # GC tests are a bit funky, but as long as we garbage_release at least once, then
256
+ # we can assume our GC works properly, but up the stakes just for the sake of it
257
+ gc_count.must_be :>, 3
258
+ end
259
+ end
260
+ end
261
+
262
+ describe Spotify::UTF8String do
115
263
  let(:char) do
116
264
  char = "\xC4"
117
265
  char.force_encoding('ISO-8859-1') if char.respond_to?(:force_encoding)
@@ -136,7 +284,7 @@ describe Spotify do
136
284
  end unless "".respond_to?(:force_encoding)
137
285
  end
138
286
 
139
- describe "Image ID" do
287
+ describe Spotify::ImageID do
140
288
  let(:context) { nil }
141
289
  let(:subject) { Spotify.find_type(:image_id) }
142
290
  let(:null_pointer) { FFI::Pointer::NULL }
@@ -27,5 +27,5 @@ Gem::Specification.new do |gem|
27
27
  gem.add_development_dependency 'rake'
28
28
  gem.add_development_dependency 'yard'
29
29
  gem.add_development_dependency 'rbgccxml'
30
- gem.add_development_dependency 'minitest', '~> 2.0'
30
+ gem.add_development_dependency 'minitest', '~> 3.0'
31
31
  end