ukiryu 0.1.6 → 0.2.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/lib/ukiryu/cache.rb +6 -0
- data/lib/ukiryu/cache_registry.rb +64 -0
- data/lib/ukiryu/cli_commands/base_command.rb +6 -5
- data/lib/ukiryu/cli_commands/config_command.rb +7 -10
- data/lib/ukiryu/cli_commands/register_command.rb +27 -18
- data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
- data/lib/ukiryu/command_builder.rb +83 -50
- data/lib/ukiryu/config.rb +13 -2
- data/lib/ukiryu/debug.rb +20 -9
- data/lib/ukiryu/definition/loader.rb +3 -3
- data/lib/ukiryu/errors.rb +37 -37
- data/lib/ukiryu/executable_locator.rb +40 -16
- data/lib/ukiryu/extractors/base_extractor.rb +2 -1
- data/lib/ukiryu/extractors/help_parser.rb +3 -0
- data/lib/ukiryu/logger.rb +51 -0
- data/lib/ukiryu/models/implementation_index.rb +2 -1
- data/lib/ukiryu/models/implementation_version.rb +18 -1
- data/lib/ukiryu/models/interface.rb +2 -1
- data/lib/ukiryu/models/run_environment.rb +0 -2
- data/lib/ukiryu/models/semantic_version.rb +174 -0
- data/lib/ukiryu/models/stage_metrics.rb +0 -1
- data/lib/ukiryu/register.rb +473 -232
- data/lib/ukiryu/shell/powershell.rb +209 -89
- data/lib/ukiryu/shell/sh.rb +4 -1
- data/lib/ukiryu/shell.rb +60 -2
- data/lib/ukiryu/tool/command_resolution.rb +2 -1
- data/lib/ukiryu/tool/executable_discovery.rb +14 -15
- data/lib/ukiryu/tool/loader.rb +543 -0
- data/lib/ukiryu/tool/version_detection.rb +1 -3
- data/lib/ukiryu/tool.rb +79 -87
- data/lib/ukiryu/tool_index.rb +127 -62
- data/lib/ukiryu/tools/base.rb +4 -2
- data/lib/ukiryu/type.rb +26 -15
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu.rb +1 -1
- data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
- data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
- data/spec/spec_helper.rb +10 -6
- data/spec/support/tool_helper.rb +2 -0
- data/spec/ukiryu/definition/loader_spec.rb +2 -2
- data/spec/ukiryu/executor_spec.rb +6 -3
- data/spec/ukiryu/models/execution_report_spec.rb +3 -2
- data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
- data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
- data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
- data/spec/ukiryu/shell/powershell_spec.rb +286 -51
- data/spec/ukiryu/tool/loader_spec.rb +148 -0
- data/spec/ukiryu/tool_index_spec.rb +110 -18
- data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
- data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
- data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
- metadata +14 -2
- data/lib/ukiryu/register_auto_manager.rb +0 -342
data/lib/ukiryu/type.rb
CHANGED
|
@@ -78,7 +78,10 @@ module Ukiryu
|
|
|
78
78
|
raise Ukiryu::Errors::ValidationError, 'File path cannot be empty' if value.empty?
|
|
79
79
|
|
|
80
80
|
# Check if file exists (only if require_existing is true)
|
|
81
|
-
|
|
81
|
+
if options[:require_existing] && !File.exist?(value)
|
|
82
|
+
raise Ukiryu::Errors::ValidationError,
|
|
83
|
+
"File not found: #{value}"
|
|
84
|
+
end
|
|
82
85
|
|
|
83
86
|
value
|
|
84
87
|
end
|
|
@@ -108,7 +111,10 @@ module Ukiryu
|
|
|
108
111
|
|
|
109
112
|
if options[:range]
|
|
110
113
|
min, max = options[:range]
|
|
111
|
-
|
|
114
|
+
if integer < min || integer > max
|
|
115
|
+
raise Ukiryu::Errors::ValidationError,
|
|
116
|
+
"Integer #{integer} out of range [#{min}, #{max}]"
|
|
117
|
+
end
|
|
112
118
|
end
|
|
113
119
|
|
|
114
120
|
if options[:min] && integer < options[:min]
|
|
@@ -136,7 +142,10 @@ module Ukiryu
|
|
|
136
142
|
|
|
137
143
|
if options[:range]
|
|
138
144
|
min, max = options[:range]
|
|
139
|
-
|
|
145
|
+
if float < min || float > max
|
|
146
|
+
raise Ukiryu::Errors::ValidationError,
|
|
147
|
+
"Float #{float} out of range [#{min}, #{max}]"
|
|
148
|
+
end
|
|
140
149
|
end
|
|
141
150
|
|
|
142
151
|
float
|
|
@@ -203,7 +212,10 @@ module Ukiryu
|
|
|
203
212
|
|
|
204
213
|
# Validate hash type
|
|
205
214
|
def validate_hash(value, options)
|
|
206
|
-
|
|
215
|
+
unless value.is_a?(Hash)
|
|
216
|
+
raise Ukiryu::Errors::ValidationError,
|
|
217
|
+
"Hash expected, got #{value.class}: #{value.inspect}"
|
|
218
|
+
end
|
|
207
219
|
|
|
208
220
|
if options[:keys]
|
|
209
221
|
unknown_keys = value.keys - options[:keys]
|
|
@@ -218,19 +230,17 @@ module Ukiryu
|
|
|
218
230
|
|
|
219
231
|
# Validate array type
|
|
220
232
|
def validate_array(value, options)
|
|
221
|
-
# Debug logging for
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
warn "[UKIRYU DEBUG Type.validate_array] options: #{options.inspect}"
|
|
226
|
-
end
|
|
233
|
+
# Debug logging for executable discovery
|
|
234
|
+
Logger.debug("Type.validate_array value.class: #{value.class}", category: :executable)
|
|
235
|
+
Logger.debug("Type.validate_array value.inspect: #{value.inspect}", category: :executable)
|
|
236
|
+
Logger.debug("Type.validate_array options: #{options.inspect}", category: :executable)
|
|
227
237
|
|
|
228
238
|
array = value.is_a?(Array) ? value : [value]
|
|
229
239
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
Logger.debug("Type.validate_array after conversion, array.class: #{array.class}",
|
|
241
|
+
category: :executable)
|
|
242
|
+
Logger.debug("Type.validate_array after conversion, array.inspect: #{array.inspect}",
|
|
243
|
+
category: :executable)
|
|
234
244
|
|
|
235
245
|
if options[:min] && array.size < options[:min]
|
|
236
246
|
raise Ukiryu::Errors::ValidationError,
|
|
@@ -259,7 +269,8 @@ module Ukiryu
|
|
|
259
269
|
# Validate element type if specified
|
|
260
270
|
array = array.map { |v| validate(v, options[:of], options) } if options[:of]
|
|
261
271
|
|
|
262
|
-
|
|
272
|
+
Logger.debug("Type.validate_array returning array.inspect: #{array.inspect}",
|
|
273
|
+
category: :executable)
|
|
263
274
|
|
|
264
275
|
array
|
|
265
276
|
end
|
data/lib/ukiryu/version.rb
CHANGED
data/lib/ukiryu.rb
CHANGED
|
@@ -46,11 +46,11 @@ module Ukiryu
|
|
|
46
46
|
# Internal Tool implementation classes - lazy load with autoload
|
|
47
47
|
autoload :CommandBuilder, 'ukiryu/command_builder'
|
|
48
48
|
autoload :Cache, 'ukiryu/cache'
|
|
49
|
+
autoload :CacheRegistry, 'ukiryu/cache_registry'
|
|
49
50
|
autoload :ExecutableLocator, 'ukiryu/executable_locator'
|
|
50
51
|
autoload :VersionDetector, 'ukiryu/version_detector'
|
|
51
52
|
autoload :ToolIndex, 'ukiryu/tool_index'
|
|
52
53
|
autoload :ManPageParser, 'ukiryu/man_page_parser'
|
|
53
|
-
autoload :RegisterAutoManager, 'ukiryu/register_auto_manager'
|
|
54
54
|
|
|
55
55
|
# Model classes - lazy load with autoload (these are directly under Ukiryu namespace)
|
|
56
56
|
autoload :ToolMetadata, 'ukiryu/models/tool_metadata'
|
|
@@ -7,6 +7,8 @@ homepage: https://www.ghostscript.com/
|
|
|
7
7
|
version: '10.0'
|
|
8
8
|
aliases:
|
|
9
9
|
- gs
|
|
10
|
+
- gswin64c
|
|
11
|
+
- gswin32c
|
|
10
12
|
version_detection:
|
|
11
13
|
command: "--version"
|
|
12
14
|
pattern: "(\\d+\\.\\d+)"
|
|
@@ -18,6 +20,7 @@ search_paths:
|
|
|
18
20
|
- "/usr/bin/gs"
|
|
19
21
|
windows:
|
|
20
22
|
- "C:/Program Files/gs/gs*/bin/gswin64c.exe"
|
|
23
|
+
- "C:/Program Files/gs/gs*/bin/gswin32c.exe"
|
|
21
24
|
profiles:
|
|
22
25
|
- name: unix
|
|
23
26
|
display_name: Ghostscript on Unix
|
|
@@ -67,3 +70,50 @@ profiles:
|
|
|
67
70
|
default: true
|
|
68
71
|
assignment_delimiter: none
|
|
69
72
|
name: convert
|
|
73
|
+
- name: windows
|
|
74
|
+
display_name: Ghostscript on Windows
|
|
75
|
+
platforms:
|
|
76
|
+
- windows
|
|
77
|
+
shells:
|
|
78
|
+
- powershell
|
|
79
|
+
- cmd
|
|
80
|
+
version: ">= 9.0"
|
|
81
|
+
option_style: single_dash_equals
|
|
82
|
+
commands:
|
|
83
|
+
- description: Convert PostScript or PDF to other formats
|
|
84
|
+
usage: gswin64c [options] input-file
|
|
85
|
+
arguments:
|
|
86
|
+
- name: inputs
|
|
87
|
+
type: file
|
|
88
|
+
variadic: true
|
|
89
|
+
min: 1
|
|
90
|
+
position: last
|
|
91
|
+
description: Input file(s) to process
|
|
92
|
+
options:
|
|
93
|
+
- name: device
|
|
94
|
+
type: string
|
|
95
|
+
cli: "-sDEVICE"
|
|
96
|
+
description: Output device (e.g., png16m, jpeg, pdfwrite)
|
|
97
|
+
assignment_delimiter: equals
|
|
98
|
+
- name: output
|
|
99
|
+
type: file
|
|
100
|
+
cli: "-sOutputFile"
|
|
101
|
+
description: Output file path
|
|
102
|
+
assignment_delimiter: equals
|
|
103
|
+
- name: resolution
|
|
104
|
+
type: string
|
|
105
|
+
cli: "-r"
|
|
106
|
+
description: Resolution (e.g., 150, 300, or 600x600)
|
|
107
|
+
assignment_delimiter: auto
|
|
108
|
+
flags:
|
|
109
|
+
- name: quiet
|
|
110
|
+
cli: "-q"
|
|
111
|
+
description: Quiet mode
|
|
112
|
+
default: false
|
|
113
|
+
assignment_delimiter: none
|
|
114
|
+
- name: batch
|
|
115
|
+
cli: "-dBATCH"
|
|
116
|
+
description: Batch mode
|
|
117
|
+
default: true
|
|
118
|
+
assignment_delimiter: none
|
|
119
|
+
name: convert
|
|
@@ -6,10 +6,15 @@ homepage: https://www.ghostscript.com/
|
|
|
6
6
|
version: '1.0'
|
|
7
7
|
aliases:
|
|
8
8
|
- gs
|
|
9
|
+
- gswin64c
|
|
10
|
+
- gswin32c
|
|
9
11
|
version_detection:
|
|
10
12
|
command: "--version"
|
|
11
13
|
pattern: "(\\d+\\.\\d+)"
|
|
12
14
|
search_paths:
|
|
15
|
+
windows:
|
|
16
|
+
- "C:/Program Files/gs/gs*/bin/gswin64c.exe"
|
|
17
|
+
- "C:/Program Files/gs/gs*/bin/gswin32c.exe"
|
|
13
18
|
implements: ghostscript/1.0
|
|
14
19
|
profiles:
|
|
15
20
|
- name: unix
|
|
@@ -21,6 +26,7 @@ profiles:
|
|
|
21
26
|
- zsh
|
|
22
27
|
- sh
|
|
23
28
|
- fish
|
|
29
|
+
- powershell
|
|
24
30
|
option_style: single_dash_space
|
|
25
31
|
executable_name: ghostscript
|
|
26
32
|
commands:
|
data/spec/spec_helper.rb
CHANGED
|
@@ -8,7 +8,7 @@ require 'timeout'
|
|
|
8
8
|
require 'ukiryu'
|
|
9
9
|
|
|
10
10
|
# Require all support files
|
|
11
|
-
Dir[File.join(__dir__,
|
|
11
|
+
Dir[File.join(__dir__, 'support', '**', '*.rb')].sort.each { |f| require f }
|
|
12
12
|
|
|
13
13
|
RSpec.configure do |config|
|
|
14
14
|
# Enable flags like --only-failures and --next-failure
|
|
@@ -40,12 +40,12 @@ RSpec.configure do |config|
|
|
|
40
40
|
# Set up test register path if UKIRYU_REGISTER is not already set
|
|
41
41
|
unless ENV['UKIRYU_REGISTER']
|
|
42
42
|
test_register = File.expand_path('fixtures/register', __dir__)
|
|
43
|
-
if Dir.exist?(test_register)
|
|
44
|
-
ENV['UKIRYU_REGISTER'] = test_register
|
|
45
|
-
Ukiryu::Register.default_register_path = test_register
|
|
46
|
-
end
|
|
43
|
+
ENV['UKIRYU_REGISTER'] = test_register if Dir.exist?(test_register)
|
|
47
44
|
end
|
|
48
45
|
|
|
46
|
+
# Reset the default register to pick up the test register
|
|
47
|
+
Ukiryu::Register.reset_default
|
|
48
|
+
|
|
49
49
|
# Reset ToolIndex to pick up the new register path
|
|
50
50
|
Ukiryu::ToolIndex.reset
|
|
51
51
|
|
|
@@ -59,11 +59,15 @@ RSpec.configure do |config|
|
|
|
59
59
|
# Reset singleton state before each test to prevent pollution
|
|
60
60
|
config.before(:each) do
|
|
61
61
|
Ukiryu::Config.reset!
|
|
62
|
-
Ukiryu::Register.
|
|
62
|
+
Ukiryu::Register.reset_default
|
|
63
63
|
Ukiryu::ToolIndex.reset
|
|
64
64
|
Ukiryu::Tool.clear_cache
|
|
65
65
|
Ukiryu::Runtime.instance.reset!
|
|
66
66
|
Ukiryu::Tools::Generator.clear_cache
|
|
67
|
+
Ukiryu::Shell.reset
|
|
68
|
+
|
|
69
|
+
# Clean up test shell environment variable that may be left by other tests
|
|
70
|
+
ENV.delete('UKIRYU_TEST_SHELL')
|
|
67
71
|
|
|
68
72
|
# Remove generated tool classes from Tools namespace
|
|
69
73
|
Ukiryu::Tools.constants.each do |const|
|
data/spec/support/tool_helper.rb
CHANGED
|
@@ -174,8 +174,8 @@ RSpec.describe Ukiryu::Definition::Loader do
|
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
describe '.profile_cache' do
|
|
177
|
-
it 'returns a
|
|
178
|
-
expect(described_class.profile_cache).to be_a(
|
|
177
|
+
it 'returns a bounded cache' do
|
|
178
|
+
expect(described_class.profile_cache).to be_a(Ukiryu::Cache)
|
|
179
179
|
end
|
|
180
180
|
|
|
181
181
|
it 'returns same cache on multiple calls' do
|
|
@@ -152,7 +152,8 @@ RSpec.describe Ukiryu::Executor do
|
|
|
152
152
|
# Use platform-appropriate command that exits with specific code
|
|
153
153
|
result = if Ukiryu::Platform.windows?
|
|
154
154
|
# Windows: PowerShell supports exit codes
|
|
155
|
-
executor.execute('powershell', ['-Command', 'exit 42'], allow_failure: true, shell: :powershell,
|
|
155
|
+
executor.execute('powershell', ['-Command', 'exit 42'], allow_failure: true, shell: :powershell,
|
|
156
|
+
timeout: 30)
|
|
156
157
|
else
|
|
157
158
|
executor.execute('sh', ['-c', 'exit 42'], allow_failure: true, shell: shell_symbol, timeout: 30)
|
|
158
159
|
end
|
|
@@ -215,7 +216,8 @@ RSpec.describe Ukiryu::Executor do
|
|
|
215
216
|
# Use echo as a cross-platform command that always works
|
|
216
217
|
result = if Ukiryu::Platform.windows?
|
|
217
218
|
# On Windows with PowerShell, use echo command via PowerShell
|
|
218
|
-
executor.execute('powershell', ['-Command', 'echo test'], allow_failure: true, shell: :powershell,
|
|
219
|
+
executor.execute('powershell', ['-Command', 'echo test'], allow_failure: true, shell: :powershell,
|
|
220
|
+
timeout: 90)
|
|
219
221
|
else
|
|
220
222
|
executor.execute('echo', ['test'], allow_failure: true, shell: shell_symbol, timeout: 90)
|
|
221
223
|
end
|
|
@@ -415,7 +417,8 @@ RSpec.describe Ukiryu::Executor do
|
|
|
415
417
|
end
|
|
416
418
|
|
|
417
419
|
it 'handles commands with special characters in arguments' do
|
|
418
|
-
result = executor.execute('echo', ['hello "quoted" test\'s'], allow_failure: true, shell: shell_symbol,
|
|
420
|
+
result = executor.execute('echo', ['hello "quoted" test\'s'], allow_failure: true, shell: shell_symbol,
|
|
421
|
+
timeout: 90)
|
|
419
422
|
|
|
420
423
|
expect(result.stdout.strip).to include('hello')
|
|
421
424
|
end
|
|
@@ -77,9 +77,10 @@ RSpec.describe Ukiryu::Models::ExecutionReport do
|
|
|
77
77
|
# In Docker/CI, may be 0 or an actual value
|
|
78
78
|
expect(stage.memory_after).to be_a(Integer)
|
|
79
79
|
expect(stage.memory_after).to be >= 0
|
|
80
|
-
#
|
|
81
|
-
expect(stage.memory_after).to be >= stage.memory_before if stage.memory_before > 0
|
|
80
|
+
# Memory delta is calculated (may be negative due to GC)
|
|
82
81
|
expect(stage.memory_delta).to be_a(Integer)
|
|
82
|
+
# Verify delta calculation is correct
|
|
83
|
+
expect(stage.memory_delta).to eq(stage.memory_after - stage.memory_before)
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
it 'marks the stage as successful by default' do
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'ukiryu/models/semantic_version'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Ukiryu::Models::SemanticVersion do
|
|
7
|
+
describe '.parse' do
|
|
8
|
+
it 'parses simple version' do
|
|
9
|
+
expect(described_class.parse('10.0')).to eq([10, 0])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'parses three-part version' do
|
|
13
|
+
expect(described_class.parse('1.2.3')).to eq([1, 2, 3])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'parses single number version' do
|
|
17
|
+
expect(described_class.parse('5')).to eq([5])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'handles nil' do
|
|
21
|
+
expect(described_class.parse(nil)).to eq([0])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'handles empty string' do
|
|
25
|
+
expect(described_class.parse('')).to eq([0])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'handles non-numeric parts as 0' do
|
|
29
|
+
expect(described_class.parse('1.alpha.3')).to eq([1, 0, 3])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe '.compare' do
|
|
34
|
+
it 'returns 1 when first version is greater' do
|
|
35
|
+
expect(described_class.compare('10.0', '9.5')).to eq(1)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns -1 when first version is lesser' do
|
|
39
|
+
expect(described_class.compare('9.5', '10.0')).to eq(-1)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'returns 0 when versions are equal' do
|
|
43
|
+
expect(described_class.compare('10.0', '10.0')).to eq(0)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'handles different segment counts' do
|
|
47
|
+
expect(described_class.compare('10.0.1', '10.0')).to eq(1)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'handles single vs multi-part versions' do
|
|
51
|
+
expect(described_class.compare('10', '9.5')).to eq(1)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '#initialize' do
|
|
56
|
+
it 'stores original string' do
|
|
57
|
+
version = described_class.new('10.0')
|
|
58
|
+
expect(version.original).to eq('10.0')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'parses segments' do
|
|
62
|
+
version = described_class.new('10.0.5')
|
|
63
|
+
expect(version.segments).to eq([10, 0, 5])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'handles integer input' do
|
|
67
|
+
version = described_class.new(10)
|
|
68
|
+
expect(version.segments).to eq([10])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'handles nil input' do
|
|
72
|
+
version = described_class.new(nil)
|
|
73
|
+
expect(version.segments).to eq([0])
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#<=>' do
|
|
78
|
+
it 'compares 10.0 > 9.5 (the critical bug case)' do
|
|
79
|
+
v1 = described_class.new('10.0')
|
|
80
|
+
v2 = described_class.new('9.5')
|
|
81
|
+
expect(v1 <=> v2).to eq(1)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'compares 9.5 < 10.0' do
|
|
85
|
+
v1 = described_class.new('9.5')
|
|
86
|
+
v2 = described_class.new('10.0')
|
|
87
|
+
expect(v1 <=> v2).to eq(-1)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'compares equal versions as 0' do
|
|
91
|
+
v1 = described_class.new('10.0')
|
|
92
|
+
v2 = described_class.new('10.0')
|
|
93
|
+
expect(v1 <=> v2).to eq(0)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'compares 1.10.0 > 1.9.9' do
|
|
97
|
+
v1 = described_class.new('1.10.0')
|
|
98
|
+
v2 = described_class.new('1.9.9')
|
|
99
|
+
expect(v1 <=> v2).to eq(1)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'compares with string' do
|
|
103
|
+
version = described_class.new('10.0')
|
|
104
|
+
expect(version <=> '9.5').to eq(1)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'compares with integer' do
|
|
108
|
+
version = described_class.new('10.0')
|
|
109
|
+
expect(version <=> 9).to eq(1)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'returns nil for non-comparable' do
|
|
113
|
+
version = described_class.new('10.0')
|
|
114
|
+
expect(version <=> Object.new).to be_nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe '#==' do
|
|
119
|
+
it 'returns true for equal versions' do
|
|
120
|
+
v1 = described_class.new('10.0')
|
|
121
|
+
v2 = described_class.new('10.0')
|
|
122
|
+
expect(v1 == v2).to be true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'returns true for equivalent versions with different segment counts' do
|
|
126
|
+
v1 = described_class.new('10.0')
|
|
127
|
+
v2 = described_class.new('10.0.0')
|
|
128
|
+
expect(v1 == v2).to be true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'returns false for different versions' do
|
|
132
|
+
v1 = described_class.new('10.0')
|
|
133
|
+
v2 = described_class.new('9.5')
|
|
134
|
+
expect(v1 == v2).to be false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'compares with string' do
|
|
138
|
+
version = described_class.new('10.0')
|
|
139
|
+
expect(version == '10.0').to be true
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
describe '#>' do
|
|
144
|
+
it 'returns true when greater' do
|
|
145
|
+
expect(described_class.new('10.0')).to be > described_class.new('9.5')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'returns false when equal' do
|
|
149
|
+
expect(described_class.new('10.0')).not_to be > described_class.new('10.0')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'returns false when lesser' do
|
|
153
|
+
expect(described_class.new('9.5')).not_to be > described_class.new('10.0')
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe '#<' do
|
|
158
|
+
it 'returns true when lesser' do
|
|
159
|
+
expect(described_class.new('9.5')).to be < described_class.new('10.0')
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'returns false when equal' do
|
|
163
|
+
expect(described_class.new('10.0')).not_to be < described_class.new('10.0')
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe '#>=' do
|
|
168
|
+
it 'returns true when greater' do
|
|
169
|
+
expect(described_class.new('10.0')).to be >= described_class.new('9.5')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'returns true when equal' do
|
|
173
|
+
expect(described_class.new('10.0')).to be >= described_class.new('10.0')
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'returns false when lesser' do
|
|
177
|
+
expect(described_class.new('9.5')).not_to be >= described_class.new('10.0')
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
describe '#<=' do
|
|
182
|
+
it 'returns true when lesser' do
|
|
183
|
+
expect(described_class.new('9.5')).to be <= described_class.new('10.0')
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'returns true when equal' do
|
|
187
|
+
expect(described_class.new('10.0')).to be <= described_class.new('10.0')
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
describe '#to_s' do
|
|
192
|
+
it 'returns version string' do
|
|
193
|
+
version = described_class.new('10.0.5')
|
|
194
|
+
expect(version.to_s).to eq('10.0.5')
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
describe '#inspect' do
|
|
199
|
+
it 'returns inspect string' do
|
|
200
|
+
version = described_class.new('10.0')
|
|
201
|
+
expect(version.inspect).to eq('#<Ukiryu::Models::SemanticVersion 10.0>')
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
describe '#hash and #eql?' do
|
|
206
|
+
it 'can be used as hash key' do
|
|
207
|
+
v1 = described_class.new('10.0')
|
|
208
|
+
v2 = described_class.new('10.0')
|
|
209
|
+
v3 = described_class.new('9.5')
|
|
210
|
+
|
|
211
|
+
hash = { v1 => 'first', v3 => 'second' }
|
|
212
|
+
|
|
213
|
+
expect(hash[v2]).to eq('first')
|
|
214
|
+
expect(hash[v3]).to eq('second')
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'eql? returns true for equal versions' do
|
|
218
|
+
v1 = described_class.new('10.0')
|
|
219
|
+
v2 = described_class.new('10.0')
|
|
220
|
+
expect(v1.eql?(v2)).to be true
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'eql? returns false for different versions' do
|
|
224
|
+
v1 = described_class.new('10.0')
|
|
225
|
+
v2 = described_class.new('9.5')
|
|
226
|
+
expect(v1.eql?(v2)).to be false
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
describe 'Comparable integration' do
|
|
231
|
+
it 'works with Array#max' do
|
|
232
|
+
versions = [
|
|
233
|
+
described_class.new('9.5'),
|
|
234
|
+
described_class.new('10.0'),
|
|
235
|
+
described_class.new('8.0')
|
|
236
|
+
]
|
|
237
|
+
expect(versions.max.to_s).to eq('10.0')
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'works with Array#sort' do
|
|
241
|
+
versions = [
|
|
242
|
+
described_class.new('9.5'),
|
|
243
|
+
described_class.new('10.0'),
|
|
244
|
+
described_class.new('8.0')
|
|
245
|
+
]
|
|
246
|
+
expect(versions.sort.map(&:to_s)).to eq(['8.0', '9.5', '10.0'])
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'works with Array#min' do
|
|
250
|
+
versions = [
|
|
251
|
+
described_class.new('9.5'),
|
|
252
|
+
described_class.new('10.0'),
|
|
253
|
+
described_class.new('8.0')
|
|
254
|
+
]
|
|
255
|
+
expect(versions.min.to_s).to eq('8.0')
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
describe 'real-world Ghostscript case' do
|
|
260
|
+
it 'correctly selects 10.0 over 9.5' do
|
|
261
|
+
v10 = described_class.new('10.0')
|
|
262
|
+
v95 = described_class.new('9.5')
|
|
263
|
+
|
|
264
|
+
# This was the bug: alphabetical sort would select 9.5 over 10.0
|
|
265
|
+
# because '9' > '1' in ASCII
|
|
266
|
+
expect([v95, v10].max).to eq(v10)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'demonstrates the bug with string comparison' do
|
|
270
|
+
# Show why string comparison is wrong
|
|
271
|
+
filenames = ['9.5.yaml', '10.0.yaml']
|
|
272
|
+
|
|
273
|
+
# WRONG: alphabetical max gives '9.5.yaml' (because '9' > '1')
|
|
274
|
+
alphabetical_max = filenames.max
|
|
275
|
+
expect(alphabetical_max).to eq('9.5.yaml')
|
|
276
|
+
|
|
277
|
+
# CORRECT: semantic version comparison gives '10.0.yaml'
|
|
278
|
+
semantic_max = filenames.max_by do |f|
|
|
279
|
+
described_class.new(File.basename(f, '.yaml'))
|
|
280
|
+
end
|
|
281
|
+
expect(semantic_max).to eq('10.0.yaml')
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|