cliver 0.2.2 → 0.3.1

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.
@@ -4,6 +4,7 @@ require File.expand_path('../core_ext/file', __FILE__)
4
4
 
5
5
  require 'cliver/version'
6
6
  require 'cliver/dependency'
7
+ require 'cliver/shell_capture'
7
8
  require 'cliver/detector'
8
9
  require 'cliver/filter'
9
10
 
@@ -49,16 +50,19 @@ module Cliver
49
50
  end
50
51
 
51
52
  # Verify an absolute-path to an executable.
52
- # @param (see Cliver::Dependency#initialize)
53
- # EXCEPT: executable must be a single, absolute path.
54
- # @option options [Boolean] :strict (true) @see Cliver::Dependency::initialize
55
- # @raise (see Cliver::Dependency#detect!)
56
- # @return (see Cliver::Dependency#detect!)
53
+ # @overload verify!(executable, *requirements, options = {})
54
+ # @param executable [String] absolute path to an executable
55
+ # @param requirements (see Cliver::Dependency#initialize)
56
+ # @option options (see Cliver::Dependency::initialize)
57
+ # @raise (see Cliver::Dependency#detect!)
58
+ # @return (see Cliver::Dependency#detect!)
57
59
  def self.verify!(executable, *args, &block)
58
60
  unless File.absolute_path?(executable)
59
61
  raise ArgumentError, "executable path must be absolute, " +
60
62
  "got '#{executable.inspect}'."
61
63
  end
64
+ options = args.last.kind_of?(Hash) ? args.pop : {}
65
+ args << options.merge(:path => '.') # ensure path non-empty.
62
66
  Dependency::new(executable, *args, &block).detect!
63
67
  end
64
68
 
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  require 'rubygems/requirement'
3
+ require 'set'
3
4
 
4
5
  module Cliver
5
6
  # This is how a dependency is specified.
@@ -197,18 +198,18 @@ module Cliver
197
198
  def find_executables
198
199
  return enum_for(:find_executables) unless block_given?
199
200
 
200
- exts = ENV.has_key?('PATHEXT') ? ENV.fetch('PATHEXT').split(';') : ['']
201
+ exts = (ENV.has_key?('PATHEXT') ? ENV.fetch('PATHEXT').split(';') : []) << ''
201
202
  paths = @path.sub('*', ENV['PATH']).split(File::PATH_SEPARATOR)
203
+ raise ArgumentError.new('No PATH to search!') if paths.empty?
202
204
  cmds = strict? ? @executables.first(1) : @executables
203
205
 
204
- detected_exes = []
206
+ lookup_cache = Set.new
205
207
  cmds.product(paths, exts).map do |cmd, path, ext|
206
208
  exe = File.absolute_path?(cmd) ? cmd : File.expand_path("#{cmd}#{ext}", path)
207
209
 
210
+ next unless lookup_cache.add?(exe) # don't yield the same exe path 2x
208
211
  next unless File.executable?(exe)
209
- next if detected_exes.include?(exe) # don't yield the same exe path 2x
210
212
 
211
- detected_exes << exe
212
213
  yield exe
213
214
  end
214
215
  end
@@ -40,15 +40,15 @@ module Cliver
40
40
  # @return [String] - should be contain {Gem::Version}-parsable
41
41
  # version number.
42
42
  def detect_version(executable_path)
43
- output = shell_out_and_capture version_command(executable_path).shelljoin
44
- if $?.exitstatus == 127
43
+ capture = ShellCapture.new(version_command(executable_path))
44
+ unless capture.command_found
45
45
  raise Cliver::Dependency::NotFound.new(
46
46
  "Could not find an executable at given path '#{executable_path}'." +
47
47
  "If this path was not specified explicitly, it is probably a " +
48
48
  "bug in [Cliver](https://github.com/yaauie/cliver/issues)."
49
49
  )
50
50
  end
51
- output[version_pattern]
51
+ capture.stdout[version_pattern] || capture.stderr[version_pattern]
52
52
  end
53
53
 
54
54
  # This is the interface that any detector must have.
@@ -80,13 +80,5 @@ module Cliver
80
80
  def version_command(executable_path)
81
81
  [executable_path, *Array(command_arg)]
82
82
  end
83
-
84
- private
85
-
86
- # @api private
87
- # A boundary that is useful for testing.
88
- def shell_out_and_capture(command)
89
- `#{command} 2>&1`
90
- end
91
83
  end
92
84
  end
