Skip to content

Commit f758358

Browse files
committed
✅ Add more tests
1 parent 09f4e50 commit f758358

File tree

6 files changed

+354
-2
lines changed

6 files changed

+354
-2
lines changed

lib/rbs/merge/file_analysis.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,13 @@ def build_freeze_blocks(markers)
189189
end_marker: marker[:text],
190190
)
191191
else
192-
DebugLogger.warn("Unmatched freeze end marker at line #{marker[:line]}")
192+
DebugLogger.warning("Unmatched freeze end marker at line #{marker[:line]}")
193193
end
194194
end
195195
end
196196

197197
stack.each do |unmatched|
198-
DebugLogger.warn("Unmatched freeze start marker at line #{unmatched[:line]}")
198+
DebugLogger.warning("Unmatched freeze start marker at line #{unmatched[:line]}")
199199
end
200200

201201
blocks

spec/rbs/merge/file_aligner_spec.rb

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,120 @@ def inspect: () -> String
222222
expect(alignment).to be_an(Array)
223223
end
224224
end
225+
226+
describe "edge cases in alignment" do
227+
context "with duplicate signatures" do
228+
let(:template_source) do
229+
<<~RBS
230+
class Foo
231+
end
232+
class Foo
233+
end
234+
RBS
235+
end
236+
let(:dest_source) do
237+
<<~RBS
238+
class Foo
239+
end
240+
RBS
241+
end
242+
let(:template_analysis) { Rbs::Merge::FileAnalysis.new(template_source) }
243+
let(:dest_analysis) { Rbs::Merge::FileAnalysis.new(dest_source) }
244+
245+
it "pairs only matching indices (second template remains unmatched)" do
246+
aligner = described_class.new(template_analysis, dest_analysis)
247+
alignment = aligner.align
248+
249+
matches = alignment.select { |e| e[:type] == :match }
250+
template_only = alignment.select { |e| e[:type] == :template_only }
251+
252+
# One match, one template_only
253+
expect(matches.size).to eq(1)
254+
expect(template_only.size).to eq(1)
255+
end
256+
end
257+
258+
context "with more dest matches than template" do
259+
let(:template_source) do
260+
<<~RBS
261+
class Foo
262+
end
263+
RBS
264+
end
265+
let(:dest_source) do
266+
<<~RBS
267+
class Foo
268+
end
269+
class Foo
270+
end
271+
RBS
272+
end
273+
let(:template_analysis) { Rbs::Merge::FileAnalysis.new(template_source) }
274+
let(:dest_analysis) { Rbs::Merge::FileAnalysis.new(dest_source) }
275+
276+
it "pairs first dest with template, second dest remains unmatched" do
277+
aligner = described_class.new(template_analysis, dest_analysis)
278+
alignment = aligner.align
279+
280+
matches = alignment.select { |e| e[:type] == :match }
281+
dest_only = alignment.select { |e| e[:type] == :dest_only }
282+
283+
# One match, one dest_only (zip produces nil for missing element)
284+
expect(matches.size).to eq(1)
285+
expect(dest_only.size).to eq(1)
286+
end
287+
end
288+
289+
context "with nil signature" do
290+
let(:template_source) { "class Foo\nend" }
291+
let(:dest_source) { "class Foo\nend" }
292+
let(:template_analysis) { Rbs::Merge::FileAnalysis.new(template_source) }
293+
let(:dest_analysis) { Rbs::Merge::FileAnalysis.new(dest_source) }
294+
295+
it "excludes entries with nil signatures from signature map" do
296+
# Mock signature_at to return nil
297+
allow(template_analysis).to receive(:signature_at).and_return(nil)
298+
299+
aligner = described_class.new(template_analysis, dest_analysis)
300+
alignment = aligner.align
301+
302+
# With nil signature, no matches can be made
303+
matches = alignment.select { |e| e[:type] == :match }
304+
expect(matches).to be_empty
305+
end
306+
end
307+
308+
context "with unknown entry type in sort" do
309+
let(:template_source) { "class Foo\nend" }
310+
let(:dest_source) { "class Bar\nend" }
311+
let(:template_analysis) { Rbs::Merge::FileAnalysis.new(template_source) }
312+
let(:dest_analysis) { Rbs::Merge::FileAnalysis.new(dest_source) }
313+
314+
it "handles unknown entry types with fallback sort key" do
315+
aligner = described_class.new(template_analysis, dest_analysis)
316+
alignment = aligner.align
317+
318+
# Inject an unknown type entry to test the else branch
319+
unknown_entry = {type: :unknown, template_index: 0, dest_index: 0}
320+
alignment << unknown_entry
321+
322+
# Re-sort (private method, but we can test indirectly)
323+
sorted = alignment.sort_by do |entry|
324+
case entry[:type]
325+
when :match
326+
[0, entry[:dest_index], entry[:template_index]]
327+
when :dest_only
328+
[1, entry[:dest_index], 0]
329+
when :template_only
330+
[2, entry[:template_index], 0]
331+
else
332+
[3, 0, 0]
333+
end
334+
end
335+
336+
# Unknown type should sort last
337+
expect(sorted.last[:type]).to eq(:unknown)
338+
end
339+
end
340+
end
225341
end

