itunes_store_transporter 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Changes +12 -0
- data/README.rdoc +18 -11
- data/bin/itms +72 -45
- data/lib/itunes/store/transporter.rb +0 -1
- data/lib/itunes/store/transporter/command.rb +21 -22
- data/lib/itunes/store/transporter/command/lookup.rb +4 -3
- data/lib/itunes/store/transporter/command/status.rb +23 -9
- data/lib/itunes/store/transporter/command/verify.rb +8 -3
- data/lib/itunes/store/transporter/output_parser.rb +53 -54
- data/lib/itunes/store/transporter/shell.rb +5 -1
- data/lib/itunes/store/transporter/version.rb +1 -1
- data/spec/command_spec.rb +72 -39
- data/spec/fixtures/errors_and_warnings.yml +45 -0
- data/spec/fixtures/providers.yml +5 -0
- data/spec/fixtures/status.yml +29 -0
- data/spec/fixtures/stderr.yml +20 -0
- data/spec/fixtures/stdout.yml +5 -0
- data/spec/shell_spec.rb +17 -10
- data/spec/spec_helper.rb +14 -15
- metadata +27 -12
@@ -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
|
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
|
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.
|
21
|
-
next unless line =~ /\
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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 =
|
31
|
+
parser = OutputParser.new(stderr_lines)
|
26
32
|
if parser.errors.any?
|
27
|
-
raise
|
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
|
-
|
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
|
-
|
14
|
-
|
13
|
+
attr :errors
|
14
|
+
attr :warnings
|
15
15
|
|
16
|
-
|
17
|
-
|
16
|
+
ERROR_LINE = />\s+ERROR:\s+(.+)/
|
17
|
+
WARNING_LINE = />\s+WARN:\s+(.+)/
|
18
18
|
|
19
|
-
|
20
|
-
|
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
|
-
|
26
|
+
/\bunable to authenticate the package/i ]
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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.
|
54
|
-
|
55
|
-
|
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
|
-
|
57
|
+
end
|
59
58
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
74
|
+
message.gsub! /^"/, ""
|
76
75
|
message.gsub! /"(?: at .+)?$/, ""
|
77
76
|
|
78
|
-
|
79
|
-
|
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
|
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?
|
data/spec/command_spec.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
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
|
-
|
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
|