cli-forge 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +5 -1
  3. data/Gemfile +27 -8
  4. data/Guardfile +3 -5
  5. data/README.md +1 -1
  6. data/Rakefile +3 -3
  7. data/cli-forge.gemspec +1 -1
  8. data/lib/cli_forge.rb +27 -0
  9. data/lib/cli_forge/autoload_convention.rb +4 -1
  10. data/lib/cli_forge/bin_command.rb +17 -0
  11. data/lib/cli_forge/command_set.rb +38 -0
  12. data/lib/cli_forge/configuration.rb +66 -0
  13. data/lib/cli_forge/default_configuration.rb +27 -0
  14. data/lib/cli_forge/embedded_commands.rb +3 -0
  15. data/lib/cli_forge/embedded_commands/help.rb +11 -0
  16. data/lib/cli_forge/runner.rb +42 -0
  17. data/lib/cli_forge/version.rb +1 -1
  18. data/spec/common/fixture_config.rb +13 -0
  19. data/spec/fixtures/autoload_convention/abc_one_two_three.rb +5 -0
  20. data/spec/fixtures/bins/foo-bar +0 -0
  21. data/spec/fixtures/bins2/foo-bar +0 -0
  22. data/spec/fixtures/bins2/foo-too +0 -0
  23. data/spec/fixtures/bins3/foo-bar +0 -0
  24. data/spec/spec_helper.rb +2 -1
  25. data/spec/unit/cli_forge/autoload_convention/const_missing_spec.rb +6 -0
  26. data/spec/unit/cli_forge/class_methods/caller_path_spec.rb +38 -0
  27. data/spec/unit/cli_forge/class_methods/guess_bin_name_spec.rb +17 -0
  28. data/spec/unit/cli_forge/class_methods/start_spec.rb +93 -0
  29. data/spec/unit/cli_forge/command_set/element_reader_spec.rb +55 -0
  30. data/spec/unit/cli_forge/configuration/register_argument_filter_spec.rb +26 -0
  31. data/spec/unit/cli_forge/configuration/register_command_spec.rb +26 -0
  32. data/spec/unit/cli_forge/configuration/remove_argument_filter_spec.rb +29 -0
  33. data/spec/unit/cli_forge/configuration/remove_command_spec.rb +30 -0
  34. data/spec/unit/cli_forge/runner/start_spec.rb +28 -0
  35. data/tasks/spec/coverage.rake +1 -1
  36. data/tasks/spec/mutate.rake +122 -15
  37. metadata +31 -3
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  coverage/
2
+ pkg/
2
3
  tmp/
3
4
  .DS_Store
4
5
  Gemfile.lock
@@ -1,5 +1,5 @@
1
1
  language: ruby
2
- bundler_args: --without debugging development
2
+ bundler_args: --without debugging guard
3
3
  script: bundle exec rake spec:ci
4
4
  rvm:
5
5
  - 1.8.7
@@ -11,3 +11,7 @@ rvm:
11
11
  - jruby-18mode
12
12
  - jruby-19mode
13
13
  - ree
14
+ - topaz
15
+ matrix:
16
+ allow_failures:
17
+ - rvm: topaz
data/Gemfile CHANGED
@@ -13,7 +13,10 @@ group :test do
13
13
 
14
14
  # The preferred code mutation library.
15
15
  # MIT License - https://github.com/mbj/mutant/blob/master/LICENSE
16
- gem "mutant", ">= 0.2.20", "< 1.0.0", :platforms => :ruby_19
16
+ gem "mutant", "~> 0.2", ">= 0.2.20", :platforms => :ruby_19
17
+
18
+ # Dependency of mutant; this version fixes some breaking source gen bugs.
19
+ gem "to_source", ">= 0.2.20"
17
20
 
18
21
  # Cover all the things - https://github.com/colszowka/simplecov
19
22
  # MIT License - https://github.com/colszowka/simplecov/blob/master/LICENSE
@@ -34,7 +37,7 @@ group :debugging do
34
37
  gem "debugger", "~> 1.3", :platforms => :mri
35
38
  end
36
39
 
37
- group :development do
40
+ group :guard do
38
41
  # A generic file system event handler; spin it up and see the tests fly
39
42
  # MIT License - https://github.com/guard/guard/blob/master/LICENSE
40
43
  gem "guard", "~> 1.6"
@@ -51,11 +54,27 @@ group :development do
51
54
  # MIT License - https://github.com/guard/guard-rspec/blob/master/LICENSE
52
55
  gem "guard-rspec", "~> 2.4"
53
56
 
54
- # Growl notification protocol gem - give us fancy warnings when things go awry
55
- # MIT License - https://github.com/snaka/ruby_gntp/blob/master/lib/ruby_gntp.rb
56
- gem "ruby_gntp", "~> 0.3"
57
-
58
- # FS event hooks for OS X
57
+ # File system event hooks for OS X
59
58
  # MIT License - https://github.com/thibaudgg/rb-fsevent/blob/master/LICENSE
