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