ncs_mdes_warehouse 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/CHANGELOG.md +27 -0
  2. data/Rakefile +2 -2
  3. data/lib/ncs_navigator/warehouse.rb +2 -0
  4. data/lib/ncs_navigator/warehouse/configuration.rb +50 -0
  5. data/lib/ncs_navigator/warehouse/hooks.rb +21 -0
  6. data/lib/ncs_navigator/warehouse/hooks/etl_status_email.rb +88 -0
  7. data/lib/ncs_navigator/warehouse/mailer_templates/etl_status_email/failure_message.text.erb +8 -0
  8. data/lib/ncs_navigator/warehouse/mailer_templates/etl_status_email/success_message.text.erb +7 -0
  9. data/lib/ncs_navigator/warehouse/stringify_trace.rb +31 -0
  10. data/lib/ncs_navigator/warehouse/transform_load.rb +23 -3
  11. data/lib/ncs_navigator/warehouse/transform_status.rb +11 -2
  12. data/lib/ncs_navigator/warehouse/transformers/database.rb +13 -1
  13. data/lib/ncs_navigator/warehouse/transformers/enum_transformer.rb +65 -20
  14. data/lib/ncs_navigator/warehouse/transformers/subprocess_transformer.rb +4 -1
  15. data/lib/ncs_navigator/warehouse/transformers/vdr_xml/reader.rb +8 -0
  16. data/lib/ncs_navigator/warehouse/version.rb +1 -1
  17. data/ncs_mdes_warehouse.gemspec +1 -0
  18. data/spec/navigator.ini +3 -0
  19. data/spec/ncs_navigator/warehouse/configuration_spec.rb +79 -2
  20. data/spec/ncs_navigator/warehouse/data_mapper_spec.rb +1 -1
  21. data/spec/ncs_navigator/warehouse/database_initializer_spec.rb +1 -1
  22. data/spec/ncs_navigator/warehouse/hooks/etl_status_email_spec.rb +126 -0
  23. data/spec/ncs_navigator/warehouse/models/mdes_model_spec.rb +1 -1
  24. data/spec/ncs_navigator/warehouse/postgresql/pgpass_spec.rb +1 -1
  25. data/spec/ncs_navigator/warehouse/stringify_trace_spec.rb +58 -0
  26. data/spec/ncs_navigator/warehouse/table_modeler_spec.rb +1 -1
  27. data/spec/ncs_navigator/warehouse/transform_load_spec.rb +119 -5
  28. data/spec/ncs_navigator/warehouse/transform_status_spec.rb +32 -1
  29. data/spec/ncs_navigator/warehouse/transformers/database_spec.rb +11 -1
  30. data/spec/ncs_navigator/warehouse/transformers/enum_transformer_spec.rb +75 -1
  31. data/spec/ncs_navigator/warehouse/transformers/sampling_units_spec.rb +1 -1
  32. data/spec/ncs_navigator/warehouse/transformers/subprocess_transformer_spec.rb +7 -1
  33. data/spec/ncs_navigator/warehouse/transformers/vdr_xml/reader_spec.rb +14 -3
  34. data/spec/ncs_navigator/warehouse/transformers/vdr_xml_spec.rb +1 -1
  35. data/spec/ncs_navigator/warehouse/xml_emitter_spec.rb +1 -1
  36. data/spec/ncs_navigator/warehouse_spec.rb +1 -1
  37. data/spec/spec_helper.rb +3 -1
  38. metadata +307 -154
@@ -3,6 +3,7 @@ require 'childprocess'
3
3
  require 'forwardable'
4
4
  require 'json'
5
5
  require 'fileutils'
6
+ require 'bundler'
6
7
 
7
8
  module NcsNavigator::Warehouse::Transformers
8
9
  ##
@@ -99,7 +100,9 @@ module NcsNavigator::Warehouse::Transformers
99
100
  shell.say "Spawning subprocess transformer `#{exec_and_args.join(' ')}`"
100
101
 
101
102
  FileUtils.cd directory do
102
- process.start
103
+ Bundler.with_clean_env do
104
+ process.start
105
+ end
103
106
  end
104
107
 
105
108
  log.info " PID is #{process.pid}"
@@ -39,6 +39,14 @@ class NcsNavigator::Warehouse::Transformers::VdrXml
39
39
  @record_count = 0
40
40
  end
41
41
 
42
+ def name
43
+ if @filename
44
+ "VDR XML #{@filename}"
45
+ else
46
+ "VDR XML #{@io.inspect}"
47
+ end
48
+ end
49
+
42
50
  ##
