mingle_events 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -4,7 +4,9 @@ h3. Overview
4
4
 
5
5
  Mingle 3.3 introduced a new Events API in the form of an "Atom feed":http://www.thoughtworks-studios.com/mingle/3.3/help/mingle_api_events.html. The Mingle team and ThoughtWorks Studios are big believers in the use of Atom for exposing events. Atom is a widely used standard, and this event API style puts the issue of robust event delivery in the hands of the consumer, where it belongs. In fact, we'd argue this is the only feasible means of robust, scalable event delivery, short of spending hundreds of thousands or millions of dollars on enterprise buses and such. Atom-delivered events are cheap, scalable, standards-based, and robust.
6
6
 
7
- However, we do accept that asking integrators wishing to consume events to implement polling is not ideal. Writing polling consumers can be tedious. And this tedium gets in the way of writing sweet Mingle integrations. We are addressing this by publishing libraries such as this, which if effective, fully hide the mechanics of event polling from the consumer. The consumer only need worry about the processing of events. Said processing is modeled in the style of 'pipes and filters.'
7
+ However, we do accept that asking integrators wishing to consume events to implement polling is not ideal. Writing polling consumers can be tedious. And this tedium gets in the way of writing sweet Mingle integrations. We are addressing this by publishing libraries such as this, which if effective, fully hide the mechanics of event polling from the consumer. The consumer only need worry about the processing of events.
8
+
9
+ The library supports both basic event analysis as well as polling for purposes of filtering and processing, e.g., re-publishing. The polling and processing portion is modeled in the style of 'pipes and filters.'
8
10
 
9
11
  h3. Installation
10
12
 
@@ -20,35 +22,68 @@ h3. Source
20
22
  git clone git://github.com/ThoughtWorksStudios/mingle_events.git
21
23
  </pre>
22
24
 
23
- h2. Quick example
25
+ h2. Quick examples
26
+
27
+ h3. Get the latest events, from time zero
28
+
29
+ The event fetcher will manage it's own state, so fetch_latest can be called repeatedly to fetch only new events. The first fetch will crawl all the way back to the very first event in the project's history.
24
30
 
25
31
  <pre>
26
- # configure access to mingle
27
32
  mingle_access = MingleEvents::MingleBasicAuthAccess.new('https://localhost:7071', 'david', 'p')
33
+ event_fetcher = MingleEvents::ProjectEventFetcher.new('my_project', mingle_access)
34
+ latest_events = event_fetcher.fetch_latest
35
+ </pre>
36
+
37
+ h3. Get the latest events, from now
38
+
39
+ Similar to the previous example, but a call to reset_to_now will tell the fetcher only to pull new stuff, starting now. If reset_now has been previously called, it will be ignored, and events will be fetched to the last seen event. If you have called reset_now previously, but really do wish to reset to now, you can call reset followed by reset_to_now.
40
+
41
+ <pre>
42
+ mingle_access = MingleEvents::MingleBasicAuthAccess.new('https://localhost:7071', 'david', 'p')
43
+ event_fetcher = MingleEvents::ProjectEventFetcher.new('my_project', mingle_access)
44
+ latest_events = event_fetcher.reset_to_now
45
+ latest_events = event_fetcher.fetch_latest
46
+ </pre>
47
+
48
+ h3. Historical analysis, via event playback
49
+
50
+ One of the great usages of event playback is historical analysis. E.g., if you wanted to see the number of stories added each day to your project, you could write a processor that counts new stories and takes a snapshot at the end of each day, where the end of the day is the first time you see an event for the next day. If you are doing this sort of analysis you will not want to poll the entire project's event history every time as that is quite time consuming.
51
+
52
+ The assumption is that you've previously fetched the latest events. Or at least you've fetched the events in which you are interested. Use all_fetched_entries to load previously fetched events, they're all cached on disk so you don't pay the prices of retrieving from Mingle.
53
+
54
+ <pre>
55
+ event_fetcher = MingleEvents::ProjectEventFetcher.new('my_project', mingle_access)
56
+ event_fetcher.all_fetched_entries.each do |e|
57
+ # do something interesting with each event (see examples folder for real analysis examples)
58
+ end
59
+ </pre>
60
+
61
+ h3. Polling and processing events via a pipeline
62
+
63
+ You can poll Mingle for new events at regular intervals and process those events to do things such as filter to events you are interested in and post them to other systems with which you wish to integrate Mingle. This example posts all new comments to an HTTP end point. You'd need to use cron or a similar scheduler to run it at regular intervals.
28
64
 
29
- # assemble processing pipeline
65
+ <pre>
30
66
  post_comments_to_another_service = MingleEvents::Processors::Pipeline.new([
31
67
  MingleEvents::Processors::CategoryFilter.new([MingleEvents::Feed::Category::COMMENT_ADDITION]),
32
68
  MingleEvents::Processors::HttpPostPublisher.new('http://localhost:4567/')
33
69
  ])
34
-
35
- # poll once
36
70
  MingleEvents::Poller.new(mingle_access, {'test_project' => [post_comments_to_another_service]}).run_once
37
71
  </pre>
38
72
 
39
- h2. High level design
73
+ A more detailed dive into the polling and processing design follows.
74
+
75
+ h2. Polling and processing
40
76
 
41
- This library pumps the stream of a Mingle project's events through a pipeline of processors that you specify. The processors can do things such as "filter out any events that are not sourced from a Story" or "post an event to an HTTP end-point."
77
+ h3. High level design
78
+
79
+ The Poller class can pump the stream of a Mingle project's events through a pipeline of processors that you specify. The processors can do things such as "filter out any events that are not sourced from a Story" or "post an event to an HTTP end-point."
42
80
 
