semantic_puppet 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +38 -0
- data/README.md +64 -0
- data/Rakefile +69 -0
- data/lib/semantic_puppet.rb +7 -0
- data/lib/semantic_puppet/dependency.rb +181 -0
- data/lib/semantic_puppet/dependency/graph.rb +60 -0
- data/lib/semantic_puppet/dependency/graph_node.rb +117 -0
- data/lib/semantic_puppet/dependency/module_release.rb +46 -0
- data/lib/semantic_puppet/dependency/source.rb +25 -0
- data/lib/semantic_puppet/dependency/unsatisfiable_graph.rb +31 -0
- data/lib/semantic_puppet/version.rb +185 -0
- data/lib/semantic_puppet/version_range.rb +422 -0
- data/semantic_puppet.gemspec +30 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/unit/semantic_puppet/dependency/graph_node_spec.rb +141 -0
- data/spec/unit/semantic_puppet/dependency/graph_spec.rb +162 -0
- data/spec/unit/semantic_puppet/dependency/module_release_spec.rb +143 -0
- data/spec/unit/semantic_puppet/dependency/source_spec.rb +5 -0
- data/spec/unit/semantic_puppet/dependency/unsatisfiable_graph_spec.rb +44 -0
- data/spec/unit/semantic_puppet/dependency_spec.rb +383 -0
- data/spec/unit/semantic_puppet/version_range_spec.rb +307 -0
- data/spec/unit/semantic_puppet/version_spec.rb +644 -0
- metadata +169 -0
@@ -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
|