thrust 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/bin/thrust +2 -9
  3. data/lib/config/{ios_example.yml → thrust_example.yml} +26 -19
  4. data/lib/tasks/autotag.rake +2 -2
  5. data/lib/tasks/cedar.rake +8 -11
  6. data/lib/tasks/testflight.rake +5 -14
  7. data/lib/tasks/version.rake +3 -7
  8. data/lib/thrust.rb +11 -13
  9. data/lib/thrust/agv_tool.rb +14 -0
  10. data/lib/thrust/app_config.rb +24 -10
  11. data/lib/thrust/cedar_results_parser.rb +7 -0
  12. data/lib/thrust/config_loader.rb +48 -0
  13. data/lib/thrust/deploy.rb +53 -0
  14. data/lib/thrust/deploy_provider.rb +17 -0
  15. data/lib/thrust/deployment_target.rb +6 -6
  16. data/lib/thrust/execution_helper.rb +1 -1
  17. data/lib/thrust/executor.rb +2 -1
  18. data/lib/thrust/ios_spec_launcher.rb +36 -0
  19. data/lib/thrust/osx_spec_launcher.rb +15 -0
  20. data/lib/thrust/scheme_parser.rb +35 -0
  21. data/lib/thrust/{ios_spec_target.rb → spec_target.rb} +1 -3
  22. data/lib/thrust/tasks.rb +1 -1
  23. data/lib/thrust/tasks/autotag/list.rb +2 -2
  24. data/lib/thrust/tasks/clean.rb +5 -5
  25. data/lib/thrust/tasks/focused_specs.rb +14 -4
  26. data/lib/thrust/tasks/nof.rb +8 -3
  27. data/lib/thrust/tasks/spec_runner.rb +75 -0
  28. data/lib/thrust/xcode_tools.rb +159 -0
  29. data/lib/thrust/xcode_tools_provider.rb +11 -0
  30. metadata +36 -23
  31. data/lib/config/android_example.yml +0 -17
  32. data/lib/thrust/android/deploy.rb +0 -38
  33. data/lib/thrust/android/deploy_provider.rb +0 -16
  34. data/lib/thrust/android/tools.rb +0 -43
  35. data/lib/thrust/config.rb +0 -43
  36. data/lib/thrust/ios/agv_tool.rb +0 -16
  37. data/lib/thrust/ios/cedar.rb +0 -27
  38. data/lib/thrust/ios/deploy.rb +0 -55
  39. data/lib/thrust/ios/deploy_provider.rb +0 -19
  40. data/lib/thrust/ios/x_code_tools.rb +0 -154
  41. data/lib/thrust/ios/x_code_tools_provider.rb +0 -13
  42. data/lib/thrust/tasks/ios_specs.rb +0 -43
@@ -1,9 +1,9 @@
1
1
  module Thrust
2
2
  class DeploymentTarget
3
3
  attr_reader :distribution_list,
4
- :ios_build_configuration,
5
- :ios_provisioning_search_query,
6
- :ios_target,
4
+ :build_configuration,
5
+ :provisioning_search_query,
6
+ :target,
7
7
  :note_generation_method,
8
8
  :notify,
9
9
  :versioning_method,
@@ -11,9 +11,9 @@ module Thrust
11
11
 
12
12
  def initialize(attributes)
13
13
  @distribution_list = attributes['distribution_list']
14
- @ios_build_configuration = attributes['ios_build_configuration']
15
- @ios_provisioning_search_query = attributes['ios_provisioning_search_query']
16
- @ios_target = attributes['ios_target']
14
+ @build_configuration = attributes['build_configuration']
15
+ @provisioning_search_query = attributes['provisioning_search_query']
16
+ @target = attributes['target']
17
17
  @note_generation_method = attributes['note_generation_method']
18
18
  @notify = attributes['notify']
19
19
  @versioning_method = attributes['versioning_method']
@@ -6,7 +6,7 @@ module Thrust
6
6
 
7
7
  def capture_status_and_output_from_command(command, env = {})
8
8
  env_string = ''
9
- for key in env.keys
9
+ env.keys.each do |key|
10
10
  env_string += "#{key}=#{env[key]} "
