dash-mario 0.15
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/CHANGELOG +93 -0
- data/Gemfile +16 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +100 -0
- data/Rakefile +34 -0
- data/dash-mario.gemspec +22 -0
- data/lib/dash-fu/mario.rb +154 -0
- data/lib/dash-fu/marios/backtweets.rb +57 -0
- data/lib/dash-fu/marios/backtweets.yml +9 -0
- data/lib/dash-fu/marios/github.rb +110 -0
- data/lib/dash-fu/marios/github.yml +15 -0
- data/lib/dash-fu/marios/github_issues.rb +91 -0
- data/lib/dash-fu/marios/github_issues.yml +14 -0
- data/lib/dash-fu/marios/ruby_gems.rb +55 -0
- data/lib/dash-fu/marios/ruby_gems.yml +6 -0
- data/test/api_keys.yml +1 -0
- data/test/backtweets_test.rb +151 -0
- data/test/cassettes/backtweets.yml +63 -0
- data/test/cassettes/github.yml +126 -0
- data/test/cassettes/github_issues.yml +123 -0
- data/test/cassettes/ruby_gems.yml +36 -0
- data/test/github_issues_test.rb +194 -0
- data/test/github_test.rb +251 -0
- data/test/helpers/activity.rb +21 -0
- data/test/helpers/metric.rb +22 -0
- data/test/helpers/person.rb +25 -0
- data/test/helpers/test.rb +161 -0
- data/test/ruby_gems_test.rb +164 -0
- data/test/setup.rb +31 -0
- data/test/test.log +852 -0
- metadata +164 -0
data/test/github_test.rb
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
require_relative "setup"
|
2
|
+
|
3
|
+
test DashFu::Mario::Github do
|
4
|
+
context "setup" do
|
5
|
+
setup { setup_source "repo"=>"assaf/vanity", "branch"=>"master" }
|
6
|
+
|
7
|
+
should "default branch to master" do
|
8
|
+
setup_source "repo"=>"assaf/vanity", "branch"=>" "
|
9
|
+
assert_equal "master", source["branch"]
|
10
|
+
end
|
11
|
+
|
12
|
+
context "metric" do
|
13
|
+
subject { metric }
|
14
|
+
|
15
|
+
should "use repository name" do
|
16
|
+
assert_equal "Github: assaf/vanity", subject.name
|
17
|
+
end
|
18
|
+
|
19
|
+
should "measure totals" do
|
20
|
+
assert subject.totals
|
21
|
+
end
|
22
|
+
|
23
|
+
should "capture commits" do
|
24
|
+
assert subject.columns.include?(:id=>"commits", :label=>"Commits")
|
25
|
+
end
|
26
|
+
|
27
|
+
should "capture watchers" do
|
28
|
+
assert subject.columns.include?(:id=>"watchers", :label=>"Watchers")
|
29
|
+
end
|
30
|
+
|
31
|
+
should "capture forks" do
|
32
|
+
assert subject.columns.include?(:id=>"forks", :label=>"Forks")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
context "validation" do
|
39
|
+
should "raise error if repository name missing" do
|
40
|
+
assert_raise(RuntimeError) { setup_source "repo"=>" ", "branch"=>"master" }
|
41
|
+
end
|
42
|
+
|
43
|
+
should "raise error if repository name not user/repo" do
|
44
|
+
assert_raise(RuntimeError) { setup_source "repo"=>"vanity", "branch"=>"master" }
|
45
|
+
end
|
46
|
+
|
47
|
+
should "allow alphanumeric, minus and underscore" do
|
48
|
+
setup_source "repo"=>"assaf/the-vanity_0", "branch"=>"master"
|
49
|
+
assert metric.valid?
|
50
|
+
end
|
51
|
+
|
52
|
+
should "create valid metric" do
|
53
|
+
setup_source "repo"=>"assaf/vanity", "branch"=>"master"
|
54
|
+
assert metric.valid?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
context "update" do
|
60
|
+
setup { setup_source "repo"=>"assaf/vanity", "branch"=>"master" }
|
61
|
+
|
62
|
+
should "handle 404" do
|
63
|
+
stub_request(:get, interactions.first.uri).to_return :status=>404
|
64
|
+
assert_raise(RuntimeError) { update_source }
|
65
|
+
assert_equal "Could not find the repository assaf/vanity", last_error
|
66
|
+
end
|
67
|
+
|
68
|
+
should "handle 401" do
|
69
|
+
stub_request(:get, interactions.first.uri).to_return :status=>401
|
70
|
+
assert_raise(RuntimeError) { update_source }
|
71
|
+
assert_equal "You are not authorized to access this repository, or invalid username/password", last_error
|
72
|
+
end
|
73
|
+
|
74
|
+
should "handle other error" do
|
75
|
+
stub_request(:get, interactions.first.uri).to_return :status=>500
|
76
|
+
assert_raise(RuntimeError) { update_source }
|
77
|
+
assert_equal "Last request didn't go as expected, trying again later", last_error
|
78
|
+
end
|
79
|
+
|
80
|
+
should "handle invlid document entity" do
|
81
|
+
stub_request(:get, interactions.first.uri).to_return :body=>"Not JSON"
|
82
|
+
assert_raise(RuntimeError) { update_source }
|
83
|
+
assert_equal "Last request didn't go as expected, trying again later", last_error
|
84
|
+
end
|
85
|
+
|
86
|
+
should "capture number of commits" do
|
87
|
+
update_source
|
88
|
+
assert_equal 35, totals[:commits]
|
89
|
+
end
|
90
|
+
|
91
|
+
should "capture number of wacthers" do
|
92
|
+
update_source
|
93
|
+
assert_equal 534, totals[:watchers]
|
94
|
+
end
|
95
|
+
|
96
|
+
should "capture number of forks" do
|
97
|
+
update_source
|
98
|
+
assert_equal 36, totals[:forks]
|
99
|
+
end
|
100
|
+
|
101
|
+
should "not create any activity" do
|
102
|
+
update_source
|
103
|
+
assert activities.empty?
|
104
|
+
end
|
105
|
+
|
106
|
+
context "repeating" do
|
107
|
+
setup do
|
108
|
+
update_source
|
109
|
+
repo = interactions.select { |i| i.uri =~ /repos\/show/ }
|
110
|
+
stub_request(:get, repo.first.uri).to_return :body=>repo.last.response.body
|
111
|
+
commits = interactions.select { |i| i.uri =~ /commits\/list/ }
|
112
|
+
stub_request(:get, commits.first.uri).to_return :body=>commits.last.response.body
|
113
|
+
update_source
|
114
|
+
end
|
115
|
+
|
116
|
+
should "update watchers" do
|
117
|
+
assert_equal 555, totals[:watchers]
|
118
|
+
end
|
119
|
+
|
120
|
+
should "update forks" do
|
121
|
+
assert_equal 38, totals[:forks]
|
122
|
+
end
|
123
|
+
|
124
|
+
should "capture new number of commits" do
|
125
|
+
assert_equal 39, totals[:commits]
|
126
|
+
end
|
127
|
+
|
128
|
+
context "activity" do
|
129
|
+
subject { activity }
|
130
|
+
|
131
|
+
should "capture commit URL" do
|
132
|
+
assert_equal "http://github.com/assaf/vanity/commit/dd154a9fdd2ac534b62b55b8acac2fd092d65439", subject.url
|
133
|
+
end
|
134
|
+
|
135
|
+
should "capture commit SHA" do
|
136
|
+
assert_equal "cc156a9fdd2ac534b62b55b8acac2fd092d65439", subject.uid
|
137
|
+
end
|
138
|
+
|
139
|
+
should "capture timestamp" do
|
140
|
+
assert_equal Time.parse("2010-08-06T00:22:01-07:00 UTC"), subject.timestamp
|
141
|
+
end
|
142
|
+
|
143
|
+
should "capture push" do
|
144
|
+
assert_match %{pushed to master at <a href="http://github.com/assaf/vanity">assaf/vanity</a>:}, subject.html
|
145
|
+
end
|
146
|
+
|
147
|
+
should "tag as push" do
|
148
|
+
assert_contains subject.tags, "push"
|
149
|
+
end
|
150
|
+
|
151
|
+
should "be valid" do
|
152
|
+
subject.validate
|
153
|
+
assert subject.valid?
|
154
|
+
end
|
155
|
+
|
156
|
+
context "person" do
|
157
|
+
subject { activity.person }
|
158
|
+
|
159
|
+
should "capture full name" do
|
160
|
+
assert_equal "Assaf Arkin", subject.fullname
|
161
|
+
end
|
162
|
+
|
163
|
+
should "capture email" do
|
164
|
+
assert_equal "assaf@labnotes.org", subject.email
|
165
|
+
end
|
166
|
+
|
167
|
+
should "capture identity" do
|
168
|
+
assert_contains subject.identities, "github.com:assaf"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context "commit message" do
|
173
|
+
subject { (Nokogiri::HTML(activity.html)/"blockquote").inner_text.strip }
|
174
|
+
|
175
|
+
should "start with commit SHA" do
|
176
|
+
assert_match /^cc156a9 /, subject
|
177
|
+
end
|
178
|
+
|
179
|
+
should "include first 50 characters of message" do
|
180
|
+
with_message "This is a very long message and we're only going to show the first 50 characters of it."
|
181
|
+
assert_equal 50, subject[/\s(.*)/, 1].length
|
182
|
+
end
|
183
|
+
|
184
|
+
should "use only first line of message" do
|
185
|
+
with_message "This message is made\nof two lines"
|
186
|
+
assert_match "This message is made", subject[/\s(.*)/, 1]
|
187
|
+
end
|
188
|
+
|
189
|
+
def with_message(message)
|
190
|
+
commits = interactions.select { |i| i.uri =~ /commits\/list/ }
|
191
|
+
commit = JSON.parse(commits.first.response.body)["commits"].first
|
192
|
+
commit["message"] = message
|
193
|
+
stub_request(:get, commits.first.uri).to_return :body=>{ :commits=>[commit] }.to_json
|
194
|
+
update_source
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "sequential commits" do
|
199
|
+
setup do
|
200
|
+
interaction = interactions.select { |i| i.uri =~ /commits\/list/ }.last
|
201
|
+
stub_request(:get, interaction.uri).to_return :body=>interaction.response.body
|
202
|
+
update_source
|
203
|
+
end
|
204
|
+
subject { (Nokogiri::HTML(activity.html)/"blockquote") }
|
205
|
+
|
206
|
+
should "show as multiple activities" do
|
207
|
+
assert_equal 3, activities.count # 4 commits -> 3 activities
|
208
|
+
end
|
209
|
+
|
210
|
+
should "merge related commits into single activity" do
|
211
|
+
assert_equal 2, subject.length # last one has two commits
|
212
|
+
end
|
213
|
+
|
214
|
+
should "merge related commits in order they were listed" do
|
215
|
+
first, second = subject.map { |bq| bq.inner_text.strip }
|
216
|
+
assert_equal "cc156a9 Most recent commit", first
|
217
|
+
assert_equal "dd156a9 Not most recent commit", second
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
context "meta" do
|
227
|
+
setup { setup_source "repo"=>"assaf/vanity", "branch"=>"master" }
|
228
|
+
subject { meta }
|
229
|
+
|
230
|
+
should "link to repository" do
|
231
|
+
assert_contains subject, :title=>"Repository", :text=>"assaf/vanity", :url=>"http://github.com/assaf/vanity"
|
232
|
+
end
|
233
|
+
|
234
|
+
should "include project description" do
|
235
|
+
assert_contains subject, :text=>"Experiment Driven Development for Ruby"
|
236
|
+
end
|
237
|
+
|
238
|
+
should "link to project home page" do
|
239
|
+
assert_contains subject, :title=>"Home page", :url=>"http://vanity.labnotes.org"
|
240
|
+
end
|
241
|
+
|
242
|
+
should "list branch name" do
|
243
|
+
assert_contains subject, :title=>"Branch", :text=>"master"
|
244
|
+
end
|
245
|
+
|
246
|
+
should "show last commit message" do
|
247
|
+
assert_contains subject, :title=>"Commit", :text=>"Gemfile changes necessary to pass test suite.",
|
248
|
+
:url=>"http://github.com/assaf/vanity/commit/dd154a9fdd2ac534b62b55b8acac2fd092d6543a"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Activity
|
2
|
+
def initialize(args = {})
|
3
|
+
args.each do |name, value|
|
4
|
+
send "#{name}=", value
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_accessor :uid, :html, :text, :url, :timestamp, :tags, :person
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
validate rescue return false
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate
|
16
|
+
raise "Must specify html or text" if html.blank? && text.blank?
|
17
|
+
raise "Tags must be alphanumeric, underscores allowed" unless tags.nil? || tags.all? { |tag| tag =~ /^[\w_]+$/ }
|
18
|
+
person.validate if person
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Metric
|
2
|
+
def initialize(args = {})
|
3
|
+
args.each do |name, value|
|
4
|
+
send "#{name}=", value
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_accessor :name, :columns, :totals
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
validate rescue return false
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate
|
16
|
+
fail "Metric name missing or a blank string" if name.blank?
|
17
|
+
fail "Metric must have at least one column" if columns.empty?
|
18
|
+
col_ids = columns.map { |col| col[:id] }
|
19
|
+
fail "All metric columns must have an id" unless col_ids.all? { |col_id| col_id }
|
20
|
+
fail "Metric columns must have unique ids" unless col_ids.uniq == col_ids
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Person
|
2
|
+
|
3
|
+
def initialize(args = {})
|
4
|
+
args[:identities] = Array.wrap(args[:identities])
|
5
|
+
args.each do |name, value|
|
6
|
+
send "#{name}=", value
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :fullname, :email, :identities, :photo_url
|
11
|
+
|
12
|
+
def valid?
|
13
|
+
validate rescue return false
|
14
|
+
return true
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate
|
18
|
+
if photo_url
|
19
|
+
uri = URI.parse(photo_url)
|
20
|
+
fail "Photo URL must be absolute HTTP URL" unless uri.scheme == "http" && uri.absolute?
|
21
|
+
end
|
22
|
+
fail "Identities must be of the form name:value" unless identities.nil? || identities.all? { |id| id =~ /^[\w\.]+:.+$/ }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# class, includes the modules DashFu::Mario::TestHelpers and Webmock, sets the
|
2
|
+
# Mario class, and other nicities.
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# test DashFu::Mario::RubyGems do
|
6
|
+
# . . .
|
7
|
+
# end
|
8
|
+
def test(klass, &block)
|
9
|
+
Class.new(Test::Unit::TestCase).tap do |test_case|
|
10
|
+
test_case.class_eval do
|
11
|
+
include WebMock
|
12
|
+
include DashFu::Mario::TestHelpers
|
13
|
+
end
|
14
|
+
test_case.instance_variable_set :@mario_class, klass
|
15
|
+
test_case.class_eval &block
|
16
|
+
Object.const_set "#{klass.name.demodulize}Test", test_case
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
module DashFu
|
22
|
+
module Mario
|
23
|
+
module TestHelpers
|
24
|
+
|
25
|
+
# Default setup. If you override setup, remember to call super.
|
26
|
+
def setup
|
27
|
+
WebMock.reset_webmock
|
28
|
+
VCR.insert_cassette(mario_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Default teardown. If you override teardown, remember to call super.
|
32
|
+
def teardown
|
33
|
+
VCR.eject_cassette
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the Mario. New Mario instance for each test.
|
37
|
+
def mario
|
38
|
+
@mario ||= mario_class.new
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets up source using supplied parameters, also calls validate.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# assert_raise RuntimeError do
|
45
|
+
# setup_source "gem_name"=>""
|
46
|
+
# end
|
47
|
+
# @example
|
48
|
+
# setup { setup_source "gem_name"=>"vanity" }
|
49
|
+
def setup_source(params)
|
50
|
+
mario.setup source, params
|
51
|
+
mario.validate source
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the source. New source for each test.
|
55
|
+
def source
|
56
|
+
@source ||= {}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Updates the source. If an error occurs, raises an exception. You can get
|
60
|
+
# the last error message by calling #last_error.
|
61
|
+
def update_source(request = nil)
|
62
|
+
@last_error = nil
|
63
|
+
mario.update source, request, &updator
|
64
|
+
rescue
|
65
|
+
@last_error = $!.message
|
66
|
+
raise
|
67
|
+
end
|
68
|
+
|
69
|
+
# Last error after calling #update_source.
|
70
|
+
attr_reader :last_error
|
71
|
+
|
72
|
+
# Returns a new metric. See Metric.
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
# assert_equal "My metric", metric.name
|
76
|
+
def metric
|
77
|
+
values = { :name=>source["metric.name"], :columns=>source["metric.columns"], :totals=>!!source["metric.totals"] }
|
78
|
+
Metric.new(values)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Performs an update on the source and returns the meta-data.
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# assert meta.include?(:title=>"Version", :text=>"1.4.0")
|
85
|
+
def meta
|
86
|
+
mario.update source, nil, &updator
|
87
|
+
mario.meta(source)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns totals collected by this source.
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# assert_equal({ :hits=>54 }, totals)
|
94
|
+
def totals
|
95
|
+
@totals ||= {}
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns all activities reported by this source. See Activity.
|
99
|
+
def activities
|
100
|
+
@activities ||= []
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns last (most recent) activity.
|
104
|
+
#
|
105
|
+
# @example
|
106
|
+
# assert_equal "Pushed a new release of awesome", activity.body
|
107
|
+
def activity
|
108
|
+
activities.last
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the named fixture. Fixture looked up in the file
|
112
|
+
# test/fixtures/<mario_id>.rb
|
113
|
+
def fixture(name)
|
114
|
+
fixtures = self.class.instance_variable_get(:@fixtures)
|
115
|
+
unless fixtures
|
116
|
+
fixtures = YAML.load_file(File.dirname(__FILE__) + "/fixtures/#{mario_id}.yml")
|
117
|
+
self.class.instance_variable_set :@fixtures, fixtures
|
118
|
+
end
|
119
|
+
fixtures[name]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the VCR interactions.
|
123
|
+
def interactions
|
124
|
+
VCR.current_cassette.recorded_interactions
|
125
|
+
end
|
126
|
+
|
127
|
+
protected
|
128
|
+
|
129
|
+
# Mario identifier (e.g. RubyGems => ruby_gems).
|
130
|
+
def mario_id
|
131
|
+
@mario_id ||= mario_class.name.demodulize.underscore
|
132
|
+
end
|
133
|
+
|
134
|
+
# The Mario class.
|
135
|
+
def mario_class
|
136
|
+
@mario_class ||= self.class.instance_variable_get :@mario_class
|
137
|
+
end
|
138
|
+
|
139
|
+
# Source update block.
|
140
|
+
def updator
|
141
|
+
lambda do |args|
|
142
|
+
if args[:set]
|
143
|
+
totals.update args[:set]
|
144
|
+
elsif args[:inc]
|
145
|
+
args[:inc].each do |name, value|
|
146
|
+
totals[name] ||= 0
|
147
|
+
totals[name] += value
|
148
|
+
end
|
149
|
+
elsif args[:activity]
|
150
|
+
values = args[:activity].clone
|
151
|
+
if person = values.delete(:person)
|
152
|
+
values[:person] = Person.new(person)
|
153
|
+
end
|
154
|
+
activities.push Activity.new(values)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|