dash-bees 0.18

Sign up to get free protection for your applications and to get access to all the features.
@@ -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