spec/rbs/merge/file_analysis_spec.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,4 +446,73 @@ class Foo
446446
end
447447
end
448448
end
449+
450+
describe "freeze marker edge cases" do
451+
context "with unmatched freeze end marker" do
452+
let(:source) do
453+
<<~RBS
454+
class Foo
455+
end
456+
# rbs-merge:unfreeze
457+
RBS
458+
end
459+
460+
it "warns about unmatched end marker but still parses" do
461+
expect(Rbs::Merge::DebugLogger).to receive(:warning).with(/Unmatched freeze end marker/)
462+
analysis = described_class.new(source)
463+
expect(analysis.valid?).to be true
464+
expect(analysis.statements.size).to eq(1)
465+
end
466+
end
467+
468+
context "with unmatched freeze start marker" do
469+
let(:source) do
470+
<<~RBS
471+
# rbs-merge:freeze
472+
class Foo
473+
end
474+
RBS
475+
end
476+
477+
it "warns about unmatched start marker but still parses" do
478+
expect(Rbs::Merge::DebugLogger).to receive(:warning).with(/Unmatched freeze start marker/)
479+
analysis = described_class.new(source)
480+
expect(analysis.valid?).to be true
481+
end
482+
end
483+
484+
context "with invalid marker type" do
485+
let(:source) do
486+
<<~RBS
487+
# rbs-merge:invalid
488+
class Foo
489+
end
490+
RBS
491+
end
492+
493+
it "ignores invalid marker types" do
494+
analysis = described_class.new(source)
495+
expect(analysis.valid?).to be true
496+
# No freeze blocks created for invalid marker
497+
expect(analysis.statements.none? { |s| s.is_a?(Rbs::Merge::FreezeNode) }).to be true
498+
end
499+
end
500+
501+
context "with no freeze markers" do
502+
let(:source) do
503+
<<~RBS
504+
class Foo
505+
end
506+
class Bar
507+
end
508+
RBS
509+
end
510+
511+
it "returns declarations without modification" do
512+
analysis = described_class.new(source)
513+
expect(analysis.statements.size).to eq(2)
514+
expect(analysis.statements).to all(be_a(RBS::AST::Declarations::Class))
515+
end
516+
end
517+
end
449518
end

spec/rbs/merge/freeze_node_spec.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,48 @@ def bar: () -> void
164164
expect { Rbs::Merge::FileAnalysis.new(invalid_source) }
165165
.to raise_error(described_class::InvalidStructureError)
166166
end
167+
168+
it "includes node names in error message" do
169+
expect { Rbs::Merge::FileAnalysis.new(invalid_source) }
170+
.to raise_error(described_class::InvalidStructureError, /Foo.*lines/)
171+
end
172+
end
173+
174+
context "with partial overlap and node without name method" do
175+
# Test the else branch in validate_structure! (line 103)
176+
# where node.respond_to?(:name) is false
177+
it "uses class name when node doesn't respond to :name" do
178+
# Create a freeze node with a mock node that doesn't have :name
179+
source = <<~RBS
180+
class Foo
181+
end
182+
RBS
183+
analysis = Rbs::Merge::FileAnalysis.new(source)
184+
185+
# Create a mock node without :name method that partially overlaps
186+
# Freeze block: lines 2-4, Node: lines 1-3 (partial overlap)
187+
nameless_node = double(
188+
"NamelessNode",
189+
location: double(start_line: 1, end_line: 3),
190+
)
191+
# Explicitly make it NOT respond to :name
192+
allow(nameless_node).to receive(:respond_to?).with(:name).and_return(false)
193+
194+
# Validation happens during initialize, so the error is raised there
195+
# Freeze block lines 2-4, node lines 1-3 creates partial overlap:
196+
# - NOT fully_contained (node starts before freeze block)
197+
# - NOT encompasses (node doesn't end after freeze block)
198+
# - NOT fully_outside (overlaps at lines 2-3)
199+
expect {
200+
described_class.new(
201+
start_line: 2,
202+
end_line: 4,
203+
analysis: analysis,
204+
nodes: [],
205+
overlapping_nodes: [nameless_node],
206+
)
207+
}.to raise_error(described_class::InvalidStructureError, /Double/)
208+
end
167209
end
168210

169211
context "with fully contained declaration" do

spec/rbs/merge/merge_result_spec.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,75 @@ def bar: () -> void
215215
expect(output).to include("class CommentedClass")
216216
end
217217
end
218+
219+
describe "#add_recursive_merge edge cases" do
220+
it "removes trailing empty line when content ends with newline" do
221+
# Content ending with newline will have empty last element after split
222+
merged = "class Foo\nend\n"
223+
result.add_recursive_merge(merged, template_index: 0, dest_index: 0)
224+
# The trailing empty element should be removed
225+
expect(result.content.last).not_to eq("")
226+
end
227+
228+
it "handles content without trailing newline" do
229+
merged = "class Foo\nend"
230+
result.add_recursive_merge(merged, template_index: 0, dest_index: 0)
231+
expect(result.content).to eq(["class Foo", "end"])
232+
end
233+
end
234+
235+
describe "#to_s edge cases" do
236+
it "adds trailing newline if content doesn't end with one" do
237+
result.add_raw(["class Foo", "end"], decision: :custom)
238+
output = result.to_s
239+
expect(output).to end_with("\n")
240+
end
241+
242+
it "doesn't double newline if content already ends with newline" do
243+
# This is an edge case - normally lines don't include newlines
244+
# but testing the unless branch
245+
result.add_raw(["class Foo", "end"], decision: :custom)
246+
output = result.to_s
247+
expect(output).not_to end_with("\n\n")
248+
end
249+
end
250+
251+
describe "extract_lines with FreezeNode" do
252+
let(:dest_with_freeze) do
253+
<<~RBS
254+
# rbs-merge:freeze
255+
type frozen = String
256+
# rbs-merge:unfreeze
257+
RBS
258+
end
259+
let(:dest_analysis_frozen) { Rbs::Merge::FileAnalysis.new(dest_with_freeze) }
260+
let(:result_frozen) { described_class.new(template_analysis, dest_analysis_frozen) }
261+
262+
it "extracts lines using start_line and end_line for FreezeNode" do
263+
freeze_node = dest_analysis_frozen.freeze_blocks.first
264+
result_frozen.add_freeze_block(freeze_node)
265+
output = result_frozen.to_s
266+
# 3 lines of content + trailing newline = 3 lines when split by newline
267+
expect(output.lines.count).to eq(3)
268+
end
269+
end
270+
271+
describe "extract_lines without comments" do
272+
let(:source_no_comments) do
273+
<<~RBS
274+
class NoComment
275+
def bar: () -> void
276+
end
277+
RBS
278+
end
279+
let(:analysis_no_comments) { Rbs::Merge::FileAnalysis.new(source_no_comments) }
280+
let(:result_no_comments) { described_class.new(analysis_no_comments, analysis_no_comments) }
281+
282+
it "extracts lines using declaration location when no comment" do
283+
result_no_comments.add_from_template(0)
284+
output = result_no_comments.to_s
285+
expect(output).to include("class NoComment")
286+
expect(output).not_to include("#")
287+
end
288+
end
218289
end

spec/rbs/merge/smart_merger_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,4 +443,58 @@ def baz: () -> void
443443
expect(result.to_s).to include("# Destination comment")
444444
end
445445
end
446+
447+
describe "merge_result caching" do
448+
let(:template) { "class Foo\nend\n" }
449+
let(:destination) { "class Bar\nend\n" }
450+
451+
it "caches the merge_result on subsequent calls" do
452+
merger = described_class.new(template, destination)
453+
result1 = merger.merge_result
454+
result2 = merger.merge_result
455+
expect(result1).to be(result2) # Same object identity
456+
end
457+
end
458+
459+
describe "process_match with :template source resolution" do
460+
let(:template) do
461+
<<~RBS
462+
type my_alias = String
463+
RBS
464+
end
465+
let(:destination) do
466+
<<~RBS
467+
type my_alias = Integer
468+
RBS
469+
end
470+
471+
it "uses template content when signature_match_preference is :template" do
472+
merger = described_class.new(template, destination, signature_match_preference: :template)
473+
result = merger.merge_result
474+
expect(result.to_s).to include("type my_alias = String")
475+
expect(result.to_s).not_to include("Integer")
476+
end
477+
end
478+
479+
describe "process_match with FreezeNode in matched entry" do
480+
let(:template) do
481+
<<~RBS
482+
type frozen_type = String
483+
RBS
484+
end
485+
let(:destination) do
486+
<<~RBS
487+
# rbs-merge:freeze
488+
type frozen_type = Integer | Symbol
489+
# rbs-merge:unfreeze
490+
RBS
491+
end
492+
493+
it "uses freeze block content even when template has matching declaration" do
494+
merger = described_class.new(template, destination)
495+
result = merger.merge_result
496+
expect(result.to_s).to include("type frozen_type = Integer | Symbol")
497+
expect(result.to_s).to include("rbs-merge:freeze")
498+
end
499+
end
446500
end

0 commit comments

Comments
 (0)