From d768b1912c1d5f5bcf91ef3ac9dff626b42df6c4 Mon Sep 17 00:00:00 2001 From: Masterisk-F Date: Sat, 18 Jul 2026 14:26:19 +0900 Subject: [PATCH 1/2] fix(cli): reset track selection on disc rescan to prevent TypeError When the user rescans the disc (menu option 2) after having visited the track selection menu (option 4), the cached @selection in CliTracklist becomes stale. If the new disc has fewer tracks, the old track numbers cause @disc.getLengthSector(track) to return nil, producing: TypeError: nil can't be coerced into Float Fix by recreating @cliTracklist on rescan (same pattern as CliDisc#refreshDisc which creates Disc.new()). The new instance lazily initializes @selection from the current disc's track list, discarding any stale state. This mirrors the GTK interface (GtkDisc#refreshDisc) which resets @selection = [] on every refresh. Also add spec verifying that a freshly created CliTracklist returns the current disc's track list even when a previous instance had a different track count. --- bin/rubyripper_cli | 2 +- spec/cli/cliTracklist_spec.rb | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 spec/cli/cliTracklist_spec.rb diff --git a/bin/rubyripper_cli b/bin/rubyripper_cli index a6dd0af..b71eb1c 100755 --- a/bin/rubyripper_cli +++ b/bin/rubyripper_cli @@ -130,7 +130,7 @@ private @out.puts _("Thanks for using RubyRipperRemix.") @out.puts _("Have a nice day!") when 1 then @cliPrefs.show() - when 2 then @cliDisc.show() + when 2 then @cliDisc.show() ; @cliTracklist = CliTracklist.new(@cliDisc, @out, @int) when 3 then @cliDisc.changeMetadata() when 4 then @cliTracklist.show() when 5 then startRip() diff --git a/spec/cli/cliTracklist_spec.rb b/spec/cli/cliTracklist_spec.rb new file mode 100644 index 0000000..0c7028f --- /dev/null +++ b/spec/cli/cliTracklist_spec.rb @@ -0,0 +1,60 @@ +#!/usr/bin/env ruby +# RubyRipperRemix - A secure ripper for Linux/BSD/OSX +# Copyright (C) 2026 Masterisk-F +# +# This file is part of RubyRipperRemix. RubyRipperRemix is free software: you can +# redistribute it and/or modify it under the terms of the GNU General +# Public License as published by the Free Software Foundation, either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see + +require 'rubyripper/cli/cliTracklist' + +describe CliTracklist do + let(:out) {StringIO.new} + let(:int) {double('CliGetInt').as_null_object} + let(:prefs) {double('Preferences::Main').as_null_object} + + context "When selection is requested without prior user interaction" do + it "should return all tracks from the disc" do + cli_disc = double('CliDisc').as_null_object + expect(cli_disc).to receive(:tracks).and_return({1 => 'Track 1', 2 => 'Track 2', 3 => 'Track 3'}) + tracklist = CliTracklist.new(cli_disc, out, int, prefs) + expect(tracklist.selection).to eq([1, 2, 3]) + end + + it "should lazily cache the selection after first access" do + cli_disc = double('CliDisc').as_null_object + expect(cli_disc).to receive(:tracks).once.and_return({1 => 'a', 2 => 'b'}) + tracklist = CliTracklist.new(cli_disc, out, int, prefs) + expect(tracklist.selection).to eq([1, 2]) + # second call should use cache, not call tracks again + expect(tracklist.selection).to eq([1, 2]) + end + end + + context "When the disc is rescanned" do + it "should get fresh track selection from the new disc when CliTracklist is recreated" do + cli_disc = double('CliDisc').as_null_object + + # First disc has 5 tracks + expect(cli_disc).to receive(:tracks).once.and_return({1 => 'Song A', 2 => 'Song B', 3 => 'Song C', 4 => 'Song D', 5 => 'Song E'}) + first_tracklist = CliTracklist.new(cli_disc, out, int, prefs) + expect(first_tracklist.selection).to eq([1, 2, 3, 4, 5]) + + # Simulate a rescan: CliDisc.tracks returns only 3 tracks now + expect(cli_disc).to receive(:tracks).once.and_return({1 => 'New A', 2 => 'New B', 3 => 'New C'}) + second_tracklist = CliTracklist.new(cli_disc, out, int, prefs) + + # The new instance should start fresh with all tracks from the new disc + expect(second_tracklist.selection).to eq([1, 2, 3]) + end + end +end From 80f9ec6445b1a177290ca8da7fab892ef10a662b Mon Sep 17 00:00:00 2001 From: Masterisk-F Date: Mon, 20 Jul 2026 00:08:34 +0900 Subject: [PATCH 2/2] fix(cli): display scan errors in rescan menu option (option 2) When the user selects option 2 (rescan drive) in the main menu, scan errors (e.g. no disc in drive) were silently swallowed: @cliDisc.show() stores the error in @cliDisc.error but never displays it, unlike prepare() which calls showError() afterward. Add the missing showError() call after @cliDisc.show() in the loopMainMenu option 2 branch, matching the existing prepare() pattern. Also add a new spec file (spec/cli/commandLineInterface_spec.rb) with a test case that verifies scan errors are displayed. --- bin/rubyripper_cli | 2 +- spec/cli/commandLineInterface_spec.rb | 64 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 spec/cli/commandLineInterface_spec.rb diff --git a/bin/rubyripper_cli b/bin/rubyripper_cli index b71eb1c..454f4f1 100755 --- a/bin/rubyripper_cli +++ b/bin/rubyripper_cli @@ -130,7 +130,7 @@ private @out.puts _("Thanks for using RubyRipperRemix.") @out.puts _("Have a nice day!") when 1 then @cliPrefs.show() - when 2 then @cliDisc.show() ; @cliTracklist = CliTracklist.new(@cliDisc, @out, @int) + when 2 then @cliDisc.show() ; showError(@cliDisc.error) unless @cliDisc.error.nil? ; @cliTracklist = CliTracklist.new(@cliDisc, @out, @int) when 3 then @cliDisc.changeMetadata() when 4 then @cliTracklist.show() when 5 then startRip() diff --git a/spec/cli/commandLineInterface_spec.rb b/spec/cli/commandLineInterface_spec.rb new file mode 100644 index 0000000..9e4ceec --- /dev/null +++ b/spec/cli/commandLineInterface_spec.rb @@ -0,0 +1,64 @@ +#!/usr/bin/env ruby +# RubyRipperRemix - A secure ripper for Linux/BSD/OSX +# Copyright (C) 2026 Masterisk-F +# +# This file is part of RubyRipperRemix. RubyRipperRemix is free software: you can +# redistribute it and/or modify it under the terms of the GNU General +# Public License as published by the Free Software Foundation, either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see + +require 'rubyripper/base' +require 'rubyripper/errors' +require 'rubyripper/cli/cliArguments' +require 'rubyripper/cli/cliDisc' +require 'rubyripper/cli/cliTracklist' + +# CommandLineInterface is defined in bin/rubyripper_cli, +# which is not in the lib load path. Load it by explicit path. +# Load by absolute path: require/require_relative cannot resolve files without .rb extension. +load File.expand_path('../../bin/rubyripper_cli', File.dirname(__FILE__)) + +describe CommandLineInterface do + let(:out) {StringIO.new} + let(:int) {double('CliGetInt').as_null_object} + let(:cli_disc) {double('CliDisc').as_null_object} + let(:cli_prefs) {double('CliPreferences').as_null_object} + + before do + # CliTracklist.new (called during option 2 at line 133) resolves + # Preferences::Main.instance when prefs is not injected. + # In spec mode ($run_specs = true), Singleton is excluded, so define instance here. + prefs_instance = double('PreferencesMain').as_null_object + allow(prefs_instance).to receive(:image).and_return(false) + allow(Preferences::Main).to receive(:instance).and_return(prefs_instance) + end + + def setup_cli(cli_disc: nil, int: nil, prefs: nil) + CommandLineInterface.new(nil, out, prefs || cli_prefs, cli_disc || self.cli_disc, int || self.int) + end + + context "When option 2 (rescan) is selected with a scan error" do + it "should display the scan error" do + # Stub @int to return 2 (rescan) then 99 (exit main menu) + expect(int).to receive(:get).with("Please type the number of your choice", 99) + .and_return(2, 99) + + # CliDisc with non-nil error (simulating a failed scan) + allow(cli_disc).to receive(:error).and_return([:noDiscYet, '3']) + allow(cli_disc).to receive(:tracks).and_return({1 => 'Track 1'}) + allow(cli_disc).to receive(:cd).and_return(double('Disc').as_null_object) + + cli = setup_cli(cli_disc: cli_disc, int: int) + + expect { cli.send(:loopMainMenu) }.to output(/Error/).to_stdout + end + end +end