cliver 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,42 +2,94 @@
2
2
 
3
3
  Sometimes Ruby apps shell out to command-line executables, but there is no
4
4
  standard way to ensure those underlying dependencies are met. Users usually
5
- find out via a nasty stack-trace and whatever wasn't captured on stderr.
5
+ find out via a nasty stack-trace and whatever wasn't captured on stderr, or by
6
+ the odd behavior exposed by a version mismatch.
6
7
 
7
- `Cliver` is a simple gem that provides an easy way to make assertions about
8
+ `Cliver` is a simple gem that provides an easy way to detect and use
8
9
  command-line dependencies. Under the covers, it uses [rubygems/requirements][]
9
10
  so it supports the version requirements you're used to providing in your
10
11
  gemspec.
11
12
 
12
13
  ## Usage
13
14
 
15
+ ### Detect and Detect!
16
+
17
+ The detect methods search your entire path until they find a matching executable
18
+ or run out of places to look.
19
+
14
20
  ```ruby
15
- Cliver.assert('subl') # no version requirements
16
- Cliver.assert('bzip2', '~> 1.0.6') # one version requirement
17
- Cliver.assert('racc', '>= 1.0', '< 1.4.9') # many version requirements
21
+ # no version requirements
22
+ Cliver.detect('subl')
23
+ # => '/Users/yaauie/.bin/subl'
24
+
25
+ # one version requirement
26
+ Cliver.detect('bzip2', '~> 1.0.6')
27
+ # => '/usr/bin/bzip2'
28
+
29
+ # many version requirements
30
+ Cliver.detect('racc', '>= 1.0', '< 1.4.9')
31
+ # => '/Users/yaauie/.rbenv/versions/1.9.3-p194/bin/racc'
32
+
33
+ # dependency not met
34
+ Cliver.detect('racc', '~> 10.4.9')
35
+ # => nil
36
+
37
+ # detect! raises Cliver::Dependency::NotMet exceptions when the dependency
38
+ # cannot be met.
39
+ Cliver.detect!('ruby', '1.8.5')
40
+ # Cliver::Dependency::VersionMismatch
41
+ # Could not find an executable ruby that matched the
42
+ # requirements '1.8.5'. Found versions were {'/usr/bin/ruby'=> '1.8.7'}
43
+ Cliver.detect!('asdfasdf')
44
+ # Cliver::Dependency::NotFound
45
+ # Could not find an executable asdfasdf on your path
18
46
  ```
19
47
 
20
- If the executable can't be found on your path at all, a
21
- `Cliver::Assertion::DependencyNotFound` exception is raised; if the version
22
- reached does not meet the requirements, a `Cliver::Assertion::DependencyVersionMismatch`
23
- exception is raised; both inherit from `Cliver::Assertion::DependencyNotMet`
48
+ ### Assert
49
+
50
+ The assert method is useful when you do not have control over how the
51
+ dependency is shelled-out to and require that the first matching executable on
52
+ your path satisfies your version requirements. It is the equivalent of the
53
+ detect! method with `strict: true` option.
24
54
 
25
55
  ## Advanced Usage:
26
56
 
57
+ ### Version Detectors
58
+
27
59
  Some programs don't provide nice 'version 1.2.3' strings in their `--version`
28
60
  output; `Cliver` lets you provide your own version detector with a pattern.
29
61
 
30
62
  ```ruby
31
63
  Cliver.assert('python', '~> 1.7',
32
- detector: Cliver::Detector.new(/(?<=Python )[0-9][.0-9a-z]+/))
64
+ detector: /(?<=Python )[0-9][.0-9a-z]+/)
33
65
  ```
34
66
 
35
67
  Other programs don't provide a standard `--version`; `Cliver::Detector` also
36
68
  allows you to provide your own arg to get the version:
37
69
 
70
+ ```ruby
71
+ # single-argument command
72
+ Cliver.assert('janky', '~> 10.1.alpha',
73
+ detector: '--release-version')
74
+
75
+ # multi-argument command
76
+ Cliver.detect('ruby', '~> 1.8.7',
77
+ detector: [['-e', 'puts RUBY_VERSION']])
78
+ ```
79
+
80
+ You can use both custom pattern and custom command by supplying an array:
81
+
38
82
  ```ruby
39
83
  Cliver.assert('janky', '~> 10.1.alpha',
40
- detector: Cliver::Detector.new('--release-version'))
84
+ detector: ['--release-version', /.*/])
85
+ ```
86
+
87
+ And even supply multiple arguments in an Array, too:
88
+
89
+ ```ruby
90
+ # multi-argument command
91
+ Cliver.detect('ruby', '~> 1.8.7',
92
+ detector: ['-e', 'puts RUBY_VERSION'])
41
93
  ```
42
94
 
