integrity-integrity 0.1.9.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +12 -0
- data/CHANGES +48 -0
- data/README.md +82 -0
- data/Rakefile +58 -0
- data/bin/integrity +4 -0
- data/config/config.sample.ru +21 -0
- data/config/config.sample.yml +45 -0
- data/config/heroku/.gems +1 -0
- data/config/heroku/Rakefile +6 -0
- data/config/heroku/config.ru +7 -0
- data/config/heroku/integrity-config.rb +15 -0
- data/config/thin.sample.yml +13 -0
- data/integrity.gemspec +136 -0
- data/lib/integrity.rb +77 -0
- data/lib/integrity/app.rb +138 -0
- data/lib/integrity/author.rb +39 -0
- data/lib/integrity/build.rb +52 -0
- data/lib/integrity/commit.rb +60 -0
- data/lib/integrity/core_ext/object.rb +6 -0
- data/lib/integrity/helpers.rb +15 -0
- data/lib/integrity/helpers/authorization.rb +33 -0
- data/lib/integrity/helpers/breadcrumbs.rb +20 -0
- data/lib/integrity/helpers/pretty_output.rb +45 -0
- data/lib/integrity/helpers/rendering.rb +49 -0
- data/lib/integrity/helpers/resources.rb +19 -0
- data/lib/integrity/helpers/urls.rb +59 -0
- data/lib/integrity/installer.rb +145 -0
- data/lib/integrity/migrations.rb +151 -0
- data/lib/integrity/notifier.rb +44 -0
- data/lib/integrity/notifier/base.rb +74 -0
- data/lib/integrity/notifier/test.rb +52 -0
- data/lib/integrity/notifier/test/fixtures.rb +108 -0
- data/lib/integrity/notifier/test/hpricot_matcher.rb +38 -0
- data/lib/integrity/project.rb +93 -0
- data/lib/integrity/project/notifiers.rb +31 -0
- data/lib/integrity/project/push.rb +43 -0
- data/lib/integrity/project_builder.rb +56 -0
- data/lib/integrity/scm.rb +19 -0
- data/lib/integrity/scm/git.rb +84 -0
- data/lib/integrity/scm/git/uri.rb +57 -0
- data/public/buttons.css +82 -0
- data/public/reset.css +7 -0
- data/public/spinner.gif +0 -0
- data/test/acceptance/api_test.rb +97 -0
- data/test/acceptance/browse_project_builds_test.rb +65 -0
- data/test/acceptance/browse_project_test.rb +99 -0
- data/test/acceptance/build_notifications_test.rb +114 -0
- data/test/acceptance/create_project_test.rb +97 -0
- data/test/acceptance/delete_project_test.rb +53 -0
- data/test/acceptance/edit_project_test.rb +117 -0
- data/test/acceptance/error_page_test.rb +21 -0
- data/test/acceptance/installer_test.rb +81 -0
- data/test/acceptance/manual_build_project_test.rb +82 -0
- data/test/acceptance/not_found_page_test.rb +29 -0
- data/test/acceptance/project_syndication_test.rb +30 -0
- data/test/acceptance/stylesheet_test.rb +26 -0
- data/test/acceptance/unauthorized_page_test.rb +20 -0
- data/test/helpers.rb +75 -0
- data/test/helpers/acceptance.rb +82 -0
- data/test/helpers/acceptance/email_notifier.rb +52 -0
- data/test/helpers/acceptance/git_helper.rb +99 -0
- data/test/helpers/acceptance/notifier_helper.rb +47 -0
- data/test/helpers/acceptance/textfile_notifier.rb +26 -0
- data/test/helpers/expectations.rb +4 -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/predicates.rb +37 -0
- data/test/helpers/initial_migration_fixture.sql +44 -0
- data/test/unit/build_test.rb +72 -0
- data/test/unit/commit_test.rb +66 -0
- data/test/unit/helpers_test.rb +103 -0
- data/test/unit/integrity_test.rb +35 -0
- data/test/unit/migrations_test.rb +57 -0
- data/test/unit/notifier/base_test.rb +43 -0
- data/test/unit/notifier_test.rb +96 -0
- data/test/unit/project_builder_test.rb +118 -0
- data/test/unit/project_test.rb +344 -0
- data/test/unit/scm_test.rb +54 -0
- data/views/_commit_info.haml +24 -0
- data/views/build.haml +2 -0
- data/views/error.haml +37 -0
- data/views/home.haml +21 -0
- data/views/integrity.sass +400 -0
- data/views/layout.haml +29 -0
- data/views/new.haml +50 -0
- data/views/not_found.haml +31 -0
- data/views/notifier.haml +7 -0
- data/views/project.builder +21 -0
- data/views/project.haml +30 -0
- data/views/unauthorized.haml +38 -0
- metadata +323 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
module Integrity
|
2
|
+
class Notifier
|
3
|
+
class Email < Notifier::Base
|
4
|
+
attr_reader :to, :from
|
5
|
+
|
6
|
+
def self.to_haml
|
7
|
+
<<-EOF
|
8
|
+
%p.normal
|
9
|
+
%label{ :for => "email_notifier_to" } Send to
|
10
|
+
%input.text#email_notifier_to{ :name => "notifiers[Email][to]", :type => "text", :value => config["to"] }
|
11
|
+
|
12
|
+
%p.normal
|
13
|
+
%label{ :for => "email_notifier_from" } Send from
|
14
|
+
%input.text#email_notifier_from{ :name => "notifiers[Email][from]", :type => "text", :value => config["from"] }
|
15
|
+
|
16
|
+
%h3 SMTP Server Configuration
|
17
|
+
|
18
|
+
%p.normal
|
19
|
+
%label{ :for => "email_notifier_host" } Host : Port
|
20
|
+
= succeed " : " do
|
21
|
+
%input.text#email_notifier_host{ :name => "notifiers[Email][host]", :value => config["host"], :style => "width: 24.5em;", :type => "text" }
|
22
|
+
%input.text#email_notifier_port{ :name => "notifiers[Email][port]", :value => config["port"], :style => "width: 3.5em;", :type => "text" }
|
23
|
+
|
24
|
+
%p.normal
|
25
|
+
%label{ :for => "email_notifier_user" } User
|
26
|
+
%input.text#email_notifier_user{ :name => "notifiers[Email][user]", :value => config["user"], :type => "text" }
|
27
|
+
|
28
|
+
%p.normal
|
29
|
+
%label{ :for => "email_notifier_pass" } Password
|
30
|
+
%input.text#email_notifier_pass{ :name => "notifiers[Email][pass]", :value => config["pass"], :type => "text" }
|
31
|
+
|
32
|
+
%p.normal
|
33
|
+
%label{ :for => "email_notifier_auth" } Auth type
|
34
|
+
%input.text#email_notifier_auth{ :name => "notifiers[Email][auth]", :value => (config["auth"] || "plain"), :type => "text" }
|
35
|
+
|
36
|
+
%p.normal
|
37
|
+
%label{ :for => "email_notifier_domain" } Domain
|
38
|
+
%input.text#email_notifier_domain{ :name => "notifiers[Email][domain]", :value => config["domain"], :type => "text" }
|
39
|
+
EOF
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(build, config={})
|
43
|
+
@to = config.delete("to")
|
44
|
+
@from = config.delete("from")
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
def deliver!
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -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: %H%n:author: %n :name: %an%n :email: %ae%n)
|
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,47 @@
|
|
1
|
+
module NotifierHelper
|
2
|
+
def fill_in_email_notifier
|
3
|
+
fill_in "notifiers[Email][to]", :with => "quentin@example.com"
|
4
|
+
fill_in "notifiers[Email][from]", :with => "ci@example.com"
|
5
|
+
fill_in "notifiers[Email][user]", :with => "inspector"
|
6
|
+
fill_in "notifiers[Email][pass]", :with => "gadget"
|
7
|
+
fill_in "notifiers[Email][auth]", :with => "simple"
|
8
|
+
fill_in "notifiers[Email][domain]", :with => "example.com"
|
9
|
+
end
|
10
|
+
|
11
|
+
def fill_in_project_info(name, repo)
|
12
|
+
fill_in "Name", :with => name
|
13
|
+
fill_in "Git repository", :with => repo
|
14
|
+
fill_in "Branch to track", :with => "master"
|
15
|
+
fill_in "Build script", :with => "rake"
|
16
|
+
check "Public project"
|
17
|
+
|
18
|
+
fill_in_email_notifier
|
19
|
+
end
|
20
|
+
|
21
|
+
def assert_have_email_notifier
|
22
|
+
assert_have_tag "input#email_notifier_to[@value='quentin@example.com']"
|
23
|
+
assert_have_tag "input#email_notifier_from[@value='ci@example.com']"
|
24
|
+
assert_have_tag "input#email_notifier_user[@value='inspector']"
|
25
|
+
assert_have_tag "input#email_notifier_pass[@value='gadget']"
|
26
|
+
assert_have_tag "input#email_notifier_auth[@value='simple']"
|
27
|
+
assert_have_tag "input#email_notifier_domain[@value='example.com']"
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_project(name, repo)
|
31
|
+
visit "/new"
|
32
|
+
fill_in_project_info(name, repo)
|
33
|
+
click_button "Create Project"
|
34
|
+
|
35
|
+
assert_have_tag("h1", :content => name)
|
36
|
+
click_link 'Edit Project'
|
37
|
+
assert_have_email_notifier
|
38
|
+
end
|
39
|
+
|
40
|
+
def edit_project(name)
|
41
|
+
visit "/#{name}"
|
42
|
+
click_link "Edit Project"
|
43
|
+
assert_have_email_notifier
|
44
|
+
fill_in :branch, :with => "testing"
|
45
|
+
click_button "Update Project"
|
46
|
+
end
|
47
|
+
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,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
|