itunes_store_transporter 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ require "tmpdir"
1
2
  require "fileutils"
2
3
  require "itunes/store/transporter/errors"
3
4
  require "itunes/store/transporter/command"
@@ -24,7 +25,7 @@ module ITunes
24
25
  options[:destination] = Dir.mktmpdir
25
26
  super
26
27
  ensure
27
- FileUtils.rm_rf(options[:destination])
28
+ FileUtils.rm_rf(options[:destination]) if options[:destination]
28
29
  end
29
30
 
30
31
  protected
@@ -33,13 +34,13 @@ module ITunes
33
34
  path = File.join(options[:destination], "#{id}.itmsp", "metadata.xml")
34
35
 
35
36
  if !File.exists?(path)
36
- raise ITunes::Store::Transporter::TransporterError, "No metadata file exists at #{path}"
37
+ raise TransporterError, "No metadata file exists at #{path}"
37
38
  end
38
39
 
39
40
  begin
40
41
  metadata = File.read(path)
41
42
  rescue StandardError => e
42
- raise ITunes::Store::Transporter::TransporterError, "Failed to read metadata file #{path}: #{e}"
43
+ raise TransporterError, "Failed to read metadata file #{path}: #{e}"
43
44
  end
44
45
 
45
46
  metadata
@@ -10,24 +10,38 @@ module ITunes
10
10
  #
11
11
  class Status < Mode
12
12
  def initialize(*config)
13
- super
13
+ super
14
14
  options.on *VENDOR_ID
15
15
  end
16
16
 
17
17
  protected
18
18
  def handle_success(stdout_lines, stderr_lines, options)
19
19
  status = {}
20
- stdout_lines.each do |line|
21
- next unless line =~ /\A\s*\w/
22
- key, value = line.split(/:\s+/, 2).map(&:strip)
23
- key.gsub!(/\s+/, "_")
24
- key.downcase!
25
- status[key.to_sym] = value
20
+ while line = stdout_lines.shift
21
+ next unless line =~ /\S+/
22
+ if line =~ /\A--+/
23
+ entry = {}
24
+ while line = stdout_lines.shift
25
+ break unless line =~ /\A\s*\w/
26
+ key, value = parse_line(line)
27
+ entry[key] = value
28
+ end
29
+ (status[:status] ||= []) << entry
30
+ else
31
+ key, value = parse_line(line)
32
+ status[key] = value
33
+ end
26
34
  end
27
35
  status
28
- end
29
- end
36
+ end
30
37
 
38
+ def parse_line(line)
39
+ key, value = line.split(/:\s+/, 2).map(&:strip)
40
+ key.gsub!(/\s+/, "_")
41
+ key.downcase!
42
+ [key.to_sym, value]
43
+ end
44
+ end
31
45
  end
32
46
  end
33
47
  end
@@ -20,11 +20,17 @@ module ITunes
20
20
  end
21
21
 
22
22
  protected
23
+ def create_transporter_options(optz)
24
+ # Include the option if false
25
+ optz[:verify_assets] = !optz[:verify_assets] if optz.include?(:verify_assets)
26
+ super
27
+ end
28
+
23
29
  # Verify mode returns 0 if there are no packages to verify but will emit an error message about the lack of packages
24
30
  def handle_success(stdout_lines, stderr_lines, options)
25
- parser = Transporter::OutputParser.new(stderr_lines)
31
+ parser = OutputParser.new(stderr_lines)
26
32
  if parser.errors.any?
27
- raise ITunes::Store::Transporter::ExecutionError.new(parser.errors, 0)
33
+ raise ExecutionError.new(parser.errors, 0)
28
34
  else
29
35
  true
30
36
  end
@@ -34,4 +40,3 @@ module ITunes
34
40
  end
35
41
  end
36
42
  end
37
-
@@ -5,78 +5,77 @@ module ITunes
5
5
  class Transporter
6
6
  class OutputParser
7
7
 
8
- ##
9
- # This class extracts error and warning messages output by +iTMSTransporter+. For each message
10
- # is creates an instance of ITunes::Store::Transporter::TransporterMessage
11
- #
8
+ ##
9
+ # This class extracts error and warning messages output by +iTMSTransporter+. For each message
10
+ # is creates an instance of ITunes::Store::Transporter::TransporterMessage
11
+ #
12
12
 
13
- attr :errors
14
- attr :warnings
13
+ attr :errors
14
+ attr :warnings
15
15
 
16
- ERROR_LINE = />\s+ERROR:\s+(.+)/
17
- WARNING_LINE = />\s+WARN:\s+(.+)/
16
+ ERROR_LINE = />\s+ERROR:\s+(.+)/
17
+ WARNING_LINE = />\s+WARN:\s+(.+)/
18
18
 
19
- # Generic messages we want to ignore.
20
- SKIP_ERRORS = [ /\boperation was not successful/i,
19
+ # Generic messages we want to ignore.
20
+ SKIP_ERRORS = [ /\boperation was not successful/i,
21
21
  /\bunable to verify the package/i,
22
22
  /\bwill NOT be verified/,
23
23
  /^an error has occurred/i,
24
24
  /^an error occurred while/i,
25
25
  /^unknown operation/i,
26
- /\bunable to authenticate the package/i ]
26
+ /\bunable to authenticate the package/i ]
27
27
 
28
- ##
29
- # === Arguments
30
- #
31
- # [output (Array)] +iTMSTransporter+ output
32
- #
33
- def initialize(output)
34
- @errors = []
35
- @warnings = []
36
- parse_output(output) if Array === output
37
- end
28
+ ##
29
+ # === Arguments
30
+ #
31
+ # [output (Array)] +iTMSTransporter+ output
32
+ #
33
+ def initialize(output)
34
+ @errors = []
35
+ @warnings = []
36
+ parse_output(output) if Array === output
37
+ end
38
38
 
39
- private
40
- def parse_output(output)
41
- output.each do |line|
42
- if line =~ ERROR_LINE
43
- error = $1
44
- next if SKIP_ERRORS.any? { |skip| error =~ skip }
45
- errors << create_message(error)
46
- elsif line =~ WARNING_LINE
47
- warnings << create_message($1)
48
- end
49
- end
39
+ private
40
+ def parse_output(output)
41
+ output.each do |line|
42
+ if line =~ ERROR_LINE
43
+ error = $1
44
+ next if SKIP_ERRORS.any? { |skip| error =~ skip }
45
+ errors << create_message(error)
46
+ elsif line =~ WARNING_LINE
47
+ warnings << create_message($1)
48
+ end
49
+ end
50
50
 
51
- # Unique messages only. The block form of uniq() not available on Ruby < 1.9.2
51
+ # Unique messages only. The block form of uniq() is not available on Ruby < 1.9.2
52
52
  [errors, warnings].each do |e|
53
- e.replace(e.inject({}) do |uniq, x|
54
- uniq[x.message] = x
55
- uniq
56
- end.values)
53
+ next if e.empty?
54
+ uniq = {}
55
+ e.replace(e.select { |m| uniq.include?(m.message) ? false : uniq[m.message] = true })
57
56
  end
58
- end
57
+ end
59
58
 
60
- def create_message(line)
61
- case line
62
- when /^(?:ERROR|WARNING)\s+ITMS-(\d+):\s+(.+)/
63
- code = $1
64
- message = $2
65
- when /(.+)\s+\((\d+)\)$/,
59
+ def create_message(line)
60
+ case line
61
+ when /^(?:ERROR|WARNING)\s+ITMS-(\d+):\s+(.+)/
62
+ code = $1
63
+ message = $2
64
+ when /(.+)\s+\((\d+)\)$/,
66
65
  # Is this correct?
67
- /(.+)\s+errorCode\s+=\s+\((\d+)\)$/
68
- message = $1
69
- code = $2
70
- else
71
- message = line
72
- code = nil
73
- end
66
+ /(.+)\s+errorCode\s+=\s+\((\d+)\)$/
67
+ message = $1
68
+ code = $2
69
+ else
70
+ message = line
71
+ code = nil
72
+ end
74
73
 
75
- message.gsub! /^"/, ""
74
+ message.gsub! /^"/, ""
76
75
  message.gsub! /"(?: at .+)?$/, ""
77
76
 
78
- TransporterMessage.new(message, code ? code.to_i : nil)
79
- end
77
+ TransporterMessage.new(message, code ? code.to_i : nil)
78
+ end
80
79
  end
81
80
  end
82
81
  end
@@ -1,4 +1,5 @@
1
1
  require "childprocess"
2
+ require "itunes/store/transporter/errors"
2
3
 
3
4
  module ITunes
4
5
  module Store
@@ -35,6 +36,8 @@ module ITunes
35
36
  end
36
37
 
37
38
  def exec(argv, &block)
39
+ raise ArgumentError, "block required" unless block_given?
40
+
38
41
  begin
39
42
  process = ChildProcess.build(path, *argv)
40
43
 
@@ -54,7 +57,7 @@ module ITunes
54
57
 
55
58
  poll(stdout[0], stderr[0], &block)
56
59
  rescue ChildProcess::Error, SystemCallError => e
57
- raise ITunes::Store::Transporter::TransporterError, e.message
60
+ raise TransporterError, e.message
58
61
  ensure
59
62
  process.wait if process.alive?
60
63
  [ stdout, stderr ].flatten.each { |io| io.close if !io.closed? }
@@ -68,6 +71,7 @@ module ITunes
68
71
  read = [ stdout, stderr ]
69
72
 
70
73
  loop do
74
+ # TODO: Not working on jruby
71
75
  if ready = select(read, nil, nil, 1)
72
76
  ready.each do |set|
73
77
  next unless set.any?
@@ -1,7 +1,7 @@
1
1
  module ITunes
2
2
  module Store
3
3
  class Transporter
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
6
6
  end
7
7
  end
@@ -3,12 +3,12 @@ require "stringio"
3
3
 
4
4
  shared_examples_for "a transporter option" do |option, expected|
5
5
  it "creates the correct command line argument" do
6
- ITunes::Store::Transporter::Shell.any_instance.stub(:exec) { |*arg| arg.first.should include(*expected); 0 }
6
+ expect_shell_args(*expected)
7
7
  subject.run(options.merge(option))
8
8
  end
9
9
  end
10
10
 
11
- shared_examples_for "a vendor_id option" do
11
+ shared_examples_for "a vendor_id option" do
12
12
  it_should_behave_like "a transporter option", { :vendor_id => "vID" }, "-vendor_id", "vID"
13
13
  end
14
14
 
@@ -22,25 +22,25 @@ shared_examples_for "a transporter option that expects a directory" do |option,
22
22
  lambda { subject.run(options.merge(option => "__baaaaahd_directory__")) }.should raise_exception(ITunes::Store::Transporter::OptionError, /does not exist/)
23
23
  end
24
24
  end
25
- end
25
+ end
26
26
 
27
27
  shared_examples_for "a boolean transporter option" do |option, expected|
28
- context "when true" do
28
+ context "when true" do
29
29
  it "creates the command line argument" do
30
- ITunes::Store::Transporter::Shell.any_instance.stub(:exec) { |*arg| arg.first.should include(*expected); 0 }
30
+ expect_shell_args(*expected)
31
31
  subject.run(options.merge(option => true))
32
32
  end
33
33
  end
34
34
 
35
- context "when false" do
35
+ context "when false" do
36
36
  it "does not create the command line argument" do
37
- ITunes::Store::Transporter::Shell.any_instance.stub(:exec) { |*arg| arg.first.should_not include(*expected); 0 }
37
+ ITunes::Store::Transporter::Shell.any_instance.should_receive(:exec) { |*arg| arg.first.should_not include(*expected); 0 }
38
38
  subject.run(options.merge(option => false))
39
39
  end
40
40
  end
41
41
 
42
- context "when not boolean" do
43
- it "raises an OptionError" do
42
+ context "when not boolean" do
43
+ it "raises an OptionError" do
44
44
  lambda { subject.run(options.merge(option => "sshaw")) }.should raise_exception(ITunes::Store::Transporter::OptionError, /does not accept/)
45
45
  end
46
46
  end
@@ -76,7 +76,7 @@ shared_examples_for "a subclass of Command::Base" do
76
76
  it "automatically sets NoPause to true" do
77
77
  ENV["PROGRAMFILES"] = "C:\\"
78
78
  shell = ITunes::Store::Transporter::Shell
79
- shell.any_instance.stub(:exec) { |*arg| arg.first.should include("-WONoPause", "true"); 0 }
79
+ expect_shell_args("-WONoPause", "true")
80
80
  shell.stub(:windows? => true)
81
81
  subject.run(options)
82
82
  end
@@ -166,7 +166,7 @@ shared_examples_for "a subclass of Command::Base" do
166
166
  e.errors.should have(2).items
167
167
 
168
168
  # just check one
169
- e.errors[0].should be_a(ITunes::Store::Transporter::TransporterMessage)
169
+ e.errors[0].should be_a(ITunes::Store::Transporter::TransporterMessage)
170
170
  e.errors[0].code.should == 9000
171
171
  e.errors[0].message.should match("Your audio of screwed up!")
172
172
  e.errors[1].code.should == 4009
@@ -297,7 +297,7 @@ describe ITunes::Store::Transporter::Command::Upload do
297
297
  describe ":log_history" do
298
298
  it_should_behave_like "a transporter option that expects a directory", :log_history, "-loghistory"
299
299
  end
300
-
300
+
301
301
  describe ":success" do
302
302
  it_should_behave_like "a transporter option that expects a directory", :success, "-success"
303
303
  end
@@ -321,27 +321,27 @@ describe ITunes::Store::Transporter::Command::Lookup do
321
321
  before(:each) do
322
322
  @tmpdir = Dir.mktmpdir
323
323
  Dir.stub(:mktmpdir => @tmpdir)
324
-
324
+
325
325
  id = options[:vendor_id] || options[:apple_id]
326
326
  @package = File.join(@tmpdir, "#{id}.itmsp")
327
327
  Dir.mkdir(@package)
328
-
328
+
329
329
  @metadata = "<x>Metadata</x>"
330
330
  File.open(File.join(@package, "metadata.xml"), "w") { |io| io.write(@metadata) }
331
-
332
- mock_output
333
331
  end
334
-
332
+
335
333
  after(:each) { FileUtils.rm_rf(@tmpdir) }
336
-
334
+
337
335
  describe "#run" do
338
- context "when successful" do
336
+ before { mock_output }
337
+
338
+ context "when successful" do
339
339
  it "returns the metadata and deletes the temp directory used to output the metadata" do
340
340
  subject.run(options).should == @metadata
341
341
  File.exists?(@tmpdir).should be_false
342
342
  end
343
343
 
344
- context "when the metadata file was not created" do
344
+ context "when the metadata file was not created" do
345
345
  before { FileUtils.rm_rf(@tmpdir) }
346
346
 
347
347
  it "raises a TransporterError" do
@@ -354,14 +354,14 @@ describe ITunes::Store::Transporter::Command::Lookup do
354
354
  # One of these two should be requied, but they should be mutually exclusive
355
355
  describe "options" do
356
356
  describe ":vendor_id" do
357
- let(:options) { create_options({:vendor_id => "vID"}) }
357
+ let(:options) { create_options({:vendor_id => "vID"}) }
358
358
  it_should_behave_like "a vendor_id option"
359
359
  end
360
360
 
361
361
  describe ":apple_id" do
362
- let(:options) { create_options({:apple_id => "aID"}) }
362
+ let(:options) { create_options({:apple_id => "aID"}) }
363
363
  it_should_behave_like "a transporter option", { :apple_id => "aID" }, "-apple_id", "aID"
364
- end
364
+ end
365
365
  end
366
366
  end
367
367
 
@@ -407,24 +407,45 @@ describe ITunes::Store::Transporter::Command::Status do
407
407
 
408
408
  describe "#run" do
409
409
  context "when successful" do
410
- it "returns the status information for the package" do
411
- mock_output(:stdout => "status.vendor_id_123123", :stderr => "stderr.info")
412
- subject.run(options).should == {
413
- :vendor_identifier => "123123",
414
- :apple_identifier => "123123",
415
- :itunesconnect_status => "Not ready for sale",
416
- :upload_created => "2000-01-01 00:00:00",
417
- :upload_state => "Uploaded",
418
- :upload_state_id => "1",
419
- :content_state => "Irie",
420
- :content_state_id => "2"
421
- }
410
+ context "with a single status" do
411
+ it "returns the status information for the package" do
412
+ mock_output(:stdout => "status.vendor_id_123123", :stderr => "stderr.info")
413
+ subject.run(options).should == {
414
+ :vendor_identifier => "123123",
415
+ :apple_identifier => "123123",
416
+ :itunesconnect_status => "Not ready for sale",
417
+ :status => [ { :upload_created => "2000-01-01 00:00:00",
418
+ :upload_state => "Uploaded",
419
+ :upload_state_id => "1",
420
+ :content_state => "Irie",
421
+ :content_state_id => "2" } ]
422
+ }
423
+ end
424
+ end
425
+
426
+ context "with multiple status" do
427
+ it "returns all the status information for the package" do
428
+ mock_output(:stdout => "status.vendor_id_789789", :stderr => "stderr.info")
429
+ subject.run(options).should == {
430
+ :vendor_identifier => "789789",
431
+ :status => [ { :upload_created => "2000-01-01 00:00:00",
432
+ :upload_state => "Uploaded",
433
+ :upload_state_id => "1",
434
+ :content_state => "Irie",
435
+ :content_state_id => "2" },
436
+ { :upload_created => "2000-01-02 00:00:00",
437
+ :upload_state => "Some Failure",
438
+ :upload_state_id => "2",
439
+ :content_state => "Still Irie",
440
+ :content_state_id => "3" } ]
441
+ }
442
+ end
422
443
  end
423
444
  end
424
445
  end
425
446
 
426
- describe "options" do
427
- describe ":vendor_id" do
447
+ describe "options" do
448
+ describe ":vendor_id" do
428
449
  it_should_behave_like "a vendor_id option"
429
450
  end
430
451
  end
@@ -448,9 +469,9 @@ describe ITunes::Store::Transporter::Command::Verify do
448
469
  end
449
470
  end
450
471
 
451
- # If no packages were verfied it exits with 0 but emits an error message
452
472
  context "with errors" do
453
473
  it "raises an ExecutionError" do
474
+ # If no packages were verfied it exits with 0 but emits an error message
454
475
  mock_output(:exit => 0, :stderr => "stderr.errors");
455
476
  lambda { subject.run(options) }.should raise_exception(ITunes::Store::Transporter::ExecutionError)
456
477
  end
@@ -460,7 +481,19 @@ describe ITunes::Store::Transporter::Command::Verify do
460
481
 
461
482
  describe "options" do
462
483
  describe ":verify_assets" do
463
- it_should_behave_like "a boolean transporter option", :verify_assets, "-disableAssetVerification"
484
+ context "when true" do
485
+ it "does not create the command line argument" do
486
+ ITunes::Store::Transporter::Shell.any_instance.should_receive(:exec) { |*arg| arg.first.should_not include("-disableAssetVerification"); 0 }
487
+ subject.run(options.merge(:verify_assets => true))
488
+ end
489
+ end
490
+
491
+ context "when false" do
492
+ it "creates the command line argument" do
493
+ expect_shell_args("-disableAssetVerification")
494
+ subject.run(options.merge(:verify_assets => false))
495
+ end
496
+ end
464
497
  end
465
498
  end
466
499
  end