thrust 0.5.2 → 0.6.0

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.
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