cliver 0.0.1 → 0.1.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/CONTRIBUTING.md ADDED
@@ -0,0 +1,35 @@
1
+ # Contributing
2
+
3
+ `Cliver` is [MIT-liennsed](LICENSE.txt) and intends to follow the
4
+ [pull-request-hack][].
5
+
6
+ ## Git-Flow
7
+
8
+ `Cliver` follows the [git-flow][] branching model, which means that every
9
+ commit on `master` is a release. The default working branch is `develop`, so
10
+ in general please keep feature pull-requests based against the current
11
+ `develop`.
12
+
13
+ - fork cliver
14
+ - use the git-flow model to start your feature or hotfix
15
+ - make some commits (please include specs)
16
+ - submit a pull-request
17
+
18
+ ## Bug Reporting
19
+
20
+ Please include clear steps-to-reproduce. Spec files are especially welcome;
21
+ a failing spec can be contributed as a pull-request against `develop`.
22
+
23
+ ## Ruby Appraiser
24
+
25
+ `Cliver` uses the [ruby-appraiser][] gem via [pre-commit][] hook, which can be
26
+ activated by installing [icefox/git-hooks][] and running `git-hooks --install`.
27
+ Reek and Rubocop are strong guidelines; use them to reduce defects as much as
28
+ you can, but if you believe clarity will be sacrificed they can be bypassed
29
+ with the `--no-verify` flag.
30
+
31
+ [git-flow]: http://nvie.com/posts/a-successful-git-branching-model/
32
+ [pre-commit]: .githooks/pre-commit/ruby-appraiser
33
+ [ruby-appraiser]: https://github.com/simplymeasured/ruby-appraiser
34
+ [icefox/git-hooks]: https://github.com/icefox/git-hooks
35
+ [pull-request-hack]: http://felixge.de/2013/03/11/the-pull-request-hack.html
data/README.md CHANGED
@@ -19,27 +19,45 @@ Cliver.assert('racc', '>= 1.0', '< 1.4.9') # many version requirements
19
19
 
20
20
  If the executable can't be found on your path at all, a
21
21
  `Cliver::Assertion::DependencyNotFound` exception is raised; if the version
22
- reached does not meet the requirements, a `Cliver::Assertion::VersionMismatch`
23
- exception is raised.
22
+ reached does not meet the requirements, a `Cliver::Assertion::DependencyVersionMismatch`
23
+ exception is raised; both inherit from `Cliver::Assertion::DependencyNotMet`
24
24
 
25
25
  ## Advanced Usage:
26
26
 
27
27
  Some programs don't provide nice 'version 1.2.3' strings in their `--version`
28
- output; `Cliver` lets you provide your own matcher, whose first group is the
29
- string version.
28
+ output; `Cliver` lets you provide your own version detector with a pattern.
30
29
 
31
30
  ```ruby
32
- Cliver.assert('python', '~> 1.7', version_matcher: /Python ([0-9.]+)/)
31
+ Cliver.assert('python', '~> 1.7',
32
+ detector: Cliver::Detector.new(/(?<=Python )[0-9][.0-9a-z]+/))
33
33
  ```
34
34
 
35
- Other programs don't provide a standard `--version`; `Cliver` allows you to
36
- provide your own arg:
35
+ Other programs don't provide a standard `--version`; `Cliver::Detector` also
36
+ allows you to provide your own arg to get the version:
37
37
 
38
38
  ```ruby
39
- Cliver.assert('janky', '~> 10.1.alpha', version_arg: '--release-version')
39
+ Cliver.assert('janky', '~> 10.1.alpha',
40
+ detector: Cliver::Detector.new('--release-version'))
40
41
  ```
41
42
 
42
- It obeys all the same rules as `Gem::Requirement`, including pre-release
43
- semantics.
43
+ Alternatively, you can supply your own detector (anything that responds to
44
+ `#to_proc`) in the options hash or as a block, so long as it returns a
45
+ `Gem::Version`-parsable version number; if it returns nil or false when
46
+ version requirements are given, a descriptive `ArgumentError` is raised.
47
+
48
+ ```ruby
49
+ Cliver.assert('oddball', '~> 10.1.alpha') do |oddball_path|
50
+ File.read(File.expand_path('../VERSION', oddball_path)).chomp
51
+ end
52
+ ```
53
+
54
+ Since `Cliver` uses `Gem::Requirement` for version comparrisons, it obeys all
55
+ the same rules including pre-release semantics.
56
+
57
+ ## See Also:
58
+
59
+ - [Contributing](CONTRIBUTING.md)
60
+ - [License](LICENSE.txt)
61
+
44
62
 
45
63
  [rubygems/requirements]: https://github.com/rubygems/rubygems/blob/master/lib/rubygems/requirement.rb
data/lib/cliver.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # encoding: utf-8
2
2
  require 'cliver/version'
3
3
  require 'cliver/assertion'
4
+ require 'cliver/detector'
5
+ require 'cliver/detector/default'
4
6
 
5
7
  module Cliver
6
8
  # See Cliver::Assertion#assert
@@ -6,15 +6,16 @@ module Cliver
6
6
  # The core of Cliver, Assertion is responsible for detecting the
7
7
  # installed version of a binary and determining if it meets the requirements
8
8
  class Assertion
9
- VersionMismatch = Class.new(ArgumentError)
10
- DependencyNotFound = Class.new(ArgumentError)
9
+ DependencyNotMet = Class.new(ArgumentError)
10
+ DependencyVersionMismatch = Class.new(DependencyNotMet)
11
+ DependencyNotFound = Class.new(DependencyNotMet)
11
12
 
12
13
  EXECUTABLE_PATTERN = /\A[a-z][a-zA-Z0-9\-_]*\z/.freeze
13
14
 
14
15
  # Creates a new instance with the args and calls #assert.
15
16
  # @see #assert
16
- def self.assert!(*args)
17
- new(*args).assert!
17
+ def self.assert!(*args, &block)
18
+ new(*args, &block).assert!
18
19
  end
19
20
 
20
21
  # @overload initialize(executable, *requirements, options = {})
@@ -28,36 +29,48 @@ module Cliver
28
29
  # alphanumeric pre-release suffix
29
30
  # @see Gem::Requirement::new
30
31
  # @param options [Hash<Symbol,Object>]
31
- # @options options [#match] :version_matcher
32
- # @options options [String] :version_arg
33
- def initialize(executable, *args)
34
- options = args.last.kind_of?(Hash) ? args.pop : {}
32
+ # @options options [Cliver::Detector, #to_proc] :detector
33
+ # @yieldparam [String] full path to executable
34
+ # @yieldreturn [String] Gem::Version-parsable string version
35
+ def initialize(executable, *args, &detector)
35
36
  raise ArgumentError, 'executable' unless executable[EXECUTABLE_PATTERN]
36
37
 
38
+ options = args.last.kind_of?(Hash) ? args.pop : {}
39
+
37
40
  @executable = executable.dup.freeze
38
- @requirement = Gem::Requirement.new(args)
39
- @version_arg = options.fetch(:version_arg, '--version')
40
- @version_matcher = options.fetch(:version_matcher,
41
- /version ([0-9][.0-9a-z]+)/i)
41
+ @requirement = Gem::Requirement.new(args) unless args.empty?
42
+ @detector = detector || options.fetch(:detector) { Detector.new }
42
43
  end
43
44
 
44
- # @raise [VersionMismatch] if installed version does not match requirement
45
+ # @raise [DependencyVersionMismatch] if installed version does not match
45
46
  # @raise [DependencyNotFound] if no installed version on your path
46
47
  def assert!
47
48
  version = installed_version
48
- version || raise(DependencyNotFound, "#{@executable} missing.")
49
- unless @requirement.satisfied_by?(version)
50
- raise VersionMismatch,
51
- "got #{version}, expected #{@requirement}"
49
+ raise(DependencyNotFound, "#{@executable} missing.") unless version
50
+
51
+ if @requirement && !@requirement.satisfied_by?(Gem::Version.new(version))
52
+ raise DependencyVersionMismatch,
53
+ "expected #{@executable} to be #{@requirement}, got #{version}"
52
54
  end
53
55
  end
54
56
 
