vers 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +97 -49
- data/lib/vers/constraint.rb +6 -7
- data/lib/vers/interval.rb +24 -14
- data/lib/vers/parser.rb +7 -9
- data/lib/vers/version.rb +16 -13
- data/lib/vers/version_range.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13ef83ab41f6ecd9046acb7a33190abc7648805878ffb1c5cf1e3741fa3e7ae5
|
|
4
|
+
data.tar.gz: 5c8982aeb33abb1e1dec9b17a04693f6dfb7395ec29761680cdd3ca5c85df20e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5375be69c79738814d42123b157392448603c12c4b5a7f54bde3107bbf12b9f885d08efbe44cddf1c5f2d78b48394eefa73cd720e77928259227a883db273aa
|
|
7
|
+
data.tar.gz: a6aa16f032331174794e130096751ae08b37cf04ebae8da23ac07a022959ee493c766b98a873908e436d2d69d234623352857b4fd04dce4c71168c6ca4255317
|
data/Rakefile
CHANGED
|
@@ -187,74 +187,122 @@ namespace :benchmark do
|
|
|
187
187
|
task :memory do
|
|
188
188
|
require "benchmark"
|
|
189
189
|
require "json"
|
|
190
|
+
require "objspace"
|
|
190
191
|
require_relative "lib/vers"
|
|
191
|
-
|
|
192
|
-
puts "
|
|
192
|
+
|
|
193
|
+
puts "VERS Memory & Allocation Benchmarks"
|
|
193
194
|
puts "=" * 50
|
|
194
|
-
|
|
195
|
-
# Load sample ranges
|
|
195
|
+
|
|
196
196
|
test_data_file = File.join(__dir__, "test-suite-data.json")
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
unless File.exist?(test_data_file)
|
|
199
|
-
puts "
|
|
199
|
+
puts "test-suite-data.json not found. Using fallback examples."
|
|
200
200
|
sample_ranges = [
|
|
201
|
-
{ input
|
|
202
|
-
{ input
|
|
203
|
-
{ input
|
|
201
|
+
{ "input" => "^1.2.3", "scheme" => "npm" },
|
|
202
|
+
{ "input" => "~> 1.2", "scheme" => "gem" },
|
|
203
|
+
{ "input" => ">=1.0,<2.0", "scheme" => "pypi" }
|
|
204
204
|
]
|
|
205
205
|
else
|
|
206
206
|
test_data = JSON.parse(File.read(test_data_file))
|
|
207
207
|
sample_ranges = test_data.select { |data| !data["is_invalid"] }.first(100)
|
|
208
208
|
end
|
|
209
|
-
|
|
210
|
-
puts "
|
|
209
|
+
|
|
210
|
+
puts "Sample size: #{sample_ranges.length} version ranges"
|
|
211
211
|
puts
|
|
212
|
-
|
|
213
|
-
#
|
|
214
|
-
|
|
212
|
+
|
|
213
|
+
# Measure object allocations during cold parsing (no cache)
|
|
214
|
+
Vers::Version.class_variable_set(:@@version_cache, {})
|
|
215
|
+
Vers::Constraint.class_variable_set(:@@constraint_cache, {})
|
|
216
|
+
Vers::Parser.class_variable_set(:@@parser_cache, {})
|
|
217
|
+
|
|
218
|
+
GC.start
|
|
219
|
+
GC.disable
|
|
220
|
+
before = ObjectSpace.count_objects.dup
|
|
221
|
+
|
|
215
222
|
version_ranges = []
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
begin
|
|
220
|
-
parsed = Vers.parse_native(range['input'], range['scheme'])
|
|
221
|
-
version_ranges << parsed
|
|
222
|
-
rescue
|
|
223
|
-
# Skip invalid ranges
|
|
224
|
-
end
|
|
225
|
-
end
|
|
223
|
+
sample_ranges.each do |range|
|
|
224
|
+
parsed = Vers.parse_native(range['input'], range['scheme']) rescue nil
|
|
225
|
+
version_ranges << parsed if parsed
|
|
226
226
|
end
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
|
|
228
|
+
after = ObjectSpace.count_objects
|
|
229
|
+
GC.enable
|
|
230
|
+
|
|
231
|
+
string_alloc = after[:T_STRING] - before[:T_STRING]
|
|
232
|
+
array_alloc = after[:T_ARRAY] - before[:T_ARRAY]
|
|
233
|
+
hash_alloc = after[:T_HASH] - before[:T_HASH]
|
|
234
|
+
object_alloc = after[:T_OBJECT] - before[:T_OBJECT]
|
|
235
|
+
match_alloc = (after[:T_MATCH] || 0) - (before[:T_MATCH] || 0)
|
|
236
|
+
total_alloc = string_alloc + array_alloc + hash_alloc + object_alloc + match_alloc
|
|
237
|
+
|
|
238
|
+
puts "Cold parse allocations (#{version_ranges.length} ranges, no cache):"
|
|
239
|
+
puts " Total objects: #{total_alloc}"
|
|
240
|
+
puts " Strings: #{string_alloc}"
|
|
241
|
+
puts " Arrays: #{array_alloc}"
|
|
242
|
+
puts " Hashes: #{hash_alloc}"
|
|
243
|
+
puts " Objects: #{object_alloc}"
|
|
244
|
+
puts " MatchData: #{match_alloc}"
|
|
245
|
+
puts " Per range: #{(total_alloc.to_f / version_ranges.length).round(1)}"
|
|
230
246
|
puts
|
|
231
|
-
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
247
|
+
|
|
248
|
+
# Measure cached parse allocations (everything already cached)
|
|
249
|
+
GC.start
|
|
250
|
+
GC.disable
|
|
251
|
+
before = ObjectSpace.count_objects.dup
|
|
252
|
+
|
|
253
|
+
sample_ranges.each do |range|
|
|
254
|
+
Vers.parse_native(range['input'], range['scheme']) rescue nil
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
after = ObjectSpace.count_objects
|
|
258
|
+
GC.enable
|
|
259
|
+
|
|
260
|
+
cached_strings = after[:T_STRING] - before[:T_STRING]
|
|
261
|
+
cached_arrays = after[:T_ARRAY] - before[:T_ARRAY]
|
|
262
|
+
cached_objects = after[:T_OBJECT] - before[:T_OBJECT]
|
|
263
|
+
cached_match = (after[:T_MATCH] || 0) - (before[:T_MATCH] || 0)
|
|
264
|
+
cached_alloc = cached_strings + cached_arrays + cached_objects + cached_match
|
|
265
|
+
|
|
266
|
+
puts "Cached parse allocations (#{version_ranges.length} ranges, warm cache):"
|
|
267
|
+
puts " Total objects: #{cached_alloc}"
|
|
268
|
+
puts " Strings: #{cached_strings}"
|
|
269
|
+
puts " MatchData: #{cached_match}"
|
|
270
|
+
puts " Per range: #{(cached_alloc.to_f / version_ranges.length).round(1)}"
|
|
237
271
|
puts
|
|
238
|
-
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
272
|
+
|
|
273
|
+
# Measure memory size of retained objects
|
|
274
|
+
total_memsize = version_ranges.sum { |r| ObjectSpace.memsize_of(r) }
|
|
275
|
+
puts "Retained memory:"
|
|
276
|
+
puts " #{version_ranges.length} VersionRange objects: #{total_memsize} bytes"
|
|
277
|
+
puts " Average per object: #{(total_memsize.to_f / version_ranges.length).round(0)} bytes"
|
|
278
|
+
puts
|
|
279
|
+
|
|
280
|
+
# Repeated operation allocations
|
|
281
|
+
puts "Repeated operation allocations (#{version_ranges.length} calls each):"
|
|
282
|
+
|
|
242
283
|
operations = {
|
|
243
|
-
"to_s
|
|
244
|
-
"contains?
|
|
245
|
-
"empty?
|
|
246
|
-
"unbounded? check" => proc { version_ranges.each(&:unbounded?) }
|
|
284
|
+
"to_s" => proc { version_ranges.each(&:to_s) },
|
|
285
|
+
"contains?" => proc { version_ranges.each { |r| r.contains?("1.5.0") } },
|
|
286
|
+
"empty?" => proc { version_ranges.each(&:empty?) },
|
|
247
287
|
}
|
|
248
|
-
|
|
249
|
-
operations.each do |
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
288
|
+
|
|
289
|
+
operations.each do |name, op|
|
|
290
|
+
# Warm up
|
|
291
|
+
op.call
|
|
292
|
+
|
|
293
|
+
GC.start
|
|
294
|
+
GC.disable
|
|
295
|
+
before = ObjectSpace.count_objects.dup
|
|
296
|
+
op.call
|
|
297
|
+
after = ObjectSpace.count_objects
|
|
298
|
+
GC.enable
|
|
299
|
+
|
|
300
|
+
allocs = [:T_STRING, :T_ARRAY, :T_OBJECT, :T_MATCH, :T_HASH].sum { |k| (after[k] || 0) - (before[k] || 0) }
|
|
301
|
+
puts " #{name.ljust(15)}: #{allocs} allocations"
|
|
254
302
|
end
|
|
255
|
-
|
|
303
|
+
|
|
256
304
|
puts
|
|
257
|
-
puts "
|
|
305
|
+
puts "Memory benchmark completed!"
|
|
258
306
|
end
|
|
259
307
|
|
|
260
308
|
desc "Run complexity stress tests"
|
data/lib/vers/constraint.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Vers
|
|
|
23
23
|
|
|
24
24
|
# Cache for parsed constraints
|
|
25
25
|
@@constraint_cache = {}
|
|
26
|
-
@@cache_size_limit =
|
|
26
|
+
@@cache_size_limit = 1000
|
|
27
27
|
|
|
28
28
|
attr_reader :operator, :version
|
|
29
29
|
|
|
@@ -54,14 +54,13 @@ module Vers
|
|
|
54
54
|
# Vers::Constraint.parse("!=2.0.0") # => #<Vers::Constraint:0x... @operator="!=", @version="2.0.0">
|
|
55
55
|
#
|
|
56
56
|
def self.parse(constraint_string)
|
|
57
|
-
|
|
57
|
+
return @@constraint_cache[constraint_string] if @@constraint_cache.key?(constraint_string)
|
|
58
|
+
|
|
58
59
|
if @@constraint_cache.size >= @@cache_size_limit
|
|
59
|
-
@@constraint_cache.
|
|
60
|
+
keys = @@constraint_cache.keys
|
|
61
|
+
keys.first(keys.size / 2).each { |k| @@constraint_cache.delete(k) }
|
|
60
62
|
end
|
|
61
|
-
|
|
62
|
-
# Return cached constraint if available
|
|
63
|
-
return @@constraint_cache[constraint_string] if @@constraint_cache.key?(constraint_string)
|
|
64
|
-
|
|
63
|
+
|
|
65
64
|
constraint = parse_uncached(constraint_string)
|
|
66
65
|
@@constraint_cache[constraint_string] = constraint
|
|
67
66
|
constraint
|
data/lib/vers/interval.rb
CHANGED
|
@@ -12,8 +12,7 @@ module Vers
|
|
|
12
12
|
@min_inclusive = min_inclusive
|
|
13
13
|
@max_inclusive = max_inclusive
|
|
14
14
|
@scheme = scheme
|
|
15
|
-
|
|
16
|
-
validate_bounds!
|
|
15
|
+
@empty = compute_empty
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
def self.empty(scheme: nil)
|
|
@@ -37,9 +36,7 @@ module Vers
|
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
def empty?
|
|
40
|
-
|
|
41
|
-
return true if min && max && version_compare(min, max) == 0 && (!min_inclusive || !max_inclusive)
|
|
42
|
-
false
|
|
39
|
+
@empty
|
|
43
40
|
end
|
|
44
41
|
|
|
45
42
|
def unbounded?
|
|
@@ -180,7 +177,22 @@ module Vers
|
|
|
180
177
|
|
|
181
178
|
def overlaps?(other)
|
|
182
179
|
return false if empty? || other.empty?
|
|
183
|
-
|
|
180
|
+
return true if unbounded? || other.unbounded?
|
|
181
|
+
|
|
182
|
+
# Check if the intervals can't overlap by comparing bounds directly
|
|
183
|
+
if max && other.min
|
|
184
|
+
cmp = version_compare(max, other.min)
|
|
185
|
+
return false if cmp < 0
|
|
186
|
+
return false if cmp == 0 && (!max_inclusive || !other.min_inclusive)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if min && other.max
|
|
190
|
+
cmp = version_compare(min, other.max)
|
|
191
|
+
return false if cmp > 0
|
|
192
|
+
return false if cmp == 0 && (!min_inclusive || !other.max_inclusive)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
true
|
|
184
196
|
end
|
|
185
197
|
|
|
186
198
|
def adjacent?(other)
|
|
@@ -211,14 +223,12 @@ module Vers
|
|
|
211
223
|
|
|
212
224
|
private
|
|
213
225
|
|
|
214
|
-
def
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
elsif comparison == 0 && (!min_inclusive || !max_inclusive)
|
|
221
|
-
return
|
|
226
|
+
def compute_empty
|
|
227
|
+
if min && max
|
|
228
|
+
cmp = version_compare(min, max)
|
|
229
|
+
cmp > 0 || (cmp == 0 && (!min_inclusive || !max_inclusive))
|
|
230
|
+
else
|
|
231
|
+
false
|
|
222
232
|
end
|
|
223
233
|
end
|
|
224
234
|
|
data/lib/vers/parser.rb
CHANGED
|
@@ -26,10 +26,11 @@ module Vers
|
|
|
26
26
|
NPM_HYPHEN_REGEX = /\A(.+?)\s+-\s+(.+)\z/
|
|
27
27
|
NPM_X_RANGE_MAJOR_REGEX = /\A(\d+)\.x\z/
|
|
28
28
|
NPM_X_RANGE_MINOR_REGEX = /\A(\d+)\.(\d+)\.x\z/
|
|
29
|
+
OPERATOR_PREFIX_REGEX = /\A[><=!]+/
|
|
29
30
|
|
|
30
31
|
# Cache for parsed ranges to improve performance
|
|
31
32
|
@@parser_cache = {}
|
|
32
|
-
@@cache_size_limit =
|
|
33
|
+
@@cache_size_limit = 500
|
|
33
34
|
|
|
34
35
|
##
|
|
35
36
|
# Parses a vers URI string into a VersionRange
|
|
@@ -151,7 +152,7 @@ module Vers
|
|
|
151
152
|
private
|
|
152
153
|
|
|
153
154
|
def sort_key_for_constraint(constraint)
|
|
154
|
-
version = constraint.sub(
|
|
155
|
+
version = constraint.sub(OPERATOR_PREFIX_REGEX, '')
|
|
155
156
|
v = Version.cached_new(version)
|
|
156
157
|
[v, constraint]
|
|
157
158
|
end
|
|
@@ -231,15 +232,12 @@ module Vers
|
|
|
231
232
|
end
|
|
232
233
|
|
|
233
234
|
def parse_npm_single_range(range_string)
|
|
234
|
-
# Check cache first
|
|
235
235
|
cache_key = "npm:#{range_string}"
|
|
236
|
-
if @@parser_cache.key?(cache_key)
|
|
237
|
-
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# Limit cache size
|
|
236
|
+
return @@parser_cache[cache_key] if @@parser_cache.key?(cache_key)
|
|
237
|
+
|
|
241
238
|
if @@parser_cache.size >= @@cache_size_limit
|
|
242
|
-
@@parser_cache.
|
|
239
|
+
keys = @@parser_cache.keys
|
|
240
|
+
keys.first(keys.size / 2).each { |k| @@parser_cache.delete(k) }
|
|
243
241
|
end
|
|
244
242
|
|
|
245
243
|
result = case range_string
|
data/lib/vers/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Vers
|
|
4
|
-
VERSION = "1.
|
|
4
|
+
VERSION = "1.3.0"
|
|
5
5
|
|
|
6
6
|
##
|
|
7
7
|
# Handles version comparison and normalization across different package ecosystems.
|
|
@@ -18,7 +18,7 @@ module Vers
|
|
|
18
18
|
class Version
|
|
19
19
|
# Cache for parsed versions to avoid repeated parsing
|
|
20
20
|
@@version_cache = {}
|
|
21
|
-
@@cache_size_limit =
|
|
21
|
+
@@cache_size_limit = 2000
|
|
22
22
|
# Regex for parsing semantic version components including build metadata
|
|
23
23
|
SEMANTIC_VERSION_REGEX = /\A(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([^+]+))?(?:\+(.+))?\z/
|
|
24
24
|
|
|
@@ -41,11 +41,12 @@ module Vers
|
|
|
41
41
|
# @return [Version] Cached or new Version object
|
|
42
42
|
#
|
|
43
43
|
def self.cached_new(version_string)
|
|
44
|
-
# Limit cache size to prevent memory bloat
|
|
45
44
|
if @@version_cache.size >= @@cache_size_limit
|
|
46
|
-
|
|
45
|
+
# Keep the most recent half instead of clearing everything
|
|
46
|
+
keys = @@version_cache.keys
|
|
47
|
+
keys.first(keys.size / 2).each { |k| @@version_cache.delete(k) }
|
|
47
48
|
end
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
@@version_cache[version_string] ||= new(version_string)
|
|
50
51
|
end
|
|
51
52
|
|
|
@@ -145,15 +146,17 @@ module Vers
|
|
|
145
146
|
# @return [String] The normalized version string
|
|
146
147
|
#
|
|
147
148
|
def to_s
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
@to_s ||= begin
|
|
150
|
+
version = "#{major || 0}"
|
|
151
|
+
version += ".#{minor || 0}"
|
|
152
|
+
version += ".#{patch || 0}"
|
|
153
|
+
version += "-#{prerelease}" if prerelease
|
|
154
|
+
version.freeze
|
|
155
|
+
end
|
|
153
156
|
end
|
|
154
157
|
|
|
155
158
|
def ==(other)
|
|
156
|
-
other.is_a?(Version) && self <=> other == 0
|
|
159
|
+
other.is_a?(Version) && (self <=> other) == 0
|
|
157
160
|
end
|
|
158
161
|
|
|
159
162
|
def <(other)
|
|
@@ -320,7 +323,7 @@ module Vers
|
|
|
320
323
|
@original = @original.sub(/\Av/i, '')
|
|
321
324
|
|
|
322
325
|
# Handle simple numeric versions (optimized case)
|
|
323
|
-
if @original.match(/^\d+$/)
|
|
326
|
+
if @original.match?(/^\d+$/)
|
|
324
327
|
@major = @original.to_i
|
|
325
328
|
return
|
|
326
329
|
end
|
|
@@ -382,7 +385,7 @@ module Vers
|
|
|
382
385
|
return 1 if part_b.nil?
|
|
383
386
|
|
|
384
387
|
# Try numeric comparison first
|
|
385
|
-
if part_a.match(/^\d+$/) && part_b.match(/^\d+$/)
|
|
388
|
+
if part_a.match?(/^\d+$/) && part_b.match?(/^\d+$/)
|
|
386
389
|
numeric_cmp = part_a.to_i <=> part_b.to_i
|
|
387
390
|
return numeric_cmp unless numeric_cmp == 0
|
|
388
391
|
else
|
data/lib/vers/version_range.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Vers
|
|
|
9
9
|
|
|
10
10
|
def initialize(intervals = [], raw_constraints: nil, scheme: nil)
|
|
11
11
|
@scheme = scheme
|
|
12
|
-
@intervals = intervals.
|
|
12
|
+
@intervals = intervals.select { |i| i && !i.empty? }
|
|
13
13
|
if @scheme
|
|
14
14
|
@intervals.sort! { |a, b| compare_interval_bounds(a, b) }
|
|
15
15
|
else
|