Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Initial implementation and documentation.

  • Loading branch information...
commit 8f1723f3e3a46adea1900f2e6a14298e3a983c70 1 parent f78c02d
Josh Nichols authored July 21, 2010
120  README.rdoc
Source Rendered
... ...
@@ -1,15 +1,125 @@
1 1
 = capistrano-spec
2 2
 
3  
-Description goes here.
  3
+Capistrano... the final frontier of testing... well, maybe not final, but it is a frontier. I had set out to do some bug fixing and some BDDing on some of my capistrano code, but found it wasn't really obvious how to do so. As a result, I set out to write capistrano-spec and document how to test capistrano libraries.
  4
+
  5
+== Designing your capistrano extension
  6
+
  7
+In the wild, you'll mostly commonly come across two patterns:
  8
+
  9
+* files living under recipes/* that are autoloaded
  10
+* files living under lib that are required from config/deploy.rb
  11
+
  12
+In these files, you can start using the capistrano top-level methods, like `namespace` or `task`, like:
  13
+
  14
+  # in recipes/speak.rb or lib/speak.rb
  15
+  task :speak do
  16
+    set :message, 'oh hai'
  17
+    puts message
  18
+  end
  19
+
  20
+Capistrano does some trickery to `require` and `load` so that if you `require` or `load`, the file is ran in the context of a Capistrano::Configuration, where all the `task` and `namespace` methods you know and love will.
  21
+
  22
+Some consider this a little gross, because it'd be easy to accidentally require/load this without being in the context of a Capistrano::Configuration. The answer to this is to pull use `Capistrano::Configuration.instance` to make sure it's evaluted in that context:
  23
+
  24
+  # in recipes/speak.rb or lib/speak.rb
  25
+  Capistrano::Configuration.instance(true).load do
  26
+    task :speak do
  27
+      set :message, 'oh hai'
  28
+      puts message
  29
+    end
  30
+  end
  31
+
  32
+There's a problem though: it's not particular testable. You can't take some `Capistrano::Configuration` and easily bring your task into it.
  33
+
  34
+So, here's what I recommend instead: create a method for taking a configuration, and adding your goodies to it.
  35
+
  36
+  require 'capistrano'
  37
+  module Capistrano
  38
+    module Speak
  39
+      def self.load_into(configuration)
  40
+        configuration.load do
  41
+          task :speak do
  42
+            set :message, 'oh hai'
  43
+            puts message
  44
+          end
  45
+        end
  46
+      end
  47
+    end
  48
+  end
  49
+
  50
+  # may as well load it if we have it
  51
+  if Capistrano::Configuration.instance
  52
+    Capistrano::Speak.load_into(Capistrano::Configuration.instance)
  53
+  end
  54
+
  55
+Now, we're going to be able to test this. Behold!
  56
+
  57
+== Testing
  58
+
  59
+Alright, we can start testing by making Capistrano::Configuration and load Capistrano::Speak into it.
  60
+
  61
+  describe Capistrano::Speak, "loaded into a configuration" do
  62
+    before do
  63
+      @configuration = Capistrano::Configuration.new
  64
+      Capistrano::Speak.load_into(@configuration)
  65
+    end
  66
+  
  67
+  end
  68
+
  69
+
  70
+Now you have access to a configuration, so you can start poking around the `@configuration` object as you see fit.
  71
+
  72
+Now, remember, by `set`ting values, you can access them using `fetch`:
  73
+
  74
+  before do
  75
+    @configuration.set :foo, 'bar'
  76
+  end
  77
+  
  78
+  it "should define foo" do
  79
+    @configuration.fetch(:foo).should == 'bar'
  80
+  end
  81
+
  82
+You can also find and execute tasks, so you can verify if you successfully set a value:
  83
+
  84
+
  85
+  describe 'speak task' do
  86
+    before do
  87
+      @configuration.find_and_execute_task('speak')
  88
+    end
  89
+    
  90
+    it "should define message" do
  91
+      @configuration.fetch(:message).should == 'oh hai'
  92
+    end
  93
+  end
  94
+
  95
+One thing you might be wondering now is... that's cool, but what about working with remote servers? I have just the trick for you: extensions to Capistrano::Configuration to track what files were up or downloaded and what commands were run. Now, this is no substitution for manually testing your capistrano recipe by running it on the server, but it is good for sanity checking.
  96
+
  97
+  before do
  98
+    @configuration = Capistrano::Configuration.new
  99
+    @configuration.extend(Capistrano::Spec::ConfigurationExtension)
  100
+  end
  101
+
  102
+  it "should run yes" do
  103
+    @configuration.run "yes"
  104
+    @configuration.should have_run("yes")
  105
+  end
  106
+
  107
+  it "should upload foo" do
  108
+    @configuration.upload 'foo', '/tmp/foo'
  109
+    @configuration.should have_uploaded('foo').to('/tmp/foo')
  110
+  end
  111
+
  112
+  it "should have gotten" do
  113
+    @configuration.get '/tmp/bar', 'bar'
  114
+    @configuration.should have_gotten('/tmp/bar').to('bar')
  115
+  end
4 116
 
5 117
 == Note on Patches/Pull Requests
6 118
  
7 119
 * Fork the project.
8 120
 * Make your feature addition or bug fix.
9  
-* Add tests for it. This is important so I don't break it in a
10  
-  future version unintentionally.
11  
-* Commit, do not mess with rakefile, version, or history.
12  
-  (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
  121
+* Add tests for it. This is important so I don't break it in a future version unintentionally.
  122
+* Commit, do not mess with rakefile, version, or history.  (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13 123
 * Send me a pull request. Bonus points for topic branches.
14 124
 
15 125
 == Copyright
6  Rakefile
@@ -5,8 +5,10 @@ begin
5 5
   require 'jeweler'
6 6
   Jeweler::Tasks.new do |gem|
7 7
     gem.name = "capistrano-spec"
8  
-    gem.summary = %Q{TODO: one-line summary of your gem}
9  
-    gem.description = %Q{TODO: longer description of your gem}
  8
+    gem.version = '0.1.0'
  9
+
  10
+    gem.summary = %Q{Test your capistrano recipes}
  11
+    gem.description = %Q{Helpers and matchers for capistrano}
10 12
     gem.email = "josh@technicalpickles.com"
11 13
     gem.homepage = "http://github.com/technicalpickles/capistrano-spec"
12 14
     gem.authors = ["Joshua Nichols"]
53  capistrano-spec.gemspec
... ...
@@ -0,0 +1,53 @@
  1
+# Generated by jeweler
  2
+# DO NOT EDIT THIS FILE DIRECTLY
  3
+# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
  4
+# -*- encoding: utf-8 -*-
  5
+
  6
+Gem::Specification.new do |s|
  7
+  s.name = %q{capistrano-spec}
  8
+  s.version = "0.1.0"
  9
+
  10
+  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
  11
+  s.authors = ["Joshua Nichols"]
  12
+  s.date = %q{2010-07-20}
  13
+  s.description = %q{Helpers and matchers for capistrano}
  14
+  s.email = %q{josh@technicalpickles.com}
  15
+  s.extra_rdoc_files = [
  16
+    "LICENSE",
  17
+     "README.rdoc"
  18
+  ]
  19
+  s.files = [
  20
+    ".document",
  21
+     ".gitignore",
  22
+     "LICENSE",
  23
+     "README.rdoc",
  24
+     "Rakefile",
  25
+     "lib/capistrano-spec.rb",
  26
+     "spec/capistrano-spec_spec.rb",
  27
+     "spec/spec.opts",
  28
+     "spec/spec_helper.rb"
  29
+  ]
  30
+  s.homepage = %q{http://github.com/technicalpickles/capistrano-spec}
  31
+  s.rdoc_options = ["--charset=UTF-8"]
  32
+  s.require_paths = ["lib"]
  33
+  s.rubygems_version = %q{1.3.6}
  34
+  s.summary = %q{Test your capistrano recipes}
  35
+  s.test_files = [
  36
+    "spec/capistrano-spec_spec.rb",
  37
+     "spec/spec_helper.rb"
  38
+  ]
  39
+
  40
+  if s.respond_to? :specification_version then
  41
+    current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
  42
+    s.specification_version = 3
  43
+
  44
+    if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
  45
+      s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
  46
+    else
  47
+      s.add_dependency(%q<rspec>, [">= 1.2.9"])
  48
+    end
  49
+  else
  50
+    s.add_dependency(%q<rspec>, [">= 1.2.9"])
  51
+  end
  52
+end
  53
+
1  lib/capistrano-spec.rb
... ...
@@ -0,0 +1 @@
  1
+require 'capistrano/spec'
167  lib/capistrano/spec.rb
... ...
@@ -0,0 +1,167 @@
  1
+module Capistrano
  2
+  module Spec
  3
+    module ConfigurationExtension
  4
+      def get(remote_path, path, options={}, &block)
  5
+        gets[remote_path] = {:path => path, :options => options, :block => block}
  6
+      end
  7
+
  8
+      def gets
  9
+        @gets ||= {}
  10
+      end
  11
+
  12
+      def run(cmd, options={}, &block)
  13
+        runs[cmd] = {:options => options, :block => block}
  14
+      end
  15
+
  16
+      def runs
  17
+        @runs ||= {}
  18
+      end
  19
+
  20
+      def upload(from, to, options={}, &block)
  21
+        uploads[from] = {:to => to, :options => options, :block => block}
  22
+      end
  23
+
  24
+      def uploads
  25
+        @uploads ||= {}
  26
+      end
  27
+      
  28
+    end
  29
+
  30
+    module Helpers
  31
+      def find_callback(configuration, on, task)
  32
+        if task.kind_of?(String)
  33
+          task = configuration.find_task(task)
  34
+        end
  35
+
  36
+        callbacks = configuration.callbacks[on]
  37
+
  38
+        callbacks && callbacks.select do |task_callback|
  39
+          task_callback.applies_to?(task) || task_callback.source == task.fully_qualified_name
  40
+        end
  41
+      end
  42
+
  43
+    end
  44
+
  45
+    module Matchers
  46
+      extend ::Spec::Matchers::DSL
  47
+
  48
+      define :callback do |task_name|
  49
+        extend Helpers
  50
+
  51
+        match do |configuration|
  52
+          @task = configuration.find_task(task_name)
  53
+          callbacks = find_callback(configuration, @on, @task)
  54
+
  55
+          if callbacks
  56
+            @callback = callbacks.first
  57
+
  58
+            if @callback && @after_task_name
  59
+              @after_task = configuration.find_task(@after_task_name)
  60
+              @callback.applies_to?(@after_task)
  61
+            elsif @callback && @before_task_name
  62
+              @before_task = configuration.find_task(@before_task_name)
  63
+              @callback.applies_to?(@before_task)
  64
+            else
  65
+              ! @callback.nil?
  66
+            end
  67
+          else
  68
+            false
  69
+          end
  70
+        end
  71
+
  72
+        def on(on)
  73
+          @on = on
  74
+          self
  75
+        end
  76
+
  77
+        def before(before_task_name)
  78
+          @on = :before
  79
+          @before_task_name = before_task_name
  80
+          self
  81
+        end
  82
+
  83
+        def after(after_task_name)
  84
+          @on = :after
  85
+          @after_task_name = after_task_name
  86
+          self
  87
+        end
  88
+
  89
+        failure_message_for_should do |actual|
  90
+          if @after_task_name
  91
+            "expected configuration to callback #{task_name.inspect} #{@on} #{@after_task_name.inspect}, but did not"
  92
+          elsif @before_task_name
  93
+            "expected configuration to callback #{task_name.inspect} #{@on} #{@before_task_name.inspect}, but did not"
  94
+          else
  95
+            "expected configuration to callback #{task_name.inspect} on #{@on}, but did not"
  96
+          end
  97
+        end
  98
+
  99
+      end
  100
+
  101
+      define :have_gotten do |path|
  102
+        match do |configuration|
  103
+
  104
+          get = configuration.gets[path]
  105
+          if @to
  106
+            get && get[:path] == @to
  107
+          else
  108
+            get
  109
+          end
  110
+        end
  111
+
  112
+        def to(to)
  113
+          @to = to
  114
+          self
  115
+        end
  116
+
  117
+        failure_message_for_should do |actual|
  118
+          if @to
  119
+            "expected configuration to get #{path} to #{@to}, but did not"
  120
+          else
  121
+            "expected configuration to get #{path}, but did not"
  122
+          end
  123
+        end
  124
+      end
  125
+
  126
+      define :have_uploaded do |path|
  127
+        match do |configuration|
  128
+          upload = configuration.uploads[path]
  129
+          if @to
  130
+            upload && upload[:to] == @to
  131
+          else
  132
+            upload
  133
+          end
  134
+        end
  135
+
  136
+        def to(to)
  137
+          @to = to
  138
+          self
  139
+        end
  140
+
  141
+        failure_message_for_should do |actual|
  142
+          if @to
  143
+            "expected configuration to upload #{path} to #{@to}, but did not"
  144
+          else
  145
+            "expected configuration to upload #{path}, but did not"
  146
+          end
  147
+        end
  148
+      end
  149
+
  150
+      define :have_run do |cmd|
  151
+
  152
+        match do |configuration|
  153
+          run = configuration.runs[cmd]
  154
+
  155
+          run
  156
+        end
  157
+
  158
+        failure_message_for_should do |actual|
  159
+          "expected configuration to run #{cmd}, but did not"
  160
+        end
  161
+        
  162
+      end
  163
+
  164
+    end
  165
+  end
  166
+end
  167
+

0 notes on commit 8f1723f

Please sign in to comment.
Something went wrong with that request. Please try again.