Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add Ruby SDK tests and tooling

+1170 -2
+9
sdk/ruby/README.md
··· 1 1 # rockbox 2 2 3 + [![Gem Version](https://img.shields.io/gem/v/rockbox?style=flat-square&logo=rubygems&logoColor=white&color=E9573F)](https://rubygems.org/gems/rockbox) 4 + [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-CC342D?style=flat-square&logo=ruby&logoColor=white)](https://www.ruby-lang.org/) 5 + [![Downloads](https://img.shields.io/gem/dt/rockbox?style=flat-square&logo=rubygems&logoColor=white&color=blue)](https://rubygems.org/gems/rockbox) 6 + [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](./LICENSE) 7 + [![GraphQL](https://img.shields.io/badge/GraphQL-client-E10098?style=flat-square&logo=graphql&logoColor=white)](https://graphql.org/) 8 + [![WebSocket](https://img.shields.io/badge/WebSocket-realtime-4353FF?style=flat-square&logo=socketdotio&logoColor=white)](https://github.com/enhancv/websocket-client-simple) 9 + [![Plugins](https://img.shields.io/badge/plugins-extensible-8A2BE2?style=flat-square&logo=rubygems&logoColor=white)](#plugin-system) 10 + [![GitHub](https://img.shields.io/badge/github-rockbox--zig-181717?style=flat-square&logo=github&logoColor=white)](https://github.com/tsirysndr/rockbox-zig) 11 + 3 12 Ruby SDK for [Rockbox Zig](https://github.com/tsirysndr/rockbox-zig) — a builder-friendly, block-friendly GraphQL client with real-time event subscriptions and a plugin system. 4 13 5 14 ```ruby
+12
sdk/ruby/Rakefile
··· 1 + # frozen_string_literal: true 2 + 3 + require "rake/testtask" 4 + 5 + Rake::TestTask.new(:test) do |t| 6 + t.libs << "lib" 7 + t.libs << "test" 8 + t.test_files = FileList["test/**/*_test.rb"] 9 + t.warning = false 10 + end 11 + 12 + task default: :test
+1 -1
sdk/ruby/rockbox.gemspec
··· 30 30 31 31 spec.add_dependency "websocket-client-simple", "~> 0.6" 32 32 33 - spec.add_development_dependency "bundler", "~> 2.0" 33 + spec.add_development_dependency "bundler", "~> 4.0" 34 34 spec.add_development_dependency "minitest", "~> 5.0" 35 35 spec.add_development_dependency "rake", "~> 13.0" 36 36 end
+52
sdk/ruby/test/api/browse_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "../test_helper" 4 + 5 + class BrowseApiTest < Minitest::Test 6 + def setup 7 + @http = Rockbox::FakeHttp.new 8 + @browse = Rockbox::Api::Browse.new(@http) 9 + end 10 + 11 + def test_entries_returns_struct_array 12 + @http.will_return(tree_get_entries: [ 13 + { name: "Pink Floyd", attr: 0x10, time_write: 0, customaction: nil, display_name: "Pink Floyd" }, 14 + { name: "song.mp3", attr: 0x00, time_write: 0, customaction: nil, display_name: "song.mp3" } 15 + ]) 16 + entries = @browse.entries("/Music") 17 + assert_equal 2, entries.size 18 + assert_kind_of Rockbox::Entry, entries.first 19 + end 20 + 21 + def test_entries_with_nil_path_sends_no_variables 22 + @http.will_return(tree_get_entries: []) 23 + @browse.entries 24 + assert_nil @http.last_call.variables 25 + end 26 + 27 + def test_entries_with_path_sends_path_variable 28 + @http.will_return(tree_get_entries: []) 29 + @browse.entries("/Music") 30 + assert_equal({ path: "/Music" }, @http.last_call.variables) 31 + end 32 + 33 + def test_directories_filters_dir_attr_only 34 + @http.will_return(tree_get_entries: [ 35 + { name: "Pink Floyd", attr: 0x10, time_write: 0, customaction: nil, display_name: nil }, 36 + { name: "song.mp3", attr: 0x00, time_write: 0, customaction: nil, display_name: nil } 37 + ]) 38 + dirs = @browse.directories 39 + assert_equal 1, dirs.size 40 + assert_equal "Pink Floyd", dirs.first.name 41 + end 42 + 43 + def test_files_excludes_directories 44 + @http.will_return(tree_get_entries: [ 45 + { name: "Pink Floyd", attr: 0x10, time_write: 0, customaction: nil, display_name: nil }, 46 + { name: "song.mp3", attr: 0x00, time_write: 0, customaction: nil, display_name: nil } 47 + ]) 48 + files = @browse.files 49 + assert_equal 1, files.size 50 + assert_equal "song.mp3", files.first.name 51 + end 52 + end
+71
sdk/ruby/test/api/library_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "../test_helper" 4 + 5 + class LibraryApiTest < Minitest::Test 6 + def setup 7 + @http = Rockbox::FakeHttp.new 8 + @library = Rockbox::Api::Library.new(@http) 9 + end 10 + 11 + def test_albums_maps_to_album_structs 12 + @http.will_return(albums: [ 13 + { id: "a1", title: "Wish You Were Here" }, 14 + { id: "a2", title: "The Wall" } 15 + ]) 16 + albums = @library.albums 17 + assert_equal 2, albums.size 18 + assert_kind_of Rockbox::Album, albums.first 19 + assert_equal "Wish You Were Here", albums.first.title 20 + end 21 + 22 + def test_albums_with_nil_response 23 + @http.will_return(albums: nil) 24 + assert_equal [], @library.albums 25 + end 26 + 27 + def test_album_returns_nil_when_not_found 28 + @http.will_return(album: nil) 29 + assert_nil @library.album("missing") 30 + end 31 + 32 + def test_album_passes_id_variable 33 + @http.will_return(album: { id: "a1", title: "Wish You Were Here" }) 34 + @library.album("a1") 35 + assert_equal({ id: "a1" }, @http.last_call.variables) 36 + end 37 + 38 + def test_like_album_passes_id 39 + @library.like_album("a1") 40 + assert_equal({ id: "a1" }, @http.last_call.variables) 41 + end 42 + 43 + def test_search_assembles_search_results 44 + @http.will_return(search: { 45 + artists: [{ id: "ar1", name: "Pink Floyd" }], 46 + albums: [{ id: "al1", title: "Animals" }], 47 + tracks: [{ id: "t1", title: "Pigs" }], 48 + liked_tracks: [], 49 + liked_albums: [] 50 + }) 51 + results = @library.search("pink floyd") 52 + assert_kind_of Rockbox::SearchResults, results 53 + assert_equal "Pink Floyd", results.artists.first.name 54 + assert_equal "Animals", results.albums.first.title 55 + assert_equal "Pigs", results.tracks.first.title 56 + end 57 + 58 + def test_search_when_field_is_nil 59 + @http.will_return(search: nil) 60 + results = @library.search("x") 61 + assert_equal [], results.tracks 62 + assert_equal [], results.albums 63 + assert_equal [], results.artists 64 + end 65 + 66 + def test_scan_emits_no_variables 67 + @library.scan 68 + assert_nil @http.last_call.variables 69 + assert_match(/scanLibrary/, @http.last_call.query) 70 + end 71 + end
+66
sdk/ruby/test/api/playback_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "../test_helper" 4 + 5 + class PlaybackApiTest < Minitest::Test 6 + def setup 7 + @http = Rockbox::FakeHttp.new 8 + @playback = Rockbox::Api::Playback.new(@http) 9 + end 10 + 11 + def test_status_returns_integer 12 + @http.will_return(status: 1) 13 + assert_equal 1, @playback.status 14 + end 15 + 16 + def test_status_name_maps_through_playback_status 17 + @http.will_return(status: 3) 18 + assert_equal :paused, @playback.status_name 19 + end 20 + 21 + def test_current_track_builds_struct 22 + @http.will_return(current_track: { id: "t1", title: "Money", artist: "Pink Floyd" }) 23 + track = @playback.current_track 24 + assert_kind_of Rockbox::Track, track 25 + assert_equal "Money", track.title 26 + end 27 + 28 + def test_current_track_returns_nil_when_absent 29 + @http.will_return(current_track: nil) 30 + assert_nil @playback.current_track 31 + end 32 + 33 + def test_play_sends_elapsed_and_offset 34 + @playback.play(elapsed: 1000, offset: 50) 35 + assert_equal({ elapsed: 1000, offset: 50 }, @http.last_call.variables) 36 + end 37 + 38 + def test_play_album_compacts_nil_options 39 + @playback.play_album("alb_1") 40 + assert_equal({ album_id: "alb_1" }, @http.last_call.variables) 41 + end 42 + 43 + def test_play_album_includes_explicit_options 44 + @playback.play_album("alb_1", shuffle: true, position: 0) 45 + assert_equal({ album_id: "alb_1", shuffle: true, position: 0 }, 46 + @http.last_call.variables) 47 + end 48 + 49 + def test_play_album_keeps_false_value 50 + @playback.play_album("alb_1", shuffle: false) 51 + assert_equal({ album_id: "alb_1", shuffle: false }, @http.last_call.variables) 52 + end 53 + 54 + def test_seek_uses_new_time_variable 55 + @playback.seek(60_000) 56 + assert_equal({ new_time: 60_000 }, @http.last_call.variables) 57 + end 58 + 59 + def test_simple_transport_controls_send_no_variables 60 + %i[pause resume next! previous! stop flush_and_reload].each do |method| 61 + @http.calls.clear 62 + @playback.public_send(method) 63 + assert_nil @http.last_call.variables, "#{method} should not send variables" 64 + end 65 + end 66 + end
+92
sdk/ruby/test/api/playlist_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "../test_helper" 4 + 5 + class PlaylistApiTest < Minitest::Test 6 + def setup 7 + @http = Rockbox::FakeHttp.new 8 + @playlist = Rockbox::Api::Playlist.new(@http) 9 + end 10 + 11 + def test_current_builds_playlist_with_track_structs 12 + @http.will_return(playlist_get_current: { 13 + amount: 2, index: 0, max_playlist_size: 100, first_index: 0, 14 + last_insert_pos: 0, seed: 0, last_shuffled_start: 0, 15 + tracks: [ 16 + { id: "t1", title: "Money" }, 17 + { id: "t2", title: "Time" } 18 + ] 19 + }) 20 + pl = @playlist.current 21 + assert_kind_of Rockbox::Playlist, pl 22 + assert_equal 2, pl.amount 23 + assert_equal 2, pl.tracks.size 24 + assert_kind_of Rockbox::Track, pl.tracks.first 25 + assert_equal "Money", pl.tracks.first.title 26 + end 27 + 28 + def test_current_when_response_empty 29 + @http.will_return(playlist_get_current: nil) 30 + pl = @playlist.current 31 + assert_kind_of Rockbox::Playlist, pl 32 + assert_equal [], pl.tracks 33 + end 34 + 35 + def test_amount_returns_integer 36 + @http.will_return(playlist_amount: 42) 37 + assert_equal 42, @playlist.amount 38 + end 39 + 40 + def test_insert_tracks_default_position_is_NEXT 41 + @playlist.insert_tracks(["/a.mp3", "/b.mp3"]) 42 + assert_equal Rockbox::InsertPosition::NEXT, @http.last_call.variables[:position] 43 + assert_equal ["/a.mp3", "/b.mp3"], @http.last_call.variables[:tracks] 44 + assert_nil @http.last_call.variables[:playlist_id] 45 + end 46 + 47 + def test_insert_tracks_custom_position_and_playlist_id 48 + @playlist.insert_tracks(["/a.mp3"], 49 + position: Rockbox::InsertPosition::LAST, 50 + playlist_id: "pl_1") 51 + vars = @http.last_call.variables 52 + assert_equal Rockbox::InsertPosition::LAST, vars[:position] 53 + assert_equal "pl_1", vars[:playlist_id] 54 + end 55 + 56 + def test_insert_directory_default_is_LAST 57 + @playlist.insert_directory("/Music/Pink Floyd") 58 + assert_equal Rockbox::InsertPosition::LAST, @http.last_call.variables[:position] 59 + assert_equal "/Music/Pink Floyd", @http.last_call.variables[:directory] 60 + end 61 + 62 + def test_insert_album_passes_album_id_and_position 63 + @playlist.insert_album("alb_1") 64 + assert_equal "alb_1", @http.last_call.variables[:album_id] 65 + assert_equal Rockbox::InsertPosition::LAST, @http.last_call.variables[:position] 66 + end 67 + 68 + def test_remove_track_passes_index 69 + @playlist.remove_track(3) 70 + assert_equal({ index: 3 }, @http.last_call.variables) 71 + end 72 + 73 + def test_clear_sends_no_variables 74 + @playlist.clear 75 + assert_nil @http.last_call.variables 76 + end 77 + 78 + def test_create_passes_name_and_tracks 79 + @playlist.create("Tonight", ["/a.mp3", "/b.mp3"]) 80 + assert_equal({ name: "Tonight", tracks: ["/a.mp3", "/b.mp3"] }, @http.last_call.variables) 81 + end 82 + 83 + def test_start_compacts_nil_options 84 + @playlist.start 85 + assert_equal({}, @http.last_call.variables) 86 + end 87 + 88 + def test_start_includes_set_options_only 89 + @playlist.start(start_index: 0, elapsed: 1000) 90 + assert_equal({ start_index: 0, elapsed: 1000 }, @http.last_call.variables) 91 + end 92 + end
+72
sdk/ruby/test/api/settings_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "../test_helper" 4 + 5 + class SettingsApiTest < Minitest::Test 6 + def setup 7 + @http = Rockbox::FakeHttp.new 8 + @settings = Rockbox::Api::Settings.new(@http) 9 + end 10 + 11 + def test_get_builds_user_settings_struct 12 + @http.will_return(global_settings: { 13 + music_dir: "/Music", volume: -10, balance: 0, bass: 4, treble: 0, 14 + shuffle: true, player_name: "Living Room", 15 + eq_band_settings: [{ cutoff: 200, q: 1, gain: 3 }], 16 + replaygain_settings: { noclip: true, type: 1, preamp: 0 }, 17 + compressor_settings: nil 18 + }) 19 + s = @settings.get 20 + assert_kind_of Rockbox::UserSettings, s 21 + assert_equal "/Music", s.music_dir 22 + assert_equal(-10, s.volume) 23 + assert_equal "Living Room", s.player_name 24 + assert_equal 1, s.eq_band_settings.size 25 + assert_kind_of Rockbox::EqBandSetting, s.eq_band_settings.first 26 + assert_equal 200, s.eq_band_settings.first.cutoff 27 + assert_kind_of Rockbox::ReplaygainSettings, s.replaygain_settings 28 + assert_equal 1, s.replaygain_settings.type 29 + assert_nil s.compressor_settings 30 + end 31 + 32 + def test_get_when_global_settings_missing 33 + @http.will_return(global_settings: nil) 34 + s = @settings.get 35 + assert_kind_of Rockbox::UserSettings, s 36 + assert_nil s.volume 37 + assert_equal [], s.eq_band_settings 38 + end 39 + 40 + def test_save_with_hash 41 + @settings.save(volume: -10, shuffle: true) 42 + assert_equal({ settings: { volume: -10, shuffle: true } }, 43 + @http.last_call.variables) 44 + end 45 + 46 + def test_save_with_block_uses_set_attrs 47 + @settings.save do |s| 48 + s.volume = -10 49 + s.bass = 4 50 + s.shuffle = true 51 + end 52 + assert_equal({ settings: { volume: -10, bass: 4, shuffle: true } }, 53 + @http.last_call.variables) 54 + end 55 + 56 + def test_save_block_attrs_can_be_extended_with_hash 57 + @settings.save({ player_name: "Kitchen" }) do |s| 58 + s.volume = -5 59 + end 60 + vars = @http.last_call.variables 61 + assert_equal(-5, vars[:settings][:volume]) 62 + assert_equal "Kitchen", vars[:settings][:player_name] 63 + end 64 + 65 + def test_save_with_no_args_raises 66 + assert_raises(ArgumentError) { @settings.save } 67 + end 68 + 69 + def test_save_with_empty_block_raises 70 + assert_raises(ArgumentError) { @settings.save {} } 71 + end 72 + end
+37
sdk/ruby/test/api/sound_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "../test_helper" 4 + 5 + class SoundApiTest < Minitest::Test 6 + def setup 7 + @http = Rockbox::FakeHttp.new 8 + @sound = Rockbox::Api::Sound.new(@http) 9 + end 10 + 11 + def test_volume_returns_struct 12 + @http.will_return(volume: { volume: -10, min: -120, max: 12 }) 13 + info = @sound.volume 14 + assert_kind_of Rockbox::VolumeInfo, info 15 + assert_equal(-10, info.volume) 16 + assert_equal 12, info.max 17 + end 18 + 19 + def test_adjust_sends_steps_and_returns_resulting_volume 20 + @http.will_return(adjust_volume: -5) 21 + result = @sound.adjust(3) 22 + assert_equal(-5, result) 23 + assert_equal({ steps: 3 }, @http.last_call.variables) 24 + end 25 + 26 + def test_up_sends_plus_one 27 + @http.will_return(adjust_volume: 0) 28 + @sound.up 29 + assert_equal({ steps: 1 }, @http.last_call.variables) 30 + end 31 + 32 + def test_down_sends_minus_one 33 + @http.will_return(adjust_volume: 0) 34 + @sound.down 35 + assert_equal({ steps: -1 }, @http.last_call.variables) 36 + end 37 + end
+27
sdk/ruby/test/api/system_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "../test_helper" 4 + 5 + class SystemApiTest < Minitest::Test 6 + def setup 7 + @http = Rockbox::FakeHttp.new 8 + @system = Rockbox::Api::System.new(@http) 9 + end 10 + 11 + def test_version_returns_string 12 + @http.will_return(rockbox_version: "Rockbox v3.15") 13 + assert_equal "Rockbox v3.15", @system.version 14 + end 15 + 16 + def test_status_builds_struct 17 + @http.will_return(global_status: { 18 + resume_index: 0, resume_crc32: 0, resume_elapsed: 0, resume_offset: 0, 19 + runtime: 100, topruntime: 200, dircache_size: 0, 20 + last_screen: 0, viewer_icon_count: 0, last_volume_change: 0 21 + }) 22 + status = @system.status 23 + assert_kind_of Rockbox::SystemStatus, status 24 + assert_equal 100, status.runtime 25 + assert_equal 200, status.topruntime 26 + end 27 + end
+70
sdk/ruby/test/case_conversion_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "test_helper" 4 + 5 + class CaseConversionTest < Minitest::Test 6 + CC = Rockbox::CaseConversion 7 + 8 + def test_camelize_simple 9 + assert_equal "albumId", CC.camelize(:album_id) 10 + assert_equal "trackString", CC.camelize("track_string") 11 + assert_equal "id", CC.camelize("id") 12 + assert_equal "", CC.camelize("") 13 + end 14 + 15 + def test_camelize_leaves_single_word_lowercase 16 + assert_equal "title", CC.camelize("title") 17 + end 18 + 19 + def test_snakeize_simple 20 + assert_equal "album_id", CC.snakeize("albumId") 21 + assert_equal "track_string", CC.snakeize("trackString") 22 + assert_equal "id", CC.snakeize("id") 23 + end 24 + 25 + def test_snakeize_runs_of_caps 26 + # "trackHTTPUrl" -> the regex handles letter+CAP boundaries. 27 + assert_equal "track_http_url".tr("_", "_"), CC.snakeize("trackHttpUrl") 28 + end 29 + 30 + def test_deep_camelize_hash 31 + input = { album_id: "abc", artist_name: "Pink Floyd" } 32 + output = CC.deep_camelize(input) 33 + assert_equal({ "albumId" => "abc", "artistName" => "Pink Floyd" }, output) 34 + end 35 + 36 + def test_deep_camelize_nested 37 + input = { settings: { player_name: "Living Room", eq_band_settings: [{ q_value: 1 }] } } 38 + out = CC.deep_camelize(input) 39 + assert_equal "Living Room", out["settings"]["playerName"] 40 + assert_equal 1, out["settings"]["eqBandSettings"][0]["qValue"] 41 + end 42 + 43 + def test_deep_camelize_passes_through_scalars 44 + assert_equal 42, CC.deep_camelize(42) 45 + assert_equal "x", CC.deep_camelize("x") 46 + assert_nil CC.deep_camelize(nil) 47 + assert_equal true, CC.deep_camelize(true) 48 + end 49 + 50 + def test_deep_snakeize_returns_symbol_keys 51 + input = { "albumId" => "abc", "trackString" => "1/12" } 52 + out = CC.deep_snakeize(input) 53 + assert_equal "abc", out[:album_id] 54 + assert_equal "1/12", out[:track_string] 55 + end 56 + 57 + def test_deep_snakeize_nested_with_array 58 + input = { "tracks" => [{ "trackId" => "t1" }, { "trackId" => "t2" }] } 59 + out = CC.deep_snakeize(input) 60 + assert_equal "t1", out[:tracks][0][:track_id] 61 + assert_equal "t2", out[:tracks][1][:track_id] 62 + end 63 + 64 + def test_round_trip_preserves_structure 65 + original = { album_id: "x", tracks: [{ track_string: "1/12" }] } 66 + camelized = CC.deep_camelize(original) 67 + round_tripped = CC.deep_snakeize(camelized) 68 + assert_equal original, round_tripped 69 + end 70 + end
+118
sdk/ruby/test/client_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "test_helper" 4 + 5 + # Tests Client wiring: builder DSL, top-level shorthand, event delegation, 6 + # plugin lifecycle, and the raw #query escape hatch. We swap the HTTP 7 + # transport for a FakeHttp on the constructed client to keep things offline. 8 + class ClientTest < Minitest::Test 9 + def build_client 10 + client = Rockbox::Client.new(host: "localhost", port: 6062) 11 + fake = Rockbox::FakeHttp.new 12 + client.instance_variable_set(:@http, fake) 13 + [client, fake] 14 + end 15 + 16 + def test_default_namespaces_present 17 + client, = build_client 18 + %i[playback library playlist saved_playlists smart_playlists 19 + sound settings system browse devices bluetooth].each do |ns| 20 + refute_nil client.public_send(ns), "#{ns} should be present" 21 + end 22 + end 23 + 24 + def test_builder_dsl_yields_configuration 25 + client = Rockbox::Client.build do |c| 26 + c.host = "192.168.1.42" 27 + c.port = 7000 28 + end 29 + assert_equal "http://192.168.1.42:7000/graphql", client.configuration.resolved_http_url 30 + end 31 + 32 + def test_top_level_shorthand_with_kwargs 33 + client = Rockbox.new(host: "example.com") 34 + assert_kind_of Rockbox::Client, client 35 + assert_equal "example.com", client.configuration.host 36 + end 37 + 38 + def test_top_level_shorthand_with_block 39 + client = Rockbox.new do |c| 40 + c.host = "example.com" 41 + end 42 + assert_kind_of Rockbox::Client, client 43 + assert_equal "example.com", client.configuration.host 44 + end 45 + 46 + def test_event_delegation_to_emitter 47 + client, = build_client 48 + received = nil 49 + client.on(:track_changed) { |t| received = t } 50 + client.emit(:track_changed, "Money") 51 + assert_equal "Money", received 52 + end 53 + 54 + def test_once_via_client 55 + client, = build_client 56 + count = 0 57 + client.once(:tick) { count += 1 } 58 + client.emit(:tick) 59 + client.emit(:tick) 60 + assert_equal 1, count 61 + end 62 + 63 + def test_remove_all_listeners_via_client 64 + client, = build_client 65 + fired = false 66 + client.on(:tick) { fired = true } 67 + client.remove_all_listeners 68 + client.emit(:tick) 69 + refute fired 70 + end 71 + 72 + def test_raw_query_passes_through_http 73 + client, fake = build_client 74 + fake.will_return(some_field: 42) 75 + result = client.query("query Foo { foo }", { id: "abc" }) 76 + assert_equal({ some_field: 42 }, result) 77 + assert_equal "query Foo { foo }", fake.last_call.query 78 + assert_equal({ id: "abc" }, fake.last_call.variables) 79 + end 80 + 81 + # ---- plugin lifecycle --------------------------------------------------- 82 + 83 + class CapturingPlugin < Rockbox::Plugin 84 + attr_reader :ctx 85 + 86 + def name; "capture"; end 87 + def version; "0.0.1"; end 88 + 89 + def install(ctx) 90 + @ctx = ctx 91 + end 92 + end 93 + 94 + def test_use_installs_plugin_with_query_and_events 95 + client, fake = build_client 96 + plugin = CapturingPlugin.new 97 + client.use(plugin) 98 + assert_equal [plugin], client.installed_plugins 99 + 100 + # The query lambda routes through the same HTTP transport. 101 + fake.will_return(some_field: 1) 102 + plugin.ctx.query.call("query Foo { foo }") 103 + assert_equal "query Foo { foo }", fake.last_call.query 104 + 105 + # The events object lets plugins listen on the same emitter. 106 + received = nil 107 + plugin.ctx.events.on(:track_changed) { |t| received = t } 108 + client.emit(:track_changed, "Money") 109 + assert_equal "Money", received 110 + end 111 + 112 + def test_unuse_removes_plugin 113 + client, = build_client 114 + client.use(CapturingPlugin.new) 115 + client.unuse("capture") 116 + assert_empty client.installed_plugins 117 + end 118 + end
+43
sdk/ruby/test/configuration_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "test_helper" 4 + 5 + class ConfigurationTest < Minitest::Test 6 + def test_defaults 7 + config = Rockbox::Configuration.new 8 + assert_equal "localhost", config.resolved_host 9 + assert_equal 6062, config.resolved_port 10 + assert_equal "http://localhost:6062/graphql", config.resolved_http_url 11 + assert_equal "ws://localhost:6062/graphql", config.resolved_ws_url 12 + end 13 + 14 + def test_kwargs_override_defaults 15 + config = Rockbox::Configuration.new(host: "192.168.1.42", port: 7000) 16 + assert_equal "http://192.168.1.42:7000/graphql", config.resolved_http_url 17 + assert_equal "ws://192.168.1.42:7000/graphql", config.resolved_ws_url 18 + end 19 + 20 + def test_explicit_urls_take_precedence 21 + config = Rockbox::Configuration.new( 22 + host: "ignored", 23 + port: 1, 24 + http_url: "https://music.home/graphql", 25 + ws_url: "wss://music.home/graphql" 26 + ) 27 + assert_equal "https://music.home/graphql", config.resolved_http_url 28 + assert_equal "wss://music.home/graphql", config.resolved_ws_url 29 + end 30 + 31 + def test_attr_writer_after_construction 32 + config = Rockbox::Configuration.new 33 + config.host = "example.com" 34 + config.port = 9000 35 + assert_equal "http://example.com:9000/graphql", config.resolved_http_url 36 + end 37 + 38 + def test_timeouts_passthrough 39 + config = Rockbox::Configuration.new(open_timeout: 1, read_timeout: 2) 40 + assert_equal 1, config.open_timeout 41 + assert_equal 2, config.read_timeout 42 + end 43 + end
+37
sdk/ruby/test/errors_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "test_helper" 4 + 5 + class ErrorsTest < Minitest::Test 6 + def test_error_carries_cause 7 + cause = StandardError.new("inner") 8 + err = Rockbox::Error.new("outer", cause: cause) 9 + assert_equal "outer", err.message 10 + assert_same cause, err.cause 11 + end 12 + 13 + def test_network_error_is_an_error 14 + assert_kind_of Rockbox::Error, Rockbox::NetworkError.new("oops") 15 + end 16 + 17 + def test_graphql_error_concatenates_messages_with_symbol_keys 18 + err = Rockbox::GraphQLError.new([ 19 + { message: "first" }, 20 + { message: "second" } 21 + ]) 22 + assert_equal "first; second", err.message 23 + assert_equal 2, err.errors.size 24 + end 25 + 26 + def test_graphql_error_concatenates_messages_with_string_keys 27 + err = Rockbox::GraphQLError.new([ 28 + { "message" => "boom" } 29 + ]) 30 + assert_equal "boom", err.message 31 + end 32 + 33 + def test_graphql_error_handles_messageless_entries 34 + err = Rockbox::GraphQLError.new([{ message: "ok" }, { code: "X" }]) 35 + assert_equal "ok", err.message 36 + end 37 + end
+102
sdk/ruby/test/events_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "test_helper" 4 + 5 + class EventsTest < Minitest::Test 6 + def setup 7 + @emitter = Rockbox::EventEmitter.new 8 + end 9 + 10 + def test_on_and_emit_passes_payload 11 + received = nil 12 + @emitter.on(:track_changed) { |t| received = t } 13 + @emitter.emit(:track_changed, "Money") 14 + assert_equal "Money", received 15 + end 16 + 17 + def test_on_returns_self_for_chaining 18 + assert_same @emitter, @emitter.on(:x) {} 19 + end 20 + 21 + def test_on_requires_block 22 + assert_raises(ArgumentError) { @emitter.on(:x) } 23 + end 24 + 25 + def test_emit_with_zero_arity_listener 26 + fired = 0 27 + @emitter.on(:ws_open) { fired += 1 } 28 + @emitter.emit(:ws_open) 29 + assert_equal 1, fired 30 + end 31 + 32 + def test_emit_no_payload_calls_listener_without_args 33 + fired = false 34 + @emitter.on(:ws_open) { |_payload| fired = true } 35 + @emitter.emit(:ws_open) # nil payload 36 + # listener arity is 1 but payload is nil — implementation calls without args. 37 + assert_equal true, fired 38 + end 39 + 40 + def test_string_event_name_normalized_to_symbol 41 + received = nil 42 + @emitter.on("track_changed") { |t| received = t } 43 + @emitter.emit("track_changed", "ok") 44 + assert_equal "ok", received 45 + end 46 + 47 + def test_multiple_listeners_fire_in_order 48 + log = [] 49 + @emitter.on(:tick) { log << :a } 50 + @emitter.on(:tick) { log << :b } 51 + @emitter.emit(:tick) 52 + assert_equal %i[a b], log 53 + end 54 + 55 + def test_once_only_fires_once 56 + count = 0 57 + @emitter.once(:tick) { count += 1 } 58 + @emitter.emit(:tick) 59 + @emitter.emit(:tick) 60 + assert_equal 1, count 61 + end 62 + 63 + def test_off_with_proc_removes_specific_listener 64 + log = [] 65 + listener = ->(p) { log << p } 66 + @emitter.on(:tick, &listener) 67 + @emitter.on(:tick) { log << :other } 68 + @emitter.off(:tick, listener) 69 + @emitter.emit(:tick, :payload) 70 + assert_equal [:other], log 71 + end 72 + 73 + def test_off_with_no_target_removes_all_listeners_for_event 74 + fired = 0 75 + @emitter.on(:tick) { fired += 1 } 76 + @emitter.on(:tick) { fired += 1 } 77 + @emitter.off(:tick) 78 + @emitter.emit(:tick) 79 + assert_equal 0, fired 80 + end 81 + 82 + def test_remove_all_listeners_clears_everything 83 + fired = 0 84 + @emitter.on(:a) { fired += 1 } 85 + @emitter.on(:b) { fired += 1 } 86 + @emitter.remove_all_listeners 87 + @emitter.emit(:a) 88 + @emitter.emit(:b) 89 + assert_equal 0, fired 90 + end 91 + 92 + def test_remove_all_listeners_for_one_event 93 + a, b = 0, 0 94 + @emitter.on(:a) { a += 1 } 95 + @emitter.on(:b) { b += 1 } 96 + @emitter.remove_all_listeners(:a) 97 + @emitter.emit(:a) 98 + @emitter.emit(:b) 99 + assert_equal 0, a 100 + assert_equal 1, b 101 + end 102 + end
+85
sdk/ruby/test/plugin_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "test_helper" 4 + 5 + class PluginTest < Minitest::Test 6 + class TestPlugin < Rockbox::Plugin 7 + attr_reader :installed_with, :uninstalled 8 + 9 + def initialize(name = "test-plugin") 10 + super() 11 + @plugin_name = name 12 + @uninstalled = false 13 + end 14 + 15 + def name; @plugin_name; end 16 + def version; "1.0.0"; end 17 + 18 + def install(ctx) 19 + @installed_with = ctx 20 + end 21 + 22 + def uninstall 23 + @uninstalled = true 24 + end 25 + end 26 + 27 + def setup 28 + @registry = Rockbox::PluginRegistry.new 29 + @events = Rockbox::EventEmitter.new 30 + @ctx = Rockbox::PluginContext.new(query: ->(_, _ = nil) {}, events: @events) 31 + end 32 + 33 + def test_register_calls_install_with_context 34 + plugin = TestPlugin.new 35 + @registry.register(plugin, @ctx) 36 + assert_same @ctx, plugin.installed_with 37 + assert @registry.installed?("test-plugin") 38 + end 39 + 40 + def test_double_register_raises 41 + @registry.register(TestPlugin.new, @ctx) 42 + assert_raises(ArgumentError) { @registry.register(TestPlugin.new, @ctx) } 43 + end 44 + 45 + def test_unregister_calls_uninstall 46 + plugin = TestPlugin.new 47 + @registry.register(plugin, @ctx) 48 + @registry.unregister("test-plugin") 49 + assert plugin.uninstalled 50 + refute @registry.installed?("test-plugin") 51 + end 52 + 53 + def test_unregister_unknown_is_a_noop 54 + assert_nil @registry.unregister("nope") 55 + end 56 + 57 + def test_list_returns_registered_plugins 58 + a = TestPlugin.new("a") 59 + b = TestPlugin.new("b") 60 + @registry.register(a, @ctx) 61 + @registry.register(b, @ctx) 62 + assert_equal %w[a b], @registry.list.map(&:name).sort 63 + end 64 + 65 + def test_plugin_base_class_defaults 66 + base = Rockbox::Plugin.new 67 + assert_raises(NotImplementedError) { base.name } 68 + assert_equal "0.0.0", base.version 69 + assert_nil base.description 70 + assert_nil base.install(nil) 71 + assert_nil base.uninstall 72 + end 73 + 74 + def test_plugin_context_query_bang_delegates 75 + captured = nil 76 + ctx = Rockbox::PluginContext.new( 77 + query: ->(q, vars = nil) { captured = [q, vars]; { ok: true } }, 78 + events: @events 79 + ) 80 + result = ctx.query!("query Foo { foo }", { id: 1 }) 81 + assert_equal({ ok: true }, result) 82 + assert_equal "query Foo { foo }", captured[0] 83 + assert_equal({ id: 1 }, captured[1]) 84 + end 85 + end
+48
sdk/ruby/test/test_helper.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require "minitest/autorun" 4 + require "json" 5 + 6 + require "rockbox" 7 + 8 + module Rockbox 9 + # Drop-in replacement for HttpTransport used in tests. Records every call 10 + # and returns scripted responses without touching the network. 11 + class FakeHttp 12 + Call = Struct.new(:query, :variables) 13 + 14 + attr_reader :calls 15 + 16 + def initialize 17 + @calls = [] 18 + @scripted = [] 19 + @default = {} 20 + end 21 + 22 + # Queue a response. The next #execute returns it (FIFO). If empty, the 23 + # default response is returned. 24 + def will_return(data) 25 + @scripted << data 26 + self 27 + end 28 + 29 + def default=(data) 30 + @default = data 31 + end 32 + 33 + # Match the HttpTransport contract. 34 + def execute(query, variables = nil) 35 + @calls << Call.new(query, variables) 36 + @scripted.empty? ? @default : @scripted.shift 37 + end 38 + 39 + # Convenience matchers for assertions. 40 + def last_call 41 + @calls.last 42 + end 43 + 44 + def call_count 45 + @calls.size 46 + end 47 + end 48 + end
+132
sdk/ruby/test/transport_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "test_helper" 4 + 5 + class HttpTransportTest < Minitest::Test 6 + # Stand-in for Net::HTTPSuccess. 7 + class FakeSuccess < Net::HTTPSuccess 8 + def initialize(body) 9 + super("1.1", "200", "OK") 10 + @fake_body = body 11 + end 12 + 13 + def body; @fake_body; end 14 + end 15 + 16 + class FakeFailure < Net::HTTPInternalServerError 17 + def initialize 18 + super("1.1", "500", "Internal Server Error") 19 + end 20 + 21 + def body; "boom"; end 22 + end 23 + 24 + # Patches Net::HTTP for the duration of a block: every request returns the 25 + # supplied response and we capture the request body for assertions. 26 + def with_stubbed_http(response, captured: []) 27 + Net::HTTP.class_eval do 28 + alias_method :__real_request, :request 29 + define_method(:request) do |req| 30 + captured << JSON.parse(req.body) 31 + response 32 + end 33 + end 34 + yield captured 35 + ensure 36 + Net::HTTP.class_eval do 37 + alias_method :request, :__real_request 38 + remove_method :__real_request 39 + end 40 + end 41 + 42 + def test_execute_returns_snake_cased_data 43 + captured = [] 44 + response = FakeSuccess.new(JSON.generate("data" => { "albumId" => "abc" })) 45 + 46 + with_stubbed_http(response, captured: captured) do 47 + transport = Rockbox::HttpTransport.new("http://localhost:6062/graphql") 48 + result = transport.execute("query Foo { foo }") 49 + assert_equal({ album_id: "abc" }, result) 50 + end 51 + end 52 + 53 + def test_execute_camelizes_outgoing_variables 54 + captured = [] 55 + response = FakeSuccess.new(JSON.generate("data" => {})) 56 + 57 + with_stubbed_http(response, captured: captured) do 58 + transport = Rockbox::HttpTransport.new("http://localhost:6062/graphql") 59 + transport.execute("mutation X($albumId: String!) { x(albumId: $albumId) }", 60 + { album_id: "abc" }) 61 + end 62 + 63 + body = captured.first 64 + assert_equal({ "albumId" => "abc" }, body["variables"]) 65 + end 66 + 67 + def test_execute_omits_variables_when_empty 68 + captured = [] 69 + response = FakeSuccess.new(JSON.generate("data" => {})) 70 + 71 + with_stubbed_http(response, captured: captured) do 72 + transport = Rockbox::HttpTransport.new("http://localhost:6062/graphql") 73 + transport.execute("query Foo { foo }") 74 + transport.execute("query Foo { foo }", {}) 75 + end 76 + 77 + captured.each { |body| refute body.key?("variables") } 78 + end 79 + 80 + def test_execute_returns_empty_hash_when_data_is_nil 81 + response = FakeSuccess.new(JSON.generate("data" => nil)) 82 + with_stubbed_http(response) do 83 + transport = Rockbox::HttpTransport.new("http://localhost:6062/graphql") 84 + assert_equal({}, transport.execute("query Foo { foo }")) 85 + end 86 + end 87 + 88 + def test_graphql_errors_raise_graphql_error 89 + response = FakeSuccess.new(JSON.generate( 90 + "data" => nil, 91 + "errors" => [{ "message" => "missing argument" }] 92 + )) 93 + 94 + with_stubbed_http(response) do 95 + transport = Rockbox::HttpTransport.new("http://localhost:6062/graphql") 96 + err = assert_raises(Rockbox::GraphQLError) { transport.execute("query Bad { bad }") } 97 + assert_equal "missing argument", err.message 98 + end 99 + end 100 + 101 + def test_non_2xx_response_raises_network_error 102 + with_stubbed_http(FakeFailure.new) do 103 + transport = Rockbox::HttpTransport.new("http://localhost:6062/graphql") 104 + assert_raises(Rockbox::NetworkError) { transport.execute("query Foo { foo }") } 105 + end 106 + end 107 + 108 + def test_invalid_json_raises_network_error 109 + response = FakeSuccess.new("not json {{{") 110 + with_stubbed_http(response) do 111 + transport = Rockbox::HttpTransport.new("http://localhost:6062/graphql") 112 + assert_raises(Rockbox::NetworkError) { transport.execute("query Foo { foo }") } 113 + end 114 + end 115 + 116 + def test_econnrefused_raises_network_error 117 + Net::HTTP.class_eval do 118 + alias_method :__real_request, :request 119 + define_method(:request) { |_req| raise Errno::ECONNREFUSED } 120 + end 121 + 122 + transport = Rockbox::HttpTransport.new("http://localhost:6062/graphql") 123 + err = assert_raises(Rockbox::NetworkError) { transport.execute("query Foo { foo }") } 124 + assert_match(/Failed to reach Rockbox/, err.message) 125 + assert_kind_of Errno::ECONNREFUSED, err.cause 126 + ensure 127 + Net::HTTP.class_eval do 128 + alias_method :request, :__real_request 129 + remove_method :__real_request 130 + end 131 + end 132 + end
+95
sdk/ruby/test/types_test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + require_relative "test_helper" 4 + 5 + class TypesTest < Minitest::Test 6 + def test_playback_status_constants 7 + assert_equal 0, Rockbox::PlaybackStatus::STOPPED 8 + assert_equal 1, Rockbox::PlaybackStatus::PLAYING 9 + assert_equal 3, Rockbox::PlaybackStatus::PAUSED 10 + end 11 + 12 + def test_playback_status_name_lookup 13 + assert_equal :stopped, Rockbox::PlaybackStatus.name(0) 14 + assert_equal :playing, Rockbox::PlaybackStatus.name(1) 15 + assert_equal :paused, Rockbox::PlaybackStatus.name(3) 16 + assert_equal :unknown, Rockbox::PlaybackStatus.name(99) 17 + assert_equal :unknown, Rockbox::PlaybackStatus.name(nil) 18 + end 19 + 20 + def test_insert_position_constants 21 + assert_equal 0, Rockbox::InsertPosition::NEXT 22 + assert_equal 1, Rockbox::InsertPosition::AFTER_CURRENT 23 + assert_equal 2, Rockbox::InsertPosition::LAST 24 + assert_equal 3, Rockbox::InsertPosition::FIRST 25 + end 26 + 27 + def test_track_from_hash_extracts_known_fields 28 + hash = { 29 + id: "t1", title: "Money", artist: "Pink Floyd", 30 + length: 382_000, elapsed: 0, 31 + unknown_extension: "should be ignored" 32 + } 33 + track = Rockbox::Track.from_hash(hash) 34 + assert_equal "t1", track.id 35 + assert_equal "Money", track.title 36 + assert_equal 382_000, track.length 37 + refute track.respond_to?(:unknown_extension) 38 + end 39 + 40 + def test_track_from_hash_returns_nil_for_nil 41 + assert_nil Rockbox::Track.from_hash(nil) 42 + end 43 + 44 + def test_track_from_hash_missing_fields_default_to_nil 45 + track = Rockbox::Track.from_hash(id: "t1") 46 + assert_equal "t1", track.id 47 + assert_nil track.title 48 + assert_nil track.artist 49 + end 50 + 51 + def test_album_known_members 52 + assert_includes Rockbox::Album.known_members, :id 53 + assert_includes Rockbox::Album.known_members, :title 54 + assert_includes Rockbox::Album.known_members, :tracks 55 + end 56 + 57 + def test_volume_info_from_hash 58 + info = Rockbox::VolumeInfo.from_hash(volume: -10, min: -120, max: 12) 59 + assert_equal(-10, info.volume) 60 + assert_equal(-120, info.min) 61 + assert_equal 12, info.max 62 + end 63 + 64 + def test_directory_predicate_set_bit 65 + entry = Rockbox::Entry.new(name: "Pink Floyd", attr: 0x10, time_write: 0, 66 + customaction: nil, display_name: nil) 67 + assert Rockbox.directory?(entry) 68 + end 69 + 70 + def test_directory_predicate_combined_bits 71 + # 0x18 has the dir bit set plus another flag. 72 + entry = Rockbox::Entry.new(name: "x", attr: 0x18, time_write: 0, 73 + customaction: nil, display_name: nil) 74 + assert Rockbox.directory?(entry) 75 + end 76 + 77 + def test_directory_predicate_unset 78 + entry = Rockbox::Entry.new(name: "song.mp3", attr: 0, time_write: 0, 79 + customaction: nil, display_name: nil) 80 + refute Rockbox.directory?(entry) 81 + end 82 + 83 + def test_directory_predicate_nil_attr 84 + entry = Rockbox::Entry.new(name: "x", attr: nil, time_write: nil, 85 + customaction: nil, display_name: nil) 86 + refute Rockbox.directory?(entry) 87 + end 88 + 89 + def test_search_results_with_empty_arrays 90 + results = Rockbox::SearchResults.new( 91 + artists: [], albums: [], tracks: [], liked_tracks: [], liked_albums: [] 92 + ) 93 + assert_equal 0, results.tracks.size 94 + end 95 + end
+1 -1
sdk/typescript/README.md
··· 37 37 `rockboxd` must be running and reachable. By default the SDK connects to `http://localhost:6062/graphql`. Start rockboxd with: 38 38 39 39 ```sh 40 - ./zig/zig-out/bin/rockboxd 40 + rockbox start 41 41 ``` 42 42 43 43 ---