semantic_puppet 0.1.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.
- 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
|