60
- gem "rb-fsevent", "~> 0.9", :require => RUBY_PLATFORM.include?("darwin") && "rb-fsevent"
59
+ gem "rb-fsevent", "~> 0.9"
60
+
61
+ # File system event hooks for Linux
62
+ # MIT License - https://github.com/nex3/rb-inotify/blob/master/MIT-LICENSE
63
+ gem "rb-inotify", "~> 0.9"
64
+
65
+ # File system event hooks for Windows
66
+ # MIT License - https://github.com/stereobooster/rb-fchange/blob/master/LICENSE.md
67
+ gem "rb-fchange", "~> 0.0"
68
+
69
+ # OS X 10.8+ notification center support
70
+ # MIT License - https://github.com/Springest/terminal-notifier-guard#license
71
+ gem "terminal-notifier-guard", "~> 1.5"
72
+
73
+ # libnotify bindings (Linux)
74
+ # MIT License - https://github.com/splattael/libnotify/blob/master/LICENSE
75
+ gem "libnotify", "~> 0.8"
76
+
77
+ # notifu adapter (Windows)
78
+ # Unknown License
79
+ gem "rb-notifu", "~> 0.0"
61
80
  end
data/Guardfile CHANGED
@@ -3,8 +3,6 @@ GLOBAL_SPEC_FILES = [
3
3
  ".rspec",
4
4
  %r{^spec/.*_helper\.rb$},
5
5
  %r{^spec/common/.*\.rb$},
6
- "lib/cli_forge.rb",
7
- "lib/cli_forge/autoload_convention.rb",
8
6
  ]
9
7
 
10
8
  def specs_for_path(path)
@@ -26,9 +24,9 @@ guard "spork", rspec_port: 2733 do
26
24
  end
27
25
 
28
26
  guard "rspec", cli: "--drb --drb-port 2733" do
29
- GLOBAL_SPEC_FILES.each do |pattern|
30
- watch(pattern) { "spec" }
31
- end
27
+ watch("lib/cli_forge.rb") { "spec" }
28
+ watch("lib/cli_forge/autoload_convention.rb") { "spec" }
29
+ watch(%r{^spec/fixtures/.*\.rb$}) { "spec" }
32
30
 
33
31
  watch(%r{^spec/.+_spec\.rb$})