43
51
  # Reads the XML and yields each encountered record as a warehouse
44
52
  # model instance to the provided block. It does not validate the
@@ -1,5 +1,5 @@
1
1
  module NcsNavigator
2
2
  module Warehouse
3
- VERSION = '0.4.1'
3
+ VERSION = '0.5.0'
4
4
  end
5
5
  end
@@ -37,6 +37,7 @@ Gem::Specification.new do |s|
37
37
  end
38
38
 
39
39
  s.add_dependency 'bcdatabase', '~> 1.1'
40
+ s.add_dependency 'actionmailer', '~> 3.0'
40
41
 
41
42
  s.add_development_dependency 'rspec', '~> 2.6'
42
43
  s.add_development_dependency 'rake', '~> 0.9.2'
data/spec/navigator.ini CHANGED
@@ -11,3 +11,6 @@ uri = "https://locahost/core"
11
11
 
12
12
  [PSC]
13
13
  uri = "https://localhost/psc"
14
+
15
+ [SMTP]
16
+ host = "smtp.ncs.gov"
@@ -1,4 +1,4 @@
1
- require File.expand_path('../../../spec_helper', __FILE__)
1
+ require 'spec_helper'
2
2
 
3
3
  module NcsNavigator::Warehouse
4
4
  describe Configuration do
@@ -41,6 +41,54 @@ module NcsNavigator::Warehouse
41
41
  end
42
42
  end
43
43
 
44
+ describe 'add_post_etl_hook' do
45
+ let(:success_hook) {
46
+ Object.new.tap do |o|
47
+ class << o
48
+ def etl_succeeded; end
49
+ end
50
+ end
51
+ }
52
+
53
+ let(:failure_hook) {
54
+ Object.new.tap do |o|
55
+ class << o
56
+ def etl_failed; end
57
+ end
58
+ end
59
+ }
60
+
61
+ it 'adds a hook with an `etl_succeeded` method' do
62
+ config.add_post_etl_hook(success_hook)
63
+ config.post_etl_hooks.should == [success_hook]
64
+ end
65
+
66
+ it 'adds a hook with an `etl_failed` method' do
67
+ config.add_post_etl_hook(failure_hook)
68
+ config.post_etl_hooks.should == [failure_hook]
69
+ end
70
+
71
+ describe 'with an object without either etl callback method' do
72
+ it 'gives a helpful message if the object is constructable' do
73
+ lambda { config.add_post_etl_hook(String) }.
74
+ should raise_error('String does not have an etl_succeeded or etl_failed method. Perhaps you meant String.new?')
75
+ end
76
+
77
+ it 'gives a helpful message if the object is an instance' do
78
+ lambda { config.add_post_etl_hook('not a good hook') }.
79
+ should raise_error('"not a good hook" does not have an etl_succeeded or etl_failed method.')
80
+ end
81
+
82
+ it 'does not add the object as a hook' do
83
+ begin
84
+ config.add_post_etl_hook('not a good hook')
85
+ rescue
86
+ end
87
+ config.post_etl_hooks.should be_empty
88
+ end
89
+ end
90
+ end
91
+
44
92
  describe '#mdes_version=' do
45
93
  context 'for a known version', :slow, :use_mdes, :modifies_warehouse_state do
46
94
  it 'makes the models available' do
@@ -141,7 +189,10 @@ module NcsNavigator::Warehouse
141
189
 
142
190
  describe '#shell_io' do
143
191
  it 'is $stderr by default' do
144
- config.shell_io.should be($stderr)
192
+ config.shell_io.object_id.should be($stderr.object_id)
193
+ # `should be($stderr)` and `should equal($stderr)` do not work
194
+ # with RSpec 2.10 and ci_reporter 1.6.6 because `should` is
195
+ # apparently delegated to the wrapped stream.
145
196
  end
146
197
 
147
198
  it 'can be set' do
@@ -281,6 +332,32 @@ module NcsNavigator::Warehouse
281
332
  end
282
333
  end
283
334
 
335
+ describe '#set_up_action_mailer' do
336
+ before do
337
+ config.set_up_action_mailer
338
+ end
339
+
340
+ it 'sets the SMTP settings from navigator.ini' do
341
+ ActionMailer::Base.smtp_settings[:address].should == 'smtp.ncs.gov'
342
+ end
343
+
344
+ it 'sets the delivery mode to SMTP' do
345
+ ActionMailer::Base.delivery_method.should == :smtp
346
+ end
347
+
348
+ it 'sets the template path appropriately' do
349
+ ActionMailer::Base.view_paths.collect(&:to_s).should == [
350
+ File.expand_path('../../../../lib/ncs_navigator/warehouse/mailer_templates', __FILE__)
351
+ ]
352
+ end
353
+
354
+ it 'only gets set up once' do
355
+ ActionMailer::Base.delivery_method = :test
356
+ config.set_up_action_mailer
357
+ ActionMailer::Base.delivery_method.should == :test
358
+ end
359
+ end
360
+
284
361
  describe 'bcdatabase' do
285
362
  describe 'group', :modifies_warehouse_state do
286
363
  subject { config.bcdatabase_group }
@@ -1,4 +1,4 @@
1
- require File.expand_path('../../../spec_helper', __FILE__)
1
+ require 'spec_helper'
2
2
 
3
3
  module NcsNavigator::Warehouse::DataMapper
4
4
  shared_examples 'a string-based NCS type' do
@@ -1,4 +1,4 @@
1
- require File.expand_path('../../../spec_helper', __FILE__)
1
+ require 'spec_helper'
2
2
 
3
3
  module NcsNavigator::Warehouse
4
4
  describe DatabaseInitializer do
@@ -0,0 +1,126 @@
1
+ require 'spec_helper'
2
+
3
+ module NcsNavigator::Warehouse::Hooks
4
+ describe EtlStatusEmail do
5
+ let(:config) {
6
+ NcsNavigator::Warehouse::Configuration.new.tap do |c|
7
+ c.log_file = tmpdir + "#{File.basename(__FILE__)}.log"
8
+ c.output_level = :quiet
9
+ end
10
+ }
11
+
12
+ let(:mailer) {
13
+ EtlStatusEmail.new(:to => to)
14
+ }
15
+
16
+ let(:to) {
17
+ ['jane@gcsc.net', 'joe@northwestern.edu']
18
+ }
19
+
20
+ let(:statuses) {
21
+ [
22
+ NcsNavigator::Warehouse::TransformStatus.memory_only('SP',
23
+ :start_time => Time.parse('2011-04-03T00:06:23'),
24
+ :end_time => Time.parse('2011-04-03T00:53:00'),
25
+ :record_count => 5,
26
+ :position => 0),
27
+ NcsNavigator::Warehouse::TransformStatus.memory_only('Co',
28
+ :start_time => Time.parse('2011-04-03T00:53:01'),
29
+ :end_time => Time.parse('2011-04-03T02:32:32'),
30
+ :record_count => 11,
31
+ :position => 1)
32
+ ]
33
+ }
34
+
35
+ let(:hook_args) {
36
+ {
37
+ :configuration => config,
38
+ :transform_statuses => statuses
39
+ }
40
+ }
41
+
42
+ let(:message) { ActionMailer::Base.deliveries.first }
43
+
44
+ before do
45
+ config.set_up_action_mailer
46
+ ActionMailer::Base.delivery_method = :test
47
+ end
48
+
49
+ after do
50
+ ActionMailer::Base.deliveries.clear
51
+ end
52
+
53
+ shared_examples 'an ETL result call' do
54
+ it 'sends an e-mail message' do
55
+ ActionMailer::Base.deliveries.size.should == 1
56
+ end
57
+
58
+ it 'sends the message to the configured addresses' do
59
+ message.to.should == to
60
+ end
61
+
62
+ it 'sends the message to the appropriate people based on their staff roles' do
63
+ pending 'would be nice: #2083'
64
+ end
65
+
66
+ describe 'the message content' do
67
+ it 'includes the duration' do
68
+ message.body.should =~ /2:26:09/
69
+ end
70
+
71
+ it 'includes the record count' do
72
+ message.body.should =~ /16 total records/
73
+ end
74
+
75
+ it 'includes a link to the warehouse dashboard' do
76
+ pending "there isn't one"
77
+ end
78
+ end
79
+ end
80
+
81
+ describe '#etl_succeeded' do
82
+ before do
83
+ mailer.etl_succeeded(hook_args)
84
+ end
85
+
86
+ include_examples 'an ETL result call'
87
+
88
+ describe 'the message subject' do
89
+ it 'indicates that the ETL succeeded' do
90
+ message.subject.should == '[NCS Navigator] Warehouse load successful'
91
+ end
92
+ end
93
+
94
+ describe 'the message content' do
95
+ it 'indicates that the ETL succeeded' do
96
+ message.body.should =~ /All transformations executed successfully./
97
+ end
98
+ end
99
+ end
100
+
101
+ describe '#etl_failed' do
102
+ before do
103
+ statuses[1].transform_errors <<
104
+ NcsNavigator::Warehouse::TransformError.new(:message => 'Not good enough')
105
+
106
+ mailer.etl_failed(hook_args)
107
+ end
108
+
109
+ include_examples 'an ETL result call'
110
+
111
+ describe 'the message subject' do
112
+ it 'indicates that the ETL failed' do
113
+ message.subject.should == '[NCS Navigator] Warehouse load failed'
114
+ end
115
+ end
116
+
117
+ describe 'the message content' do
118
+ it 'indicates that the ETL failed' do
119
+ message.body.
120
+ should =~ /1 transformation executed successfully.\s+1 transformation failed./
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+
@@ -1,4 +1,4 @@
1
- require File.expand_path('../../../../spec_helper', __FILE__)
1
+ require 'spec_helper'
2
2
 
3
3
  module NcsNavigator::Warehouse::Models
4
4
  module Spec
@@ -1,4 +1,4 @@
1
- require File.expand_path('../../../../spec_helper', __FILE__)
1
+ require 'spec_helper'
2
2
 
3
3
  module NcsNavigator::Warehouse::PostgreSQL
4
4
  describe Pgpass do
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ module NcsNavigator::Warehouse
4
+ describe StringifyTrace do
5
+ describe '#stringify_trace' do
6
+ it 'is available when mixed in' do
7
+ host = Class.new do
8
+ include StringifyTrace
9
+
10
+ def m
11
+ stringify_trace(['foo'])
12
+ end
13
+ end.new
14
+
15
+ lambda { host.m }.should_not raise_error
16
+ end
17
+ end
18
+
19
+ describe '.stringify_trace' do
20
+ let(:simple_trace) {
21
+ [
22
+ "(irb):2:in `irb_binding'",
23
+ "workspace.rb:80:in `eval'",
24
+ "foo/caller.rb:1024:in `<main>'"
25
+ ]
26
+ }
27
+
28
+ it 'aligns everything nicely' do
29
+ StringifyTrace.stringify_trace(simple_trace).should == <<-EXPECTED.chomp
30
+ (irb): 2:in `irb_binding'
31
+ workspace.rb: 80:in `eval'
32
+ foo/caller.rb:1024:in `<main>'
33
+ EXPECTED
34
+ end
35
+
36
+ it 'includes lines that cannot be parsed' do
37
+ StringifyTrace.stringify_trace(simple_trace + ['something up over here']).
38
+ should == <<-EXPECTED.chomp
39
+ (irb): 2:in `irb_binding'
40
+ workspace.rb: 80:in `eval'
41
+ foo/caller.rb:1024:in `<main>'
42
+ something up over here
43
+ EXPECTED
44
+ end
45
+
46
+ it 'aligns lines without specific context info' do
47
+ StringifyTrace.stringify_trace(simple_trace + ['bar/zap.rb:423']).
48
+ should == <<-EXPECTED.chomp
49
+ (irb): 2:in `irb_binding'
50
+ workspace.rb: 80:in `eval'
51
+ foo/caller.rb:1024:in `<main>'
52
+ bar/zap.rb: 423
53
+ EXPECTED
54
+ end
55
+ end
56
+ end
57
+ end
58
+
@@ -1,4 +1,4 @@
1
- require File.expand_path('../../../spec_helper', __FILE__)
1
+ require 'spec_helper'
2
2
 
3
3
  require 'ncs_navigator/mdes'
4
4
 
@@ -1,4 +1,4 @@
1
- require File.expand_path('../../../spec_helper', __FILE__)
1
+ require 'spec_helper'
2
2
 
3
3
  module NcsNavigator::Warehouse
4
4
  describe TransformLoad, :modifies_warehouse_state do
@@ -114,12 +114,13 @@ module NcsNavigator::Warehouse
114
114
 
115
115
  describe 'with a failure' do
116
116
  let(:runs) { [] }
117
+ let(:statuses) { TransformStatus.all(:order => [:id.desc]) }
117
118
 
118
119
  before do
119
120
  config.add_transformer(BlockTransformer.new { |s| runs << 'A' })
120
121
  config.add_transformer(BlockTransformer.new { |s|
121
122
  runs << 'B'
122
- fail 'No thanks.'
123
+ fail 'No thanks'
123
124
  })
124
125
  config.add_transformer(BlockTransformer.new { |s| runs << 'C3' })
125
126
  @result = loader.run
@@ -132,6 +133,11 @@ module NcsNavigator::Warehouse
132
133
  it 'returns false' do
133
134
  @result.should == false
134
135
  end
136
+
137
+ it 'records the error including the trace' do
138
+ statuses[1].transform_errors.first.message.
139
+ should =~ /No thanks\n.*#{File.basename(__FILE__)}:\s*\d+/
140
+ end
135
141
  end
136
142
 
137
143
  describe 'with an integrity error on transaction commit' do
@@ -162,10 +168,86 @@ module NcsNavigator::Warehouse
162
168
  end
163
169
  end
164
170
 
165
- describe 'sending e-mail' do
166
- it 'sends on success'
171
+ describe 'with post-ETL hooks' do
172
+ let(:hook_a) { RecordingHook.new }
173
+ let(:hook_success) { RecordingHook.new(:succeeded) }
174
+ let(:hook_failure) { RecordingHook.new(:failed) }
175
+ let(:hook_error) {
176
+ Class.new do
177
+ def etl_succeeded(ts)
178
+ fail 'Hook broke'
179
+ end
180
+ alias :etl_failed :etl_succeeded
181
+ end.new
182
+ }
183
+
184
+ let(:hooks_invoked) {
185
+ [hook_a, hook_success, hook_failure].map(&:invoked?)
186
+ }
187
+
188
+ before do
189
+ config.add_post_etl_hook(hook_a)
190
+ config.add_post_etl_hook(hook_success)
191
+ config.add_post_etl_hook(hook_failure)
192
+ end
193
+
194
+ shared_examples 'all hooks' do
195
+ before do
196
+ loader.run
197
+ end
198
+
199
+ it 'sends each hook the statuses' do
200
+ hook_a.transform_statuses.size.should == 1
201
+ end
202
+
203
+ it 'sends each hook the configuration' do
204
+ hook_a.configuration.should == config
205
+ end
206
+
207
+ it 'runs all the hooks even when one fails' do
208
+ config.post_etl_hooks.unshift hook_error
209
+
210
+ loader.run
211
+
212
+ hooks_invoked.should == expected_invoke_pattern
213
+ end
214
+ end
215
+
216
+ describe 'when the transform fails' do
217
+ let(:expected_invoke_pattern) { [true, false, true] }
218
+
219
+ before do
220
+ config.add_transformer(BlockTransformer.new { |s| fail 'Nope.' })
221
+ end
222
+
223
+ include_examples 'all hooks'
167
224
 
168
- it 'sends on failure'
225
+ it 'runs all the hooks that have an etl_failed method' do
226
+ hooks_invoked.should == expected_invoke_pattern
227
+ end
228
+
229
+ it 'sends each hook the overall status' do
230
+ hook_a.should be_failure
231
+ end
232
+ end
233
+
234
+ describe 'when the transform succeeds' do
235
+ let(:expected_invoke_pattern) { [true, true, false] }
236
+
237
+ before do
238
+ config.add_transformer(BlockTransformer.new { })
239
+ end
240
+
241
+ include_examples 'all hooks'
242
+
243
+ it 'runs all the hooks that have an etl_succeeded method' do
244
+ hooks_invoked.should == expected_invoke_pattern
245
+ end
246
+
247
+ it 'sends each hook the overall status' do
248
+ hook_a.should be_success
249
+ end
250
+ end
169
251
  end
170
252
 
171
253
  # This is a crappy test; it would be better if it could be done
@@ -195,5 +277,37 @@ module NcsNavigator::Warehouse
195
277
  @block.call(status)
196
278
  end
197
279
  end
280
+
281
+ class ::RecordingHook
282
+ attr_reader :transform_statuses, :configuration
283
+
284
+ def initialize(*modes)
285
+ @invoked = false
286
+ @modes = modes.empty? ? [:succeeded, :failed] : modes
287
+
288
+ @modes.each do |mode|
289
+ instance_eval <<-RUBY
290
+ def etl_#{mode}(args)
291
+ @invoked = true
292
+ @transform_statuses = args[:transform_statuses]
293
+ @configuration = args[:configuration]
294
+ @success = #{mode == :succeeded}
295
+ end
296
+ RUBY
297
+ end
298
+ end
299
+
300
+ def invoked?
301
+ @invoked
302
+ end
303
+
304
+ def success?
305
+ @success
306
+ end
307
+
308
+ def failure?
309
+ !@success.nil? && !@success
310
+ end
311
+ end
198
312
  end
199
313
  end