43
81
  !http://thoughtworksstudios.github.com/mingle_events_design.png!
44
82
 
45
83
  As stated in the opening paragraph, the aim of this library is to hide the mechanics of event polling, making the user's focus solely the definition of the processing pipeline. This library supplies fundamental event processors, such as card type filters, atom category filters, and http publishers. This library should also make it easy for you to write custom processors.
46
84
 
47
- h2. Events and entries
48
-
49
- You might get confused looking at the source code as to what's an Atom entry and what's a Mingle event. We're still trying to clean that up a bit, but for all intents and purposes, they are the same thing. The Atom feed represents Mingle events in the form of Atom entries. For the most part we try to use the word 'entry' in the context of the feed and 'event' in the context of processing.
50
85
 
51
- h2. Processors, filters, and pipelines
86
+ h3. Processors, filters, and pipelines
52
87
 
53
88
  Processors, filters, and pipelines are all processors with the same interface. The fundamental model for pipes and filters, or pipelining, is that there is a single, common interface for processing input and returning output. In this context of Mingle event processing, the interface is basically "events in, events out" where "events in" is the list of unprocessed events and "events out" are the processed events. Processed events might be enriched, filtered, untouched but emailed, etc.
54
89
 
@@ -56,10 +91,11 @@ This library ships the following processors:
56
91
  * MingleEvents::Processors::CardData -- loads data for each card that sourced an event in the current stream. This processor requires some special handling (see next section).
57
92
  * MingleEvents::Processors::CardTypeFilter -- filters events to those sourced by cards of specific card type(s)
58
93
  * MingleEvents::Processors::CategoryFilter -- filters events to those with specified Atom categories. Mingle's Atom Categories are specified in MingleEvents::Category
94
+ * MingleEvents::Processors::CustomPropertyFilter -- filters events to those with the specified value of a single project-level custom property
59
95
  * MingleEvents::Processors::HttpPostPublisher -- posts event's raw XML to an HTTP endpoint
60
96
  * MingleEvents::Processors::Pipeline -- manages to processing of events by a sequence of processors
61
97
 
62
- h2. Card Data
98
+ h3. Card Data
63
99
 
64
100
  CardData is a special processor in that it implements a second interface, beyond event processing. This interface is one that allows the lookup of data for the card that sourced the event (if the event was actually sourced by a card). As looking up card data requires accessing additional Mingle server resources, you want to take special care that you don't make repeated requests for the same resources. If you have multiple processors requiring CardData, be sure to use a single instance of CardData across your entire pipeline.
65
101
 
@@ -76,9 +112,7 @@ post_commenting_on_high_priority_bugs_and_stories = MingleEvents::Processors::Pi
76
112
 
77
113
  Note that CardData will provide data for the version of the card that was created by the event you are processing and *not* the current version of the card.
78
114
 
79
- (For users familiar with earlier versions of this library, you will notice that the bulk loading of card data is currently not being taken advantage of. This is because I needed to change some internals of event broadcasting such that processing a big project's entire history did not cause OOM issues. I will very soon restore the bulk loading optimization for when the poller is simply reading the last handful of events. CardData still works, you just might see your client making a few more calls to Mingle.)
80
-
81
- h2. Writing your own processor
115
+ h3. Writing your own processor
82
116
 
83
117
  In ruby code, the processing interface is a single method named 'process_events' that has a single parameter, the list of unprocessed events' and returns a list of the processed events.
84
118
 
@@ -110,24 +144,11 @@ Be absolutely sure that any processor you write returns a list of events. If you
110
144
 
111
145
  Each event that is passed to the processor is an instance of type MingleEvents::Entry which is a Ruby wrapper around an Atom event. The Entry class makes it easy to access information such as author, Atom categories, whether the event was sourced by a card, etc. As the model is not yet complete, the Entry class also exposes the raw XML of the entry.
112
146
 
113
- (If you are wondering why the interface for processing is for a list of events rather than a single event, it is because of the above mentioned bulk loading of card data. This may change in the future. CardData may get its own special interface to perform a pre-processing bulk load, and the event processing interface will move to a single event at a time.)
114
-
115
- h2. Retry & Error handling
147
+ h3. Retry & Error handling
116
148
 
117
149
  As of now, retry is not implemented. If an error occurs during event processing, the error will be logged and processing will stop. The next run will re-start at the point of the last error.
118
150
 
119
- h2. Historical analysis
120
-
121
- One of the great usages of event playback is historical analysis. E.g., if you wanted to see the number of stories added each day to your project, you could write a processor that counts new stories and takes a snapshot at the end of each day, where the end of the day is the first time you see an event for the next day. If you are doing this sort of analysis you will not want to poll the entire project's event history every time as that is quite time consuming.
122
-
123
- It is possible to repeatedly run analysis against previously fetched events using the ProjectEventFetcher class that is only used in the internals of the standard polling mechanism. ProjectEventFetcher does most of the real work of reading new events from the server and making them available for local processing, so it's not a bad class to get to know.
124
-
125
- <pre>
126
- event_fetcher = MingleEvents::ProjectEventFetcher.new('my_project', mingle_access)
127
- event_fetcher.all_fetched_entries.each do |e|
128
- # do something interesting with each event
129
- end
130
- </pre>
151
+ h2. Events and entries
131
152
 
132
- Until there's time for further documentation take a look at card_count_by_day.rb and story_count_by_day.rb in the examples folder.
153
+ You might get confused looking at the source code, documentation, etc. as to what's an Atom entry and what's a Mingle event. We're still trying to clean that up a bit, but for all intents and purposes, they are the same thing. The Atom feed represents Mingle events in the form of Atom entries. For the most part we try to use the word 'entry' in the context of the feed and 'event' in the context of processing, but there's still cleanup to be done.
133
154
 
@@ -17,10 +17,8 @@ module MingleEvents
17
17
  @processors_by_project_identifier.each do |project_identifier, processors|
18
18
  fetcher = ProjectEventFetcher.new(project_identifier, @mingle_access)
19
19
  fetcher.reset if options[:clean]
20
- fetcher.fetch_latest.each do |entry|
21
- MingleEvents.log.info("About to process event #{entry.entry_id}...")
22
- processors.each{|p| p.process_events([entry])}
23
- end
20
+ latest_events = fetcher.fetch_latest.to_a
21
+ processors.each{|p| p.process_events(latest_events)}
24
22
  end
25
23
  end
26
24
 
@@ -1,5 +1,7 @@
1
1
  require 'cgi'
2
2
 
3
+ require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'filter'))
4
+ require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'processor'))
3
5
  require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'card_data'))
4
6
  require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'category_filter'))
5
7
  require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'card_type_filter'))
@@ -2,7 +2,7 @@ module MingleEvents
2
2
  module Processors
3
3
 
4
4
  # Removes all events from stream not triggered by the specified author
5
- class AuthorFilter
5
+ class AuthorFilter < Filter
6
6
 
7
7
  def initialize(spec, mingle_access, project_identifier)
8
8
  unless spec.size == 1
@@ -12,10 +12,10 @@ module MingleEvents
12
12
  @author_spec = AuthorSpec.new(spec, mingle_access, project_identifier)
13
13
  end
14
14
 
15
- def process_events(events)
16
- events.select{|event| @author_spec.event_triggered_by?(event)}
15
+ def match?(event)
16
+ @author_spec.event_triggered_by?(event)
17
17
  end
18
-
18
+
19
19
  class AuthorSpec
20
20
 
21
21
  def initialize(spec, mingle_access, project_identifier)
@@ -42,13 +42,35 @@ module MingleEvents
42
42
 
43
43
  def load_bulk_card_data
44
44
  @card_data_by_number_and_version = {}
45
-
46
- # TODO: figure out max number of card numbers before we have to chunk this up. or does mingle
47
- # figure out how to handle a too-large IN clause?
45
+
48
46
  card_numbers = @card_events.map(&:card_number).uniq
49
47
  path = "/api/v2/projects/#{@project_identifier}/cards/execute_mql.xml?mql=WHERE number IN (#{card_numbers.join(',')})"
50
48
 
51
- raw_xml = @mingle_access.fetch_page(URI.escape(path))
49
+ # TODO: figure out whether it makes sense to chunk a large count of card numbers
50
+ # into multiple requests so that the MQL "IN" clause doesn't explode. For now, we'll
51
+ # just punt by logging the error and letting the individual card data load explode
52
+ # if there's a real problem. In most polling scenarios, this is a highly unlikely
53
+ # problem as there will usually be 1 or a few events.
54
+ begin
55
+ raw_xml = @mingle_access.fetch_page(URI.escape(path))
56
+ rescue
57
+ msg = %{
58
+
59
+ There was an error while attempting bulk load of card data.
60
+ Individual data loads for each card will still be attempted.
61
+
62
+ Root cause:
63
+
64
+ #{$!.message}
65
+
66
+ Stack Trace:
67
+
68
+ #{($!.backtrace || []).join("\n")}
69
+
70
+ }
71
+ MingleEvents.log.info(msg)
72
+ return
73
+ end
52
74
  doc = Nokogiri::XML(raw_xml)
53
75
 
54
76
  doc.search('/results/result').map do |card_result|
@@ -8,21 +8,19 @@ module MingleEvents
8
8
  # and this filtering, the event will be filtered as there is no means
9
9
  # to determine its type. Therefore, it's recommended to also
10
10
  # subscribe a 'CardDeleted' processor to the same project.
11
- class CardTypeFilter
11
+ class CardTypeFilter < Filter
12
12
 
13
13
  def initialize(card_types, card_data)
14
14
  @card_types = card_types
15
15
  @card_data = card_data
16
16
  end
17
-
18
- def process_events(events)
19
- events.select do |event|
20
- event.card? &&
21
- @card_data.for_card_event(event) &&
22
- @card_types.include?(@card_data.for_card_event(event)[:card_type_name])
23
- end
17
+
18
+ def match?(event)
19
+ event.card? &&
20
+ @card_data.for_card_event(event) &&
21
+ @card_types.include?(@card_data.for_card_event(event)[:card_type_name])
24
22
  end
25
-
23
+
26
24
  end
27
25
  end
28
26
  end
@@ -2,18 +2,16 @@ module MingleEvents
2
2
  module Processors
3
3
 
4
4
  # Removes events from the stream that do not match all of the specified categories
5
- class CategoryFilter
5
+ class CategoryFilter < Filter
6
6
 
7
7
  def initialize(categories)
8
8
  @categories = categories
9
9
  end
10
-
11
- def process_events(events)
12
- events.select do |event|
13
- @categories.all?{|c| event.categories.include?(c)}
14
- end
10
+
11
+ def match?(event)
12
+ @categories.all?{|c| event.categories.include?(c)}
15
13
  end
16
-
14
+
17
15
  end
18
16
  end
19
17
  end
@@ -9,22 +9,20 @@ module MingleEvents
9
9
  # and this filtering, the event will be filtered as there is no means
10
10
  # to determine its type. Therefore, it's recommended to also