34
32
  watch(%r{^lib/(.+)\.rb$}) { |m| specs_for_path(m[1]) }
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- CLI Forge [![Gem Version](https://badge.fury.io/rb/cli-forge.png)](https://rubygems.org/gems/cli-forge) [![Build Status](https://secure.travis-ci.org/nevir/cli-forge.png?branch=master)](http://travis-ci.org/nevir/cli-forge) [![Coverage Status](https://coveralls.io/repos/nevir/cli-forge/badge.png?branch=master)](https://coveralls.io/r/nevir/cli-forge) [![Code Climate](https://codeclimate.com/github/nevir/cli-forge.png)](https://codeclimate.com/github/nevir/cli-forge) [![Dependency Status](https://gemnasium.com/nevir/cli-forge.png)](https://gemnasium.com/nevir/cli-forge)
1
+ CLI Forge [![Gem Version](https://badge.fury.io/rb/cli-forge.png)](https://rubygems.org/gems/cli-forge) [![Build Status](https://secure.travis-ci.org/nevir/cli-forge.png?branch=master)](http://travis-ci.org/nevir/cli-forge) [![Dependency Status](https://gemnasium.com/nevir/cli-forge.png)](https://gemnasium.com/nevir/cli-forge) [![Coverage Status](https://coveralls.io/repos/nevir/cli-forge/badge.png?branch=master)](https://coveralls.io/r/nevir/cli-forge) [![Code Climate](https://codeclimate.com/github/nevir/cli-forge.png)](https://codeclimate.com/github/nevir/cli-forge)
2
2
  =========
3
3
 
4
4
  Beat your CLI tool suites into submission!
data/Rakefile CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
3
 
4
- PROJECT_PATH = File.expand_path("..", __FILE__)
5
- Dir["#{PROJECT_PATH}/tasks/**/*.rake"].each do |path|
4
+ PROJECT_ROOT = File.expand_path("..", __FILE__)
5
+ Dir["#{PROJECT_ROOT}/tasks/**/*.rake"].each do |path|
6
6
  load path
7
7
  end
8
8
 
9
- $LOAD_PATH.unshift File.join(PROJECT_PATH, "lib")
9
+ $LOAD_PATH.unshift File.join(PROJECT_ROOT, "lib")
10
10
 
11
11
 
12
12
  desc "Run the full test suite"
@@ -5,7 +5,7 @@ require File.expand_path("../lib/cli_forge/version", __FILE__)
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "cli-forge"
7
7
  gem.version = CLIForge::VERSION
8
- gem.homepage = "http://github.com/nevir/cli-forge"
8
+ gem.homepage = "https://github.com/nevir/cli-forge"
9
9
  gem.summary = "Beat your CLI tool suites into submission!"
10
10
  gem.description = "A library for building CLI tools that are intended to be easily extensible (git style)"
11
11
  gem.author = "Ian MacLeod"
@@ -2,4 +2,31 @@ require "cli_forge/autoload_convention"
2
2
 
3
3
  module CLIForge
4
4
  extend CLIForge::AutoloadConvention
5
+
6
+ def self.start(bin_name=nil, &block)
7
+ config = CLIForge::DefaultConfiguration.new
8
+
9
+ config.search_paths = Array(caller_path(caller.first))
10
+ config.search_paths += ENV["PATH"].split(":")
11
+ config.search_paths.uniq!
12
+
13
+ block.call(config) if block
14
+
15
+ config.bin_name ||= bin_name || guess_bin_name
16
+
17
+ CLIForge::Runner.new(config).start(ARGV)
18
+ end
19
+
20
+ def self.guess_bin_name
21
+ File.basename($0)
22
+ end
23
+
24
+ def self.caller_path(stack_line)
25
+ return unless stack_line
26
+ stack_path = stack_line.split(":").first
27
+ return if stack_path =~ /^\(.*\)$/
28
+
29
+ File.expand_path(File.dirname(stack_path))
30
+ end
31
+
5
32
  end
@@ -15,7 +15,10 @@ module CLIForge
15
15
  def const_missing(sym)
16
16
  full_sym = "#{self.name}::#{sym}"
17
17
  path_parts = full_sym.split("::").map { |part|
18
- part.gsub(/([^A-Z])([A-Z]+)/, "\\1_\\2").downcase
18
+ part.gsub! /([^A-Z])([A-Z]+)/, "\\1_\\2" # OneTwo -> One_Two
19
+ part.gsub! /([A-Z]+)([A-Z][^A-Z]+)/, "\\1_\\2" # ABCOne -> ABC_One
20
+
21
+ part.downcase
19
22
  }
20
23
 
21
24
  require File.join(*path_parts)
@@ -0,0 +1,17 @@
1
+ class CLIForge::BinCommand
2
+
3
+ def initialize(path)
4
+ @path = path
5
+ end
6
+
7
+ attr_reader :path
8
+
9
+ def to_s
10
+ "#<#{self.class} #{path}>"
11
+ end
12
+
13
+ def call(arguments)
14
+ exec path, *arguments
15
+ end
16
+
17
+ end
@@ -0,0 +1,38 @@
1
+ class CLIForge::CommandSet
2
+
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def [](command_name)
8
+ if embedded_command = @config.embedded_commands[command_name.to_sym]
9
+ return embedded_command
10
+ end
11
+
12
+ target_bin = @config.bin_name + @config.bin_separator + command_name.to_s
13
+ commands = find_bin_commands(target_bin)
14
+
15
+ # if commands.size > 1
16
+ # TODO: Warn
17
+ # end
18
+
19
+ commands.first
20
+ end
21
+
22
+ private
23
+
24
+ def clean_search_paths
25
+ @config.search_paths.map { |path| File.expand_path(path) }.uniq
26
+ end
27
+
28
+ # Given a glob expression for the bin name, find any matching commands on the
29
+ # search paths.
30
+ def find_bin_commands(glob_expression)
31
+ clean_search_paths.reduce([]) { |paths, search_path|
32
+ paths + Dir["#{search_path}/#{glob_expression}"].map { |found_bin|
33
+ CLIForge::BinCommand.new(found_bin)
34
+ }
35
+ }
36
+ end
37
+
38
+ end
@@ -0,0 +1,66 @@
1
+ class CLIForge::Configuration
2
+
3
+ def initialize
4
+ @default_command = "help"
5
+ @bin_separator = "-"
6
+
7
+ @search_paths = []
8
+ @argument_filters = {}
9
+ @embedded_commands = {}
10
+ end
11
+
12
+ # The name of the binary for this program; used to determine where to find
13
+ # sub-commands, help, etc.
14
+ attr_accessor :bin_name
15
+
16
+ # If no command is given, fall back to this.
17
+ attr_accessor :default_command
18
+
19
+ # Paths to search for sub command binaries.
20
+ attr_accessor :search_paths
21
+
22
+ # Separator to use when searching for subcommands.
23
+ attr_accessor :bin_separator
24
+
25
+
26
+ # Argument Filters
27
+ # ----------------
28
+ # Argument filters are named procs that are given the current command's
29
+ # arguments and have the opportunity to modify them.
30
+ #
31
+ # The return value of the proc is used as the new argument set.
32
+ def register_argument_filter(name, &block)
33
+ @argument_filters[name.to_sym] = block
34
+ end
35
+
36
+ # Remove a previously registered argument filter.
37
+ def remove_argument_filter(name)
38
+ @argument_filters.delete(name.to_sym)
39
+ end
40
+
41
+ # The active argument filters.
42
+ def argument_filters
43
+ @argument_filters.values
44
+ end
45
+
46
+
47
+ # Embedded Sub Commands
48
+ # ---------------------
49
+ # In addition to exposing sub commands via external bins, you can register
50
+ # specific commands directly:
51
+ def register_command(name, command_object)
52
+ @embedded_commands[name.to_sym] = command_object
53
+ end
54
+
55
+ # Remove a previosuly registered command.
56
+ #
57
+ # This _does not_ block an external bin by the same name from being run!
58
+ def remove_command(name)
59
+ @embedded_commands.delete(name.to_sym)
60
+ end
61
+
62
+ # The set of embedded commands. Prefer using `register_command` and
63
+ # `remove_command`.
64
+ attr_reader :embedded_commands
65
+
66
+ end
@@ -0,0 +1,27 @@
1
+ class CLIForge::DefaultConfiguration < CLIForge::Configuration
2
+
3
+ def initialize
4
+ super
5
+
6
+ with_help_command
7
+ with_implicit_help_flags
8
+ end
9
+
10
+ private
11
+
12
+ # Help is one of those global commands that pretty much everyone wants
13
+ def with_help_command
14
+ register_command :help, CLIForge::EmbeddedCommands::Help.new(self)
15
+ end
16
+
17
+ # Passing a help flag to any command results in us calling the help command.
18
+ def with_implicit_help_flags
19
+ register_argument_filter(:help_flags) { |arguments|
20
+ new_arguments = arguments - ["--help", "-h", "/?"]
21
+ new_arguments.unshift("help") if new_arguments.length != arguments.length
22
+
23
+ new_arguments
24
+ }
25
+ end
26
+
27
+ end
@@ -0,0 +1,3 @@
1
+ module CLIForge::EmbeddedCommands
2
+ extend CLIForge::AutoloadConvention
3
+ end
@@ -0,0 +1,11 @@
1
+ class CLIForge::EmbeddedCommands::Help
2
+
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def call(arguments)
8
+ puts "help and stuff"
9
+ end
10
+
11
+ end
@@ -0,0 +1,42 @@
1
+ class CLIForge::Runner
2
+
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def start(arguments)
8
+ arguments = filter_arguments(arguments.dup)
9
+ command_name = pop_command(arguments) || @config.default_command
10
+ command_set = CLIForge::CommandSet.new(@config)
11
+
12
+ unless command = command_set[command_name]
13
+ # TODO: Output!
14
+ command = command_set[@config.default_command]
15
+ end
16
+
17
+ # TODO: Output!
18
+ return 1 unless command
19
+
20
+ command.call(arguments)
21
+ end
22
+
23
+ private
24
+
25
+ def filter_arguments(arguments)
26
+ @config.argument_filters.each do |argument_filter|
27
+ arguments = argument_filter.call(arguments)
28
+ end
29
+
30
+ # TODO: Validate args
31
+
32
+ arguments
33
+ end
34
+
35
+ def pop_command(arguments)
36
+ index = arguments.find_index { |a| !a.start_with? "-" }
37
+ return unless index
38
+
39
+ arguments.delete_at(index)
40
+ end
41
+
42
+ end
@@ -1,3 +1,3 @@
1
1
  module CLIForge
2
- VERSION = "0.0.0"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,13 @@
1
+ shared_context "fixture config" do
2
+
3
+ let(:config) {
4
+ CLIForge::Configuration.new.tap do |config|
5
+ config.bin_name = "foo"
6
+
7
+ config.search_paths << File.join(FIXTURE_ROOT, "bins")
8
+ config.search_paths << File.join(FIXTURE_ROOT, "bins2")
9
+ config.search_paths << File.join(FIXTURE_ROOT, "bins3")
10
+ end
11
+ }
12
+
13
+ end
@@ -0,0 +1,5 @@
1
+ module Fixtures; end
2
+
3
+ module Fixtures::AutoloadConvention
4
+ ABCOneTwoThree = :you_can_count!
5
+ end
File without changes
File without changes
File without changes
File without changes
@@ -16,7 +16,8 @@ end
16
16
 
17
17
  Spork.prefork do
18
18
  # Allow requires relative to the spec dir
19
- SPEC_ROOT = File.expand_path("..", __FILE__)
19
+ SPEC_ROOT = File.expand_path("..", __FILE__)
20
+ FIXTURE_ROOT = File.join(SPEC_ROOT, "fixtures")
20
21
  $LOAD_PATH << SPEC_ROOT
21
22
 
22
23
  require "rspec"
@@ -40,6 +40,12 @@ describe CLIForge::AutoloadConvention, "#const_missing" do
40
40
  expect(namespace::ALLCAPS).to eq(:yelling)
41
41
  end
42
42
 
43
+ it "should split ACRONYMSEndingWithRegularNames" do
44
+ namespace.should_receive(:require).with("fixtures/autoload_convention/abc_one_two_three").and_call_original
45
+
46
+ expect(namespace::ABCOneTwoThree).to eq(:you_can_count!)
47
+ end
48
+
43
49
  it "shouldn't pick up constants in parent namespaces" do
44
50
  namespace.should_receive(:require).with("fixtures/autoload_convention/string").and_call_original
45
51
 
@@ -0,0 +1,38 @@
1
+ describe CLIForge, ".caller_path" do
2
+
3
+ def spec_caller_line
4
+ caller.first
5
+ end
6
+
7
+ it "should extract the current platform's stack format" do
8
+ caller_line = spec_caller_line
9
+ path = described_class.caller_path(caller_line)
10
+
11
+ expect(path).to eq(File.dirname(__FILE__)), "Extracted #{path.inspect} from #{caller_line.inspect}"
12
+ end
13
+
14
+ it "should extract paths from well formed MRI stack lines" do
15
+ path = described_class.caller_path("/usr/bin/foo:6:in `<main>'")
16
+
17
+ expect(path).to eq("/usr/bin")
18
+ end
19
+
20
+ it "should expand relative paths" do
21
+ path = described_class.caller_path("bin/bar:6:in `<main>'")
22
+
23
+ expect(path).to eq(File.expand_path("bin"))
24
+ end
25
+
26
+ it "should not freak out if given nil" do
27
+ path = described_class.caller_path(nil)
28
+
29
+ expect(path).to eq(nil)
30
+ end
31
+
32
+ it "should not expose fake paths as the current path" do
33
+ path = described_class.caller_path("(irb):3:in `irb_binding'")
34
+
35
+ expect(path).to eq(nil)
36
+ end
37
+
38
+ end
@@ -0,0 +1,17 @@
1
+ describe CLIForge, ".guess_bin_name" do
2
+
3
+ it "should return the basename of $0" do
4
+ old_name = $0
5
+
6
+ begin
7
+ $0 = "/foo/bar/baz-biff"
8
+ expect(described_class.send(:guess_bin_name)).to eq("baz-biff")
9
+
10
+ ensure
11
+ $0 = old_name
12
+ end
13
+ end
14
+
15
+ it "should freak out if we don't have a reasonable $0"
16
+
17
+ end
@@ -0,0 +1,93 @@
1
+ describe CLIForge, ".start" do
2
+
3
+ let(:mock_runner) {
4
+ double("CLIForge::Runner").tap do |runner|
5
+ runner.stub(:start)
6
+ end
7
+ }
8
+
9
+ before(:each) do
10
+ CLIForge::Runner.stub(:new) { |config|
11
+ @config = config
12
+ mock_runner
13
+ }
14
+ end
15
+
16
+ # Stub out our environment
17
+ around(:each) do |spec|
18
+ old_path, old_argv = ENV["PATH"], ARGV
19
+
20
+ begin
21
+ spec.run
22
+ ensure
23
+ ENV["PATH"] = old_path
24
+ Object.send(:remove_const, :ARGV)
25
+ ::ARGV = old_argv
26
+ end
27
+ end
28
+
29
+
30
+ it "should not require any arguments" do
31
+ mock_runner.should_receive(:start)
32
+ described_class.should_receive(:guess_bin_name)
33
+
34
+ described_class.start
35
+ end
36
+
37
+ it "should pass the configuration to the given block" do
38
+ mock_runner.should_receive(:start)
39
+ described_class.should_not_receive(:guess_bin_name)
40
+
41
+ described_class.start do |config|
42
+ config.bin_name = "foo-bar"
43
+ end
44
+
45
+ expect(@config.bin_name).to eq("foo-bar")
46
+ end
47
+
48
+ describe "special-cased configuration properties" do
49
+
50
+ it "should consume the passed name if given" do
51
+ mock_runner.should_receive(:start)
52
+ described_class.should_not_receive(:guess_bin_name)
53
+
54
+ described_class.start("awesomesauce")
55
+
56
+ expect(@config.bin_name).to eq("awesomesauce")
57
+ end
58
+
59
+ it "should extract the caller path" do
60
+ ENV["PATH"] = ""
61
+
62
+ described_class.start
63
+
64
+ expect(@config.search_paths).to eq([
65
+ File.dirname(__FILE__)
66
+ ])
67
+ end
68
+
69
+ it "should pull search paths in order from PATH" do
70
+ ENV["PATH"] = "/usr/bin:/usr/local/bin:/foo/bar:/usr/bin"
71
+ described_class.stub(:caller_path) { nil }
72
+
73
+ described_class.start
74
+
75
+ expect(@config.search_paths).to eq([
76
+ "/usr/bin",
77
+ "/usr/local/bin",
78
+ "/foo/bar"
79
+ ])
80
+ end
81
+
82
+ it "should extract ARGV" do
83
+ Object.send(:remove_const, :ARGV)
84
+ ::ARGV = ["my-bin", "sub-command", "--stuff"]
85
+
86
+ mock_runner.should_receive(:start).with(["my-bin", "sub-command", "--stuff"])
87
+
88
+ described_class.start
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,55 @@
1
+ describe CLIForge::CommandSet, "#[]" do
2
+ include_context "fixture config"
3
+
4
+ subject {
5
+ described_class.new(config)
6
+ }
7
+
8
+ it "should return nil if no command was found" do
9
+ expect(subject[:missing_command]).to eq(nil)
10
+ end
11
+
12
+ describe "bin commands" do
13
+
14
+ it "should return the first matching command on the search paths" do
15
+ command = subject[:bar]
16
+
17
+ expect(command).to be_a(CLIForge::BinCommand)
18
+ expect(command.path).to eq(File.join(FIXTURE_ROOT, "bins", "foo-bar"))
19
+ end
20
+
21
+ it "should search subsequent search paths" do
22
+ command = subject[:too]
23
+
24
+ expect(command).to be_a(CLIForge::BinCommand)
25
+ expect(command.path).to eq(File.join(FIXTURE_ROOT, "bins2", "foo-too"))
26
+ end
27
+
28
+ it "should accept strings" do
29
+ command = subject["bar"]
30
+
31
+ expect(command).to be_a(CLIForge::BinCommand)
32
+ expect(command.path).to eq(File.join(FIXTURE_ROOT, "bins", "foo-bar"))
33
+ end
34
+
35
+ end
36
+
37
+ describe "embedded commands" do
38
+
39
+ it "should return embedded commands" do
40
+ command = double("TestCommand")
41
+ config.register_command("my-command", command)
42
+
43
+ expect(subject["my-command"]).to be(command)
44
+ end
45
+
46
+ it "should prioritize embedded commands" do
47
+ command = double("ABetterBar")
48
+ config.register_command(:bar, command)
49
+
50
+ expect(subject[:bar]).to be(command)
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,26 @@
1
+ describe CLIForge::Configuration, "#register_argument_filter" do
2
+
3
+ it "should register the filter" do
4
+ subject.register_argument_filter(:hi) { "hi" }
5
+
6
+ expect(subject.argument_filters.size).to eq(1)
7
+ expect(subject.argument_filters[0].call).to eq("hi")
8
+ end
9
+
10
+ it "should allow you to register over existing commands" do
11
+ subject.register_argument_filter(:hi) { "hi" }
12
+ subject.register_argument_filter(:hi) { "bye" }
13
+
14
+ expect(subject.argument_filters.size).to eq(1)
15
+ expect(subject.argument_filters[0].call).to eq("bye")
16
+ end
17
+
18
+ it "should coerce string names to symbols" do
19
+ subject.register_argument_filter("hi") { "hi" }
20
+ subject.register_argument_filter(:hi) { "bye" }
21
+
22
+ expect(subject.argument_filters.size).to eq(1)
23
+ expect(subject.argument_filters[0].call).to eq("bye")
24
+ end
25
+
26
+ end
@@ -0,0 +1,26 @@
1
+ describe CLIForge::Configuration, "#register_command" do
2
+
3
+ it "should register the command" do
4
+ command = double("TestCommand")
5
+ subject.register_command(:foo, command)
6
+
7
+ expect(subject.embedded_commands[:foo]).to be(command)
8
+ end
9
+
10
+ it "should coerce string names to symbols" do
11
+ command = double("TestCommand")
12
+ subject.register_command("foo", command)
13
+
14
+ expect(subject.embedded_commands[:foo]).to be(command)
15
+ end
16
+
17
+ it "should allow you to register over existing commands" do
18
+ command1 = double("TestCommand1")
19
+ command2 = double("TestCommand2")
20
+ subject.register_command(:foo, command1)
21
+ subject.register_command(:foo, command2)
22
+
23
+ expect(subject.embedded_commands[:foo]).to be(command2)
24
+ end
25
+
26
+ end
@@ -0,0 +1,29 @@
1
+ describe CLIForge::Configuration, "#remove_argument_filter" do
2
+
3
+ it "should remove the argument filter" do
4
+ expect(subject.argument_filters).to eq([])
5
+
6
+ subject.register_argument_filter(:hi) { "hi" }
7
+ subject.remove_argument_filter(:hi)
8
+
9
+ expect(subject.argument_filters).to eq([])
10
+ end
11
+
12
+ it "should not freak out if the argument filter doesn't exist" do
13
+ expect(subject.argument_filters).to eq([])
14
+
15
+ subject.remove_argument_filter(:bar)
16
+
17
+ expect(subject.argument_filters).to eq([])
18
+ end
19
+
20
+ it "should coerce string names to symbols" do
21
+ expect(subject.argument_filters).to eq([])
22
+
23
+ subject.register_argument_filter(:hi) { "hi" }
24
+ subject.remove_argument_filter("hi")
25
+
26
+ expect(subject.argument_filters).to eq([])
27
+ end
28
+
29
+ end
@@ -0,0 +1,30 @@
1
+ describe CLIForge::Configuration, "#remove_command" do
2
+
3
+ it "should remove the command" do
4
+ command = double("TestCommand")
5
+ subject.register_command(:foo, command)
6
+ subject.remove_command(:foo)
7
+
8
+ expect(subject.embedded_commands[:foo]).to eq(nil)
9
+ end
10
+
11
+ it "should not freak out if the command doesn't exist" do
12
+ expect(subject.embedded_commands[:bar]).to eq(nil)
13
+
14
+ subject.remove_command(:bar)
15
+
16
+ expect(subject.embedded_commands[:bar]).to eq(nil)
17
+ end
18
+
19
+ it "should coerce string names to symbols" do
20
+ expect(subject.embedded_commands[:bar]).to eq(nil)
21
+
22
+ command = double("TestCommand")
23
+ subject.register_command(:bar, command)
24
+ subject.remove_command("bar")
25
+
26
+ expect(subject.embedded_commands[:bar]).to eq(nil)
27
+ expect(subject.embedded_commands["bar"]).to eq(nil)
28
+ end
29
+
30
+ end
@@ -0,0 +1,28 @@
1
+ describe CLIForge::Runner, "#start" do
2
+ include_context "fixture config"
3
+
4
+ subject {
5
+ described_class.new(config)
6
+ }
7
+
8
+ let(:default_command) {
9
+ double("DefaultCommand").tap do |command|
10
+ command.stub(:call) { "Called and stuff" }
11
+ end
12
+ }
13
+
14
+ describe "edge cases" do
15
+
16
+ it "should not modify the input arguments" do
17
+ config.register_command(:default, default_command)
18
+
19
+ arguments = ["default", "and", "stuff"]
20
+
21
+ subject.start(arguments)
22
+
23
+ expect(arguments).to eq(["default", "and", "stuff"])
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -8,7 +8,7 @@ namespace :spec do
8
8
  Rake::Task["spec"].execute
9
9
 
10
10
  if RUBY_PLATFORM.include? "darwin"
11
- `open #{File.join(PROJECT_PATH, "coverage", "index.html")}`
11
+ `open #{File.join(PROJECT_ROOT, "coverage", "index.html")}`
12
12
  end
13
13
 
14
14
  ENV["COVERAGE"] = prev
@@ -1,30 +1,21 @@
1
1
  namespace :spec do
2
2
 
3
- def mutant_supported?
4
- return false unless RUBY_VERSION.start_with?("1.9")
5
-
6
- begin
7
- return false unless RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx"
8
- rescue NameError
9
- return false
10
- end
11
-
12
- true
13
- end
14
-
15
3
  desc "Runs tests with code mutation"
16
4
  task :mutate, [:focus_on] do |t, args|
17
5
  next unless mutant_supported?
18
6
 
19
7
  require "cli_forge"
20
8
  require "mutant"
9
+ require "mutant/constants"
10
+ require "mutant/mutation/filter/regexp"
21
11
 
22
12
  # You can focus on a particular symbol/method by passing it to the task:
23
13
  # rake spec:mutate[AutoloadConvention#const_missing], for example.
24
14
  if args.focus_on
25
- matcher = Mutant::Matcher.from_string("::CLIForge::#{args.focus_on}")
15
+ matcher = matcher_for_filter(args.focus_on)
16
+ # Otherwise we're doing a full mutation suite
26
17
  else
27
- matcher = Mutant::Matcher::ObjectSpace.new(/\ACLIForge(::|#|\.).+\Z/)
18
+ matcher = all_matcher
28
19
  end
29
20
 
30
21
  # Mutant doesn't have a public scripting API yet; so we're cheating.
@@ -37,7 +28,7 @@ namespace :spec do
37
28
  :strategy => Mutant::Strategy::Rspec::DM2.new(config),
38
29
  :killer => Mutant::Killer::Rspec,
39
30
  :matcher => matcher,
40
- :filter => Mutant::Mutation::Filter::ALL,
31
+ :filter => filter_for_specs,
41
32
  :reporter => Mutant::Reporter::CLI.new(config)
42
33
  )
43
34
 
@@ -47,4 +38,120 @@ namespace :spec do
47
38
  end
48
39
  end
49
40
 
41
+ def mutant_supported?
42
+ return false unless RUBY_VERSION.start_with?("1.9")
43
+ # TODO: Rubinius crashes under mutant:
44
+ # https://github.com/rubinius/rubinius/issues/2186
45
+ return false if RUBY_ENGINE == "rbx"
46
+
47
+ begin
48
+ return false unless RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx"
49
+ rescue NameError
50
+ return false
51
+ end
52
+
53
+ true
54
+ end
55
+
56
+ # Also preloads the related constants
57
+ def matcher_for_filter(filter)
58
+ # Method on CLIForge?
59
+ if filter.start_with?(".") || filter.start_with?("#")
60
+ matcher = Mutant::Matcher.from_string("::CLIForge#{filter}")
61
+
62
+ # Or regular constant?
63
+ else
64
+ matcher = Mutant::Matcher.from_string("::CLIForge::#{filter}")
65
+ # Force that symbol to load
66
+ const = CLIForge
67
+ filter[/^[^#\.]+/].split("::").each do |const_name|
68
+ const = const.const_get(const_name.to_sym)
69
+ end
70
+ end
71
+
72
+ matcher
73
+ end
74
+
75
+ def all_matcher
76
+ # Force everything to load
77
+ Dir["#{PROJECT_ROOT}/lib/cli_forge/**/*.rb"].each do |path|
78
+ require path[/lib.(cli_forge..+)\.rb$/, 1]
79
+ end
80
+
81
+ unit_folders = Dir["spec/unit/**/*/"].map { |f| f[/^spec.unit.(.+)./, 1] }
82
+ unit_constants = unit_folders.map do |folder|
83
+ folder.gsub(File::SEPARATOR, "::").gsub("_", "").downcase
84
+ end
85
+
86
+ Mutant::Matcher::ObjectSpace.new(/^CLIForge/)
87
+ end
88
+
89
+ def spec_filter_klass
90
+ # Avoid loading w/ rake
91
+ filter_klass = Class.new(Mutant::Mutation::Filter)
92
+ filter_klass.class_eval do
93
+ def initialize(regexp)
94
+ @regexp = regexp
95
+ end
96
+
97
+ def match?(mutation)
98
+ @regexp =~ mutation.subject.matcher.identification
99
+ end
100
+ end
101
+
102
+ filter_klass
103
+ end
104
+
105
+ def filter_for_specs
106
+ # Only run mutations for methods that we have specs for. Basic code
107
+ # coverage can cover simple cases.
108
+ spec_symbols = Dir["#{PROJECT_ROOT}/spec/unit/**/*_spec.rb"].map { |path|
109
+ path_to_symbol(path[/spec.unit.(.+)_spec\.rb$/, 1])
110
+ }
111
+ safe_symbols = spec_symbols.map { |s| Regexp.escape(s) }
112
+
113
+ spec_filter_klass.new(/^(#{safe_symbols.join("|")})$/i)
114
+ end
115
+
116
+ # convert a spec path back to a (case insensitive) symbol
117
+ def path_to_symbol(spec_path)
118
+ parts = spec_path.split(/[\/\\]/)
119
+ method = symbolicate_method_name(parts.pop)
120
+ if parts.last == "class_methods"
121
+ method = ".#{method}"
122
+ parts.pop
123
+ else
124
+ method = "##{method}"
125
+ end
126
+
127
+ parts.join("::").gsub("_", "") + method
128
+ end
129
+
130
+ # https://github.com/mbj/mutant/blob/master/lib/mutant/constants.rb
131
+ def postfix_expansions
132
+ # @postfix_expansions ||= Mutant::METHOD_POSTFIX_EXPANSIONS.invert
133
+ @postfix_expansions ||= {
134
+ '_predicate' => '?',
135
+ '_writer' => '=',
136
+ '_bang' => '!'
137
+ }
138
+ end
139
+
140
+ def operator_expansions
141
+ @operator_expansions ||= Mutant::OPERATOR_EXPANSIONS.invert
142
+ end
143
+
144
+ def symbolicate_method_name(method_name)
145
+ operator = operator_expansions[method_name.to_sym]
146
+ return operator.to_s if operator
147
+
148
+ postfix_expansions.each do |postfix, symbol|
149
+ if method_name.end_with? postfix
150
+ method_name = method_name[0...-postfix.size] + symbol.to_s
151
+ end
152
+ end
153
+
154
+ method_name
155
+ end
156
+
50
157
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cli-forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.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-03-02 00:00:00.000000000 Z
12
+ date: 2013-03-07 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: A library for building CLI tools that are intended to be easily extensible
15
15
  (git style)
@@ -31,20 +31,42 @@ files:
31
31
  - lib/cli-forge.rb
32
32
  - lib/cli_forge.rb
33
33
  - lib/cli_forge/autoload_convention.rb
34
+ - lib/cli_forge/bin_command.rb
35
+ - lib/cli_forge/command_set.rb
36
+ - lib/cli_forge/configuration.rb
37
+ - lib/cli_forge/default_configuration.rb
38
+ - lib/cli_forge/embedded_commands.rb
39
+ - lib/cli_forge/embedded_commands/help.rb
40
+ - lib/cli_forge/runner.rb
34
41
  - lib/cli_forge/version.rb
42
+ - spec/common/fixture_config.rb
43
+ - spec/fixtures/autoload_convention/abc_one_two_three.rb
35
44
  - spec/fixtures/autoload_convention/allcaps.rb
36
45
  - spec/fixtures/autoload_convention/mismatched.rb
37
46
  - spec/fixtures/autoload_convention/multi_token.rb
38
47
  - spec/fixtures/autoload_convention/single.rb
39
48
  - spec/fixtures/autoload_convention/string.rb
49
+ - spec/fixtures/bins/foo-bar
50
+ - spec/fixtures/bins2/foo-bar
51
+ - spec/fixtures/bins2/foo-too
52
+ - spec/fixtures/bins3/foo-bar
40
53
  - spec/spec_helper.rb
41
54
  - spec/unit/cli_forge/autoload_convention/const_missing_spec.rb
55
+ - spec/unit/cli_forge/class_methods/caller_path_spec.rb
56
+ - spec/unit/cli_forge/class_methods/guess_bin_name_spec.rb
57
+ - spec/unit/cli_forge/class_methods/start_spec.rb
58
+ - spec/unit/cli_forge/command_set/element_reader_spec.rb
59
+ - spec/unit/cli_forge/configuration/register_argument_filter_spec.rb
60
+ - spec/unit/cli_forge/configuration/register_command_spec.rb
61
+ - spec/unit/cli_forge/configuration/remove_argument_filter_spec.rb
62
+ - spec/unit/cli_forge/configuration/remove_command_spec.rb
63
+ - spec/unit/cli_forge/runner/start_spec.rb
42
64
  - tasks/console.rake
43
65
  - tasks/spec.rake
44
66
  - tasks/spec/ci.rake
45
67
  - tasks/spec/coverage.rake
46
68
  - tasks/spec/mutate.rake
47
- homepage: http://github.com/nevir/cli-forge
69
+ homepage: https://github.com/nevir/cli-forge
48
70
  licenses: []
49
71
  post_install_message:
50
72
  rdoc_options: []
@@ -56,12 +78,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
56
78
  - - ! '>='
57
79
  - !ruby/object:Gem::Version
58
80
  version: '0'
81
+ segments:
82
+ - 0
83
+ hash: 850686175948221601
59
84
  required_rubygems_version: !ruby/object:Gem::Requirement
60
85
  none: false
61
86
  requirements:
62
87
  - - ! '>='
63
88
  - !ruby/object:Gem::Version
64
89
  version: '0'
90
+ segments:
91
+ - 0
92
+ hash: 850686175948221601
65
93
  requirements: []
66
94
  rubyforge_project:
67
95
  rubygems_version: 1.8.23