11
11
  end
12
12
 
@@ -36,10 +36,11 @@ module Thrust
36
36
  @out.puts "Executing #{cmd} and checking for FAILURE"
37
37
  execution = @execution_helper.capture_status_and_output_from_command("#{cmd} 2>&1", env)
38
38
  result = execution[:output]
39
+
39
40
  @out.puts "Results:"
40
41
  @out.puts result
41
42
 
42
- result.include?("Finished") && !result.include?("FAILURE") && !result.include?("EXCEPTION")
43
+ Thrust::CedarResultsParser.parse_results_for_success(result)
43
44
  end
44
45
  end
45
46
  end
@@ -0,0 +1,36 @@
1
+ require 'tmpdir'
2
+
3
+ module Thrust
4
+ class IOSSpecLauncher
5
+ def initialize(out = $stdout, thrust_executor = Thrust::Executor.new)
6
+ @thrust_executor = thrust_executor
7
+ @out = out
8
+ end
9
+
10
+ def run(executable_name, build_configuration, build_sdk, os_version, device_name, timeout, build_directory, simulator_binary, environment_variables)
11
+ device_type_id = "com.apple.CoreSimulator.SimDeviceType.#{device_name}, #{os_version}"
12
+ app_executable = File.join(build_directory, "#{build_configuration}-#{build_sdk}", "#{executable_name}.app")
13
+ simulator_binary ||= 'ios-sim'
14
+ output_file = "tmp/thrust_specs_output"
15
+
16
+ arguments = ["--devicetypeid \"#{device_type_id}\"",
17
+ "--timeout #{timeout || '30'}",
18
+ "--stdout #{output_file}",
19
+ "--setenv CFFIXED_USER_HOME=\"#{Dir.tmpdir}\"",]
20
+
21
+ environment_variables.each do |key, value|
22
+ arguments << "--setenv #{key}=\"#{value}\""
23
+ end
24
+
25
+ @thrust_executor.system_or_exit("#{simulator_binary} launch #{app_executable} #{arguments.compact.join(' ')}")
26
+
27
+ results = File.read(output_file)
28
+ FileUtils.rm_r('tmp')
29
+
30
+ @out.puts 'Results:'
31
+ @out.puts results
32
+
33
+ Thrust::CedarResultsParser.parse_results_for_success(results)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ module Thrust
2
+ class OSXSpecLauncher
3
+ def initialize(out = $stdout, thrust_executor = Thrust::Executor.new)
4
+ @thrust_executor = thrust_executor
5
+ @out = out
6
+ end
7
+
8
+ def run(executable_name, build_configuration, build_directory, environment_variables)
9
+ build_path = File.join(build_directory, build_configuration)
10
+ app_executable = File.join(build_path, executable_name)
11
+ env = {'DYLD_FRAMEWORK_PATH' => "\"#{build_path}\""}.merge(environment_variables)
12
+ @thrust_executor.check_command_for_failure("\"#{app_executable}\"", env)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ require 'nori'
2
+
3
+ module Thrust
4
+ class SchemeParser
5
+ def parse_environment_variables(scheme, xcodeproj_path = nil)
6
+ config = load_scheme(scheme, xcodeproj_path)
7
+
8
+ env = {}
9
+
10
+ if config['Scheme']['LaunchAction']['EnvironmentVariables']
11
+ environment_variables = config['Scheme']['LaunchAction']['EnvironmentVariables']['EnvironmentVariable']
12
+ environment_variables = [environment_variables] unless environment_variables.is_a?(Array)
13
+
14
+ environment_variables.each do |environment_variable|
15
+ if environment_variable['@isEnabled'] == 'YES'
16
+ env[environment_variable['@key']] = environment_variable['@value']
17
+ end
18
+ end
19
+ end
20
+
21
+ env
22
+ end
23
+
24
+ private
25
+
26
+ def load_scheme(scheme, xcodeproj_path)
27
+ scheme_path = "**/#{scheme}.xcscheme"
28
+ scheme_path = "#{xcodeproj_path}/#{scheme_path}" if xcodeproj_path
29
+
30
+ scheme_file = Dir.glob(scheme_path).first
31
+ parser = Nori.new(parser: :rexml)
32
+ parser.parse(File.read(scheme_file))
33
+ end
34
+ end
35
+ end
@@ -1,12 +1,11 @@
1
1
  module Thrust
2
- class IOSSpecTarget
2
+ class SpecTarget
3
3
  attr_reader :build_configuration,
4
4
  :build_sdk,
5
5
  :device,
6
6
  :device_name,
7
7
  :os_version,
8
8
  :scheme,
9
- :target,
10
9
  :type,
11
10
  :timeout
12
11
 
@@ -17,7 +16,6 @@ module Thrust
17
16
  @device_name = attributes['device_name']
18
17
  @os_version = attributes['os_version']
19
18
  @scheme = attributes['scheme']
20
- @target = attributes['target']
21
19
  @type = attributes['type'] || 'app'
22
20
  @timeout = attributes['timeout']
23
21
  end
@@ -1,4 +1,4 @@
1
- load File.expand_path('../../tasks/cedar.rake', __FILE__) unless File.exists?('AndroidManifest.xml')
1
+ load File.expand_path('../../tasks/cedar.rake', __FILE__)
2
2
  load File.expand_path('../../tasks/autotag.rake', __FILE__)
3
3
  load File.expand_path('../../tasks/testflight.rake', __FILE__)
4
4
  load File.expand_path('../../tasks/version.rake', __FILE__)
@@ -6,8 +6,8 @@ module Thrust
6
6
  @git = git
7
7
  end
8
8
 
9
- def run(thrust)
10
- thrust.app_config.deployment_targets.each do |deployment_target, _|
9
+ def run(app_config)
10
+ app_config.deployment_targets.each do |deployment_target, _|
11
11
  puts @git.commit_summary_for_last_deploy(deployment_target)
12
12
  end
13
13
  end
@@ -1,18 +1,18 @@
1
1
  module Thrust
2
2
  module Tasks
3
3
  class Clean
4
- def initialize(out = $stdout, xcode_tools_provider = Thrust::IOS::XCodeToolsProvider.new)
4
+ def initialize(out = $stdout, xcode_tools_provider = Thrust::XcodeToolsProvider.new)
5
5
  @xcode_tools_provider = xcode_tools_provider
6
6
  @out = out
7
7
  end
8
8
 
9
- def run(thrust)
9
+ def run(app_config)
10
10
  tools_options = {
11
- project_name: thrust.app_config.project_name,
12
- workspace_name: thrust.app_config.workspace_name
11
+ project_name: app_config.project_name,
12
+ workspace_name: app_config.workspace_name
13
13
  }
14
14
 
15
- xcode_tools = @xcode_tools_provider.instance(@out, nil, thrust.build_dir, tools_options)
15
+ xcode_tools = @xcode_tools_provider.instance(@out, nil, app_config.build_directory, tools_options)
16
16
  xcode_tools.clean_build
17
17
  end
18
18
  end
@@ -3,14 +3,24 @@ module Thrust
3
3
  class FocusedSpecs
4
4
  FOCUSED_METHODS = %w[fit(@ fcontext(@ fdescribe(@]
5
5
 
6
- def initialize(executor = Thrust::Executor.new)
6
+ def initialize(out = $stdout, executor = Thrust::Executor.new)
7
+ @out = out
7
8
  @executor = executor
8
9
  end
9
10
 
10
- def run(thrust)
11
+ def run(app_config)
12
+ if app_config.spec_directories.empty?
13
+ @out.puts 'Unable to find focused specs without `spec_directories` defined in thrust.yml.'.red
14
+ exit 1
15
+ end
16
+
11
17
  pattern = FOCUSED_METHODS.join("\\|")
12
- directories = thrust.app_config.ios_spec_targets.values.map(&:target).join(' ')
13
- @executor.system_or_exit %Q[grep -l -r -e "\\(#{pattern}\\)" #{directories} | grep -v 'Frameworks'; exit 0]
18
+ directories = app_config.spec_directories.map{ |sd| "\"#{sd}\"" }.join(' ')
19
+ output = @executor.capture_output_from_system %Q[grep -l -r -e "\\(#{pattern}\\)" #{directories} | grep -v 'Frameworks' || true]
20
+
21
+ @out.puts output
22
+
23
+ output.split("\n")
14
24
  end
15
25
  end
16
26
  end
@@ -3,17 +3,22 @@ module Thrust
3
3
  class Nof
4
4
  FOCUSED_METHODS = %w[fit(@ fcontext(@ fdescribe(@]
5
5
 
6
- def initialize(executor = Thrust::Executor.new)
6
+ def initialize(executor = Thrust::Executor.new, focused_specs = Thrust::Tasks::FocusedSpecs.new)
7
7
  @executor = executor
8
+ @focused_specs = focused_specs
8
9
  end
9
10
 
10
- def run
11
+ def run(app_config)
11
12
  substitutions = FOCUSED_METHODS.map do |method|
12
13
  unfocused_method = method.sub(/^f/, '')
13
14
  "-e 's/#{method}/#{unfocused_method}/g;'"
14
15
  end
15
16
 
16
- @executor.system_or_exit %Q[rake focused_specs | xargs -I filename sed -i '' #{substitutions.join(' ')} "filename"]
17
+ focused_spec_files = @focused_specs.run(app_config)
18
+ unless focused_spec_files.empty?
19
+ quoted_spec_files = focused_spec_files.map { |file| "\"#{file}\"" }.join(' ')
20
+ @executor.system_or_exit %Q[sed -i '' #{substitutions.join(' ')} #{quoted_spec_files}]
21
+ end
17
22
  end
18
23
  end
19
24
  end
@@ -0,0 +1,75 @@
1
+ require 'nori'
2
+ require 'json'
3
+
4
+ module Thrust
5
+ module Tasks
6
+ class SpecRunner
7
+ def initialize(out = $stdout,
8
+ xcode_tools_provider = Thrust::XcodeToolsProvider.new,
9
+ ios_spec_launcher = Thrust::IOSSpecLauncher.new,
10
+ osx_spec_launcher = Thrust::OSXSpecLauncher.new,
11
+ scheme_parser = Thrust::SchemeParser.new)
12
+ @xcode_tools_provider = xcode_tools_provider
13
+ @ios_spec_launcher = ios_spec_launcher
14
+ @osx_spec_launcher = osx_spec_launcher
15
+ @scheme_parser = scheme_parser
16
+ @out = out
17
+ end
18
+
19
+ def run(app_config, target_info, args)
20
+ build_configuration = target_info.build_configuration
21
+ type = target_info.type
22
+ scheme = target_info.scheme
23
+ build_sdk = target_info.build_sdk
24
+ os_version = args[:os_version] || target_info.os_version
25
+ device_name = args[:device_name] || target_info.device_name
26
+
27
+ if device_name
28
+ substitution_map = {'bundle' => '-', 'app' => ' '}
29
+ destination_map = {'bundle' => ' ', 'app' => '-'}
30
+ device_name.gsub!(substitution_map[type], destination_map[type])
31
+ end
32
+
33
+ xcode_tools = @xcode_tools_provider.instance(@out,
34
+ build_configuration,
35
+ app_config.build_directory,
36
+ project_name: app_config.project_name,
37
+ workspace_name: app_config.workspace_name)
38
+
39
+ if type == 'app'
40
+ xcode_tools.build_scheme(scheme, build_sdk)
41
+
42
+ executable_name = xcode_tools.find_executable_name(scheme)
43
+ environment_variables = @scheme_parser.parse_environment_variables(scheme, app_config.path_to_xcodeproj)
44
+ environment_variables['CEDAR_RANDOM_SEED'] = ENV['CEDAR_RANDOM_SEED'] if ENV['CEDAR_RANDOM_SEED']
45
+
46
+ if build_sdk.include?('macosx')
47
+ @osx_spec_launcher.run(executable_name,
48
+ build_configuration,
49
+ app_config.build_directory,
50
+ environment_variables)
51
+ else
52
+ xcode_tools.kill_simulator
53
+
54
+ @ios_spec_launcher.run(executable_name,
55
+ build_configuration,
56
+ build_sdk,
57
+ os_version,
58
+ device_name,
59
+ target_info.timeout,
60
+ app_config.build_directory,
61
+ app_config.ios_sim_path,
62
+ environment_variables)
63
+ end
64
+ else
65
+ xcode_tools.test(scheme,
66
+ build_configuration,
67
+ os_version,
68
+ device_name,
69
+ target_info.timeout,
70
+ app_config.build_directory)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,159 @@
1
+ module Thrust
2
+ class XcodeTools
3
+ ProvisioningProfileNotFound = Class.new(StandardError)
4
+ ProvisioningProfileNotEmbedded = Class.new(StandardError)
5
+
6
+ def initialize(thrust_executor, out, build_configuration, build_directory, options = {})
7
+ @thrust_executor = thrust_executor
8
+ @out = out
9
+ @git = Thrust::Git.new(@out, @thrust_executor)
10
+ @build_configuration = build_configuration
11
+ @build_directory = build_directory
12
+ @project_name = options[:project_name]
13
+ @workspace_name = options[:workspace_name]
14
+ raise "project_name OR workspace_name required" unless @project_name.nil? ^ @workspace_name.nil?
15
+ end
16
+
17
+ def cleanly_create_ipa(target, app_name, signing_identity, provision_search_query = nil)
18
+ kill_simulator
19
+ build_target(target, 'iphoneos', true)
20
+ ipa_name = create_ipa(app_name, signing_identity, provision_search_query)
21
+ verify_provision(app_name, provision_search_query)
22
+
23
+ return ipa_name
24
+ end
25
+
26
+ def build_configuration_directory
27
+ "#{@build_directory}/#{@build_configuration}-iphoneos"
28
+ end
29
+
30
+ def clean_build
31
+ @out.puts 'Cleaning...'
32
+ FileUtils.rm_rf(@build_directory)
33
+ end
34
+
35
+ def build_scheme(scheme, build_sdk, clean = false)
36
+ @out.puts 'Building...'
37
+ build("-scheme \"#{scheme}\"", build_sdk, clean)
38
+ end
39
+
40
+ def build_target(target, build_sdk, clean = false)
41
+ @out.puts 'Building...'
42
+ build("-target \"#{target}\"", build_sdk, clean)
43
+ end
44
+
45
+ def test(scheme, build_configuration, os_version, device_name, timeout, build_dir)
46
+ destination = "OS=#{os_version},name=#{device_name}"
47
+ timeout ||= "30"
48
+
49
+ cmd = [
50
+ "xcodebuild",
51
+ "test",
52
+ "-scheme '#{scheme}'",
53
+ "-configuration '#{build_configuration}'",
54
+ "-destination '#{destination}'",
55
+ "-destination-timeout '#{timeout}'",
56
+ "SYMROOT='#{build_dir}'"
57
+ ].join(' ')
58
+
59
+ @thrust_executor.check_command_for_failure(cmd)
60
+ end
61
+
62
+ def kill_simulator
63
+ @out.puts('Killing simulator...')
64
+ @thrust_executor.system %q[killall -m -KILL "gdb"]
65
+ @thrust_executor.system %q[killall -m -KILL "otest"]
66
+ @thrust_executor.system %q[killall -m -KILL "iOS Simulator"]
67
+ end
68
+
69
+ def find_executable_name(scheme)
70
+ build_settings = @thrust_executor.capture_output_from_system("xcodebuild -scheme \"#{scheme}\" -showBuildSettings")
71
+ matches = build_settings.match(/EXECUTABLE_NAME = (.*)$/)
72
+ matches.captures.first
73
+ end
74
+
75
+ private
76
+
77
+ def provision_path(provision_search_query)
78
+ provision_search_path = File.expand_path("~/Library/MobileDevice/Provisioning Profiles")
79
+ command = %Q(find '#{provision_search_path}' -print0 | xargs -0 grep -lr '#{provision_search_query}' --null | xargs -0 ls -t)
80
+ provisioning_profile = @thrust_executor.capture_output_from_system(command).split("\n").first
81
+ if !provisioning_profile
82
+ raise(ProvisioningProfileNotFound, "\nCouldn't find provisioning profiles matching #{provision_search_query}.\n\nThe command used was:\n\n#{command}")
83
+ end
84
+ provisioning_profile
85
+ end
86
+
87
+ def build(scheme_or_target_flag, build_sdk, clean)
88
+ sdk_flag = build_sdk ? "-sdk #{build_sdk}" : nil
89
+ configuration_build_dir = File.join(@build_directory, "#{@build_configuration}-#{build_sdk}")
90
+ configuration_build_dir_option = build_sdk.include?('macosx') ? nil : "CONFIGURATION_BUILD_DIR=\"#{configuration_build_dir}\""
91
+
92
+ command = [
93
+ 'set -o pipefail &&',
94
+ 'xcodebuild',
95
+ project_or_workspace_flag,
96
+ scheme_or_target_flag,
97
+ "-configuration #{@build_configuration}",
98
+ sdk_flag,
99
+ clean ? 'clean build' : nil,
100
+ "SYMROOT=\"#{@build_directory}\"",
101
+ configuration_build_dir_option,
102
+ '2>&1',
103
+ "| grep -v 'backing file'"
104
+ ].compact.join(' ')
105
+ output_file = output_file("#{@build_configuration}-build")
106
+ begin
107
+ @thrust_executor.system_or_exit(command, output_file)
108
+ rescue Thrust::Executor::CommandFailed => e
109
+ @out.write File.read(output_file)
110
+ raise e
111
+ end
112
+ end
113
+
114
+ def create_ipa(app_name, signing_identity, provision_search_query)
115
+ @out.puts 'Packaging...'
116
+ app_filepath = "#{build_configuration_directory}/#{app_name}.app"
117
+ ipa_filepath = "#{build_configuration_directory}/#{app_name}.ipa"
118
+ package_command = [
119
+ "xcrun",
120
+ "-sdk iphoneos",
121
+ "-v PackageApplication",
122
+ "'#{app_filepath}'",
123
+ "-o '#{ipa_filepath}'",
124
+ "--embed '#{provision_path(provision_search_query)}'"
125
+ ].join(' ')
126
+ @thrust_executor.system_or_exit(package_command)
127
+
128
+ @thrust_executor.system_or_exit("cd '#{build_configuration_directory}' && unzip '#{app_name}.ipa'")
129
+ @thrust_executor.system_or_exit("/usr/bin/codesign --verify --force --preserve-metadata=identifier,entitlements --sign '#{signing_identity}' '#{build_configuration_directory}/Payload/#{app_name}.app'")
130
+ @thrust_executor.system_or_exit("cd '#{build_configuration_directory}' && zip -qr '#{app_name}.ipa' 'Payload'")
131
+
132
+ ipa_filepath
133
+ end
134
+
135
+ def verify_provision(app_name, provision_search_query)
136
+ @out.puts 'Verifying provisioning profile...'
137
+ embedded_filename = "#{build_configuration_directory}/#{app_name}.app/embedded.mobileprovision"
138
+ correct_provision_filename = provision_path(provision_search_query)
139
+
140
+ if !FileUtils.cmp(embedded_filename, correct_provision_filename)
141
+ raise(ProvisioningProfileNotEmbedded, "Wrong mobile provision embedded by xcrun. Check your xcode provisioning profile settings.")
142
+ end
143
+ end
144
+
145
+ def output_file(target)
146
+ output_dir = if ENV['IS_CI_BOX']
147
+ ENV['CC_BUILD_ARTIFACTS']
148
+ else
149
+ File.exists?(@build_directory) ? @build_directory : FileUtils.mkdir_p(@build_directory)
150
+ end
151
+
152
+ File.join(output_dir, "#{target}.output").tap { |file| @out.puts "Output: #{file}" }
153
+ end
154
+
155
+ def project_or_workspace_flag
156
+ @workspace_name ? "-workspace #{@workspace_name}.xcworkspace" : "-project #{@project_name}.xcodeproj"
157
+ end
158
+ end
159
+ end