11
11
  # subscribe a 'CardDeleted' processor to the same project.
12
- class CustomPropertyFilter
12
+ class CustomPropertyFilter < Filter
13
13
 
14
14
  def initialize(property_name, property_value, card_data)
15
15
  @property_name = property_name
16
16
  @property_value = property_value
17
17
  @card_data = card_data
18
18
  end
19
-
20
- def process_events(events)
21
- events.select do |event|
22
- event.card? &&
23
- @card_data.for_card_event(event) &&
24
- @property_value == @card_data.for_card_event(event)[:custom_properties][@property_name]
25
- end
19
+
20
+ def match?(event)
21
+ event.card? &&
22
+ @card_data.for_card_event(event) &&
23
+ @property_value == @card_data.for_card_event(event)[:custom_properties][@property_name]
26
24
  end
27
-
25
+
28
26
  end
29
27
  end
30
28
  end
@@ -0,0 +1,17 @@
1
+ module MingleEvents
2
+ module Processors
3
+
4
+ class Filter
5
+
6
+ def process_events(events)
7
+ events.select{|e| match?(e)}
8
+ end
9
+
10
+ def match?(event)
11
+ raise "Subclass responsibility!"
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -1,17 +1,13 @@
1
1
  module MingleEvents
2
2
  module Processors
3
3
 
4
- class HttpPostPublisher
4
+ class HttpPostPublisher < Processor
5
5
 
6
6
  def initialize(url)
7
7
  @url = url
8
8
  end
9
9
 
10
- def process_events(events)
11
- events.map{|e| process_event(e)}
12
- end
13
-
14
- def process_event(event)
10
+ def process(event)
15
11
  Net::HTTP.post_form(URI.parse(@url), {'event' => event.raw_xml}).body
16
12
  end
17
13
 
@@ -17,4 +17,5 @@ module MingleEvents
17
17
  end
18
18
  end
19
19
  end
20
+
20
21
  end
@@ -0,0 +1,17 @@
1
+ module MingleEvents
2
+ module Processors
3
+
4
+ class Processor
5
+
6
+ def process_events(events)
7
+ events.map{|e| process(e)}
8
+ end
9
+
10
+ def process(event)
11
+ raise "Subclass responsibility!"
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -2,13 +2,9 @@ module MingleEvents
2
2
  module Processors
3
3
 
4
4
  # Writes each event in stream to stdout, mostly for demonstration purposes
5
- class PutsPublisher
5
+ class PutsPublisher < Processor
6
6
 
7
- def process_events(events)
8
- events.map{|e| process_event(e)}
9
- end
10
-
11
- def process_event(event)
7
+ def process(event)
12
8
  puts "Processing event #{event}"
13
9
  end
14
10
 
@@ -1,6 +1,10 @@
1
1
  module MingleEvents
2
2
 
3
3
  # fetch all unseen events and write them to disk for future processing
4
+ #
5
+ # this class is messy and needs some cleanup, but some things are in here
6
+ # for a reason. specifically, for historical analysis, we can process each event,
7
+ # one at at time, reading it off disk, to avoid massive memory consumption.
4
8
  class ProjectEventFetcher
5
9
 
6
10
  def initialize(project_identifier, mingle_access, state_dir=nil)
@@ -16,9 +20,19 @@ module MingleEvents
16
20
  FileUtils.rm_rf(@state_dir)
17
21
  end
18
22
 
23
+ # setup fetcher to only fetch new events, occuring beyond "right now"
24
+ def reset_to_now
25
+ return if last_entry_fetched
26
+
27
+ latest_event = page_with_latest_entries.entries.first
28
+ return if latest_event.nil?
29
+ write_entry_to_disk(latest_event, nil)
30
+ update_current_state(latest_event, latest_event)
31
+ end
32
+
19
33
  # fetch the latest events from mingle, i.e., the ones not previously seen
20
34
  def fetch_latest
21
- page = Feed::Page.new("/api/v2/projects/#{@project_identifier}/feeds/events.xml", @mingle_access)
35
+ page = page_with_latest_entries
22
36
  most_recent_new_entry = page.entries.first
23
37
  last_fetched_entry = load_last_fetched_entry
24
38
  last_fetched_entry_seen = false
@@ -79,6 +93,10 @@ module MingleEvents
79
93
 
80
94
  private
81
95
 
96
+ def page_with_latest_entries
97
+ Feed::Page.new("/api/v2/projects/#{@project_identifier}/feeds/events.xml", @mingle_access)
98
+ end
99
+
82
100
  def current_state_entry(info_file_key)
83
101
  if info_file = load_current_state[info_file_key]
84
102
  Feed::Entry.from_snippet(YAML.load(File.new(info_file))[:entry_xml])
@@ -270,75 +270,6 @@ module MingleEvents
270
270
  assert_nil(change[:new_value])
271
271
  end
272
272
 
