ast-merge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +46 -0
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +227 -0
- data/FUNDING.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +852 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/ast/merge/ast_node.rb +87 -0
- data/lib/ast/merge/comment/block.rb +195 -0
- data/lib/ast/merge/comment/empty.rb +78 -0
- data/lib/ast/merge/comment/line.rb +138 -0
- data/lib/ast/merge/comment/parser.rb +278 -0
- data/lib/ast/merge/comment/style.rb +282 -0
- data/lib/ast/merge/comment.rb +36 -0
- data/lib/ast/merge/conflict_resolver_base.rb +399 -0
- data/lib/ast/merge/debug_logger.rb +271 -0
- data/lib/ast/merge/fenced_code_block_detector.rb +211 -0
- data/lib/ast/merge/file_analyzable.rb +307 -0
- data/lib/ast/merge/freezable.rb +82 -0
- data/lib/ast/merge/freeze_node_base.rb +434 -0
- data/lib/ast/merge/match_refiner_base.rb +312 -0
- data/lib/ast/merge/match_score_base.rb +135 -0
- data/lib/ast/merge/merge_result_base.rb +169 -0
- data/lib/ast/merge/merger_config.rb +258 -0
- data/lib/ast/merge/node_typing.rb +373 -0
- data/lib/ast/merge/region.rb +124 -0
- data/lib/ast/merge/region_detector_base.rb +114 -0
- data/lib/ast/merge/region_mergeable.rb +364 -0
- data/lib/ast/merge/rspec/shared_examples/conflict_resolver_base.rb +416 -0
- data/lib/ast/merge/rspec/shared_examples/debug_logger.rb +174 -0
- data/lib/ast/merge/rspec/shared_examples/file_analyzable.rb +193 -0
- data/lib/ast/merge/rspec/shared_examples/freeze_node_base.rb +219 -0
- data/lib/ast/merge/rspec/shared_examples/merge_result_base.rb +106 -0
- data/lib/ast/merge/rspec/shared_examples/merger_config.rb +202 -0
- data/lib/ast/merge/rspec/shared_examples/reproducible_merge.rb +115 -0
- data/lib/ast/merge/rspec/shared_examples.rb +26 -0
- data/lib/ast/merge/rspec.rb +4 -0
- data/lib/ast/merge/section_typing.rb +303 -0
- data/lib/ast/merge/smart_merger_base.rb +417 -0
- data/lib/ast/merge/text/conflict_resolver.rb +161 -0
- data/lib/ast/merge/text/file_analysis.rb +168 -0
- data/lib/ast/merge/text/line_node.rb +142 -0
- data/lib/ast/merge/text/merge_result.rb +42 -0
- data/lib/ast/merge/text/section.rb +93 -0
- data/lib/ast/merge/text/section_splitter.rb +397 -0
- data/lib/ast/merge/text/smart_merger.rb +141 -0
- data/lib/ast/merge/text/word_node.rb +86 -0
- data/lib/ast/merge/text.rb +35 -0
- data/lib/ast/merge/toml_frontmatter_detector.rb +88 -0
- data/lib/ast/merge/version.rb +12 -0
- data/lib/ast/merge/yaml_frontmatter_detector.rb +108 -0
- data/lib/ast/merge.rb +165 -0
- data/lib/ast-merge.rb +4 -0
- data/sig/ast/merge.rbs +195 -0
- data.tar.gz.sig +0 -0
- metadata +326 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared examples for validating FileAnalyzable integration
|
|
4
|
+
#
|
|
5
|
+
# Usage in your spec:
|
|
6
|
+
# require "ast/merge/rspec/shared_examples/file_analyzable"
|
|
7
|
+
#
|
|
8
|
+
# RSpec.describe MyMerge::FileAnalysis do
|
|
9
|
+
# it_behaves_like "Ast::Merge::FileAnalyzable" do
|
|
10
|
+
# let(:file_analysis_class) { MyMerge::FileAnalysis }
|
|
11
|
+
# let(:freeze_node_class) { MyMerge::FreezeNode }
|
|
12
|
+
# let(:sample_source) { "# Some valid source for this parser" }
|
|
13
|
+
# let(:sample_source_with_freeze) do
|
|
14
|
+
# <<~SOURCE
|
|
15
|
+
# # Some source
|
|
16
|
+
# # my-merge:freeze
|
|
17
|
+
# frozen content
|
|
18
|
+
# # my-merge:unfreeze
|
|
19
|
+
# # More source
|
|
20
|
+
# SOURCE
|
|
21
|
+
# end
|
|
22
|
+
# # Factory to create file analysis instance
|
|
23
|
+
# let(:build_file_analysis) { ->(source, **opts) { file_analysis_class.new(source, **opts) } }
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# @note The extending class should include Ast::Merge::FileAnalyzable
|
|
28
|
+
|
|
29
|
+
RSpec.shared_examples("Ast::Merge::FileAnalyzable") do
|
|
30
|
+
# Required let blocks:
|
|
31
|
+
# - file_analysis_class: The class under test (e.g., MyMerge::FileAnalysis)
|
|
32
|
+
# - freeze_node_class: The freeze node class (e.g., MyMerge::FreezeNode)
|
|
33
|
+
# - sample_source: A valid source string for this parser
|
|
34
|
+
# - sample_source_with_freeze: Source containing a freeze block
|
|
35
|
+
# - build_file_analysis: Lambda that creates a file analysis instance
|
|
36
|
+
|
|
37
|
+
describe "module inclusion" do
|
|
38
|
+
it "includes Ast::Merge::FileAnalyzable" do
|
|
39
|
+
expect(file_analysis_class.ancestors).to(include(Ast::Merge::FileAnalyzable))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "attr_readers from FileAnalyzable" do
|
|
44
|
+
let(:analysis) { build_file_analysis.call(sample_source) }
|
|
45
|
+
|
|
46
|
+
it "has #source reader" do
|
|
47
|
+
expect(analysis).to(respond_to(:source))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "has #lines reader" do
|
|
51
|
+
expect(analysis).to(respond_to(:lines))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "has #freeze_token reader" do
|
|
55
|
+
expect(analysis).to(respond_to(:freeze_token))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "has #signature_generator reader" do
|
|
59
|
+
expect(analysis).to(respond_to(:signature_generator))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "has #statements reader" do
|
|
63
|
+
expect(analysis).to(respond_to(:statements))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe "#freeze_blocks" do
|
|
68
|
+
context "without freeze blocks" do
|
|
69
|
+
let(:analysis) { build_file_analysis.call(sample_source) }
|
|
70
|
+
|
|
71
|
+
it "returns an empty array" do
|
|
72
|
+
expect(analysis.freeze_blocks).to(eq([]))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
context "with freeze blocks" do
|
|
77
|
+
let(:analysis) { build_file_analysis.call(sample_source_with_freeze) }
|
|
78
|
+
|
|
79
|
+
it "returns an array of FreezeNode instances" do
|
|
80
|
+
expect(analysis.freeze_blocks).to(be_an(Array))
|
|
81
|
+
analysis.freeze_blocks.each do |block|
|
|
82
|
+
expect(block).to(be_a(freeze_node_class))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe "#in_freeze_block?" do
|
|
89
|
+
let(:analysis) { build_file_analysis.call(sample_source_with_freeze) }
|
|
90
|
+
|
|
91
|
+
it "returns false for lines outside freeze blocks" do
|
|
92
|
+
expect(analysis.in_freeze_block?(1)).to(be(false))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "responds to the method" do
|
|
96
|
+
expect(analysis).to(respond_to(:in_freeze_block?))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "#freeze_block_at" do
|
|
101
|
+
let(:analysis) { build_file_analysis.call(sample_source_with_freeze) }
|
|
102
|
+
|
|
103
|
+
it "returns nil for lines outside freeze blocks" do
|
|
104
|
+
expect(analysis.freeze_block_at(1)).to(be_nil)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "responds to the method" do
|
|
108
|
+
expect(analysis).to(respond_to(:freeze_block_at))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe "#signature_at" do
|
|
113
|
+
let(:analysis) { build_file_analysis.call(sample_source) }
|
|
114
|
+
|
|
115
|
+
it "returns nil for invalid index" do
|
|
116
|
+
expect(analysis.signature_at(-1)).to(be_nil)
|
|
117
|
+
expect(analysis.signature_at(9999)).to(be_nil)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "responds to the method" do
|
|
121
|
+
expect(analysis).to(respond_to(:signature_at))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe "#line_at" do
|
|
126
|
+
let(:analysis) { build_file_analysis.call(sample_source) }
|
|
127
|
+
|
|
128
|
+
it "returns nil for line 0" do
|
|
129
|
+
expect(analysis.line_at(0)).to(be_nil)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "returns nil for negative line" do
|
|
133
|
+
expect(analysis.line_at(-1)).to(be_nil)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "responds to the method" do
|
|
137
|
+
expect(analysis).to(respond_to(:line_at))
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe "#normalized_line" do
|
|
142
|
+
let(:analysis) { build_file_analysis.call(sample_source) }
|
|
143
|
+
|
|
144
|
+
it "responds to the method" do
|
|
145
|
+
expect(analysis).to(respond_to(:normalized_line))
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe "#generate_signature" do
|
|
150
|
+
let(:analysis) { build_file_analysis.call(sample_source) }
|
|
151
|
+
|
|
152
|
+
it "responds to the method" do
|
|
153
|
+
expect(analysis).to(respond_to(:generate_signature))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
context "with NodeTyping::Wrapper" do
|
|
157
|
+
let(:analysis) { build_file_analysis.call(sample_source) }
|
|
158
|
+
|
|
159
|
+
it "unwraps NodeTyping::Wrapper and computes signature from underlying node" do
|
|
160
|
+
# Skip if no statements to test with
|
|
161
|
+
skip "No statements available for testing" if analysis.statements.empty?
|
|
162
|
+
|
|
163
|
+
node = analysis.statements.first
|
|
164
|
+
|
|
165
|
+
# Create a NodeTyping::Wrapper around the node
|
|
166
|
+
wrapper = Ast::Merge::NodeTyping::Wrapper.new(node, :test_type)
|
|
167
|
+
|
|
168
|
+
# Create a signature generator that returns the wrapper
|
|
169
|
+
wrapped_analysis = build_file_analysis.call(
|
|
170
|
+
sample_source,
|
|
171
|
+
signature_generator: ->(_n) { wrapper },
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# The signature should be computed from the unwrapped node, not the wrapper itself
|
|
175
|
+
# Get the expected signature from the unwrapped node
|
|
176
|
+
expected_sig = analysis.generate_signature(node)
|
|
177
|
+
|
|
178
|
+
# The wrapped analysis should produce the same signature
|
|
179
|
+
actual_sig = wrapped_analysis.generate_signature(node)
|
|
180
|
+
|
|
181
|
+
expect(actual_sig).to(eq(expected_sig))
|
|
182
|
+
expect(actual_sig).not_to(be_a(Ast::Merge::NodeTyping::Wrapper))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "recognizes NodeTyping::Wrapper in fallthrough_node?" do
|
|
186
|
+
node = analysis.statements.first || {type: :test}
|
|
187
|
+
wrapper = Ast::Merge::NodeTyping::Wrapper.new(node, :test_type)
|
|
188
|
+
|
|
189
|
+
expect(analysis.send(:fallthrough_node?, wrapper)).to(be(true))
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared examples for validating FreezeNodeBase integration
|
|
4
|
+
#
|
|
5
|
+
# Usage in your spec:
|
|
6
|
+
# require "ast/merge/rspec/shared_examples/freeze_node_base"
|
|
7
|
+
#
|
|
8
|
+
# RSpec.describe MyMerge::FreezeNode do
|
|
9
|
+
# it_behaves_like "Ast::Merge::FreezeNodeBase" do
|
|
10
|
+
# let(:freeze_node_class) { MyMerge::FreezeNode }
|
|
11
|
+
# let(:default_pattern_type) { :hash_comment }
|
|
12
|
+
# # Factory to create a freeze node instance
|
|
13
|
+
# let(:build_freeze_node) do
|
|
14
|
+
# ->(start_line:, end_line:, **opts) {
|
|
15
|
+
# freeze_node_class.new(start_line: start_line, end_line: end_line, **opts)
|
|
16
|
+
# }
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @note The extending class should inherit from or behave like Ast::Merge::FreezeNodeBase
|
|
22
|
+
|
|
23
|
+
RSpec.shared_examples("Ast::Merge::FreezeNodeBase") do
|
|
24
|
+
# Required let blocks:
|
|
25
|
+
# - freeze_node_class: The class under test (e.g., MyMerge::FreezeNode)
|
|
26
|
+
# - default_pattern_type: The default pattern type (:hash_comment, etc.)
|
|
27
|
+
# - build_freeze_node: Lambda that creates a freeze node instance
|
|
28
|
+
|
|
29
|
+
describe "class methods" do
|
|
30
|
+
describe ".pattern_for" do
|
|
31
|
+
it "returns a hash with :start and :end keys" do
|
|
32
|
+
pattern = freeze_node_class.pattern_for(:hash_comment)
|
|
33
|
+
expect(pattern).to(be_a(Hash))
|
|
34
|
+
expect(pattern).to(have_key(:start))
|
|
35
|
+
expect(pattern).to(have_key(:end))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "returns Regexp patterns" do
|
|
39
|
+
pattern = freeze_node_class.pattern_for(:hash_comment)
|
|
40
|
+
expect(pattern[:start]).to(be_a(Regexp))
|
|
41
|
+
expect(pattern[:end]).to(be_a(Regexp))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "raises ArgumentError for unknown pattern type" do
|
|
45
|
+
expect { freeze_node_class.pattern_for(:unknown_pattern) }
|
|
46
|
+
.to(raise_error(ArgumentError, /unknown pattern type/i))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe ".start_pattern" do
|
|
51
|
+
it "returns a Regexp" do
|
|
52
|
+
expect(freeze_node_class.start_pattern).to(be_a(Regexp))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "accepts pattern_type argument" do
|
|
56
|
+
expect(freeze_node_class.start_pattern(:hash_comment)).to(be_a(Regexp))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe ".end_pattern" do
|
|
61
|
+
it "returns a Regexp" do
|
|
62
|
+
expect(freeze_node_class.end_pattern).to(be_a(Regexp))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "accepts pattern_type argument" do
|
|
66
|
+
expect(freeze_node_class.end_pattern(:hash_comment)).to(be_a(Regexp))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe ".freeze_start?" do
|
|
71
|
+
it "returns true for valid freeze start markers" do
|
|
72
|
+
expect(freeze_node_class.freeze_start?("# token:freeze")).to(be(true))
|
|
73
|
+
expect(freeze_node_class.freeze_start?(" # my-merge:freeze")).to(be(true))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "returns false for invalid markers" do
|
|
77
|
+
expect(freeze_node_class.freeze_start?("not a marker")).to(be(false))
|
|
78
|
+
expect(freeze_node_class.freeze_start?("# unfreeze")).to(be(false))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "returns false for nil" do
|
|
82
|
+
expect(freeze_node_class.freeze_start?(nil)).to(be(false))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe ".freeze_end?" do
|
|
87
|
+
it "returns true for valid freeze end markers" do
|
|
88
|
+
expect(freeze_node_class.freeze_end?("# token:unfreeze")).to(be(true))
|
|
89
|
+
expect(freeze_node_class.freeze_end?(" # my-merge:unfreeze")).to(be(true))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "returns false for invalid markers" do
|
|
93
|
+
expect(freeze_node_class.freeze_end?("not a marker")).to(be(false))
|
|
94
|
+
expect(freeze_node_class.freeze_end?("# freeze")).to(be(false))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "returns false for nil" do
|
|
98
|
+
expect(freeze_node_class.freeze_end?(nil)).to(be(false))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe ".pattern_types" do
|
|
103
|
+
it "returns an array of symbols" do
|
|
104
|
+
types = freeze_node_class.pattern_types
|
|
105
|
+
expect(types).to(be_an(Array))
|
|
106
|
+
expect(types).to(all(be_a(Symbol)))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "includes :hash_comment" do
|
|
110
|
+
expect(freeze_node_class.pattern_types).to(include(:hash_comment))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "instance methods" do
|
|
116
|
+
let(:freeze_node) { build_freeze_node.call(start_line: 5, end_line: 10) }
|
|
117
|
+
|
|
118
|
+
describe "#start_line" do
|
|
119
|
+
it "returns the start line number" do
|
|
120
|
+
expect(freeze_node.start_line).to(eq(5))
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe "#end_line" do
|
|
125
|
+
it "returns the end line number" do
|
|
126
|
+
expect(freeze_node.end_line).to(eq(10))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe "#location" do
|
|
131
|
+
it "returns a location object" do
|
|
132
|
+
expect(freeze_node.location).to(respond_to(:start_line))
|
|
133
|
+
expect(freeze_node.location).to(respond_to(:end_line))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "has correct line numbers" do
|
|
137
|
+
expect(freeze_node.location.start_line).to(eq(5))
|
|
138
|
+
expect(freeze_node.location.end_line).to(eq(10))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe "#freeze_node?" do
|
|
143
|
+
it "returns true" do
|
|
144
|
+
expect(freeze_node.freeze_node?).to(be(true))
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
describe "#signature" do
|
|
149
|
+
it "returns an Array" do
|
|
150
|
+
expect(freeze_node.signature).to(be_an(Array))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "starts with :FreezeNode" do
|
|
154
|
+
expect(freeze_node.signature.first).to(eq(:FreezeNode))
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe "#inspect" do
|
|
159
|
+
it "returns a string representation" do
|
|
160
|
+
expect(freeze_node.inspect).to(be_a(String))
|
|
161
|
+
expect(freeze_node.inspect).to(include("5"))
|
|
162
|
+
expect(freeze_node.inspect).to(include("10"))
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
describe "#pattern_type" do
|
|
167
|
+
it "returns the pattern type" do
|
|
168
|
+
expect(freeze_node.pattern_type).to(be_a(Symbol))
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
describe "InvalidStructureError" do
|
|
174
|
+
it "is defined" do
|
|
175
|
+
expect(freeze_node_class::InvalidStructureError).to(be_a(Class))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "is a StandardError" do
|
|
179
|
+
expect(freeze_node_class::InvalidStructureError.ancestors).to(include(StandardError))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "accepts start_line, end_line, and unclosed_nodes" do
|
|
183
|
+
error = freeze_node_class::InvalidStructureError.new(
|
|
184
|
+
"test error",
|
|
185
|
+
start_line: 1,
|
|
186
|
+
end_line: 10,
|
|
187
|
+
unclosed_nodes: [],
|
|
188
|
+
)
|
|
189
|
+
expect(error.start_line).to(eq(1))
|
|
190
|
+
expect(error.end_line).to(eq(10))
|
|
191
|
+
expect(error.unclosed_nodes).to(eq([]))
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
describe "Location struct" do
|
|
196
|
+
let(:location) { freeze_node_class::Location.new(5, 10) }
|
|
197
|
+
|
|
198
|
+
it "has start_line" do
|
|
199
|
+
expect(location.start_line).to(eq(5))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it "has end_line" do
|
|
203
|
+
expect(location.end_line).to(eq(10))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "responds to cover?" do
|
|
207
|
+
expect(location).to(respond_to(:cover?))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "#cover? returns true for lines within range" do
|
|
211
|
+
expect(location.cover?(7)).to(be(true))
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "#cover? returns false for lines outside range" do
|
|
215
|
+
expect(location.cover?(3)).to(be(false))
|
|
216
|
+
expect(location.cover?(15)).to(be(false))
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared examples for validating MergeResultBase integration
|
|
4
|
+
#
|
|
5
|
+
# Usage in your spec:
|
|
6
|
+
# require "ast/merge/rspec/shared_examples/merge_result_base"
|
|
7
|
+
#
|
|
8
|
+
# RSpec.describe MyMerge::MergeResult do
|
|
9
|
+
# it_behaves_like "Ast::Merge::MergeResultBase" do
|
|
10
|
+
# let(:merge_result_class) { MyMerge::MergeResult }
|
|
11
|
+
# # Factory to create a merge result instance
|
|
12
|
+
# let(:build_merge_result) { -> { merge_result_class.new } }
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# @note The extending class should inherit from or behave like Ast::Merge::MergeResultBase
|
|
17
|
+
|
|
18
|
+
RSpec.shared_examples("Ast::Merge::MergeResultBase") do
|
|
19
|
+
# Required let blocks:
|
|
20
|
+
# - merge_result_class: The class under test
|
|
21
|
+
# - build_merge_result: Lambda that creates a merge result instance
|
|
22
|
+
|
|
23
|
+
describe "decision constants" do
|
|
24
|
+
it "has DECISION_KEPT_TEMPLATE" do
|
|
25
|
+
expect(merge_result_class::DECISION_KEPT_TEMPLATE).to(eq(:kept_template))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "has DECISION_KEPT_DEST" do
|
|
29
|
+
expect(merge_result_class::DECISION_KEPT_DEST).to(eq(:kept_destination))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "has DECISION_MERGED" do
|
|
33
|
+
expect(merge_result_class::DECISION_MERGED).to(eq(:merged))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "has DECISION_ADDED" do
|
|
37
|
+
expect(merge_result_class::DECISION_ADDED).to(eq(:added))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "has DECISION_FREEZE_BLOCK" do
|
|
41
|
+
expect(merge_result_class::DECISION_FREEZE_BLOCK).to(eq(:freeze_block))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "has DECISION_REPLACED" do
|
|
45
|
+
expect(merge_result_class::DECISION_REPLACED).to(eq(:replaced))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "has DECISION_APPENDED" do
|
|
49
|
+
expect(merge_result_class::DECISION_APPENDED).to(eq(:appended))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "instance methods" do
|
|
54
|
+
let(:result) { build_merge_result.call }
|
|
55
|
+
|
|
56
|
+
describe "#lines" do
|
|
57
|
+
it "returns an Array" do
|
|
58
|
+
expect(result.lines).to(be_an(Array))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "is initially empty" do
|
|
62
|
+
expect(result.lines).to(be_empty)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "#decisions" do
|
|
67
|
+
it "returns an Array" do
|
|
68
|
+
expect(result.decisions).to(be_an(Array))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "is initially empty" do
|
|
72
|
+
expect(result.decisions).to(be_empty)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe "#empty?" do
|
|
77
|
+
it "returns true when no lines" do
|
|
78
|
+
expect(result.empty?).to(be(true))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "#line_count" do
|
|
83
|
+
it "returns 0 for empty result" do
|
|
84
|
+
expect(result.line_count).to(eq(0))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe "#decision_summary" do
|
|
89
|
+
it "returns a Hash" do
|
|
90
|
+
expect(result.decision_summary).to(be_a(Hash))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "returns empty hash for empty result" do
|
|
94
|
+
expect(result.decision_summary).to(eq({}))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe "#inspect" do
|
|
99
|
+
it "returns a string representation" do
|
|
100
|
+
expect(result.inspect).to(be_a(String))
|
|
101
|
+
expect(result.inspect).to(include("lines="))
|
|
102
|
+
expect(result.inspect).to(include("decisions="))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|