55
57
  # @private
58
+ # @return [nil] if no version present
59
+ # @return [String] Gem::Version-parsable string version
60
+ # @return [true] if present and no requirements (optimization)
56
61
  def installed_version
57
- command = "which #{@executable} && #{@executable} #{@version_arg}"
58
- command_out, _ = Open3.capture2e(command)
59
- match = @version_matcher.match(command_out)
60
- match && Gem::Version.new(match[1])
62
+ which, _ = Open3.capture2e("which #{@executable}")
63
+ executable_path = which.chomp
64
+ return nil if executable_path.empty?
65
+ return true unless @requirement
66
+
67
+ @detector.to_proc.call(executable_path).tap do |version|
68
+ unless version
69
+ raise ArgumentError,
70
+ "found #{@executable} at '#{executable_path}' " +
71
+ 'but could not detect its version.'
72
+ end
73
+ end
61
74
  end
62
75
  end
63
76
  end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+ require 'open3'
3
+
4
+ module Cliver
5
+ # The interface for Cliver::Detector classes.
6
+ # @see Cliver::Detector::Default for reference implementation
7
+ module Detector
8
+ # Forward to default implementation
9
+ def self.new(*args, &block)
10
+ Default.new(*args, &block)
11
+ end
12
+
13
+ # @param executable [String] the executable to test
14
+ # @return [Array<String>]
15
+ def version_command(executable)
16
+ raise NotImplementedError unless defined? super
17
+ super
18
+ end
19
+
20
+ # @return [Regexp] - the pattern used against the output
21
+ # of the #version_command, which should
22
+ # typically be Gem::Version-parsable.
23
+ def version_pattern
24
+ raise NotImplementedError unless defined? super
25
+ super
26
+ end
27
+
28
+ # @param executable [String] - the path to the executable to test
29
+ # @return [String] - should be Gem::Version-parsable.
30
+ def detect_version(executable)
31
+ output, _ = Open3.capture2e(*version_command(executable))
32
+ ver = output.scan(version_pattern)
33
+ ver && ver.first
34
+ end
35
+
36
+ # This is the interface that any detector must have.
37
+ # @see #detect_version for the returned proc's method signature.
38
+ # @return [Proc]
39
+ def to_proc
40
+ method(:detect_version).to_proc
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+ require 'open3'
3
+
4
+ module Cliver
5
+ # Default implementation of Cliver::Detector
6
+ # Requires a command argument (default '--version')
7
+ # and a pattern-matcher Regexp with sensible default.
8
+ class Detector::Default < Struct.new(:command_arg, :version_pattern)
9
+ include Detector
10
+
11
+ DEFAULT_VERSION_PATTERN = /(?<=version )[0-9][.0-9a-z]+/i.freeze
12
+ DEFAULT_COMMAND_ARG = '--version'.freeze
13
+
14
+ # Forgiving input, allows either argument if only one supplied.
15
+ #
16
+ # @overload initialize(command_arg)
17
+ # @overload initialize(version_pattern)
18
+ # @overload initialize(command_arg, version_pattern)
19
+ # @param command_arg [String] ('--version')
20
+ # @param version_pattern [Regexp] (/(?<=version )[0-9][.0-9a-z]+/i)
21
+ def initialize(*args)
22
+ command_arg = args.shift if args.first.kind_of?(String)
23
+ version_pattern = args.shift
24
+ super(command_arg, version_pattern)
25
+ end
26
+
27
+ def version_pattern
28
+ super || DEFAULT_VERSION_PATTERN
29
+ end
30
+
31
+ def command_arg
32
+ super || DEFAULT_COMMAND_ARG
33
+ end
34
+
35
+ def version_command(executable)
36
+ [executable, command_arg]
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Cliver
4
- VERSION = '0.0.1'
4
+ VERSION = '0.1.0'
5
5
  end
@@ -2,11 +2,14 @@
2
2
  require 'cliver/assertion'
3
3
 
4
4
  describe Cliver::Assertion do
5
- let(:mismatch_exception) { Cliver::Assertion::VersionMismatch }
5
+ let(:mismatch_exception) { Cliver::Assertion::DependencyVersionMismatch }
6
6
  let(:missing_exception) { Cliver::Assertion::DependencyNotFound }
