junkie 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
data/bin/junkie CHANGED
@@ -5,6 +5,14 @@
5
5
  $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
6
6
 
7
7
  require 'junkie'
8
- include Junkie
8
+ require 'logging'
9
+
10
+ Logging.appenders.stdout(
11
+ 'stdout',
12
+ :level => :debug,
13
+ :layout => Logging.layouts.pattern(:pattern => '[%d] %-5l: %m\n')
14
+ )
15
+ Logging.logger.root.appenders = Logging.appenders.stdout
9
16
 
17
+ include Junkie
10
18
  start_reactor
data/lib/junkie/errors.rb CHANGED
@@ -7,6 +7,8 @@ module Junkie
7
7
  class HTTP404Error < Exception; end
8
8
  class HTTP500Error < Exception; end
9
9
 
10
+ class InvalidResponse < Exception; end
11
+
10
12
  class InvalidConfigError < Exception; end
11
13
 
12
14
  class InvalidStateError < Exception; end
@@ -30,8 +30,8 @@ module Junkie
30
30
  protocol: 'http',
31
31
  host: 'localhost',
32
32
  port: 8000,
33
- api_user: '',
34
- api_password: ''
33
+ api_user: 'user',
34
+ api_password: 'password'
35
35
  }
36
36
 
37
37
  def initialize()
@@ -7,6 +7,8 @@ module Junkie
7
7
  class Observer
8
8
  include Log, Helper, Config
9
9
 
10
+ attr_accessor :api
11
+
10
12
  DEFAULT_CONFIG = {
11
13
  watchdog_refresh: 10, # interval the watchdog_timer is fired
12
14
  }
@@ -25,7 +27,6 @@ module Junkie
25
27
  @ready_for_new_links = true
26
28
  @watchdog_enabled = false
27
29
  @skipped_timer_at_first_complete_detection = false
28
- @should_send_it_on_channel = false
29
30
 
30
31
  @channels[:episodes].subscribe do |episode|
31
32
  next unless episode.status == :found
@@ -45,24 +46,13 @@ module Junkie
45
46
  }
46
47
 
47
48
  EM.add_periodic_timer(@config[:watchdog_refresh]) do
48
-
49
- # temporary fix for the SystemStackError
50
- if @should_send_it_on_channel
51
- log.info("Sending complete episode out on the channel")
52
- @channels[:episodes].push(@active_episode)
53
- @active_episode = nil
54
-
55
- @should_send_it_on_channel = false
56
- end
57
-
58
49
  monitor_progress if @watchdog_enabled
59
50
 
60
- in_fiber {
61
- add_next_episode_to_pyload
62
- }
51
+ in_fiber { add_next_episode_to_pyload }
63
52
  end
64
53
  end
65
54
 
55
+ private
66
56
 
67
57
  # Add next episode to Pyload which downloads the episode and extracts it
68
58
  #
@@ -99,81 +89,132 @@ module Junkie
99
89
  @ready_for_new_links
100
90
  end
101
91
 
102
- private
103
92
 
104
- # This method is called from the watchdog timer periodically
93
+ # Utility method that fetches the QueueData, validates it and returns
94
+ # the JSON Object as a real Ruby Object
105
95
  #
106
- # It monitors the download process and reacts depending on the results
96
+ # @raise [Junkie::InvalidResponse] if the response is faulty
97
+ # @return [Array] the JSON response as an object
98
+ def get_queue_data
99
+ data = @api.call(:getQueueData)
100
+
101
+ if data && data.is_a?(Array)
102
+ return data
103
+ end
104
+ raise Junkie::InvalidResponse,
105
+ "'#{data}' is not a valid response from Pyload"
106
+ end
107
+
108
+
109
+ #########################################################################
110
+ # This method is called from the watchdog timer periodically #
111
+ # #
112
+ # It monitors the download process and reacts depending on the results #
107
113
  def monitor_progress
108
114
  log.debug("Watchdog timer has been fired")
109
115
  in_fiber {
110
116
  catch(:break) {
111
- queue_data = @api.call(:getQueueData)
117
+ queue_data = get_queue_data
112
118
 
113
- if queue_data.empty? and @active_episode.nil?
114
- log.info("Empty Pyload queue, I cancel the watchdog timer")
115
- @watchdog_enabled = false
116
- throw :break
117
- end
119
+ # check for empty queue and disable watchdog if needed
120
+ check_for_empty_queue(queue_data)
118
121
 
119
122
  # extract package IDs and map them to current downloads
120
123
  update_package_ids(queue_data)
121
124
 
122
- if has_queue_any_failed_links?(queue_data)
123
- log.info("There are failed links in the Queue, will fix this")
124
- @api.call(:restartFailed)
125
- throw :break
126
- end
127
-
128
- # look for complete downloads
129
- pids = get_complete_downloads(queue_data)
130
-
131
- if not pids.empty?
132
- if @skipped_timer_at_first_complete_detection
133
-
134
- # post process complete active download and send it out on the
135
- # channel
136
- if pids.include? @active_episode.pid
137
- log.info("'#{@active_episode}' is extracted completely")
138
- @active_episode.pid = nil
139
- @active_episode.status = :extracted
140
-
141
- @should_send_it_on_channel = true
142
- end
143
-
144
- # remove all complete packages
145
- @api.call(:deletePackages, {pids: pids})
146
- log.info("Complete packages are removed from Pyload Queue")
147
-
148
- @skipped_timer_at_first_complete_detection = false
149
- @ready_for_new_links = true
150
- else
151
- # If a package is complete, we are sometimes so fast to remove
152
- # it, before the extracting can be started, so we will skip it
153
- # the first time
154
- log.info("Complete package detected, skip the first time")
155
- @skipped_timer_at_first_complete_detection = true
156
- end
157
- end
125
+ check_for_failed_links(queue_data)
126
+
127
+ check_for_complete_packages(queue_data)
158
128
  }
159
129
  }
130
+ rescue Junkie::InvalidResponse => e
131
+ log.error("Pyload returns InvalidResponse (#{e}), I will ignore this")
160
132
  end
161
133
 
162
134
 
163
- # Searches for failed links in the pyload queue
135
+ # Checks if the Pyload Queue is empty and disables the watchdog if
136
+ # it is really real
164
137
  #
165
138
  # @param [Array] queue_data returned from :getQueueData api method
139
+ def check_for_empty_queue(queue_data)
140
+ if queue_data.empty?
141
+
142
+ unless @active_episode.nil?
143
+ log.info("Empty queue detected but a download is set as active")
144
+ return
145
+ end
146
+
147
+ log.info("Empty Pyload queue detected, will double check")
148
+
149
+ # check twice if the queue is definitely empty
150
+ check_data = get_queue_data
151
+ if check_data.empty?
152
+ log.info("Double check is successful, I will disable the watchdog")
153
+ @watchdog_enabled = false
154
+ throw :break
155
+ else
156
+ log.info("Double check failed, Queue is not empty")
157
+ end
158
+ end
159
+ end
160
+
161
+ # Searches for failed links in the pyload queue and reacts on this
162
+ # through issuing a restartFailed call
166
163
  #
167
- # @return [Boolean] true if there are any failed links, false otherwise
168
- def has_queue_any_failed_links?(queue_data)
164
+ # @param [Array] queue_data returned from :getQueueData api method
165
+ def check_for_failed_links(queue_data)
166
+ failed = 0
169
167
  queue_data.each do |package|
170
168
  package['links'].each do |link|
171
169
  status = Pyload::Api::FILE_STATUS[link['status']]
172
- return true if status == :failed
170
+ (failed += 1) if status == :failed
173
171
  end
174
172
  end
175
173
 
176
- false
174
+ if failed > 0
175
+ log.info("There are failed links in the Queue, will fix this")
176
+ @api.call(:restartFailed)
177
+ throw :break
178
+ end
179
+ end
180
+
181
+
182
+ # Looks for complete packages and does some post-processing
183
+ #
184
+ # @param [Array] queue_data returned from :getQueueData api method
185
+ def check_for_complete_packages(queue_data)
186
+ pids = get_complete_downloads(queue_data)
187
+
188
+ throw :break if pids.empty?
189
+
190
+ if @skipped_timer_at_first_complete_detection
191
+
192
+ # post process complete active download and send it out on the
193
+ # channel
194
+ if pids.include? @active_episode.pid
195
+ log.info("'#{@active_episode}' has been extracted completely")
196
+ @active_episode.pid = nil
197
+ @active_episode.status = :extracted
198
+
199
+ log.info("Sending complete episode out on the channel")
200
+ @channels[:episodes].push(@active_episode)
201
+ @active_episode = nil
202
+ end
203
+
204
+ # remove all complete packages
205
+ @api.call(:deletePackages, { pids: pids })
206
+ log.info("Complete packages are removed from Pyload Queue #{ pids }")
207
+
208
+ @skipped_timer_at_first_complete_detection = false
209
+ @ready_for_new_links = true
210
+
211
+ else
212
+ # If a package is complete, we are sometimes so fast to remove
213
+ # it, before the extracting can be started, so we will skip it
214
+ # the first time
215
+ log.info("Complete package detected, skip the first time")
216
+ @skipped_timer_at_first_complete_detection = true
217
+ end
177
218
  end
178
219
 
179
220
 
@@ -189,46 +230,45 @@ module Junkie
189
230
 
190
231
  next if package['linksdone'] == 0
191
232
 
192
- # When extracting is in progress the status of the first link is set
193
- # to :extracting
194
- extracting = package['links'].select do |link|
195
- Pyload::Api::FILE_STATUS[link['status']] == :extracting
233
+ invalid_links = package['links'].reject do |e|
234
+ [:done, :skipped].include?(
235
+ Junkie::Pyload::Api::FILE_STATUS[e['status']])
196
236
  end
197
- next unless extracting.empty?
237
+ next unless invalid_links.empty?
198
238
 
199
239
  sizetotal = package['sizetotal'].to_i
200
240
  sizedone = package['sizedone'].to_i
201
241
 
202
- if sizetotal > 0 && sizedone > 0
203
-
204
- if sizetotal == sizedone
242
+ if sizetotal > 0 && sizedone > 0 && sizetotal == sizedone
205
243
  pids << package['pid']
206
- end
207
244
  end
208
245
  end
209
246
 
210
247
  pids
211
248
  end
212
249
 
250
+
213
251
  # extract package IDs and map them to current downloads
214
252
  #
215
253
  # @param [Array] queue_data returned from :getQueueData api method
216
254
  def update_package_ids(queue_data)
217
- queue_data.each do |package|
218
- pid = package['pid']
219
- next if @active_episode.pid == pid
220
255
 
221
- if @active_episode.pid == pid-1
222
- log.info("Package ID has been changed, I will correct this")
256
+ if queue_data.size == 1
257
+ pid = queue_data[0]['pid']
258
+ if @active_episode && @active_episode.pid != pid
259
+ log.info("Package ID has been changed, New PID is #{pid}")
223
260
  @active_episode.pid = pid
224
- next
225
261
  end
226
262
 
263
+ elsif queue_data.size > 1
227
264
  raise InvalidStateError,
228
- "PackageID #{pid} can't be mapped to active Download"
265
+ "There is more than 1 Package in the Queue"
229
266
  end
230
267
  end
231
268
 
269
+
270
+ # Removes existing packages from the Pyload Queue and creates a valid
271
+ # state to begin communication
232
272
  def cleanup
233
273
  log.debug("Made a cleanup of Pyload's Queue")
234
274
  @api.call(:stopAllDownloads)
@@ -1,3 +1,3 @@
1
1
  module Junkie
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
data/lib/junkie.rb CHANGED
@@ -9,14 +9,6 @@ require 'junkie/patched/sjunkieex'
9
9
  require 'junkie/pyload/api'
10
10
  require 'junkie/pyload/observer'
11
11
  require 'junkie/notification/twitter'
12
- require 'logging'
13
-
14
- Logging.appenders.stdout(
15
- 'stdout',
16
- :level => :debug,
17
- :layout => Logging.layouts.pattern(:pattern => '[%d] %-5l: %m\n')
18
- )
19
- Logging.logger.root.appenders = Logging.appenders.stdout
20
12
 
21
13
  module Junkie
22
14
 
@@ -0,0 +1,94 @@
1
+ require 'json'
2
+
3
+ module Junkie::Pyload::Observer::TestData
4
+ def self.get_fixture
5
+ json = <<-eos
6
+ [
7
+ {
8
+ "name": "onetreehill_sl_s09e04",
9
+ "links": [
10
+ {
11
+ "status": 12,
12
+ "format_size": "102.00 MiB",
13
+ "name": "onetreehill_sl_s09e04.part1.rar",
14
+ "plugin": "UploadedTo",
15
+ "url": "http:\/\/uploaded.net\/file\/kigscay8",
16
+ "packageID": 11,
17
+ "fid": 827,
18
+ "error": "",
19
+ "statusmsg": "downloade",
20
+ "order": 0,
21
+ "size": 106954752
22
+ },
23
+ {
24
+ "status": 12,
25
+ "format_size": "102.00 MiB",
26
+ "name": "onetreehill_sl_s09e04.part2.rar",
27
+ "plugin": "UploadedTo",
28
+ "url": "http:\/\/uploaded.net\/file\/pm6m7v3d",
29
+ "packageID": 11,
30
+ "fid": 828,
31
+ "error": "",
32
+ "statusmsg": "downloade",
33
+ "order": 1,
34
+ "size": 106954752
35
+ },
36
+ {
37
+ "status": 12,
38
+ "format_size": "102.00 MiB",
39
+ "name": "onetreehill_sl_s09e04.part3.rar",
40
+ "plugin": "UploadedTo",
41
+ "url": "http:\/\/uploaded.net\/file\/3tzo52uh",
42
+ "packageID": 11,
43
+ "fid": 829,
44
+ "error": "",
45
+ "statusmsg": "downloade",
46
+ "order": 2,
47
+ "size": 106954752
48
+ },
49
+ {
50
+ "status": 2,
51
+ "format_size": "102.00 MiB",
52
+ "name": "onetreehill_sl_s09e04.part4.rar",
53
+ "plugin": "UploadedTo",
54
+ "url": "http:\/\/uploaded.net\/file\/0e367cl3\/onetreehill_sl_s09e04.part4.rar",
55
+ "packageID": 11,
56
+ "fid": 830,
57
+ "error": "",
58
+ "statusmsg": "online",
59
+ "order": 3,
60
+ "size": 106954752
61
+ },
62
+ {
63
+ "status": 2,
64
+ "format_size": "78.96 MiB",
65
+ "name": "onetreehill_sl_s09e04.part5.rar",
66
+ "plugin": "UploadedTo",
67
+ "url": "http:\/\/uploaded.net\/file\/9iuhwfjk\/onetreehill_sl_s09e04.part5.rar",
68
+ "packageID": 11,
69
+ "fid": 831,
70
+ "error": "",
71
+ "statusmsg": "online",
72
+ "order": 4,
73
+ "size": 82795518
74
+ }
75
+ ],
76
+ "dest": 1,
77
+ "pid": 11,
78
+ "site": "",
79
+ "linksdone": 0,
80
+ "fids": null,
81
+ "sizetotal": 510614526,
82
+ "sizedone": 0,
83
+ "linkstotal": null,
84
+ "folder": "onetreehill_sl_s09e04",
85
+ "password": "",
86
+ "order": 0
87
+ }
88
+ ]
89
+ eos
90
+
91
+ JSON.load(json)
92
+ end
93
+
94
+ end
@@ -0,0 +1,216 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/fixtures/pyload_queue_data')
3
+ require 'yaml'
4
+
5
+ describe Junkie::Pyload::Observer do
6
+ include EMHelper
7
+
8
+ # remove the Config module behaviour
9
+ before(:each) do
10
+ Junkie::Config.stub!(:get_config) do |clas|
11
+ clas.class::DEFAULT_CONFIG
12
+ end
13
+
14
+ stub_const("Junkie::CONFIG_FILE", '/dev/null')
15
+ end
16
+
17
+ let(:channels) { {episodes: EM::Channel.new} }
18
+ let(:epi_channel) { channels[:episodes]}
19
+ let(:episode) do
20
+ epi = Junkie::Episode.new( "One Tree Hill", "http://local.host/12345",
21
+ "One.Tree.Hill.S09E01.Testepisode")
22
+ epi.status = :found
23
+ epi
24
+ end
25
+
26
+ context "monitor the pyload progress" do
27
+
28
+ let(:observer) do
29
+ ob = Junkie::Pyload::Observer.new(channels)
30
+ ob.api.stub(:call).and_return(nil)
31
+ ob.instance_variable_set(:@found_episodes, [episode])
32
+ ob.instance_variable_set(:@ready_for_new_links, true)
33
+ ob.send(:add_next_episode_to_pyload)
34
+
35
+ episode.pid = 10
36
+ ob
37
+ end
38
+
39
+ context "#get_queue_data" do
40
+
41
+ it "should throw InvalidResponse on empty string" do
42
+ observer.api.should_receive(:call).and_return("")
43
+ expect {
44
+ observer.send(:get_queue_data)
45
+ }.to raise_error(Junkie::InvalidResponse)
46
+ end
47
+
48
+ it "should throw InvalidResponse on empty response" do
49
+ observer.api.should_receive(:call).and_return(nil)
50
+ expect {
51
+ observer.send(:get_queue_data)
52
+ }.to raise_error(Junkie::InvalidResponse)
53
+ end
54
+
55
+ it "should return the valid response" do
56
+ response = [ {pid: 13} ]
57
+ observer.api.should_receive(:call).and_return(response)
58
+ observer.send(:get_queue_data).should eq response
59
+ end
60
+ end
61
+
62
+ context "#monitor_progress" do
63
+
64
+ let(:response) do
65
+ Junkie::Pyload::Observer::TestData::get_fixture
66
+ end
67
+
68
+ it "should catch InvalidResponse raised from #get_queue_data" do
69
+ observer.api.should_receive(:call).and_return("")
70
+
71
+ expect {
72
+ observer.send(:monitor_progress)
73
+ }.to_not raise_error(Junkie::InvalidResponse)
74
+ end
75
+
76
+ context "#check_for_empty_queue" do
77
+
78
+ it "should be an active watchdog timer" do
79
+ observer.instance_variable_get(:@watchdog_enabled).should be true
80
+ end
81
+
82
+ it "should disable the watchdog if there isn't an active download" do
83
+ observer.instance_variable_set(:@active_episode, nil)
84
+ # it double checks for emptiness
85
+ observer.should_receive(:get_queue_data).and_return([])
86
+
87
+ expect {
88
+ observer.send(:check_for_empty_queue, [])
89
+ }.to throw_symbol(:break)
90
+
91
+ observer.instance_variable_get(:@watchdog_enabled).should be false
92
+ end
93
+
94
+ it "shouldn't disable the watchdog if double check fails" do
95
+ observer.instance_variable_set(:@active_episode, nil)
96
+ observer.should_receive(:get_queue_data).and_return(response)
97
+
98
+ observer.send(:check_for_empty_queue, [])
99
+ observer.instance_variable_get(:@watchdog_enabled).should be true
100
+ end
101
+
102
+ it "shouldn't disable the watchdog if there is an active download" do
103
+ observer.send(:check_for_empty_queue, [])
104
+ observer.instance_variable_get(:@watchdog_enabled).should be true
105
+ end
106
+ end
107
+
108
+ context "#update_package_ids" do
109
+
110
+ it "should update the package id" do
111
+ observer.send(:update_package_ids, response)
112
+ episode.pid.should eq 11
113
+ end
114
+
115
+ it "should update the pid also if it is not incremented by 1" do
116
+ response[0]['pid'] = 12
117
+
118
+ expect {
119
+ observer.send(:update_package_ids, response)
120
+ }.to_not raise_error(Junkie::InvalidStateError)
121
+ episode.pid.should eq 12
122
+ end
123
+ end
124
+
125
+ context "#react_on_failed_links" do
126
+
127
+ let(:failed_id){ Junkie::Pyload::Api::FILE_STATUS.key(:failed) }
128
+
129
+ it "should call restartFailed on the Api if there are failed links" do
130
+ response[0]['links'][0]['status'] = failed_id
131
+
132
+ observer.api.should_receive(:call).with(:restartFailed)
133
+ expect {
134
+ observer.send(:check_for_failed_links, response)
135
+ }.to throw_symbol(:break)
136
+ end
137
+ end
138
+
139
+ context "#check_for_complete_packages" do
140
+
141
+ let(:file_status){ Junkie::Pyload::Api::FILE_STATUS }
142
+ let(:complete_response) do
143
+ copy = response.clone
144
+ new_links = copy[0]['links'].map do |e|
145
+ e['status'] = file_status.key(:done)
146
+ e
147
+ end
148
+ copy[0]['links'] = new_links
149
+ copy[0]['linksdone'] = new_links.size
150
+ copy[0]['sizedone'] = 12345
151
+ copy[0]['sizetotal'] = 12345
152
+ copy[0]['pid'] = 12
153
+ copy
154
+ end
155
+
156
+ context "#get_complete_downloads" do
157
+ it "should return pids where alle links are done and size is right" do
158
+ pids = observer.send(:get_complete_downloads, complete_response)
159
+ pids.should include(12)
160
+ end
161
+
162
+ it "should treat :skipped and :done links as complete" do
163
+ complete_response[0]['links'][1]['status'] = file_status.key(:skipped)
164
+ complete_response[0]['links'][3]['status'] = file_status.key(:skipped)
165
+
166
+ pids = observer.send(:get_complete_downloads, complete_response)
167
+ pids.should include(12)
168
+ end
169
+
170
+ it "should take file status more seriously than sizedone" do
171
+ complete_response[0]['links'][1]['status'] = file_status.key(:failed)
172
+
173
+ pids = observer.send(:get_complete_downloads, complete_response)
174
+ pids.should_not include(12)
175
+ end
176
+ end
177
+
178
+
179
+ context "detected complete package the first time" do
180
+
181
+ it "should set a flag if a complete package has been detected" do
182
+ observer.send(:check_for_complete_packages, complete_response)
183
+ observer.instance_variable_get(
184
+ :@skipped_timer_at_first_complete_detection).should eq true
185
+ end
186
+ end
187
+
188
+ context "detected complete package second time" do
189
+
190
+ it "should remove the complete packages from Queue" do
191
+ observer.api.should_receive(:call).with(:deletePackages, {pids: [12]})
192
+ observer.instance_variable_set(
193
+ :@skipped_timer_at_first_complete_detection, true)
194
+
195
+ em {
196
+ observer.send(:check_for_complete_packages, complete_response)
197
+ }
198
+ end
199
+
200
+ it "should send out the active episode out on the channel" do
201
+ observer.instance_variable_set(
202
+ :@skipped_timer_at_first_complete_detection, true)
203
+ episode.pid = 12
204
+
205
+ observer.send(:check_for_complete_packages, complete_response)
206
+ observer.instance_variable_get(:@active_episode).should be_nil
207
+ observer.send(:is_ready?).should be true
208
+ end
209
+
210
+ end
211
+ end
212
+
213
+ end
214
+ end
215
+ end
216
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: junkie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-11 00:00:00.000000000 Z
12
+ date: 2012-12-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
@@ -202,7 +202,9 @@ files:
202
202
  - lib/junkie/version.rb
203
203
  - spec/config_spec.rb
204
204
  - spec/environment_spec.rb
205
+ - spec/fixtures/pyload_queue_data.rb
205
206
  - spec/pyload_api_spec.rb
207
+ - spec/pyload_observer_spec.rb
206
208
  - spec/spec_helper.rb
207
209
  homepage: https://github.com/pboehm/junkie
208
210
  licenses: []
@@ -231,6 +233,8 @@ summary: TV series managament tool that does all the work you have with your ser
231
233
  test_files:
232
234
  - spec/config_spec.rb
233
235
  - spec/environment_spec.rb
236
+ - spec/fixtures/pyload_queue_data.rb
234
237
  - spec/pyload_api_spec.rb
238
+ - spec/pyload_observer_spec.rb
235
239
  - spec/spec_helper.rb
236
240
  has_rdoc: