Skip to content

Commit ca6366e

Browse files
justin808claude
andauthored
Address PR #1907 code review feedback (#1917)
* Fix critical error handling bug and improve test suite This commit addresses critical issues identified in code review: 1. **Fix error handling in silent mode** (CRITICAL) - Changed from warn/\$stderr to STDERR.puts constant - Bypasses capture_output redirection to ensure errors always appear - Added RuboCop disable pragmas with detailed comments explaining why 2. **Refactor test suite to mock at boundaries** - Removed mocking of private methods (run_rake_task_directly) - Now mocks Rake::Task and system calls directly - Tests verify actual behavior, not implementation details - More resilient to refactoring 3. **Add comprehensive error handling tests** - Test that errors appear in stderr even in silent mode - Test backtrace inclusion when DEBUG env is set - Test stdout suppression in silent mode - Test Rails fallback when not available 4. **Improve test coverage and quality** - 10 examples, all passing - Tests now verify error output reaches STDERR correctly - Added context for Rails not being available All tests pass (10 examples, 0 failures) RuboCop passes with no offenses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix critical missing StringIO require and test stubbing issues This commit addresses critical issues identified in code review: 1. **Add missing StringIO require** (BLOCKER) - Added `require "stringio"` at line 4 - Required for capture_output method at line 91 - Without this, code fails at runtime in silent mode with: NameError: uninitialized constant StringIO 2. **Fix inconsistent system call stubbing in tests** - Updated "when not in Bundler context" tests to match actual implementation - Changed from string form: `system("bundle exec rake...")` - To array form: `system("bundle", "exec", "rake", ...)` - Silent mode now correctly stubs with File::NULL options - This matches the actual implementation at lines 116-121 3. **Fix Rails fallback test stubbing** - Updated "when Rails is not available" test - Now uses correct array form system call signature All tests pass (10 examples, 0 failures) RuboCop passes with no offenses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a909aa1 commit ca6366e

File tree

2 files changed

+159
-18
lines changed

2 files changed

+159
-18
lines changed

lib/react_on_rails/dev/pack_generator.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "English"
4+
require "stringio"
45

56
module ReactOnRails
67
module Dev
@@ -105,7 +106,10 @@ def handle_rake_error(error, _silent)
105106
error_msg += "\n#{error.backtrace.join("\n")}" if ENV["DEBUG"]
106107

107108
# Always write to stderr, even in silent mode
108-
warn error_msg
109+
# Use STDERR constant instead of warn/$stderr to bypass capture_output redirection
110+
# rubocop:disable Style/StderrPuts, Style/GlobalStdStream
111+
STDERR.puts error_msg
112+
# rubocop:enable Style/StderrPuts, Style/GlobalStdStream
109113
end
110114

111115
def run_via_bundle_exec(silent: false)

spec/react_on_rails/dev/pack_generator_spec.rb

Lines changed: 154 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,167 @@
55

66
RSpec.describe ReactOnRails::Dev::PackGenerator do
77
describe ".generate" do
8-
it "runs pack generation successfully in verbose mode" do
9-
allow(described_class).to receive(:system)
10-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
11-
.and_return(true)
8+
context "when in Bundler context with Rails available" do
9+
let(:mock_task) { instance_double(Rake::Task) }
10+
let(:mock_rails_app) do
11+
# rubocop:disable RSpec/VerifiedDoubles
12+
double("Rails.application").tap do |app|
13+
allow(app).to receive(:load_tasks)
14+
allow(app).to receive(:respond_to?).with(:load_tasks).and_return(true)
15+
end
16+
# rubocop:enable RSpec/VerifiedDoubles
17+
end
1218

13-
expect { described_class.generate(verbose: true) }
14-
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
19+
before do
20+
# Setup Bundler context
21+
stub_const("Bundler", Module.new)
22+
allow(ENV).to receive(:[]).and_call_original
23+
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
24+
25+
# Setup Rails availability
26+
app = mock_rails_app
27+
rails_module = Module.new do
28+
define_singleton_method(:application) { app }
29+
define_singleton_method(:respond_to?) { |method, *| method == :application }
30+
end
31+
stub_const("Rails", rails_module)
32+
33+
# Mock Rake::Task at the boundary
34+
allow(Rake::Task).to receive(:task_defined?).with("react_on_rails:generate_packs").and_return(false)
35+
allow(Rake::Task).to receive(:[]).with("react_on_rails:generate_packs").and_return(mock_task)
36+
allow(mock_task).to receive(:reenable)
37+
allow(mock_task).to receive(:invoke)
38+
end
39+
40+
it "runs pack generation successfully in verbose mode using direct rake execution" do
41+
expect { described_class.generate(verbose: true) }
42+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
43+
44+
expect(mock_task).to have_received(:invoke)
45+
expect(mock_rails_app).to have_received(:load_tasks)
46+
end
47+
48+
it "runs pack generation successfully in quiet mode using direct rake execution" do
49+
expect { described_class.generate(verbose: false) }
50+
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
51+
52+
expect(mock_task).to have_received(:invoke)
53+
end
54+
55+
it "exits with error when pack generation fails" do
56+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed"))
57+
58+
# Mock STDERR.puts to capture output
59+
error_output = []
60+
# rubocop:disable Style/GlobalStdStream
61+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
62+
# rubocop:enable Style/GlobalStdStream
63+
64+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
65+
expect(error_output.join("\n")).to match(/Error generating packs: Task failed/)
66+
end
67+
68+
it "outputs errors to stderr even in silent mode" do
69+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error"))
70+
71+
# Mock STDERR.puts to capture output
72+
error_output = []
73+
# rubocop:disable Style/GlobalStdStream
74+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
75+
# rubocop:enable Style/GlobalStdStream
76+
77+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
78+
expect(error_output.join("\n")).to match(/Error generating packs: Silent mode error/)
79+
end
80+
81+
it "includes backtrace in error output when DEBUG env is set" do
82+
allow(ENV).to receive(:[]).with("DEBUG").and_return("true")
83+
allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Debug error"))
84+
85+
# Mock STDERR.puts to capture output
86+
error_output = []
87+
# rubocop:disable Style/GlobalStdStream
88+
allow(STDERR).to receive(:puts) { |msg| error_output << msg }
89+
# rubocop:enable Style/GlobalStdStream
90+
91+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
92+
expect(error_output.join("\n")).to match(/Error generating packs: Debug error.*pack_generator_spec\.rb/m)
93+
end
94+
95+
it "suppresses stdout in silent mode" do
96+
# Mock task to produce output
97+
allow(mock_task).to receive(:invoke) do
98+
puts "This should be suppressed"
99+
end
100+
101+
expect { described_class.generate(verbose: false) }
102+
.not_to output(/This should be suppressed/).to_stdout_from_any_process
103+
end
15104
end
16105

17-
it "runs pack generation successfully in quiet mode" do
18-
allow(described_class).to receive(:system)
19-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs", out: File::NULL, err: File::NULL)
20-
.and_return(true)
106+
context "when not in Bundler context" do
107+
before do
108+
# Ensure we're not in Bundler context
109+
hide_const("Bundler") if defined?(Bundler)
110+
end
111+
112+
it "runs pack generation successfully in verbose mode using bundle exec" do
113+
allow(described_class).to receive(:system)
114+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
115+
.and_return(true)
116+
117+
expect { described_class.generate(verbose: true) }
118+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
119+
120+
expect(described_class).to have_received(:system)
121+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
122+
end
123+
124+
it "runs pack generation successfully in quiet mode using bundle exec" do
125+
allow(described_class).to receive(:system)
126+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
127+
out: File::NULL, err: File::NULL)
128+
.and_return(true)
21129

22-
expect { described_class.generate(verbose: false) }
23-
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
130+
expect { described_class.generate(verbose: false) }
131+
.to output(/📦 Generating packs\.\.\. ✅/).to_stdout_from_any_process
132+
133+
expect(described_class).to have_received(:system)
134+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
135+
out: File::NULL, err: File::NULL)
136+
end
137+
138+
it "exits with error when pack generation fails" do
139+
allow(described_class).to receive(:system)
140+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs",
141+
out: File::NULL, err: File::NULL)
142+
.and_return(false)
143+
144+
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
145+
end
24146
end
25147

26-
it "exits with error when pack generation fails" do
27-
allow(described_class).to receive(:system)
28-
.with("bundle", "exec", "rake", "react_on_rails:generate_packs", out: File::NULL, err: File::NULL)
29-
.and_return(false)
148+
context "when Rails is not available" do
149+
before do
150+
stub_const("Bundler", Module.new)
151+
allow(ENV).to receive(:[]).and_call_original
152+
allow(ENV).to receive(:[]).with("BUNDLE_GEMFILE").and_return("/path/to/Gemfile")
153+
154+
# Rails not available
155+
hide_const("Rails") if defined?(Rails)
156+
end
157+
158+
it "falls back to bundle exec when Rails is not defined" do
159+
allow(described_class).to receive(:system)
160+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
161+
.and_return(true)
162+
163+
expect { described_class.generate(verbose: true) }
164+
.to output(/📦 Generating React on Rails packs.../).to_stdout_from_any_process
30165

31-
expect { described_class.generate(verbose: false) }.to raise_error(SystemExit)
166+
expect(described_class).to have_received(:system)
167+
.with("bundle", "exec", "rake", "react_on_rails:generate_packs")
168+
end
32169
end
33170
end
34171
end

0 commit comments

Comments
 (0)