43
95
  Alternatively, you can supply your own detector (anything that responds to
@@ -55,6 +107,8 @@ And since some programs don't always spit out nice semver-friendly version
55
107
  numbers at all, a filter proc can be supplied to clean it up. Note how the
56
108
  filter is applied to both your requirements and the executable's output:
57
109
 
110
+ ### Filters
111
+
58
112
  ```ruby
59
113
  Cliver.assert('built-thing', '~> 2013.4r8273',
60
114
  filter: proc { |ver| ver.tr('r','.') })
@@ -63,6 +117,17 @@ Cliver.assert('built-thing', '~> 2013.4r8273',
63
117
  Since `Cliver` uses `Gem::Requirement` for version comparrisons, it obeys all
64
118
  the same rules including pre-release semantics.
65
119
 
120
+ ### Search Path
121
+
122
+ By default, Cliver uses `ENV['PATH']` as its search path, but you can provide
123
+ your own. If the asterisk symbol (`*`) is included in your string, it is
124
+ replaced `ENV['PATH']`.
125
+
126
+ ```ruby
127
+ Cliver.detect('gadget', path: './bins/:*')
128
+ # => 'Users/yaauie/src/project-a/bins/gadget'
129
+ ```
130
+
66
131
  ## Supported Platforms
67
132
 
68
133
  The goal is to have full support for all platforms running ruby >= 1.9.2,
@@ -1,20 +1,48 @@
1
1
  # encoding: utf-8
2
2
  require 'cliver/version'
3
- require 'cliver/which'
4
- require 'cliver/assertion'
3
+ require 'cliver/dependency'
5
4
  require 'cliver/detector'
6
5
  require 'cliver/filter'
7
6
 
8
7
  # Cliver is tool for making dependency assertions against
9
8
  # command-line executables.
10
9
  module Cliver
11
- # @see Cliver::Assertion
12
- # @overload (see Cliver::Assertion#initialize)
13
- # @param (see Cliver::Assertion#initialize)
14
- # @raise (see Cliver::Assertion#assert!)
15
- # @return (see Cliver::Assertion#assert!)
10
+
11
+ # The primary interface for the Cliver gem allows detection of an executable
12
+ # on your path that matches a version requirement, or raise an appropriate
13
+ # exception to make resolution simple and straight-forward.
14
+ # @see Cliver::Dependency
15
+ # @overload (see Cliver::Dependency#initialize)
16
+ # @param (see Cliver::Dependency#initialize)
17
+ # @raise (see Cliver::Dependency#detect!)
18
+ # @return (see Cliver::Dependency#detect!)
19
+ def self.detect!(*args, &block)
20
+ Dependency::new(*args, &block).detect!
21
+ end
22
+
23
+ # A non-raising variant of {::detect!}, simply returns false if dependency
24
+ # cannot be found.
25
+ # @see Cliver::Dependency
26
+ # @overload (see Cliver::Dependency#initialize)
27
+ # @param (see Cliver::Dependency#initialize)
28
+ # @raise (see Cliver::Dependency#detect)
29
+ # @return (see Cliver::Dependency#detect)
30
+ def self.detect(*args, &block)
31
+ Dependency::new(*args, &block).detect
32
+ end
33
+
34
+ # A legacy interface for {::detect} with the option `strict: true`, ensures
35
+ # that the first executable on your path matches the requirements.
36
+ # @see Cliver::Dependency
37
+ # @overload (see Cliver::Dependency#initialize)
38
+ # @param (see Cliver::Dependency#initialize)
39
+ # @option options [Boolean] :strict (true) @see Cliver::Dependency::initialize
40
+ # @raise (see Cliver::Dependency#assert!)
41
+ # @return (see Cliver::Dependency#assert!)
16
42
  def self.assert(*args, &block)
17
- Assertion.new(*args, &block).assert!
43
+ options = args.last.kind_of?(Hash) ? args.pop : {}
44
+ args << options.merge(:strict => true)
45
+ Dependency::new(*args, &block).detect!
18
46
  end
19
47
 
20
48
  extend self
@@ -28,7 +56,7 @@ module Cliver
28
56
  def dependency_unmet?(*args, &block)
29
57
  Cliver.assert(*args, &block)
30
58
  false
31
- rescue Assertion::DependencyNotMet => error
59
+ rescue Dependency::NotMet => error
32
60
  # Cliver::Assertion::VersionMismatch -> 'Version Mismatch'
33
61
  reason = error.class.name.split(':').last.gsub(/([a-z])([A-Z])/, '\\1 \\2')
34
62
  "#{reason}: #{error.message}"
@@ -0,0 +1,198 @@
1
+ # encoding: utf-8
2
+ require 'rubygems/requirement'
3
+
4
+ module Cliver
5
+ # This is how a dependency is specified.
6
+ class Dependency
7
+
8
+ # An exception class raised when assertion is not met
9
+ NotMet = Class.new(ArgumentError)
10
+
11
+ # An exception that is raised when executable present, but
12
+ # no version that matches the requirements is present.
13
+ VersionMismatch = Class.new(Dependency::NotMet)
14
+
15
+ # An exception that is raised when executable is not present at all.
16
+ NotFound = Class.new(Dependency::NotMet)
17
+
18
+ # A pattern for extracting a {Gem::Version}-parsable version
19
+ PARSABLE_GEM_VERSION = /[0-9]+(.[0-9]+){0,4}(.[a-zA-Z0-9]+)?/.freeze
20
+
21
+ # @overload initialize(executables, *requirements, options = {})
22
+ # @param executables [String,Array<String>] api-compatible executable names
23
+ # e.g, ['python2','python']
24
+ # @param requirements [Array<String>, String] splat of strings
25
+ # whose elements follow the pattern
26
+ # [<operator>] <version>
27
+ # Where <operator> is optional (default '='') and in the set
28
+ # '=', '!=', '>', '<', '>=', '<=', or '~>'
29
+ # And <version> is dot-separated integers with optional
30
+ # alphanumeric pre-release suffix. See also
31
+ # {http://docs.rubygems.org/read/chapter/16 Specifying Versions}
32
+ # @param options [Hash<Symbol,Object>]
33
+ # @option options [Cliver::Detector] :detector (Detector.new)
34
+ # @option options [#to_proc, Object] :detector (see Detector::generate)
35
+ # @option options [#to_proc] :filter ({Cliver::Filter::IDENTITY})
36
+ # @option options [Boolean] :strict (false)
37
+ # true - fail if first match on path fails
38
+ # to meet version requirements.
39
+ # This is used for Cliver::assert.
40
+ # false - continue looking on path until a
41
+ # sufficient version is found.
42
+ # @option options [String] :path ('*') the path on which to search
43
+ # for executables. If an asterisk (`*`) is
44
+ # included in the supplied string, it is
45
+ # replaced with `ENV['PATH']`
46
+ #
47
+ # @yieldparam executable_path [String] (see Detector#detect_version)
48
+ # @yieldreturn [String] containing a version that, once filtered, can be
49
+ # used for comparrison.
50
+ def initialize(executables, *args, &detector)
51
+ options = args.last.kind_of?(Hash) ? args.pop : {}
52
+ @detector = Detector::generate(detector || options[:detector])
53
+ @filter = options.fetch(:filter, Filter::IDENTITY).extend(Filter)
54
+ @path = options.fetch(:path, '*')
55
+ @strict = options.fetch(:strict, false)
56
+
57
+ @executables = Array(executables).dup.freeze
58
+
59
+ @requirement = args unless args.empty?
60
+ end
61
+
62
+ # Get all the installed versions of the api-compatible executables.
63
+ # If a block is given, it yields once per found executable, lazily.
64
+ # @yieldparam executable_path [String]
65
+ # @yieldparam version [String]
66
+ # @yieldreturn [Boolean] - true if search should stop.
67
+ # @return [Hash<String,String>] executable_path, version
68
+ def installed_versions
69
+ return enum_for(:installed_versions) unless block_given?
70
+
71
+ find_executables.each do |executable_path|
72
+ version = detect_version(executable_path)
73
+
74
+ break(2) if yield(executable_path, version)
75
+ end
76
+ end
77
+
78
+ # The non-raise variant of {#detect!}
79
+ # @return (see #detect!)
80
+ # or nil if no match found.
81
+ def detect
82
+ detect!
83
+ rescue Dependency::NotMet
84
+ nil
85
+ end
86
+
87
+ # Detects an installed version of the executable that matches the
88
+ # requirements.
89
+ # @return [String] path to an executable that meets the requirements
90
+ # @raise [Cliver::Dependency::NotMet] if no match found
91
+ def detect!
92
+ installed = {}
93
+ installed_versions.each do |path, version|
94
+ installed[path] = version
95
+ return path if requirement_satisfied_by?(version)
96
+ strict?
97
+ end
98
+
99
+ # dependency not met. raise the appropriate error.
100
+ raise_not_found! if installed.empty?
101
+ raise_version_mismatch!(installed)
102
+ end
103
+
104
+ private
105
+
106
+ # @api private
107
+ # @return [Gem::Requirement]
108
+ def filtered_requirement
109
+ @filtered_requirement ||= begin
110
+ Gem::Requirement.new(@filter.requirements(@requirement))
111
+ end
112
+ end
113
+
114
+ # @api private
115
+ # @param raw_version [String]
116
+ # @return [Boolean]
117
+ def requirement_satisfied_by?(raw_version)
118
+ return true unless @requirement
119
+ parsable_version = @filter.apply(raw_version)[PARSABLE_GEM_VERSION]
120
+ parsable_version || raise(ArgumentError) # TODO: make descriptive
121
+ filtered_requirement.satisfied_by? Gem::Version.new(parsable_version)
122
+ end
123
+
124
+ # @api private
125
+ # @raise [Cliver::Dependency::NotFound] with appropriate error message
126
+ def raise_not_found!
127
+ raise Dependency::NotFound.new <<-EOERR
128
+ Could not find an executable #{@executables} on your path.
129
+ EOERR
130
+ end
131
+
132
+ # @api private
133
+ # @raise [Cliver::Dependency::VersionMismatch] with appropriate error message
134
+ # @param installed [Hash<String,String>] the found versions
135
+ def raise_version_mismatch!(installed)
136
+ raise Dependency::VersionMismatch.new <<-EOERR
137
+ Could not find an executable #{executable_description} that matched the
138
+ requirements #{requirements_description}.
139
+ Found versions were #{installed.inspect}
140
+ EOERR
141
+ end
142
+
143
+ # @api private
144
+ # @return [String] a plain-language representation of the executables
145
+ # for which we were searching
146
+ def executable_description
147
+ quoted_exes = @executables.map {|exe| "'#{exe}'" }
148
+ return quoted_exes.first if quoted_exes.size == 1
149
+
150
+ last_quoted_exec = quoted_exes.pop
151
+ "#{quoted_exes.join(', ')} or #{last_quoted_exec}"
152
+ end
153
+
154
+ # @api private
155
+ # @return [String] a plain-language representation of the requirements
156
+ def requirements_description
157
+ @requirement.map {|req| "'#{req}'" }.join(', ')
158
+ end
159
+
160
+ # If strict? is true, only attempt the first matching executable on the path
161
+ # @api private
162
+ # @return [Boolean]
163
+ def strict?
164
+ false | @strict
165
+ end
166
+
167
+ # Given a path to an executable, detect its version
168
+ # @api private
169
+ # @param executable_path [String]
170
+ # @return [String]
171
+ # @raise [ArgumentError] if version cannot be detected.
172
+ def detect_version(executable_path)
173
+ # No need to shell out if we are only checking its presence.
174
+ return '99.version_detection_not_required' unless @requirement
175
+
176
+ raw_version = @detector.to_proc.call(executable_path)
177
+ raw_version || raise(ArgumentError,
178
+ "The detector #{@detector} failed to detect the" +
179
+ "version of the executable at '#{executable_path}'")
180
+ end
181
+
182
+ # Analog of Windows `where` command, or a `which` that finds *all*
183
+ # matching executables on the supplied path.
184
+ # @return [Enumerable<String>] - the executables found, lazily.
185
+ def find_executables
186
+ return enum_for(:find_executables) unless block_given?
187
+
188
+ exts = ENV.has_key?('PATHEXT') ? ENV.fetch('PATHEXT').split(';') : ['']
189
+ paths = @path.sub('*', ENV['PATH']).split(File::PATH_SEPARATOR)
190
+ cmds = strict? ? @executables.first(1) : @executables
191
+
192
+ cmds.product(paths, exts).map do |cmd, path, ext|
193
+ exe = File.join(path, "#{cmd}#{ext}")
194
+ yield exe if File.executable?(exe)
195
+ end
196
+ end
197
+ end
198
+ end
@@ -5,9 +5,16 @@ module Cliver
5
5
  # Default implementation of the detector needed by Cliver::Assertion,
6
6
  # which will take anything that #respond_to?(:to_proc)
7
7
  class Detector < Struct.new(:command_arg, :version_pattern)
8
+ # @param detector_argument [#call, Object]
9
+ # If detector_argument responds to #call, return it; otherwise attempt
10
+ # to create an instance of self.
11
+ def self.generate(detector_argument)
12
+ return detector_argument if detector_argument.respond_to?(:call)
13
+ new(*Array(detector_argument))
14
+ end
8
15
 
9
16
  # Default pattern to use when searching {#version_command} output
10
- DEFAULT_VERSION_PATTERN = /version [0-9][.0-9a-z]+/i.freeze
17
+ DEFAULT_VERSION_PATTERN = /(version ?)?[0-9][.0-9a-z]+/i.freeze
11
18
 
12
19
  # Default command argument to use against the executable to get
13
20
  # version output
@@ -15,22 +22,25 @@ module Cliver
15
22
 
16
23
  # Forgiving input, allows either argument if only one supplied.
17
24
  #
18
- # @overload initialize(command_arg)
25
+ # @overload initialize(*command_args)
26
+ # @param command_args [Array<String>]
19
27
  # @overload initialize(version_pattern)
20
- # @overload initialize(command_arg, version_pattern)
21
- # @param command_arg [String]
22
- # @param version_pattern [Regexp]
28
+ # @param version_pattern [Regexp]
29
+ # @overload initialize(*command_args, version_pattern)
30
+ # @param command_args [Array<String>]
31
+ # @param version_pattern [Regexp]
23
32
  def initialize(*args)
24
- command_arg = args.shift if args.first.kind_of?(String)
25
- version_pattern = args.shift
26
- super(command_arg, version_pattern)
33
+ version_pattern = args.pop if args.last.kind_of?(Regexp)
34
+ command_args = args unless args.empty?
35
+
36
+ super(command_args, version_pattern)
27
37
  end
28
38
 
29
39
  # @param executable_path [String] - the path to the executable to test
30
40
  # @return [String] - should be contain {Gem::Version}-parsable
31
41
  # version number.
32
42
  def detect_version(executable_path)
33
- output = `#{version_command(executable_path).shelljoin} 2>&1`
43
+ output = shell_out_and_capture version_command(executable_path).shelljoin
34
44
  output[version_pattern]
35
45
  end
36
46
 
@@ -53,7 +63,7 @@ module Cliver
53
63
 
54
64
  # The argument to pass to the executable to get current version
55
65
  # Defaults to {DEFAULT_COMMAND_ARG}
56
- # @return [String]
66
+ # @return [String, Array<String>]
57
67
  def command_arg
58
68
  super || DEFAULT_COMMAND_ARG
59
69
  end
@@ -61,7 +71,15 @@ module Cliver
61
71
  # @param executable_path [String] the executable to test
62
72
  # @return [Array<String>]
63
73
  def version_command(executable_path)
64
- [executable_path, command_arg]
74
+ [executable_path, *Array(command_arg)]
75
+ end
76
+
77
+ private
78
+
79
+ # @api private
80
+ # A boundary that is useful for testing.
81
+ def shell_out_and_capture(command)
82
+ `#{command} 2>&1`
65
83
  end
66
84
  end
67
85
  end
@@ -6,13 +6,23 @@ module Cliver
6
6
  # The identity filter returns its input unchanged.
7
7
  IDENTITY = proc { |version| version }
8
8
 
9
+ # Apply to a list of requirements
10
+ # @param requirements [Array<String>]
11
+ # @return [Array<String>]
9
12
  def requirements(requirements)
10
13
  requirements.map do |requirement|
11
14
  req_parts = requirement.split(/\b(?=\d)/, 2)
12
15
  version = req_parts.last
13
- version.replace call(version)
16
+ version.replace apply(version)
14
17
  req_parts.join
15
18
  end
16
19
  end
20
+
21
+ # Apply to some input
22
+ # @param version [String]
23
+ # @return [String]
24
+ def apply(version)
25
+ to_proc.call(version)
26
+ end
17
27
  end
18
28
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Cliver
4
4
  # Cliver follows {http://semver.org SemVer}
5
- VERSION = '0.1.5'
5
+ VERSION = '0.2.0'
6
6
  end
@@ -21,7 +21,7 @@ describe Cliver::Detector do
21
21
  let(:version_arg) { '--release-version' }
22
22
  let(:args) { [version_arg] }
23
23
 
24
- its(:command_arg) { should eq version_arg }
24
+ its(:command_arg) { should eq [version_arg] }
25
25
  its(:version_pattern) { should eq defaults[:version_pattern] }
26
26
  end
27
27
 
@@ -38,7 +38,7 @@ describe Cliver::Detector do
38
38
  let(:regexp_arg) { /.*/ }
39
39
  let(:args) { [version_arg, regexp_arg] }
40
40
 
41
- its(:command_arg) { should eq version_arg }
41
+ its(:command_arg) { should eq [version_arg] }
42
42
  its(:version_pattern) { should eq regexp_arg }
43
43
  end
44
44
  end
@@ -1,31 +1,203 @@
1
1
  # encoding: utf-8
2
2
  require 'cliver'
3
+ require 'spec_helper'
3
4
 
4
5
  describe Cliver do
5
- it { should respond_to :assert }
6
-
7
- it { should respond_to :dependency_unmet? }
8
- context '#dependency_unmet?' do
9
- let(:requirements) { [] }
10
- let(:detector) { proc { } }
11
- subject { Cliver.dependency_unmet?(executable, *requirements, &detector) }
12
- context 'when dependency is met' do
13
- let(:executable) { 'ruby' }
6
+ # The setup. Your test will likeley interact with subject.
7
+ let(:action) { Cliver.public_send(method, *args, &block) }
8
+ subject { action }
9
+
10
+ # These can get overridden in context blocks
11
+ let(:method) { raise ArgumentError, 'spec didn\'t specify :method' }
12
+ let(:args) { raise ArgumentError, 'spec didn\'t specify :args' }
13
+ let(:block) { version_map.method(:fetch) }
14
+
15
+ 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
20
+ end
21
+
22
+ let(:options) do
23
+ {
24
+ :path => path,
25
+ :executable => executable,
26
+ }
27
+ end
28
+ let(:executables) { version_map.keys }
29
+ let(:args) do
30
+ args = [Array(executable)]
31
+ args.concat Array(requirement)
32
+ args << options
33
+ end
34
+
35
+ let(:path) { 'foo/bar:baz/bingo' }
36
+ let(:executable) { 'doodle' }
37
+ let(:requirement) { '~>1.1'}
38
+
39
+ context 'when first-found version is sufficient' do
40
+
41
+ let(:version_map) do
42
+ {'baz/bingo/doodle' => '1.2.1'}
43
+ end
44
+
45
+ context '::assert' do
46
+ let(:method) { :assert }
47
+ it 'should not raise' do
48
+ expect { action }.to_not raise_exception
49
+ end
50
+ end
51
+
52
+ context '::dependency_unmet?' do
53
+ let(:method) { :dependency_unmet? }
14
54
  it { should be_false }
15
55
  end
16
- context 'when dependency is present, but wrong version' do
17
- let(:executable) { 'ruby' }
18
- let(:requirements) { ['~> 0.1.0'] }
19
- let(:detector) { proc { RUBY_VERSION.sub('p', '.') } }
20
- it { should_not be_false }
21
- it { should match 'Dependency Version Mismatch:' }
22
- it { should match "expected .+ 'ruby' to be #{requirements}" }
23
- end
24
- context 'when dependency is not present' do
25
- let(:executable) { 'ruxxxby' }
26
- it { should_not be_false }
27
- it { should match 'Dependency Not Found:' }
28
- it { should match "'#{executable}' could not be found" }
56
+ context '::detect' do
57
+ let(:method) { :detect }
58
+ it { should eq 'baz/bingo/doodle' }
59
+ end
60
+ context '::detect!' do
61
+ let(:method) { :detect! }
62
+ it 'should not raise' do
63
+ expect { action }.to_not raise_exception
64
+ end
65
+ it { should eq 'baz/bingo/doodle' }
66
+ end
67
+ end
68
+
69
+ context 'when first-found version insufficient' do
70
+ let(:version_map) do
71
+ {'baz/bingo/doodle' => '1.0.1'}
72
+ end
73
+ context '::assert' do
74
+ let(:method) { :assert }
75
+ it 'should raise' do
76
+ expect { action }.to raise_exception Cliver::Dependency::VersionMismatch
77
+ end
78
+ end
79
+ context '::dependency_unmet?' do
80
+ let(:method) { :dependency_unmet? }
81
+ it { should be_true }
82
+ end
83
+ context '::detect' do
84
+ let(:method) { :detect }
85
+ it { should be_nil }
86
+ end
87
+ context '::detect!' do
88
+ let(:method) { :detect! }
89
+ it 'should not raise' do
90
+ expect { action }.to raise_exception Cliver::Dependency::VersionMismatch
91
+ end
92
+ end
93
+
94
+ context 'and when sufficient version found later on path' do
95
+ let(:version_map) do
96
+ {
97
+ 'foo/bar/doodle' => '0.0.1',
98
+ 'baz/bingo/doodle' => '1.1.0',
99
+ }
100
+ end
101
+ context '::assert' do
102
+ let(:method) { :assert }
103
+ it 'should raise' do
104
+ expect { action }.to raise_exception Cliver::Dependency::VersionMismatch
105
+ end
106
+ end
107
+ context '::dependency_unmet?' do
108
+ let(:method) { :dependency_unmet? }
109
+ it { should be_true }
110
+ end
111
+ context '::detect' do
112
+ let(:method) { :detect }
113
+ it { should eq 'baz/bingo/doodle'}
114
+ end
115
+ context '::detect!' do
116
+ let(:method) { :detect! }
117
+ it 'should not raise' do
118
+ expect { action }.to_not raise_exception
119
+ end
120
+ it { should eq 'baz/bingo/doodle' }
121
+ end
122
+ end
123
+ end
124
+
125
+ context 'when no found version' do
126
+ let(:version_map) { {} }
127
+
128
+ context '::assert' do
129
+ let(:method) { :assert }
130
+ it 'should raise' do
131
+ expect { action }.to raise_exception Cliver::Dependency::NotFound
132
+ end
133
+ end
134
+ context '::dependency_unmet?' do
135
+ let(:method) { :dependency_unmet? }
136
+ it { should be_true }
137
+ end
138
+ context '::detect' do
139
+ let(:method) { :detect }
140
+ it { should be_nil }
141
+ end
142
+ context '::detect!' do
143
+ let(:method) { :detect! }
144
+ it 'should not raise' do
145
+ expect { action }.to raise_exception Cliver::Dependency::NotFound
146
+ end
147
+ end
148
+ end
149
+
150
+ context 'with fallback executable names' do
151
+ let(:executable) { ['primary', 'fallback'] }
152
+ let(:requirement) { '~> 1.1' }
153
+ context 'when primary exists after secondary in path' do
154
+ context 'and primary sufficient' do
155
+ let(:version_map) do
156
+ {
157
+ 'baz/bingo/primary' => '1.1',
158
+ 'foo/bar/fallback' => '1.1'
159
+ }
160
+ end
161
+ context '::detect' do
162
+ let(:method) { :detect }
163
+ it { should eq 'baz/bingo/primary' }
164
+ end
165
+ end
166
+ context 'and primary insufficient' do
167
+ let(:version_map) do
168
+ {
169
+ 'baz/bingo/primary' => '2.1',
170
+ 'foo/bar/fallback' => '1.1'
171
+ }
172
+ end
173
+ context 'the secondary' do
174
+ context '::detect' do
175
+ let(:method) { :detect }
176
+ it { should eq 'foo/bar/fallback' }
177
+ end
178
+ end
179
+ end
180
+ end
181
+ context 'when primary does not exist in path' do
182
+ context 'and sufficient secondary does' do
183
+ let(:version_map) do
184
+ {
185
+ 'foo/bar/fallback' => '1.1'
186
+ }
187
+ end
188
+ context '::detect' do
189
+ let(:method) { :detect }
190
+ it { should eq 'foo/bar/fallback' }
191
+ end
192
+ end
193
+ end
194
+
195
+ context 'neither found' do
196
+ context '::detect' do
197
+ let(:version_map) { {} }
198
+ let(:method) { :detect }
199
+ it { should be_nil }
200
+ end
29
201
  end
30
202
  end
31
203
  end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ # 1.8.x doesn't support public_send and we use it in spec,
4
+ # so we emulate it in this monkeypatch.
5
+ class Object
6
+ def public_send(method, *args, &block)
7
+ case method.to_s
8
+ when *private_methods
9
+ raise NoMethodError, "private method `#{method}' called for #{self}"
10
+ when *protected_methods
11
+ raise NoMethodError, "protected method `#{method}' called for #{self}"
12
+ else
13
+ send(method, *args, &block)
14
+ end
15
+ end unless method_defined?(:public_send)
16
+ 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.1.5
4
+ version: 0.2.0
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-06-23 00:00:00.000000000 Z
12
+ date: 2013-07-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -124,16 +124,13 @@ files:
124
124
  - Rakefile
125
125
  - cliver.gemspec
126
126
  - lib/cliver.rb
127
- - lib/cliver/assertion.rb
127
+ - lib/cliver/dependency.rb
128
128
  - lib/cliver/detector.rb
129
129
  - lib/cliver/filter.rb
130
130
  - lib/cliver/version.rb
131
- - lib/cliver/which.rb
132
- - lib/cliver/which/posix.rb
133
- - lib/cliver/which/windows.rb
134
- - spec/cliver/assertion_spec.rb
135
131
  - spec/cliver/detector_spec.rb
136
132
  - spec/cliver_spec.rb
133
+ - spec/spec_helper.rb
137
134
  homepage: https://www.github.com/yaauie/cliver
138
135
  licenses:
139
136
  - MIT
@@ -160,7 +157,7 @@ signing_key:
160
157
  specification_version: 3
161
158
  summary: Cross-platform version constraints for cli tools
162
159
  test_files:
163
- - spec/cliver/assertion_spec.rb
164
160
  - spec/cliver/detector_spec.rb
165
161
  - spec/cliver_spec.rb
162
+ - spec/spec_helper.rb
166
163
  has_rdoc: yard
@@ -1,89 +0,0 @@
1
- # encoding: utf-8
2
- require 'open3'
3
- require 'rubygems/requirement'
4
-
5
- module Cliver
6
- # The core of Cliver, Assertion is responsible for detecting the
7
- # installed version of a binary and determining if it meets the requirements
8
- class Assertion
9
-
10
- include Which # platform-specific implementation of `which`
11
-
12
- # An exception class raised when assertion is not met
13
- DependencyNotMet = Class.new(ArgumentError)
14
-
15
- # An exception that is raised when executable present is the wrong version
16
- DependencyVersionMismatch = Class.new(DependencyNotMet)
17
-
18
- # An exception that is raised when executable is not present
19
- DependencyNotFound = Class.new(DependencyNotMet)
20
-
21
- # A pattern for extracting a {Gem::Version}-parsable version
22
- PARSABLE_GEM_VERSION = /[0-9]+(.[0-9]+){0,4}(.[a-zA-Z0-9]+)?/.freeze
23
-
24
- # @overload initialize(executable, *requirements, options = {})
25
- # @param executable [String]
26
- # @param requirements [Array<String>, String] splat of strings
27
- # whose elements follow the pattern
28
- # [<operator>] <version>
29
- # Where <operator> is optional (default '='') and in the set
30
- # '=', '!=', '>', '<', '>=', '<=', or '~>'
31
- # And <version> is dot-separated integers with optional
32
- # alphanumeric pre-release suffix. See also
33
- # {http://docs.rubygems.org/read/chapter/16 Specifying Versions}
34
- # @param options [Hash<Symbol,Object>]
35
- # @option options [Cliver::Detector, #to_proc] :detector (Detector.new)
36
- # @option options [#to_proc] :filter ({Cliver::Filter::IDENTITY})
37
- # @yieldparam [String] full path to executable
38
- # @yieldreturn [String] containing a {Gem::Version}-parsable substring
39
- def initialize(executable, *args, &detector)
40
- options = args.last.kind_of?(Hash) ? args.pop : {}
41
- @detector = detector || options.fetch(:detector) { Detector.new }
42
- @filter = options.fetch(:filter, Filter::IDENTITY).extend(Filter)
43
-
44
- @executable = executable.dup.freeze
45
-
46
- unless args.empty?
47
- @requirement = Gem::Requirement.new(@filter.requirements(args))
48
- end
49
- end
50
-
51
- # @raise [DependencyVersionMismatch] if installed version does not match
52
- # @raise [DependencyNotFound] if no installed version on your path
53
- def assert!
54
- version = installed_version ||
55
- raise(DependencyNotFound,
56
- "required command-line executable '#{@executable}' " +
57
- 'could not be found on your PATH.')
58
-
59
- if @requirement && !@requirement.satisfied_by?(Gem::Version.new(version))
60
- raise DependencyVersionMismatch,
61
- "expected command-line executable '#{@executable}' to " +
62
- "be #{@requirement}, got #{version}"
63
- end
64
- end
65
-
66
- # Finds the executable on your path using {Cliver::Which};
67
- # if the executable is present and version requirements are specified,
68
- # uses the specified detector to get the current version.
69
- # @private
70
- # @return [nil] if no version present
71
- # @return [String] Gem::Version-parsable string version
72
- # @return [true] if present and no requirements (optimization)
73
- def installed_version
74
- executable_path = which(@executable)
75
- return nil unless executable_path
76
- return true unless @requirement
77
-
78
- version_string = @detector.to_proc.call(executable_path)
79
- version_string &&= @filter.to_proc.call(version_string)
80
- (version_string && version_string[PARSABLE_GEM_VERSION]).tap do |version|
81
- unless version
82
- raise ArgumentError,
83
- "found command-line executable #{@executable} at " +
84
- "'#{executable_path}' but could not detect its version."
85
- end
86
- end
87
- end
88
- end
89
- end
@@ -1,17 +0,0 @@
1
- # encoding: utf-8
2
-
3
- module Cliver
4
- # The `which` command we love on many posix-systems needs analogues on other
5
- # systems. The Which module congitionally includes the correct implementation
6
- # into itself, so you can include it into something else.
7
- module Which
8
- case RbConfig::CONFIG['host_os']
9
- when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
10
- require 'cliver/which/windows'
11
- include Cliver::Which::Windows
12
- else
13
- require 'cliver/which/posix'
14
- include Cliver::Which::Posix
15
- end
16
- end
17
- end
@@ -1,22 +0,0 @@
1
- # encoding: utf-8
2
- require 'shellwords'
3
-
4
- module Cliver
5
- module Which
6
- # Posix implementation of Which
7
- # Required and mixed into Cliver::Which in posix environments
8
- module Posix
9
- # Posix adapter to `which`
10
- # @param executable [String]
11
- # @return [nil,String] - path to found executable
12
- def which(executable)
13
- which = `which #{Shellwords.escape executable} 2>&1`
14
- executable_path = which.chomp
15
- return nil if executable_path.empty?
16
- executable_path
17
- rescue Errno::ENOENT
18
- raise '"which" must be on your path to use Cliver on this system.'
19
- end
20
- end
21
- end
22
- end
@@ -1,25 +0,0 @@
1
- # encoding: utf-8
2
- require 'shellwords'
3
-
4
- module Cliver
5
- module Which
6
- # Windows-specific implementation of Which
7
- # Required and mixed into Cliver::Which in windows environments
8
- module Windows
9
- # Windows-specific implementation of `which`
10
- # @param executable [String]
11
- # @return [nil,String] - path to found executable
12
- def which(executable)
13
- # `where` returns newline-separated files found on path, but doesn't
14
- # ensure that they are executable as commands.
15
- where = `where #{Shellwords.escape executable} 2>&1`
16
- where.lines.map(&:chomp).find do |found|
17
- next if found.empty?
18
- File.executable?(found)
19
- end
20
- rescue Errno::ENOENT
21
- raise '"where" must be on your path to use Cliver on Windows.'
22
- end
23
- end
24
- end
25
- end
@@ -1,156 +0,0 @@
1
- # encoding: utf-8
2
- require 'cliver'
3
-
4
- describe Cliver::Assertion do
5
- let(:mismatch_exception) { Cliver::Assertion::DependencyVersionMismatch }
6
- let(:missing_exception) { Cliver::Assertion::DependencyNotFound }
7
- let(:requirements) { ['6.8'] }
8
- let(:executable) { 'fubar' }
9
- let(:detector) { nil }
10
- let(:assertion) do
11
- Cliver::Assertion.new(executable, *requirements, &detector)
12
- end
13
-
14
- context 'when dependency found' do
15
- before(:each) { assertion.stub(:installed_version) { version } }
16
-
17
- # sampling of requirements; actual implementation
18
- # is supplied by rubygems/requirement and well-tested there.
19
- context '~>' do
20
- let(:requirements) { ['~> 6.8'] }
21
- context 'when version matches exactly' do
22
- let(:version) { '6.8' }
23
- it 'should not raise' do
24
- expect { assertion.assert! }.to_not raise_exception
25
- end
26
- end
27
- context 'when major matches, and minor too low' do
28
- let(:version) { '6.7' }
29
- it 'should raise' do
30
- expect { assertion.assert! }.to raise_exception mismatch_exception
31
- end
32
- end
33
- context 'when major matches, and minor bumped' do
34
- let(:version) { '6.13' }
35
- it 'should not raise' do
36
- expect { assertion.assert! }.to_not raise_exception
37
- end
38
- end
39
- context 'when major too high' do
40
- let(:version) { '7.0' }
41
- it 'should raise' do
42
- expect { assertion.assert! }.to raise_exception mismatch_exception
43
- end
44
- end
45
- context 'patch version present' do
46
- let(:version) { '6.8.1' }
47
- it 'should not raise' do
48
- expect { assertion.assert! }.to_not raise_exception
49
- end
50
- end
51
- context 'pre-release of version that matches' do
52
- let(:version) { '6.8.a' }
53
- it 'should raise' do
54
- expect { assertion.assert! }.to raise_exception mismatch_exception
55
- end
56
- end
57
- end
58
-
59
- context 'multi [>=,<]' do
60
- let(:requirements) { ['>= 1.1.4', '< 3.1'] }
61
- context 'matches both' do
62
- let(:version) { '2.0' }
63
- it 'should not raise' do
64
- expect { assertion.assert! }.to_not raise_exception
65
- end
66
- end
67
- context 'fails one' do
68
- let(:version) { '3.1' }
69
- it 'should raise' do
70
- expect { assertion.assert! }.to raise_exception mismatch_exception
71
- end
72
- end
73
- end
74
-
75
- context 'none' do
76
- let(:requirements) { [] }
77
- let(:version) { '3.1' }
78
- it 'should not raise' do
79
- expect { assertion.assert! }.to_not raise_exception
80
- end
81
- end
82
- end
83
-
84
- context 'when dependency not found' do
85
- before(:each) { assertion.stub(:installed_version) { nil } }
86
-
87
- it 'should raise' do
88
- expect { assertion.assert! }.to raise_exception missing_exception
89
- end
90
- end
91
-
92
- context '#installed_version' do
93
- before(:each) do
94
- if `which #{executable}`.chomp.empty?
95
- pending "#{executable} not installed, test will flap."
96
- end
97
- end
98
- let(:detector_touches) { [] }
99
- context 'ruby using filter' do
100
- let(:requirement) { '~> 1.2.3p112' }
101
- let(:executable) { 'ruby' }
102
- let(:filter) { proc { |ver| ver.tr('p', '.') } }
103
- let(:detector) { proc { '1.2.3p456' } }
104
- let(:assertion) do
105
- Cliver::Assertion.new(executable, requirement, :filter => filter,
106
- :detector => detector)
107
- end
108
- let(:installed_version) { assertion.installed_version }
109
- subject { installed_version }
110
-
111
- it { should eq '1.2.3.456' }
112
- end
113
- context 'ruby with detector-block returned value' do
114
- let(:requirements) { ['~> 10.1.4'] }
115
- let(:fake_version) { 'ruby 10.1.5' }
116
- let(:executable) { 'ruby' }
117
- let(:detector) do
118
- proc do |ruby|
119
- detector_touches << true
120
- fake_version
121
- end
122
- end
123
- it 'should succeed' do
124
- expect { assertion.assert! }.to_not raise_exception
125
- end
126
- context 'the detector' do
127
- before(:each) { assertion.assert! }
128
- it 'should have been touched' do
129
- detector_touches.should_not be_empty
130
- end
131
- end
132
- context 'when block-return doesn\'t meet requirements' do
133
- let(:fake_version) { '10.1433.32.alpha' }
134
- it 'should raise' do
135
- expect { assertion.assert! }.to raise_exception mismatch_exception
136
- end
137
- end
138
- end
139
- context 'awk with no requirements' do
140
-
141
- let(:requirements) { [] }
142
- let(:executable) { 'awk' }
143
- let(:fake_version) { nil }
144
-
145
- it 'should succeed' do
146
- expect { assertion.assert! }.to_not raise_exception
147
- end
148
- context 'the detector' do
149
- before(:each) { assertion.assert! }
150
- it 'should not have been touched' do
151
- detector_touches.should be_empty
152
- end
153
- end
154
- end
155
- end
156
- end