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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ukiryu/cache.rb +6 -0
  3. data/lib/ukiryu/cache_registry.rb +64 -0
  4. data/lib/ukiryu/cli_commands/base_command.rb +6 -5
  5. data/lib/ukiryu/cli_commands/config_command.rb +7 -10
  6. data/lib/ukiryu/cli_commands/register_command.rb +27 -18
  7. data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
  8. data/lib/ukiryu/command_builder.rb +83 -50
  9. data/lib/ukiryu/config.rb +13 -2
  10. data/lib/ukiryu/debug.rb +20 -9
  11. data/lib/ukiryu/definition/loader.rb +3 -3
  12. data/lib/ukiryu/errors.rb +37 -37
  13. data/lib/ukiryu/executable_locator.rb +40 -16
  14. data/lib/ukiryu/extractors/base_extractor.rb +2 -1
  15. data/lib/ukiryu/extractors/help_parser.rb +3 -0
  16. data/lib/ukiryu/logger.rb +51 -0
  17. data/lib/ukiryu/models/implementation_index.rb +2 -1
  18. data/lib/ukiryu/models/implementation_version.rb +18 -1
  19. data/lib/ukiryu/models/interface.rb +2 -1
  20. data/lib/ukiryu/models/run_environment.rb +0 -2
  21. data/lib/ukiryu/models/semantic_version.rb +174 -0
  22. data/lib/ukiryu/models/stage_metrics.rb +0 -1
  23. data/lib/ukiryu/register.rb +473 -232
  24. data/lib/ukiryu/shell/powershell.rb +209 -89
  25. data/lib/ukiryu/shell/sh.rb +4 -1
  26. data/lib/ukiryu/shell.rb +60 -2
  27. data/lib/ukiryu/tool/command_resolution.rb +2 -1
  28. data/lib/ukiryu/tool/executable_discovery.rb +14 -15
  29. data/lib/ukiryu/tool/loader.rb +543 -0
  30. data/lib/ukiryu/tool/version_detection.rb +1 -3
  31. data/lib/ukiryu/tool.rb +79 -87
  32. data/lib/ukiryu/tool_index.rb +127 -62
  33. data/lib/ukiryu/tools/base.rb +4 -2
  34. data/lib/ukiryu/type.rb +26 -15
  35. data/lib/ukiryu/version.rb +1 -1
  36. data/lib/ukiryu.rb +1 -1
  37. data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
  38. data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
  39. data/spec/spec_helper.rb +10 -6
  40. data/spec/support/tool_helper.rb +2 -0
  41. data/spec/ukiryu/definition/loader_spec.rb +2 -2
  42. data/spec/ukiryu/executor_spec.rb +6 -3
  43. data/spec/ukiryu/models/execution_report_spec.rb +3 -2
  44. data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
  45. data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
  46. data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
  47. data/spec/ukiryu/shell/powershell_spec.rb +286 -51
  48. data/spec/ukiryu/tool/loader_spec.rb +148 -0
  49. data/spec/ukiryu/tool_index_spec.rb +110 -18
  50. data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
  51. data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
  52. data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
  53. metadata +14 -2
  54. data/lib/ukiryu/register_auto_manager.rb +0 -342
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'PowerShell Integration Tests', if: system('which pwsh > /dev/null 2>&1') do
6
+ let(:shell) { Ukiryu::Shell::PowerShell.new }
7
+ let(:env) { Ukiryu::Environment.system }
8
+
9
+ describe '#execute_command' do
10
+ context 'with dash-prefixed arguments (prefix stripping prevention)' do
11
+ it 'preserves -sDEVICE=pdfwrite style arguments' do
12
+ result = shell.execute_command('echo', ['-sDEVICE=pdfwrite', 'input.eps'], env, 30, nil)
13
+ expect(result[:status]).to eq(0)
14
+ # The full argument should be present (not stripped to just =pdfwrite)
15
+ expect(result[:stdout]).to include('-sDEVICE=pdfwrite')
16
+ end
17
+
18
+ it 'preserves multiple dash-prefixed arguments' do
19
+ args = ['-sDEVICE=pdfwrite', '-sOutputFile=output.pdf', '-dBATCH']
20
+ result = shell.execute_command('echo', args, env, 30, nil)
21
+ expect(result[:status]).to eq(0)
22
+ expect(result[:stdout]).to include('-sDEVICE=pdfwrite')
23
+ expect(result[:stdout]).to include('-sOutputFile=output.pdf')
24
+ expect(result[:stdout]).to include('-dBATCH')
25
+ end
26
+
27
+ it 'preserves -resize style arguments' do
28
+ result = shell.execute_command('echo', ['-resize', '50x50'], env, 30, nil)
29
+ expect(result[:status]).to eq(0)
30
+ expect(result[:stdout]).to include('-resize')
31
+ end
32
+ end
33
+
34
+ context 'with single quotes in arguments' do
35
+ it 'correctly escapes single quotes by doubling them' do
36
+ result = shell.execute_command('echo', ["it's a test"], env, 30, nil)
37
+ expect(result[:status]).to eq(0)
38
+ expect(result[:stdout].strip).to eq("it's a test")
39
+ end
40
+
41
+ it 'handles multiple single quotes' do
42
+ result = shell.execute_command('echo', ["it's a test's value"], env, 30, nil)
43
+ expect(result[:status]).to eq(0)
44
+ expect(result[:stdout].strip).to eq("it's a test's value")
45
+ end
46
+ end
47
+
48
+ context 'with special characters in arguments' do
49
+ it 'handles dollar signs (escaped in double quotes)' do
50
+ result = shell.execute_command('echo', ['$VAR'], env, 30, nil)
51
+ expect(result[:status]).to eq(0)
52
+ # Dollar signs are escaped with backtick in double-quoted strings
53
+ expect(result[:stdout].strip).to eq('$VAR')
54
+ end
55
+
56
+ it 'handles backticks (escaped in double quotes)' do
57
+ result = shell.execute_command('echo', ['`hello`'], env, 30, nil)
58
+ expect(result[:status]).to eq(0)
59
+ # Backticks are escaped with backtick in double-quoted strings
60
+ expect(result[:stdout].strip).to eq('`hello`')
61
+ end
62
+
63
+ it 'handles arguments with spaces' do
64
+ result = shell.execute_command('echo', ['hello world'], env, 30, nil)
65
+ expect(result[:status]).to eq(0)
66
+ expect(result[:stdout].strip).to eq('hello world')
67
+ end
68
+
69
+ it 'handles semicolons safely' do
70
+ result = shell.execute_command('echo', ['hello;world'], env, 30, nil)
71
+ expect(result[:status]).to eq(0)
72
+ expect(result[:stdout].strip).to eq('hello;world')
73
+ end
74
+ end
75
+
76
+ context 'with executable paths' do
77
+ it 'handles paths with spaces' do
78
+ # Use /bin/echo which is a simple command that exists
79
+ result = shell.execute_command('/bin/echo', ['test'], env, 30, nil)
80
+ expect(result[:status]).to eq(0)
81
+ expect(result[:stdout].strip).to eq('test')
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#join' do
87
+ context 'with dash-prefixed arguments' do
88
+ it 'quotes arguments starting with dash with double quotes' do
89
+ cmd = shell.join('gs', '-sDEVICE=pdfwrite', 'input.eps', 'output.pdf')
90
+ expect(cmd).to include('"-sDEVICE=pdfwrite"')
91
+ end
92
+
93
+ it 'quotes multiple dash-prefixed arguments' do
94
+ cmd = shell.join('gs', '-sDEVICE=pdfwrite', '-dBATCH', 'input.eps')
95
+ expect(cmd).to include('"-sDEVICE=pdfwrite"')
96
+ expect(cmd).to include('"-dBATCH"')
97
+ end
98
+ end
99
+
100
+ context 'with arguments containing special characters' do
101
+ it 'quotes and escapes arguments with dollar signs' do
102
+ cmd = shell.join('echo', '$VAR')
103
+ # Dollar sign is escaped with backtick in double-quoted strings
104
+ expect(cmd).to include('"`$VAR"')
105
+ end
106
+
107
+ it 'quotes arguments with spaces' do
108
+ cmd = shell.join('echo', 'hello world')
109
+ expect(cmd).to include('"hello world"')
110
+ end
111
+ end
112
+
113
+ context 'with simple arguments' do
114
+ it 'does not quote simple arguments' do
115
+ cmd = shell.join('echo', 'hello', 'world')
116
+ expect(cmd).to eq('echo hello world')
117
+ end
118
+ end
119
+
120
+ context 'with executable paths containing spaces' do
121
+ it 'quotes the executable path' do
122
+ cmd = shell.join('/path with space/gs', '-sDEVICE=pdfwrite')
123
+ expect(cmd).to include('"/path with space/gs"')
124
+ expect(cmd).to include('"-sDEVICE=pdfwrite"')
125
+ end
126
+ end
127
+ end
128
+
129
+ describe '#execute_command_with_stdin' do
130
+ it 'passes stdin data correctly' do
131
+ result = shell.execute_command_with_stdin('cat', [], env, 30, nil, 'hello from stdin')
132
+ expect(result[:status]).to eq(0)
133
+ expect(result[:stdout].strip).to eq('hello from stdin')
134
+ end
135
+
136
+ it 'preserves dash-prefixed arguments with stdin' do
137
+ # Use /bin/echo instead of 'echo' because PowerShell's echo alias (Write-Output)
138
+ # doesn't handle stdin piping correctly on Unix
139
+ result = shell.execute_command_with_stdin('/bin/echo', ['-sDEVICE=pdfwrite'], env, 30, nil, '')
140
+ expect(result[:status]).to eq(0)
141
+ expect(result[:stdout]).to include('-sDEVICE=pdfwrite')
142
+ end
143
+ end
144
+
145
+ describe 'real-world Ghostscript command simulation' do
146
+ it 'formats Ghostscript-style command correctly' do
147
+ # Simulate the command Vectory would run
148
+ args = [
149
+ '-sDEVICE=pdfwrite',
150
+ '-sOutputFile=output.pdf',
151
+ '-dBATCH',
152
+ '-dNOPAUSE',
153
+ 'input.ps'
154
+ ]
155
+ result = shell.execute_command('echo', args, env, 30, nil)
156
+
157
+ expect(result[:status]).to eq(0)
158
+ expect(result[:stdout]).to include('-sDEVICE=pdfwrite')
159
+ expect(result[:stdout]).to include('-sOutputFile=output.pdf')
160
+ expect(result[:stdout]).to include('-dBATCH')
161
+ expect(result[:stdout]).to include('-dNOPAUSE')
162
+ expect(result[:stdout]).to include('input.ps')
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'PowerShell Real Command Integration', if: system('which pwsh > /dev/null 2>&1') do
6
+ let(:shell) { Ukiryu::Shell::PowerShell.new }
7
+ let(:env) { Ukiryu::Environment.system }
8
+
9
+ describe 'End-to-end command execution with Ghostscript-style args' do
10
+ it 'correctly passes -sDEVICE=pdfwrite style arguments' do
11
+ # This is the exact pattern Vectory uses with Ghostscript
12
+ args = [
13
+ '-sDEVICE=pdfwrite',
14
+ '-sOutputFile=output.pdf',
15
+ '-dBATCH',
16
+ '-dNOPAUSE',
17
+ 'input.eps'
18
+ ]
19
+
20
+ result = shell.execute_command('echo', args, env, 30, nil)
21
+
22
+ expect(result[:status]).to eq(0)
23
+ expect(result[:stdout]).to include('-sDEVICE=pdfwrite')
24
+ expect(result[:stdout]).to include('-sOutputFile=output.pdf')
25
+ expect(result[:stdout]).to include('-dBATCH')
26
+ expect(result[:stdout]).to include('-dNOPAUSE')
27
+ expect(result[:stdout]).to include('input.eps')
28
+
29
+ # Ensure the full arguments are present (prefix not stripped)
30
+ expect(result[:stdout]).to match(/-sDEVICE=pdfwrite/)
31
+ expect(result[:stdout]).to match(/-sOutputFile=output.pdf/)
32
+
33
+ # Ensure we don't see arguments that start with = (prefix stripped)
34
+ # This would indicate the -sDEVICE prefix was stripped
35
+ expect(result[:stdout]).not_to match(/\s=pdfwrite\s/)
36
+ expect(result[:stdout]).not_to match(/\s=output\.pdf\s/)
37
+ end
38
+
39
+ it 'handles multiple consecutive dash-prefixed args' do
40
+ args = ['-a', '-b', '-c=value', '-d:value', 'file.txt']
41
+ result = shell.execute_command('echo', args, env, 30, nil)
42
+
43
+ expect(result[:status]).to eq(0)
44
+ expect(result[:stdout]).to include('-a')
45
+ expect(result[:stdout]).to include('-b')
46
+ expect(result[:stdout]).to include('-c=value')
47
+ expect(result[:stdout]).to include('-d:value')
48
+ end
49
+
50
+ it 'handles dash-prefixed args with spaces in values' do
51
+ args = ['-sOutputFile=C:/Program Files/output.pdf', 'input.eps']
52
+ result = shell.execute_command('echo', args, env, 30, nil)
53
+
54
+ expect(result[:status]).to eq(0)
55
+ expect(result[:stdout]).to include('-sOutputFile=C:/Program Files/output.pdf')
56
+ end
57
+ end
58
+
59
+ describe 'Command line building via join()' do
60
+ it 'produces correct command line for Ghostscript-style args' do
61
+ cmd = shell.join(
62
+ 'C:/Program Files/gs/gs10.00.0/bin/gswin64c.exe',
63
+ '-sDEVICE=pdfwrite',
64
+ '-sOutputFile=output.pdf',
65
+ '-dBATCH',
66
+ 'input.eps'
67
+ )
68
+
69
+ # All dash-prefixed args should be quoted
70
+ expect(cmd).to include('"-sDEVICE=pdfwrite"')
71
+ expect(cmd).to include('"-sOutputFile=output.pdf"')
72
+ expect(cmd).to include('"-dBATCH"')
73
+
74
+ # Simple arg should not be quoted
75
+ expect(cmd).to include(' input.eps')
76
+
77
+ # Should NOT contain stripped prefixes (standalone = without the - prefix)
78
+ expect(cmd).not_to match(/\s=pdfwrite\b/)
79
+ expect(cmd).not_to match(/\s=output\.pdf\b/)
80
+ end
81
+
82
+ it 'handles executable paths with spaces' do
83
+ cmd = shell.join('C:/Program Files/gs/gswin64c.exe', '-sDEVICE=pdfwrite')
84
+
85
+ # Executable path should be quoted
86
+ expect(cmd).to include('"C:/Program Files/gs/gswin64c.exe"')
87
+
88
+ # Dash-prefixed arg should be quoted
89
+ expect(cmd).to include('"-sDEVICE=pdfwrite"')
90
+ end
91
+ end
92
+
93
+ describe 'Tool integration with Ghostscript profile' do
94
+ let(:ghostscript_profile) { 'spec/fixtures/profiles/ghostscript_10.0.yaml' }
95
+
96
+ it 'builds correct args through Tool#build_args' do
97
+ skip 'Ghostscript profile not found' unless File.exist?(ghostscript_profile)
98
+
99
+ tool = Ukiryu::Tool.from_file(ghostscript_profile, platform: :windows, shell: :powershell)
100
+
101
+ command = tool.command_definition(:convert)
102
+ params = { device: 'pdfwrite', output: 'output.pdf', inputs: ['input.eps'] }
103
+
104
+ args = tool.build_args(command, params)
105
+
106
+ # Verify args are correct
107
+ expect(args).to be_an(Array)
108
+ expect(args).to include('-sDEVICE=pdfwrite')
109
+ expect(args).to include('-sOutputFile=output.pdf')
110
+
111
+ # Each arg should be a String, not an Array
112
+ args.each do |arg|
113
+ expect(arg).to be_a(String), "Expected String, got #{arg.class}: #{arg.inspect}"
114
+ end
115
+ end
116
+ end
117
+
118
+ describe 'Edge cases that could cause prefix stripping' do
119
+ it 'handles args passed as nested arrays (should be flattened)' do
120
+ # This simulates what might happen if someone passes args incorrectly
121
+ nested_args = [['-sDEVICE=pdfwrite', 'input.eps']]
122
+
123
+ # When nested arrays are passed to join, they get stringified
124
+ # This is the problematic behavior we need to detect
125
+ cmd = shell.join('gswin64c.exe', *nested_args)
126
+
127
+ # The nested array becomes a string like '["-sDEVICE=pdfwrite", "input.eps"]'
128
+ # which is NOT what we want
129
+ # This test documents the current behavior - we should NOT have nested arrays
130
+ expect(cmd).to include('[') # Array stringification
131
+ end
132
+
133
+ it 'correctly handles flat args vs nested args' do
134
+ flat_args = ['-sDEVICE=pdfwrite', 'input.eps']
135
+ cmd_flat = shell.join('gswin64c.exe', *flat_args)
136
+
137
+ # Flat args should NOT have array stringification
138
+ expect(cmd_flat).not_to include('[')
139
+ expect(cmd_flat).not_to include(']')
140
+ expect(cmd_flat).to include('"-sDEVICE=pdfwrite"')
141
+ end
142
+ end
143
+ end