diff --git a/bin/speckle b/bin/speckle new file mode 100755 index 0000000..dee2eb6 --- /dev/null +++ b/bin/speckle @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby + +require File.expand_path('../../lib/speckle/loader', __FILE__) + +module Speckle + + require 'speckle/cli/app' + + app = Speckle::CLI::App.new + app.start(ARGV) + +end diff --git a/lib/speckle/cli/app.rb b/lib/speckle/cli/app.rb new file mode 100644 index 0000000..fe89bf8 --- /dev/null +++ b/lib/speckle/cli/app.rb @@ -0,0 +1,18 @@ +module Speckle + module CLI + + require_relative 'environment' + require_relative 'router' + + class App + def start(args) + env = Environment.new + options = env.load(args) + + router = Router.new + router.route(options.action, options) + end + end + + end +end diff --git a/lib/speckle/cli/controller.rb b/lib/speckle/cli/controller.rb new file mode 100644 index 0000000..06f1eba --- /dev/null +++ b/lib/speckle/cli/controller.rb @@ -0,0 +1,67 @@ +module Speckle + module CLI + + require 'speckle/version' + + class Controller + + def initialize(options, rake_app) + @options = options + @rake_app = rake_app + end + + def rake(task) + @rake_app.invoke_task(task) + end + + def show_version + puts VERSION + end + + def show_help + puts @options.opts + end + + def show_error(msg = @options.error) + puts "Error: #{msg}" + puts + + show_help + end + + def show_invalid_option + show_error @options.error + end + + def show_missing_args + show_error @options.error + end + + def show_parser_error + show_error @options.error + end + + def show_no_spec_dir + show_error '"spec" directory not found' + end + + def compile + rake :compile_tests + end + + def compile_and_test + rake :compile_and_test + end + + def test + rake :test + end + + def watch + puts '--- TODO ---' + end + + end + + end +end diff --git a/lib/speckle/cli/environment.rb b/lib/speckle/cli/environment.rb new file mode 100644 index 0000000..387b25a --- /dev/null +++ b/lib/speckle/cli/environment.rb @@ -0,0 +1,148 @@ +module Speckle + + module CLI + + require 'optparse' + require 'ostruct' + + class Environment + include Loader + + def load(args) + options = OpenStruct.new + options.libs = nil + options.grep_pattern = nil + options.grep_invert = false + options.reporter = 'dot' + options.args = args + options.verbose = false + options.vim = 'vim' + options.cwd = Dir.pwd + options.root_dir = ROOT_DIR + options.speckle_build_dir = "#{ROOT_DIR}/build" + options.speckle_lib_dir = "#{ROOT_DIR}/lib" + options.skip_vimrc = false + options.slow_threshold = 10 + options.colorize = true + options.bail = false + + parser = OptionParser.new do |opts| + options.opts = opts + + opts.banner = "Usage: speckle [options] [file(s) OR directory]" + opts.separator '' + opts.separator 'Options:' + opts.separator '' + + opts.on_head('-c', '--compile', 'Only compile tests') do + options.action = :compile + end + + opts.on_head('-t', '--test', 'Only run tests') do + options.action = :test + end + + opts.on_head('-a', '--all', 'Compile and run tests (default)') do + options.action = :compile_and_test + end + + opts.on('-I', '--libs ', 'Specify additional riml library path(s)') do |libs| + options.libs = libs + end + + opts.on('-g', '--grep ', 'Only run tests matching the pattern') do |pattern| + options.grep_pattern = pattern.strip + end + + opts.on('-i', '--invert', 'Inverts --grep matches') do + options.grep_invert = true + end + + opts.on('-r', '--reporter ', 'Specify the reporter to use (spec, min, dot, tap)') do |reporter| + options.reporter = reporter.strip + end + + opts.on('-b', '--bail', 'Bail on first test failure') do + options.bail = true + end + + opts.on('-w', '--watch', 'Watch tests for changes') do + options.action = :watch + end + + opts.on('-m', '--vim ', 'Vim program used to test, default(vim)') do |vim| + options.vim = vim.strip + end + + opts.on('-s', '--slow-threshold ', Integer, 'Threshold in milliseconds to indicate slow tests') do |ms| + options.slow_threshold = ms + end + + opts.on('-k', '--skip-vimrc', 'Does not load ~/.vimrc file') do + options.skip_vimrc = true + end + + opts.on('-C', '--no-colors', 'Disable color output') do + options.colorize = false + end + + opts.on('-v', '--verbose', 'Display verbose output') do + options.verbose = true + end + + opts.on('-D', '--debug', 'Display debug output') do + options.verbose = true + options.debug = true + end + + opts.on_tail('-V', '--version', 'Print Speckle version') do + options.action = :show_version + end + + opts.on_tail('-h', '--help', 'Print Speckle help') do + options.action = :show_help + end + end + + begin + parser.parse!(args) + + if options.action.nil? + spec_dir = "#{options.cwd}/spec" + if File.directory?(spec_dir) + args << 'spec' + options.action = :compile_and_test + else + options.action = :show_no_spec_dir + end + elsif action_needs_args?(options.action) and args.empty? + spec_dir = "#{options.cwd}/spec" + if File.directory?(spec_dir) + args << 'spec' + options.action = :compile_and_test + end + end + + options.inputs = args + rescue OptionParser::InvalidOption => e + options.error = e + options.action = :show_invalid_option + rescue OptionParser::MissingArgument => e + options.error = e + options.action = :show_missing_args + rescue OptionParser::ParseError => e + options.error = e + options.action = :show_parser_error + end + + options + end + + def action_needs_args?(action) + [:compile_and_test, :compile, :test].include? action + end + end + + + end +end diff --git a/lib/speckle/cli/rake_app.rb b/lib/speckle/cli/rake_app.rb new file mode 100644 index 0000000..24a7a62 --- /dev/null +++ b/lib/speckle/cli/rake_app.rb @@ -0,0 +1,146 @@ +module Speckle + module CLI + + require 'rake' + + class RakeApp + def initialize(options) + @options = options + end + + def inputs + @options.inputs + end + + def verbose + @options.verbose + end + + def debug + @options.debug + end + + def rake + if @rake_app + return @rake_app + end + + configure_rake + Dir.chdir @options.root_dir + + @rake_app = Rake.application + @rake_app.init + @rake_app.load_rakefile + + Dir.chdir @options.cwd + @rake_app + end + + def invoke_task(name) + rake.invoke_task("speckle:#{name.to_s}") + end + + def rake_env(key, value) + unless value.nil? + ENV[key] = if value.is_a?(Array) then value.join(';') else value end + puts "rake_env: #{key} = #{ENV[key]}" if debug + end + end + + def configure_rake + rake_env('TEST_SOURCES', test_sources) + rake_env('TEST_LIBS', test_libs) + rake_env('BUILD_DIR', test_build_dir) + rake_env('TEST_COMPILED', test_compiled) + rake_env('TEST_VIM', @options.vim) + rake_env('TEST_REPORTER', @options.reporter) + rake_env('SLOW_THRESHOLD', @options.slow_threshold.to_s) + rake_env('SKIP_VIMRC', to_int(@options.skip_vimrc)) + rake_env('COLORIZE', to_int(@options.colorize)) + rake_env('BAIL', to_int(@options.bail)) + + if @options.verbose + rake_env('VERBOSE', 'yes') + end + + if @options.debug + rake_env('DEBUG', 'yes') + end + end + + def to_int(option) + option ? '1' : '0' + end + + def test_build_dir + "#{@options.cwd}/build" + end + + def test_compiled + compiled = test_sources.map do |s| + s.ext('vim') + end + + compiled + end + + def test_sources + sources = [] + grep_pattern = @options.grep_pattern + grep_invert = @options.grep_invert + unless grep_pattern.nil? + regex = Regexp.new(grep_pattern) + end + + inputs.each do |input| + if File.directory?(input) + if grep_pattern.nil? + sources << "#{input}/**/*_spec.riml" + else + sources = Dir.glob("#{input}/**/*_spec.riml") + sources.keep_if do |source| + matched = regex.match(source) + if grep_invert + matched = !matched + end + + matched + end + end + else + if grep_pattern.nil? + sources << input + else + matched = regex.match(input) + if grep_invert + matched = !matched + end + sources << input if matched + end + end + end + + sources + end + + def test_libs + input_libs = @options.libs + return nil if input_libs.nil? + + input_libs = input_libs.split(':') + input_libs << 'spec' + if File.directory?(@options.speckle_lib_dir) + input_libs << @options.speckle_lib_dir + end + + libs = [] + input_libs.each do |lib| + libs << File.absolute_path(lib) + end + + libs.join(':') + end + + end + end +end diff --git a/lib/speckle/cli/router.rb b/lib/speckle/cli/router.rb new file mode 100644 index 0000000..4659df0 --- /dev/null +++ b/lib/speckle/cli/router.rb @@ -0,0 +1,16 @@ +module Speckle + module CLI + + require_relative 'rake_app' + require_relative 'controller' + + class Router + def route(action, options) + rake_app = RakeApp.new(options) + controller = Controller.new(options, rake_app) + controller.send(action) + end + end + + end +end diff --git a/lib/speckle/loader.rb b/lib/speckle/loader.rb new file mode 100644 index 0000000..068e07d --- /dev/null +++ b/lib/speckle/loader.rb @@ -0,0 +1,10 @@ +module Speckle + + module Loader + ROOT_DIR = File.expand_path('../../../', __FILE__) + LIB_DIR = File.join(ROOT_DIR, 'lib') + + $:.unshift(LIB_DIR) unless $:.include? LIB_DIR + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..d4b5814 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1 @@ +require File.expand_path(File.dirname(__FILE__) + '/../lib/speckle/loader') diff --git a/spec/speckle/cli/environment_spec.rb b/spec/speckle/cli/environment_spec.rb new file mode 100644 index 0000000..fd4897c --- /dev/null +++ b/spec/speckle/cli/environment_spec.rb @@ -0,0 +1,296 @@ +require 'spec_helper' +require 'speckle/cli/environment' + +RSpec::Matchers.define :yield_action do |expected| + match do |actual| + env = Speckle::CLI::Environment.new + @options = env.load(actual.kind_of?(Array) ? actual : [actual]) + @options.action == expected + end + + failure_message_for_should do |actual| + "expected #{actual} to yield action :#{expected.to_s} but was :#{@options.action}" + end + + failure_message_for_should_not do |actual| + "expected #{actual} to not yield action :#{expected.to_s} but was :#{@options.action}" + end +end + +RSpec::Matchers.define :yield_option do |expected| + match do |actual| + env = Speckle::CLI::Environment.new + @options = env.load(actual.kind_of?(Array) ? actual : [actual]) + @result = @options.send(expected) + @result == true + end + + failure_message_for_should do |actual| + "expected #{actual} to yield option :#{expected.to_s}, but was #{@result}" + end + + failure_message_for_should_not do |*args| + "expected #{actual} to not yield option :#{expected.to_s}, but was #{@result}" + end +end + +RSpec::Matchers.define :have_default_option do |expected| + match do |actual| + env = Speckle::CLI::Environment.new + @options = env.load(actual.kind_of?(Array) ? actual : [actual]) + @options.send(expected) == true + end + + failure_message_for_should do |actual| + "expected #{actual} to have default option :#{expected.to_s}" + end + + failure_message_for_should_not do |actual| + "expected #{actual} to not have default option :#{expected.to_s}" + end +end + +RSpec::Matchers.define :have_default_option_value do |key, value| + match do |actual| + env = Speckle::CLI::Environment.new + @options = env.load(actual.kind_of?(Array) ? actual : [actual]) + @result = @options.send(key) + @result == value + end + + failure_message_for_should do |actual| + "expected #{actual} to have default option #{key} = #{value}, but was #{@result}" + end + + failure_message_for_should_not do |*args| + "expected #{actual} to not have default option #{key} = #{value}, but was #{@result}" + end +end + +RSpec::Matchers.define :yield_option_value do |key, value| + match do |actual| + env = Speckle::CLI::Environment.new + @options = env.load(actual.kind_of?(Array) ? actual : [actual]) + @result = @options.send(key) + @result == value + end + + failure_message_for_should do |actual| + "expected #{actual} to yield option #{key} = #{value} but was '#{@result}'" + end + + failure_message_for_should_not do |actual| + "expected #{actual} to not yield option #{key} = #{value} but was #{options.send(key)}" + end +end + +RSpec::Matchers.define :include_path do |expected| + match do |actual| + env = Speckle::CLI::Environment.new + @options = env.load(actual.kind_of?(Array) ? actual : [actual]) + @result = @options.inputs + @result.include?(expected) + end + + failure_message_for_should do |actual| + "expected #{actual} to include path #{expected}, inputs was #{@result}" + end + + failure_message_for_should_not do |actual| + "expected #{actual} to not include path #{actual}, inputs was #{@result}" + end +end + +module Speckle + module CLI + + describe 'Main options basics' do + + it 'defaults to compile_and_test without args' do + expect('').to yield_action(:compile_and_test) + end + + it 'has action :compile_and_test with -a or --all' do + expect(['-a', 'foo']).to yield_action :compile_and_test + expect(['--all', 'foo']).to yield_action :compile_and_test + end + + it 'has action :compile with -c or --compile' do + expect(['-c', 'foo']).to yield_action :compile + expect(['--compile', 'foo']).to yield_action :compile + end + + it 'has action :test with -t or --test' do + expect(['-t', 'foo']).to yield_action :test + expect(['--test', 'foo']).to yield_action :test + end + + end + + describe 'Source path defaults' do + + it 'has uses spec directory if present' do + expect('').to include_path('spec') + end + + it 'includes spec directory if no files were specified with -a or --all' do + expect('-a').to include_path('spec') + expect('--all').to include_path('spec') + end + + it 'includes spec directory if no files were specified with -c or --compile' do + expect('-c').to include_path('spec') + expect('--compile').to include_path('spec') + end + + it 'includes spec directory if no files were specified with -t or --test' do + expect('-t').to include_path('spec') + expect('--test').to include_path('spec') + end + end + + describe 'Extra options and flags' do + + it 'can load args' do + env = Environment.new + expect(env).to respond_to(:load) + end + + it 'has :show_help action for -h or --help' do + expect('-h').to yield_action(:show_help) + expect('--help').to yield_action(:show_help) + end + + it 'has :show_version action for -V or --version' do + expect('-V').to yield_action :show_version + expect('--version').to yield_action :show_version + end + + it 'has verbose flag for -v or --verbose' do + expect('-v').to yield_option 'verbose' + expect('--verbose').to yield_option 'verbose' + end + + it 'has debug flag for -D or --debug' do + expect('-D').to yield_option 'debug' + expect('--debug').to yield_option 'debug' + end + + it 'does not have colorize flag for -C or --no-colors' do + expect('-C').to_not yield_option 'colorize' + expect('--no-colors').to_not yield_option 'colorize' + end + + it 'has colorize by default' do + expect('').to have_default_option 'colorize' + end + + it 'has does not skip vimrc by default' do + expect('').to_not have_default_option 'skip_vimrc' + end + + it 'does not have skip_vimrc for -k or --skip-vimrc' do + expect('-k').to yield_option 'skip_vimrc' + expect('--skip-vimrc').to yield_option 'skip_vimrc' + end + + it 'has :watch action for -w or --watch' do + expect('-w').to yield_action :watch + expect('--watch').to yield_action :watch + end + + it 'bail by default' do + expect('').to_not have_default_option 'bail' + end + + it 'has bail option by default' do + expect('-b').to yield_option 'bail' + expect('--bail').to yield_option 'bail' + end + + it 'has default vim program' do + expect('').to have_default_option_value('vim', 'vim') + end + + it 'takes specified vim program for -m or --vim' do + expect(['-m', 'gvim']).to yield_option_value('vim', 'gvim') + expect(['--vim', 'gvim']).to yield_option_value('vim', 'gvim') + end + + it 'has a default slow threshold' do + expect('').to have_default_option_value('slow_threshold', 10) + end + + it 'takes slow_threshold for -k or --slow-threshold' do + expect(['-s', '10']).to yield_option_value('slow_threshold', 10) + expect(['--slow-threshold', '10']).to yield_option_value('slow_threshold', 10) + end + + it 'does not have default grep pattern' do + expect('').to_not have_default_option('grep_pattern') + end + + it 'takes grep pattern for -g or --grep' do + expect(['-g', '^foo']).to yield_option_value('grep_pattern', '^foo') + expect(['--grep', '^foo']).to yield_option_value('grep_pattern', '^foo') + end + + it 'does not have default invert grep pattern' do + expect('').to_not have_default_option('grep_invert') + end + + it 'takes invert grep pattern for -i or --invert' do + expect('-i').to yield_option('grep_invert') + expect('--invert').to yield_option('grep_invert') + end + + it 'takes libs from -I or --libs' do + expect(['-I', 'lorem:ipsum:dolor']).to yield_option_value('libs', 'lorem:ipsum:dolor') + end + + it 'has a default reporter' do + expect('').to have_default_option_value('reporter', 'dot') + end + + it 'takes reporter from -r or --reporter' do + expect(['-r', 'min']).to yield_option_value('reporter', 'min') + expect(['--reporter', 'min']).to yield_option_value('reporter', 'min') + end + + end + + describe 'Complete CLI options' do + def env(*args) + env = Environment.new + env.load(args) + end + + it 'works with example#1' do + opts = env('-a', 'foo', '-I', 'lorem:ipsum', '-r', 'min', '-C', '-D') + expect(opts.action).to eq(:compile_and_test) + expect(opts.inputs).to include('foo') + expect(opts.libs).to eq('lorem:ipsum') + expect(opts.reporter).to eq('min') + expect(opts.debug).to eq(true) + end + + it 'works with example#2' do + opts = env('foo', '-r', 'dot', '-I', 'lorem:ipsum', '-v') + expect(opts.action).to eq(:compile_and_test) + expect(opts.inputs).to include('foo') + expect(opts.libs).to eq('lorem:ipsum') + expect(opts.reporter).to eq('dot') + expect(opts.verbose).to eq(true) + end + + it 'works with example#3' do + opts = env('-r', 'tap', '-t', 'my_specs', '-I', 'stuff', '-D') + expect(opts.action).to eq(:test) + expect(opts.inputs).to include('my_specs') + expect(opts.reporter).to eq('tap') + expect(opts.libs).to eq('stuff') + end + + end + end +end