unified 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.
Files changed (39) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +33 -0
  6. data/Rakefile +1 -0
  7. data/lib/unified.rb +8 -0
  8. data/lib/unified/chunk.rb +88 -0
  9. data/lib/unified/diff.rb +74 -0
  10. data/lib/unified/line.rb +21 -0
  11. data/lib/unified/parser.rb +53 -0
  12. data/lib/unified/transformer.rb +25 -0
  13. data/lib/unified/version.rb +3 -0
  14. data/spec/chunk_spec.rb +134 -0
  15. data/spec/diff_spec.rb +203 -0
  16. data/spec/examples/invalid/no_chunk_header.diff +10 -0
  17. data/spec/examples/invalid/no_content.diff +0 -0
  18. data/spec/examples/invalid/no_modifications.diff +11 -0
  19. data/spec/examples/invalid/no_modified_file_header.diff +6 -0
  20. data/spec/examples/invalid/no_original_file_header.diff +6 -0
  21. data/spec/examples/valid/.DS_Store +0 -0
  22. data/spec/examples/valid/commit_revision.diff +5 -0
  23. data/spec/examples/valid/modified_filename.diff +7 -0
  24. data/spec/examples/valid/more_additions.diff +7 -0
  25. data/spec/examples/valid/more_deletions.diff +10 -0
  26. data/spec/examples/valid/multi_chunk.diff +19 -0
  27. data/spec/examples/valid/new_file.diff +8 -0
  28. data/spec/examples/valid/no_newline.diff +29 -0
  29. data/spec/examples/valid/null_git.diff +8 -0
  30. data/spec/examples/valid/removed_file.diff +8 -0
  31. data/spec/examples/valid/simple.diff +11 -0
  32. data/spec/examples/valid/simple_git.diff +29 -0
  33. data/spec/line_spec.rb +45 -0
  34. data/spec/parser_spec.rb +344 -0
  35. data/spec/spec_helper.rb +21 -0
  36. data/spec/support/matchers/parse_matchers.rb +53 -0
  37. data/spec/transformer_spec.rb +82 -0
  38. data/unified.gemspec +26 -0
  39. metadata +173 -0
@@ -0,0 +1,344 @@
1
+ require "unified/parser"
2
+ require "spec_helper"
3
+
4
+ # Hashes used to validate captures
5
+ header_without_revision = {
6
+ :filename => {:string => "Filename"},
7
+ }
8
+ header_with_revision = {
9
+ :revision => {:string => "revision"}
10
+ }.merge(header_without_revision)
11
+
12
+ original_file_header = {
13
+ :original => header_with_revision
14
+ }
15
+
16
+ modified_file_header = {
17
+ :modified => header_with_revision
18
+ }
19
+
20
+ describe "Unified::Parser" do
21
+ let(:parser) { Unified::Parser.new }
22
+ subject { parser }
23
+
24
+ it { should parse_all valid_diffs }
25
+ it { should_not parse_any invalid_diffs }
26
+
27
+ describe "#minus" do
28
+ subject { parser.minus }
29
+
30
+ it { should parse "-" }
31
+ it { should_not parse_empty_string }
32
+ it { should_not parse "+" }
33
+ end
34
+
35
+ describe "#plus" do
36
+ subject { parser.plus }
37
+
38
+ it { should parse "+" }
39
+ it { should_not parse_empty_string }
40
+ it { should_not parse "-" }
41
+ end
42
+
43
+ describe "#digit" do
44
+ subject { parser.digit }
45
+
46
+ it { should parse "9" }
47
+ it { should parse "0" }
48
+
49
+ it { should_not parse "a" }
50
+ it { should_not parse_empty_string }
51
+ it { should_not parse "2141" }
52
+ end
53
+
54
+ describe "#digits" do
55
+ subject { parser.digits }
56
+
57
+ it { should parse "123" }
58
+ it { should parse "1" }
59
+ it { should_not parse "a" }
60
+ it { should_not parse_empty_string }
61
+
62
+ end
63
+
64
+ describe "#space" do
65
+ subject { parser.space }
66
+
67
+ it { should parse " " }
68
+ it { should parse "\s" }
69
+ it { should_not parse_empty_string }
70
+ it { should_not parse "some content"}
71
+ it { should_not parse " " }
72
+ it { should_not parse "\t" }
73
+ end
74
+
75
+ describe "#space?" do
76
+ subject { parser.space? }
77
+
78
+ it { should parse " " }
79
+ it { should parse_empty_string }
80
+ it { should_not parse "a" }
81
+ it { should_not parse " " }
82
+ end
83
+
84
+ describe "#tab" do
85
+ subject { parser.tab }
86
+
87
+ it { should parse "\t" }
88
+ it { should_not parse " " }
89
+ end
90
+
91
+ describe "#newline" do
92
+ subject { parser.newline }
93
+
94
+ it { should parse "\n" }
95
+ it { should parse "\r\n" }
96
+
97
+ it { should_not parse_empty_string }
98
+ it { should_not parse "\n\n" }
99
+ end
100
+
101
+ describe "#rest_of_line" do
102
+ subject { parser.rest_of_line }
103
+
104
+ it { should parse "a long string of content" }
105
+ it { should parse "strings with\t(special_characters)" }
106
+ it { should parse_empty_string }
107
+ it { should_not parse "strings ending with a newline\n" }
108
+ end
109
+
110
+ describe "#text_until_tab" do
111
+ subject { parser.text_until_tab }
112
+
113
+ it { should parse "a string with names in" }
114
+ it { should_not parse "a string with a tab\t" }
115
+
116
+ it "should capture filename as {:string => 'filename'}" do
117
+ expect(subject.parse("String until tab")).to eq :string => "String until tab"
118
+ end
119
+ end
120
+
121
+ describe "#header_information" do
122
+ subject { parser.header_information }
123
+
124
+ it { should parse "This/is/a/filename" }
125
+ it { should parse "Filename" }
126
+ it { should parse "Filename\trevision" }
127
+
128
+ it "should capture header as {:filename => 'filename', :revision => 'revision'}" do
129
+
130
+
131
+ expect(subject.parse("Filename")).to eq header_without_revision
132
+ expect(subject.parse("Filename\trevision")).to eq header_with_revision
133
+ end
134
+ end
135
+
136
+ describe "#original_file_marker" do
137
+ subject { parser.original_file_marker }
138
+
139
+ it { should parse "--- " }
140
+ it { should_not parse_empty_string }
141
+ it { should_not parse "-- " }
142
+ it { should_not parse "---" }
143
+ it { should_not parse "---- " }
144
+ end
145
+
146
+ describe "#modified_file_marker" do
147
+ subject { parser.modified_file_marker }
148
+
149
+ it { should parse "+++ " }
150
+ it { should_not parse_empty_string }
151
+ it { should_not parse "++ " }
152
+ it { should_not parse "+++" }
153
+ it { should_not parse "++++ " }
154
+ end
155
+
156
+ describe "#original_file_header" do
157
+ subject { parser.original_file_header }
158
+
159
+ it { should parse "--- Filename\t2002-02-21 23:30:39.942229878 -0800\n" }
160
+ it { should parse "--- Filename\t2002-02-21 23:30:39.942229878 -0800\r\n" }
161
+ it { should parse "--- Filename\t(working copy)\n" }
162
+ it { should parse "--- Filename\t(working copy)\r\n" }
163
+ it { should parse "--- Filename\n" }
164
+
165
+ it { should_not parse "--- Filename\t(working copy)"}
166
+
167
+ it "should capture all header information" do
168
+ expect(subject.parse("--- Filename\trevision\n")).to eq original_file_header
169
+ end
170
+ end
171
+
172
+ describe "#modified_file_header" do
173
+ subject { parser.modified_file_header }
174
+
175
+ it { should parse "+++ Filename\t2002-02-21 23:30:39.942229878 -0800\n" }
176
+ it { should parse "+++ Filename\t2002-02-21 23:30:39.942229878 -0800\r\n" }
177
+ it { should parse "+++ Filename\t(working copy)\n" }
178
+ it { should parse "+++ Filename\t(working copy)\r\n" }
179
+ it { should parse "+++ Filename\n"}
180
+
181
+ it { should_not parse "+++ Filename\t(working copy)"}
182
+
183
+ it "should capture all header information" do
184
+ expect(subject.parse("+++ Filename\trevision\n")).to eq modified_file_header
185
+ end
186
+ end
187
+
188
+ describe "#chunk_marker" do
189
+ subject { parser.chunk_marker }
190
+
191
+ it { should parse "@@" }
192
+ it { should_not parse "@" }
193
+ it { should_not parse " @@" }
194
+ it { should_not parse "@@ " }
195
+ end
196
+
197
+ describe "#line_numbers" do
198
+ subject { parser.line_numbers }
199
+
200
+ it { should parse "1" }
201
+ it { should parse "1,5" }
202
+ it { should parse "101,101" }
203
+
204
+ it "captures line numbers as {:line_number => 'digits'}" do
205
+ expect(subject.parse("1,5")).to eq :line_number => "1"
206
+ expect(subject.parse("101,101")).to eq :line_number => "101"
207
+ expect(subject.parse("1")).to eq :line_number => "1"
208
+ end
209
+ end
210
+
211
+ describe "#chunk_header" do
212
+ subject { parser.chunk_header }
213
+
214
+ it { should parse "@@ -1 +1 @@\n" }
215
+ it { should parse "@@ -1,5 +1,5 @@\n" }
216
+ it { should parse "@@ -101,15 +102,14 @@ Some optional heading\n" }
217
+
218
+ it "captures all required information" do
219
+ expect(subject.parse("@@ -1 +1 @@\n")).to eq({
220
+ :original => {
221
+ :line_number => "1"
222
+ },
223
+ :modified => {
224
+ :line_number => "1"
225
+ }
226
+ })
227
+
228
+ expect(subject.parse("@@ -101,15 +102,14 @@ Some optional heading\n")).to eq({
229
+ :original => {
230
+ :line_number => "101"
231
+ },
232
+ :modified => {
233
+ :line_number => "102"
234
+ },
235
+ :section_heading => { string: "Some optional heading" }
236
+ })
237
+ end
238
+ end
239
+
240
+ describe "#added_line" do
241
+ subject { parser.added_line }
242
+
243
+ it { should parse "+This is an additional line" }
244
+ it { should_not parse " +This is not a valid additional line" }
245
+ it { should parse "+++Even though this looks like a header, it should parse" }
246
+ end
247
+
248
+ describe "#deleted_line" do
249
+ subject { parser.deleted_line }
250
+
251
+ it { should parse "-This is a deleted line" }
252
+ it { should_not parse " -This is not a valid deleted line" }
253
+ it { should parse "---Even though this looks like a header, it should parse" }
254
+ end
255
+
256
+ describe "#unchanged_line" do
257
+ subject { parser.unchanged_line }
258
+
259
+ it { should parse " this line is unchanged" }
260
+ it { should_not parse "This has no leading space" }
261
+ it { should_not parse_empty_string }
262
+ end
263
+
264
+ describe "#no_newline_notice" do
265
+ subject { parser.no_newline_notice }
266
+
267
+ it { should parse "\" }
268
+ end
269
+
270
+ describe "#line" do
271
+ subject { parser.line }
272
+
273
+ it { should parse "+This is an additional line" }
274
+ it { should parse "-This is a deleted line" }
275
+ it { should parse " this line is unchanged" }
276
+ it { should parse "\" }
277
+
278
+ it "captures all line content as {:string => 'content'}" do
279
+ expect(subject.parse("+This is an additional line")).to eq :string => "+This is an additional line"
280
+ expect(subject.parse("-This is a deleted line")).to eq :string => "-This is a deleted line"
281
+ expect(subject.parse(" this line is unchanged")).to eq :string => " this line is unchanged"
282
+ expect(subject.parse("\")).to eq :string => "\"
283
+ end
284
+ end
285
+
286
+ describe "#chunk" do
287
+ subject { parser.chunk }
288
+ valid_chunk = "@@ -1 +1 @@\n+addition\n-deletion\n no change"
289
+ a_chunk_with_no_lines = "@@ -1 +1 @@\n"
290
+ a_chunk_with_no_header = "+addition\n-deletion\n no change"
291
+
292
+ it { should parse valid_chunk }
293
+ it { should_not parse a_chunk_with_no_lines }
294
+ it { should_not parse a_chunk_with_no_header }
295
+
296
+ context "captures" do
297
+ let(:capture) { subject.parse valid_chunk }
298
+ it "captures as chunk" do
299
+ capture.should include :chunk
300
+ end
301
+
302
+ it "captures lines" do
303
+ capture[:chunk].should include :lines
304
+ capture[:chunk][:lines].size.should == 3
305
+ capture[:chunk][:lines].each do |line|
306
+ line.should include :line
307
+ end
308
+ end
309
+ end
310
+
311
+ end
312
+
313
+ describe "#unified_diff_header" do
314
+ subject { parser.unified_diff_header }
315
+
316
+ it { should parse "--- Filename\n+++ Filename\n" }
317
+ it { should_not parse "--- Missing header\n" }
318
+ it { should_not parse "\n+++ Missing header\n"}
319
+ end
320
+
321
+ describe "#unified_diff" do
322
+ subject { parser.unified_diff }
323
+
324
+ it { should parse valid_diffs.first }
325
+ it { should_not parse invalid_diffs.first }
326
+
327
+ context "capturing all necessary information" do
328
+ let(:result) { subject.parse valid_diffs.first }
329
+
330
+ it "captures as a diff hash" do
331
+ result.should include(:diff)
332
+ end
333
+
334
+ it "captures a diff header" do
335
+ result[:diff].should include(:diff_header)
336
+ end
337
+
338
+ it "captures chunks" do
339
+ result[:diff].should include(:chunks)
340
+ result[:diff][:chunks].size.should == 1
341
+ end
342
+ end
343
+ end
344
+ end
@@ -0,0 +1,21 @@
1
+ RSpec.configure do |config|
2
+ config.treat_symbols_as_metadata_keys_with_true_values = true
3
+ config.run_all_when_everything_filtered = true
4
+ config.filter_run :focus
5
+
6
+ config.order = 'random'
7
+ end
8
+
9
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
10
+
11
+ def load_diffs_from(dir)
12
+ Dir.glob(File.dirname(__FILE__) + '/' + dir + '/*.diff').map {|f| File.read(f) }
13
+ end
14
+
15
+ def valid_diffs
16
+ load_diffs_from("examples/valid")
17
+ end
18
+
19
+ def invalid_diffs
20
+ load_diffs_from("examples/invalid")
21
+ end
@@ -0,0 +1,53 @@
1
+ RSpec::Matchers.define :parse do |string_to_parse|
2
+ match do |parser|
3
+ begin
4
+ parser.parse(string_to_parse)
5
+ rescue Parslet::ParseFailed => e
6
+ # @exception = e
7
+ @exception = e.cause.ascii_tree
8
+ false
9
+ end
10
+ end
11
+
12
+ failure_message_for_should do |parser|
13
+ @exception + "\n\n" + string_to_parse
14
+ end
15
+ end
16
+
17
+ RSpec::Matchers.define :parse_empty_string do
18
+ match do |parser|
19
+ begin
20
+ parser.parse("")
21
+ rescue Parslet::ParseFailed => e
22
+ @exception = e.cause.ascii_tree
23
+ false
24
+ end
25
+ end
26
+ end
27
+
28
+ RSpec::Matchers.define :parse_all do |diffs|
29
+ match do |parser|
30
+ begin
31
+ diffs.each do |diff|
32
+ parser.parse(diff)
33
+ end
34
+ rescue Parslet::ParseFailed => e
35
+ @exception = e.cause.ascii_tree
36
+ false
37
+ end
38
+ end
39
+ end
40
+
41
+ # TODO Can you have aliases for RSpec matchers?
42
+ RSpec::Matchers.define :parse_any do |diffs|
43
+ match do |parser|
44
+ begin
45
+ diffs.each do |diff|
46
+ parser.parse(diff)
47
+ end
48
+ rescue Parslet::ParseFailed => e
49
+ @exception = e.cause.ascii_tree
50
+ false
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,82 @@
1
+ require "unified/transformer"
2
+ require "spec_helper"
3
+
4
+
5
+
6
+ describe "Unified::Transformer" do
7
+ def lines
8
+ [" Line 1", " Line 2", " Line 3", "-Line 5", "-Line 6", "-Line 7", "+Line 4", "+Line 5"]
9
+ end
10
+
11
+ def build_chunk_hash(original_line_number = 1, modified_line_number = 1, header = nil)
12
+ {:chunk=>{:original=>original_line_number, :modified=>modified_line_number, :section_heading=>header, :lines=>lines}}
13
+ end
14
+ let(:transformer) { Unified::Transformer.new }
15
+
16
+ context "simple values" do
17
+ it "transforms a string value" do
18
+ expect(transformer.apply({:string => "a string"})).to eq("a string")
19
+ end
20
+ end
21
+
22
+ context "Lines" do
23
+ it "transforms a line into a Unified::Line object" do
24
+ expect(transformer.apply({:line => {:string => "+A line"}})).to eq(Unified::Line.new("+A line"))
25
+ end
26
+ end
27
+
28
+ context "Chunks" do
29
+ let(:chunk) { build_chunk_hash }
30
+ it "transforms a chunk into a Unified::Chunk object" do
31
+ expect(transformer.apply(chunk)).to eq(Unified::Chunk.new(original: 1, modified: 1, section_heading: "A section heading", lines: lines))
32
+ end
33
+ end
34
+
35
+ describe "Diff header" do
36
+ let(:header) { {:original=>{:filename=>"Original", :revision=>"OrigRev"}, :modified=>{:filename=>"Modified", :revision=>"ModRev"}} }
37
+ it "transforms a diff header into filenames and revisions" do
38
+ expect(transformer.apply(header)).to eq({
39
+ original_filename: "Original",
40
+ modified_filename: "Modified",
41
+ original_revision: "OrigRev",
42
+ modified_revision: "ModRev"
43
+ })
44
+ end
45
+ end
46
+
47
+ # context "Diffs" do
48
+ # let(:chunks) { [build_chunk_hash, build_chunk_hash(101, 101)] }
49
+ # let(:diff_hash) do
50
+ # {
51
+ # :diff => {
52
+ # :diff_header => {
53
+ # :original => {
54
+ # :filename=>"Original",
55
+ # :revision=>"OriginalRevision"
56
+ # },
57
+ # :modified => {
58
+ # :filename=>"Modified",
59
+ # :revision=>"ModifiedRevision"
60
+ # }
61
+ # },
62
+ # :chunks => chunks
63
+ # }
64
+ # }
65
+ # end
66
+ # let(:diff) {
67
+ # Unified::Diff.new(
68
+ # original_filename: "Original",
69
+ # original_revision: "OriginalRevision",
70
+ # modified_filename: "Modified",
71
+ # modified_revision: "ModifiedRevision",
72
+ # chunks: [
73
+ # Unified::Chunk.new(original: 1, modified: 1, lines: lines),
74
+ # Unified::Chunk.new(original: 101, modified: 101, lines: lines)
75
+ # ]
76
+ # )
77
+ # }
78
+ # it "transforms a diff into a Unified::Diff object" do
79
+ # expect(transformer.apply(diff_hash)).to eq(diff)
80
+ # end
81
+ # end
82
+ end