cliver 0.0.1 → 0.1.0

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