7
7
  let(:requirements) { ['6.8'] }
8
8
  let(:executable) { 'fubar' }
9
- let(:assertion) { Cliver::Assertion.new(executable, *requirements) }
9
+ let(:detector) { nil }
10
+ let(:assertion) do
11
+ Cliver::Assertion.new(executable, *requirements, &detector)
12
+ end
10
13
 
11
14
  context 'when dependency found' do
12
15
  before(:each) { assertion.stub(:installed_version) { version } }
@@ -16,39 +19,38 @@ describe Cliver::Assertion do
16
19
  context '~>' do
17
20
  let(:requirements) { ['~> 6.8'] }
18
21
  context 'when version matches exactly' do
19
- let(:version) { Gem::Version.new('6.8') }
22
+ let(:version) { '6.8' }
20
23
  it 'should not raise' do
21
24
  expect { assertion.assert! }.to_not raise_exception
22
25
  end
23
26
  end
24
27
  context 'when major matches, and minor too low' do
25
- let(:version) { Gem::Version.new('6.7') }
28
+ let(:version) { '6.7' }
26
29
  it 'should raise' do
27
30
  expect { assertion.assert! }.to raise_exception mismatch_exception
28
31
  end
29
32
  end
30
33
  context 'when major matches, and minor bumped' do
31
- let(:version) { Gem::Version.new('6.13') }
34
+ let(:version) { '6.13' }
32
35
  it 'should not raise' do
33
36
  expect { assertion.assert! }.to_not raise_exception
34
37
  end
35
38
  end
36
39
  context 'when major too high' do
37
- let(:version) { Gem::Version.new('7.0') }
40
+ let(:version) { '7.0' }
38
41
  it 'should raise' do
39
42
  expect { assertion.assert! }.to raise_exception mismatch_exception
40
43
  end
41
44
  end
42
45
  context 'patch version present' do
43
- let(:version) { Gem::Version.new('6.8.1') }
46
+ let(:version) { '6.8.1' }
44
47
  it 'should not raise' do
45
48
  expect { assertion.assert! }.to_not raise_exception
46
49
  end
47
50
  end
48
51
  context 'pre-release of version that matches' do
49
- let(:version) { Gem::Version.new('6.8.a') }
52
+ let(:version) { '6.8.a' }
50
53
  it 'should raise' do
51
- version.should be_prerelease
52
54
  expect { assertion.assert! }.to raise_exception mismatch_exception
53
55
  end
54
56
  end
@@ -57,13 +59,13 @@ describe Cliver::Assertion do
57
59
  context 'multi [>=,<]' do
58
60
  let(:requirements) { ['>= 1.1.4', '< 3.1'] }
59
61
  context 'matches both' do
60
- let(:version) { Gem::Version.new('2.0') }
62
+ let(:version) { '2.0' }
61
63
  it 'should not raise' do
62
64
  expect { assertion.assert! }.to_not raise_exception
63
65
  end
64
66
  end
65
67
  context 'fails one' do
66
- let(:version) { Gem::Version.new('3.1') }
68
+ let(:version) { '3.1' }
67
69
  it 'should raise' do
68
70
  expect { assertion.assert! }.to raise_exception mismatch_exception
69
71
  end
@@ -72,7 +74,7 @@ describe Cliver::Assertion do
72
74
 
73
75
  context 'none' do
74
76
  let(:requirements) { [] }
75
- let(:version) { Gem::Version.new('3.1') }
77
+ let(:version) { '3.1' }
76
78
  it 'should not raise' do
77
79
  expect { assertion.assert! }.to_not raise_exception
78
80
  end
@@ -86,4 +88,55 @@ describe Cliver::Assertion do
86
88
  expect { assertion.assert! }.to raise_exception missing_exception
87
89
  end
88
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 with detector-block returned value' do
100
+ let(:requirements) { ['~> 10.1.4'] }
101
+ let(:fake_version) { '10.1.5' }
102
+ let(:executable) { 'ruby' }
103
+ let(:detector) do
104
+ proc do |ruby|
105
+ detector_touches << true
106
+ fake_version
107
+ end
108
+ end
109
+ it 'should succeed' do
110
+ expect { assertion.assert! }.to_not raise_exception
111
+ end
112
+ context 'the detector' do
113
+ before(:each) { assertion.assert! }
114
+ it 'should have been touched' do
115
+ detector_touches.should_not be_empty
116
+ end
117
+ end
118
+ context 'when block-return doesn\'t meet requirements' do
119
+ let(:fake_version) { '10.1.3' }
120
+ it 'should raise' do
121
+ expect { assertion.assert! }.to raise_exception mismatch_exception
122
+ end
123
+ end
124
+ end
125
+ context 'awk with no requirements' do
126
+
127
+ let(:requirements) { [] }
128
+ let(:executable) { 'awk' }
129
+ let(:fake_version) { nil }
130
+
131
+ it 'should succeed' do
132
+ expect { assertion.assert! }.to_not raise_exception
133
+ end
134
+ context 'the detector' do
135
+ before(:each) { assertion.assert! }
136
+ it 'should not have been touched' do
137
+ detector_touches.should be_empty
138
+ end
139
+ end
140
+ end
141
+ end
89
142
  end
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+ require 'cliver/detector'
3
+
4
+ describe Cliver::Detector do
5
+ let(:detector) { Cliver::Detector::Default.new(*args) }
6
+ let(:defaults) do
7
+ {
8
+ version_pattern: Cliver::Detector::Default::DEFAULT_VERSION_PATTERN,
9
+ command_arg: Cliver::Detector::Default::DEFAULT_COMMAND_ARG,
10
+ }
11
+ end
12
+ let(:args) { [] }
13
+ subject { detector }
14
+
15
+ it { should respond_to :to_proc }
16
+
17
+ its(:command_arg) { should eq defaults[:command_arg] }
18
+ its(:version_pattern) { should eq defaults[:version_pattern] }
19
+
20
+ context 'with one string argument' do
21
+ let(:version_arg) { '--release-version' }
22
+ let(:args) { [version_arg] }
23
+
24
+ its(:command_arg) { should eq version_arg }
25
+ its(:version_pattern) { should eq defaults[:version_pattern] }
26
+ end
27
+
28
+ context 'with one regexp argument' do
29
+ let(:regexp_arg) { /.*/ }
30
+ let(:args) { [regexp_arg] }
31
+
32
+ its(:command_arg) { should eq defaults[:command_arg] }
33
+ its(:version_pattern) { should eq regexp_arg }
34
+ end
35
+
36
+ context 'with both arguments' do
37
+ let(:version_arg) { '--release-version' }
38
+ let(:regexp_arg) { /.*/ }
39
+ let(:args) { [version_arg, regexp_arg] }
40
+
41
+ its(:command_arg) { should eq version_arg }
42
+ its(:version_pattern) { should eq regexp_arg }
43
+ end
44
+ 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.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -100,6 +100,7 @@ extra_rdoc_files: []
100
100
  files:
101
101
  - .githooks/pre-commit/ruby-appraiser
102
102
  - .gitignore
103
+ - CONTRIBUTING.md
103
104
  - Gemfile
104
105
  - LICENSE.txt
105
106
  - README.md
@@ -107,8 +108,11 @@ files:
107
108
  - cliver.gemspec
108
109
  - lib/cliver.rb
109
110
  - lib/cliver/assertion.rb
111
+ - lib/cliver/detector.rb
112
+ - lib/cliver/detector/default.rb
110
113
  - lib/cliver/version.rb
111
114
  - spec/cliver/assertion_spec.rb
115
+ - spec/cliver/detector_spec.rb
112
116
  - spec/cliver_spec.rb
113
117
  homepage: https://www.github.com/yaauie/cliver
114
118
  licenses:
@@ -137,5 +141,6 @@ specification_version: 3
137
141
  summary: Cross-platform version constraints for cli tools
138
142
  test_files:
139
143
  - spec/cliver/assertion_spec.rb
144
+ - spec/cliver/detector_spec.rb
140
145
  - spec/cliver_spec.rb
141
146
  has_rdoc: