metaforce 1.0.0a → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -6,3 +6,4 @@ examples/tmp
6
6
  deploy.zip
7
7
  retrieve.zip
8
8
  test.rb
9
+ .metaforce.yml
data/README.md CHANGED
@@ -125,6 +125,40 @@ client.send_email(
125
125
  )
126
126
  ```
127
127
 
128
+ ## Command-line
129
+
130
+ Metaforce also comes with a handy command line utility that can deploy and retrieve
131
+ code from Salesforce. It also allows you to watch a directory and deploy when
132
+ anything changes.
133
+
134
+ When you deploy, it will also run all tests and provide you with a report,
135
+ similar to rspec.
136
+
137
+ ```bash
138
+ $ metaforce deploy ./src
139
+ Deploying: ./src
140
+ ```
141
+
142
+ ```bash
143
+ $ metaforce watch ./src
144
+ Watching: ./src
145
+ ```
146
+
147
+ ```bash
148
+ $ metaforce retrieve ./src
149
+ Retrieving: ./src/package.xml
150
+
151
+ $ metaforce retrieve ./src/package.xml ./other-location
152
+ Retrieving: ./src/package.xml
153
+ ```
154
+
155
+ ### .metaforce.yml
156
+
157
+ The metaforce command will pull it's configuration from a `.metaforce.yml`
158
+ file, if present. You can provide options for multiple environments, then use
159
+ the `-e` swtich on the command line to use an environment. See the
160
+ [examples](examples) directory for an example.
161
+
128
162
  ## Contributing
129
163
 
130
164
  1. Fork it
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+ require 'metaforce'
5
+
6
+ begin
7
+ require 'metaforce/cli'
8
+ Metaforce::CLI.start
9
+ rescue Interrupt => e
10
+ puts "\nQuitting..."
11
+ exit 1
12
+ rescue SystemExit => e
13
+ exit e.status
14
+ end
@@ -11,18 +11,12 @@ client = Metaforce.new :username => ENV['USER'],
11
11
  Metaforce::Job.disable_threading!
12
12
 
13
13
  # Test sending an email.
14
- print 'send email to: '
14
+ print 'send email to: '; email = STDIN.gets.chomp
15
15
  client.send_email(
16
- to_addresses: [STDIN.gets.chomp],
16
+ to_addresses: [email],
17
17
  subject: 'Test',
18
18
  plain_text_body: 'Test'
19
- )
20
-
21
- # Test deployment.
22
- client.deploy('../spec/fixtures/payload.zip')
23
- .on_complete { |job| puts "Deploy Completed: #{job.id}."}
24
- .on_error { |job| puts "Deploy Failed: #{job.id}."}
25
- .perform
19
+ ) if email
26
20
 
27
21
  # Test retrieve.
28
22
  manifest = Metaforce::Manifest.new(:custom_object => ['Account'])
@@ -32,6 +26,13 @@ client.retrieve_unpackaged(manifest)
32
26
  .extract_to('./tmp')
33
27
  .perform
34
28
 
29
+ # Test deployment.
30
+ client.deploy('./tmp')
31
+ .on_complete { |job| puts "Deploy Completed: #{job.id}. #{job.result}"}
32
+ .on_error { |job| puts "Deploy Failed: #{job.id}."}
33
+ .on_poll { |job| puts "Deploy: Polled status for #{job.id}."}
34
+ .perform
35
+
35
36
  # Test delete.
36
37
  client.delete(:apex_page, 'TestPage')
37
38
  .on_complete { |job| puts "Delete Completed: #{job.id}."}
@@ -0,0 +1,124 @@
1
+ require 'listen'
2
+ require 'thor'
3
+ require 'metaforce/reporters'
4
+ require 'metaforce/reporters'
5
+
6
+ Metaforce.configuration.log = false
7
+ Metaforce::Job.disable_threading!
8
+
9
+ module Metaforce
10
+ class CLI < Thor
11
+ CONFIG_FILE = '.metaforce.yml'
12
+
13
+ include Thor::Actions
14
+
15
+ class << self
16
+ def credential_options
17
+ method_option :username, :aliases => '-u', :desc => 'Username.'
18
+ method_option :password, :aliases => '-p', :desc => 'Password.'
19
+ method_option :security_token, :aliases => '-t', :desc => 'Security Token.'
20
+ method_option :environment, :aliases => '-e', :default => 'production', :desc => 'Environment to use from config file (if present).'
21
+ method_option :host, :aliases => '-h', :desc => 'Salesforce host to connect to.'
22
+ end
23
+
24
+ def deploy_options
25
+ method_option :deploy_options, :aliases => '-o', :type => :hash, :default => { :run_all_tests => true }, :desc => 'Deploy Options'
26
+ end
27
+
28
+ def retrieve_options
29
+ method_option :retrieve_options, :aliases => '-o', :type => :hash, :default => {}, :desc => 'Retrieve Options'
30
+ end
31
+ end
32
+
33
+ desc 'deploy <path>', 'Deploy <path> to the target organization.'
34
+
35
+ credential_options
36
+ deploy_options
37
+
38
+ def deploy(path)
39
+ say "Deploying: #{path} ", :cyan
40
+ say "#{options[:deploy_options].inspect}"
41
+ client(options).deploy(path, options[:deploy_options].symbolize_keys!)
42
+ .on_complete { |job| report job.result, :deploy }
43
+ .on_error(&error)
44
+ .on_poll(&polling)
45
+ .perform
46
+ end
47
+
48
+ desc 'retrieve <manifest> <path>', 'Retrieve the components specified in <manifest> (package.xml) to <path>.'
49
+
50
+ credential_options
51
+ retrieve_options
52
+
53
+ def retrieve(manifest, path=nil)
54
+ unless path
55
+ path = manifest
56
+ manifest = File.join(path, 'package.xml')
57
+ end
58
+ say "Retrieving: #{manifest} ", :cyan
59
+ say "#{options[:retrieve_options].inspect}"
60
+ client(options).retrieve_unpackaged(manifest, options[:retrieve_options].symbolize_keys!)
61
+ .extract_to(path)
62
+ .on_complete { |job| report(job.result, :retrieve) }
63
+ .on_complete { |job| say "Extracted: #{path}", :green }
64
+ .on_error(&error)
65
+ .on_poll(&polling)
66
+ .perform
67
+ end
68
+
69
+ desc 'watch <path>', 'Deploy <path> to the target organization whenever files are changed.'
70
+
71
+ credential_options
72
+ deploy_options
73
+
74
+ def watch(path)
75
+ say "Watching: #{path}"
76
+ @watching = true
77
+ Listen.to(path) { deploy path }
78
+ end
79
+
80
+ private
81
+
82
+ def report(results, type)
83
+ reporter = "Metaforce::Reporters::#{type.to_s.camelize}Reporter".constantize.new(results)
84
+ reporter.report
85
+ exit 1 if reporter.issues?
86
+ end
87
+
88
+ def exit(status)
89
+ super(status) if not watching?
90
+ end
91
+
92
+ def watching?
93
+ !!@watching
94
+ end
95
+
96
+ def error
97
+ proc { |job| say "Error: #{job.result.inspect}" }
98
+ end
99
+
100
+ def polling
101
+ proc { |job| say 'Polling ...', :cyan }
102
+ end
103
+
104
+ def client(options)
105
+ credentials = Thor::CoreExt::HashWithIndifferentAccess.new(config ? config[options[:environment]] : {})
106
+ credentials.merge!(options.slice(:username, :password, :security_token, :host))
107
+ credentials.tap do |credentials|
108
+ credentials[:username] ||= ask('username:')
109
+ credentials[:password] ||= ask('password:')
110
+ credentials[:security_token] ||= ask('security token:')
111
+ end
112
+ Metaforce.new credentials
113
+ end
114
+
115
+ def config
116
+ YAML.load(File.open(config_file)) if File.exists?(config_file)
117
+ end
118
+
119
+ def config_file
120
+ File.expand_path(CONFIG_FILE)
121
+ end
122
+
123
+ end
124
+ end
@@ -3,6 +3,9 @@ require 'base64'
3
3
 
4
4
  module Metaforce
5
5
  class Job
6
+ DELAY_START = 1
7
+ DELAY_MULTIPLIER = 2
8
+
6
9
  autoload :Deploy, 'metaforce/job/deploy'
7
10
  autoload :Retrieve, 'metaforce/job/retrieve'
8
11
  autoload :CRUD, 'metaforce/job/crud'
@@ -39,11 +42,18 @@ module Metaforce
39
42
  self
40
43
  end
41
44
 
42
- # Public: Register a block to be called when the job has completed.
45
+ # Public: Utility method to determine if .perform has been called yet.
46
+ #
47
+ # Returns true if @id is set, false otherwise.
48
+ def started?
49
+ !!@id
50
+ end
51
+
52
+ # Public: Register a block to be called when an event occurs.
43
53
  #
44
54
  # Yields the job.
45
55
  #
46
- # &block - Proc or Lambda to be run when the job completes.
56
+ # &block - Proc or Lambda to be run when the event is triggered.
47
57
  #
48
58
  # Examples
49
59
  #
@@ -51,28 +61,26 @@ module Metaforce
51
61
  # puts "Job ##{job.id} completed!"
52
62
  # end
53
63
  #
54
- # Returns self.
55
- def on_complete(&block)
56
- @_callbacks[:on_complete] << block
57
- self
58
- end
59
-
60
- # Public: Register a block to be called when if the job fails.
61
- #
62
- # Yields the job.
63
- #
64
- # &block - Proc or Lambda to be run when the job fails.
65
- #
66
- # Examples
67
- #
68
64
  # job.on_error do |job|
69
- # puts "Job ##{job.id} failed!"
65
+ # puts "Job failed!"
66
+ # end
67
+ #
68
+ # job.on_poll do |job|
69
+ # puts "Polled status for #{job.id}"
70
70
  # end
71
71
  #
72
72
  # Returns self.
73
- def on_error(&block)
74
- @_callbacks[:on_error] << block
75
- self
73
+ #
74
+ # Signature
75
+ #
76
+ # on_complete(&block)
77
+ # on_error(&block)
78
+ # on_poll(&block)
79
+ %w[complete error poll].each do |type|
80
+ define_method :"on_#{type}" do |&block|
81
+ @_callbacks[:"on_#{type}"] << block
82
+ self
83
+ end
76
84
  end
77
85
 
78
86
  # Public: Queries the job status from the API.
@@ -84,7 +92,7 @@ module Metaforce
84
92
  #
85
93
  # Returns the AsyncResult (http://www.salesforce.com/us/developer/docs/api_meta/Content/meta_asyncresult.htm).
86
94
  def status
87
- client.status(id)
95
+ @status ||= client.status(id)
88
96
  end
89
97
 
90
98
  # Public: Returns true if the job has completed.
@@ -106,9 +114,9 @@ module Metaforce
106
114
  # job.state
107
115
  # # => 'Completed'
108
116
  #
109
- # Returns the state if the job is done, false otherwise.
117
+ # Returns the state of the job.
110
118
  def state
111
- done? && status.state
119
+ status.state
112
120
  end
113
121
 
114
122
  # Public: Check if the job is in a given state.
@@ -140,7 +148,11 @@ module Metaforce
140
148
  def disable_threading!
141
149
  self.class_eval do
142
150
  def start_heart_beat
151
+ delay = DELAY_START
143
152
  loop do
153
+ @status = nil
154
+ wait (delay = delay * DELAY_MULTIPLIER)
155
+ trigger_poll_callbacks
144
156
  trigger_callbacks && break if completed? || error?
145
157
  end
146
158
  end
@@ -157,14 +169,26 @@ module Metaforce
157
169
  def start_heart_beat
158
170
  Thread.abort_on_exception = true
159
171
  @heart_beat ||= Thread.new do
160
- delay = 1
172
+ delay = DELAY_START
161
173
  loop do
162
- sleep (delay = delay * 2)
174
+ @status = nil
175
+ wait (delay = delay * DELAY_MULTIPLIER)
176
+ trigger_poll_callbacks
163
177
  trigger_callbacks && Thread.stop if completed? || error?
164
178
  end
165
179
  end
166
180
  end
167
181
 
182
+ def trigger_poll_callbacks
183
+ @_callbacks[:on_poll].each do |block|
184
+ block.call(self)
185
+ end
186
+ end
187
+
188
+ def wait(duration)
189
+ sleep duration
190
+ end
191
+
168
192
  def trigger_callbacks
169
193
  @_callbacks[callback_type].each do |block|
170
194
  block.call(self)
@@ -37,7 +37,7 @@ module Metaforce
37
37
  #
38
38
  # Returns the DeployResult (http://www.salesforce.com/us/developer/docs/api_meta/Content/meta_deployresult.htm).
39
39
  def result
40
- client.status(id, :deploy)
40
+ @result ||= client.status(id, :deploy)
41
41
  end
42
42
 
43
43
  # Public: Returns true if the deploy was successful.
@@ -52,7 +52,9 @@ module Metaforce
52
52
  result.success
53
53
  end
54
54
 
55
- # Public: Base64 encodes the contents of the zip file.
55
+ private
56
+
57
+ # Internal: Base64 encodes the contents of the zip file.
56
58
  #
57
59
  # Examples
58
60
  #
@@ -64,8 +66,6 @@ module Metaforce
64
66
  Base64.encode64(File.open(file, 'rb').read)
65
67
  end
66
68
 
67
- private
68
-
69
69
  # Internal: Returns the path to the zip file.
70
70
  def file
71
71
  File.file?(@path) ? @path : zip_file
@@ -37,7 +37,7 @@ module Metaforce
37
37
  #
38
38
  # Returns the RetrieveResult (http://www.salesforce.com/us/developer/docs/api_meta/Content/meta_retrieveresult.htm).
39
39
  def result
40
- client.status(id, :retrieve)
40
+ @result ||= client.status(id, :retrieve)
41
41
  end
42
42
 
43
43
  # Public: Decodes the content of the returned zip file.
@@ -63,7 +63,7 @@ module Metaforce
63
63
  #
64
64
  # Returns self.
65
65
  def extract_to(destination)
66
- return on_complete { |job| job.extract_to(destination) } unless @id
66
+ return on_complete { |job| job.extract_to(destination) } unless started?
67
67
  Zip::ZipFile.open(tmp_zip_file) do |zip|
68
68
  zip.each do |f|
69
69
  path = File.join(destination, f.name)
@@ -36,7 +36,7 @@ module Metaforce
36
36
  #
37
37
  # Examples
38
38
  #
39
- # client._update(:apex_page, 'OldPage', :full_name => 'NewPage')
39
+ # client._update(:apex_page, 'OldPage', :full_name => 'TestPage', :label => 'Test page', :content => '<apex:page>hello world</apex:page>')
40
40
  def _update(type, current_name, metadata={})
41
41
  type = type.to_s.camelize
42
42
  request :update do |soap|
@@ -70,7 +70,7 @@ module Metaforce
70
70
 
71
71
  # Internal: Prepare metadata by base64 encoding any content keys.
72
72
  def prepare(metadata)
73
- metadata = [metadata] unless metadata.is_a? Array
73
+ metadata = Array[metadata].compact.flatten
74
74
  metadata.each { |m| encode_content(m) }
75
75
  metadata
76
76
  end
@@ -97,7 +97,7 @@ module Metaforce
97
97
  package = if manifest.is_a?(Metaforce::Manifest)
98
98
  manifest
99
99
  elsif manifest.is_a?(String)
100
- Metaforce::Manifest.new(File.open(manifest).read)
100
+ Metaforce::Manifest.new(::File.open(manifest).read)
101
101
  end
102
102
  options = {
103
103
  :api_version => Metaforce.configuration.api_version,
@@ -0,0 +1,2 @@
1
+ require 'metaforce/reporters/deploy_reporter'
2
+ require 'metaforce/reporters/retrieve_reporter'
@@ -0,0 +1,56 @@
1
+ require 'thor/shell/color'
2
+
3
+ module Metaforce
4
+ module Reporters
5
+ class BaseReporter < Thor::Shell::Color
6
+ def initialize(results)
7
+ super()
8
+ @results = results
9
+ end
10
+
11
+ def report
12
+ end
13
+
14
+ def report_problems
15
+ return unless problems?
16
+ say
17
+ say "Problems:", :red
18
+ say
19
+ problems.each { |message| problem(message) }
20
+ end
21
+
22
+ def problem(message)
23
+ say "#{short_padding}#{message.file_name}:#{message.line_number}", :red
24
+ say "#{long_padding}#{message.problem}"
25
+ say
26
+ end
27
+
28
+ def short_padding
29
+ ' '
30
+ end
31
+
32
+ def long_padding
33
+ ' '
34
+ end
35
+
36
+ def problems?
37
+ problems.any?
38
+ end
39
+
40
+ def issues?
41
+ problems?
42
+ end
43
+
44
+ private
45
+
46
+ def messages
47
+ @messages ||= Array[@results.messages].compact.flatten
48
+ end
49
+
50
+ def problems
51
+ messages.select { |message| message.problem }
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,69 @@
1
+ require 'metaforce/reporters/base_reporter'
2
+
3
+ module Metaforce
4
+ module Reporters
5
+ class DeployReporter < BaseReporter
6
+
7
+ def report
8
+ report_problems
9
+ report_failed_tests
10
+ report_test_results if report_test_results?
11
+ end
12
+
13
+ def report_failed_tests
14
+ return unless failures?
15
+ say
16
+ say "Failures:", :red
17
+ say
18
+ failures.each { |failure| failed(failure) }
19
+ end
20
+
21
+ def report_test_results
22
+ say
23
+ say "Finished in #{total_time} seconds"
24
+ say "#{num_tests} tests, #{num_failures} failures", failures? ? :red : :green
25
+ end
26
+
27
+ def failed(failure)
28
+ say "#{short_padding}#{failure.stack_trace}:", :red
29
+ say "#{long_padding}#{failure.message}"
30
+ say
31
+ end
32
+
33
+ def failures?
34
+ num_failures > 0
35
+ end
36
+
37
+ def issues?
38
+ problems? || failures?
39
+ end
40
+
41
+ private
42
+
43
+ def report_test_results?
44
+ @results.success && problems.empty?
45
+ end
46
+
47
+ def test_results
48
+ @results.run_test_result
49
+ end
50
+
51
+ def failures
52
+ @failures ||= Array[test_results.failures].compact.flatten
53
+ end
54
+
55
+ def total_time
56
+ test_results.total_time
57
+ end
58
+
59
+ def num_tests
60
+ test_results.num_tests_run.to_i
61
+ end
62
+
63
+ def num_failures
64
+ test_results.num_failures.to_i
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,11 @@
1
+ require 'metaforce/reporters/base_reporter'
2
+
3
+ module Metaforce
4
+ module Reporters
5
+ class RetrieveReporter < BaseReporter
6
+ def report
7
+ report_problems
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Metaforce
2
- VERSION = '1.0.0a'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -24,7 +24,10 @@ EOL
24
24
  s.add_dependency 'savon', '~> 1.2.0'
25
25
  s.add_dependency 'rubyzip', '~> 0.9.9'
26
26
  s.add_dependency 'activesupport'
27
- s.add_dependency 'hashie'
27
+ s.add_dependency 'hashie', '~> 1.2.0'
28
+ s.add_dependency 'thor', '~> 0.16.0'
29
+ s.add_dependency 'listen', '~> 0.6.0'
30
+ s.add_dependency 'rb-fsevent', '~> 0.9.1'
28
31
 
29
32
  s.add_development_dependency 'rake'
30
33
  s.add_development_dependency 'rspec'
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+ require 'metaforce/cli'
3
+
4
+ describe Metaforce::CLI do
5
+ before do
6
+ Metaforce::Client.any_instance.stub(:deploy).and_return(double('deploy job').as_null_object)
7
+ Metaforce::Client.any_instance.stub(:retrieve).and_return(double('retrieve job').as_null_object)
8
+ subject.stub(:config).and_return(nil)
9
+ end
10
+
11
+ describe 'credentials' do
12
+ let(:output) { capture(:stdout) { subject.deploy('./path') } }
13
+
14
+ context 'when supplied credentials from the command line' do
15
+ let(:options) { indifferent_hash(:username => 'foo', :password => 'bar', :security_token => 'token', :deploy_options => {}) }
16
+
17
+ it 'uses supplied credentials from command line' do
18
+ subject.options = options
19
+ Metaforce.should_receive(:new).with(indifferent_hash(options).slice(:username, :password, :security_token)).and_call_original
20
+ output.should include('Deploying: ./path')
21
+ end
22
+ end
23
+
24
+ context 'when supplied credentials from config file' do
25
+ let(:options) { indifferent_hash(:environment => 'production', :deploy_options => {}) }
26
+ let(:config) { { 'username' => 'foo', 'password' => 'bar', 'security_token' => 'token' } }
27
+
28
+ it 'uses credentials from the config file' do
29
+ subject.options = options
30
+ subject.stub(:config).and_return('production' => config)
31
+ Metaforce.should_receive(:new).with(indifferent_hash(config)).and_call_original
32
+ output.should include('Deploying: ./path')
33
+ end
34
+ end
35
+ end
36
+
37
+ def indifferent_hash(hash)
38
+ Thor::CoreExt::HashWithIndifferentAccess.new(hash)
39
+ end
40
+ end
@@ -5,28 +5,29 @@ describe Metaforce::Job::Deploy do
5
5
  let(:path) { File.expand_path('../../../fixtures/payload.zip', __FILE__) }
6
6
  let(:job) { described_class.new client, path }
7
7
 
8
- describe '.payload' do
9
- subject { job.payload }
8
+ describe '.perform' do
9
+ subject { job.perform }
10
10
 
11
11
  context 'when the path is a file' do
12
- it { should be_a String }
12
+ before do
13
+ client.should_receive(:_deploy).with(/^UEsDBA.*/, {}).and_return(Hashie::Mash.new(id: '1234'))
14
+ client.should_receive(:status).any_number_of_times.and_return(Hashie::Mash.new(done: true, state: 'Completed'))
15
+ end
16
+
17
+ it { should eq job }
18
+ its(:id) { should eq '1234' }
13
19
  end
14
20
 
15
21
  context 'when the path is a directory' do
16
- let(:path) { File.expand_path('../../../fixtures', __FILE__) }
17
- it { should be_a String }
18
- end
19
- end
22
+ before do
23
+ client.should_receive(:_deploy).with(/.*1stwAAAJI.*/, {}).and_return(Hashie::Mash.new(id: '1234'))
24
+ client.should_receive(:status).any_number_of_times.and_return(Hashie::Mash.new(done: true, state: 'Completed'))
25
+ end
20
26
 
21
- describe '.perform' do
22
- before do
23
- client.should_receive(:_deploy).with(job.payload, {}).and_return(Hashie::Mash.new(id: '1234'))
24
- client.should_receive(:status).any_number_of_times.and_return(Hashie::Mash.new(done: true, state: 'Completed'))
27
+ let(:path) { File.expand_path('../../../fixtures', __FILE__) }
28
+ it { should eq job }
29
+ its(:id) { should eq '1234' }
25
30
  end
26
-
27
- subject { job.perform }
28
- it { should eq job }
29
- its(:id) { should eq '1234' }
30
31
  end
31
32
 
32
33
  describe '.result' do
@@ -11,6 +11,22 @@ describe Metaforce::Job do
11
11
  end
12
12
  end
13
13
 
14
+ describe '.started?' do
15
+ subject { job.started? }
16
+
17
+ context 'when .perform has been called and an @id has been set' do
18
+ before do
19
+ job.instance_variable_set(:@id, '1234')
20
+ end
21
+
22
+ it { should be_true }
23
+ end
24
+
25
+ context 'when .perform has not been called and no @id has been set' do
26
+ it { should be_false }
27
+ end
28
+ end
29
+
14
30
  describe '.on_complete' do
15
31
  it 'allows the user to register an on_complete callback' do
16
32
  client.should_receive(:status).any_number_of_times.and_return(Hashie::Mash.new(done: true, state: 'Completed'))
@@ -67,7 +83,7 @@ describe Metaforce::Job do
67
83
 
68
84
  context 'when done' do
69
85
  before do
70
- client.should_receive(:status).twice.and_return(Hashie::Mash.new(done: true, state: 'Completed'))
86
+ client.should_receive(:status).and_return(Hashie::Mash.new(done: true, state: 'Completed'))
71
87
  end
72
88
 
73
89
  it { should eq 'Completed' }
@@ -85,7 +101,7 @@ describe Metaforce::Job do
85
101
  %w[Queued InProgress Completed Error].each do |state|
86
102
  describe ".#{state.underscore}?" do
87
103
  before do
88
- client.should_receive(:status).twice.and_return(Hashie::Mash.new(done: true, state: state))
104
+ client.should_receive(:status).and_return(Hashie::Mash.new(done: true, state: state))
89
105
  end
90
106
 
91
107
  subject { job.send(:"#{state.underscore}?") }
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+ require 'metaforce/reporters/base_reporter'
3
+
4
+ describe Metaforce::Reporters::BaseReporter do
5
+ let(:results) { Hashie::Mash.new(success: true) }
6
+ let(:reporter) { described_class.new results }
7
+
8
+ describe '.problem' do
9
+ context 'when a line_number is present' do
10
+ let(:problem) { Hashie::Mash.new(file_name: 'path/file', line_number: '10', problem: 'Foobar') }
11
+
12
+ it 'prints the problem' do
13
+ reporter.should_receive(:say).with(' path/file:10', :red)
14
+ reporter.should_receive(:say).with(' Foobar')
15
+ reporter.should_receive(:say).with
16
+ reporter.problem(problem)
17
+ end
18
+ end
19
+
20
+ context 'when there is no line number present' do
21
+ let(:problem) { Hashie::Mash.new(file_name: 'path/file', problem: 'Foobar') }
22
+
23
+ it 'prints the problem' do
24
+ reporter.should_receive(:say).with(' path/file:', :red)
25
+ reporter.should_receive(:say).with(' Foobar')
26
+ reporter.should_receive(:say).with
27
+ reporter.problem(problem)
28
+ end
29
+ end
30
+ end
31
+
32
+ describe '.report_problems' do
33
+ context 'when there are no problems' do
34
+ it 'does not report any problems' do
35
+ reporter.should_receive(:say).never
36
+ reporter.should_receive(:problem).never
37
+ reporter.report_problems
38
+ end
39
+ end
40
+
41
+ context 'when there are problems' do
42
+ let(:results) { Hashie::Mash.new(success: true, messages: { problem: 'Problem', file_name: 'path/file', line_number: '10' }) }
43
+
44
+ it 'prints each problem' do
45
+ reporter.should_receive(:say)
46
+ reporter.should_receive(:say).with('Problems:', :red)
47
+ reporter.should_receive(:say)
48
+ reporter.should_receive(:problem)
49
+ reporter.report_problems
50
+ end
51
+ end
52
+ end
53
+
54
+ describe '.problems?' do
55
+ subject { reporter.problems? }
56
+
57
+ context 'when there are no problems' do
58
+ it { should be_false }
59
+ end
60
+
61
+ context 'when there are problems' do
62
+ let(:results) { Hashie::Mash.new(success: true, messages: { problem: 'Problem', file_name: 'path/file', line_number: '10' }) }
63
+ it { should be_true }
64
+ end
65
+ end
66
+
67
+ describe '.issues?' do
68
+ subject { reporter.issues? }
69
+
70
+ context 'when there are no problems' do
71
+ it { should be_false }
72
+ end
73
+
74
+ context 'when there are problems' do
75
+ let(:results) { Hashie::Mash.new(success: true, messages: { problem: 'Problem', file_name: 'path/file', line_number: '10' }) }
76
+ it { should be_true }
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,124 @@
1
+ require 'spec_helper'
2
+ require 'metaforce/reporters/deploy_reporter'
3
+
4
+ describe Metaforce::Reporters::DeployReporter do
5
+ let(:results) { Hashie::Mash.new(success: true) }
6
+ let(:reporter) { described_class.new results }
7
+
8
+ describe '.failed' do
9
+ let(:failure) { Hashie::Mash.new(stack_trace: 'stack trace', message: 'message') }
10
+
11
+ it 'prints the failure' do
12
+ reporter.should_receive(:say).with(' stack trace:', :red)
13
+ reporter.should_receive(:say).with(' message')
14
+ reporter.should_receive(:say).with
15
+ reporter.failed(failure)
16
+ end
17
+ end
18
+
19
+ describe '.report' do
20
+ context 'when the deploy was successfull' do
21
+ it 'should report everything' do
22
+ reporter.should_receive(:report_problems)
23
+ reporter.should_receive(:report_failed_tests)
24
+ reporter.should_receive(:report_test_results)
25
+ reporter.report
26
+ end
27
+ end
28
+
29
+ context 'when the deploy was not successful' do
30
+ let(:results) { Hashie::Mash.new(success: false) }
31
+
32
+ it 'should report everything except test results' do
33
+ reporter.should_receive(:report_problems)
34
+ reporter.should_receive(:report_failed_tests)
35
+ reporter.should_receive(:report_test_results).never
36
+ reporter.report
37
+ end
38
+ end
39
+ end
40
+
41
+ describe '.report_failed_tests' do
42
+ let(:results) { Hashie::Mash.new(success: true, run_test_result: { num_failures: num_failures, failures: failures }) }
43
+
44
+ context 'when there are no failed tests' do
45
+ let(:failures) { nil }
46
+ let(:num_failures) { '0' }
47
+
48
+ it 'does not report any failed tests' do
49
+ reporter.should_receive(:say).never
50
+ reporter.should_receive(:failed).never
51
+ reporter.report_failed_tests
52
+ end
53
+ end
54
+
55
+ context 'when there are failed tests' do
56
+ context 'passed as an object' do
57
+ let(:failures) { { stack_trace: 'stack trace', message: 'message' } }
58
+ let(:num_failures) { '1' }
59
+
60
+ it 'prints each failed tests' do
61
+ reporter.should_receive(:say)
62
+ reporter.should_receive(:say).with('Failures:', :red)
63
+ reporter.should_receive(:say)
64
+ reporter.should_receive(:failed)
65
+ reporter.report_failed_tests
66
+ end
67
+ end
68
+
69
+ context 'passed as an array' do
70
+ let(:failures) { [{ stack_trace: 'stack trace', message: 'message' }, { stack_trace: 'stack trace 2', message: 'message 2' }] }
71
+ let(:num_failures) { '2' }
72
+
73
+ it 'prints each failed tests' do
74
+ reporter.should_receive(:say)
75
+ reporter.should_receive(:say).with('Failures:', :red)
76
+ reporter.should_receive(:say)
77
+ reporter.should_receive(:failed).twice
78
+ reporter.report_failed_tests
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ describe '.report_test_results' do
85
+ let(:results) { Hashie::Mash.new(success: true, run_test_result: { total_time: '20', num_tests_run: '10', num_failures: num_failures }) }
86
+
87
+ context 'when there are failures' do
88
+ let(:num_failures) { '5' }
89
+
90
+ it 'prints the test results in red' do
91
+ reporter.should_receive(:say)
92
+ reporter.should_receive(:say).with("Finished in 20 seconds")
93
+ reporter.should_receive(:say).with("10 tests, 5 failures", :red)
94
+ reporter.report_test_results
95
+ end
96
+ end
97
+
98
+ context 'when there are no failures' do
99
+ let(:num_failures) { '0' }
100
+
101
+ it 'prints the test results in red' do
102
+ reporter.should_receive(:say)
103
+ reporter.should_receive(:say).with("Finished in 20 seconds")
104
+ reporter.should_receive(:say).with("10 tests, 0 failures", :green)
105
+ reporter.report_test_results
106
+ end
107
+ end
108
+ end
109
+
110
+ describe '.failures?' do
111
+ let(:results) { Hashie::Mash.new(success: true, run_test_result: { total_time: '20', num_tests_run: '10', num_failures: num_failures }) }
112
+ subject { reporter.failures? }
113
+
114
+ context 'when there are failures' do
115
+ let(:num_failures) { '5' }
116
+ it { should be_true }
117
+ end
118
+
119
+ context 'when there are no failures' do
120
+ let(:num_failures) { '0' }
121
+ it { should be_false }
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+ require 'metaforce/reporters/retrieve_reporter'
3
+
4
+ describe Metaforce::Reporters::RetrieveReporter do
5
+ let(:results) { Hashie::Mash.new(success: true) }
6
+ let(:reporter) { described_class.new results }
7
+
8
+ describe '.report' do
9
+ it 'reports the problems' do
10
+ reporter.should_receive(:report_problems)
11
+ reporter.report
12
+ end
13
+ end
14
+ end
@@ -1,14 +1,17 @@
1
1
  require 'bundler'
2
2
  Bundler.require :default, :development
3
3
  require 'pp'
4
+ require 'rspec/mocks'
4
5
 
5
6
  Dir['./spec/support/**/*.rb'].sort.each {|f| require f}
6
7
 
7
8
  RSpec.configure do |config|
9
+ config.mock_with :rspec
8
10
  config.include Savon::Spec::Macros
9
11
 
10
- config.before(:suite) do
12
+ config.before(:each) do
11
13
  Metaforce::Job.disable_threading!
14
+ Metaforce::Job.any_instance.stub(:wait)
12
15
  end
13
16
  end
14
17
 
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metaforce
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0a
5
- prerelease: 5
4
+ version: 1.0.0
5
+ prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Eric J. Holmes
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-12-21 00:00:00.000000000 Z
13
+ date: 2012-12-23 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: savon
@@ -65,17 +65,65 @@ dependencies:
65
65
  requirement: !ruby/object:Gem::Requirement
66
66
  none: false
67
67
  requirements:
68
- - - ! '>='
68
+ - - ~>
69
69
  - !ruby/object:Gem::Version
70
- version: '0'
70
+ version: 1.2.0
71
71
  type: :runtime
72
72
  prerelease: false
73
73
  version_requirements: !ruby/object:Gem::Requirement
74
74
  none: false
75
75
  requirements:
76
- - - ! '>='
76
+ - - ~>
77
77
  - !ruby/object:Gem::Version
78
- version: '0'
78
+ version: 1.2.0
79
+ - !ruby/object:Gem::Dependency
80
+ name: thor
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ~>
85
+ - !ruby/object:Gem::Version
86
+ version: 0.16.0
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ~>
93
+ - !ruby/object:Gem::Version
94
+ version: 0.16.0
95
+ - !ruby/object:Gem::Dependency
96
+ name: listen
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ~>
101
+ - !ruby/object:Gem::Version
102
+ version: 0.6.0
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: 0.6.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: rb-fsevent
113
+ requirement: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ~>
117
+ - !ruby/object:Gem::Version
118
+ version: 0.9.1
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ~>
125
+ - !ruby/object:Gem::Version
126
+ version: 0.9.1
79
127
  - !ruby/object:Gem::Dependency
80
128
  name: rake
81
129
  requirement: !ruby/object:Gem::Requirement
@@ -144,7 +192,8 @@ description: A Ruby gem for interacting with the Salesforce Metadata API
144
192
  email:
145
193
  - eric@ejholmes.net
146
194
  - Kevinp@madronasg.com
147
- executables: []
195
+ executables:
196
+ - metaforce
148
197
  extensions: []
149
198
  extra_rdoc_files: []
150
199
  files:
@@ -155,9 +204,12 @@ files:
155
204
  - LICENSE
156
205
  - README.md
157
206
  - Rakefile
207
+ - bin/metaforce
208
+ - examples/.metaforce.yml
158
209
  - examples/example.rb
159
210
  - lib/metaforce.rb
160
211
  - lib/metaforce/abstract_client.rb
212
+ - lib/metaforce/cli.rb
161
213
  - lib/metaforce/client.rb
162
214
  - lib/metaforce/config.rb
163
215
  - lib/metaforce/job.rb
@@ -169,6 +221,10 @@ files:
169
221
  - lib/metaforce/metadata/client.rb
170
222
  - lib/metaforce/metadata/client/crud.rb
171
223
  - lib/metaforce/metadata/client/file.rb
224
+ - lib/metaforce/reporters.rb
225
+ - lib/metaforce/reporters/base_reporter.rb
226
+ - lib/metaforce/reporters/deploy_reporter.rb
227
+ - lib/metaforce/reporters/retrieve_reporter.rb
172
228
  - lib/metaforce/services/client.rb
173
229
  - lib/metaforce/version.rb
174
230
  - metaforce.gemspec
@@ -192,6 +248,7 @@ files:
192
248
  - spec/fixtures/requests/retrieve/in_progress.xml
193
249
  - spec/fixtures/requests/send_email/success.xml
194
250
  - spec/fixtures/requests/update/in_progress.xml
251
+ - spec/lib/cli_spec.rb
195
252
  - spec/lib/client_spec.rb
196
253
  - spec/lib/config_spec.rb
197
254
  - spec/lib/job/deploy_spec.rb
@@ -201,6 +258,9 @@ files:
201
258
  - spec/lib/manifest_spec.rb
202
259
  - spec/lib/metadata/client_spec.rb
203
260
  - spec/lib/metaforce_spec.rb
261
+ - spec/lib/reporters/base_reporter_spec.rb
262
+ - spec/lib/reporters/deploy_reporter_spec.rb
263
+ - spec/lib/reporters/retrieve_reporter_spec.rb
204
264
  - spec/lib/services/client_spec.rb
205
265
  - spec/spec_helper.rb
206
266
  - spec/support/client.rb
@@ -223,15 +283,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
223
283
  - - ! '>='
224
284
  - !ruby/object:Gem::Version
225
285
  version: '0'
226
- segments:
227
- - 0
228
- hash: -4477921491118908816
229
286
  required_rubygems_version: !ruby/object:Gem::Requirement
230
287
  none: false
231
288
  requirements:
232
- - - ! '>'
289
+ - - ! '>='
233
290
  - !ruby/object:Gem::Version
234
- version: 1.3.1
291
+ version: '0'
235
292
  requirements: []
236
293
  rubyforge_project: metaforce
237
294
  rubygems_version: 1.8.23
@@ -259,6 +316,7 @@ test_files:
259
316
  - spec/fixtures/requests/retrieve/in_progress.xml
260
317
  - spec/fixtures/requests/send_email/success.xml
261
318
  - spec/fixtures/requests/update/in_progress.xml
319
+ - spec/lib/cli_spec.rb
262
320
  - spec/lib/client_spec.rb
263
321
  - spec/lib/config_spec.rb
264
322
  - spec/lib/job/deploy_spec.rb
@@ -268,6 +326,10 @@ test_files:
268
326
  - spec/lib/manifest_spec.rb
269
327
  - spec/lib/metadata/client_spec.rb
270
328
  - spec/lib/metaforce_spec.rb
329
+ - spec/lib/reporters/base_reporter_spec.rb
330
+ - spec/lib/reporters/deploy_reporter_spec.rb
331
+ - spec/lib/reporters/retrieve_reporter_spec.rb
271
332
  - spec/lib/services/client_spec.rb
272
333
  - spec/spec_helper.rb
273
334
  - spec/support/client.rb
335
+ has_rdoc: