├── Gemfile ├── lib ├── cliver │ ├── version.rb │ ├── filter.rb │ ├── shell_capture.rb │ ├── detector.rb │ └── dependency.rb ├── core_ext │ └── file.rb └── cliver.rb ├── .travis.yml ├── Rakefile ├── .gitignore ├── .githooks └── pre-commit │ └── ruby-appraiser ├── spec ├── cliver │ ├── dependency_spec.rb │ ├── shell_capture_spec.rb │ └── detector_spec.rb ├── spec_helper.rb ├── support │ └── executable_mock.rb ├── core_ext │ └── file_spec.rb └── cliver_spec.rb ├── LICENSE.txt ├── cliver.gemspec ├── CONTRIBUTING.md └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | source 'https://rubygems.org' 3 | 4 | # Specify your gem's dependencies in cliver.gemspec 5 | gemspec 6 | -------------------------------------------------------------------------------- /lib/cliver/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Cliver 4 | # Cliver follows {http://semver.org SemVer} 5 | VERSION = '0.3.2' 6 | end 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | script: "bundle exec rake spec" 4 | rvm: 5 | - 2.0.0 6 | - 1.9.3 7 | - jruby-19mode 8 | - rbx-19mode 9 | - 1.8.7 10 | - jruby-18mode 11 | - rbx-18mode 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'bundler/gem_tasks' 3 | 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) do |spec| 6 | spec.pattern = FileList['spec/**/*_spec.rb'] 7 | spec.verbose = true 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.githooks/pre-commit/ruby-appraiser: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo -e "\033[0;36mRuby Appraiser: running\033[0m" 3 | bundle exec ruby-appraiser --mode=staged reek rubocop 4 | result_code=$? 5 | if [ $result_code -gt "0" ]; then 6 | echo -en "\033[0;31m" # RED 7 | echo "[✘] Ruby Appraiser found newly-created defects and " 8 | echo " has blocked your commit." 9 | echo " Fix the defects and commit again." 10 | echo " To bypass, commit again with --no-verify." 11 | echo -en "\033[0m" # RESET 12 | exit $result_code 13 | else 14 | echo -en "\033[0;32m" # GREEN 15 | echo "[✔] Ruby Appraiser ok" 16 | echo -en "\033[0m" #RESET 17 | fi 18 | -------------------------------------------------------------------------------- /lib/cliver/filter.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Cliver 4 | # A Namespace to hold filter procs 5 | module Filter 6 | # The identity filter returns its input unchanged. 7 | IDENTITY = proc { |version| version } 8 | 9 | # Apply to a list of requirements 10 | # @param requirements [Array] 11 | # @return [Array] 12 | def requirements(requirements) 13 | requirements.map do |requirement| 14 | req_parts = requirement.split(/\b(?=\d)/, 2) 15 | version = req_parts.last 16 | version.replace apply(version) 17 | req_parts.join 18 | end 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 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/cliver/dependency_spec.rb: -------------------------------------------------------------------------------- 1 | require 'cliver' 2 | require 'spec_helper' 3 | require 'fileutils' 4 | require 'tmpdir' 5 | 6 | describe Cliver::Dependency do 7 | let(:executable) { 'foo' } 8 | 9 | before do 10 | @paths = [] 11 | 12 | path = Dir.mktmpdir 'cliver' 13 | FileUtils.mkdir path + '/foo' 14 | @paths << path 15 | 16 | path = Dir.mktmpdir 'cliver' 17 | FileUtils.touch path + '/foo' 18 | FileUtils.chmod 0755, path + '/foo' 19 | @paths << path 20 | 21 | @expect = path + '/foo' 22 | 23 | @path = @paths.join File::PATH_SEPARATOR 24 | end 25 | 26 | after do 27 | @paths.each do |tmpdir| 28 | FileUtils.remove_entry_secure tmpdir 29 | end 30 | end 31 | 32 | it 'should not detect directory' do 33 | expect(Cliver::Dependency.new(executable, :path => @path).detect!).to eq @expect 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rspec/its' 4 | 5 | # Snooze button on rspec `should` syntax deprecations until 2016-07-01 6 | unless Time.now > Time.at(1435708800) 7 | RSpec.configure do |config| 8 | config.expect_with :rspec do |c| 9 | c.syntax = [:should, :expect] 10 | end 11 | config.mock_with :rspec do |c| 12 | c.syntax = [:should, :expect] 13 | end 14 | end 15 | end 16 | 17 | # 1.8.x doesn't support public_send and we use it in spec, 18 | # so we emulate it in this monkeypatch. 19 | class Object 20 | def public_send(method, *args, &block) 21 | case method.to_s 22 | when *private_methods 23 | raise NoMethodError, "private method `#{method}' called for #{self}" 24 | when *protected_methods 25 | raise NoMethodError, "protected method `#{method}' called for #{self}" 26 | else 27 | send(method, *args, &block) 28 | end 29 | end unless method_defined?(:public_send) 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ryan Biesemeyer 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/support/executable_mock.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class ExecutableMock 4 | def initialize(path) 5 | @path = _to_platform_abs_path(path) 6 | end 7 | attr_reader :path 8 | attr_reader :version 9 | 10 | def == (other_path) 11 | other_path = _to_platform_abs_path(other_path) 12 | if other_path[/[A-Z]:/i] # windows 13 | pattern = /\A#{self.path}(#{(ENV['PATHEXT']||'').split(';').map(&Regexp::method(:escape)).join('|')})?\Z/i 14 | pattern =~ other_path 15 | else # posix 16 | self.path == other_path 17 | end 18 | end 19 | 20 | private 21 | 22 | def _to_platform_abs_path(source) 23 | (File::absolute_path?(source, :windows) && !File::absolute_path?(source, :posix)) ? 24 | source.tr('\\','/') : 25 | File.expand_path(source) 26 | end 27 | 28 | class Registry 29 | def initialize(version_map) 30 | @registry = {} 31 | version_map.each do |path,version| 32 | @registry[ExecutableMock.new(path)] = version 33 | end 34 | end 35 | 36 | def executable?(path) 37 | false | version(path) 38 | end 39 | 40 | def version(path) 41 | key = @registry.keys.find {|exe| exe == path } 42 | key && @registry[key] 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /cliver.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'cliver/version' 5 | 6 | Gem::Specification.new do |spec| 7 | RUBY_18 = RUBY_VERSION[/\A1\.8\..*/] 8 | spec.name = 'cliver' 9 | spec.version = Cliver::VERSION 10 | spec.authors = ['Ryan Biesemeyer'] 11 | spec.email = ['ryan@yaauie.com'] 12 | spec.description = 'Assertions for command-line dependencies' 13 | spec.summary = 'Cross-platform version constraints for cli tools' 14 | spec.homepage = 'https://github.com/yaauie/cliver' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files`.split($RS) 18 | spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) } 19 | spec.test_files = spec.files.grep(/^(test|spec|features)\//) 20 | spec.require_paths = ['lib'] 21 | spec.has_rdoc = 'yard' 22 | 23 | spec.add_development_dependency 'bundler', '~> 1.3' 24 | spec.add_development_dependency 'rake' 25 | spec.add_development_dependency 'rspec', '~> 3.0' 26 | spec.add_development_dependency 'rspec-its', '~>1.2' 27 | spec.add_development_dependency 'ruby-appraiser-reek' unless RUBY_18 28 | spec.add_development_dependency 'ruby-appraiser-rubocop' unless RUBY_18 29 | spec.add_development_dependency 'yard' 30 | end 31 | -------------------------------------------------------------------------------- /lib/cliver/shell_capture.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module Cliver 4 | class ShellCapture 5 | attr_reader :stdout, :stderr, :command_found 6 | 7 | # @overload initialize(command) 8 | # @param command [Array] the command to run; components in the 9 | # given array will be passed through to Open3::popen3 10 | # @overlaod initialize(command) 11 | # @param command [String] the command to run; string will be shellsplit 12 | # and the resulting components will be passed through Open3::popen3 13 | # @return [void] 14 | def initialize(command) 15 | command = command.shellsplit unless command.kind_of?(Array) 16 | @stdout = @stderr = '' 17 | begin 18 | Open3.popen3(*command) do |i, o, e| 19 | @stdout = o.read.chomp 20 | @stderr = e.read.chomp 21 | end 22 | # Fix for ruby 1.8.7 (and probably earlier): 23 | # Open3.popen3 does not raise anything there, but the error goes to STDERR. 24 | if @stderr =~ /open3.rb:\d+:in `exec': No such file or directory -.*\(Errno::ENOENT\)/ or 25 | @stderr =~ /An exception occurred in a forked block\W+No such file or directory.*\(Errno::ENOENT\)/ 26 | @stderr = '' 27 | @command_found = false 28 | else 29 | @command_found = true 30 | end 31 | rescue Errno::ENOENT, IOError 32 | @command_found = false 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/core_ext/file.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # Core-Extensions on File 4 | class File 5 | # determine whether a String path is absolute. 6 | # @example 7 | # File.absolute_path?('foo') #=> false 8 | # File.absolute_path?('/foo') #=> true 9 | # File.absolute_path?('foo/bar') #=> false 10 | # File.absolute_path?('/foo/bar') #=> true 11 | # File.absolute_path?('C:foo/bar') #=> false 12 | # File.absolute_path?('C:/foo/bar') #=> true 13 | # @param path [String] - a pathname 14 | # @return [Boolean] 15 | def self.absolute_path?(path, platform = :default) 16 | pattern = case platform 17 | when :default then ABSOLUTE_PATH_PATTERN 18 | when :windows then WINDOWS_ABSOLUTE_PATH_PATTERN 19 | when :posix then POSIX_ABSOLUTE_PATH_PATTERN 20 | else raise ArgumentError, "Unsupported platform '#{platform.inspect}'" 21 | end 22 | 23 | false | path[pattern] 24 | end 25 | 26 | unless defined?(POSIX_ABSOLUTE_PATH_PATTERN) 27 | POSIX_ABSOLUTE_PATH_PATTERN = /\A\//.freeze 28 | end 29 | 30 | unless defined?(WINDOWS_ABSOLUTE_PATH_PATTERN) 31 | WINDOWS_ABSOLUTE_PATH_PATTERN = Regexp.union( 32 | POSIX_ABSOLUTE_PATH_PATTERN, 33 | /\A([A-Z]:)?(\\|\/)/i 34 | ).freeze 35 | end 36 | 37 | ABSOLUTE_PATH_PATTERN = begin 38 | File::ALT_SEPARATOR ? 39 | WINDOWS_ABSOLUTE_PATH_PATTERN : 40 | POSIX_ABSOLUTE_PATH_PATTERN 41 | end unless defined?(ABSOLUTE_PATH_PATTERN) 42 | end 43 | -------------------------------------------------------------------------------- /spec/cliver/shell_capture_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'cliver' 3 | 4 | describe Cliver::ShellCapture do 5 | let(:test_command) { 'test command' } 6 | subject { Cliver::ShellCapture.new(test_command) } 7 | 8 | context 'a command that exists' do 9 | let(:intended_stdout) { StringIO.new('1.1.1').tap(&:rewind) } 10 | let(:intended_stderr) { StringIO.new('foo baar 1').tap(&:rewind) } 11 | let(:intended_stdin) { StringIO.new('').tap(&:rewind) } 12 | 13 | ['test command', %w(test command)].each do |input| 14 | context "with #{input.class.name} input" do 15 | let(:test_command) { input } 16 | 17 | before(:each) do 18 | Open3.should_receive(:popen3) do |*args| 19 | args.should eq %w(test command) 20 | end.and_yield(intended_stdin, intended_stdout, intended_stderr) 21 | end 22 | 23 | its(:stdout) { should eq '1.1.1' } 24 | its(:stderr) { should eq 'foo baar 1' } 25 | its(:command_found) { should be true } 26 | end 27 | end 28 | end 29 | 30 | context 'looking for a command that does not exist' do 31 | before(:each) do 32 | Open3.should_receive(:popen3) do |*command| 33 | command.should eq test_command.shellsplit 34 | raise Errno::ENOENT.new("No such file or directory - #{command.first}") 35 | end 36 | end 37 | its(:stdout) { should eq '' } 38 | its(:stderr) { should eq '' } 39 | its(:command_found) { should be false } 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 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 | If you're submitting a bug because cliver is either reporting the wrong version 24 | for an executable or is unable to deduce the version, please supply both stdout 25 | and stderr (separately) from its `--version` (or similar) command. 26 | 27 | ## Ruby Appraiser 28 | 29 | `Cliver` uses the [ruby-appraiser][] gem via [pre-commit][] hook, which can be 30 | activated by installing [icefox/git-hooks][] and running `git-hooks --install`. 31 | Reek and Rubocop are strong guidelines; use them to reduce defects as much as 32 | you can, but if you believe clarity will be sacrificed they can be bypassed 33 | with the `--no-verify` flag. 34 | 35 | [git-flow]: http://nvie.com/posts/a-successful-git-branching-model/ 36 | [pre-commit]: .githooks/pre-commit/ruby-appraiser 37 | [ruby-appraiser]: https://github.com/simplymeasured/ruby-appraiser 38 | [icefox/git-hooks]: https://github.com/icefox/git-hooks 39 | [pull-request-hack]: http://felixge.de/2013/03/11/the-pull-request-hack.html 40 | -------------------------------------------------------------------------------- /spec/core_ext/file_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'core_ext/file' 3 | 4 | describe 'File::absolute_path?' do 5 | context 'posix' do 6 | before(:each) do 7 | stub_const("File::ALT_SEPARATOR", nil) 8 | stub_const("File::ABSOLUTE_PATH_PATTERN", File::POSIX_ABSOLUTE_PATH_PATTERN) 9 | end 10 | context 'when given an absolute path' do 11 | %w( 12 | /foo/bar 13 | /C/Windows/system32/ 14 | ).each do |path| 15 | context "(#{path})" do 16 | context 'the return value' do 17 | subject { File::absolute_path?(path) } 18 | it { should be true } 19 | end 20 | end 21 | end 22 | end 23 | context 'when given a relative path' do 24 | %w( 25 | C:/foo/bar 26 | \\foo\\bar 27 | C:\\foo\\bar 28 | foo/bar 29 | foo 30 | ./foo/bar 31 | ../foo/bar 32 | C:foo/bar 33 | ).each do |path| 34 | context "(#{path})" do 35 | context 'the return value' do 36 | subject { File::absolute_path?(path) } 37 | it { should be false } 38 | end 39 | end 40 | end 41 | end 42 | end 43 | 44 | context 'windows' do 45 | before(:each) do 46 | stub_const("File::ALT_SEPARATOR", '\\') 47 | stub_const("File::ABSOLUTE_PATH_PATTERN", File::WINDOWS_ABSOLUTE_PATH_PATTERN) 48 | end 49 | context 'when given an absolute path' do 50 | %w( 51 | /foo/bar 52 | C:/foo/bar 53 | \\foo\\bar 54 | C:\\foo\\bar 55 | /C/Windows/system32/ 56 | ).each do |path| 57 | context "(#{path})" do 58 | context 'the return value' do 59 | subject { File::absolute_path?(path) } 60 | it { should be true } 61 | end 62 | end 63 | end 64 | end 65 | context 'when given a relative path' do 66 | %w( 67 | foo/bar 68 | foo 69 | ./foo/bar 70 | ../foo/bar 71 | C:foo/bar 72 | ).each do |path| 73 | context "(#{path})" do 74 | context 'the return value' do 75 | subject { File::absolute_path?(path) } 76 | it { should be false } 77 | end 78 | end 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/cliver/detector_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'cliver' 3 | 4 | describe Cliver::Detector do 5 | let(:detector) { Cliver::Detector.new(*args) } 6 | let(:defaults) do 7 | { 8 | :version_pattern => Cliver::Detector::DEFAULT_VERSION_PATTERN, 9 | :command_arg => Cliver::Detector::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 | 45 | context 'detecting a command' do 46 | before(:each) do 47 | Cliver::ShellCapture.stub(:new => capture) 48 | end 49 | 50 | context 'that reports version on stdout' do 51 | let(:capture) { double('capture', :stdout => '1.1', 52 | :stderr => 'Warning: There is a monkey 1.2 metres left of you.', 53 | :command_found => true) } 54 | 55 | it 'should prefer the stdout output' do 56 | expect(detector.detect_version('foo')).to eq('1.1') 57 | end 58 | end 59 | 60 | context 'that reports version on stderr' do 61 | let(:capture) { double('capture', :stdout => '', 62 | :stderr => 'Version: 1.666', 63 | :command_found => true) } 64 | 65 | it 'should prefer the stderr output' do 66 | expect(detector.detect_version('foo')).to eq('1.666') 67 | end 68 | end 69 | 70 | context 'that does not exist' do 71 | let(:capture) { Cliver::ShellCapture.new('acommandnosystemshouldhave123') } 72 | 73 | it 'should raise an exception' do 74 | expect { detector.detect_version('foo') }.to raise_error(Cliver::Dependency::NotFound) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/cliver/detector.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'shellwords' 3 | 4 | module Cliver 5 | # Default implementation of the detector needed by Cliver::Assertion, 6 | # which will take anything that #respond_to?(:to_proc) 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 15 | 16 | # Default pattern to use when searching {#version_command} output 17 | DEFAULT_VERSION_PATTERN = /(version ?)?[0-9][.0-9a-z]+/i.freeze 18 | 19 | # Default command argument to use against the executable to get 20 | # version output 21 | DEFAULT_COMMAND_ARG = '--version'.freeze 22 | 23 | # Forgiving input, allows either argument if only one supplied. 24 | # 25 | # @overload initialize(*command_args) 26 | # @param command_args [Array] 27 | # @overload initialize(version_pattern) 28 | # @param version_pattern [Regexp] 29 | # @overload initialize(*command_args, version_pattern) 30 | # @param command_args [Array] 31 | # @param version_pattern [Regexp] 32 | def initialize(*args) 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) 37 | end 38 | 39 | # @param executable_path [String] - the path to the executable to test 40 | # @return [String] - should be contain {Gem::Version}-parsable 41 | # version number. 42 | def detect_version(executable_path) 43 | capture = ShellCapture.new(version_command(executable_path)) 44 | unless capture.command_found 45 | raise Cliver::Dependency::NotFound.new( 46 | "Could not find an executable at given path '#{executable_path}'." + 47 | "If this path was not specified explicitly, it is probably a " + 48 | "bug in [Cliver](https://github.com/yaauie/cliver/issues)." 49 | ) 50 | end 51 | capture.stdout[version_pattern] || capture.stderr[version_pattern] 52 | end 53 | 54 | # This is the interface that any detector must have. 55 | # If not overridden, returns a proc that wraps #detect_version 56 | # @see #detect_version 57 | # @return [Proc] following method signature of {#detect_version} 58 | def to_proc 59 | method(:detect_version).to_proc 60 | end 61 | 62 | # The pattern to match the version in {#version_command}'s output. 63 | # Defaults to {DEFAULT_VERSION_PATTERN} 64 | # @return [Regexp] - the pattern used against the output 65 | # of the #version_command, which should 66 | # contain a {Gem::Version}-parsable substring. 67 | def version_pattern 68 | super || DEFAULT_VERSION_PATTERN 69 | end 70 | 71 | # The argument to pass to the executable to get current version 72 | # Defaults to {DEFAULT_COMMAND_ARG} 73 | # @return [String, Array] 74 | def command_arg 75 | super || DEFAULT_COMMAND_ARG 76 | end 77 | 78 | # @param executable_path [String] the executable to test 79 | # @return [Array] 80 | def version_command(executable_path) 81 | [executable_path, *Array(command_arg)] 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/cliver.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.expand_path('../core_ext/file', __FILE__) 4 | 5 | require 'cliver/version' 6 | require 'cliver/dependency' 7 | require 'cliver/shell_capture' 8 | require 'cliver/detector' 9 | require 'cliver/filter' 10 | 11 | # Cliver is tool for making dependency assertions against 12 | # command-line executables. 13 | module Cliver 14 | 15 | # The primary interface for the Cliver gem allows detection of an executable 16 | # on your path that matches a version requirement, or raise an appropriate 17 | # exception to make resolution simple and straight-forward. 18 | # @see Cliver::Dependency 19 | # @overload (see Cliver::Dependency#initialize) 20 | # @param (see Cliver::Dependency#initialize) 21 | # @raise (see Cliver::Dependency#detect!) 22 | # @return (see Cliver::Dependency#detect!) 23 | def self.detect!(*args, &block) 24 | Dependency::new(*args, &block).detect! 25 | end 26 | 27 | # A non-raising variant of {::detect!}, simply returns false if dependency 28 | # cannot be found. 29 | # @see Cliver::Dependency 30 | # @overload (see Cliver::Dependency#initialize) 31 | # @param (see Cliver::Dependency#initialize) 32 | # @raise (see Cliver::Dependency#detect) 33 | # @return (see Cliver::Dependency#detect) 34 | def self.detect(*args, &block) 35 | Dependency::new(*args, &block).detect 36 | end 37 | 38 | # A legacy interface for {::detect} with the option `strict: true`, ensures 39 | # that the first executable on your path matches the requirements. 40 | # @see Cliver::Dependency 41 | # @overload (see Cliver::Dependency#initialize) 42 | # @param (see Cliver::Dependency#initialize) 43 | # @option options [Boolean] :strict (true) @see Cliver::Dependency::initialize 44 | # @raise (see Cliver::Dependency#detect!) 45 | # @return (see Cliver::Dependency#detect!) 46 | def self.assert(*args, &block) 47 | options = args.last.kind_of?(Hash) ? args.pop : {} 48 | args << options.merge(:strict => true) 49 | Dependency::new(*args, &block).detect! 50 | end 51 | 52 | # Verify an absolute-path to an executable. 53 | # @overload verify!(executable, *requirements, options = {}) 54 | # @param executable [String] absolute path to an executable 55 | # @param requirements (see Cliver::Dependency#initialize) 56 | # @option options (see Cliver::Dependency::initialize) 57 | # @raise (see Cliver::Dependency#detect!) 58 | # @return (see Cliver::Dependency#detect!) 59 | def self.verify!(executable, *args, &block) 60 | unless File.absolute_path?(executable) 61 | raise ArgumentError, "executable path must be absolute, " + 62 | "got '#{executable.inspect}'." 63 | end 64 | options = args.last.kind_of?(Hash) ? args.pop : {} 65 | args << options.merge(:path => '.') # ensure path non-empty. 66 | Dependency::new(executable, *args, &block).detect! 67 | end 68 | 69 | extend self 70 | 71 | # Wraps Cliver::assert and returns truthy/false instead of raising 72 | # @see Cliver::assert 73 | # @overload (see Cliver::Assertion#initialize) 74 | # @param (see Cliver::Assertion#initialize) 75 | # @return [False,String] either returns false or the reason why the 76 | # assertion was unmet. 77 | def dependency_unmet?(*args, &block) 78 | Cliver.assert(*args, &block) 79 | false 80 | rescue Dependency::NotMet => error 81 | # Cliver::Assertion::VersionMismatch -> 'Version Mismatch' 82 | reason = error.class.name.split(':').last.gsub(/([a-z])([A-Z])/, '\\1 \\2') 83 | "#{reason}: #{error.message}" 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cliver 2 | 3 | Sometimes Ruby apps shell out to command-line executables, but there is no 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, or by 6 | the odd behavior exposed by a version mismatch. 7 | 8 | `Cliver` is a simple gem that provides an easy way to detect and use 9 | command-line dependencies. Under the covers, it uses [rubygems/requirements][] 10 | so it supports the version requirements you're used to providing in your 11 | gemspec. 12 | 13 | ## Usage 14 | 15 | ### Get Out of My Way, CLIVER! 16 | 17 | Sometimes even Cliver can't perfectly deduce the version of an executable given 18 | its `--version` output. If you're getting an exception with a mismatch, supply 19 | the `CLIVER_NO_VERIFY` environment variable, and it will assume that the first 20 | executable on your path to match the *name* also matches the *version* 21 | constraint. Please also create an issue here so I can help you resolve the root 22 | issue. 23 | 24 | ``` sh 25 | $ command-that-uses-cliver # raises exception :( 26 | $ export CLIVER_NO_VERIFY=1 27 | $ command-that-uses-cliver # no cliver exception :) 28 | ``` 29 | 30 | ### Detect and Detect! 31 | 32 | The detect methods search your entire path until they find a matching executable 33 | or run out of places to look. 34 | 35 | ```ruby 36 | # no version requirements 37 | Cliver.detect('subl') 38 | # => '/Users/yaauie/.bin/subl' 39 | 40 | # one version requirement 41 | Cliver.detect('bzip2', '~> 1.0.6') 42 | # => '/usr/bin/bzip2' 43 | 44 | # many version requirements 45 | Cliver.detect('racc', '>= 1.0', '< 1.4.9') 46 | # => '/Users/yaauie/.rbenv/versions/1.9.3-p194/bin/racc' 47 | 48 | # dependency not met 49 | Cliver.detect('racc', '~> 10.4.9') 50 | # => nil 51 | 52 | # detect! raises Cliver::Dependency::NotMet exceptions when the dependency 53 | # cannot be met. 54 | Cliver.detect!('ruby', '1.8.5') 55 | # Cliver::Dependency::VersionMismatch 56 | # Could not find an executable ruby that matched the 57 | # requirements '1.8.5'. Found versions were {'/usr/bin/ruby'=> '1.8.7'} 58 | Cliver.detect!('asdfasdf') 59 | # Cliver::Dependency::NotFound 60 | # Could not find an executable asdfasdf on your path 61 | ``` 62 | 63 | ### Assert 64 | 65 | The assert method is useful when you do not have control over how the 66 | dependency is shelled-out to and require that the first matching executable on 67 | your path satisfies your version requirements. It is the equivalent of the 68 | detect! method with `strict: true` option. 69 | 70 | ## Advanced Usage: 71 | 72 | ### Version Detectors 73 | 74 | Some programs don't provide nice 'version 1.2.3' strings in their `--version` 75 | output; `Cliver` lets you provide your own version detector with a pattern. 76 | 77 | ```ruby 78 | Cliver.assert('python', '~> 1.7', 79 | detector: /(?<=Python )[0-9][.0-9a-z]+/) 80 | ``` 81 | 82 | Other programs don't provide a standard `--version`; `Cliver::Detector` also 83 | allows you to provide your own arg to get the version: 84 | 85 | ```ruby 86 | # single-argument command 87 | Cliver.assert('janky', '~> 10.1.alpha', 88 | detector: '--release-version') 89 | 90 | # multi-argument command 91 | Cliver.detect('ruby', '~> 1.8.7', 92 | detector: [['-e', 'puts RUBY_VERSION']]) 93 | ``` 94 | 95 | You can use both custom pattern and custom command by supplying an array: 96 | 97 | ```ruby 98 | Cliver.assert('janky', '~> 10.1.alpha', 99 | detector: ['--release-version', /.*/]) 100 | ``` 101 | 102 | And even supply multiple arguments in an Array, too: 103 | 104 | ```ruby 105 | # multi-argument command 106 | Cliver.detect('ruby', '~> 1.8.7', 107 | detector: ['-e', 'puts RUBY_VERSION']) 108 | ``` 109 | 110 | Alternatively, you can supply your own detector (anything that responds to 111 | `#to_proc`) in the options hash or as a block, so long as it returns a 112 | `Gem::Version`-parsable version number; if it returns nil or false when 113 | version requirements are given, a descriptive `ArgumentError` is raised. 114 | 115 | ```ruby 116 | Cliver.assert('oddball', '~> 10.1.alpha') do |oddball_path| 117 | File.read(File.expand_path('../VERSION', oddball_path)).chomp 118 | end 119 | ``` 120 | 121 | And since some programs don't always spit out nice semver-friendly version 122 | numbers at all, a filter proc can be supplied to clean it up. Note how the 123 | filter is applied to both your requirements and the executable's output: 124 | 125 | ### Filters 126 | 127 | ```ruby 128 | Cliver.assert('built-thing', '~> 2013.4r8273', 129 | filter: proc { |ver| ver.tr('r','.') }) 130 | ``` 131 | 132 | Since `Cliver` uses `Gem::Requirement` for version comparrisons, it obeys all 133 | the same rules including pre-release semantics. 134 | 135 | ### Search Path 136 | 137 | By default, Cliver uses `ENV['PATH']` as its search path, but you can provide 138 | your own. If the asterisk symbol (`*`) is included in your string, it is 139 | replaced `ENV['PATH']`. 140 | 141 | ```ruby 142 | Cliver.detect('gadget', path: './bins/:*') 143 | # => 'Users/yaauie/src/project-a/bins/gadget' 144 | ``` 145 | 146 | ## Supported Platforms 147 | 148 | The goal is to have full support for all platforms running ruby >= 1.9.2, 149 | including rubinius and jruby implementations, as well as basic support for 150 | legacy ruby 1.8.7. Windows has support in the codebase, 151 | but is not available as a build target in [travis_ci][]. 152 | 153 | ## See Also: 154 | 155 | - [YARD Documentation][yard-docs] 156 | - [Contributing](CONTRIBUTING.md) 157 | - [License](LICENSE.txt) 158 | 159 | 160 | [rubygems/requirements]: https://github.com/rubygems/rubygems/blob/master/lib/rubygems/requirement.rb 161 | [yard-docs]: http://yaauie.github.io/cliver/ 162 | [travis-ci]: https://travis-ci.org/yaauie/cliver 163 | -------------------------------------------------------------------------------- /lib/cliver/dependency.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'rubygems/requirement' 3 | require 'set' 4 | 5 | module Cliver 6 | # This is how a dependency is specified. 7 | class Dependency 8 | 9 | # An exception class raised when assertion is not met 10 | NotMet = Class.new(ArgumentError) 11 | 12 | # An exception that is raised when executable present, but 13 | # no version that matches the requirements is present. 14 | VersionMismatch = Class.new(Dependency::NotMet) 15 | 16 | # An exception that is raised when executable is not present at all. 17 | NotFound = Class.new(Dependency::NotMet) 18 | 19 | # A pattern for extracting a {Gem::Version}-parsable version 20 | PARSABLE_GEM_VERSION = /[0-9]+(.[0-9]+){0,4}(.[a-zA-Z0-9]+)?/.freeze 21 | 22 | # @overload initialize(executables, *requirements, options = {}) 23 | # @param executables [String,Array] api-compatible executable names 24 | # e.g, ['python2','python'] 25 | # @param requirements [Array, String] splat of strings 26 | # whose elements follow the pattern 27 | # [] 28 | # Where is optional (default '='') and in the set 29 | # '=', '!=', '>', '<', '>=', '<=', or '~>' 30 | # And is dot-separated integers with optional 31 | # alphanumeric pre-release suffix. See also 32 | # {http://docs.rubygems.org/read/chapter/16 Specifying Versions} 33 | # @param options [Hash] 34 | # @option options [Cliver::Detector] :detector (Detector.new) 35 | # @option options [#to_proc, Object] :detector (see Detector::generate) 36 | # @option options [#to_proc] :filter ({Cliver::Filter::IDENTITY}) 37 | # @option options [Boolean] :strict (false) 38 | # true - fail if first match on path fails 39 | # to meet version requirements. 40 | # This is used for Cliver::assert. 41 | # false - continue looking on path until a 42 | # sufficient version is found. 43 | # @option options [String] :path ('*') the path on which to search 44 | # for executables. If an asterisk (`*`) is 45 | # included in the supplied string, it is 46 | # replaced with `ENV['PATH']` 47 | # 48 | # @yieldparam executable_path [String] (see Detector#detect_version) 49 | # @yieldreturn [String] containing a version that, once filtered, can be 50 | # used for comparrison. 51 | def initialize(executables, *args, &detector) 52 | options = args.last.kind_of?(Hash) ? args.pop : {} 53 | @detector = Detector::generate(detector || options[:detector]) 54 | @filter = options.fetch(:filter, Filter::IDENTITY).extend(Filter) 55 | @path = options.fetch(:path, '*') 56 | @strict = options.fetch(:strict, false) 57 | 58 | @executables = Array(executables).dup.freeze 59 | @requirement = args unless args.empty? 60 | 61 | check_compatibility! 62 | end 63 | 64 | # One of these things is not like the other ones... 65 | # Some feature combinations just aren't compatible. This method ensures 66 | # the the features selected for this object are compatible with each-other. 67 | # @return [void] 68 | # @raise [ArgumentError] if incompatibility found 69 | def check_compatibility! 70 | case 71 | when @executables.any? {|exe| exe[File::SEPARATOR] && !File.absolute_path?(exe) } 72 | # if the executable contains a path component, it *must* be absolute. 73 | raise ArgumentError, "Relative-path executable requirements are not supported." 74 | end 75 | end 76 | 77 | # Get all the installed versions of the api-compatible executables. 78 | # If a block is given, it yields once per found executable, lazily. 79 | # @yieldparam executable_path [String] 80 | # @yieldparam version [String] 81 | # @yieldreturn [Boolean] - true if search should stop. 82 | # @return [Hash] executable_path, version 83 | def installed_versions 84 | return enum_for(:installed_versions) unless block_given? 85 | 86 | find_executables.each do |executable_path| 87 | version = detect_version(executable_path) 88 | 89 | break(2) if yield(executable_path, version) 90 | end 91 | end 92 | 93 | # The non-raise variant of {#detect!} 94 | # @return (see #detect!) 95 | # or nil if no match found. 96 | def detect 97 | detect! 98 | rescue Dependency::NotMet 99 | nil 100 | end 101 | 102 | # Detects an installed version of the executable that matches the 103 | # requirements. 104 | # @return [String] path to an executable that meets the requirements 105 | # @raise [Cliver::Dependency::NotMet] if no match found 106 | def detect! 107 | installed = {} 108 | installed_versions.each do |path, version| 109 | installed[path] = version 110 | return path if ENV['CLIVER_NO_VERIFY'] 111 | return path if requirement_satisfied_by?(version) 112 | strict? 113 | end 114 | 115 | # dependency not met. raise the appropriate error. 116 | raise_not_found! if installed.empty? 117 | raise_version_mismatch!(installed) 118 | end 119 | 120 | private 121 | 122 | # @api private 123 | # @return [Gem::Requirement] 124 | def filtered_requirement 125 | @filtered_requirement ||= begin 126 | Gem::Requirement.new(@filter.requirements(@requirement)) 127 | end 128 | end 129 | 130 | # @api private 131 | # @param raw_version [String] 132 | # @return [Boolean] 133 | def requirement_satisfied_by?(raw_version) 134 | return true unless @requirement 135 | parsable_version = @filter.apply(raw_version)[PARSABLE_GEM_VERSION] 136 | parsable_version || raise(ArgumentError) # TODO: make descriptive 137 | filtered_requirement.satisfied_by? Gem::Version.new(parsable_version) 138 | end 139 | 140 | # @api private 141 | # @raise [Cliver::Dependency::NotFound] with appropriate error message 142 | def raise_not_found! 143 | raise Dependency::NotFound.new( 144 | "Could not find an executable #{@executables} on your path.") 145 | end 146 | 147 | # @api private 148 | # @raise [Cliver::Dependency::VersionMismatch] with appropriate error message 149 | # @param installed [Hash] the found versions 150 | def raise_version_mismatch!(installed) 151 | raise Dependency::VersionMismatch.new( 152 | "Could not find an executable #{executable_description} that " + 153 | "matched the requirements #{requirements_description}. " + 154 | "Found versions were #{installed.inspect}.") 155 | end 156 | 157 | # @api private 158 | # @return [String] a plain-language representation of the executables 159 | # for which we were searching 160 | def executable_description 161 | quoted_exes = @executables.map {|exe| "'#{exe}'" } 162 | return quoted_exes.first if quoted_exes.size == 1 163 | 164 | last_quoted_exec = quoted_exes.pop 165 | "#{quoted_exes.join(', ')} or #{last_quoted_exec}" 166 | end 167 | 168 | # @api private 169 | # @return [String] a plain-language representation of the requirements 170 | def requirements_description 171 | @requirement.map {|req| "'#{req}'" }.join(', ') 172 | end 173 | 174 | # If strict? is true, only attempt the first matching executable on the path 175 | # @api private 176 | # @return [Boolean] 177 | def strict? 178 | false | @strict 179 | end 180 | 181 | # Given a path to an executable, detect its version 182 | # @api private 183 | # @param executable_path [String] 184 | # @return [String] 185 | # @raise [ArgumentError] if version cannot be detected. 186 | def detect_version(executable_path) 187 | # No need to shell out if we are only checking its presence. 188 | return '99.version_detection_not_required' unless @requirement 189 | 190 | raw_version = @detector.to_proc.call(executable_path) 191 | raw_version || raise(ArgumentError, 192 | "The detector #{@detector} failed to detect the " + 193 | "version of the executable at '#{executable_path}'") 194 | end 195 | 196 | # Analog of Windows `where` command, or a `which` that finds *all* 197 | # matching executables on the supplied path. 198 | # @return [Enumerable] - the executables found, lazily. 199 | def find_executables 200 | return enum_for(:find_executables) unless block_given? 201 | 202 | exts = (ENV.has_key?('PATHEXT') ? ENV.fetch('PATHEXT').split(';') : []) << '' 203 | paths = @path.sub('*', ENV['PATH']).split(File::PATH_SEPARATOR) 204 | raise ArgumentError.new('No PATH to search!') if paths.empty? 205 | cmds = strict? ? @executables.first(1) : @executables 206 | 207 | lookup_cache = Set.new 208 | cmds.product(paths, exts).map do |cmd, path, ext| 209 | exe = File.absolute_path?(cmd) ? cmd : File.expand_path("#{cmd}#{ext}", path) 210 | 211 | next unless lookup_cache.add?(exe) # don't yield the same exe path 2x 212 | next unless File.executable?(exe) 213 | next if File.directory?(exe) 214 | 215 | yield exe 216 | end 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /spec/cliver_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'cliver' 3 | require 'spec_helper' 4 | require File.expand_path('../support/executable_mock', __FILE__) 5 | 6 | RSpec::Matchers.define :be_filesystem_equivalent_of do |expected| 7 | match do |actual| 8 | ExecutableMock.new(expected) == actual 9 | end 10 | end 11 | 12 | 13 | describe Cliver do 14 | # The setup. Your test will likeley interact with subject. 15 | let(:action) { Cliver.public_send(method, *args, &block) } 16 | subject { action } 17 | 18 | # These can get overridden in context blocks 19 | let(:method) { raise ArgumentError, 'spec didn\'t specify :method' } 20 | let(:args) { raise ArgumentError, 'spec didn\'t specify :args' } 21 | let(:block) { version_directory.method(:version) } 22 | 23 | # BecauseWindows. This enables us to mock out File::executable? 24 | # and the responses from our detectors given any representation 25 | # of a file path. 26 | let(:version_directory) do 27 | ExecutableMock::Registry.new(version_map) 28 | end 29 | 30 | before(:each) do 31 | File.stub(:executable?, &version_directory.method(:executable?)) 32 | end 33 | 34 | let(:options) do 35 | { 36 | :path => path.join(File::PATH_SEPARATOR), 37 | :executable => executable, 38 | } 39 | end 40 | let(:args) do 41 | args = [Array(executable)] 42 | args.concat Array(requirement) 43 | args << options 44 | end 45 | 46 | let(:path) { ['/foo/bar','/baz/bingo'] } 47 | let(:executable) { 'doodle' } 48 | let(:requirement) { '~>1.1'} 49 | 50 | context 'when first-found version is sufficient' do 51 | 52 | let(:version_map) do 53 | {'/baz/bingo/doodle' => '1.2.1'} 54 | end 55 | 56 | context '::assert' do 57 | let(:method) { :assert } 58 | it 'should not raise' do 59 | expect { action }.to_not raise_exception 60 | end 61 | end 62 | 63 | context '::dependency_unmet?' do 64 | let(:method) { :dependency_unmet? } 65 | it { should be false } 66 | end 67 | context '::detect' do 68 | let(:method) { :detect } 69 | it { should be_filesystem_equivalent_of '/baz/bingo/doodle' } 70 | end 71 | context '::detect!' do 72 | let(:method) { :detect! } 73 | it 'should not raise' do 74 | expect { action }.to_not raise_exception 75 | end 76 | it { should be_filesystem_equivalent_of '/baz/bingo/doodle' } 77 | end 78 | end 79 | 80 | context '::verify!' do 81 | let(:method) { :verify! } 82 | let(:version_map) do 83 | {'/baz/bingo/doodle' => '0.2.1', 84 | '/baz/fiddle/doodle' => '1.1.4'} 85 | end 86 | let(:args) do 87 | args = [executable] 88 | args.concat Array(requirement) 89 | args << options 90 | end 91 | context 'when a relative path is given' do 92 | let(:executable) { 'foo/bar/doodle' } 93 | it 'should raise' do 94 | expect { action }.to raise_exception ArgumentError 95 | end 96 | end 97 | context 'when an absolute path is given' do 98 | context 'and that path is not found' do 99 | let(:executable) { '/blip/boom' } 100 | it 'should raise' do 101 | expect { action }.to raise_exception Cliver::Dependency::NotFound 102 | end 103 | end 104 | context '(windows path)' do 105 | before(:each) do 106 | stub_const('File::ABSOLUTE_PATH_PATTERN', File::WINDOWS_ABSOLUTE_PATH_PATTERN) 107 | end 108 | let(:version_map) do 109 | {'C:/baz/bingo/doodle.exe' => '0.2.1', 110 | 'C:/baz/fiddle/doodle.exe' => '1.1.4'} 111 | end 112 | context 'and executable at that path is sufficient' do 113 | let(:executable) { 'C:/baz/fiddle/doodle.exe' } 114 | it 'should not raise' do 115 | expect { action }.to_not raise_exception 116 | end 117 | end 118 | context 'and the executable at that path is not sufficent' do 119 | let(:executable) { 'C:/baz/bingo/doodle.exe' } 120 | it 'should raise' do 121 | expect { action }.to raise_exception Cliver::Dependency::VersionMismatch 122 | end 123 | end 124 | context 'and no executable exists at that path' do 125 | let(:version_map) { Hash.new } 126 | let(:executable) { 'C:/baz/fiddle/doodle.exe' } 127 | it 'should raise' do 128 | expect { action }.to raise_exception Cliver::Dependency::NotFound 129 | end 130 | end 131 | end 132 | context 'and the executable at that path is sufficent' do 133 | let(:executable) { '/baz/fiddle/doodle' } 134 | it 'should not raise' do 135 | expect { action }.to_not raise_exception 136 | end 137 | end 138 | context 'and the executable at that path is not sufficent' do 139 | let(:executable) { '/baz/bingo/doodle' } 140 | it 'should raise' do 141 | expect { action }.to raise_exception Cliver::Dependency::VersionMismatch 142 | end 143 | end 144 | end 145 | end 146 | 147 | context 'when given executable as a path' do 148 | let(:version_map) do 149 | {'/baz/bingo/doodle' => '1.2.1'} 150 | end 151 | let(:path) { ['/fiddle/foo','/deedle/dee'] } 152 | 153 | context 'that is absolute' do 154 | let(:executable) { '/baz/bingo/doodle' } 155 | %w(assert dependency_unmet? detect detect).each do |method_name| 156 | context "::#{method_name}" do 157 | let(:method) { method_name.to_sym } 158 | it 'should only detect its version once' do 159 | Cliver::Dependency.any_instance. 160 | should_receive(:detect_version). 161 | once. 162 | and_call_original 163 | action 164 | end 165 | end 166 | end 167 | end 168 | 169 | context 'that is relative' do 170 | let(:executable) { 'baz/bingo/doodle' } 171 | %w(assert dependency_unmet? detect detect).each do |method_name| 172 | context "::#{method_name}" do 173 | let(:method) { method_name.to_sym } 174 | it 'should raise an ArgumentError' do 175 | expect { action }.to raise_exception ArgumentError 176 | end 177 | end 178 | end 179 | end 180 | end 181 | 182 | context 'when first-found version insufficient' do 183 | let(:version_map) do 184 | {'/baz/bingo/doodle' => '1.0.1'} 185 | end 186 | context '::assert' do 187 | let(:method) { :assert } 188 | it 'should raise' do 189 | expect { action }.to raise_exception Cliver::Dependency::VersionMismatch 190 | end 191 | end 192 | context '::dependency_unmet?' do 193 | let(:method) { :dependency_unmet? } 194 | it { should be_truthy } 195 | end 196 | context '::detect' do 197 | let(:method) { :detect } 198 | it { should be_nil } 199 | end 200 | context '::detect!' do 201 | let(:method) { :detect! } 202 | it 'should not raise' do 203 | expect { action }.to raise_exception Cliver::Dependency::VersionMismatch 204 | end 205 | end 206 | 207 | context 'and when sufficient version found later on path' do 208 | let(:version_map) do 209 | { 210 | '/foo/bar/doodle' => '0.0.1', 211 | '/baz/bingo/doodle' => '1.1.0', 212 | } 213 | end 214 | context '::assert' do 215 | let(:method) { :assert } 216 | it 'should raise' do 217 | expect { action }.to raise_exception Cliver::Dependency::VersionMismatch 218 | end 219 | end 220 | context '::dependency_unmet?' do 221 | let(:method) { :dependency_unmet? } 222 | it { should be_truthy } 223 | end 224 | context '::detect' do 225 | let(:method) { :detect } 226 | it { should be_filesystem_equivalent_of '/baz/bingo/doodle' } 227 | end 228 | context '::detect!' do 229 | let(:method) { :detect! } 230 | it 'should not raise' do 231 | expect { action }.to_not raise_exception 232 | end 233 | it { should be_filesystem_equivalent_of '/baz/bingo/doodle' } 234 | end 235 | end 236 | end 237 | 238 | context 'when no found version' do 239 | let(:version_map) { {} } 240 | 241 | context '::assert' do 242 | let(:method) { :assert } 243 | it 'should raise' do 244 | expect { action }.to raise_exception Cliver::Dependency::NotFound 245 | end 246 | end 247 | context '::dependency_unmet?' do 248 | let(:method) { :dependency_unmet? } 249 | it { should be_truthy } 250 | end 251 | context '::detect' do 252 | let(:method) { :detect } 253 | it { should be_nil } 254 | end 255 | context '::detect!' do 256 | let(:method) { :detect! } 257 | it 'should not raise' do 258 | expect { action }.to raise_exception Cliver::Dependency::NotFound 259 | end 260 | end 261 | end 262 | 263 | context 'with fallback executable names' do 264 | let(:executable) { ['primary', 'fallback'] } 265 | let(:requirement) { '~> 1.1' } 266 | context 'when primary exists after secondary in path' do 267 | context 'and primary sufficient' do 268 | let(:version_map) do 269 | { 270 | '/baz/bingo/primary' => '1.1', 271 | '/foo/bar/fallback' => '1.1' 272 | } 273 | end 274 | context '::detect' do 275 | let(:method) { :detect } 276 | it { should be_filesystem_equivalent_of '/baz/bingo/primary' } 277 | end 278 | end 279 | context 'and primary insufficient' do 280 | let(:version_map) do 281 | { 282 | '/baz/bingo/primary' => '2.1', 283 | '/foo/bar/fallback' => '1.1' 284 | } 285 | end 286 | context 'the secondary' do 287 | context '::detect' do 288 | let(:method) { :detect } 289 | it { should be_filesystem_equivalent_of '/foo/bar/fallback' } 290 | end 291 | end 292 | end 293 | end 294 | context 'when primary does not exist in path' do 295 | context 'and sufficient secondary does' do 296 | let(:version_map) do 297 | { 298 | '/foo/bar/fallback' => '1.1' 299 | } 300 | end 301 | context '::detect' do 302 | let(:method) { :detect } 303 | it { should be_filesystem_equivalent_of '/foo/bar/fallback' } 304 | end 305 | end 306 | end 307 | 308 | context 'neither found' do 309 | context '::detect' do 310 | let(:version_map) { {} } 311 | let(:method) { :detect } 312 | it { should be_nil } 313 | end 314 | end 315 | end 316 | end 317 | --------------------------------------------------------------------------------