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 +53 -32
- data/lib/mingle_events/poller.rb +2 -4
- data/lib/mingle_events/processors.rb +2 -0
- data/lib/mingle_events/processors/author_filter.rb +4 -4
- data/lib/mingle_events/processors/card_data.rb +26 -4
- data/lib/mingle_events/processors/card_type_filter.rb +7 -9
- data/lib/mingle_events/processors/category_filter.rb +5 -7
- data/lib/mingle_events/processors/custom_property_filter.rb +7 -9
- data/lib/mingle_events/processors/filter.rb +17 -0
- data/lib/mingle_events/processors/http_post_publisher.rb +2 -6
- data/lib/mingle_events/processors/pipeline.rb +1 -0
- data/lib/mingle_events/processors/processor.rb +17 -0
- data/lib/mingle_events/processors/puts_publisher.rb +2 -6
- data/lib/mingle_events/project_event_fetcher.rb +19 -1
- data/test/mingle_events/feed/changes_test.rb +0 -69
- data/test/mingle_events/processors/author_filter_test.rb +11 -18
- data/test/mingle_events/processors/card_data_test.rb +23 -0
- data/test/mingle_events/processors/card_type_filter_test.rb +25 -27
- data/test/mingle_events/processors/category_filter_test.rb +14 -11
- data/test/mingle_events/processors/custom_property_filter_test.rb +21 -27
- data/test/mingle_events/processors/filter_test.rb +19 -0
- data/test/mingle_events/processors/processor_test.rb +19 -0
- data/test/mingle_events/project_event_fetcher_test.rb +101 -0
- data/test/test_helper.rb +13 -0
- metadata +7 -3
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.
|
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
|
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
|
-
|
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
|
-
|
73
|
+
A more detailed dive into the polling and processing design follows.
|
74
|
+
|
75
|
+
h2. Polling and processing
|
40
76
|
|
41
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
|
data/lib/mingle_events/poller.rb
CHANGED
@@ -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.
|
21
|
-
|
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
|
16
|
-
|
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
|
-
|
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
|
19
|
-
|
20
|
-
event
|
21
|
-
|
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
|
12
|
-
|
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
|
21
|
-
|
22
|
-
event
|
23
|
-
|
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
|
@@ -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
|
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
|
|
@@ -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
|
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 =
|
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
|
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
|
-
|
51
|
-
|
49
|
+
assert author_filter.match?(@event_1)
|
50
|
+
assert !author_filter.match?(@event_2)
|
52
51
|
end
|
53
52
|
|
54
|
-
def
|
53
|
+
def test_match_on_author_login
|
55
54
|
author_filter = AuthorFilter.new({:login => 'ctester'}, @dummy_mingle_access, 'atlas')
|
56
|
-
|
57
|
-
|
55
|
+
assert author_filter.match?(@event_1)
|
56
|
+
assert !author_filter.match?(@event_2)
|
58
57
|
end
|
59
58
|
|
60
|
-
def
|
59
|
+
def test_match_on_author_email
|
61
60
|
author_filter = AuthorFilter.new({:email => 'joe.developer@example.com'}, @dummy_mingle_access, 'atlas')
|
62
|
-
|
63
|
-
|
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
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
31
|
-
|
32
|
-
|
33
|
-
|
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:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
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
|