diff --git a/.gitignore b/.gitignore index b3823fa..6e3f348 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.gem coverage .shanty +Gemfile.lock diff --git a/.rubocop.yml b/.rubocop.yml index 5721fc1..23954f6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,3 +9,11 @@ LineLength: # 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/.travis.yml b/.travis.yml index 4a79210..a7d6c7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,19 @@ +sudo: false language: ruby rvm: - - 2.2.0 -notifications: - webhooks: https://hubot-shantytown.rhcloud.com/hubot/travis?room=#shantytow n + - 2.2.3 + +# We have to install Rust manually, as Travis does not yet allow multiligual +# builds. We install the latest stable Rust build. +before_install: + - mkdir ~/rust-installer + - curl -sL https://static.rust-lang.org/rustup.sh -o ~/rust-installer/rustup.sh + - sh ~/rust-installer/rustup.sh --prefix=~/rust --spec=stable -y --disable-sudo 2> /dev/null + - export PATH=~/rust/bin:$PATH + - export LD_LIBRARY_PATH=~/rust/lib:$LD_LIBRARY_PATH + - rustc --version + - cargo --version script: bundle exec ./bin/shanty --trace test + +notifications: + webhooks: https://hubot-shantytown.rhcloud.com/hubot/travis?room=#shantytown diff --git a/Gemfile.lock b/Gemfile.lock index d579810..95a99bd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,94 +4,112 @@ PATH shanty (0.3.0) acts_as_graph_vertex (~> 1.0) algorithms (~> 0.6) - attr_combined_accessor (~> 1.0) - call_me_ruby (~> 1.0) + bundler (~> 1.10) + call_me_ruby (~> 1.1) commander (~> 4.3) + gitignore_rb (~> 0.2.2) i18n (~> 0.7) + shenanigans (~> 1.0) GEM remote: https://rubygems.org/ specs: acts_as_graph_vertex (1.0.0) algorithms (0.6.1) - ast (2.0.0) - astrolabe (1.3.0) - parser (>= 2.2.0.pre.3, < 3.0) - attr_combined_accessor (1.0.0) - byebug (4.0.5) + ast (2.1.0) + astrolabe (1.3.1) + parser (~> 2.2) + builder (3.2.2) + byebug (5.0.0) columnize (= 0.9.0) - call_me_ruby (1.0.2) + call_me_ruby (1.1.1) coderay (1.1.0) columnize (0.9.0) - commander (4.3.4) + commander (4.3.5) highline (~> 1.7.2) - coveralls (0.8.1) + coveralls (0.8.2) json (~> 1.8) rest-client (>= 1.6.8, < 2) simplecov (~> 0.10.0) term-ansicolor (~> 1.3) thor (~> 0.19.1) + cucumber (2.1.0) + builder (>= 2.1.2) + cucumber-core (~> 1.3.0) + diff-lcs (>= 1.1.3) + gherkin3 (~> 3.1.0) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.1.2) + cucumber-core (1.3.0) + gherkin3 (~> 3.1.0) diff-lcs (1.2.5) docile (1.1.5) domain_name (0.5.24) unf (>= 0.0.5, < 1.0.0) - filewatcher (0.5.1) + ffi (1.9.10) + filewatcher (0.5.2) trollop (~> 2.0) - highline (1.7.2) + gherkin3 (3.1.1) + gitignore_rb (0.2.4) + ffi (~> 1.9) + highline (1.7.7) http-cookie (1.0.2) domain_name (~> 0.5) i18n (0.7.0) - json (1.8.2) + json (1.8.3) method_source (0.8.2) - mime-types (2.6.1) + mime-types (2.6.2) + multi_json (1.11.2) + multi_test (0.1.2) netrc (0.10.3) - parser (2.2.2.5) + parser (2.2.2.6) ast (>= 1.1, < 3.0) powerpack (0.1.1) pry (0.10.1) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - pry-byebug (3.1.0) - byebug (~> 4.0) + pry-byebug (3.2.0) + byebug (~> 5.0) pry (~> 0.10) rainbow (2.0.0) rest-client (1.8.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) - rspec (3.2.0) - rspec-core (~> 3.2.0) - rspec-expectations (~> 3.2.0) - rspec-mocks (~> 3.2.0) - rspec-core (3.2.3) - rspec-support (~> 3.2.0) - rspec-expectations (3.2.1) + rspec (3.3.0) + rspec-core (~> 3.3.0) + rspec-expectations (~> 3.3.0) + rspec-mocks (~> 3.3.0) + rspec-core (3.3.2) + rspec-support (~> 3.3.0) + rspec-expectations (3.3.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.2.0) - rspec-mocks (3.2.1) + rspec-support (~> 3.3.0) + rspec-mocks (3.3.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.2.0) - rspec-support (3.2.2) - rubocop (0.31.0) + rspec-support (~> 3.3.0) + rspec-support (3.3.0) + rubocop (0.34.2) astrolabe (~> 1.3) - parser (>= 2.2.2.1, < 3.0) + parser (>= 2.2.2.5, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) - rubocop-rspec (1.3.0) + rubocop-rspec (1.3.1) ruby-prof (0.15.8) ruby-progressbar (1.7.5) + shenanigans (1.0.11) simplecov (0.10.0) docile (~> 1.1.0) json (~> 1.8) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slop (3.6.0) - term-ansicolor (1.3.0) + term-ansicolor (1.3.2) tins (~> 1.0) thor (0.19.1) - tins (1.5.2) + tins (1.6.0) trollop (2.1.2) unf (0.1.4) unf_ext @@ -102,6 +120,7 @@ PLATFORMS DEPENDENCIES coveralls (~> 0.8) + cucumber (~> 2.1) filewatcher (~> 0.5) pry-byebug (~> 3.1) rspec (~> 3.2) @@ -109,3 +128,6 @@ DEPENDENCIES rubocop-rspec (~> 1.3) ruby-prof (~> 0.15) shanty! + +BUNDLED WITH + 1.10.6 diff --git a/examples/test-static-project-2/Shantyfile b/examples/test-static-project-2/Shantyfile deleted file mode 100644 index f28e046..0000000 --- a/examples/test-static-project-2/Shantyfile +++ /dev/null @@ -1 +0,0 @@ -parent 'examples/test-static-project' diff --git a/examples/test-static-project-2/test-static-project-3/Shantyfile b/examples/test-static-project-2/test-static-project-3/Shantyfile deleted file mode 100644 index 44116f7..0000000 --- a/examples/test-static-project-2/test-static-project-3/Shantyfile +++ /dev/null @@ -1 +0,0 @@ -parent 'examples/test-static-project-2' diff --git a/examples/test-static-project/Shantyfile b/examples/test-static-project/Shantyfile deleted file mode 100644 index c468b15..0000000 --- a/examples/test-static-project/Shantyfile +++ /dev/null @@ -1,2 +0,0 @@ -option 'foo', 'bar' -tag 'lux' diff --git a/spec/lib/shanty/mixins/acts_as_graph_node_spec.rb b/features/support/env.rb similarity index 100% rename from spec/lib/shanty/mixins/acts_as_graph_node_spec.rb rename to features/support/env.rb diff --git a/lib/shanty/env.rb b/lib/shanty/env.rb index 4345245..cda99b7 100644 --- a/lib/shanty/env.rb +++ b/lib/shanty/env.rb @@ -1,7 +1,10 @@ +require 'i18n' require 'logger' require 'pathname' require 'yaml' +require 'shanty/project_tree' + module Shanty # module Env @@ -10,12 +13,17 @@ module Env CONFIG_FILE = '.shanty.yml' + # 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 + @@logger = nil + @@environment = nil + @@build_number = nil + @@root = nil + @@config = nil + @@project_tree = nil end def require! @@ -27,26 +35,34 @@ def require! end def logger - @logger ||= Logger.new(STDOUT) + @@logger ||= Logger.new(STDOUT).tap do |logger| + logger.formatter = proc do |_, datetime, _, msg| + "#{datetime}: #{msg}\n" + end + end end def environment - @environment ||= ENV['SHANTY_ENV'] || 'local' + @@environment ||= ENV['SHANTY_ENV'] || 'local' end def build_number - @build_number ||= (ENV['SHANTY_BUILD_NUMBER'] || 1).to_i + @@build_number ||= (ENV['SHANTY_BUILD_NUMBER'] || 1).to_i end def root - @root ||= find_root + @@root ||= find_root + end + + def project_tree + @@project_tree ||= ProjectTree.new(root) end def config - return @config unless @config.nil? - return @config = {} unless File.exist?(config_path) + return @@config unless @@config.nil? + return @@config = {} unless File.exist?(config_path) config = YAML.load_file(config_path) || {} - @config = config[environment] || {} + @@config = config[environment] || {} end private @@ -66,11 +82,7 @@ def config_path end def find_root - if root_dir.nil? - fail "Could not find a #{CONFIG_FILE} file in this or any parent directories. \ - Please run `shanty init` in the directory you want to be the root of your project structure." - end - + fail I18n.t('missing_root', config_file: CONFIG_FILE) if root_dir.nil? root_dir end diff --git a/lib/shanty/graph.rb b/lib/shanty/graph.rb index 3463738..56e131e 100644 --- a/lib/shanty/graph.rb +++ b/lib/shanty/graph.rb @@ -12,12 +12,14 @@ class Graph extend Forwardable include Enumerable - def_delegators :@projects, :<<, :empty?, :length, :each, :values, :[], :inspect, :to_s + def_delegators :@projects, :<<, :empty?, :length, :each, :values, :[], + :inspect, :to_s # Public: Initialise a Graph. # # project_path_trie - - # projects - An array of projects to use. These are expected to be pre-sorted and linked via the ProjectLinker. + # projects - An array of projects to use. These are expected to be + # pre-sorted and linked via the ProjectLinker. def initialize(project_path_trie, projects) @project_path_trie = project_path_trie @projects = projects @@ -41,8 +43,7 @@ def by_name(name) def all_with_tags(*tags) return [] if tags.empty? select do |project| - project_tags = project.tags.map(&:to_sym) - tags.map(&:to_sym).all? { |tag| project_tags.include?(tag) } + tags.map(&:to_sym).all? { |tag| project.all_tags.include?(tag) } end end diff --git a/lib/shanty/plugin.rb b/lib/shanty/plugin.rb index c955329..dfcabbc 100644 --- a/lib/shanty/plugin.rb +++ b/lib/shanty/plugin.rb @@ -1,103 +1,61 @@ +require 'call_me_ruby' require 'shanty/env' require 'shanty/project' module Shanty # Some basic functionality for every plugin. - module Plugin + class Plugin + include CallMeRuby include Env - def self.extended(plugin) - plugins << plugin - end - - def self.plugins + def self.inherited(plugin_class) @plugins ||= [] + @plugins << plugin_class.new end - # - - def self.discover_all_projects # => [Project] - plugins.flat_map(&:wanted_projects).uniq - end - - def self.with_graph(graph) - plugins.each do |plugin| - plugin.with_graph_callbacks.each { |callback| callback.call(graph) } - end - end - - # - - def callbacks - @callbacks ||= [] - end - - def tags - @tags ||= [] + def self.all_projects + (@plugins || []).flat_map(&:projects).uniq end - def wanted_project_globs - @wanted_project_globs ||= [] + def self.all_with_graph(graph) + (@plugins || []).each { |plugin| plugin.with_graph(graph) } end - def wanted_project_callbacks - @wanted_project_callbacks ||= [] + def self.tags(*args) + (@tags ||= []).concat(args.map(&:to_sym)) end - def with_graph_callbacks - @with_graph_callbacks ||= [] + def self.projects(*globs_or_syms) + (@project_matchers ||= []).concat(globs_or_syms) end - # - - def add_to_project(project) - project.singleton_class.include(self) - callbacks.each { |args| project.subscribe(*args) } - tags.each { |tag| project.tag(tag) } + def self.with_graph(&block) + (@with_graph_callbacks ||= []) << block end - # - - def subscribe(*args) - callbacks << args - end - - def adds_tags(*args) - tags.concat(args) - end - - def wants_projects_matching(*globs, &block) - wanted_project_globs.concat(globs) - wanted_project_callbacks << block if block_given? - end - - def with_graph(&block) - with_graph_callbacks << block + 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 - # - - def wanted_projects - (wanted_projects_from_globs + wanted_projects_from_callbacks).uniq.tap do |projects| - projects.each do |project| - project.plugin(self) - end - 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) } end private - def wanted_projects_from_globs - wanted_project_globs.flat_map do |globs| - # Will make the glob absolute to the root if (and only if) it is relative. - Dir[File.expand_path(globs, root)].map do |path| - Project.new(File.absolute_path(File.dirname(path))) - end - end + 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))) } end - def wanted_projects_from_callbacks - wanted_project_callbacks.flat_map(&:call) + def projects_from_callbacks(project_matchers) + project_matchers.find_all { |glob_or_sym| glob_or_sym.is_a?(Symbol) }.flat_map { |sym| send(sym) } end end end diff --git a/lib/shanty/plugins/bundler_plugin.rb b/lib/shanty/plugins/bundler_plugin.rb index 87cf10c..6499941 100644 --- a/lib/shanty/plugins/bundler_plugin.rb +++ b/lib/shanty/plugins/bundler_plugin.rb @@ -1,17 +1,18 @@ +require 'bundler' require 'shanty/plugin' module Shanty # Public: Bundler plugin for building ruby projects. - module BundlerPlugin - extend Plugin - - adds_tags :bundler - wants_projects_matching '**/Gemfile' + class BundlerPlugin < Plugin + tags :bundler + projects '**/Gemfile' subscribe :build, :bundle_install def bundle_install - # FIXME: Add support for the --jobs argument to parallelise the bundler run. - system 'bundle install --quiet' + 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 new file mode 100644 index 0000000..72a2d65 --- /dev/null +++ b/lib/shanty/plugins/cucumber_plugin.rb @@ -0,0 +1,24 @@ +require 'shanty/plugin' + +module Shanty + # Public: Cucumber plugin for testing ruby projects. + class CucumberPlugin < Plugin + tags :cucumber + 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 + + 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))) + end + end + + def cucumber + system 'cucumber' + end + end +end diff --git a/lib/shanty/plugins/rspec_plugin.rb b/lib/shanty/plugins/rspec_plugin.rb index 615dfda..6fd6366 100644 --- a/lib/shanty/plugins/rspec_plugin.rb +++ b/lib/shanty/plugins/rspec_plugin.rb @@ -2,22 +2,21 @@ module Shanty # Public: Rspec plugin for testing ruby projects. - module RspecPlugin - extend Plugin - - adds_tags :rspec + class RspecPlugin < Plugin + tags :rspec + 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!) - wants_projects_matching do - Dir['**/{*.gemspec,Gemfile}'].each_with_object([]) do |dependency_file, acc| + projects :rspec_projects + + 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))) end end - subscribe :test, :rspec - def rspec system 'rspec' end diff --git a/lib/shanty/plugins/rubocop_plugin.rb b/lib/shanty/plugins/rubocop_plugin.rb index c817a14..1c7241c 100644 --- a/lib/shanty/plugins/rubocop_plugin.rb +++ b/lib/shanty/plugins/rubocop_plugin.rb @@ -2,11 +2,9 @@ module Shanty # Public: Rubocop plugin for style checking ruby projects. - module RubocopPlugin - extend Plugin - - adds_tags :rubocop - wants_projects_matching '**/.rubocop.yml' + class RubocopPlugin < Plugin + tags :rubocop + projects '**/.rubocop.yml' subscribe :test, :rubocop def rubocop diff --git a/lib/shanty/plugins/rubygem_plugin.rb b/lib/shanty/plugins/rubygem_plugin.rb index 3cbcb07..44519b9 100644 --- a/lib/shanty/plugins/rubygem_plugin.rb +++ b/lib/shanty/plugins/rubygem_plugin.rb @@ -2,11 +2,9 @@ module Shanty # Public: Rubygem plugin for buildin gems. - module RubygemPlugin - extend Plugin - - adds_tags :rubygem - wants_projects_matching '**/*.gemspec' + class RubygemPlugin < Plugin + tags :rubygem + projects '**/*.gemspec' subscribe :build, :build_gem def build_gem diff --git a/lib/shanty/plugins/shantyfile_plugin.rb b/lib/shanty/plugins/shantyfile_plugin.rb index 43a8665..e83128c 100644 --- a/lib/shanty/plugins/shantyfile_plugin.rb +++ b/lib/shanty/plugins/shantyfile_plugin.rb @@ -2,10 +2,16 @@ module Shanty # Public: Plugin for finding all directories marked with a Shantyfile. - module ShantyfilePlugin - extend Plugin + class ShantyfilePlugin < Plugin + tags :shantyfile + projects :shantyfile_projects - adds_tags :shantyfile - wants_projects_matching '**/Shantyfile' + 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) + end + end + end end end diff --git a/lib/shanty/project.rb b/lib/shanty/project.rb index 830ff3c..a6ea843 100644 --- a/lib/shanty/project.rb +++ b/lib/shanty/project.rb @@ -1,6 +1,6 @@ require 'acts_as_graph_vertex' -require 'attr_combined_accessor' require 'call_me_ruby' +require 'pathname' require 'shanty/env' @@ -8,11 +8,9 @@ module Shanty # Public: Represents a project in the current repository. class Project include ActsAsGraphVertex - include CallMeRuby include Env - attr_combined_accessor :name, :tags, :options - attr_accessor :path, :parents_by_path + attr_accessor :name, :path, :tags, :options # Multiton or Flyweight pattern - only allow once instance per unique path. # @@ -36,37 +34,39 @@ def clear! # # path - The path to the project. def initialize(path) - fail('Path to project must be a directory.') unless File.directory?(path) + pathname = Pathname.new(File.expand_path(path, root)) + fail('Path to project must be a directory.') unless pathname.directory? @path = path @name = File.basename(path) - @parents_by_path = [] + @plugins = [] @tags = [] @options = {} end - def execute_shantyfile! - shantyfile_path = File.join(@path, 'Shantyfile') - return unless File.exist?(shantyfile_path) - instance_eval(File.read(shantyfile_path), shantyfile_path) + def add_plugin(plugin) + @plugins << plugin end - def plugin(plugin) - plugin.add_to_project(self) + def remove_plugin(plugin_class) + @plugins.delete_if { |plugin| plugin.is_a?(plugin_class) } end - def parent(parent) - # Will make the parent path absolute to the root if (and only if) it is relative. - @parents_by_path << File.expand_path(parent, root) - end - - def tag(tag) - @tags << tag + # Public: The tags assigned to this project, and any tags provided by any + # plugins operating on this project. + # + # Returns an Array of symbols representing the tags on this project. + def all_tags + (@tags + @plugins.flat_map { |plugin| plugin.class.tags }).map(&:to_sym).uniq end - def option(key, value) - @options[key] = value + 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, *args) == false + end end # Public: The absolute paths to the artifacts that would be created by this @@ -83,7 +83,7 @@ def artifact_paths # # Returns a simple String representation of this instance. def to_s - "#{name}" + name end # Public: Overriden String conversion method to return a more detailed @@ -95,18 +95,10 @@ def inspect { name: @name, path: @path, - tags: @tags, + tags: all_tags, options: @options, - parents_by_path: @parents_by_path + parents: parents.map(&:path) }.inspect end - - def within_project_dir - return unless block_given? - - Dir.chdir(path) do - yield - end - end end end diff --git a/lib/shanty/project_linker.rb b/lib/shanty/project_sorter.rb similarity index 56% rename from lib/shanty/project_linker.rb rename to lib/shanty/project_sorter.rb index 8604a7b..3f36793 100644 --- a/lib/shanty/project_linker.rb +++ b/lib/shanty/project_sorter.rb @@ -5,8 +5,8 @@ require 'shanty/graph' module Shanty - # Public: - class ProjectLinker + # Public: Sorts projects using Tarjan's strongly connected components algorithm. + class ProjectSorter include TSort # Public: Initialise a ProjectLinker. @@ -16,23 +16,12 @@ def initialize(projects) @projects = projects end - # Private: Given a list of projects, construct the parent/child - # relationships between them given a list of their parents/children by name - # as defined on the project instances. + # Private: Given a list of projects, sort them and construct a graph. # - # Returns a Graph with the projects linked and sorted. - def link - @projects.each do |project| - project.parents_by_path.each do |parent_path| - parent_dependency = project_path_trie.get(parent_path) - if parent_dependency.nil? - fail("Cannot find project at path #{parent_path}, which was specified as a dependency for #{project}") - end - - project.add_parent(parent_dependency) - end - end - + # The sorting uses Tarjan's strongly connected components algorithm. + # + # Returns a Graph with the projects sorted. + def sort Graph.new(project_path_trie, tsort) end diff --git a/lib/shanty/project_tree.rb b/lib/shanty/project_tree.rb new file mode 100644 index 0000000..d95783f --- /dev/null +++ b/lib/shanty/project_tree.rb @@ -0,0 +1,45 @@ +module Shanty + # Public: Encapsulates the directory tree of a Shanty project. This provides a + # way to glob the project tree in a performant way without globing through + # files that are ignored by Git, SVN some other VCS. + # + # FIXME: The ignores are not implemented yet, this work has been recorded in + # issue #9 (https://github.com/shantytown/shanty/issues/9). + class ProjectTree + # 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. + IGNORE_REGEX = /(vendor|.git)/ + + # Public: Initialise the ProjectTree instance. + # + # root - The absolute path to the root of the project within which any file + # operations should be performed. + def initialize(root) + @root = root + end + + # Public: Get the full list of files in the project tree, with any files + # ignored by Git, SVN or some other VCS removed from the list. + # + # Returns an Array of Strings where the strings are paths within the + # project. + def files + @files ||= Dir.glob(File.join(@root, '**/*'), GLOB_FLAGS).select do |path| + File.file?(path) && !(path =~ IGNORE_REGEX) + end + end + + # Public: Get a list of the files in the project tree that match any of the + # given globs, with any files ignored by Git, SVN or some other VCS + # removed from the list. + # + # Returns an Array of Strings where the strings are paths within the + # project. + def glob(*globs) + files.find_all do |path| + globs.any? { |pattern| File.fnmatch(pattern, path, File::FNM_EXTGLOB) } + end + end + end +end diff --git a/lib/shanty/task_env.rb b/lib/shanty/task_env.rb index c0790b6..0f71bba 100644 --- a/lib/shanty/task_env.rb +++ b/lib/shanty/task_env.rb @@ -1,5 +1,5 @@ require 'shanty/plugin' -require 'shanty/project_linker' +require 'shanty/project_sorter' module Shanty # @@ -12,11 +12,8 @@ def clear! end def graph - return @graph unless @graph.nil? - - projects = Plugin.discover_all_projects.each(&:execute_shantyfile!) - @graph ||= ProjectLinker.new(projects).link.tap do |graph| - Plugin.with_graph(graph) + @graph ||= ProjectSorter.new(Plugin.all_projects).sort.tap do |graph| + Plugin.all_with_graph(graph) end end end diff --git a/lib/shanty/task_sets/basic_task_set.rb b/lib/shanty/task_sets/basic_task_set.rb index 98ac234..045798a 100644 --- a/lib/shanty/task_sets/basic_task_set.rb +++ b/lib/shanty/task_sets/basic_task_set.rb @@ -14,31 +14,39 @@ def init desc 'projects [--tags TAG,TAG,...]', 'tasks.projects.desc' option :tags, type: :array, desc: 'tasks.common.options.tags' def projects(options) - filtered_graph(options).each { |project| puts project } + filtered_graph(*tags_from_options(options)).each do |project| + puts "#{project.name} (#{project.path})#{project.tags.map { |tag| "\n - #{tag}" }.join}" + 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(options, :build) + 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(options, :test) + run_common_task(:test, *tags_from_options(options)) end private - def run_common_task(options, task) - filtered_graph(options).each do |project| - fail I18n.t("tasks.#{task}.failed", project: project) unless project.publish(task) + 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) + end end end - def filtered_graph(options) - return scoped_graph.all_with_tags(*options.tags.split(',')) unless options.tags.nil? + def filtered_graph(*tags) + return scoped_graph.all_with_tags(*tags) unless tags.empty? scoped_graph end diff --git a/shanty.gemspec b/shanty.gemspec index be49f12..ca53658 100644 --- a/shanty.gemspec +++ b/shanty.gemspec @@ -21,13 +21,16 @@ Gem::Specification.new do |gem| gem.files = Dir['**/*'].select { |d| d =~ %r{^(README|bin/|ext/|lib/)} } gem.add_dependency 'acts_as_graph_vertex', '~>1.0' + gem.add_dependency 'bundler', '~>1.10' gem.add_dependency 'algorithms', '~>0.6' - gem.add_dependency 'attr_combined_accessor', '~>1.0' - gem.add_dependency 'call_me_ruby', '~>1.0' + gem.add_dependency 'call_me_ruby', '~>1.1' gem.add_dependency 'commander', '~>4.3' + 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' gem.add_development_dependency 'filewatcher', '~>0.5' gem.add_development_dependency 'pry-byebug', '~> 3.1' gem.add_development_dependency 'rspec', '~> 3.2' diff --git a/spec/fixtures/test_plugin.rb b/spec/fixtures/test_plugin.rb deleted file mode 100644 index 0553821..0000000 --- a/spec/fixtures/test_plugin.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'shanty/plugin' - -module Shanty - # Test Plugin fixture. - module TestPlugin - extend Plugin - - adds_tags :test - subscribe :foo, :bar - subscribe :cats, :dogs, :rabies - - def bar; end - - def rabies; end - end -end diff --git a/spec/fixtures/test_project_with_plugin.rb b/spec/fixtures/test_project_with_plugin.rb deleted file mode 100644 index 101306c..0000000 --- a/spec/fixtures/test_project_with_plugin.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'shanty/project' -require_fixture 'test_plugin' - -module Shanty - # Test Project fixture which includes the test Plugin. - class TestProjectWithPlugin < Project - include TestPlugin - end -end diff --git a/spec/fixtures/test_unused_plugin.rb b/spec/fixtures/test_unused_plugin.rb index 0fe009e..b488fdd 100644 --- a/spec/fixtures/test_unused_plugin.rb +++ b/spec/fixtures/test_unused_plugin.rb @@ -3,7 +3,5 @@ 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. - module UnusedPlugin - extend Plugin - end + class UnusedPlugin < Plugin; end end diff --git a/spec/lib/shanty/env_spec.rb b/spec/lib/shanty/env_spec.rb index ed88234..a232bb5 100644 --- a/spec/lib/shanty/env_spec.rb +++ b/spec/lib/shanty/env_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' require 'fileutils' +require 'i18n' require 'tmpdir' require 'shanty/env' @@ -103,7 +104,7 @@ module Shanty 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 + expect { subject.root }.to raise_error(I18n.t('missing_root')) end end diff --git a/spec/lib/shanty/graph_spec.rb b/spec/lib/shanty/graph_spec.rb index 204765e..89724f6 100644 --- a/spec/lib/shanty/graph_spec.rb +++ b/spec/lib/shanty/graph_spec.rb @@ -3,9 +3,6 @@ require 'shanty/graph' require 'shanty/project' -require_fixture 'test_plugin' -require_fixture 'test_unused_plugin' - # Allows all classes to be refereneced without the module name module Shanty RSpec.describe(Graph) do @@ -29,6 +26,10 @@ module Shanty end describe('#all_with_tags') do + before do + subject.first.tags << 'test' + end + it('returns an empty array when no tags are given') do expect(subject.all_with_tags).to be_empty end @@ -38,7 +39,7 @@ module Shanty end it('returns the correct projects when matching tags are given') do - expect(subject.all_with_tags('test')).to match_array(subject) + expect(subject.all_with_tags('test')).to match_array([subject.first]) end end diff --git a/spec/lib/shanty/plugin_spec.rb b/spec/lib/shanty/plugin_spec.rb index 0e8c5cd..1dfa7f5 100644 --- a/spec/lib/shanty/plugin_spec.rb +++ b/spec/lib/shanty/plugin_spec.rb @@ -1,36 +1,125 @@ require 'spec_helper' require 'shanty/plugin' -require_fixture 'test_plugin' -require_fixture 'test_project_with_plugin' - # All classes referenced belong to the shanty project module Shanty RSpec.describe(Plugin) do - include_context('basics') - subject { TestPlugin } - let(:project) { TestProjectWithPlugin.new(project_path) } - let(:callbacks) { [%i(foo bar), %i(cats dogs rabies)] } + 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 } - describe('.add_to_project') do - it('includes the plugin into the singleton class of the given project') do - expect(project.singleton_class).to receive(:include).with(subject) + 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 - subject.add_to_project(project) + 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 - it('calls subscribe on the project for each callback in the plugin') do - callbacks.each do |callback| - expect(project).to receive(:subscribe).with(*callback) - end + 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 + end + + describe('.all_with_graph') do + before do + described_class.instance_variable_set(:@plugins, [subject]) + end - subject.add_to_project(project) + 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('.subscribe') do - it('stores all the given arguments in the callbacks') do - expect(subject.instance_variable_get(:@callbacks)).to contain_exactly(*callbacks) + describe('.tags') do + it('stores the given tags') do + plugin_class.tags(:foo, :marbles) + + expect(plugin_class.instance_variable_get(:@tags)).to match_array([:foo, :marbles]) + end + + it('converts any given tags to symbols') do + plugin_class.tags('bar', 'lux') + + expect(plugin_class.instance_variable_get(:@tags)).to match_array([:bar, :lux]) + end + end + + describe('.projects') do + it('stores the given globs or symbols') do + plugin_class.projects('**/foo', :bar) + + expect(plugin_class.instance_variable_get(:@project_matchers)).to match_array(['**/foo', :bar]) + end + 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 + + describe('#projects') do + let(:project_tree) { double('project_tree') } + + it('returns no projects if there are no matchers') do + expect(subject.projects).to be_empty + end + + 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 + 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 end end diff --git a/spec/lib/shanty/plugins/bundler_plugin_spec.rb b/spec/lib/shanty/plugins/bundler_plugin_spec.rb index 0f05f7b..32c14b2 100644 --- a/spec/lib/shanty/plugins/bundler_plugin_spec.rb +++ b/spec/lib/shanty/plugins/bundler_plugin_spec.rb @@ -5,10 +5,17 @@ module Shanty RSpec.describe(BundlerPlugin) do include_context('basics') - subject { Class.new { include BundlerPlugin }.new } + + it('adds the bundler tag') do + expect(described_class).to add_tags(:bundler) + end + + it('finds projects that have a Gemfile') do + expect(described_class).to define_projects.with('**/Gemfile') + end it('subscribes to the build event') do - expect(BundlerPlugin.callbacks).to include([:build, :bundle_install]) + expect(described_class).to subscribe_to(:build).with(:bundle_install) end describe('#bundle_install') do diff --git a/spec/lib/shanty/plugins/cucumber_plugin_spec.rb b/spec/lib/shanty/plugins/cucumber_plugin_spec.rb new file mode 100644 index 0000000..ed741c4 --- /dev/null +++ b/spec/lib/shanty/plugins/cucumber_plugin_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' +require 'shanty/plugins/cucumber_plugin' + +# All classes referenced belong to the shanty project +module Shanty + RSpec.describe(CucumberPlugin) do + include_context('basics') + + it('adds the cucumber tag') do + expect(described_class).to add_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('subscribes to the test event') do + expect(described_class).to subscribe_to(:test).with(:cucumber) + end + + describe('#cucumber') do + it('calls cucumber') do + expect(subject).to receive(:system).with('cucumber') + + subject.cucumber + end + end + end +end diff --git a/spec/lib/shanty/plugins/rspec_plugin_spec.rb b/spec/lib/shanty/plugins/rspec_plugin_spec.rb index dfba60e..06c3ae0 100644 --- a/spec/lib/shanty/plugins/rspec_plugin_spec.rb +++ b/spec/lib/shanty/plugins/rspec_plugin_spec.rb @@ -5,10 +5,17 @@ module Shanty RSpec.describe(RspecPlugin) do include_context('basics') - subject { Class.new { include RspecPlugin }.new } + + it('adds the rspec tag') do + expect(described_class).to add_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('subscribes to the test event') do - expect(RspecPlugin.callbacks).to include([:test, :rspec]) + expect(described_class).to subscribe_to(:test).with(:rspec) end describe('#rspec') do diff --git a/spec/lib/shanty/plugins/rubocop_plugin_spec.rb b/spec/lib/shanty/plugins/rubocop_plugin_spec.rb index d235827..e7f1507 100644 --- a/spec/lib/shanty/plugins/rubocop_plugin_spec.rb +++ b/spec/lib/shanty/plugins/rubocop_plugin_spec.rb @@ -5,10 +5,17 @@ module Shanty RSpec.describe(RubocopPlugin) do include_context('basics') - subject { Class.new { include RubocopPlugin }.new } + + it('adds the rubocop tag') do + expect(described_class).to add_tags(:rubocop) + end + + it('finds projects that have a .rubocop.yml file') do + expect(described_class).to define_projects.with('**/.rubocop.yml') + end it('subscribes to the test event') do - expect(RubocopPlugin.callbacks).to include([:test, :rubocop]) + expect(described_class).to subscribe_to(:test).with(:rubocop) end describe('#rubocop') do diff --git a/spec/lib/shanty/plugins/rubygem_plugin_spec.rb b/spec/lib/shanty/plugins/rubygem_plugin_spec.rb index 0f5f745..453e72e 100644 --- a/spec/lib/shanty/plugins/rubygem_plugin_spec.rb +++ b/spec/lib/shanty/plugins/rubygem_plugin_spec.rb @@ -5,10 +5,17 @@ module Shanty RSpec.describe(RubygemPlugin) do include_context('basics') - subject { Class.new { include RubygemPlugin }.new } - it('subscribes to the test event') do - expect(RubygemPlugin.callbacks).to include([:build, :build_gem]) + it('adds the rubygem tag') do + expect(described_class).to add_tags(:rubygem) + end + + it('finds projects that have a *.gemspec file') do + expect(described_class).to define_projects.with('**/*.gemspec') + end + + it('subscribes to the build event') do + expect(described_class).to subscribe_to(:build).with(:build_gem) end describe('#build_gem') do diff --git a/spec/lib/shanty/plugins/shantyfile_plugin_spec.rb b/spec/lib/shanty/plugins/shantyfile_plugin_spec.rb new file mode 100644 index 0000000..a25faa0 --- /dev/null +++ b/spec/lib/shanty/plugins/shantyfile_plugin_spec.rb @@ -0,0 +1,39 @@ +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') + + it('adds the shantyfile tag') do + expect(described_class).to add_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 + + 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')) + + expect(subject.shantyfile_projects).to match_array([ + projects[:one], + projects[:three] + ]) + 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")') + + subject.shantyfile_projects + + 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 + end + end +end diff --git a/spec/lib/shanty/project_linker_spec.rb b/spec/lib/shanty/project_linker_spec.rb deleted file mode 100644 index d115d7b..0000000 --- a/spec/lib/shanty/project_linker_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' -require 'tmpdir' -require 'shanty/graph' -require 'shanty/project' - -require_fixture 'test_plugin' -require_fixture 'test_unused_plugin' - -# Allows all classes to be refereneced without the module name -module Shanty - RSpec.describe(ProjectLinker) do - include_context('graph') - subject { ProjectLinker.new(projects.values) } - - describe('#link') do - let(:missing_parent) { 'foo-bar-does-not-exist' } - - it('throws an exception if any of the projects have a dependency on a project that does not exist') do - project.parent(missing_parent) - - expect { subject.link }.to raise_error("Cannot find project at path #{File.join(root, missing_parent)}, which \ -was specified as a dependency for shanty") - end - - it('returns a graph with the projects linked together in parent relationships') do - graph = subject.link - - expect(graph[0].parents.map(&:path)).to eql([]) - expect(graph[1].parents.map(&:path)).to eql([project_paths[:one]]) - expect(graph[2].parents.map(&:path)).to eql([project_paths[:two]]) - end - - it('returns a graph with the projects linked together in child relationships') do - graph = subject.link - - expect(graph[0].children.map(&:path)).to eql([project_paths[:two]]) - expect(graph[1].children.map(&:path)).to eql([project_paths[:three]]) - expect(graph[2].children.map(&:path)).to eql([]) - end - - it("returns a graph with the projects sorted using Tarjan's strongly connected components algorithm") do - graph = subject.link - - expect(graph[0].path).to equal(project_paths[:one]) - expect(graph[1].path).to equal(project_paths[:two]) - expect(graph[2].path).to equal(project_paths[:three]) - end - end - end -end diff --git a/spec/lib/shanty/project_sorter_spec.rb b/spec/lib/shanty/project_sorter_spec.rb new file mode 100644 index 0000000..85ac155 --- /dev/null +++ b/spec/lib/shanty/project_sorter_spec.rb @@ -0,0 +1,24 @@ +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 7b2982c..d6d3e43 100644 --- a/spec/lib/shanty/project_spec.rb +++ b/spec/lib/shanty/project_spec.rb @@ -1,42 +1,86 @@ require 'spec_helper' require 'shanty/project' -require_fixture 'test_plugin' - # All classes referenced belong to the shanty project module Shanty RSpec.describe(Project) do include_context('graph') subject { Shanty::Project.new(project_path) } - describe('#plugin') do - it('calls to the plugin to add it to the project') do - expect(Shanty::TestPlugin).to receive(:add_to_project).with(subject) - subject.plugin(Shanty::TestPlugin) - end - end - describe('#name') do - it('returns the name from the project template in the constructor') do - expect(subject.name).to eql('shanty') + 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 template in the constructor') 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 + end + describe('#options') do it('defaults the options to an empty object') do expect(subject.options).to eql({}) end end - describe('#parents_by_path') do - it('defaults the parents to an empty array') do - expect(subject.parents_by_path).to eql([]) + describe('#add_plugin') do + it('adds the given plugin to the project') do + plugin = double('plugin') + + subject.add_plugin(plugin) + + 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]) + + subject.remove_plugin(plugin_class) + + expect(subject.instance_variable_get(:@plugins)).to eql([]) + 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) + + subject.publish(:foo) + + expect(subject).to_not receive(:publish) + end + + 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) + + 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) + + expect(subject.publish(:foo)).to be(false) + expect(next_plugin).to_not receive(:subscribed?) end end @@ -48,46 +92,20 @@ module Shanty describe('#to_s') do it('returns a simple string representation of the project') do - expect(subject.to_s).to eql('shanty') + 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: 'shanty', + name: 'one', path: project_path, tags: [], options: {}, - parents_by_path: [] + parents: [] }.inspect) end end - - describe('#within_project_dir') do - it('does nothing if there is no block given') do - expect(Dir).to_not receive(:chdir) - - subject.within_project_dir - end - - it('yields the given block') do - expect { |b| subject.within_project_dir(&b) }.to yield_with_no_args - end - - it('yields the given block with the correct working directory') do - subject.within_project_dir do - expect(Dir.pwd).to eql(project_path) - end - end - - it('changes the working directory back at the end') do - expected_pwd = Dir.pwd - - subject.within_project_dir - - expect(Dir.pwd).to eql(expected_pwd) - end - end end end diff --git a/spec/lib/shanty/project_tree_spec.rb b/spec/lib/shanty/project_tree_spec.rb new file mode 100644 index 0000000..cb63e5e --- /dev/null +++ b/spec/lib/shanty/project_tree_spec.rb @@ -0,0 +1,42 @@ +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 index 2bf5c49..accf343 100644 --- a/spec/lib/shanty/task_env_spec.rb +++ b/spec/lib/shanty/task_env_spec.rb @@ -8,7 +8,7 @@ module Shanty describe('#graph') do it('discovers all the projects') do - expect(Plugin).to receive(:discover_all_projects).and_return([]) + expect(Plugin).to receive(:all_projects).and_return([]) subject.graph end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2ed322e..29fd119 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,23 +1,37 @@ require 'coveralls' Coveralls.wear! +require 'fileutils' require 'i18n' +require 'logger' +require 'pathname' +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_fixture 'test_plugin' +def require_matchers(path) + require File.join(__dir__, 'support', 'matchers', path) +end + +require_matchers 'call_me_ruby' +require_matchers 'plugin' I18n.enforce_available_locales = false RSpec.configure do |config| + config.expect_with :rspec do |c| + c.include_chain_clauses_in_custom_matcher_descriptions = true + end + config.mock_with(:rspec) do |mocks| mocks.verify_partial_doubles = true end @@ -26,34 +40,49 @@ def require_fixture(path) Shanty::Env.clear! Shanty::TaskEnv.clear! Shanty::Project.clear! + + Shanty::Env.logger.level = Logger::FATAL end end RSpec.shared_context('basics') do - let(:root) { File.expand_path(File.join(__dir__, '..')) } + around do |example| + FileUtils.touch(File.join(root, '.shanty.yml')) + project_paths.values.each do |project_path| + FileUtils.mkdir_p(project_path) + end + + Dir.chdir(root) do + example.run + end + + 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_paths) do { - three: File.join(root, 'examples', 'test-static-project-2', 'test-static-project-3'), - two: File.join(root, 'examples', 'test-static-project-2'), - one: File.join(root, 'examples', 'test-static-project'), - shanty: File.join(root) + one: File.join(root, 'one'), + two: File.join(root, 'two'), + three: File.join(root, 'two', 'three') } end - let(:project_path) { project_paths[:shanty] } + let(:project_path) { project_paths[:one] } end RSpec.shared_context('graph') do include_context('basics') let(:projects) do - Hash[project_paths.map do |key, project_path| - [key, Shanty::Project.new(project_path).tap do |project| - project.plugin(Shanty::TestPlugin) - project.execute_shantyfile! - end] - end] + project_paths.each_with_object({}) do |(key, project_path), acc| + acc[key] = Shanty::Project.new(project_path) + end end - let(:project) { projects[:shanty] } + let(:project) { projects[:one] } let(:project_path_trie) do Containers::Trie.new.tap do |trie| projects.values.map { |project| trie[project.path] = project } diff --git a/spec/support/matchers/call_me_ruby.rb b/spec/support/matchers/call_me_ruby.rb new file mode 100644 index 0000000..3ac18d0 --- /dev/null +++ b/spec/support/matchers/call_me_ruby.rb @@ -0,0 +1,12 @@ +# Something, something, darkside. +module Shanty + RSpec::Matchers.define(:subscribe_to) do |event| + match do |actual| + expect(actual.instance_variable_get(:@class_callbacks)).to include(event => @callbacks) + end + + chain(:with) do |*callbacks| + @callbacks = callbacks + end + end +end diff --git a/spec/support/matchers/plugin.rb b/spec/support/matchers/plugin.rb new file mode 100644 index 0000000..d411f13 --- /dev/null +++ b/spec/support/matchers/plugin.rb @@ -0,0 +1,18 @@ +# Something, something, darkside. +module Shanty + RSpec::Matchers.define(:add_tags) do |*tags| + match do |actual| + expect(actual.instance_variable_get(:@tags)).to include(*tags) + end + end + + RSpec::Matchers.define(:define_projects) do + match do |actual| + expect(actual.instance_variable_get(:@project_matchers)).to include(*(@matchers || [])) + end + + chain :with do |*matchers| + @matchers = matchers + end + end +end diff --git a/translations/en.yml b/translations/en.yml index 1391868..294aff6 100644 --- a/translations/en.yml +++ b/translations/en.yml @@ -1,5 +1,6 @@ --- en: + missing_root: Could not find a %{config_file} file in this or any parent directories. Please run `shanty init` in the directory you want to be the root of your project structure tasks: common: options: