cliver 0.1.5 → 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.
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