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.
@@ -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