From bf1edd84bfe5b1a4cc401c22754bce3ad60cdd41 Mon Sep 17 00:00:00 2001 From: Nathan Kleyn Date: Thu, 1 Oct 2015 16:12:34 +0100 Subject: [PATCH] Remove all global state, turn Env into a monad. Removed the global state from the following items: * Turned Env into a passable object, encapsulating the RWS monad pattern. * Removed TaskEnv. * Got rid of the multiton Project pattern. * Got rid of the inherited hooks for Plugins and TaskSets, store in the Env as required now. * Cleaned up Plugin. * Addd Shantyconfig for Ruby based configuration. --- .rubocop.yml | 15 -- .shanty.yml | 0 Shantyconfig | 5 + lib/shanty.rb | 24 +- lib/shanty/cli.rb | 12 +- lib/shanty/config.rb | 47 ---- lib/shanty/env.rb | 90 +++---- lib/shanty/{project_tree.rb => file_tree.rb} | 2 +- .../{project_sorter.rb => graph_factory.rb} | 30 ++- lib/shanty/logger.rb | 15 ++ lib/shanty/plugin.rb | 81 +++--- lib/shanty/plugin_dsl.rb | 17 ++ lib/shanty/plugins/bundler_plugin.rb | 18 +- lib/shanty/plugins/cucumber_plugin.rb | 31 +-- lib/shanty/plugins/rspec_plugin.rb | 31 +-- lib/shanty/plugins/rubocop_plugin.rb | 14 +- lib/shanty/plugins/rubygem_plugin.rb | 46 ++-- lib/shanty/plugins/shantyfile_plugin.rb | 17 +- lib/shanty/project.rb | 46 ++-- lib/shanty/task_env.rb | 20 -- lib/shanty/task_set.rb | 18 +- lib/shanty/task_sets/basic_task_set.rb | 90 +++---- shanty.gemspec | 2 - spec/fixtures/test_task_set.rb | 17 +- spec/fixtures/test_unused_plugin.rb | 8 +- spec/lib/shanty/artifact_spec.rb | 63 +++-- spec/lib/shanty/cli_spec.rb | 231 ++++++++++-------- spec/lib/shanty/config_spec.rb | 79 ------ spec/lib/shanty/env_spec.rb | 162 ++++-------- spec/lib/shanty/file_tree_spec.rb | 39 +++ spec/lib/shanty/graph_factory_spec.rb | 48 ++++ spec/lib/shanty/graph_spec.rb | 130 ++++++---- spec/lib/shanty/logger_spec.rb | 10 + spec/lib/shanty/plugin_dsl_spec.rb | 43 ++++ spec/lib/shanty/plugin_spec.rb | 196 +++------------ .../lib/shanty/plugins/bundler_plugin_spec.rb | 33 ++- .../shanty/plugins/cucumber_plugin_spec.rb | 65 ++--- spec/lib/shanty/plugins/rspec_plugin_spec.rb | 66 ++--- .../lib/shanty/plugins/rubocop_plugin_spec.rb | 33 ++- .../lib/shanty/plugins/rubygem_plugin_spec.rb | 82 +++---- .../shanty/plugins/shantyfile_plugin_spec.rb | 54 ++-- spec/lib/shanty/project_sorter_spec.rb | 24 -- spec/lib/shanty/project_spec.rb | 220 ++++++++--------- spec/lib/shanty/project_tree_spec.rb | 42 ---- spec/lib/shanty/task_env_spec.rb | 21 -- spec/lib/shanty/task_set_spec.rb | 108 ++++---- spec/spec_helper.rb | 60 +---- spec/support/contexts/plugin.rb | 18 ++ spec/support/contexts/workspace.rb | 24 ++ spec/support/matchers/plugin.rb | 14 +- 50 files changed, 1165 insertions(+), 1396 deletions(-) delete mode 100644 .shanty.yml create mode 100644 Shantyconfig delete mode 100644 lib/shanty/config.rb rename lib/shanty/{project_tree.rb => file_tree.rb} (99%) rename lib/shanty/{project_sorter.rb => graph_factory.rb} (70%) create mode 100644 lib/shanty/logger.rb create mode 100644 lib/shanty/plugin_dsl.rb delete mode 100644 lib/shanty/task_env.rb delete mode 100644 spec/lib/shanty/config_spec.rb create mode 100644 spec/lib/shanty/file_tree_spec.rb create mode 100644 spec/lib/shanty/graph_factory_spec.rb create mode 100644 spec/lib/shanty/logger_spec.rb create mode 100644 spec/lib/shanty/plugin_dsl_spec.rb delete mode 100644 spec/lib/shanty/project_sorter_spec.rb delete mode 100644 spec/lib/shanty/project_tree_spec.rb delete mode 100644 spec/lib/shanty/task_env_spec.rb create mode 100644 spec/support/contexts/plugin.rb create mode 100644 spec/support/contexts/workspace.rb diff --git a/.rubocop.yml b/.rubocop.yml index d0b934d..520aa61 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,18 +5,3 @@ LineLength: Metrics/ModuleLength: Max: 150 - -# This is a stupid lint. Yes, module_function is more explanatory than extend self for most use cases. However, the -# former does not work if you want the methods to be able to call private methods; the latter does. See -# https://practicingruby.com/articles/ruby-and-the-singleton-pattern-dont-get-along for a detailed explanation of the -# problem. -ModuleFunction: - Enabled: False - -# FIXME: Only enabling because we are using the singleton pattern still for the -# Env and TaskEnv classes and they store memoized values that need to be -# preserved even after mixing in. Without class vars, this does not work. We -# plan to refactor these singletons out and into class instances that are passed -# around instead as needed. -ClassVars: - Enabled: False diff --git a/.shanty.yml b/.shanty.yml deleted file mode 100644 index e69de29..0000000 diff --git a/Shantyconfig b/Shantyconfig new file mode 100644 index 0000000..716f6f3 --- /dev/null +++ b/Shantyconfig @@ -0,0 +1,5 @@ +require 'shanty/plugins/bundler_plugin' +require 'shanty/plugins/cucumber_plugin' +require 'shanty/plugins/rspec_plugin' +require 'shanty/plugins/rubocop_plugin' +require 'shanty/plugins/rubygem_plugin' diff --git a/lib/shanty.rb b/lib/shanty.rb index 04378d3..9d0f3d7 100644 --- a/lib/shanty.rb +++ b/lib/shanty.rb @@ -1,30 +1,38 @@ require 'i18n' require 'pathname' - require 'shanty/cli' require 'shanty/env' require 'shanty/graph' - -# Require all plugins and task sets. -Dir[File.join(__dir__, 'shanty', '{plugins,task_sets}', '*.rb')].each { |f| require f } +require 'shanty/graph_factory' module Shanty # Main shanty class class Shanty - include Env - # This is the root directory where the Shanty gem is located. Do not confuse this with the root of the repository # in which Shanty is operating, which is available via the TaskEnv class. GEM_ROOT = File.expand_path(File.join(__dir__, '..')) + def initialize + @env = Env.new + end + def start! setup_i18n - require! - Cli.new(TaskSet.task_sets).run + execute_shantyconfig! + Cli.new(@env.task_sets, @env, graph).run end private + def execute_shantyconfig! + config_path = File.join(@env.root, Env::CONFIG_FILE) + @env.instance_eval(File.read(config_path), config_path) + end + + def graph + GraphFactory.new(@env).graph + end + def setup_i18n I18n.enforce_available_locales = true I18n.load_path = Dir[File.join(GEM_ROOT, 'translations', '*.yml')] diff --git a/lib/shanty/cli.rb b/lib/shanty/cli.rb index 1254595..969311f 100644 --- a/lib/shanty/cli.rb +++ b/lib/shanty/cli.rb @@ -3,8 +3,6 @@ require 'shanty/info' require 'shanty/task_set' require 'shanty/env' -require 'shenanigans/hash/to_ostruct' -require 'deep_merge' module Shanty # Public: Handle the CLI interface between the user and the registered tasks @@ -16,8 +14,10 @@ class Cli CONFIG_FORMAT = '[plugin]:[key] [value]' - def initialize(task_sets) + def initialize(task_sets, env, graph) @task_sets = task_sets + @env = env + @graph = graph end def tasks @@ -42,7 +42,7 @@ def run def global_config global_option('-c', '--config [CONFIG]', "Add config via command line in the format #{CONFIG_FORMAT}") do |config| if (match = config.match(/(?\S+):(?\S+)\s+(?\S+)/)) - Env.config.merge!(match[:plugin] => { match[:key] => match[:value] }) + @env.config[match[:plugin].to_sym][match[:key].to_sym] = match[:value] else fail(I18n.t('cli.invalid_config_param', actual: config, expected: CONFIG_FORMAT)) end @@ -73,14 +73,14 @@ def add_options_to_command(task, command) def add_action_to_command(name, task, command) command.action do |_, options| task = tasks[name] - options.default(Hash[defaults_for_options(task)]) + options.default(defaults_for_options(task).to_h) enforce_required_options(task, options) execute_task(name, task, options) end end def execute_task(name, task, options) - klass = task[:klass].new + klass = task[:klass].new(@env, @graph) arity = klass.method(name).arity args = (arity >= 1 ? [options] : []) klass.send(name, *args) diff --git a/lib/shanty/config.rb b/lib/shanty/config.rb deleted file mode 100644 index b1f9e1f..0000000 --- a/lib/shanty/config.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'shenanigans/hash/to_ostruct' -require 'deep_merge' - -module Shanty - # Public: Configuration class for shanty - class Config - CONFIG_FILE = '.shanty.yml' - - def initialize(root, environment) - @root = root - @environment = environment - end - - def merge!(new_config) - @config = config_hash.clone.deep_merge(new_config) - end - - def [](key) - to_ostruct[key] - end - - def method_missing(m) - to_ostruct.send(m) || OpenStruct.new - end - - def respond_to?(method_sym, include_private = false) - super || to_ostruct.respond_to?(method_sym, include_private) - end - - def to_ostruct - config_hash.to_ostruct - end - - private - - def config_hash - return @config unless @config.nil? - return @config = {} unless File.exist?(config_path) - config = YAML.load_file(config_path) || {} - @config = config[@environment] || {} - end - - def config_path - "#{@root}/#{CONFIG_FILE}" - end - end -end diff --git a/lib/shanty/env.rb b/lib/shanty/env.rb index 8ea4abc..a276bbe 100644 --- a/lib/shanty/env.rb +++ b/lib/shanty/env.rb @@ -1,84 +1,60 @@ require 'i18n' require 'logger' require 'pathname' +require 'shanty/file_tree' +require 'shanty/plugin' +require 'shanty/plugins/shantyfile_plugin' +require 'shanty/task_set' +require 'shanty/task_sets/basic_task_set' require 'yaml' -require 'shenanigans/hash/to_ostruct' - -require 'shanty/config' -require 'shanty/project_tree' module Shanty - # - module Env - # Idiom to allow singletons that can be mixed in: http://ozmm.org/posts/singin_singletons.html - extend self - - CONFIG_FILE = '.shanty.yml' + # RWS Monad pattern. + class Env + CONFIG_FILE = 'Shantyconfig' - # This must be defined first due to being a class var that isn't first - # first accessed with ||=. - @@config = nil - - def clear! - @@logger = nil - @@environment = nil - @@build_number = nil - @@root = nil - @@config = nil - @@project_tree = nil + def root + @root ||= find_root end - def require! - Dir.chdir(root) do - (config['require'] || []).each do |path| - requires_in_path(path).each { |f| require File.join(root, f) } - end - end + def file_tree + @file_tree ||= FileTree.new(root) end - def logger - @@logger ||= Logger.new($stdout).tap do |logger| - logger.formatter = proc do |_, datetime, _, msg| - "#{datetime}: #{msg}\n" - end - end + def plugins + @plugins ||= [Plugins::ShantyfilePlugin] end - def environment - @@environment ||= ENV['SHANTY_ENV'] || 'local' + def task_sets + @task_sets ||= [TaskSets::BasicTaskSet] end - def build_number - @@build_number ||= (ENV['SHANTY_BUILD_NUMBER'] || 1).to_i + def projects + @projects ||= {} end - def root - @@root ||= find_root + def config + @config ||= Hash.new { |h, k| h[k] = {} } end - def project_tree - @@project_tree ||= ProjectTree.new(root) - end + def require(*args) + new_plugins = [] + new_task_sets = [] - def config - @@config ||= Config.new(root, environment) - rescue RuntimeError - # Create config object without .shanty.yml if the project root cannot be resolved - @@config ||= Config.new(nil, environment) - end + Plugin.send(:define_singleton_method, :inherited) { |klass| new_plugins << klass } + TaskSet.send(:define_singleton_method, :inherited) { |klass| new_task_sets << klass } - private + super(*args) - def requires_in_path(path) - if File.directory?(path) - Dir[File.join(path, '**', '*.rb')] - elsif File.exist?(path) - [path] - else - Dir[path] - end + Plugin.singleton_class.send(:remove_method, :inherited) + TaskSet.singleton_class.send(:remove_method, :inherited) + + plugins.concat(new_plugins) + task_sets.concat(new_task_sets) end + private + def find_root fail I18n.t('missing_root', config_file: CONFIG_FILE) if root_dir.nil? root_dir diff --git a/lib/shanty/project_tree.rb b/lib/shanty/file_tree.rb similarity index 99% rename from lib/shanty/project_tree.rb rename to lib/shanty/file_tree.rb index 167e787..8814453 100644 --- a/lib/shanty/project_tree.rb +++ b/lib/shanty/file_tree.rb @@ -5,7 +5,7 @@ module Shanty # # FIXME: The ignores are not implemented yet, this work has been recorded in # issue #9 (https://github.com/shantytown/shanty/issues/9). - class ProjectTree + class FileTree # Allow double globbing, and matching hidden files. GLOB_FLAGS = File::FNM_EXTGLOB | File::FNM_DOTMATCH # FIXME: Basic ignores until the .gitignore file can be loaded instead. diff --git a/lib/shanty/project_sorter.rb b/lib/shanty/graph_factory.rb similarity index 70% rename from lib/shanty/project_sorter.rb rename to lib/shanty/graph_factory.rb index 3f36793..8420490 100644 --- a/lib/shanty/project_sorter.rb +++ b/lib/shanty/graph_factory.rb @@ -6,14 +6,14 @@ module Shanty # Public: Sorts projects using Tarjan's strongly connected components algorithm. - class ProjectSorter + class GraphFactory include TSort # Public: Initialise a ProjectLinker. # - # projects - An array of projects to use. These will be mutated and sorted when the linking takes place. - def initialize(projects) - @projects = projects + # env - ... + def initialize(env) + @env = env end # Private: Given a list of projects, sort them and construct a graph. @@ -21,24 +21,30 @@ def initialize(projects) # The sorting uses Tarjan's strongly connected components algorithm. # # Returns a Graph with the projects sorted. - def sort + def graph Graph.new(project_path_trie, tsort) end private + def project_path_trie + @project_path_trie ||= Containers::Trie.new.tap do |trie| + projects.map { |project| trie[project.path] = project } + end + end + + def projects + @projects ||= @env.plugins.each_with_object(Set.new) do |plugin, s| + s.merge(plugin.projects(@env)) + end + end + def tsort_each_node - @projects.each { |p| yield p } + projects.each { |p| yield p } end def tsort_each_child(project) project_path_trie.get(project.path).parents.each { |p| yield p } end - - def project_path_trie - @project_path_trie ||= Containers::Trie.new.tap do |trie| - @projects.map { |project| trie[project.path] = project } - end - end end end diff --git a/lib/shanty/logger.rb b/lib/shanty/logger.rb new file mode 100644 index 0000000..e8bc00d --- /dev/null +++ b/lib/shanty/logger.rb @@ -0,0 +1,15 @@ +module Shanty + # Small mixin that gives the receiver class a logger method. This method simply wraps the Ruby Logger class with a + # nice consistent logging format. + module Logger + module_function + + def logger + @logger ||= ::Logger.new($stdout).tap do |logger| + logger.formatter = proc do |_, datetime, _, msg| + "#{datetime}: #{msg}\n" + end + end + end + end +end diff --git a/lib/shanty/plugin.rb b/lib/shanty/plugin.rb index 2da1256..289aab0 100644 --- a/lib/shanty/plugin.rb +++ b/lib/shanty/plugin.rb @@ -1,83 +1,58 @@ require 'call_me_ruby' -require 'shenanigans/hash/to_ostruct' -require 'shanty/env' +require 'shanty/plugin_dsl' require 'shanty/project' module Shanty # Some basic functionality for every plugin. class Plugin include CallMeRuby - include Env - extend Env + extend PluginDsl - def self.inherited(plugin_class) - @plugins ||= [] - @plugins << plugin_class.new - end - - def self.plugins - (@plugins || []) - end - - def self.all_projects - (@plugins || []).flat_map(&:projects).uniq - end + attr_reader :project, :env - def self.all_with_graph(graph) - (@plugins || []).each { |plugin| plugin.with_graph(graph) } + def initialize(project, env) + @project = project + @env = env end - def self.tags(*args) - (@tags ||= [name]).concat(args.map(&:to_sym)) + # External getters + def self.project_providers + @project_providers ||= [] end - def self.option(option, default = nil) - (@options ||= {})[option] = default + def self.project_globs + @project_globs ||= [] end - def self.options - (@options ||= {}).merge(config[name].to_h).to_ostruct - end - - def self.projects(*globs_or_syms) - (@project_matchers ||= []).concat(globs_or_syms) - end - - def self.with_graph(&block) - (@with_graph_callbacks ||= []) << block + def self.tags + @tags ||= [name] end def self.name to_s.split('::').last.downcase.gsub('plugin', '').to_sym end - def projects - project_matchers = self.class.instance_variable_get(:@project_matchers) - return [] if project_matchers.nil? || project_matchers.empty? - (projects_from_globs(project_matchers) + projects_from_callbacks(project_matchers)).uniq.tap do |projects| - projects.each { |project| project.add_plugin(self) } - end - end + # Outside builders - def artifacts(_) - [] - end + def self.projects(env) + Set.new.tap do |projects| + projects.merge(project_providers.flat_map { |provider| send(provider, env) }) + env.file_tree.glob(*project_globs).each do |path| + projects << find_or_create_project(File.dirname(path), env) + end - def with_graph(graph) - callbacks = self.class.instance_variable_get(:@with_graph_callbacks) - return [] if callbacks.nil? || callbacks.empty? - callbacks.each { |callback| callback.call(graph) } + projects.each { |project| project.add_plugin(self) } + end end - private - - def projects_from_globs(project_matchers) - globs = project_matchers.find_all { |glob_or_sym| glob_or_sym.is_a?(String) } - project_tree.glob(*globs).map { |path| Project.new(File.absolute_path(File.dirname(path))) } + # Inner class methods + def self.find_or_create_project(path, env) + path = File.absolute_path(path) + (env.projects[path] ||= Project.new(path, env)) end - def projects_from_callbacks(project_matchers) - project_matchers.find_all { |glob_or_sym| glob_or_sym.is_a?(Symbol) }.flat_map { |sym| send(sym) } + def artifacts(_) + [] end end end diff --git a/lib/shanty/plugin_dsl.rb b/lib/shanty/plugin_dsl.rb new file mode 100644 index 0000000..ebc1f19 --- /dev/null +++ b/lib/shanty/plugin_dsl.rb @@ -0,0 +1,17 @@ +module Shanty + # Small mixin providing the DSL style class methods that plugin authors will rely on. This module is expected to be + # mixed into a Plugin class. + module PluginDsl + def provides_projects(*syms) + project_providers.concat(syms.map(&:to_sym)) + end + + def provides_projects_containing(*globs) + project_globs.concat(globs.map(&:to_s)) + end + + def provides_tags(*args) + tags.concat(args.map(&:to_sym)) + end + end +end diff --git a/lib/shanty/plugins/bundler_plugin.rb b/lib/shanty/plugins/bundler_plugin.rb index 2fdf378..cc5418c 100644 --- a/lib/shanty/plugins/bundler_plugin.rb +++ b/lib/shanty/plugins/bundler_plugin.rb @@ -2,15 +2,17 @@ require 'shanty/plugin' module Shanty - # Public: Bundler plugin for building ruby projects. - class BundlerPlugin < Plugin - projects '**/Gemfile' - subscribe :build, :bundle_install + module Plugins + # Public: Bundler plugin for building ruby projects. + class BundlerPlugin < Plugin + provides_projects_containing '**/Gemfile' + subscribe :build, :bundle_install - def bundle_install(_) - Bundler.with_clean_env do - # FIXME: Add support for the --jobs argument to parallelise the bundler run. - system 'bundle install --quiet' + def bundle_install + Bundler.with_clean_env do + # FIXME: Add support for the --jobs argument to parallelise the bundler run. + system 'bundle install --quiet' + end end end end diff --git a/lib/shanty/plugins/cucumber_plugin.rb b/lib/shanty/plugins/cucumber_plugin.rb index 188c7f4..e7c9a4c 100644 --- a/lib/shanty/plugins/cucumber_plugin.rb +++ b/lib/shanty/plugins/cucumber_plugin.rb @@ -1,23 +1,26 @@ require 'shanty/plugin' +require 'shanty/project' module Shanty - # Public: Cucumber plugin for testing ruby projects. - class CucumberPlugin < Plugin - subscribe :test, :cucumber - # By default, we'll detect Cucumber in a project if has a dependency on it in a Gemfile or *.gemspec file. If you - # don't use these files, you'll need to import the plugin manually using a Shantyfile as we can't tell if RSpec is - # being used from the presence of a spec directory alone (this can be many other testing frameworks!) - projects :cucumber_projects + module Plugins + # Public: Cucumber plugin for testing ruby projects. + class CucumberPlugin < Plugin + # By default, we'll detect Cucumber in a project if has a dependency on it in a Gemfile or *.gemspec file. If you + # don't use these files, you'll need to import the plugin manually using a Shantyfile as we can't tell if RSpec is + # being used from the presence of a spec directory alone (this can be many other testing frameworks!) + provides_projects :cucumber_projects + subscribe :test, :cucumber - def cucumber_projects - project_tree.glob('**/{*.gemspec,Gemfile}').each_with_object([]) do |dependency_file, acc| - next unless File.read(dependency_file) =~ /['"]cucumber['"]/ - acc << Project.new(File.absolute_path(File.dirname(dependency_file))) + def self.cucumber_projects(env) + env.file_tree.glob('**/{*.gemspec,Gemfile}').each_with_object([]) do |dependency_file, acc| + next unless File.read(dependency_file) =~ /['"]cucumber['"]/ + acc << find_or_create_project(File.absolute_path(File.dirname(dependency_file)), env) + end end - end - def cucumber(_) - system 'cucumber' + def cucumber + system 'cucumber' + end end end end diff --git a/lib/shanty/plugins/rspec_plugin.rb b/lib/shanty/plugins/rspec_plugin.rb index 5e1675b..02fb9f6 100644 --- a/lib/shanty/plugins/rspec_plugin.rb +++ b/lib/shanty/plugins/rspec_plugin.rb @@ -1,23 +1,26 @@ require 'shanty/plugin' +require 'shanty/project' module Shanty - # Public: Rspec plugin for testing ruby projects. - class RspecPlugin < Plugin - subscribe :test, :rspec - # By default, we'll detect RSpec in a project if has a dependency on it in a Gemfile or *.gemspec file. If you don't - # use these files, you'll need to import the plugin manually using a Shantyfile as we can't tell if RSpec is being - # used from the presence of a spec directory alone (this can be many other testing frameworks!) - projects :rspec_projects + module Plugins + # Public: Rspec plugin for testing ruby projects. + class RspecPlugin < Plugin + # By default, we'll detect RSpec in a project if has a dependency on it in a Gemfile or *.gemspec file. If you + # don't use these files, you'll need to import the plugin manually using a Shantyfile as we can't tell if RSpec is + # being used from the presence of a spec directory alone (this can be many other testing frameworks!) + provides_projects :rspec_projects + subscribe :test, :rspec - def rspec_projects - project_tree.glob('**/{*.gemspec,Gemfile}').each_with_object([]) do |dependency_file, acc| - next unless File.read(dependency_file) =~ /['"]rspec['"]/ - acc << Project.new(File.absolute_path(File.dirname(dependency_file))) + def self.rspec_projects(env) + env.file_tree.glob('**/{*.gemspec,Gemfile}').each_with_object([]) do |dependency_file, acc| + next unless File.read(dependency_file) =~ /['"]rspec['"]/ + acc << find_or_create_project(File.absolute_path(File.dirname(dependency_file)), env) + end end - end - def rspec(_) - system 'rspec' + def rspec + system 'rspec' + end end end end diff --git a/lib/shanty/plugins/rubocop_plugin.rb b/lib/shanty/plugins/rubocop_plugin.rb index 7161848..4318e4a 100644 --- a/lib/shanty/plugins/rubocop_plugin.rb +++ b/lib/shanty/plugins/rubocop_plugin.rb @@ -1,13 +1,15 @@ require 'shanty/plugin' module Shanty - # Public: Rubocop plugin for style checking ruby projects. - class RubocopPlugin < Plugin - projects '**/.rubocop.yml' - subscribe :test, :rubocop + module Plugins + # Public: Rubocop plugin for style checking ruby projects. + class RubocopPlugin < Plugin + provides_projects_containing '**/.rubocop.yml' + subscribe :test, :rubocop - def rubocop(_) - system 'rubocop' + def rubocop + system 'rubocop' + end end end end diff --git a/lib/shanty/plugins/rubygem_plugin.rb b/lib/shanty/plugins/rubygem_plugin.rb index a98c83e..4b1a139 100644 --- a/lib/shanty/plugins/rubygem_plugin.rb +++ b/lib/shanty/plugins/rubygem_plugin.rb @@ -2,35 +2,37 @@ require 'shanty/artifact' module Shanty - # Public: Rubygem plugin for buildin gems. - class RubygemPlugin < Plugin - ARTIFACT_EXTENSION = 'gem' + module Plugins + # Public: Rubygem plugin for buildin gems. + class RubygemPlugin < Plugin + ARTIFACT_EXTENSION = 'gem' - projects '**/*.gemspec' - subscribe :build, :build_gem - tags :gem + provides_projects_containing '**/*.gemspec' + provides_tags :gem + subscribe :build, :build_gem - def build_gem(project) - gemspec_files(project).each do |file| - system "gem build #{file}" + def build_gem + gemspec_files.each do |file| + system "gem build #{file}" + end end - end - def artifacts(project) - gemspec_files(project).flat_map do |file| - gemspec = Gem::Specification.load(file) - Artifact.new( - ARTIFACT_EXTENSION, - 'rubygem', - URI("file://#{project.path}/#{gemspec.name}-#{gemspec.version}.#{ARTIFACT_EXTENSION}") - ) + def artifacts + gemspec_files.flat_map do |file| + gemspec = Gem::Specification.load(file) + Artifact.new( + ARTIFACT_EXTENSION, + 'rubygem', + URI("file://#{project.path}/#{gemspec.name}-#{gemspec.version}.#{ARTIFACT_EXTENSION}") + ) + end end - end - private + private - def gemspec_files(project) - @gemspec_files ||= project_tree.glob(File.join(project.path, '*.gemspec')) + def gemspec_files + @gemspec_files ||= env.file_tree.glob(File.join(project.path, '*.gemspec')) + end end end end diff --git a/lib/shanty/plugins/shantyfile_plugin.rb b/lib/shanty/plugins/shantyfile_plugin.rb index f5cb226..200f283 100644 --- a/lib/shanty/plugins/shantyfile_plugin.rb +++ b/lib/shanty/plugins/shantyfile_plugin.rb @@ -1,14 +1,17 @@ require 'shanty/plugin' +require 'shanty/project' module Shanty - # Public: Plugin for finding all directories marked with a Shantyfile. - class ShantyfilePlugin < Plugin - projects :shantyfile_projects + module Plugins + # Public: Plugin for finding all directories marked with a Shantyfile. + class ShantyfilePlugin < Plugin + provides_projects :shantyfile_projects - def shantyfile_projects - project_tree.glob('**/Shantyfile').map do |shantyfile_path| - Project.new(File.absolute_path(File.dirname(shantyfile_path))).tap do |project| - project.instance_eval(File.read(shantyfile_path), shantyfile_path) + def self.shantyfile_projects(env) + env.file_tree.glob('**/Shantyfile').map do |shantyfile_path| + find_or_create_project(File.absolute_path(File.dirname(shantyfile_path)), env).tap do |project| + project.instance_eval(File.read(shantyfile_path), shantyfile_path) + end end end end diff --git a/lib/shanty/project.rb b/lib/shanty/project.rb index a4f5a28..94fc72c 100644 --- a/lib/shanty/project.rb +++ b/lib/shanty/project.rb @@ -1,56 +1,38 @@ require 'acts_as_graph_vertex' require 'call_me_ruby' -require 'pathname' - -require 'shanty/env' +require 'shanty/logger' module Shanty # Public: Represents a project in the current repository. class Project include ActsAsGraphVertex - include Env - - attr_accessor :artifacts, :name, :path, :tags, :options - - # Multiton or Flyweight pattern - only allow once instance per unique path. - # - # See https://en.wikipedia.org/wiki/Multiton_pattern, http://en.wikipedia.org/wiki/Flyweight_pattern, or - # http://blog.rubybestpractices.com/posts/gregory/059-issue-25-creational-design-patterns.html for more information. - # - # Note that this is _not_ currently threadsafe. - class << self - alias_method :__new__, :new + include Logger - def new(path) - (@instances ||= {})[path] ||= __new__(path) - end - - def clear! - @instances = {} - end - end + attr_accessor :path, :env, :name, :artifacts, :tags, :config # Public: Initialise the Project instance. # # path - The path to the project. - def initialize(path) - pathname = Pathname.new(File.expand_path(path, root)) - fail('Path to project must be a directory.') unless pathname.directory? + # env - The instance of the environment this project should have access to. + def initialize(path, env) + full_path = File.expand_path(path, env.root) + fail('Path to project must be a directory.') unless File.directory?(full_path) - @path = path + @path = full_path + @env = env - @name = File.basename(path) + @name = File.basename(full_path) # FIXME: When changed is implemented properly, redefine this to default # to false. @changed = true @artifacts = [] @plugins = [] @tags = [] - @options = {} + @config = {} end def add_plugin(plugin) - @plugins << plugin + @plugins << plugin.new(self, env) end def remove_plugin(plugin_class) @@ -91,7 +73,7 @@ def publish(name, *args) @plugins.each do |plugin| next unless plugin.subscribed?(name) logger.info("Executing #{name} on the #{plugin.class} plugin...") - return false if plugin.publish(name, self, *args) == false + return false if plugin.publish(name, *args) == false end end @@ -114,7 +96,7 @@ def inspect name: @name, path: @path, tags: all_tags, - options: @options, + config: @config, parents: parents.map(&:path) }.inspect end diff --git a/lib/shanty/task_env.rb b/lib/shanty/task_env.rb deleted file mode 100644 index 0f71bba..0000000 --- a/lib/shanty/task_env.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'shanty/plugin' -require 'shanty/project_sorter' - -module Shanty - # - module TaskEnv - # Idiom to allow singletons that can be mixed in: http://ozmm.org/posts/singin_singletons.html - extend self - - def clear! - @graph = nil - end - - def graph - @graph ||= ProjectSorter.new(Plugin.all_projects).sort.tap do |graph| - Plugin.all_with_graph(graph) - end - end - end -end diff --git a/lib/shanty/task_set.rb b/lib/shanty/task_set.rb index ac33c0c..d84cb11 100644 --- a/lib/shanty/task_set.rb +++ b/lib/shanty/task_set.rb @@ -1,21 +1,11 @@ -require 'shanty/env' -require 'shanty/task_env' - module Shanty # Public: Discover shanty tasks class TaskSet - include Env - include TaskEnv - - # This method is auto-triggered by Ruby whenever a class inherits from - # Shanty::TaskSet. This means we can build up a list of all the tasks - # without requiring them to register with us - neat! - def self.inherited(task_set) - task_sets << task_set - end + attr_reader :env, :graph - def self.task_sets - @task_sets ||= [] + def initialize(env, graph) + @env = env + @graph = graph end def self.tasks diff --git a/lib/shanty/task_sets/basic_task_set.rb b/lib/shanty/task_sets/basic_task_set.rb index 4f4a118..9baf64d 100644 --- a/lib/shanty/task_sets/basic_task_set.rb +++ b/lib/shanty/task_sets/basic_task_set.rb @@ -4,63 +4,65 @@ require 'shanty/plugin' module Shanty - # Public: A set of basic tasks that can be applied to all projects and that - # ship with the core of Shanty. - class BasicTasks < TaskSet - desc 'init', 'tasks.init.desc' - def init - FileUtils.touch(File.join(Dir.pwd, '.shanty.yml')) - end + module TaskSets + # Public: A set of basic tasks that can be applied to all projects and that + # ship with the core of Shanty. + class BasicTaskSet < TaskSet + desc 'init', 'tasks.init.desc' + def init + FileUtils.touch(File.join(Dir.pwd, '.shanty.yml')) + end - desc 'plugins', 'tasks.plugins.desc' - def plugins(_) - Plugin.plugins.each do |plugin| - puts plugin.class.name + desc 'plugins', 'tasks.plugins.desc' + def plugins(_) + Plugin.plugins.each do |plugin| + puts plugin.class.name + end end - end - desc 'projects [--tags TAG,TAG,...]', 'tasks.projects.desc' - option :tags, type: :array, desc: 'tasks.common.options.tags' - def projects(options) - filtered_graph(*tags_from_options(options)).each do |project| - puts "#{project.name} (#{project.path})#{project.tags.map { |tag| "\n - #{tag}" }.join}" + desc 'projects [--tags TAG,TAG,...]', 'tasks.projects.desc' + option :tags, type: :array, desc: 'tasks.common.options.tags' + def projects(options) + filtered_graph(*tags_from_options(options)).each do |project| + puts "#{project.name} (#{project.path})#{project.all_tags.map { |tag| "\n - #{tag}" }.join}" + end end - end - desc 'build [--tags TAG,TAG,...]', 'tasks.build.desc' - option :tags, type: :array, desc: 'tasks.common.options.tags' - def build(options) - run_common_task(:build, *tags_from_options(options)) - end + desc 'build [--tags TAG,TAG,...]', 'tasks.build.desc' + option :tags, type: :array, desc: 'tasks.common.options.tags' + def build(options) + run_common_task(:build, *tags_from_options(options)) + end - desc 'test [--tags TAG,TAG,...]', 'tasks.test.desc' - option :tags, type: :array, desc: 'tasks.common.options.tags' - def test(options) - run_common_task(:test, *tags_from_options(options)) - end + desc 'test [--tags TAG,TAG,...]', 'tasks.test.desc' + option :tags, type: :array, desc: 'tasks.common.options.tags' + def test(options) + run_common_task(:test, *tags_from_options(options)) + end - private + private - def tags_from_options(options) - (options.tags || '').split(',') - end + def tags_from_options(options) + (options.tags || '').split(',') + end - def run_common_task(task, *tags) - filtered_graph(*tags).each do |project| - Dir.chdir(project.path) do - fail I18n.t("tasks.#{task}.failed", project: project) unless project.publish(task) + def run_common_task(task, *tags) + filtered_graph(*tags).each do |project| + Dir.chdir(project.path) do + fail I18n.t("tasks.#{task}.failed", project: project) unless project.publish(task) + end end end - end - def filtered_graph(*tags) - return scoped_graph.all_with_tags(*tags) unless tags.empty? - scoped_graph - end + def filtered_graph(*tags) + return scoped_graph.all_with_tags(*tags) unless tags.empty? + scoped_graph + end - def scoped_graph - return graph if Dir.pwd == root - graph.projects_within_path(Dir.pwd) + def scoped_graph + return graph if Dir.pwd == env.root + graph.projects_within_path(Dir.pwd) + end end end end diff --git a/shanty.gemspec b/shanty.gemspec index 976ec80..e0a9f96 100644 --- a/shanty.gemspec +++ b/shanty.gemspec @@ -25,10 +25,8 @@ Gem::Specification.new do |gem| gem.add_dependency 'bundler', '~>1.10' gem.add_dependency 'call_me_ruby', '~>1.1' gem.add_dependency 'commander', '~>4.3' - gem.add_dependency 'deep_merge', '~>1.0' gem.add_dependency 'gitignore_rb', '~>0.2.2' gem.add_dependency 'i18n', '~>0.7' - gem.add_dependency 'shenanigans', '~>1.0' gem.add_development_dependency 'coveralls', '~>0.8' gem.add_development_dependency 'cucumber', '~>2.1' diff --git a/spec/fixtures/test_task_set.rb b/spec/fixtures/test_task_set.rb index e72d03a..a1c09de 100644 --- a/spec/fixtures/test_task_set.rb +++ b/spec/fixtures/test_task_set.rb @@ -1,14 +1,11 @@ require 'shanty/task_set' -module Shanty - # Test TaskSet fixture. - class TestTaskSet < TaskSet - desc 'foo [--cat CAT] [--dog] --catweasel CATWEASEL', 'test.foo.desc' - option :cat, desc: 'test.options.cat' - option :dog, type: :boolean, desc: 'test.options.dog' - option :catweasel, required: true, type: :string, desc: 'test.options.catweasel' - def foo(options) - options - end +class TestTaskSet < Shanty::TaskSet + desc 'foo [--cat CAT] [--dog] --catweasel CATWEASEL', 'test.foo.desc' + option :cat, desc: 'test.options.cat' + option :dog, type: :boolean, desc: 'test.options.dog' + option :catweasel, required: true, type: :string, desc: 'test.options.catweasel' + def foo(options) + options end end diff --git a/spec/fixtures/test_unused_plugin.rb b/spec/fixtures/test_unused_plugin.rb index b488fdd..53829b0 100644 --- a/spec/fixtures/test_unused_plugin.rb +++ b/spec/fixtures/test_unused_plugin.rb @@ -1,7 +1,5 @@ require 'shanty/plugin' -module Shanty - # Test unused Plugin fixture, used for testing whether looking plugins up by this plugin type, given none of them - # have it included, returns nothing. - class UnusedPlugin < Plugin; end -end +# Test unused Plugin fixture, used for testing whether looking plugins up by this plugin type, given none of them +# have it included, returns nothing. +class UnusedPlugin < Shanty::Plugin; end diff --git a/spec/lib/shanty/artifact_spec.rb b/spec/lib/shanty/artifact_spec.rb index a9085af..1dddf28 100644 --- a/spec/lib/shanty/artifact_spec.rb +++ b/spec/lib/shanty/artifact_spec.rb @@ -1,44 +1,41 @@ require 'shanty/artifact' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(Artifact) do - subject { described_class.new('html', 'test', uri) } - let(:relative_path_artifact) { described_class.new('html', 'test', URI('file:kim/cage.html')) } - let(:http_artifact) { described_class.new('html', 'test', URI('http://www.nic.com/kim/cage.html')) } - let(:schemless_artifact) { described_class.new('html', 'test', URI('kim/cage.html')) } - - let(:uri) { URI('file:///nic/kim/cage.html') } - - describe('.new') do - it('fails if the URI is not an absolute path') do - expect { relative_path_artifact }.to raise_error('URI is not absolute') - end - - it('fails if there is no scheme present in the URI') do - expect { schemless_artifact }.to raise_error('Scheme not present on URI') - end +RSpec.describe(Shanty::Artifact) do + subject { described_class.new('html', 'test', uri) } + let(:relative_path_artifact) { described_class.new('html', 'test', URI('file:kim/cage.html')) } + let(:http_artifact) { described_class.new('html', 'test', URI('http://www.nic.com/kim/cage.html')) } + let(:schemless_artifact) { described_class.new('html', 'test', URI('kim/cage.html')) } + + let(:uri) { URI('file:///nic/kim/cage.html') } + + describe('.new') do + it('fails if the URI is not an absolute path') do + expect { relative_path_artifact }.to raise_error('URI is not absolute') + end + + it('fails if there is no scheme present in the URI') do + expect { schemless_artifact }.to raise_error('Scheme not present on URI') end + end - describe('#local?') do - it('returns false if the file is not a local file') do - expect(http_artifact.local?).to be(false) - end + describe('#local?') do + it('returns false if the file is not a local file') do + expect(http_artifact.local?).to be(false) + end - it('returns true if the file is a local file') do - expect(subject.local?).to be(true) - end + it('returns true if the file is a local file') do + expect(subject.local?).to be(true) end + end - describe('#to_local_path') do - it('throws an exception if the resource is not local') do - allow(subject).to receive(:local?).and_return(false) - expect { subject.to_local_path }.to raise_error('URI is not a local resource') - end + describe('#to_local_path') do + it('throws an exception if the resource is not local') do + allow(subject).to receive(:local?).and_return(false) + expect { subject.to_local_path }.to raise_error('URI is not a local resource') + end - it('returns the path if the resource is local') do - expect(subject.to_local_path).to eql('/nic/kim/cage.html') - end + it('returns the path if the resource is local') do + expect(subject.to_local_path).to eql('/nic/kim/cage.html') end end end diff --git a/spec/lib/shanty/cli_spec.rb b/spec/lib/shanty/cli_spec.rb index db4cf12..295e320 100644 --- a/spec/lib/shanty/cli_spec.rb +++ b/spec/lib/shanty/cli_spec.rb @@ -4,144 +4,171 @@ require 'shanty/cli' require 'shanty/info' require 'shanty/env' -require 'shenanigans/hash/to_ostruct' -require_fixture 'test_task_set' - -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(Cli) do - let(:task_sets) { [TestTaskSet] } - subject { Cli.new(task_sets) } - - describe('#tasks') do - it('returns a list of tasks from all given task sets') do - expect(subject.tasks.keys).to contain_exactly(:foo) - - command = subject.tasks[:foo] - expect(command[:syntax]).to eql('foo [--cat CAT] [--dog] --catweasel CATWEASEL') - expect(command[:desc]).to eql('test.foo.desc') - expect(command[:klass]).to equal(TestTaskSet) - - options = command[:options] - expect(options.keys).to contain_exactly(:cat, :dog, :catweasel) - expect(options[:cat]).to eql(desc: 'test.options.cat') - expect(options[:dog]).to eql(type: :boolean, desc: 'test.options.dog') - expect(options[:catweasel]).to eql(required: true, type: :string, desc: 'test.options.catweasel') - end +require 'shanty/task_set' + +RSpec.describe(Shanty::Cli) do + subject { described_class.new([task_set], env, graph) } + let(:env) { double('env') } + let(:graph) { double('graph') } + let(:config) { Hash.new { |h, k| h[k] = {} } } + let(:task_set) do + double('task set').tap do |d| + allow(d).to receive(:tasks).and_return( + foo: { + klass: d, + options: { + cat: { + desc: 'test.options.cat' + }, + dog: { + type: :boolean, + desc: 'test.options.dog' + }, + catweasel: { + required: true, + type: :string, + desc: 'test.options.catweasel' + } + }, + syntax: 'foo [--cat CAT] [--dog] --catweasel CATWEASEL', + desc: 'test.foo.desc' + } + ) end + end - describe('#run') do - before do - loop do - ARGV.pop - break if ARGV.empty? - end + before do + allow(env).to receive(:config).and_return(config) + end - allow(subject).to receive(:program).and_call_original - allow(subject).to receive(:command).and_call_original - end + describe('#tasks') do + it('returns a list of tasks from all given task sets') do + expect(subject.tasks.keys).to contain_exactly(:foo) - it('sets the name of the program') do - allow(subject).to receive(:run!) - expect(subject).to receive(:program).with(:name, 'Shanty') + command = subject.tasks[:foo] + expect(command[:syntax]).to eql('foo [--cat CAT] [--dog] --catweasel CATWEASEL') + expect(command[:desc]).to eql('test.foo.desc') + expect(command[:klass]).to equal(task_set) - subject.run + options = command[:options] + expect(options.keys).to contain_exactly(:cat, :dog, :catweasel) + expect(options[:cat]).to eql(desc: 'test.options.cat') + expect(options[:dog]).to eql(type: :boolean, desc: 'test.options.dog') + expect(options[:catweasel]).to eql(required: true, type: :string, desc: 'test.options.catweasel') + end + end + + describe('#run') do + before do + loop do + ARGV.pop + break if ARGV.empty? end - it('sets the version of the program') do - allow(subject).to receive(:run!) - expect(subject).to receive(:program).with(:version, Info::VERSION) + allow(subject).to receive(:program).and_call_original + allow(subject).to receive(:command).and_call_original + end - subject.run - end + it('sets the name of the program') do + allow(subject).to receive(:run!) + expect(subject).to receive(:program).with(:name, 'Shanty') - it('sets the description of the program') do - allow(subject).to receive(:run!) - expect(subject).to receive(:program).with(:version, Info::VERSION) + subject.run + end - subject.run - end + it('sets the version of the program') do + allow(subject).to receive(:run!) + expect(subject).to receive(:program).with(:version, Shanty::Info::VERSION) - it('sets up the tasks for the program') do - allow(subject).to receive(:run!) - expect(subject).to receive(:command) - expect(subject.defined_commands).to include('foo') + subject.run + end - command = subject.defined_commands['foo'] - expect(command.description).to eql('test.foo.desc') + it('sets the description of the program') do + allow(subject).to receive(:run!) + expect(subject).to receive(:program).with(:version, Shanty::Info::VERSION) - options = command.options.map { |o| o[:description] } - expect(options).to contain_exactly('test.options.cat', 'test.options.dog', 'test.options.catweasel') + subject.run + end - subject.run - end + it('sets up the tasks for the program') do + allow(subject).to receive(:run!) + expect(subject).to receive(:command) + expect(subject.defined_commands).to include('foo') - it('runs the CLI program') do - allow(subject).to receive(:run!) - expect(subject).to receive(:run!) + command = subject.defined_commands['foo'] + expect(command.description).to eql('test.foo.desc') - subject.run - end + options = command.options.map { |o| o[:description] } + expect(options).to contain_exactly('test.options.cat', 'test.options.dog', 'test.options.catweasel') - it('fails to run with no command specified') do - expect(Commander::Runner.instance).to receive(:abort).with('invalid command. Use --help for more information') - subject.run - end + subject.run + end - it('fails to run an invalid command') do - expect(Commander::Runner.instance).to receive(:abort).with('invalid command. Use --help for more information') + it('runs the CLI program') do + allow(subject).to receive(:run!) + expect(subject).to receive(:run!) - ARGV.concat(%w(xulu)) + subject.run + end - subject.run - end + it('fails to run with no command specified') do + expect(Commander::Runner.instance).to receive(:abort).with('invalid command. Use --help for more information') + subject.run + end - it('fails to run a command when run without required options') do - # FIXME: Commander catches the exception and rethrows it with extra stuff, we should move away from commander - expect(Commander::Runner.instance).to receive(:abort).with( - include(I18n.t('cli.params_missing', missing: 'catweasel')) - ) + it('fails to run an invalid command') do + expect(Commander::Runner.instance).to receive(:abort).with('invalid command. Use --help for more information') - ARGV.concat(%w(foo)) + ARGV.concat(%w(xulu)) - subject.run - end + subject.run + end - it('executes a command correctly when run') do - ARGV.concat(%w(foo --cat=iamaniamscat --dog --catweasel=noiamacatweasel)) + it('fails to run a command when run without required options') do + # FIXME: Commander catches the exception and rethrows it with extra stuff, we should move away from commander + expect(Commander::Runner.instance).to receive(:abort).with( + include(I18n.t('cli.params_missing', missing: 'catweasel')) + ) - subject.run - end + ARGV.concat(%w(foo)) - it('executes a command correctly when run without non-required options') do - ARGV.concat(%w(foo --catweasel=noiamacatweasel)) + subject.run + end - subject.run - end + it('executes a command correctly when run') do + ARGV.concat(%w(foo --cat=iamaniamscat --dog --catweasel=noiamacatweasel)) - it('fails to run a command with config options if config is in incorrect format') do - ARGV.concat(%w(-c nic foo)) + subject.run + end - expect do - subject.run - end.to raise_error(I18n.t('cli.invalid_config_param', actual: 'nic', expected: Cli::CONFIG_FORMAT)) - end + it('executes a command correctly when run without non-required options') do + ARGV.concat(%w(foo --catweasel=noiamacatweasel)) + + subject.run + end - it('runs a command with a config option') do - ARGV.concat(['-c nic:kim cage']) + it('fails to run a command with config options if config is in incorrect format') do + ARGV.concat(%w(-c nic foo)) + expect do subject.run + end.to raise_error(I18n.t('cli.invalid_config_param', actual: 'nic', expected: Shanty::Cli::CONFIG_FORMAT)) + end - expect(Env.config.nic).to eql({ kim: 'cage' }.to_ostruct) - end + it('runs a command with a config option') do + ARGV.concat(['-c nic:kim cage']) - it('runs a command with multiple config options') do - ARGV.concat(['-c nic:kim cage', '-c nic:copolla cage']) + subject.run - subject.run + expect(config[:nic]).to eql(kim: 'cage') + end - expect(Env.config.nic).to eql({ kim: 'cage', copolla: 'cage' }.to_ostruct) - end + it('runs a command with multiple config options') do + ARGV.concat(['-c nic:kim cage', '-c nic:copolla cage']) + + subject.run + + expect(config[:nic]).to eql(kim: 'cage', copolla: 'cage') end end end diff --git a/spec/lib/shanty/config_spec.rb b/spec/lib/shanty/config_spec.rb deleted file mode 100644 index 8f60c28..0000000 --- a/spec/lib/shanty/config_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'shanty/config' -require 'shenanigans/hash/to_ostruct' -require 'deep_merge' - -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(Config) do - subject { Config.new('nic', 'test') } - - let(:subconfig) { { 'kim' => 'cage' } } - let(:config) { { 'nic' => subconfig } } - let(:env_config) { { 'test' => config } } - - before do - allow(File).to receive(:exist?) { true } - allow(YAML).to receive(:load_file) { env_config } - end - - describe(described_class) do - it('can respond like an OpenStruct') do - expect(subject.nic).to eql(subconfig.to_ostruct) - end - - it('handles no config file existing') do - allow(File).to receive(:exist?) { false } - expect(subject.nic).to eql(OpenStruct.new) - end - - it('handles no data in the config file') do - allow(YAML).to receive(:load_file) { false } - expect(subject.nic).to eql(OpenStruct.new) - end - - it('loads config from a file') do - allow(File).to receive(:exist?).and_call_original - allow(YAML).to receive(:load_file).and_call_original - Dir.mktmpdir('shanty-tests') do |tmp_path| - Dir.chdir(tmp_path) { FileUtils.touch('.shanty.yml') } - expect(Config.new(tmp_path, 'test').nic).to eql(OpenStruct.new) - end - end - - it('returns nothing if .shanty.yml does not exist') do - allow(File).to receive(:exist?).and_call_original - allow(YAML).to receive(:load_file).and_call_original - expect(subject.nic).to eql(OpenStruct.new) - end - end - - describe('#[]') do - it('can respond like a hash') do - expect(subject['nic']).to eql(subconfig.to_ostruct) - end - end - - describe('#merge!') do - it('can merge in new config') do - added_config = { 'copolla' => 'cage' } - subject.merge!('nic' => added_config) - - expect(subject['nic']).to eql(subconfig.deep_merge(added_config).to_ostruct) - end - end - - describe('#respond_to?') do - it('is able to repond to a method on the config class') do - expect(subject.respond_to?(:merge!)).to be(true) - end - - it('is able to respond to a method on the underlying openstruct') do - expect(subject.respond_to?(:nic)).to be(true) - end - - it('is not able to respond to a non-existent method') do - expect(subject.respond_to?(:copolla)).to be(false) - end - end - end -end diff --git a/spec/lib/shanty/env_spec.rb b/spec/lib/shanty/env_spec.rb index f0ca126..f554bd9 100644 --- a/spec/lib/shanty/env_spec.rb +++ b/spec/lib/shanty/env_spec.rb @@ -1,134 +1,76 @@ -require 'spec_helper' require 'fileutils' require 'i18n' -require 'tmpdir' require 'shanty/env' -require 'shenanigans/hash/to_ostruct' -require 'deep_merge' +require 'shanty/file_tree' +require 'shanty/plugins/shantyfile_plugin' +require 'shanty/task_sets/basic_task_set' +require 'shanty/file_tree' +require 'spec_helper' +require 'tmpdir' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(Env) do - around do |example| - Dir.mktmpdir('shanty-tests') do |tmp_path| - Dir.chdir(tmp_path) do - FileUtils.touch('.shanty.yml') - example.run - end +RSpec.describe(Shanty::Env) do + around do |example| + Dir.mktmpdir('shanty-tests') do |tmp_path| + Dir.chdir(tmp_path) do + FileUtils.touch('Shantyconfig') + example.run end end + end - describe('#require!') do - let(:file_config) { { 'local' => { 'require' => ['foo', 'bar.foo'] } } } - let(:glob_config) { { 'local' => { 'require' => ['**/*.foo'] } } } - let(:dir_config) { { 'local' => { 'require' => ['lux'] } } } - - before do - FileUtils.touch(['foo', 'bar.foo']) - FileUtils.mkdir('lux') - FileUtils.touch(['lux/luxy.rb', 'lux/floxy.rb', 'lux/luxoo.foo']) - end - - it('requires nothing when there is no "require" config entry') do - expect(subject).to_not receive(:require) - - subject.require! - end - - it('requires entries that are files') do - allow(YAML).to receive(:load_file) { file_config } - - expect(subject).to receive(:require).with(File.join(Dir.pwd, 'foo')) - expect(subject).to receive(:require).with(File.join(Dir.pwd, 'bar.foo')) - - subject.require! - end - - it('requires entries that are globs') do - allow(YAML).to receive(:load_file) { glob_config } - - expect(subject).to receive(:require).with(File.join(Dir.pwd, 'bar.foo')) - expect(subject).to receive(:require).with(File.join(Dir.pwd, 'lux/luxoo.foo')) - - subject.require! - end - - it('requires entries that are directories') do - allow(YAML).to receive(:load_file) { dir_config } - - expect(subject).to receive(:require).with(File.join(Dir.pwd, 'lux/luxy.rb')) - expect(subject).to receive(:require).with(File.join(Dir.pwd, 'lux/floxy.rb')) - - subject.require! - end + describe('#root') do + it('returns the path to the closest ancestor folder with a .shanty.yml file in it') do + expect(subject.root).to eql(Dir.pwd) end - describe('#logger') do - it('returns a logger object') do - expect(subject.logger).to be_a(Logger) - end + it('throws an exception if no ancestor folders have a .shanty.yml file in them') do + FileUtils.rm('Shantyconfig') + expect { subject.root }.to raise_error(I18n.t('missing_root', config_file: described_class::CONFIG_FILE)) end + end - describe('#environment') do - before do - ENV.delete('SHANTY_ENV') - end - - it('returns the SHANTY_ENV environment variable if set') do - ENV['SHANTY_ENV'] = 'foo' - expect(subject.environment).to eql('foo') - end - - it('returns local if SHANTY_ENV is not set') do - expect(subject.environment).to eql('local') - end + describe('#file_tree') do + it('returns a file tree') do + expect(subject.file_tree).to be_a(Shanty::FileTree) end + end - describe('#build_number') do - before do - ENV.delete('SHANTY_BUILD_NUMBER') - end - - it('returns the SHANTY_BUILD_NUMBER environment variable if set') do - ENV['SHANTY_BUILD_NUMBER'] = '123' - expect(subject.build_number).to eql(123) - end - - it('returns 1 if SHANTY_BUILD_NUMBER is not set') do - expect(subject.build_number).to eql(1) - end + describe('#plugins') do + it('defaults to just the Shantyfile plugin') do + expect(subject.plugins).to contain_exactly(Shanty::Plugins::ShantyfilePlugin) end + end - describe('#root') do - it('returns the path to the closest ancestor folder with a .shanty.yml file in it') do - expect(subject.root).to eql(Dir.pwd) - end + describe('#task_sets') do + it('defaults to just the basic task set') do + expect(subject.task_sets).to contain_exactly(Shanty::TaskSets::BasicTaskSet) + end + end - it('throws an exception if no ancestor folders have a .shanty.yml file in them') do - FileUtils.rm('.shanty.yml') - expect { subject.root }.to raise_error(I18n.t('missing_root', config_file: Env::CONFIG_FILE)) - end + describe('#projects') do + it('defaults to an empty hash') do + expect(subject.projects).to be_a(Hash) + expect(subject.projects).to be_empty end + end - describe('#config') do - before do - ENV['SHANTY_ENV'] = 'stray_cats' - end + describe('#config') do + it('returns an empty hash for any missing keys') do + expect(subject.config[:foo]).to eql({}) + end + end - it('handles no config file existing') do - allow(File).to receive(:exist?) { false } - expect(subject.config.nic).to eql(OpenStruct.new) - end + describe('#require') do + it('calls require properly for anything passed in') do + pending('need to check how we can capture the require calls') - it('handles no data in the config file') do - allow(YAML).to receive(:load_file) { false } - expect(subject.config) - end + expect(subject.singleton_class).to receive(:require).with('foo') - it('returns nothing if .shanty.yml does not exist') do - FileUtils.rm('.shanty.yml') - expect(subject.config.nic).to eql(OpenStruct.new) - end + subject.require('foo') end + + it('captures any required plugins') + + it('captures any required task sets') end end diff --git a/spec/lib/shanty/file_tree_spec.rb b/spec/lib/shanty/file_tree_spec.rb new file mode 100644 index 0000000..2d2247a --- /dev/null +++ b/spec/lib/shanty/file_tree_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' +require 'shanty/file_tree' + +RSpec.describe(Shanty::FileTree) do + include_context('workspace') + subject { described_class.new(root) } + + before do + [ + File.join(project_paths.first, 'Shantyfile'), + # FIXME: Right now, the ignores are not procesed. This will be + # implemented as part of issue #9. + # File.join(project_paths[1], 'ignored'), + File.join(project_paths[2], 'Shantyfile') + ].each { |path| FileUtils.touch(path) } + + File.write(File.join(root, '.gitignore'), 'ignored') + end + + describe('#files') do + it('returns all the files within the root') do + expect(subject.files).to match_array([ + File.join(root, 'Shantyconfig'), + File.join(root, '.gitignore'), + File.join(project_paths.first, 'Shantyfile'), + File.join(project_paths[2], 'Shantyfile') + ]) + end + end + + describe('#glob') do + it('returns all the files within the root that match any of the given globs') do + expect(subject.glob('**/Shantyfile', 'badglob')).to match_array([ + File.join(project_paths.first, 'Shantyfile'), + File.join(project_paths[2], 'Shantyfile') + ]) + end + end +end diff --git a/spec/lib/shanty/graph_factory_spec.rb b/spec/lib/shanty/graph_factory_spec.rb new file mode 100644 index 0000000..9887272 --- /dev/null +++ b/spec/lib/shanty/graph_factory_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' +require 'tmpdir' +require 'shanty/graph_factory' + +RSpec.describe(Shanty::GraphFactory) do + include_context('workspace') + subject { described_class.new(env) } + let(:env) { double('env') } + let(:plugins) do + [ + double('plugin one'), + double('plugin two'), + double('plugin three') + ] + end + let(:projects) do + [ + double('project one'), + double('project two'), + double('project three') + ] + end + + before do + allow(env).to receive(:plugins).and_return(plugins) + allow(plugins.first).to receive(:projects).and_return([projects.first]) + allow(plugins[1]).to receive(:projects).and_return([projects[1]]) + allow(plugins[2]).to receive(:projects).and_return([projects[2]]) + + allow(projects.first).to receive(:path).and_return(project_paths.first) + allow(projects[1]).to receive(:path).and_return(project_paths[1]) + allow(projects[2]).to receive(:path).and_return(project_paths[2]) + + allow(projects.first).to receive(:parents).and_return([]) + allow(projects[1]).to receive(:parents).and_return([projects.first]) + allow(projects[2]).to receive(:parents).and_return([projects[1]]) + end + + describe('#graph') do + it("returns a graph with the projects sorted using Tarjan's strongly connected components algorithm") do + graph = subject.graph + + expect(graph.first).to eql(projects.first) + expect(graph[1]).to eql(projects[1]) + expect(graph[2]).to eql(projects[2]) + end + end +end diff --git a/spec/lib/shanty/graph_spec.rb b/spec/lib/shanty/graph_spec.rb index 5210e01..b9a2053 100644 --- a/spec/lib/shanty/graph_spec.rb +++ b/spec/lib/shanty/graph_spec.rb @@ -1,74 +1,100 @@ require 'spec_helper' -require 'tmpdir' require 'shanty/graph' -require 'shanty/project' - -# Allows all classes to be refereneced without the module name -module Shanty - RSpec.describe(Graph) do - include_context('graph') - subject { graph } - - describe('#each') do - it('returns the projects exactly as passed in') do - expect(subject.each.to_a).to eql(projects.values) - end + +RSpec.describe(Shanty::Graph) do + subject { described_class.new(project_path_trie, projects) } + let(:project_path_trie) { double('project path trie') } + let(:projects) do + [ + double('project one'), + double('project two'), + double('project three') + ] + end + + describe('#each') do + it('returns the projects exactly as passed in') do + expect(subject.each.to_a).to eql(projects) end + end - describe('#by_name') do - it('returns nil when finding a name that does not exist') do - expect(subject.by_name('foobarlux')).to be_nil - end + describe('#by_name') do + before do + allow(projects.first).to receive(:name).and_return('foo') + allow(projects[1]).to receive(:name).and_return('bar') + allow(projects[2]).to receive(:name).and_return('lux') + end - it('returns the correct project when finding a name that does exist') do - expect(subject.by_name(subject.first.name)).to equal(subject.first) - end + it('returns nil when finding a name that does not exist') do + expect(subject.by_name('foobarlux')).to be_nil end - describe('#all_with_tags') do - before do - subject.first.tags << 'test' - end + it('returns the correct project when finding a name that does exist') do + expect(subject.by_name('foo')).to equal(projects.first) + end + end - it('returns an empty array when no tags are given') do - expect(subject.all_with_tags).to be_empty - end + describe('#all_with_tags') do + before do + allow(projects.first).to receive(:all_tags).and_return([:foo]) + allow(projects[1]).to receive(:all_tags).and_return([:foo, :bar]) + allow(projects[2]).to receive(:all_tags).and_return([:lux]) + end - it('returns an empty array when no projects match the tags given') do - expect(subject.all_with_tags('foo')).to be_empty - end + it('returns an empty array when no tags are given') do + expect(subject.all_with_tags).to be_empty + end - it('returns the correct projects when matching tags are given') do - expect(subject.all_with_tags('test')).to match_array([subject.first]) - end + it('returns an empty array when no projects match the tags given') do + expect(subject.all_with_tags(:foobarlux)).to be_empty end - describe('#changed') do - it('returns all the changed projects') do - allow(projects[:one]).to receive(:changed?).and_return(true) - allow(projects[:two]).to receive(:changed?).and_return(false) - allow(projects[:three]).to receive(:changed?).and_return(true) + it('returns the correct projects when matching tags are given') do + expect(subject.all_with_tags(:foo)).to contain_exactly(projects.first, projects[1]) + end - expect(subject.changed).to match_array([projects[:one], projects[:three]]) - end + it('converts the given tags to symbols when checking for matches') do + expect(subject.all_with_tags('bar')).to contain_exactly(projects[1]) end + end - describe('#owner_of_file') do - it('returns nil if the given folder is outside of any project') do - expect(subject.owner_of_file('/tmp')).to be_nil - end + describe('#changed') do + before do + allow(projects.first).to receive(:changed?).and_return(true) + allow(projects[1]).to receive(:changed?).and_return(false) + allow(projects[2]).to receive(:changed?).and_return(true) + end + + it('returns all the changed projects') do + expect(subject.changed).to contain_exactly(projects.first, projects[2]) + end + end + + describe('#owner_of_file') do + it('returns nil if the given folder is outside of any project') do + allow(project_path_trie).to receive(:longest_prefix) + allow(project_path_trie).to receive(:[]) - it('returns the correct project that owns a given folder') do - expect(subject.owner_of_file(project_paths[:three]).path).to eql(project_paths[:three]) - end + expect(subject.owner_of_file('/tmp')).to be_nil end - describe('#projects_within_path') do - let(:projects_within_path) { subject.projects_within_path(project_paths[:two]) } + it('returns the correct project that owns a given folder') do + allow(project_path_trie).to receive(:longest_prefix).with('/foo/bar').and_return('/foo') + allow(project_path_trie).to receive(:[]).with('/foo').and_return(projects.first) + + expect(subject.owner_of_file('/foo/bar')).to eql(projects.first) + end + end + + describe('#projects_within_path') do + before do + allow(projects.first).to receive(:path).and_return('/foo') + allow(projects[1]).to receive(:path).and_return('/foo/bar') + allow(projects[2]).to receive(:path).and_return('/lux') + end - it('returns all the projects at or below the given path') do - expect(projects_within_path.map(&:path)).to contain_exactly(project_paths[:two], project_paths[:three]) - end + it('returns all the projects at or below the given path') do + expect(subject.projects_within_path('/foo')).to contain_exactly(projects.first, projects[1]) end end end diff --git a/spec/lib/shanty/logger_spec.rb b/spec/lib/shanty/logger_spec.rb new file mode 100644 index 0000000..062156b --- /dev/null +++ b/spec/lib/shanty/logger_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' +require 'shanty/logger' + +RSpec.describe(Shanty::Logger) do + describe('#logger') do + it('returns a logger object') do + expect(subject.logger).to be_a(Logger) + end + end +end diff --git a/spec/lib/shanty/plugin_dsl_spec.rb b/spec/lib/shanty/plugin_dsl_spec.rb new file mode 100644 index 0000000..2a2c29e --- /dev/null +++ b/spec/lib/shanty/plugin_dsl_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require 'shanty/plugin' +require 'shanty/plugin_dsl' + +RSpec.describe(Shanty::PluginDsl) do + include_context('workspace') + + subject { Class.new(Shanty::Plugin) } + + describe('.provides_projects') do + it('stores the given symbols') do + subject.provides_projects(:foo, :bar) + + expect(subject.instance_variable_get(:@project_providers)).to contain_exactly(:foo, :bar) + end + end + + describe('.provides_projects_containing') do + it('stores the given globs') do + subject.provides_projects_containing('**/foo', '**/bar') + + expect(subject.instance_variable_get(:@project_globs)).to contain_exactly('**/foo', '**/bar') + end + end + + describe('.provides_tags') do + before do + allow(subject).to receive(:name).and_return(:plugin_name) + end + + it('stores the given tags') do + subject.provides_tags(:foo, :marbles) + + expect(subject.instance_variable_get(:@tags)).to contain_exactly(:plugin_name, :foo, :marbles) + end + + it('converts any given tags to symbols') do + subject.provides_tags('bar', 'lux') + + expect(subject.instance_variable_get(:@tags)).to contain_exactly(:plugin_name, :bar, :lux) + end + end +end diff --git a/spec/lib/shanty/plugin_spec.rb b/spec/lib/shanty/plugin_spec.rb index b031ed1..11b393e 100644 --- a/spec/lib/shanty/plugin_spec.rb +++ b/spec/lib/shanty/plugin_spec.rb @@ -1,182 +1,66 @@ require 'spec_helper' require 'shanty/plugin' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(Plugin) do - include_context('graph') - let(:graph) { double('graph') } - let(:plugin_class) do - Class.new(described_class) do - def foo; end - end - end - subject { plugin_class.new } - - around do |example| - plugins = described_class.instance_variable_get(:@plugins).clone - described_class.instance_variable_set(:@plugins, [subject]) - example.run - described_class.instance_variable_set(:@plugins, plugins) - end - - describe('.plugins') do - it('finds all the loaded plugins') do - expect(described_class.plugins.length).to be(1) - end - end +RSpec.describe(Shanty::Plugin) do + include_context('workspace') - describe('.name') do - it('gets the name of the class') do - expect(plugin_class.name).to eql(plugin_class.to_s.downcase.to_sym) - end - end - - describe('.inherited') do - it('stores a new instance of any class that extends Plugin') do - expect(described_class.instance_variable_get(:@plugins).size).to eq(1) - expect(described_class.instance_variable_get(:@plugins).first).to be_instance_of(plugin_class) - end - end + subject { plugin_class.new(env, project) } + let(:plugin_class) { Class.new(described_class) } + let(:env) { double('env') } + let(:project) { double('project') } - describe('.all_projects') do - it('returns all the nominated projects from all the registered plugins') do - allow(subject).to receive(:projects).and_return(project) - expect(described_class.all_projects).to match_array(project) - end + describe('.name') do + it('gets the name of the class') do + expect(plugin_class.name).to eql(plugin_class.to_s.downcase.to_sym) end + end - describe('.all_with_graph') do - before do - described_class.instance_variable_set(:@plugins, [subject]) - end - - it('calls #with_graph on every registered plugin') do - expect(subject).to receive(:with_graph).with(graph) - - described_class.all_with_graph(graph) - end - end - - describe('.option') do - it('can set an expected option default') do - allow(plugin_class).to receive(:config).and_return(plugin_class.name => {}) - - plugin_class.option(:nic, 'cage') - - expect(plugin_class.options[:nic]).to eql('cage') - end - end - - describe('.options') do - it('returns a default option value') do - allow(plugin_class).to receive(:config).and_return(plugin_class.name => {}) - - plugin_class.option(:nic, 'cage') - - expect(plugin_class.options[:nic]).to eql('cage') - end - - it('returns an existing option value') do - allow(plugin_class).to receive(:config).and_return(plugin_class.name => { test: 'test' }) - - expect(plugin_class.options[:test]).to eql('test') - end - - it('overrides a default option value') do - allow(plugin_class).to receive(:config).and_return(plugin_class.name => { nic: 'cage' }) - - plugin_class.option(:nic, 'copolla') - - expect(plugin_class.options[:nic]).to eql('cage') - end - - it('returns nothing for a non-existent option') do - allow(plugin_class).to receive(:config).and_return(plugin_class.name => {}) + describe('.projects') do + before do + allow(env).to receive(:file_tree).and_return(file_tree) + allow(env).to receive(:projects).and_return({}) + allow(env).to receive(:root).and_return(root) + allow(file_tree).to receive(:glob).and_return([]) + allow(project).to receive(:add_plugin) - expect(plugin_class.options[:nic]).to be_nil - end + plugin_class.define_singleton_method(:foo) { |_| [] } end - describe('.tags') do - it('stores the given tags') do - plugin_class.tags(:foo, :marbles) - - expect(plugin_class.instance_variable_get(:@tags)).to match_array([plugin_class.name.to_sym, :foo, :marbles]) - end - - it('converts any given tags to symbols') do - plugin_class.tags('bar', 'lux') + let(:file_tree) { double('file tree') } + let(:project) { double('project') } - expect(plugin_class.instance_variable_get(:@tags)).to match_array([plugin_class.name.to_sym, :bar, :lux]) - end + it('returns no projects if there are no matchers') do + expect(plugin_class.projects(env)).to be_empty end - describe('.projects') do - it('stores the given globs or symbols') do - plugin_class.projects('**/foo', :bar) + it('returns projects matching any stored globs') do + paths = project_paths.map { |p| File.join(p, 'foo') } + plugin_class.project_globs.concat(['**/foo', '**/bar']) + allow(file_tree).to receive(:glob).with('**/foo', '**/bar').and_return(paths) - expect(plugin_class.instance_variable_get(:@project_matchers)).to match_array(['**/foo', :bar]) - end + expect(plugin_class.projects(env).map(&:path)).to match_array(project_paths) end - describe('.with_graph') do - it('stores the passed block') do - block = proc {} - plugin_class.with_graph(&block) - - expect(plugin_class.instance_variable_get(:@with_graph_callbacks)).to match_array([block]) - end - end + it('returns projects provided by the stored callbacks') do + allow(plugin_class).to receive(:foo).and_return([project]) + plugin_class.project_providers << :foo - describe('#artifacts') do - it('returns no artifacts when artifacts have not been implemented') do - expect(subject.artifacts(project)).to be_empty - end + expect(plugin_class.projects(env)).to match_array([project]) end - describe('#projects') do - let(:project_tree) { double('project_tree') } + it('adds the current plugin to the project') do + allow(plugin_class).to receive(:foo).and_return([project]) + plugin_class.project_providers << :foo - it('returns no projects if there are no matchers') do - expect(subject.projects).to be_empty - end + expect(project).to receive(:add_plugin).with(plugin_class) - it('returns projects matching any stored globs') do - paths = project_paths.values.map { |p| File.join(p, 'foo') } - plugin_class.projects('**/foo', '**/bar') - allow(subject).to receive(:project_tree).and_return(project_tree) - allow(project_tree).to receive(:glob).with('**/foo', '**/bar').and_return(paths) - - expect(subject.projects).to match_array(projects.values) - end - - it('returns projects provided by the stored callbacks') do - allow(subject).to receive(:foo).and_return(projects.values) - plugin_class.projects(:foo) - - expect(subject.projects).to match_array(projects.values) - end - - it('adds the current plugin to the project') do - allow(subject).to receive(:foo).and_return([project]) - plugin_class.projects(:foo) - - expect(project).to receive(:add_plugin).with(subject) - - subject.projects - end + plugin_class.projects(env) end + end - describe('#with_graph') do - it('calls the stored callbacks with the given graph') do - block = proc {} - plugin_class.with_graph(&block) - - expect(block).to receive(:call).with(graph) - - subject.with_graph(graph) - end + describe('#artifacts') do + it('returns no artifacts when artifacts have not been implemented') do + expect(subject.artifacts(project)).to be_empty end end end diff --git a/spec/lib/shanty/plugins/bundler_plugin_spec.rb b/spec/lib/shanty/plugins/bundler_plugin_spec.rb index e10e19a..117a2b7 100644 --- a/spec/lib/shanty/plugins/bundler_plugin_spec.rb +++ b/spec/lib/shanty/plugins/bundler_plugin_spec.rb @@ -1,29 +1,26 @@ require 'spec_helper' require 'shanty/plugins/bundler_plugin' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(BundlerPlugin) do - include_context('graph') +RSpec.describe(Shanty::Plugins::BundlerPlugin) do + include_context('plugin') - it('adds the bundler tag automatically') do - expect(described_class.tags).to match_array([:bundler]) - end + it('adds the bundler tag automatically') do + expect(described_class).to provide_tags(:bundler) + end - it('finds projects that have a Gemfile') do - expect(described_class).to define_projects.with('**/Gemfile') - end + it('finds projects that have a Gemfile') do + expect(described_class).to provide_projects_containing('**/Gemfile') + end - it('subscribes to the build event') do - expect(described_class).to subscribe_to(:build).with(:bundle_install) - end + it('subscribes to the build event') do + expect(described_class).to subscribe_to(:build).with(:bundle_install) + end - describe('#bundle_install') do - it('calls bundler install') do - expect(subject).to receive(:system).with('bundle install --quiet') + describe('#bundle_install') do + it('calls bundler install') do + expect(subject).to receive(:system).with('bundle install --quiet') - subject.bundle_install(project) - end + subject.bundle_install end end end diff --git a/spec/lib/shanty/plugins/cucumber_plugin_spec.rb b/spec/lib/shanty/plugins/cucumber_plugin_spec.rb index 7bbb45e..71928f8 100644 --- a/spec/lib/shanty/plugins/cucumber_plugin_spec.rb +++ b/spec/lib/shanty/plugins/cucumber_plugin_spec.rb @@ -1,49 +1,52 @@ require 'spec_helper' require 'shanty/plugins/cucumber_plugin' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(CucumberPlugin) do - include_context('graph') +RSpec.describe(Shanty::Plugins::CucumberPlugin) do + include_context('plugin') - it('adds the cucumber tag automatically') do - expect(described_class.tags).to match_array([:cucumber]) - end + it('adds the cucumber tag automatically') do + expect(described_class).to provide_tags(:cucumber) + end - it('finds projects by calling a method to locate the ones that depend on Cucumber') do - expect(described_class).to define_projects.with(:cucumber_projects) - end + it('finds projects by calling a method to locate the ones that depend on Cucumber') do + expect(described_class).to provide_projects(:cucumber_projects) + end - it('subscribes to the test event') do - expect(described_class).to subscribe_to(:test).with(:cucumber) - end + it('subscribes to the test event') do + expect(described_class).to subscribe_to(:test).with(:cucumber) + end - describe('#cucumber_projects') do - it('returns projects with a dependency on Cucumber in a Gemfile') do - File.write(File.join(project_paths[:one], 'Gemfile'), "gem 'cucumber', '~> 1.2.3'") + describe('#cucumber_projects') do + it('returns projects with a dependency on Cucumber in a Gemfile') do + gemfile_path = File.join(project_paths.first, 'Gemfile') + File.write(gemfile_path, "gem 'cucumber', '~> 1.2.3'") + allow(file_tree).to receive(:glob).and_return([gemfile_path]) - expect(subject.cucumber_projects).to match_array([projects[:one]]) - end + expect(described_class.cucumber_projects(env).map(&:path)).to contain_exactly(project_paths.first) + end - it('returns projects with a dependency on Cucumber in a *.gemspec') do - File.write(File.join(project_paths[:two], 'foo.gemspec'), "gem.add_dependency 'cucumber', '~> 1.2.3") + it('returns projects with a dependency on Cucumber in a *.gemspec') do + gemspec_path = File.join(project_paths[1], 'foo.gemspec') + File.write(gemspec_path, "gem.add_dependency 'cucumber', '~> 1.2.3") + allow(file_tree).to receive(:glob).and_return([gemspec_path]) - expect(subject.cucumber_projects).to match_array([projects[:two]]) - end + expect(described_class.cucumber_projects(env).map(&:path)).to contain_exactly(project_paths[1]) + end - it('does not return projects with no dependency on Cucumber') do - File.write(File.join(project_paths[:three], 'Gemfile'), "gem 'foo', '~> 1.2.3'") + it('does not return projects with no dependency on Cucumber') do + gemfile_path = File.join(project_paths[2], 'Gemfile') + File.write(gemfile_path, "gem 'foo', '~> 1.2.3'") + allow(file_tree).to receive(:glob).and_return([gemfile_path]) - expect(subject.cucumber_projects).to be_empty - end + expect(described_class.cucumber_projects(env)).to be_empty end + end - describe('#cucumber') do - it('calls cucumber') do - expect(subject).to receive(:system).with('cucumber') + describe('#cucumber') do + it('calls cucumber') do + expect(subject).to receive(:system).with('cucumber') - subject.cucumber(project) - end + subject.cucumber end end end diff --git a/spec/lib/shanty/plugins/rspec_plugin_spec.rb b/spec/lib/shanty/plugins/rspec_plugin_spec.rb index a3db2b8..03d5b5d 100644 --- a/spec/lib/shanty/plugins/rspec_plugin_spec.rb +++ b/spec/lib/shanty/plugins/rspec_plugin_spec.rb @@ -1,50 +1,52 @@ -require 'fileutils' require 'spec_helper' require 'shanty/plugins/rspec_plugin' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(RspecPlugin) do - include_context('graph') +RSpec.describe(Shanty::Plugins::RspecPlugin) do + include_context('plugin') - it('adds the rspec tag automatically') do - expect(described_class.tags).to match_array([:rspec]) - end + it('adds the rspec tag automatically') do + expect(described_class).to provide_tags(:rspec) + end - it('finds projects by calling a method to locate the ones that depend on RSpec') do - expect(described_class).to define_projects.with(:rspec_projects) - end + it('finds projects by calling a method to locate the ones that depend on Rspec') do + expect(described_class).to provide_projects(:rspec_projects) + end - it('subscribes to the test event') do - expect(described_class).to subscribe_to(:test).with(:rspec) - end + it('subscribes to the test event') do + expect(described_class).to subscribe_to(:test).with(:rspec) + end - describe('#rspec_projects') do - it('returns projects with a dependency on RSpec in a Gemfile') do - File.write(File.join(project_paths[:one], 'Gemfile'), "gem 'rspec', '~> 1.2.3'") + describe('#rspec_projects') do + it('returns projects with a dependency on RSpec in a Gemfile') do + gemfile_path = File.join(project_paths.first, 'Gemfile') + File.write(gemfile_path, "gem 'rspec', '~> 1.2.3'") + allow(file_tree).to receive(:glob).and_return([gemfile_path]) - expect(subject.rspec_projects).to match_array([projects[:one]]) - end + expect(described_class.rspec_projects(env).map(&:path)).to contain_exactly(project_paths.first) + end - it('returns projects with a dependency on RSpec in a *.gemspec') do - File.write(File.join(project_paths[:two], 'foo.gemspec'), "gem.add_dependency 'rspec', '~> 1.2.3") + it('returns projects with a dependency on RSpec in a *.gemspec') do + gemspec_path = File.join(project_paths[1], 'foo.gemspec') + File.write(gemspec_path, "gem.add_dependency 'rspec', '~> 1.2.3") + allow(file_tree).to receive(:glob).and_return([gemspec_path]) - expect(subject.rspec_projects).to match_array([projects[:two]]) - end + expect(described_class.rspec_projects(env).map(&:path)).to contain_exactly(project_paths[1]) + end - it('does not return projects with no dependency on RSpec') do - File.write(File.join(project_paths[:three], 'Gemfile'), "gem 'foo', '~> 1.2.3'") + it('does not return projects with no dependency on RSpec') do + gemfile_path = File.join(project_paths[2], 'Gemfile') + File.write(gemfile_path, "gem 'foo', '~> 1.2.3'") + allow(file_tree).to receive(:glob).and_return([gemfile_path]) - expect(subject.rspec_projects).to be_empty - end + expect(described_class.rspec_projects(env)).to be_empty end + end - describe('#rspec') do - it('calls rspec') do - expect(subject).to receive(:system).with('rspec') + describe('#rspec') do + it('calls rspec') do + expect(subject).to receive(:system).with('rspec') - subject.rspec(project) - end + subject.rspec end end end diff --git a/spec/lib/shanty/plugins/rubocop_plugin_spec.rb b/spec/lib/shanty/plugins/rubocop_plugin_spec.rb index 85244e2..80a9e61 100644 --- a/spec/lib/shanty/plugins/rubocop_plugin_spec.rb +++ b/spec/lib/shanty/plugins/rubocop_plugin_spec.rb @@ -1,29 +1,26 @@ require 'spec_helper' require 'shanty/plugins/rubocop_plugin' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(RubocopPlugin) do - include_context('graph') +RSpec.describe(Shanty::Plugins::RubocopPlugin) do + include_context('plugin') - it('adds the rubocop tag automatically') do - expect(described_class.tags).to match_array([:rubocop]) - end + it('adds the rubocop tag automatically') do + expect(described_class).to provide_tags(:rubocop) + end - it('finds projects that have a .rubocop.yml file') do - expect(described_class).to define_projects.with('**/.rubocop.yml') - end + it('finds projects that have a .rubocop.yml file') do + expect(described_class).to provide_projects_containing('**/.rubocop.yml') + end - it('subscribes to the test event') do - expect(described_class).to subscribe_to(:test).with(:rubocop) - end + it('subscribes to the test event') do + expect(described_class).to subscribe_to(:test).with(:rubocop) + end - describe('#rubocop') do - it('calls rubocop') do - expect(subject).to receive(:system).with('rubocop') + describe('#rubocop') do + it('calls rubocop') do + expect(subject).to receive(:system).with('rubocop') - subject.rubocop(project) - end + subject.rubocop end end end diff --git a/spec/lib/shanty/plugins/rubygem_plugin_spec.rb b/spec/lib/shanty/plugins/rubygem_plugin_spec.rb index 32cb937..b2e480e 100644 --- a/spec/lib/shanty/plugins/rubygem_plugin_spec.rb +++ b/spec/lib/shanty/plugins/rubygem_plugin_spec.rb @@ -2,57 +2,57 @@ require 'fileutils' require 'shanty/plugins/rubygem_plugin' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(RubygemPlugin) do - include_context('graph') - let(:gemspec_path) { File.join(project_path, 'foo.gemspec') } - - before do - File.write(gemspec_path, <<-eof) - Gem::Specification.new do |gem| - gem.name = 'foo' - gem.version = '1.2.3' - end - eof - end +RSpec.describe(Shanty::Plugins::RubygemPlugin) do + include_context('plugin') + let(:gemspec_path) { File.join(project_path, 'foo.gemspec') } + + before do + allow(file_tree).to receive(:glob).and_return([gemspec_path]) + allow(project).to receive(:path).and_return(project_paths.first) + + File.write(gemspec_path, <<-eof) + Gem::Specification.new do |gem| + gem.name = 'foo' + gem.version = '1.2.3' + end + eof + end - it('adds the gem tag') do - expect(described_class).to add_tags(:gem) - end + it('adds the gem tag') do + expect(described_class).to provide_tags(:rubygem, :gem) + end - it('adds the rubygem tag automatically') do - expect(described_class.tags).to match_array([:rubygem, :gem]) - end + it('adds the rubygem tag automatically') do + expect(described_class).to provide_tags(:rubygem) + end - it('finds projects that have a *.gemspec file') do - expect(described_class).to define_projects.with('**/*.gemspec') - end + it('finds projects that have a *.gemspec file') do + expect(described_class).to provide_projects_containing('**/*.gemspec') + end - it('subscribes to the build event') do - expect(described_class).to subscribe_to(:build).with(:build_gem) - end + it('subscribes to the build event') do + expect(described_class).to subscribe_to(:build).with(:build_gem) + end - describe('#build_gem') do - it('calls gem build') do - expect(subject).to receive(:system).with("gem build #{gemspec_path}") + describe('#build_gem') do + it('calls gem build') do + expect(subject).to receive(:system).with("gem build #{gemspec_path}") - subject.build_gem(project) - end + subject.build_gem end + end - describe('#artifacts') do - let(:gem_path) { File.join(project_path, 'foo-1.2.3.gem') } + describe('#artifacts') do + let(:gem_path) { File.join(project_path, 'foo-1.2.3.gem') } - it('lists the project artifacts') do - result = subject.artifacts(project) + it('lists the project artifacts') do + result = subject.artifacts - expect(result.length).to eql(1) - expect(result.first.to_local_path).to eql(gem_path) - expect(result.first.file_extension).to eql('gem') - expect(result.first.local?).to be true - expect(result.first.uri.scheme).to eql('file') - end + expect(result.length).to eql(1) + expect(result.first.to_local_path).to eql(gem_path) + expect(result.first.file_extension).to eql('gem') + expect(result.first.local?).to be true + expect(result.first.uri.scheme).to eql('file') end end end diff --git a/spec/lib/shanty/plugins/shantyfile_plugin_spec.rb b/spec/lib/shanty/plugins/shantyfile_plugin_spec.rb index 47ffe57..edd1f37 100644 --- a/spec/lib/shanty/plugins/shantyfile_plugin_spec.rb +++ b/spec/lib/shanty/plugins/shantyfile_plugin_spec.rb @@ -1,39 +1,43 @@ require 'spec_helper' require 'shanty/plugins/shantyfile_plugin' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(ShantyfilePlugin) do - include_context('graph') +RSpec.describe(Shanty::Plugins::ShantyfilePlugin) do + include_context('plugin') - it('adds the shantyfile tag shantyfile') do - expect(described_class.tags).to match_array([:shantyfile]) - end + it('adds the shantyfile tag automatically') do + expect(described_class).to provide_tags(:shantyfile) + end - it('finds projects by calling a method to locate the ones that have a Shantyfile') do - expect(described_class).to define_projects.with(:shantyfile_projects) - end + it('finds projects by calling a method to locate the ones that have a Shantyfile') do + expect(described_class).to provide_projects(:shantyfile_projects) + end - describe('#shantyfile_projects') do - it('finds all projects with a Shantyfile') do - FileUtils.touch(File.join(project_paths[:one], 'Shantyfile')) - FileUtils.touch(File.join(project_paths[:three], 'Shantyfile')) + describe('#shantyfile_projects') do + before do + allow(file_tree).to receive(:glob).and_return(shantyfiles) - expect(subject.shantyfile_projects).to match_array([ - projects[:one], - projects[:three] - ]) + shantyfiles.each_with_index do |shantyfile, i| + File.write(shantyfile, "instance_variable_set(:@this_is_a_test, #{i})") end + end - it('executes the found Shantyfiles within the context of the project') do - File.write(File.join(project_paths[:one], 'Shantyfile'), 'instance_variable_set(:@this_is_a_test, "foo")') - File.write(File.join(project_paths[:three], 'Shantyfile'), 'instance_variable_set(:@this_is_a_test, "bar")') + let(:shantyfiles) do + [ + File.join(project_paths.first, 'Shantyfile'), + File.join(project_paths[2], 'Shantyfile') + ] + end + + it('finds all projects with a Shantyfile') do + result = described_class.shantyfile_projects(env).map(&:path) + expect(result).to contain_exactly(project_paths.first, project_paths[2]) + end - subject.shantyfile_projects + it('executes the found Shantyfiles within the context of the project') do + projects = described_class.shantyfile_projects(env) - expect(projects[:one].instance_variable_get(:@this_is_a_test)).to eq('foo') - expect(projects[:three].instance_variable_get(:@this_is_a_test)).to eq('bar') - end + expect(projects.first.instance_variable_get(:@this_is_a_test)).to eq(0) + expect(projects[1].instance_variable_get(:@this_is_a_test)).to eq(1) end end end diff --git a/spec/lib/shanty/project_sorter_spec.rb b/spec/lib/shanty/project_sorter_spec.rb deleted file mode 100644 index 85ac155..0000000 --- a/spec/lib/shanty/project_sorter_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'spec_helper' -require 'tmpdir' -require 'shanty/project_sorter' - -# Allows all classes to be refereneced without the module name -module Shanty - RSpec.describe(ProjectSorter) do - include_context('graph') - subject { ProjectSorter.new(projects.values) } - - describe('#sort') do - it("returns a graph with the projects sorted using Tarjan's strongly connected components algorithm") do - projects[:two].add_parent(projects[:one]) - projects[:three].add_parent(projects[:two]) - - graph = subject.sort - - expect(graph[0].path).to eql(project_paths[:one]) - expect(graph[1].path).to eql(project_paths[:two]) - expect(graph[2].path).to eql(project_paths[:three]) - end - end - end -end diff --git a/spec/lib/shanty/project_spec.rb b/spec/lib/shanty/project_spec.rb index 801cacb..ea4e1af 100644 --- a/spec/lib/shanty/project_spec.rb +++ b/spec/lib/shanty/project_spec.rb @@ -1,157 +1,159 @@ require 'spec_helper' require 'shanty/project' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(Project) do - include_context('graph') - subject { Shanty::Project.new(project_path) } +RSpec.describe(Shanty::Project) do + include_context('workspace') + subject { described_class.new(project_path, env) } + let(:env) do + double('env').tap do |d| + allow(d).to receive(:root) { root } + end + end + let(:plugin) { double('plugin') } + let(:plugin_class) { double('plugin class') } - describe('#name') do - it('returns the name from the project in the constructor') do - expect(subject.name).to eql('one') - end + describe('#name') do + it('returns the name from the project in the constructor') do + expect(subject.name).to eql('one') end + end - describe('#path') do - it('returns the path from the project in the constructor') do - expect(subject.path).to eql(project_path) - end + describe('#path') do + it('returns the path from the project in the constructor') do + expect(subject.path).to eql(project_path) end + end - describe('#tags') do - it('defaults the tags to an empty array') do - expect(subject.tags).to eql([]) - end + describe('#tags') do + it('defaults the tags to an empty array') do + expect(subject.tags).to eql([]) end + end - describe('#options') do - it('defaults the options to an empty object') do - expect(subject.options).to eql({}) - end + describe('#config') do + it('defaults the config to an empty object') do + expect(subject.config).to eql({}) end + end - describe('#add_plugin') do - it('adds the given plugin to the project') do - plugin = double('plugin') + describe('#add_plugin') do + it('adds the given plugin to the project') do + allow(plugin_class).to receive(:new).and_return(plugin) - subject.add_plugin(plugin) + subject.add_plugin(plugin_class) - expect(subject.instance_variable_get(:@plugins)).to match_array([plugin]) - end + expect(subject.instance_variable_get(:@plugins)).to match_array([plugin]) end + end - describe('#remove_plugin') do - it('removes any plugins of the given class') do - plugin_class = Class.new - plugin = plugin_class.new - subject.instance_variable_set(:@plugins, [plugin]) + describe('#remove_plugin') do + it('removes any plugins of the given class') do + subject.instance_variable_set(:@plugins, [plugin]) + allow(plugin).to receive(:is_a?).with(plugin_class).and_return(true) - subject.remove_plugin(plugin_class) + subject.remove_plugin(plugin_class) - expect(subject.instance_variable_get(:@plugins)).to eql([]) - end + expect(subject.instance_variable_get(:@plugins)).to eql([]) end + end - describe('#all_artifacts') do - it('defaults the artifacts to an empty array') do - expect(subject.all_artifacts).to eql([]) - end + describe('#all_artifacts') do + it('defaults the artifacts to an empty array') do + expect(subject.all_artifacts).to eql([]) end + end - describe('#changed?') do - before do - # FIXME: Remove this when the default of changed is properly set to - # false once changed detection is actually working. - subject.instance_variable_set(:@changed, false) - end + describe('#changed?') do + before do + # FIXME: Remove this when the default of changed is properly set to + # false once changed detection is actually working. + subject.instance_variable_set(:@changed, false) + end - it('defaults changed to false') do - pending(<<-eof) - This will only pass once the default value is set to true when we - have proper change detection working. - eof - # FIXME: Delete the following setup eventually. - subject.instance_variable_set(:@changed, true) + it('defaults changed to false') do + pending(<<-eof) + This will only pass once the default value is set to true when we + have proper change detection working. + eof + # FIXME: Delete the following setup eventually. + subject.instance_variable_set(:@changed, true) - expect(subject.changed?).to be(false) - end + expect(subject.changed?).to be(false) + end - it('returns true if the changed flag is true on the current project') do - subject.instance_variable_set(:@changed, true) + it('returns true if the changed flag is true on the current project') do + subject.instance_variable_set(:@changed, true) - expect(subject.changed?).to be(true) - end + expect(subject.changed?).to be(true) + end - it('returns true if any of the parents are changed') do - parent = double('parent') - allow(parent).to receive(:add_child) - allow(parent).to receive(:changed?).and_return(true) - subject.add_parent(parent) + it('returns true if any of the parents are changed') do + parent = double('parent') + allow(parent).to receive(:add_child) + allow(parent).to receive(:changed?).and_return(true) + subject.add_parent(parent) - expect(subject.changed?).to be(true) - end + expect(subject.changed?).to be(true) + end - it('returns false if the changed flag is false and the parents are unchanged') do - expect(subject.changed?).to be(false) - end + it('returns false if the changed flag is false and the parents are unchanged') do + expect(subject.changed?).to be(false) end + end - describe('#changed!') do - it('adds the changed flag to true on the current project') do - subject.changed! + describe('#changed!') do + it('adds the changed flag to true on the current project') do + subject.changed! - expect(subject.changed?).to be(true) - end + expect(subject.changed?).to be(true) end + end - describe('#publish') do - before { subject.add_plugin(plugin) } - let(:plugin) { double('plugin') } - - it('skips over any plugins that are not subscribed to the event') do - allow(plugin).to receive(:subscribed?).and_return(false) + describe('#publish') do + before { subject.instance_variable_get(:@plugins) << plugin } + let(:plugin) { double('plugin') } - subject.publish(:foo) + it('skips over any plugins that are not subscribed to the event') do + allow(plugin).to receive(:subscribed?).and_return(false) - expect(subject).to_not receive(:publish) - end + subject.publish(:foo) - it('publishes the event on any listening plugins') do - allow(plugin).to receive(:subscribed?).and_return(true) + expect(subject).to_not receive(:publish) + end - expect(plugin).to receive(:publish).with(:foo, subject, :bar, :lux) + it('publishes the event on any listening plugins') do + allow(plugin).to receive(:subscribed?).and_return(true) + expect(plugin).to receive(:publish).with(:foo, :bar, :lux).and_return(true) - subject.publish(:foo, :bar, :lux) - end + subject.publish(:foo, :bar, :lux) + end - it('returns false early if any of the plugin publishes return false') do - next_plugin = double('next plugin') - project.add_plugin(next_plugin) - allow(plugin).to receive(:subscribed?).and_return(true) - expect(plugin).to receive(:publish).and_return(false) + it('returns false early if any of the plugin publish calls return false') do + next_plugin = double('next plugin') + subject.instance_variable_get(:@plugins) << next_plugin + allow(plugin).to receive(:subscribed?).and_return(true) + expect(plugin).to receive(:publish).and_return(false) - expect(subject.publish(:foo)).to be(false) - expect(next_plugin).to_not receive(:subscribed?) - end + expect(subject.publish(:foo)).to be(false) + expect(next_plugin).to_not receive(:subscribed?) end + end - describe('#to_s') do - it('returns a simple string representation of the project') do - expect(subject.to_s).to eql('one') - end + describe('#to_s') do + it('returns a simple string representation of the project') do + expect(subject.to_s).to eql('one') end + end - describe('#inspect') do - it('returns a detailed string representation of the project') do - expect(subject.inspect).to eql({ - name: 'one', - path: project_path, - tags: [], - options: {}, - parents: [] - }.inspect) - end + describe('#inspect') do + it('returns a detailed string representation of the project') do + expect(subject.inspect).to eql({ + name: 'one', + path: project_path, + tags: [], + config: {}, + parents: [] + }.inspect) end end end diff --git a/spec/lib/shanty/project_tree_spec.rb b/spec/lib/shanty/project_tree_spec.rb deleted file mode 100644 index cb63e5e..0000000 --- a/spec/lib/shanty/project_tree_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'spec_helper' -require 'shanty/plugin' - -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(ProjectTree) do - include_context('basics') - subject { described_class.new(root) } - - before do - [ - File.join(project_paths[:one], 'Shantyfile'), - # FIXME: Right now, the ignores are not procesed. This will be - # implemented as part of issue #9. - # File.join(project_paths[:two], 'ignored'), - File.join(project_paths[:three], 'Shantyfile') - ].each { |path| FileUtils.touch(path) } - - File.write(File.join(root, '.gitignore'), 'ignored') - end - - describe('#files') do - it('returns all the files within the root') do - expect(subject.files).to match_array([ - File.join(root, '.shanty.yml'), - File.join(root, '.gitignore'), - File.join(project_paths[:one], 'Shantyfile'), - File.join(project_paths[:three], 'Shantyfile') - ]) - end - end - - describe('#glob') do - it('returns all the files within the root that match any of the given globs') do - expect(subject.glob('**/Shantyfile', 'badglob')).to match_array([ - File.join(project_paths[:one], 'Shantyfile'), - File.join(project_paths[:three], 'Shantyfile') - ]) - end - end - end -end diff --git a/spec/lib/shanty/task_env_spec.rb b/spec/lib/shanty/task_env_spec.rb deleted file mode 100644 index accf343..0000000 --- a/spec/lib/shanty/task_env_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' -require 'shanty/task_env' - -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(TaskEnv) do - include_context('graph') - - describe('#graph') do - it('discovers all the projects') do - expect(Plugin).to receive(:all_projects).and_return([]) - - subject.graph - end - - it('returns a graph') do - expect(subject.graph).to be_a(Graph) - end - end - end -end diff --git a/spec/lib/shanty/task_set_spec.rb b/spec/lib/shanty/task_set_spec.rb index 90d5f03..67dd228 100644 --- a/spec/lib/shanty/task_set_spec.rb +++ b/spec/lib/shanty/task_set_spec.rb @@ -1,78 +1,84 @@ require 'spec_helper' require 'shanty/task_set' -require_fixture 'test_task_set' -# All classes referenced belong to the shanty project -module Shanty - RSpec.describe(TaskSet) do - include_context('basics') - - after do - TaskSet.instance_variable_set(:@partial_task, nil) +RSpec.describe(Shanty::TaskSet) do + include_context('workspace') + subject { test_plugin.new(env, graph) } + let(:env) { double('env') } + let(:graph) { double('graph') } + let(:test_plugin) do + Class.new(described_class) do + desc 'foo [--cat CAT] [--dog] --catweasel CATWEASEL', 'test.foo.desc' + option :cat, desc: 'test.options.cat' + option :dog, type: :boolean, desc: 'test.options.dog' + option :catweasel, required: true, type: :string, desc: 'test.options.catweasel' + def foo(options) + options + end end + end - describe('.inherited') do - let(:task_set) { Class.new(TestTaskSet) } - - it('adds the inheriting task set to the array of registered task sets') do - TaskSet.inherited(task_set) + after do + described_class.instance_variable_set(:@partial_task, nil) + end - expect(TaskSet.instance_variable_get(:@task_sets)).to include(task_set) - end + describe('.tasks') do + it('returns the registered tasks') do + expect(test_plugin.tasks).to include(:foo) end + end - describe('.task_sets') do - it('returns the registered task sets') do - expect(TaskSet.task_sets).to include(TestTaskSet) - end + describe('.partial_task') do + it('returns a partial task') do + expect(test_plugin.partial_task).to eql(klass: test_plugin, options: {}) end + end - describe('.tasks') do - it('returns the registered tasks') do - expect(TestTaskSet.tasks).to include(:foo) - end - end + describe('.desc') do + it('sets the syntax field of the current partial task') do + described_class.desc('foo', 'bar') - describe('.partial_task') do - it('returns a partial task') do - expect(TestTaskSet.partial_task).to eql(klass: TestTaskSet, options: {}) - end + expect(described_class.instance_variable_get(:@partial_task)).to include(syntax: 'foo') end - describe('.desc') do - it('sets the syntax field of the current partial task') do - TaskSet.desc('foo', 'bar') + it('sets the desc field of the current partial task') do + described_class.desc('foo', 'bar') - expect(TaskSet.instance_variable_get(:@partial_task)).to include(syntax: 'foo') - end + expect(described_class.instance_variable_get(:@partial_task)).to include(desc: 'bar') + end + end - it('sets the desc field of the current partial task') do - TaskSet.desc('foo', 'bar') + describe('.option') do + it('sets the given option with the given attrs in the current partial task') do + described_class.option('foo', bar: 'lux') - expect(TaskSet.instance_variable_get(:@partial_task)).to include(desc: 'bar') - end + expect(described_class.instance_variable_get(:@partial_task)[:options]).to include('foo' => { bar: 'lux' }) end + end - describe('.option') do - it('sets the given option with the given attrs in the current partial task') do - TaskSet.option('foo', bar: 'lux') + describe('.method_added') do + it('adds the partial task to the list of tasks with the name of the method that was added') do + described_class.method_added(:woowoo) - expect(TaskSet.instance_variable_get(:@partial_task)[:options]).to include('foo' => { bar: 'lux' }) - end + expect(described_class.instance_variable_get(:@tasks)).to include(:woowoo) end - describe('.method_added') do - it('adds the partial task to the list of tasks with the name of the method that was added') do - TaskSet.method_added(:woowoo) + it('resets the partial task') do + described_class.method_added(:weewah) - expect(TaskSet.instance_variable_get(:@tasks)).to include(:woowoo) - end + expect(described_class.instance_variable_get(:@partial_task)).to be_nil + end + end - it('resets the partial task') do - TaskSet.method_added(:weewah) + describe('#env') do + it('returns the env passed to the constructor') do + expect(subject.env).to eql(env) + end + end - expect(TaskSet.instance_variable_get(:@partial_task)).to be_nil - end + describe('#graph') do + it('returns the graph passed to the constructor') do + expect(subject.graph).to eql(graph) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d4eba82..ed16d8e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,35 +1,21 @@ -require 'simplecov' require 'coveralls' - -SimpleCov.formatter = Coveralls::SimpleCov::Formatter -SimpleCov.start do - add_filter '/spec/' -end - require 'fileutils' require 'i18n' require 'logger' require 'pathname' +require 'simplecov' require 'tmpdir' -require 'shanty/env' -require 'shanty/graph' -require 'shanty/project' -require 'shanty/task_env' -require 'shanty/plugins/rspec_plugin' -require 'shanty/plugins/rubocop_plugin' -require 'shanty/plugins/shantyfile_plugin' -def require_fixture(path) - require File.join(__dir__, 'fixtures', path) -end +require_relative 'support/contexts/plugin' +require_relative 'support/contexts/workspace' +require_relative 'support/matchers/call_me_ruby' +require_relative 'support/matchers/plugin' -def require_matchers(path) - require File.join(__dir__, 'support', 'matchers', path) +SimpleCov.formatter = Coveralls::SimpleCov::Formatter +SimpleCov.start do + add_filter '/spec/' end -require_matchers 'call_me_ruby' -require_matchers 'plugin' - I18n.enforce_available_locales = false I18n.load_path = Dir[File.expand_path(File.join(__dir__, '..', 'translations', '*.yml'))] @@ -41,19 +27,12 @@ def require_matchers(path) config.mock_with(:rspec) do |mocks| mocks.verify_partial_doubles = true end - - config.before(:example) do - Shanty::Env.clear! - Shanty::TaskEnv.clear! - Shanty::Project.clear! - Shanty::Env.logger.level = Logger::ERROR - end end -RSpec.shared_context('basics') do +RSpec.shared_context('') do around do |example| FileUtils.touch(File.join(root, '.shanty.yml')) - project_paths.values.each do |project_path| + project_paths.each do |project_path| FileUtils.mkdir_p(project_path) end @@ -76,22 +55,5 @@ def require_matchers(path) three: File.join(root, 'two', 'three') } end - let(:project_path) { project_paths[:one] } -end - -RSpec.shared_context('graph') do - include_context('basics') - - let(:projects) do - project_paths.each_with_object({}) do |(key, project_path), acc| - acc[key] = Shanty::Project.new(project_path) - end - end - let(:project) { projects[:one] } - let(:project_path_trie) do - Containers::Trie.new.tap do |trie| - projects.values.map { |project| trie[project.path] = project } - end - end - let(:graph) { Shanty::Graph.new(project_path_trie, projects.values) } + let(:project_path) { project_paths.first } end diff --git a/spec/support/contexts/plugin.rb b/spec/support/contexts/plugin.rb new file mode 100644 index 0000000..7fdd447 --- /dev/null +++ b/spec/support/contexts/plugin.rb @@ -0,0 +1,18 @@ +require_relative 'workspace' + +RSpec.shared_context('plugin') do + include_context('workspace') + subject { described_class.new(project, env) } + + before do + allow(env).to receive(:file_tree).and_return(file_tree) + allow(env).to receive(:projects).and_return({}) + allow(env).to receive(:root).and_return(root) + + allow(file_tree).to receive(:glob).and_return([]) + end + + let(:project) { double('project') } + let(:env) { double('env') } + let(:file_tree) { double('file tree') } +end diff --git a/spec/support/contexts/workspace.rb b/spec/support/contexts/workspace.rb new file mode 100644 index 0000000..47e6287 --- /dev/null +++ b/spec/support/contexts/workspace.rb @@ -0,0 +1,24 @@ +RSpec.shared_context('workspace') do + around do |example| + FileUtils.touch(File.join(root, 'Shantyconfig')) + project_paths.each { |project_path| FileUtils.mkdir_p(project_path) } + + Dir.chdir(root) { example.run } + + FileUtils.rm_rf(root) + end + + # We have to use `realpath` for this as, at least on Mac OS X, the temporary + # dir path that is returned is actually a symlink. Shanty resolves this + # internally, so if we want to compare to any of the paths correctly we'll + # need to resolve it too. + let(:root) { Pathname.new(Dir.mktmpdir('shanty-test')).realpath } + let(:project_path) { project_paths.first } + let(:project_paths) do + [ + File.join(root, 'one'), + File.join(root, 'two'), + File.join(root, 'two', 'three') + ] + end +end diff --git a/spec/support/matchers/plugin.rb b/spec/support/matchers/plugin.rb index d411f13..ff077d8 100644 --- a/spec/support/matchers/plugin.rb +++ b/spec/support/matchers/plugin.rb @@ -1,18 +1,20 @@ # Something, something, darkside. module Shanty - RSpec::Matchers.define(:add_tags) do |*tags| + RSpec::Matchers.define(:provide_tags) do |*tags| match do |actual| - expect(actual.instance_variable_get(:@tags)).to include(*tags) + expect(actual.tags).to include(*tags) end end - RSpec::Matchers.define(:define_projects) do + RSpec::Matchers.define(:provide_projects) do |*syms| match do |actual| - expect(actual.instance_variable_get(:@project_matchers)).to include(*(@matchers || [])) + expect(actual.instance_variable_get(:@project_providers)).to include(*syms) end + end - chain :with do |*matchers| - @matchers = matchers + RSpec::Matchers.define(:provide_projects_containing) do |*globs| + match do |actual| + expect(actual.instance_variable_get(:@project_globs)).to include(*globs) end end end