cliver 0.2.2 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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