foca-integrity 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +19 -106
- data/Rakefile +53 -25
- data/VERSION.yml +1 -1
- data/app.rb +24 -153
- data/bin/integrity +2 -80
- data/config/config.sample.ru +2 -1
- data/config/config.sample.yml +4 -0
- data/integrity.gemspec +16 -11
- data/lib/integrity/build.rb +10 -5
- data/lib/integrity/helpers/authorization.rb +33 -0
- data/lib/integrity/helpers/breadcrumbs.rb +20 -0
- data/lib/integrity/helpers/forms.rb +28 -0
- data/lib/integrity/helpers/pretty_output.rb +45 -0
- data/lib/integrity/helpers/rendering.rb +14 -0
- data/lib/integrity/helpers/resources.rb +13 -0
- data/lib/integrity/helpers/urls.rb +47 -0
- data/lib/integrity/helpers.rb +16 -0
- data/lib/integrity/installer.rb +133 -0
- data/lib/integrity/migrations.rb +50 -0
- data/lib/integrity/notifier/base.rb +1 -1
- data/lib/integrity/notifier.rb +2 -2
- data/lib/integrity/project.rb +40 -13
- data/lib/integrity/{builder.rb → project_builder.rb} +1 -3
- data/lib/integrity/scm/git.rb +4 -4
- data/lib/integrity/scm.rb +5 -8
- data/lib/integrity.rb +37 -26
- data/test/helpers/acceptance/git_helper.rb +99 -0
- data/test/helpers/acceptance/textfile_notifier.rb +26 -0
- data/test/helpers/acceptance.rb +126 -0
- data/test/helpers/expectations/be_a.rb +23 -0
- data/test/helpers/expectations/change.rb +90 -0
- data/test/helpers/expectations/have.rb +105 -0
- data/test/helpers/expectations/have_tag.rb +128 -0
- data/test/helpers/expectations/predicates.rb +37 -0
- data/test/helpers/expectations.rb +5 -0
- data/test/helpers/fixtures.rb +83 -0
- data/test/helpers.rb +48 -0
- data/views/_build_info.haml +18 -0
- data/views/build.haml +1 -1
- data/views/error.haml +10 -3
- data/views/home.haml +3 -2
- data/views/integrity.sass +1 -1
- data/views/layout.haml +3 -0
- data/views/new.haml +10 -13
- data/views/notifier.haml +1 -1
- data/views/project.builder +21 -0
- data/views/project.haml +9 -13
- metadata +38 -11
- data/lib/integrity/core_ext/time.rb +0 -13
- data/spec/form_field_matchers.rb +0 -91
- data/spec/spec_helper.rb +0 -135
- data/vendor/sinatra-hacks/lib/hacks.rb +0 -49
- data/views/build_info.haml +0 -22
data/lib/integrity/scm/git.rb
CHANGED
@@ -44,7 +44,7 @@ module Integrity
|
|
44
44
|
|
45
45
|
def clone
|
46
46
|
log "Cloning #{uri} to #{working_directory}"
|
47
|
-
`git clone #{uri} #{working_directory}`
|
47
|
+
`git clone #{uri} #{working_directory} &>/dev/null`
|
48
48
|
end
|
49
49
|
|
50
50
|
def checkout(treeish=nil)
|
@@ -55,12 +55,12 @@ module Integrity
|
|
55
55
|
end
|
56
56
|
|
57
57
|
log "Checking-out #{strategy}"
|
58
|
-
`cd #{working_directory} && git checkout #{strategy}`
|
58
|
+
`cd #{working_directory} && git checkout #{strategy} &>/dev/null`
|
59
59
|
end
|
60
60
|
|
61
61
|
def pull
|
62
62
|
log "Pull-ing in #{working_directory}"
|
63
|
-
`cd #{working_directory} && git pull`
|
63
|
+
`cd #{working_directory} && git pull &>/dev/null`
|
64
64
|
end
|
65
65
|
|
66
66
|
def local_branches
|
@@ -72,7 +72,7 @@ module Integrity
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def on_branch?
|
75
|
-
File.basename(`cd #{working_directory} && git symbolic-ref HEAD`).chomp == branch
|
75
|
+
File.basename(`cd #{working_directory} && git symbolic-ref HEAD &>/dev/null`).chomp == branch
|
76
76
|
end
|
77
77
|
|
78
78
|
def log(message)
|
data/lib/integrity/scm.rb
CHANGED
@@ -5,18 +5,15 @@ module Integrity
|
|
5
5
|
def self.new(uri, *args)
|
6
6
|
scm_class_for(uri).new(uri, *args)
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def self.working_tree_path(uri)
|
10
10
|
scm_class_for(uri).working_tree_path(uri)
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
private
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
when /\.git\/?/ then Git
|
18
|
-
else raise SCMUnknownError, "could not find any SCM based on string '#{string}'"
|
19
|
-
end
|
14
|
+
def self.scm_class_for(uri)
|
15
|
+
return Git if uri.scheme == "git" || uri.path =~ /\.git\/?/
|
16
|
+
raise SCMUnknownError, "could not find any SCM based on URI '#{uri.to_s}'"
|
20
17
|
end
|
21
18
|
end
|
22
19
|
end
|
data/lib/integrity.rb
CHANGED
@@ -1,32 +1,35 @@
|
|
1
1
|
__DIR__ = File.dirname(__FILE__)
|
2
2
|
$:.unshift "#{__DIR__}/integrity", *Dir["#{__DIR__}/../vendor/**/lib"].to_a
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
4
|
+
require "rubygems"
|
5
|
+
require "json"
|
6
|
+
require "dm-core"
|
7
|
+
require "dm-validations"
|
8
|
+
require "dm-types"
|
9
|
+
require "dm-timestamps"
|
10
|
+
require "dm-aggregates"
|
11
11
|
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
12
|
+
require "yaml"
|
13
|
+
require "logger"
|
14
|
+
require "digest/sha1"
|
15
|
+
require "timeout"
|
16
|
+
require "ostruct"
|
17
|
+
require "fileutils"
|
15
18
|
|
16
|
-
require
|
17
|
-
require
|
18
|
-
require 'core_ext/time'
|
19
|
+
require "core_ext/object"
|
20
|
+
require "core_ext/string"
|
19
21
|
|
20
|
-
require
|
21
|
-
require
|
22
|
-
require
|
23
|
-
require
|
24
|
-
require
|
25
|
-
require
|
22
|
+
require "project"
|
23
|
+
require "build"
|
24
|
+
require "project_builder"
|
25
|
+
require "scm"
|
26
|
+
require "scm/git"
|
27
|
+
require "notifier"
|
26
28
|
|
27
29
|
module Integrity
|
28
30
|
def self.new(config_file = nil)
|
29
31
|
self.config = config_file unless config_file.nil?
|
32
|
+
DataMapper.logger = self.logger if config[:log_debug_info]
|
30
33
|
DataMapper.setup(:default, config[:database_uri])
|
31
34
|
end
|
32
35
|
|
@@ -35,12 +38,13 @@ module Integrity
|
|
35
38
|
end
|
36
39
|
|
37
40
|
def self.default_configuration
|
38
|
-
@defaults ||= { :database_uri =>
|
39
|
-
:export_directory => root /
|
41
|
+
@defaults ||= { :database_uri => "sqlite3::memory:",
|
42
|
+
:export_directory => root / "exports",
|
40
43
|
:log => STDOUT,
|
41
|
-
:base_uri =>
|
44
|
+
:base_uri => "http://localhost:8910",
|
42
45
|
:use_basic_auth => false,
|
43
|
-
:build_all_commits => true
|
46
|
+
:build_all_commits => true,
|
47
|
+
:log_debug_info => false }
|
44
48
|
end
|
45
49
|
|
46
50
|
def self.config
|
@@ -50,7 +54,7 @@ module Integrity
|
|
50
54
|
def self.config=(file)
|
51
55
|
@config = default_configuration.merge(YAML.load_file(file))
|
52
56
|
end
|
53
|
-
|
57
|
+
|
54
58
|
def self.log(message, &block)
|
55
59
|
logger.info(message, &block)
|
56
60
|
end
|
@@ -60,9 +64,16 @@ module Integrity
|
|
60
64
|
logger.formatter = LogFormatter.new
|
61
65
|
end
|
62
66
|
end
|
63
|
-
|
67
|
+
|
68
|
+
def self.version
|
69
|
+
@version ||= begin
|
70
|
+
file = YAML.load_file(Integrity.root / "VERSION.yml")
|
71
|
+
"#{file['major']}.#{file['minor']}.#{file['patch']}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
64
75
|
private
|
65
|
-
|
76
|
+
|
66
77
|
class LogFormatter < Logger::Formatter
|
67
78
|
def call(severity, time, progname, msg)
|
68
79
|
time.strftime("[%H:%M:%S] ") + msg2str(msg) + "\n"
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module GitHelper
|
2
|
+
@@_git_repositories = Hash.new {|h,k| h[k] = Repo.new(k) }
|
3
|
+
|
4
|
+
def git_repo(name)
|
5
|
+
@@_git_repositories[name]
|
6
|
+
end
|
7
|
+
|
8
|
+
def destroy_all_git_repos
|
9
|
+
@@_git_repositories.each {|n,r| r.destroy }
|
10
|
+
@@_git_repositories.clear
|
11
|
+
end
|
12
|
+
|
13
|
+
class Repo
|
14
|
+
attr_reader :path
|
15
|
+
|
16
|
+
def initialize(name)
|
17
|
+
@name = name
|
18
|
+
@path = "/tmp" / @name.to_s
|
19
|
+
create
|
20
|
+
end
|
21
|
+
|
22
|
+
def path
|
23
|
+
@path / ".git"
|
24
|
+
end
|
25
|
+
|
26
|
+
def create
|
27
|
+
destroy
|
28
|
+
FileUtils.mkdir_p @path
|
29
|
+
|
30
|
+
Dir.chdir(@path) do
|
31
|
+
system 'git init &>/dev/null'
|
32
|
+
system 'git config user.name "John Doe"'
|
33
|
+
system 'git config user.email "johndoe@example.org"'
|
34
|
+
system 'echo "just a test repo" >> README'
|
35
|
+
system 'git add README &>/dev/null'
|
36
|
+
system 'git commit -m "First commit" &>/dev/null'
|
37
|
+
end
|
38
|
+
|
39
|
+
add_successful_commit
|
40
|
+
end
|
41
|
+
|
42
|
+
def commits
|
43
|
+
Dir.chdir(@path) do
|
44
|
+
commits = `git log --pretty=oneline`.collect { |line| line.split(" ").first }
|
45
|
+
commits.inject([]) do |commits, sha1|
|
46
|
+
format = %Q(---%n:message: >-%n %s%n:timestamp: %ci%n:id: #{sha1})
|
47
|
+
commits << YAML.load(`git show -s --pretty=format:"#{format}" #{sha1}`)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_commit(message, &action)
|
53
|
+
Dir.chdir(@path) do
|
54
|
+
yield action
|
55
|
+
system %Q(git commit -m "#{message}" &>/dev/null)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_failing_commit
|
60
|
+
add_commit "This commit will fail" do
|
61
|
+
system %Q(echo '#{build_script(false)}' > test)
|
62
|
+
system %Q(chmod +x test)
|
63
|
+
system %Q(git add test &>/dev/null)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_successful_commit
|
68
|
+
add_commit "This commit will work" do
|
69
|
+
system %Q(echo '#{build_script(true)}' > test)
|
70
|
+
system %Q(chmod +x test)
|
71
|
+
system %Q(git add test &>/dev/null)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def head
|
76
|
+
Dir.chdir(@path) do
|
77
|
+
`git log --pretty=format:%H | head -1`.chomp
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def short_head
|
82
|
+
head[0..6]
|
83
|
+
end
|
84
|
+
|
85
|
+
def destroy
|
86
|
+
FileUtils.rm_rf @path if File.directory?(@path)
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
def build_script(successful=true)
|
92
|
+
<<-script
|
93
|
+
#!/bin/sh
|
94
|
+
echo "Running tests..."
|
95
|
+
exit #{successful ? 0 : 1}
|
96
|
+
script
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Integrity
|
2
|
+
class Notifier
|
3
|
+
class Textfile < Notifier::Base
|
4
|
+
def self.to_haml
|
5
|
+
<<-haml
|
6
|
+
%p.normal
|
7
|
+
%label{ :for => "textfile_notifier_file" } File
|
8
|
+
%input.text#textfile_notifier_file{ :name => "notifiers[Textfile][file]", :type => "text", :value => config["file"] }
|
9
|
+
haml
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(build, config={})
|
13
|
+
super
|
14
|
+
@file = @config["file"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def deliver!
|
18
|
+
File.open(@file, "a") do |f|
|
19
|
+
f.puts "=== #{short_message} ==="
|
20
|
+
f.puts
|
21
|
+
f.puts full_message
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'webrat/rack'
|
2
|
+
require 'sinatra'
|
3
|
+
require 'sinatra/test'
|
4
|
+
|
5
|
+
disable :run
|
6
|
+
disable :reload
|
7
|
+
|
8
|
+
Webrat.configuration.instance_variable_set("@mode", :sinatra)
|
9
|
+
|
10
|
+
module Webrat
|
11
|
+
class SinatraSession < Session
|
12
|
+
DEFAULT_DOMAIN = "integrity.example.org"
|
13
|
+
|
14
|
+
def initialize(context = nil)
|
15
|
+
super(context)
|
16
|
+
@sinatra_test = Sinatra::TestHarness.new
|
17
|
+
end
|
18
|
+
|
19
|
+
%w(get head post put delete).each do |verb|
|
20
|
+
class_eval <<-METHOD
|
21
|
+
def #{verb}(path, data, headers = {})
|
22
|
+
params = data.inject({}) do |data, (key,value)|
|
23
|
+
data[key] = Rack::Utils.unescape(value)
|
24
|
+
data
|
25
|
+
end
|
26
|
+
headers['HTTP_HOST'] = DEFAULT_DOMAIN
|
27
|
+
@sinatra_test.#{verb}(path, params, headers)
|
28
|
+
end
|
29
|
+
METHOD
|
30
|
+
end
|
31
|
+
|
32
|
+
def response_body
|
33
|
+
@sinatra_test.body
|
34
|
+
end
|
35
|
+
|
36
|
+
def response_code
|
37
|
+
@sinatra_test.status
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def response
|
43
|
+
@sinatra_test.response
|
44
|
+
end
|
45
|
+
|
46
|
+
def current_host
|
47
|
+
URI.parse(current_url).host || DEFAULT_DOMAIN
|
48
|
+
end
|
49
|
+
|
50
|
+
def response_location_host
|
51
|
+
URI.parse(response_location).host || DEFAULT_DOMAIN
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
require Integrity.root / "app"
|
57
|
+
require File.dirname(__FILE__) / "acceptance/git_helper"
|
58
|
+
|
59
|
+
module AcceptanceHelper
|
60
|
+
include FileUtils
|
61
|
+
|
62
|
+
def export_directory
|
63
|
+
Integrity.root / "exports"
|
64
|
+
end
|
65
|
+
|
66
|
+
def enable_auth!
|
67
|
+
Integrity.config[:use_basic_auth] = true
|
68
|
+
Integrity.config[:admin_username] = "admin"
|
69
|
+
Integrity.config[:admin_password] = "test"
|
70
|
+
Integrity.config[:hash_admin_password] = false
|
71
|
+
end
|
72
|
+
|
73
|
+
def login_as(user, password)
|
74
|
+
def AcceptanceHelper.logged_in; true; end
|
75
|
+
basic_auth user, password
|
76
|
+
visit "/login"
|
77
|
+
Sinatra::Application.before { login_required if AcceptanceHelper.logged_in }
|
78
|
+
end
|
79
|
+
|
80
|
+
def log_out
|
81
|
+
def AcceptanceHelper.logged_in; false; end
|
82
|
+
@_webrat_session = Webrat::SinatraSession.new(self)
|
83
|
+
end
|
84
|
+
|
85
|
+
def disable_auth!
|
86
|
+
Integrity.config[:use_basic_auth] = false
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_and_create_export_directory!
|
90
|
+
FileUtils.rm_r(export_directory) if File.directory?(export_directory)
|
91
|
+
FileUtils.mkdir(export_directory)
|
92
|
+
Integrity.config[:export_directory] = export_directory
|
93
|
+
end
|
94
|
+
|
95
|
+
def setup_log!
|
96
|
+
pathname = Integrity.root / "integrity.log"
|
97
|
+
FileUtils.rm pathname if File.exists?(pathname)
|
98
|
+
Integrity.config[:log] = pathname
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class Test::Unit::AcceptanceTestCase < Test::Unit::TestCase
|
103
|
+
include AcceptanceHelper
|
104
|
+
include Test::Storyteller
|
105
|
+
include GitHelper
|
106
|
+
include Webrat::Methods
|
107
|
+
Webrat::Methods.delegate_to_session :response_code
|
108
|
+
|
109
|
+
before(:all) do
|
110
|
+
Integrity.config[:base_uri] = "http://#{Webrat::SinatraSession::DEFAULT_DOMAIN}"
|
111
|
+
end
|
112
|
+
|
113
|
+
before(:each) do
|
114
|
+
# ensure each scenario is run in a clean sandbox
|
115
|
+
setup_and_reset_database!
|
116
|
+
enable_auth!
|
117
|
+
setup_log!
|
118
|
+
set_and_create_export_directory!
|
119
|
+
log_out
|
120
|
+
end
|
121
|
+
|
122
|
+
after(:each) do
|
123
|
+
destroy_all_git_repos
|
124
|
+
rm_r export_directory if File.directory?(export_directory)
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Matchy::Expectations
|
2
|
+
class BeAExpectation < Base
|
3
|
+
def matches?(receiver)
|
4
|
+
@receiver = receiver
|
5
|
+
@receiver.is_a?(@expected)
|
6
|
+
end
|
7
|
+
|
8
|
+
def failure_message
|
9
|
+
"Expected #{@receiver.inspect} to be a #{@expected.inspect}."
|
10
|
+
end
|
11
|
+
|
12
|
+
def negative_failure_message
|
13
|
+
"Expected #{@receiver.inspect} to not be a #{@expected.inspect}."
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module TestCaseExtensions
|
18
|
+
def be_a(obj)
|
19
|
+
Matchy::Expectations::BeAExpectation.new(obj, self)
|
20
|
+
end
|
21
|
+
alias :be_an :be_a
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Matchy::Expectations
|
2
|
+
class ChangeExpectation < Base
|
3
|
+
def initialize(receiver=nil, message=nil, test_case=nil, &block)
|
4
|
+
@message = message || "result"
|
5
|
+
@value_proc = block || lambda {
|
6
|
+
receiver.__send__(message)
|
7
|
+
}
|
8
|
+
@test_case = test_case
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches?(event_proc)
|
12
|
+
raise_block_syntax_error if block_given?
|
13
|
+
|
14
|
+
@before = evaluate_value_proc
|
15
|
+
event_proc.call
|
16
|
+
@after = evaluate_value_proc
|
17
|
+
|
18
|
+
return false if @from unless @from == @before
|
19
|
+
return false if @to unless @to == @after
|
20
|
+
return (@before + @amount == @after) if @amount
|
21
|
+
return ((@after - @before) >= @minimum) if @minimum
|
22
|
+
return ((@after - @before) <= @maximum) if @maximum
|
23
|
+
return @before != @after
|
24
|
+
end
|
25
|
+
|
26
|
+
def raise_block_syntax_error
|
27
|
+
raise ArgumentError, "block passed to should or should_not change must use {} instead of do/end"
|
28
|
+
end
|
29
|
+
|
30
|
+
def evaluate_value_proc
|
31
|
+
@value_proc.call
|
32
|
+
end
|
33
|
+
|
34
|
+
def failure_message
|
35
|
+
if @to
|
36
|
+
"#{@message} should have been changed to #{@to.inspect}, but is now #{@after.inspect}"
|
37
|
+
elsif @from
|
38
|
+
"#{@message} should have initially been #{@from.inspect}, but was #{@before.inspect}"
|
39
|
+
elsif @amount
|
40
|
+
"#{@message} should have been changed by #{@amount.inspect}, but was changed by #{actual_delta.inspect}"
|
41
|
+
elsif @minimum
|
42
|
+
"#{@message} should have been changed by at least #{@minimum.inspect}, but was changed by #{actual_delta.inspect}"
|
43
|
+
elsif @maximum
|
44
|
+
"#{@message} should have been changed by at most #{@maximum.inspect}, but was changed by #{actual_delta.inspect}"
|
45
|
+
else
|
46
|
+
"#{@message} should have changed, but is still #{@before.inspect}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def actual_delta
|
51
|
+
@after - @before
|
52
|
+
end
|
53
|
+
|
54
|
+
def negative_failure_message
|
55
|
+
"#{@message} should not have changed, but did change from #{@before.inspect} to #{@after.inspect}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def by(amount)
|
59
|
+
@amount = amount
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def by_at_least(minimum)
|
64
|
+
@minimum = minimum
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def by_at_most(maximum)
|
69
|
+
@maximum = maximum
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def to(to)
|
74
|
+
@to = to
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
def from (from)
|
79
|
+
@from = from
|
80
|
+
self
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
module TestCaseExtensions
|
86
|
+
def change(receiver=nil, message=nil, &block)
|
87
|
+
Matchy::Expectations::ChangeExpectation.new(receiver, message, self, &block)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Matchy::Expectations
|
2
|
+
class HaveExpectation < Base
|
3
|
+
def initialize(expected, relativity=:exactly, test_case = nil)
|
4
|
+
@expected = (expected == :no ? 0 : expected)
|
5
|
+
@relativity = relativity
|
6
|
+
@test_case = test_case
|
7
|
+
end
|
8
|
+
|
9
|
+
def relativities
|
10
|
+
@relativities ||= {
|
11
|
+
:exactly => "",
|
12
|
+
:at_least => "at least ",
|
13
|
+
:at_most => "at most "
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(collection_owner)
|
18
|
+
if collection_owner.respond_to?(@collection_name)
|
19
|
+
collection = collection_owner.__send__(@collection_name, *@args, &@block)
|
20
|
+
elsif (@plural_collection_name && collection_owner.respond_to?(@plural_collection_name))
|
21
|
+
collection = collection_owner.__send__(@plural_collection_name, *@args, &@block)
|
22
|
+
elsif (collection_owner.respond_to?(:length) || collection_owner.respond_to?(:size))
|
23
|
+
collection = collection_owner
|
24
|
+
else
|
25
|
+
collection_owner.__send__(@collection_name, *@args, &@block)
|
26
|
+
end
|
27
|
+
@given = collection.size if collection.respond_to?(:size)
|
28
|
+
@given = collection.length if collection.respond_to?(:length)
|
29
|
+
raise not_a_collection if @given.nil?
|
30
|
+
return @given >= @expected if @relativity == :at_least
|
31
|
+
return @given <= @expected if @relativity == :at_most
|
32
|
+
return @given == @expected
|
33
|
+
end
|
34
|
+
|
35
|
+
def not_a_collection
|
36
|
+
"expected #{@collection_name} to be a collection but it does not respond to #length or #size"
|
37
|
+
end
|
38
|
+
|
39
|
+
def failure_message
|
40
|
+
"expected #{relative_expectation} #{@collection_name}, got #{@given}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def negative_failure_message
|
44
|
+
if @relativity == :exactly
|
45
|
+
return "expected target not to have #{@expected} #{@collection_name}, got #{@given}"
|
46
|
+
elsif @relativity == :at_most
|
47
|
+
return <<-EOF
|
48
|
+
Isn't life confusing enough?
|
49
|
+
Instead of having to figure out the meaning of this:
|
50
|
+
should_not have_at_most(#{@expected}).#{@collection_name}
|
51
|
+
We recommend that you use this instead:
|
52
|
+
should have_at_least(#{@expected + 1}).#{@collection_name}
|
53
|
+
EOF
|
54
|
+
elsif @relativity == :at_least
|
55
|
+
return <<-EOF
|
56
|
+
Isn't life confusing enough?
|
57
|
+
Instead of having to figure out the meaning of this:
|
58
|
+
should_not have_at_least(#{@expected}).#{@collection_name}
|
59
|
+
We recommend that you use this instead:
|
60
|
+
should have_at_most(#{@expected - 1}).#{@collection_name}
|
61
|
+
EOF
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def description
|
66
|
+
"have #{relative_expectation} #{@collection_name}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def respond_to?(sym)
|
70
|
+
@expected.respond_to?(sym) || super
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def method_missing(sym, *args, &block)
|
76
|
+
@collection_name = sym
|
77
|
+
if inflector = (defined?(ActiveSupport::Inflector) ? ActiveSupport::Inflector : (defined?(Inflector) ? Inflector : nil))
|
78
|
+
@plural_collection_name = inflector.pluralize(sym.to_s)
|
79
|
+
end
|
80
|
+
@args = args
|
81
|
+
@block = block
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def relative_expectation
|
86
|
+
"#{relativities[@relativity]}#{@expected}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
module TestCaseExtensions
|
92
|
+
def have(n)
|
93
|
+
HaveExpectation.new(n, :exactly, self)
|
94
|
+
end
|
95
|
+
alias :have_exactly :have
|
96
|
+
|
97
|
+
def have_at_least(n)
|
98
|
+
HaveExpectation.new(n, :at_least, self)
|
99
|
+
end
|
100
|
+
|
101
|
+
def have_at_most(n)
|
102
|
+
HaveExpectation.new(n, :at_most, self)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|