273
- def test_foo
274
- foo = %{
275
- <entry>
276
- <id>https://mingle09.thoughtworks.com/projects/mingle/events/index/1344945</id>
277
- <title>story #67 CRUD Project created</title>
278
- <updated>2006-11-13T05:45:06Z</updated>
279
- <author>
280
- <name>Jon Tirsen</name>
281
- <email>jtirsen@thoughtworks.com</email>
282
- <uri>https://mingle09.thoughtworks.com/api/v2/users/10040.xml</uri>
283
- </author>
284
- <link href="https://mingle09.thoughtworks.com/api/v2/projects/mingle/cards/67.xml" rel="http://www.thoughtworks-studios.com/ns/mingle#event-source" type="application/vnd.mingle+xml" title="story #67"/>
285
- <link href="https://mingle09.thoughtworks.com/projects/mingle/cards/67" rel="http://www.thoughtworks-studios.com/ns/mingle#event-source" type="text/html" title="story #67"/>
286
- <link href="https://mingle09.thoughtworks.com/api/v2/projects/mingle/cards/67.xml?version=1" rel="http://www.thoughtworks-studios.com/ns/mingle#version" type="application/vnd.mingle+xml" title="story #67 (v1)"/>
287
- <link href="https://mingle09.thoughtworks.com/projects/mingle/cards/67?version=1" rel="http://www.thoughtworks-studios.com/ns/mingle#version" type="text/html" title="story #67 (v1)"/>
288
- <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
289
- <category term="card-creation" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
290
- <category term="card-type-change" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
291
- <category term="description-change" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
292
- <category term="name-change" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
293
- <category term="property-change" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
294
- <content type="application/vnd.mingle+xml">
295
- <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
296
- <change type="card-creation"/>
297
- <change type="card-type-change">
298
- <old_value nil="true"/>
299
- <new_value>
300
- <card_type url="https://mingle09.thoughtworks.com/api/v2/projects/mingle/card_types/10134.xml">
301
- <name>story</name>
302
- </card_type>
303
- </new_value>
304
- </change>
305
- <change type="description-change">
306
- </change>
307
- <change type="name-change">
308
- <old_value nil="true"/>
309
- <new_value>CRUD Project</new_value>
310
- </change>
311
- <change type="property-change">
312
- <property_definition url="https://mingle09.thoughtworks.com/api/v2/projects/mingle/property_definitions/10380.xml">
313
- <name>release</name>
314
- <position nil="true"/>
315
- <data_type>string</data_type>
316
- <is_numeric type="boolean">false</is_numeric>
317
- </property_definition>
318
- <old_value nil="true"/>
319
- <new_value>0.92</new_value>
320
- </change>
321
- <change type="property-change">
322
- <property_definition url="https://mingle09.thoughtworks.com/api/v2/projects/mingle/property_definitions/10381.xml">
323
- <name>Priority</name>
324
- <position nil="true"/>
325
- <data_type>string</data_type>
326
- <is_numeric type="boolean">false</is_numeric>
327
- </property_definition>
328
- <old_value nil="true"/>
329
- <new_value>Should</new_value>
330
- </change>
331
- </changes>
332
- </content>
333
- </entry>
334
- }
335
-
336
- entry = Entry.from_snippet(foo)
337
- entry.changes.each do |c|
338
- puts c
339
- end
340
- end
341
-
342
273
  end
343
274
  end
344
275
  end
@@ -33,9 +33,8 @@ module MingleEvents
33
33
  @event_1 = stub_event(1, {:uri => "http://example.com/users/10.xml", :login => 'ctester'})
34
34
  @event_2 = stub_event(2, {:uri => "http://example.com/users/17.xml", :login => 'jdeveloper'})
35
35
  @event_3 = stub_event(3, {:uri => "http://example.com/users/10.xml", :login => 'ctester'})
36
- @unprocessed_events = [@event_1, @event_2, @event_3]
37
36
  end
38
-
37
+
39
38
  def test_filter_can_only_be_constructed_with_a_single_criteria
40
39
  begin
41
40
  AuthorFilter.new({:url => 'foo', :email => 'bar'}, nil, nil)
@@ -45,30 +44,24 @@ module MingleEvents
45
44
  end
46
45
  end
47
46
 
48
- def test_filter_by_author_url
47
+ def test_match_on_author_url
49
48
  author_filter = AuthorFilter.new({:url => 'http://example.com/users/10.xml'}, @dummy_mingle_access, 'atlas')
50
- filtered_events = author_filter.process_events(@unprocessed_events)
51
- assert_equal([@event_1, @event_3], filtered_events)
49
+ assert author_filter.match?(@event_1)
50
+ assert !author_filter.match?(@event_2)
52
51
  end
53
52
 
54
- def test_filter_by_author_login
53
+ def test_match_on_author_login
55
54
  author_filter = AuthorFilter.new({:login => 'ctester'}, @dummy_mingle_access, 'atlas')
56
- filtered_events = author_filter.process_events(@unprocessed_events)
57
- assert_equal([@event_1, @event_3], filtered_events)
55
+ assert author_filter.match?(@event_1)
56
+ assert !author_filter.match?(@event_2)
58
57
  end
59
58
 
60
- def test_filter_by_author_email
59
+ def test_match_on_author_email
61
60
  author_filter = AuthorFilter.new({:email => 'joe.developer@example.com'}, @dummy_mingle_access, 'atlas')
62
- filtered_events = author_filter.process_events(@unprocessed_events)
63
- assert_equal([@event_2], filtered_events)
64
- end
65
-
66
- def test_filter_returns_empty_list_when_no_match
67
- author_filter = AuthorFilter.new({:email => 'sammy.soso@example.com'}, @dummy_mingle_access, 'atlas')
68
- filtered_events = author_filter.process_events(@unprocessed_events)
69
- assert_equal([], filtered_events)
61
+ assert !author_filter.match?(@event_1)
62
+ assert author_filter.match?(@event_2)
70
63
  end
71
-
64
+
72
65
  private
73
66
 
74
67
  def stub_event(entry_id, author)
@@ -188,6 +188,29 @@ module MingleEvents
188
188
 
189
189
  assert_nil(card_data.for_card_event(event_1))
190
190
  end
191
+
192
+ def test_survives_bulk_load_exploding
193
+ event = stub_event(4, 103, 13, ['card', 'property-change'])
194
+
195
+ dummy_mingle_access = StubMingleAccess.new
196
+ dummy_mingle_access.register_explosion(URI.escape('/api/v2/projects/atlas/cards/execute_mql.xml?mql=WHERE number IN (103)'))
197
+
198
+ dummy_mingle_access.register_page_content('http://example.com?version=13',%{
199
+ <card>
200
+ <number type="integer">103</number>
201
+ <card_type url="https://localhost:7071/api/v2/projects/atlas/card_types/21.xml">
202
+ <name>epic</name>
203
+ </card_type>
204
+ <version type="integer">13</version>
205
+ </card>
206
+ })
207
+
208
+ card_data = CardData.new(dummy_mingle_access, 'atlas')
209
+ card_data.process_events([event])
210
+
211
+ assert_correct_basic_card_data_for_event({:number => 103, :card_type_name => 'epic', :version => 13}, card_data, event)
212
+ end
213
+
191
214
 
192
215
  private
193
216
 
@@ -4,40 +4,38 @@ module MingleEvents
4
4
  module Processors
5
5
 
6
6
  class CardTypeFilterTest < Test::Unit::TestCase
7
-
8
- def test_filters_events_on_card_type
9
- event_1 = stub_event(true)
10
- event_2 = stub_event(false)
11
- event_3 = stub_event(true)
12
- event_4 = stub_event(true)
13
- event_5 = stub_event(true)
7
+
8
+ def setup
9
+ @story_event = stub_event(true)
10
+ @page_event = stub_event(false)
11
+ @bug_event = stub_event(true)
12
+ @issue_event = stub_event(true)
14
13
 
