semantic_puppet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+ require 'semantic_puppet/dependency/source'
3
+
4
+ describe SemanticPuppet::Dependency::Source do
5
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+ require 'semantic_puppet/dependency/unsatisfiable_graph'
3
+
4
+ describe SemanticPuppet::Dependency::UnsatisfiableGraph do
5
+
6
+ let(:modules) { %w[ foo bar baz ] }
7
+ let(:graph) { double('Graph', :modules => modules) }
8
+ let(:instance) { described_class.new(graph) }
9
+
10
+ subject { instance }
11
+
12
+ describe '#message' do
13
+ subject { instance.message }
14
+
15
+ it { should match /#{instance.send(:sentence_from_list, modules)}/ }
16
+ end
17
+
18
+ describe '#sentence_from_list' do
19
+
20
+ subject { instance.send(:sentence_from_list, modules) }
21
+
22
+ context 'with a list of one item' do
23
+ let(:modules) { %w[ foo ] }
24
+ it { should eql 'foo' }
25
+ end
26
+
27
+ context 'with a list of two items' do
28
+ let(:modules) { %w[ foo bar ] }
29
+ it { should eql 'foo and bar' }
30
+ end
31
+
32
+ context 'with a list of three items' do
33
+ let(:modules) { %w[ foo bar baz ] }
34
+ it { should eql 'foo, bar, and baz' }
35
+ end
36
+
37
+ context 'with a list of more than three items' do
38
+ let(:modules) { %w[ foo bar baz quux ] }
39
+ it { should eql 'foo, bar, baz, and quux' }
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,383 @@
1
+ require 'spec_helper'
2
+ require 'semantic_puppet/dependency'
3
+
4
+ describe SemanticPuppet::Dependency do
5
+ def create_release(source, name, version, deps = {})
6
+ SemanticPuppet::Dependency::ModuleRelease.new(
7
+ source,
8
+ name,
9
+ SemanticPuppet::Version.parse(version),
10
+ Hash[deps.map { |k, v| [k, SemanticPuppet::VersionRange.parse(v) ] }]
11
+ )
12
+ end
13
+
14
+ describe '.sources' do
15
+ it 'defaults to an empty list' do
16
+ expect(subject.sources).to be_empty
17
+ end
18
+
19
+ it 'is frozen' do
20
+ expect(subject.sources).to be_frozen
21
+ end
22
+
23
+ it 'can be modified by using #add_source' do
24
+ subject.add_source(SemanticPuppet::Dependency::Source.new)
25
+ expect(subject.sources).to_not be_empty
26
+ end
27
+
28
+ it 'can be emptied by using #clear_sources' do
29
+ subject.add_source(SemanticPuppet::Dependency::Source.new)
30
+ subject.clear_sources
31
+ expect(subject.sources).to be_empty
32
+ end
33
+ end
34
+
35
+ describe '.query' do
36
+ context 'without sources' do
37
+ it 'returns an unsatisfied ModuleRelease' do
38
+ expect(subject.query('module_name' => '1.0.0')).to_not be_satisfied
39
+ end
40
+ end
41
+
42
+ context 'with one source' do
43
+ let(:source) { double('Source') }
44
+
45
+ before { SemanticPuppet::Dependency.add_source(source) }
46
+
47
+ it 'queries the source for release information' do
48
+ source.should_receive(:fetch).with('module_name').and_return([])
49
+
50
+ SemanticPuppet::Dependency.query('module_name' => '1.0.0')
51
+ end
52
+
53
+ it 'queries the source for each dependency' do
54
+ source.should_receive(:fetch).with('module_name').and_return([
55
+ create_release(source, 'module_name', '1.0.0', 'bar' => '1.0.0')
56
+ ])
57
+ source.should_receive(:fetch).with('bar').and_return([])
58
+
59
+ SemanticPuppet::Dependency.query('module_name' => '1.0.0')
60
+ end
61
+
62
+ it 'queries the source for each dependency only once' do
63
+ source.should_receive(:fetch).with('module_name').and_return([
64
+ create_release(
65
+ source,
66
+ 'module_name',
67
+ '1.0.0',
68
+ 'bar' => '1.0.0', 'baz' => '0.0.2'
69
+ )
70
+ ])
71
+ source.should_receive(:fetch).with('bar').and_return([
72
+ create_release(source, 'bar', '1.0.0', 'baz' => '0.0.3')
73
+ ])
74
+ source.should_receive(:fetch).with('baz').once.and_return([])
75
+
76
+ SemanticPuppet::Dependency.query('module_name' => '1.0.0')
77
+ end
78
+
79
+ it 'returns a ModuleRelease with the requested dependencies' do
80
+ source.stub(:fetch).and_return([])
81
+
82
+ result = SemanticPuppet::Dependency.query('foo' => '1.0.0', 'bar' => '1.0.0')
83
+ expect(result.dependency_names).to match_array %w[ foo bar ]
84
+ end
85
+
86
+ it 'populates the returned ModuleRelease with related dependencies' do
87
+ source.stub(:fetch).and_return(
88
+ [ foo = create_release(source, 'foo', '1.0.0', 'bar' => '1.0.0') ],
89
+ [ bar = create_release(source, 'bar', '1.0.0') ]
90
+ )
91
+
92
+ result = SemanticPuppet::Dependency.query('foo' => '1.0.0', 'bar' => '1.0.0')
93
+ expect(result.dependencies['foo']).to eql SortedSet.new([ foo ])
94
+ expect(result.dependencies['bar']).to eql SortedSet.new([ bar ])
95
+ end
96
+
97
+ it 'populates all returned ModuleReleases with related dependencies' do
98
+ source.stub(:fetch).and_return(
99
+ [ foo = create_release(source, 'foo', '1.0.0', 'bar' => '1.0.0') ],
100
+ [ bar = create_release(source, 'bar', '1.0.0', 'baz' => '0.1.0') ],
101
+ [ baz = create_release(source, 'baz', '0.1.0', 'baz' => '1.0.0') ]
102
+ )
103
+
104
+ result = SemanticPuppet::Dependency.query('foo' => '1.0.0')
105
+ expect(result.dependencies['foo']).to eql SortedSet.new([ foo ])
106
+ expect(foo.dependencies['bar']).to eql SortedSet.new([ bar ])
107
+ expect(bar.dependencies['baz']).to eql SortedSet.new([ baz ])
108
+ end
109
+ end
110
+
111
+ context 'with multiple sources' do
112
+ let(:source1) { double('SourceOne') }
113
+ let(:source2) { double('SourceTwo') }
114
+ let(:source3) { double('SourceThree') }
115
+
116
+ before do
117
+ SemanticPuppet::Dependency.add_source(source1)
118
+ SemanticPuppet::Dependency.add_source(source2)
119
+ SemanticPuppet::Dependency.add_source(source3)
120
+ end
121
+
122
+ it 'queries each source in turn' do
123
+ source1.should_receive(:fetch).with('module_name').and_return([])
124
+ source2.should_receive(:fetch).with('module_name').and_return([])
125
+ source3.should_receive(:fetch).with('module_name').and_return([])
126
+
127
+ SemanticPuppet::Dependency.query('module_name' => '1.0.0')
128
+ end
129
+
130
+ it 'resolves all dependencies against all sources' do
131
+ source1.should_receive(:fetch).with('module_name').and_return([
132
+ create_release(source1, 'module_name', '1.0.0', 'bar' => '1.0.0')
133
+ ])
134
+ source2.should_receive(:fetch).with('module_name').and_return([])
135
+ source3.should_receive(:fetch).with('module_name').and_return([])
136
+
137
+ source1.should_receive(:fetch).with('bar').and_return([])
138
+ source2.should_receive(:fetch).with('bar').and_return([])
139
+ source3.should_receive(:fetch).with('bar').and_return([])
140
+
141
+ SemanticPuppet::Dependency.query('module_name' => '1.0.0')
142
+ end
143
+ end
144
+ end
145
+
146
+ describe '.resolve' do
147
+ def add_source_modules(name, versions, deps = {})
148
+ versions = Array(versions)
149
+ releases = versions.map { |ver| create_release(source, name, ver, deps) }
150
+ source.stub(:fetch).with(name).and_return(modules[name].concat(releases))
151
+ end
152
+
153
+ def subject(specs)
154
+ graph = SemanticPuppet::Dependency.query(specs)
155
+ yield graph if block_given?
156
+ expect(graph.dependencies).to_not be_empty
157
+ result = SemanticPuppet::Dependency.resolve(graph)
158
+ expect(graph.dependencies).to_not be_empty
159
+ result.map { |rel| [ rel.name, rel.version.to_s ] }
160
+ end
161
+
162
+ let(:modules) { Hash.new { |h,k| h[k] = [] }}
163
+ let(:source) { double('Source', :priority => 0) }
164
+
165
+ before { SemanticPuppet::Dependency.add_source(source) }
166
+
167
+ context 'for a module without dependencies' do
168
+ def foo(range)
169
+ subject('foo' => range).map { |x| x.last }
170
+ end
171
+
172
+ it 'returns the greatest release matching the version range' do
173
+ add_source_modules('foo', %w[ 0.9.0 1.0.0 1.1.0 2.0.0 ])
174
+
175
+ expect(foo('1.x')).to eql %w[ 1.1.0 ]
176
+ end
177
+
178
+ context 'when the query includes both stable and prerelease versions' do
179
+ it 'returns the greatest stable release matching the range' do
180
+ add_source_modules('foo', %w[ 0.9.0 1.0.0 1.1.0 1.2.0-pre 2.0.0 ])
181
+
182
+ expect(foo('1.x')).to eql %w[ 1.1.0 ]
183
+ end
184
+ end
185
+
186
+ context 'when the query omits all stable versions' do
187
+ it 'returns the greatest prerelease version matching the range' do
188
+ add_source_modules('foo', %w[ 1.0.0 1.1.0-a 1.1.0-b 2.0.0 ])
189
+
190
+ expect(foo('1.1.x')).to eql %w[ 1.1.0-b ]
191
+ expect(foo('1.1.0-a')).to eql %w[ 1.1.0-a ]
192
+ end
193
+ end
194
+
195
+ context 'when the query omits all versions' do
196
+ it 'fails with an appropriate message' do
197
+ add_source_modules('foo', %w[ 1.0.0 1.1.0-a 1.1.0 ])
198
+
199
+ with_message = /Could not find satisfying releases/
200
+ expect { foo('2.x') }.to raise_exception with_message
201
+ expect { foo('2.x') }.to raise_exception /\bfoo\b/
202
+ end
203
+ end
204
+ end
205
+
206
+ context 'for a module with dependencies' do
207
+ def foo(range)
208
+ subject('foo' => range)
209
+ end
210
+
211
+ it 'returns the greatest releases matching the dependency range' do
212
+ add_source_modules('foo', '1.1.0', 'bar' => '1.x')
213
+ add_source_modules('bar', %w[ 0.9.0 1.0.0 1.1.0 1.2.0 2.0.0 ])
214
+
215
+ expect(foo('1.1.0')).to include %w[ foo 1.1.0 ], %w[ bar 1.2.0 ]
216
+ end
217
+
218
+ context 'when the dependency has both stable and prerelease versions' do
219
+ it 'returns the greatest stable release matching the range' do
220
+ add_source_modules('foo', '1.1.0', 'bar' => '1.x')
221
+ add_source_modules('bar', %w[ 0.9.0 1.0.0 1.1.0 1.2.0-pre 2.0.0 ])
222
+
223
+ expect(foo('1.1.0')).to include %w[ foo 1.1.0 ], %w[ bar 1.1.0 ]
224
+ end
225
+ end
226
+
227
+ context 'when the dependency has no stable versions' do
228
+ it 'returns the greatest prerelease version matching the range' do
229
+ add_source_modules('foo', '1.1.0', 'bar' => '1.1.x')
230
+ add_source_modules('foo', '1.1.1', 'bar' => '1.1.0-a')
231
+ add_source_modules('bar', %w[ 1.0.0 1.1.0-a 1.1.0-b 2.0.0 ])
232
+
233
+ expect(foo('1.1.0')).to include %w[ foo 1.1.0 ], %w[ bar 1.1.0-b ]
234
+ expect(foo('1.1.1')).to include %w[ foo 1.1.1 ], %w[ bar 1.1.0-a ]
235
+ end
236
+ end
237
+
238
+ context 'when the dependency cannot be satisfied' do
239
+ it 'fails with an appropriate message' do
240
+ add_source_modules('foo', %w[ 1.1.0 ], 'bar' => '1.x')
241
+ add_source_modules('bar', %w[ 0.0.1 0.1.0-a 0.1.0 ])
242
+
243
+ with_message = /Could not find satisfying releases/
244
+ expect { foo('1.1.0') }.to raise_exception with_message
245
+ expect { foo('1.1.0') }.to raise_exception /\bfoo\b/
246
+ end
247
+ end
248
+ end
249
+
250
+ context 'for a module with competing dependencies' do
251
+ def foo(range)
252
+ subject('foo' => range)
253
+ end
254
+
255
+ context 'that overlap' do
256
+ it 'returns the greatest release satisfying all dependencies' do
257
+ add_source_modules('foo', '1.1.0', 'bar' => '1.0.0', 'baz' => '1.0.0')
258
+ add_source_modules('bar', '1.0.0', 'quxx' => '1.x')
259
+ add_source_modules('baz', '1.0.0', 'quxx' => '1.1.x')
260
+ add_source_modules('quxx', %w[ 0.9.0 1.0.0 1.1.0 1.1.1 1.2.0 2.0.0 ])
261
+
262
+ expect(foo('1.1.0')).to_not include %w[ quxx 1.2.0 ]
263
+ expect(foo('1.1.0')).to include %w[ quxx 1.1.1 ]
264
+ end
265
+ end
266
+
267
+ context 'that do not overlap' do
268
+ it 'fails with an appropriate message' do
269
+ add_source_modules('foo','1.1.0', 'bar' => '1.0.0', 'baz' => '1.0.0')
270
+ add_source_modules('bar','1.0.0', 'quxx' => '1.x')
271
+ add_source_modules('baz','1.0.0', 'quxx' => '2.x')
272
+ add_source_modules('quxx', %w[ 0.9.0 1.0.0 1.1.0 1.1.1 1.2.0 2.0.0 ])
273
+
274
+ with_message = /Could not find satisfying releases/
275
+ expect { foo('1.1.0') }.to raise_exception with_message
276
+ expect { foo('1.1.0') }.to raise_exception /\bfoo\b/
277
+ end
278
+ end
279
+ end
280
+
281
+ context 'for a module with circular dependencies' do
282
+ def foo(range)
283
+ subject('foo' => range)
284
+ end
285
+
286
+ context 'that can be resolved' do
287
+ it 'terminates' do
288
+ add_source_modules('foo', '1.1.0', 'foo' => '1.x')
289
+
290
+ expect(foo('1.1.0')).to include %w[ foo 1.1.0 ]
291
+ end
292
+ end
293
+
294
+ context 'that cannot be resolved' do
295
+ it 'fails with an appropriate message' do
296
+ add_source_modules('foo', '1.1.0', 'foo' => '1.0.0')
297
+
298
+ with_message = /Could not find satisfying releases/
299
+ expect { foo('1.1.0') }.to raise_exception with_message
300
+ expect { foo('1.1.0') }.to raise_exception /\bfoo\b/
301
+ end
302
+ end
303
+ end
304
+
305
+ context 'for a module with dependencies' do
306
+ context 'that violate module constraints on the graph' do
307
+ def foo(range)
308
+ subject('foo' => range) do |graph|
309
+ graph.add_constraint('no downgrade', 'bar', '> 3.0.0') do |node|
310
+ SemanticPuppet::VersionRange.parse('> 3.0.0') === node.version
311
+ end
312
+ end
313
+ end
314
+
315
+ context 'that can be resolved' do
316
+ it 'terminates' do
317
+ add_source_modules('foo', '1.1.0', 'bar' => '1.x')
318
+ add_source_modules('foo', '1.2.0', 'bar' => '>= 2.0.0')
319
+ add_source_modules('bar', '1.0.0')
320
+ add_source_modules('bar', '2.0.0', 'baz' => '>= 1.0.0')
321
+ add_source_modules('bar', '3.0.0')
322
+ add_source_modules('bar', '3.0.1')
323
+ add_source_modules('baz', '1.0.0')
324
+
325
+ expect(foo('1.x')).to include %w[ foo 1.2.0 ], %w[ bar 3.0.1 ]
326
+ end
327
+ end
328
+
329
+ context 'that cannot be resolved' do
330
+ it 'fails with an appropriate message' do
331
+ add_source_modules('foo', '1.1.0', 'bar' => '1.x')
332
+ add_source_modules('foo', '1.2.0', 'bar' => '2.x')
333
+ add_source_modules('bar', '1.0.0', 'baz' => '1.x')
334
+ add_source_modules('bar', '2.0.0', 'baz' => '1.x')
335
+ add_source_modules('baz', '1.0.0')
336
+ add_source_modules('baz', '3.0.0')
337
+ add_source_modules('baz', '3.0.1')
338
+
339
+ with_message = /Could not find satisfying releases/
340
+ expect { foo('1.x') }.to raise_exception with_message
341
+ expect { foo('1.x') }.to raise_exception /\bfoo\b/
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ context 'that violate graph constraints' do
348
+ def foo(range)
349
+ subject('foo' => range) do |graph|
350
+ graph.add_graph_constraint('uniqueness') do |nodes|
351
+ nodes.none? { |node| node.name =~ /z/ }
352
+ end
353
+ end
354
+ end
355
+
356
+ context 'that can be resolved' do
357
+ it 'terminates' do
358
+ add_source_modules('foo', '1.1.0', 'bar' => '1.x')
359
+ add_source_modules('foo', '1.2.0', 'bar' => '2.x')
360
+ add_source_modules('bar', '1.0.0')
361
+ add_source_modules('bar', '2.0.0', 'baz' => '1.0.0')
362
+ add_source_modules('baz', '1.0.0')
363
+
364
+ expect(foo('1.x')).to include %w[ foo 1.1.0 ], %w[ bar 1.0.0 ]
365
+ end
366
+ end
367
+
368
+ context 'that cannot be resolved' do
369
+ it 'fails with an appropriate message' do
370
+ add_source_modules('foo', '1.1.0', 'bar' => '1.x')
371
+ add_source_modules('foo', '1.2.0', 'bar' => '2.x')
372
+ add_source_modules('bar', '1.0.0', 'baz' => '1.0.0')
373
+ add_source_modules('bar', '2.0.0', 'baz' => '1.0.0')
374
+ add_source_modules('baz', '1.0.0')
375
+
376
+ with_message = /Could not find satisfying releases/
377
+ expect { foo('1.1.0') }.to raise_exception with_message
378
+ expect { foo('1.1.0') }.to raise_exception /\bfoo\b/
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end
@@ -0,0 +1,307 @@
1
+ require 'spec_helper'
2
+ require 'semantic_puppet/version'
3
+
4
+ describe SemanticPuppet::VersionRange do
5
+
6
+ describe '.parse' do
7
+ def self.test_range(range_list, str, includes, excludes)
8
+ Array(range_list).each do |expr|
9
+ example "#{expr.inspect} stringifies as #{str}" do
10
+ range = SemanticPuppet::VersionRange.parse(expr)
11
+ expect(range.to_s).to eql str
12
+ end
13
+
14
+ includes.each do |vstring|
15
+ example "#{expr.inspect} includes #{vstring}" do
16
+ range = SemanticPuppet::VersionRange.parse(expr)
17
+ expect(range).to include(SemanticPuppet::Version.parse(vstring))
18
+ end
19
+
20
+ example "parse(#{expr.inspect}).to_s includes #{vstring}" do
21
+ range = SemanticPuppet::VersionRange.parse(expr)
22
+ range = SemanticPuppet::VersionRange.parse(range.to_s)
23
+ expect(range).to include(SemanticPuppet::Version.parse(vstring))
24
+ end
25
+ end
26
+
27
+ excludes.each do |vstring|
28
+ example "#{expr.inspect} excludes #{vstring}" do
29
+ range = SemanticPuppet::VersionRange.parse(expr)
30
+ expect(range).to_not include(SemanticPuppet::Version.parse(vstring))
31
+ end
32
+
33
+ example "parse(#{expr.inspect}).to_s excludes #{vstring}" do
34
+ range = SemanticPuppet::VersionRange.parse(expr)
35
+ range = SemanticPuppet::VersionRange.parse(range.to_s)
36
+ expect(range).to_not include(SemanticPuppet::Version.parse(vstring))
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ context 'loose version expressions' do
43
+ expressions = {
44
+ [ '1.2.3-alpha' ] => {
45
+ :to_str => '1.2.3-alpha',
46
+ :includes => [ '1.2.3-alpha' ],
47
+ :excludes => [ '1.2.3-999', '1.2.3-beta' ],
48
+ },
49
+ [ '1.2.3' ] => {
50
+ :to_str => '1.2.3',
51
+ :includes => [ '1.2.3-alpha', '1.2.3' ],
52
+ :excludes => [ '1.2.2', '1.2.4-alpha' ],
53
+ },
54
+ [ '1.2', '1.2.x', '1.2.X' ] => {
55
+ :to_str => '1.2.x',
56
+ :includes => [ '1.2.0-alpha', '1.2.0', '1.2.999' ],
57
+ :excludes => [ '1.1.999', '1.3.0-0' ],
58
+ },
59
+ [ '1', '1.x', '1.X' ] => {
60
+ :to_str => '1.x',
61
+ :includes => [ '1.0.0-alpha', '1.999.0' ],
62
+ :excludes => [ '0.999.999', '2.0.0-0' ],
63
+ },
64
+ }
65
+
66
+ expressions.each do |range, vs|
67
+ test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
68
+ end
69
+ end
70
+
71
+ context 'open-ended expressions' do
72
+ expressions = {
73
+ [ '>1.2.3', '> 1.2.3' ] => {
74
+ :to_str => '>=1.2.4',
75
+ :includes => [ '1.2.4-0', '999.0.0' ],
76
+ :excludes => [ '1.2.3' ],
77
+ },
78
+ [ '>1.2.3-alpha', '> 1.2.3-alpha' ] => {
79
+ :to_str => '>1.2.3-alpha',
80
+ :includes => [ '1.2.3-alpha.0', '1.2.3-alpha0', '999.0.0' ],
81
+ :excludes => [ '1.2.3-alpha' ],
82
+ },
83
+
84
+ [ '>=1.2.3', '>= 1.2.3' ] => {
85
+ :to_str => '>=1.2.3',
86
+ :includes => [ '1.2.3-0', '999.0.0' ],
87
+ :excludes => [ '1.2.2' ],
88
+ },
89
+ [ '>=1.2.3-alpha', '>= 1.2.3-alpha' ] => {
90
+ :to_str => '>=1.2.3-alpha',
91
+ :includes => [ '1.2.3-alpha', '1.2.3-alpha0', '999.0.0' ],
92
+ :excludes => [ '1.2.3-alph' ],
93
+ },
94
+
95
+ [ '<1.2.3', '< 1.2.3' ] => {
96
+ :to_str => '<1.2.3',
97
+ :includes => [ '0.0.0-0', '1.2.2' ],
98
+ :excludes => [ '1.2.3-0', '2.0.0' ],
99
+ },
100
+ [ '<1.2.3-alpha', '< 1.2.3-alpha' ] => {
101
+ :to_str => '<1.2.3-alpha',
102
+ :includes => [ '0.0.0-0', '1.2.3-alph' ],
103
+ :excludes => [ '1.2.3-alpha', '2.0.0' ],
104
+ },
105
+
106
+ [ '<=1.2.3', '<= 1.2.3' ] => {
107
+ :to_str => '<1.2.4',
108
+ :includes => [ '0.0.0-0', '1.2.3' ],
109
+ :excludes => [ '1.2.4-0' ],
110
+ },
111
+ [ '<=1.2.3-alpha', '<= 1.2.3-alpha' ] => {
112
+ :to_str => '<=1.2.3-alpha',
113
+ :includes => [ '0.0.0-0', '1.2.3-alpha' ],
114
+ :excludes => [ '1.2.3-alpha0', '1.2.3-alpha.0', '1.2.3-alpha'.next ],
115
+ },
116
+ }
117
+
118
+ expressions.each do |range, vs|
119
+ test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
120
+ end
121
+ end
122
+
123
+ context '"reasonably close" expressions' do
124
+ expressions = {
125
+ [ '~ 1', '~1' ] => {
126
+ :to_str => '1.x',
127
+ :includes => [ '1.0.0-0', '1.999.999' ],
128
+ :excludes => [ '0.999.999', '2.0.0-0' ],
129
+ },
130
+ [ '~ 1.2', '~1.2' ] => {
131
+ :to_str => '1.2.x',
132
+ :includes => [ '1.2.0-0', '1.2.999' ],
133
+ :excludes => [ '1.1.999', '1.3.0-0' ],
134
+ },
135
+ [ '~ 1.2.3', '~1.2.3' ] => {
136
+ :to_str => '>=1.2.3 <1.3.0',
137
+ :includes => [ '1.2.3-0', '1.2.5' ],
138
+ :excludes => [ '1.2.2', '1.3.0-0' ],
139
+ },
140
+ [ '~ 1.2.3-alpha', '~1.2.3-alpha' ] => {
141
+ :to_str => '>=1.2.3-alpha <1.2.4',
142
+ :includes => [ '1.2.3-alpha', '1.2.3' ],
143
+ :excludes => [ '1.2.3-alph', '1.2.4-0' ],
144
+ },
145
+ }
146
+
147
+ expressions.each do |range, vs|
148
+ test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
149
+ end
150
+ end
151
+
152
+ context 'inclusive range expressions' do
153
+ expressions = {
154
+ '1.2.3 - 1.3.4' => {
155
+ :to_str => '>=1.2.3 <1.3.5',
156
+ :includes => [ '1.2.3-0', '1.3.4' ],
157
+ :excludes => [ '1.2.2', '1.3.5-0' ],
158
+ },
159
+ '1.2.3 - 1.3.4-alpha' => {
160
+ :to_str => '>=1.2.3 <=1.3.4-alpha',
161
+ :includes => [ '1.2.3-0', '1.3.4-alpha' ],
162
+ :excludes => [ '1.2.2', '1.3.4-alpha0', '1.3.5' ],
163
+ },
164
+
165
+ '1.2.3-alpha - 1.3.4' => {
166
+ :to_str => '>=1.2.3-alpha <1.3.5',
167
+ :includes => [ '1.2.3-alpha', '1.3.4' ],
168
+ :excludes => [ '1.2.3-alph', '1.3.5-0' ],
169
+ },
170
+ '1.2.3-alpha - 1.3.4-alpha' => {
171
+ :to_str => '>=1.2.3-alpha <=1.3.4-alpha',
172
+ :includes => [ '1.2.3-alpha', '1.3.4-alpha' ],
173
+ :excludes => [ '1.2.3-alph', '1.3.4-alpha0', '1.3.5' ],
174
+ },
175
+ }
176
+
177
+ expressions.each do |range, vs|
178
+ test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
179
+ end
180
+ end
181
+
182
+ context 'unioned expressions' do
183
+ expressions = {
184
+ [ '1.2 <1.2.5' ] => {
185
+ :to_str => '>=1.2.0 <1.2.5',
186
+ :includes => [ '1.2.0-0', '1.2.4' ],
187
+ :excludes => [ '1.1.999', '1.2.5-0', '1.9.0' ],
188
+ },
189
+ [ '1 <=1.2.5' ] => {
190
+ :to_str => '>=1.0.0 <1.2.6',
191
+ :includes => [ '1.0.0-0', '1.2.5' ],
192
+ :excludes => [ '0.999.999', '1.2.6-0', '1.9.0' ],
193
+ },
194
+ [ '>1.0.0 >2.0.0 >=3.0.0 <5.0.0' ] => {
195
+ :to_str => '>=3.0.0 <5.0.0',
196
+ :includes => [ '3.0.0-0', '4.999.999' ],
197
+ :excludes => [ '2.999.999', '5.0.0-0' ],
198
+ },
199
+ [ '<1.0.0 >2.0.0' ] => {
200
+ :to_str => '<0.0.0',
201
+ :includes => [ ],
202
+ :excludes => [ '0.0.0-0' ],
203
+ },
204
+ }
205
+
206
+ expressions.each do |range, vs|
207
+ test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
208
+ end
209
+ end
210
+
211
+ context 'invalid expressions' do
212
+ example 'raise an appropriate exception' do
213
+ ex = [ ArgumentError, 'Unparsable version range: "invalid"' ]
214
+ expect { SemanticPuppet::VersionRange.parse('invalid') }.to raise_error(*ex)
215
+ end
216
+ end
217
+ end
218
+
219
+ describe '#intersection' do
220
+ def self.v(num)
221
+ SemanticPuppet::Version.parse("#{num}.0.0")
222
+ end
223
+
224
+ def self.range(x, y, ex = false)
225
+ SemanticPuppet::VersionRange.new(v(x), v(y), ex)
226
+ end
227
+
228
+ EMPTY_RANGE = SemanticPuppet::VersionRange::EMPTY_RANGE
229
+
230
+ tests = {
231
+ # This falls entirely before the target range
232
+ range(1, 4) => [ EMPTY_RANGE ],
233
+
234
+ # This falls entirely after the target range
235
+ range(11, 15) => [ EMPTY_RANGE ],
236
+
237
+ # This overlaps the beginning of the target range
238
+ range(1, 6) => [ range(5, 6) ],
239
+
240
+ # This overlaps the end of the target range
241
+ range(9, 15) => [ range(9, 10), range(9, 10, true) ],
242
+
243
+ # This shares the first value of the target range
244
+ range(1, 5) => [ range(5, 5) ],
245
+
246
+ # This shares the last value of the target range
247
+ range(10, 15) => [ range(10, 10), EMPTY_RANGE ],
248
+
249
+ # This shares both values with the target range
250
+ range(5, 10) => [ range(5, 10), range(5, 10, true) ],
251
+
252
+ # This is a superset of the target range
253
+ range(4, 11) => [ range(5, 10), range(5, 10, true) ],
254
+
255
+ # This is a subset of the target range
256
+ range(6, 9) => [ range(6, 9) ],
257
+
258
+ # This shares the first value of the target range, but excludes it
259
+ range(1, 5, true) => [ EMPTY_RANGE ],
260
+
261
+ # This overlaps the beginning of the target range, with an excluded end
262
+ range(1, 7, true) => [ range(5, 7, true) ],
263
+
264
+ # This shares both values with the target range, and excludes the end
265
+ range(5, 10, true) => [ range(5, 10, true) ],
266
+ }
267
+
268
+ inclusive = range(5, 10)
269
+ context "between #{inclusive} &" do
270
+ tests.each do |subject, result|
271
+ result = result.first
272
+
273
+ example subject do
274
+ expect(inclusive & subject).to eql(result)
275
+ end
276
+ end
277
+ end
278
+
279
+ exclusive = range(5, 10, true)
280
+ context "between #{exclusive} &" do
281
+ tests.each do |subject, result|
282
+ result = result.last
283
+
284
+ example subject do
285
+ expect(exclusive & subject).to eql(result)
286
+ end
287
+ end
288
+ end
289
+
290
+ context 'is commutative' do
291
+ tests.each do |subject, _|
292
+ example "between #{inclusive} & #{subject}" do
293
+ expect(inclusive & subject).to eql(subject & inclusive)
294
+ end
295
+ example "between #{exclusive} & #{subject}" do
296
+ expect(exclusive & subject).to eql(subject & exclusive)
297
+ end
298
+ end
299
+ end
300
+
301
+ it 'cannot intersect with non-VersionRanges' do
302
+ msg = "value must be a SemanticPuppet::VersionRange"
303
+ expect { inclusive.intersection(1..2) }.to raise_error(msg)
304
+ end
305
+ end
306
+
307
+ end