cli-forge 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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