@@ -0,0 +1,35 @@
1
+ require 'open3'
2
+
3
+ module Cliver
4
+ class ShellCapture
5
+ attr_reader :stdout, :stderr, :command_found
6
+
7
+ # @overlaod initialize(command)
8
+ # @param command [String] the command to run
9
+ # @overload initialize(command)
10
+ # @param command [Array<String>] the command to run; elements in
11
+ # the supplied array will be shelljoined.
12
+ # @return [void]
13
+ def initialize(command)
14
+ command = command.shelljoin if command.kind_of?(Array)
15
+ @stdout = @stderr = ''
16
+ begin
17
+ Open3.popen3(command) do |i, o, e|
18
+ @stdout = o.read.chomp
19
+ @stderr = e.read.chomp
20
+ end
21
+ # Fix for ruby 1.8.7 (and probably earlier):
22
+ # Open3.popen3 does not raise anything there, but the error goes to STDERR.
23
+ if @stderr =~ /open3.rb:\d+:in `exec': No such file or directory -.*\(Errno::ENOENT\)/ or
24
+ @stderr =~ /An exception occurred in a forked block\W+No such file or directory.*\(Errno::ENOENT\)/
25
+ @stderr = ''
26
+ @command_found = false
27
+ else
28
+ @command_found = true
29
+ end
30
+ rescue Errno::ENOENT, IOError
31
+ @command_found = false
32
+ end
33
+ end
34
+ end
35
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Cliver
4
4
  # Cliver follows {http://semver.org SemVer}
5
- VERSION = '0.2.2'
5
+ VERSION = '0.3.1'
6
6
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
+ # Core-Extensions on File
3
4
  class File
4
5
  # determine whether a String path is absolute.
5
6
  # @example
@@ -9,10 +10,17 @@ class File
9
10
  # File.absolute_path?('/foo/bar') #=> true
10
11
  # File.absolute_path?('C:foo/bar') #=> false
11
12
  # File.absolute_path?('C:/foo/bar') #=> true
12
- # @param [String] - a pathname
13
+ # @param path [String] - a pathname
13
14
  # @return [Boolean]
14
- def self.absolute_path?(path)
15
- false | path[ABSOLUTE_PATH_PATTERN]
15
+ def self.absolute_path?(path, platform = :default)
16
+ pattern = case platform
17
+ when :default then ABSOLUTE_PATH_PATTERN
18
+ when :windows then WINDOWS_ABSOLUTE_PATH_PATTERN
19
+ when :posix then POSIX_ABSOLUTE_PATH_PATTERN
20
+ else raise ArgumentError, "Unsupported platform '#{platform.inspect}'"
21
+ end
22
+
23
+ false | path[pattern]
16
24
  end
17
25
 
18
26
  unless defined?(POSIX_ABSOLUTE_PATH_PATTERN)
@@ -41,4 +41,38 @@ describe Cliver::Detector do
41
41
  its(:command_arg) { should eq [version_arg] }
42
42
  its(:version_pattern) { should eq regexp_arg }
43
43
  end
44
+
45
+ context 'detecting a command' do
46
+ before(:each) do
47
+ Cliver::ShellCapture.stub(:new => capture)
48
+ end
49
+
50
+ context 'that reports version on stdout' do
51
+ let(:capture) { double('capture', :stdout => '1.1',
52
+ :stderr => 'Warning: There is a monkey 1.2 metres left of you.',
53
+ :command_found => true) }
54
+
55
+ it 'should prefer the stdout output' do
56
+ expect(detector.detect_version('foo')).to eq('1.1')
57
+ end
58
+ end
59
+
60
+ context 'that reports version on stderr' do
61
+ let(:capture) { double('capture', :stdout => '',
62
+ :stderr => 'Version: 1.666',
63
+ :command_found => true) }
64
+
65
+ it 'should prefer the stderr output' do
66
+ expect(detector.detect_version('foo')).to eq('1.666')
67
+ end
68
+ end
69
+
70
+ context 'that does not exist' do
71
+ let(:capture) { Cliver::ShellCapture.new('acommandnosystemshouldhave123') }
72
+
73
+ it 'should raise an exception' do
74
+ expect { detector.detect_version('foo') }.to raise_error(Cliver::Dependency::NotFound)
75
+ end
76
+ end
77
+ end
44
78
  end
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+ require 'cliver'
3
+
4
+ describe Cliver::ShellCapture do
5
+ let(:test_command) { 'test command' }
6
+ subject { Cliver::ShellCapture.new(test_command) }
7
+
8
+ context 'a command that exists' do
9
+ let(:intended_stdout) { StringIO.new('1.1.1').tap(&:rewind) }
10
+ let(:intended_stderr) { StringIO.new('foo baar 1').tap(&:rewind) }
11
+ let(:intended_stdin) { StringIO.new('').tap(&:rewind) }
12
+
13
+ ['test command', %w(test command)].each do |input|
14
+ context "with #{input.class.name} input" do
15
+ let(:test_command) { input }
16
+
17
+ before(:each) do
18
+ Open3.should_receive(:popen3) do |*args|
19
+ args.size.should eq 1
20
+ args.first.should == 'test command'
21
+ end.and_yield(intended_stdin, intended_stdout, intended_stderr)
22
+ end
23
+
24
+ its(:stdout) { should eq '1.1.1' }
25
+ its(:stderr) { should eq 'foo baar 1' }
26
+ its(:command_found) { should be_true }
27
+ end
28
+ end
29
+ end
30
+
31
+ context 'looking for a command that does not exist' do
32
+ before(:each) do
33
+ Open3.should_receive(:popen3) do |command|
34
+ command.should eq test_command
35
+ raise Errno::ENOENT.new("No such file or directory - #{test_command}")
36
+ end
37
+ end
38
+ its(:stdout) { should eq '' }
39
+ its(:stderr) { should eq '' }
40
+ its(:command_found) { should be_false }
41
+ end
42
+ end
@@ -1,6 +1,14 @@
1
1
  # encoding: utf-8
2
2
  require 'cliver'
3
3
  require 'spec_helper'
4
+ require File.expand_path('../support/executable_mock', __FILE__)
5
+
6
+ RSpec::Matchers.define :be_filesystem_equivalent_of do |expected|
7
+ match do |actual|
8
+ ExecutableMock.new(expected) == actual
9
+ end
10
+ end
11
+
4
12
 
5
13
  describe Cliver do
6
14
  # The setup. Your test will likeley interact with subject.
@@ -10,29 +18,32 @@ describe Cliver do
10
18
  # These can get overridden in context blocks
11
19
  let(:method) { raise ArgumentError, 'spec didn\'t specify :method' }
12
20
  let(:args) { raise ArgumentError, 'spec didn\'t specify :args' }
13
- let(:block) { version_map.method(:fetch) }
21
+ let(:block) { version_directory.method(:version) }
22
+
23
+ # BecauseWindows. This enables us to mock out File::executable?
24
+ # and the responses from our detectors given any representation
25
+ # of a file path.
26
+ let(:version_directory) do
27
+ ExecutableMock::Registry.new(version_map)
28
+ end
14
29
 
15
30
  before(:each) do
16
- File.stub(:executable?) { |path| executables.include? path }
17
- # Cliver::Dependency.any_instance.stub(:detect_version) do |arg|
18
- # version_map[arg]
19
- # end
31
+ File.stub(:executable?, &version_directory.method(:executable?))
20
32
  end
21
33
 
22
34
  let(:options) do
23
35
  {
24
- :path => path,
36
+ :path => path.join(File::PATH_SEPARATOR),
25
37
  :executable => executable,
26
38
  }
27
39
  end
28
- let(:executables) { version_map.keys }
29
40
  let(:args) do
30
41
  args = [Array(executable)]
31
42
  args.concat Array(requirement)
32
43
  args << options
33
44
  end
34
45
 
35
- let(:path) { '/foo/bar:/baz/bingo' }
46
+ let(:path) { ['/foo/bar','/baz/bingo'] }
36
47
  let(:executable) { 'doodle' }
37
48
  let(:requirement) { '~>1.1'}
38
49
 
@@ -55,14 +66,14 @@ describe Cliver do
55
66
  end
56
67
  context '::detect' do
57
68
  let(:method) { :detect }
58
- it { should eq '/baz/bingo/doodle' }
69
+ it { should be_filesystem_equivalent_of '/baz/bingo/doodle' }
59
70
  end
60
71
  context '::detect!' do
61
72
  let(:method) { :detect! }
62
73
  it 'should not raise' do
63
74
  expect { action }.to_not raise_exception
64
75
  end
65
- it { should eq '/baz/bingo/doodle' }
76
+ it { should be_filesystem_equivalent_of '/baz/bingo/doodle' }
66
77
  end
67
78
  end
68
79
 
@@ -137,7 +148,7 @@ describe Cliver do
137
148
  let(:version_map) do
138
149
  {'/baz/bingo/doodle' => '1.2.1'}
139
150
  end
140
- let(:path) { '/fiddle/foo:/deedle/dee'}
151
+ let(:path) { ['/fiddle/foo','/deedle/dee'] }
141
152
 
142
153
  context 'that is absolute' do
143
154
  let(:executable) { '/baz/bingo/doodle' }
@@ -212,14 +223,14 @@ describe Cliver do
212
223
  end
213
224
  context '::detect' do
214
225
  let(:method) { :detect }
215
- it { should eq '/baz/bingo/doodle'}
226
+ it { should be_filesystem_equivalent_of '/baz/bingo/doodle' }
216
227
  end
217
228
  context '::detect!' do
218
229
  let(:method) { :detect! }
219
230
  it 'should not raise' do
220
231
  expect { action }.to_not raise_exception
221
232
  end
222
- it { should eq '/baz/bingo/doodle' }
233
+ it { should be_filesystem_equivalent_of '/baz/bingo/doodle' }
223
234
  end
224
235
  end
225
236
  end
@@ -262,7 +273,7 @@ describe Cliver do
262
273
  end
263
274
  context '::detect' do
264
275
  let(:method) { :detect }
265
- it { should eq '/baz/bingo/primary' }
276
+ it { should be_filesystem_equivalent_of '/baz/bingo/primary' }
266
277
  end
267
278
  end
268
279
  context 'and primary insufficient' do
@@ -275,7 +286,7 @@ describe Cliver do
275
286
  context 'the secondary' do
276
287
  context '::detect' do
277
288
  let(:method) { :detect }
278
- it { should eq '/foo/bar/fallback' }
289
+ it { should be_filesystem_equivalent_of '/foo/bar/fallback' }
279
290
  end
280
291
  end
281
292
  end
@@ -289,7 +300,7 @@ describe Cliver do
289
300
  end
290
301
  context '::detect' do
291
302
  let(:method) { :detect }
292
- it { should eq '/foo/bar/fallback' }
303
+ it { should be_filesystem_equivalent_of '/foo/bar/fallback' }
293
304
  end
294
305
  end
295
306
  end
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ class ExecutableMock
4
+ def initialize(path)
5
+ @path = _to_platform_abs_path(path)
6
+ end
7
+ attr_reader :path
8
+ attr_reader :version
9
+
10
+ def == (other_path)
11
+ other_path = _to_platform_abs_path(other_path)
12
+ if other_path[/[A-Z]:/i] # windows
13
+ pattern = /\A#{self.path}(#{(ENV['PATHEXT']||'').split(';').map(&Regexp::method(:escape)).join('|')})?\Z/i
14
+ pattern =~ other_path
15
+ else # posix
16
+ self.path == other_path
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def _to_platform_abs_path(source)
23
+ (File::absolute_path?(source, :windows) && !File::absolute_path?(source, :posix)) ?
24
+ source.tr('\\','/') :
25
+ File.expand_path(source)
26
+ end
27
+
28
+ class Registry
29
+ def initialize(version_map)
30
+ @registry = {}
31
+ version_map.each do |path,version|
32
+ @registry[ExecutableMock.new(path)] = version
33
+ end
34
+ end
35
+
36
+ def executable?(path)
37
+ false | version(path)
38
+ end
39
+
40
+ def version(path)
41
+ key = @registry.keys.find {|exe| exe == path }
42
+ key && @registry[key]
43
+ end
44
+ end
45
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cliver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-09-18 00:00:00.000000000 Z
12
+ date: 2013-10-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -127,12 +127,15 @@ files:
127
127
  - lib/cliver/dependency.rb
128
128
  - lib/cliver/detector.rb
129
129
  - lib/cliver/filter.rb
130
+ - lib/cliver/shell_capture.rb
130
131
  - lib/cliver/version.rb
131
132
  - lib/core_ext/file.rb
132
133
  - spec/cliver/detector_spec.rb
134
+ - spec/cliver/shell_capture_spec.rb
133
135
  - spec/cliver_spec.rb
134
136
  - spec/core_ext/file_spec.rb
135
137
  - spec/spec_helper.rb
138
+ - spec/support/executable_mock.rb
136
139
  homepage: https://www.github.com/yaauie/cliver
137
140
  licenses:
138
141
  - MIT
@@ -160,7 +163,9 @@ specification_version: 3
160
163
  summary: Cross-platform version constraints for cli tools
161
164
  test_files:
162
165
  - spec/cliver/detector_spec.rb
166
+ - spec/cliver/shell_capture_spec.rb
163
167
  - spec/cliver_spec.rb
164
168
  - spec/core_ext/file_spec.rb
165
169
  - spec/spec_helper.rb
170
+ - spec/support/executable_mock.rb
166
171
  has_rdoc: yard