foca-integrity 0.1.4 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|