15
- card_data = {
16
- event_1 => {:card_type_name => 'story'},
17
- event_3 => {:card_type_name => 'bug'},
18
- event_4 => {:card_type_name => 'story'},
19
- event_5 => {:card_type_name => 'issue'}
14
+ @card_data = {
15
+ @story_event => {:card_type_name => 'story'},
16
+ @bug_event => {:card_type_name => 'bug'},
17
+ @issue_event => {:card_type_name => 'issue'}
20
18
  }
21
- def card_data.for_card_event(card_event)
19
+ def @card_data.for_card_event(card_event)
22
20
  self[card_event]
23
21
  end
24
22
 
25
- filter = CardTypeFilter.new(['story', 'issue'], card_data)
26
- filtered_events = filter.process_events([event_1, event_2, event_3, event_4, event_5])
27
- assert_equal([event_1, event_4, event_5], filtered_events)
23
+ @filter = CardTypeFilter.new(['story', 'issue'], @card_data)
28
24
  end
29
25
 
30
- def test_drops_events_for_deleted_cards
31
- event_1 = stub_event(true)
32
-
33
- card_data = {}
34
- def card_data.for_card_event(card_event)
35
- self[card_event]
36
- end
37
-
38
- filter = CardTypeFilter.new(['story', 'issue'], card_data)
39
- filtered_events = filter.process_events([event_1])
40
- assert_equal([], filtered_events)
26
+ def test_does_not_match_non_card_events
27
+ assert !@filter.match?(@page_event)
28
+ end
29
+
30
+ def test_match_on_card_type
31
+ assert @filter.match?(@story_event)
32
+ assert @filter.match?(@issue_event)
33
+ assert !@filter.match?(@bug_event)
34
+ end
35
+
36
+ def test_does_not_match_deleted_cards
37
+ @card_data[@story_event] = nil
38
+ assert !@filter.match?(@story_event)
41
39
  end
42
40
 
43
41
  private
@@ -3,17 +3,20 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_hel
3
3
  module MingleEvents
4
4
  module Processors
5
5
  class CategoryFilterTest < Test::Unit::TestCase
6
-
7
- def test_removes_events_without_matching_categories
8
- event_1 = stub_event(1, [Feed::Category::CARD, Feed::Category::COMMENT_ADDITION])
9
- event_2 = stub_event(2, [Feed::Category::CARD, Feed::Category::PROPERTY_CHANGE])
10
- event_3 = stub_event(3, [Feed::Category::REVISION_COMMIT])
11
- event_4 = stub_event(4, [Feed::Category::CARD, Feed::Category::PROPERTY_CHANGE])
12
- event_5 = stub_event(5, [])
13
- events = [event_1, event_2, event_3, event_4, event_5]
14
-
15
- filter = CategoryFilter.new([Feed::Category::CARD, Feed::Category::PROPERTY_CHANGE])
16
- assert_equal([event_2, event_4], filter.process_events(events))
6
+
7
+ def test_match_against_one_category
8
+ filter = CategoryFilter.new([Feed::Category::CARD])
9
+ assert filter.match?(stub_event(1, [Feed::Category::CARD, Feed::Category::COMMENT_ADDITION]))
10
+ assert filter.match?(stub_event(1, [Feed::Category::CARD]))
11
+ assert !filter.match?(stub_event(1, [Feed::Category::COMMENT_ADDITION]))
12
+ assert !filter.match?(stub_event(1, [Feed::Category::REVISION_COMMIT, Feed::Category::COMMENT_ADDITION]))
13
+ end
14
+
15
+ def test_match_against_multiple_categories
16
+ filter = CategoryFilter.new([Feed::Category::CARD, Feed::Category::COMMENT_ADDITION])
17
+ assert filter.match?(stub_event(1, [Feed::Category::CARD, Feed::Category::COMMENT_ADDITION]))
18
+ assert !filter.match?(stub_event(1, [Feed::Category::CARD]))
19
+ assert !filter.match?(stub_event(1, [Feed::Category::REVISION_COMMIT, Feed::Category::COMMENT_ADDITION]))
17
20
  end
18
21
 
19
22
  private
@@ -5,41 +5,35 @@ module MingleEvents
5
5
 
6
6
  class CustomPropertyFilterTest < Test::Unit::TestCase
7
7
 
8
- def test_filters_events_on_custom_property
9
- event_1 = stub_event(true)
10
- event_2 = stub_event(false)
11
- event_3 = stub_event(true)
12
- event_4 = stub_event(true)
13
- event_5 = stub_event(true)
8
+ def setup
9
+ @high_priority_event = stub_event(true)
10
+ @page_event = stub_event(false)
11
+ @low_priority_event = stub_event(true)
12
+ @high_severity_event = stub_event(true)
14
13
 
15
- card_data = {
16
- event_1 => {:custom_properties => {'Priority' => 'High'}},
17
- event_3 => {:custom_properties => {'Priority' => 'Low'}},
18
- event_4 => {:custom_properties => {'Priority' => 'High'}},
19
- event_5 => {:custom_properties => {'Severity' => 'High'}}
14
+ @card_data = {
15
+ @high_priority_event => {:custom_properties => {'Priority' => 'High'}},
16
+ @low_priority_event => {:custom_properties => {'Priority' => 'Low'}},
17
+ @high_severity_event => {:custom_properties => {'Severity' => 'High'}}
20
18
  }
21
- def card_data.for_card_event(card_event)
19
+ def @card_data.for_card_event(card_event)
22
20
  self[card_event]
23
21
  end
24
22
 
25
- filter = CustomPropertyFilter.new('Priority', 'High', card_data)
26
- filtered_events = filter.process_events([event_1, event_2, event_3, event_4, event_5])
27
- assert_equal([event_1, event_4], filtered_events)
23
+ @filter = CustomPropertyFilter.new('Priority', 'High', @card_data)
28
24
  end
29
-
30
- def test_drops_events_for_deleted_cards
31
- event_1 = stub_event(true)
32
-
33
- card_data = {}
34
- def card_data.for_card_event(card_event)
35
- self[card_event]
36
- end
37
-
38
- filter = CustomPropertyFilter.new('Priority', 'High', card_data)
39
- filtered_events = filter.process_events([event_1])
40
- assert_equal([], filtered_events)
25
+
26
+ def test_match_on_property_value
27
+ assert @filter.match?(@high_priority_event)
28
+ assert !@filter.match?(@low_priority_event)
29
+ assert !@filter.match?(@high_severity_event)
41
30
  end
42
31
 
32
+ def test_does_not_match_delete_card
33
+ @card_data[@high_priority_event] = nil
34
+ assert !@filter.match?(@high_priority_event)
35
+ end
36
+
43
37
  private
44
38
 
45
39
  def stub_event(is_card)
@@ -0,0 +1,19 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ module Processors
5
+ class FilterTest < Test::Unit::TestCase
6
+
7
+ def test_returns_events_that_match
8
+ assert_equal([0,2,4], MatchEvenFilter.new.process_events([0,1,2,3,4,5]))
9
+ end
10
+
11
+ class MatchEvenFilter < Filter
12
+ def match?(event)
13
+ event % 2 == 0
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ module Processors
5
+ class ProcessorTest < Test::Unit::TestCase
6
+
7
+ def test_returns_events_that_match
8
+ assert_equal([0,2,4], DoubleProcessor.new.process_events([0,1,2]))
9
+ end
10
+
11
+ class DoubleProcessor < Processor
12
+ def process(event)
13
+ event * 2
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -56,6 +56,107 @@ module MingleEvents
56
56
  assert_nil fetcher.last_entry_fetched
57
57
  end
58
58
 
59
+ def test_reset_to_now_when_project_has_previous_history
60
+ state_dir = temp_dir
61
+ mingle_access = stub_mingle_access
62
+ fetcher = ProjectEventFetcher.new('atlas', mingle_access, state_dir)
63
+
64
+ fetcher.reset_to_now
65
+ assert fetcher.fetch_latest.to_a.empty?
66
+
67
+ mingle_access.register_page_content('/api/v2/projects/atlas/feeds/events.xml',%{
68
+ <feed xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
69
+
70
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="current"/>
71
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="self"/>
72
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=2" rel="next"/>
73
+
74
+ <entry>
75
+ <id>https://mingle.example.com/projects/atlas/events/index/104</id>
76
+ <title>entry 104</title>
77
+ <updated>2011-02-03T08:14:42Z</updated>
78
+ <author><name>Bob</name></author>
79
+ </entry>
80
+ <entry>
81
+ <id>https://mingle.example.com/projects/atlas/events/index/103</id>
82
+ <title>entry 103</title>
83
+ <updated>2011-02-03T08:12:42Z</updated>
84
+ <author><name>Bob</name></author>
85
+ </entry>
86
+ </feed>
87
+ })
88
+
89
+ assert_equal([entry(104)], fetcher.fetch_latest.to_a)
90
+ end
91
+
92
+ def test_reset_to_now_is_ignored_if_there_is_already_local_current_state
93
+ state_dir = temp_dir
94
+ mingle_access = stub_mingle_access
95
+ fetcher = ProjectEventFetcher.new('atlas', mingle_access, state_dir)
96
+ fetcher.fetch_latest # bring current state up to 103
97
+
98
+ mingle_access.register_page_content('/api/v2/projects/atlas/feeds/events.xml',%{
99
+ <feed xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
100
+
101
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="current"/>
102
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="self"/>
103
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml?page=2" rel="next"/>
104
+
105
+ <entry>
106
+ <id>https://mingle.example.com/projects/atlas/events/index/104</id>
107
+ <title>entry 104</title>
108
+ <updated>2011-02-03T08:14:42Z</updated>
109
+ <author><name>Bob</name></author>
110
+ </entry>
111
+ <entry>
112
+ <id>https://mingle.example.com/projects/atlas/events/index/103</id>
113
+ <title>entry 103</title>
114
+ <updated>2011-02-03T08:12:42Z</updated>
115
+ <author><name>Bob</name></author>
116
+ </entry>
117
+ </feed>
118
+ })
119
+
120
+ fetcher.reset_to_now # if not ignored, next call would return no events rather than 104
121
+ assert_equal([entry(104)], fetcher.fetch_latest.to_a)
122
+ end
123
+
124
+ def test_reset_to_now_when_project_has_no_previous_history
125
+ state_dir = temp_dir
126
+ mingle_access = StubMingleAccess.new
127
+ mingle_access.register_page_content('/api/v2/projects/atlas/feeds/events.xml',%{
128
+ <?xml version="1.0" encoding="UTF-8"?>
129
+ <feed xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
130
+ <title>Mingle Events: Blank Project</title>
131
+ <id>https://mingle.example.com/api/v2/projects/blank_project/feeds/events.xml</id>
132
+ <link href="https://mingle.example.com/api/v2/projects/blank_project/feeds/events.xml" rel="current"/>
133
+ <link href="https://mingle.example.com/api/v2/projects/blank_project/feeds/events.xml" rel="self"/>
134
+ <updated>2011-08-04T19:42:04Z</updated>
135
+ </feed>})
136
+ fetcher = ProjectEventFetcher.new('atlas', mingle_access, state_dir)
137
+ fetcher.fetch_latest
138
+
139
+ fetcher.reset_to_now
140
+ assert fetcher.fetch_latest.to_a.empty?
141
+
142
+ mingle_access.register_page_content('/api/v2/projects/atlas/feeds/events.xml',%{
143
+ <feed xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
144
+
145
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="current"/>
146
+ <link href="https://mingle.example.com/api/v2/projects/atlas/feeds/events.xml" rel="self"/>
147
+
148
+ <entry>
149
+ <id>https://mingle.example.com/projects/atlas/events/index/104</id>
150
+ <title>entry 104</title>
151
+ <updated>2011-02-03T08:14:42Z</updated>
152
+ <author><name>Bob</name></author>
153
+ </entry>
154
+ </feed>
155
+ })
156
+
157
+ assert_equal([entry(104)], fetcher.fetch_latest.to_a)
158
+ end
159
+
59
160
  private
60
161
 
61
162
  def setup_current_state(first_entry_id, second_entry_id, last_entry_id, fetcher)
data/test/test_helper.rb CHANGED
@@ -119,6 +119,7 @@ class Test::Unit::TestCase
119
119
  def initialize
120
120
  @pages_by_path = {}
121
121
  @not_found_pages = []
122
+ @exploding_pages = []
122
123
  end
123
124
 
124
125
  def base_url
@@ -132,6 +133,10 @@ class Test::Unit::TestCase
132
133
  def register_page_not_found(path)
133
134
  @not_found_pages << path
134
135
  end
136
+
137
+ def register_explosion(path)
138
+ @exploding_pages << path
139
+ end
135
140
 
136
141
  def fetch_page(path)
137
142
  if @not_found_pages.include?(path)
@@ -141,6 +146,14 @@ class Test::Unit::TestCase
141
146
  end
142
147
  raise MingleEvents::HttpError.new(rsp, path)
143
148
  end
149
+
150
+ if @exploding_pages.include?(path)
151
+ rsp = Net::HTTPNotFound.new(nil, '500', 'Server exploded!')
152
+ def rsp.body
153
+ "500!!!!!"
154
+ end
155
+ raise MingleEvents::HttpError.new(rsp, path)
156
+ end
144
157
 
145
158
  raise "Attempting to fetch page at #{path}, but your test has not registered content for this path! Registered paths: #{@pages_by_path.keys.inspect}" unless @pages_by_path.key?(path)
146
159
  @pages_by_path[path]
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mingle_events
3
3
  version: !ruby/object:Gem::Version
4
- hash: 17
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 7
10
- version: 0.0.7
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - David Rice
@@ -72,8 +72,10 @@ files:
72
72
  - lib/mingle_events/processors/card_type_filter.rb
73
73
  - lib/mingle_events/processors/category_filter.rb
74
74
  - lib/mingle_events/processors/custom_property_filter.rb
75
+ - lib/mingle_events/processors/filter.rb
75
76
  - lib/mingle_events/processors/http_post_publisher.rb
76
77
  - lib/mingle_events/processors/pipeline.rb
78
+ - lib/mingle_events/processors/processor.rb
77
79
  - lib/mingle_events/processors/puts_publisher.rb
78
80
  - lib/mingle_events/processors.rb
79
81
  - lib/mingle_events/project_custom_properties.rb
@@ -92,7 +94,9 @@ files:
92
94
  - test/mingle_events/processors/card_type_filter_test.rb
93
95
  - test/mingle_events/processors/category_filter_test.rb
94
96
  - test/mingle_events/processors/custom_property_filter_test.rb
97
+ - test/mingle_events/processors/filter_test.rb
95
98
  - test/mingle_events/processors/pipeline_test.rb
99
+ - test/mingle_events/processors/processor_test.rb
96
100
  - test/mingle_events/project_custom_properties_test.rb
97
101
  - test/mingle_events/project_event_fetcher_test.rb
98
102
  - test/test_helper.rb