illuminator 0.1.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.
- checksums.yaml +7 -0
- data/gem/README.md +37 -0
- data/gem/bin/illuminatorTestRunner.rb +22 -0
- data/gem/lib/illuminator.rb +171 -0
- data/gem/lib/illuminator/argument-parsing.rb +299 -0
- data/gem/lib/illuminator/automation-builder.rb +39 -0
- data/gem/lib/illuminator/automation-runner.rb +589 -0
- data/gem/lib/illuminator/build-artifacts.rb +118 -0
- data/gem/lib/illuminator/device-installer.rb +45 -0
- data/gem/lib/illuminator/host-utils.rb +42 -0
- data/gem/lib/illuminator/instruments-runner.rb +301 -0
- data/gem/lib/illuminator/javascript-runner.rb +98 -0
- data/gem/lib/illuminator/listeners/console-logger.rb +32 -0
- data/gem/lib/illuminator/listeners/full-output.rb +13 -0
- data/gem/lib/illuminator/listeners/instruments-listener.rb +22 -0
- data/gem/lib/illuminator/listeners/intermittent-failure-detector.rb +49 -0
- data/gem/lib/illuminator/listeners/pretty-output.rb +26 -0
- data/gem/lib/illuminator/listeners/saltinel-agent.rb +66 -0
- data/gem/lib/illuminator/listeners/saltinel-listener.rb +26 -0
- data/gem/lib/illuminator/listeners/start-detector.rb +52 -0
- data/gem/lib/illuminator/listeners/stop-detector.rb +46 -0
- data/gem/lib/illuminator/listeners/test-listener.rb +58 -0
- data/gem/lib/illuminator/listeners/trace-error-detector.rb +38 -0
- data/gem/lib/illuminator/options.rb +96 -0
- data/gem/lib/illuminator/resources/IlluminatorGeneratedEnvironment.erb +13 -0
- data/gem/lib/illuminator/resources/IlluminatorGeneratedRunnerForInstruments.erb +19 -0
- data/gem/lib/illuminator/test-definitions.rb +23 -0
- data/gem/lib/illuminator/test-suite.rb +155 -0
- data/gem/lib/illuminator/version.rb +3 -0
- data/gem/lib/illuminator/xcode-builder.rb +144 -0
- data/gem/lib/illuminator/xcode-utils.rb +219 -0
- data/gem/resources/BuildConfiguration.xcconfig +10 -0
- data/gem/resources/js/AppMap.js +767 -0
- data/gem/resources/js/Automator.js +1132 -0
- data/gem/resources/js/Base64.js +142 -0
- data/gem/resources/js/Bridge.js +102 -0
- data/gem/resources/js/Config.js +92 -0
- data/gem/resources/js/Extensions.js +2025 -0
- data/gem/resources/js/Illuminator.js +228 -0
- data/gem/resources/js/Preferences.js +24 -0
- data/gem/resources/scripts/UIAutomationBridge.rb +248 -0
- data/gem/resources/scripts/common.applescript +25 -0
- data/gem/resources/scripts/diff_png.sh +61 -0
- data/gem/resources/scripts/kill_all_sim_processes.sh +17 -0
- data/gem/resources/scripts/plist_to_json.sh +40 -0
- data/gem/resources/scripts/set_hardware_keyboard.applescript +0 -0
- metadata +225 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module TraceErrorDetectorEventSink
|
4
|
+
|
5
|
+
# fatal means that restarting instruments won't fix it
|
6
|
+
def trace_error_detector_triggered(fatal, message)
|
7
|
+
puts " +++ If you're seeing this, #{self.class.name}.#{__method__} was not overridden"
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
# TraceErrorDetector monitors the logs for things that indicate a transient failure to start instruments
|
13
|
+
# - "unable to install app"
|
14
|
+
# - etc
|
15
|
+
class TraceErrorDetector < InstrumentsListener
|
16
|
+
|
17
|
+
attr_accessor :event_sink
|
18
|
+
|
19
|
+
def trigger(fatal, message)
|
20
|
+
@event_sink.trace_error_detector_triggered(fatal, message)
|
21
|
+
end
|
22
|
+
|
23
|
+
def receive message
|
24
|
+
itr = "Instruments Trace Error : Target failed to run:"
|
25
|
+
if message.full_line =~ /#{itr} Unable to install app with path:/
|
26
|
+
trigger(false, "Failed to install app because #{message.full_line.split(': ')[-1]}")
|
27
|
+
elsif message.full_line =~ /#{itr} The operation couldn’t be completed./
|
28
|
+
trigger(false, "An operation couldn't be completed because #{message.full_line.split(': ')[-1]}")
|
29
|
+
elsif message.full_line =~ /Instruments Trace Error/i
|
30
|
+
trigger(false, message.full_line.split(' : ')[1..-1].join)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_automation_finished
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
class RecursiveOpenStruct < OpenStruct
|
5
|
+
|
6
|
+
def initialize(hash=nil)
|
7
|
+
# preprocess hash objects into openstruct objects
|
8
|
+
unless hash.nil?
|
9
|
+
hash.each do |k, v|
|
10
|
+
hash[k] = RecursiveOpenStruct.new(v) if v.is_a? Hash
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
# recursively translate the openstruct hierarchy to a hash hierarchy
|
18
|
+
def to_h
|
19
|
+
ret = super
|
20
|
+
|
21
|
+
ret.each do |k, v|
|
22
|
+
ret[k] = v.to_h if v.is_a? RecursiveOpenStruct
|
23
|
+
end
|
24
|
+
|
25
|
+
return ret
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Illuminator
|
30
|
+
class Options < RecursiveOpenStruct
|
31
|
+
|
32
|
+
def initialize(hash=nil)
|
33
|
+
super
|
34
|
+
return unless hash.nil?
|
35
|
+
|
36
|
+
# stub out all the branches
|
37
|
+
self.xcode = RecursiveOpenStruct.new
|
38
|
+
self.instruments = RecursiveOpenStruct.new
|
39
|
+
self.simulator = RecursiveOpenStruct.new
|
40
|
+
self.javascript = RecursiveOpenStruct.new
|
41
|
+
self.illuminator = RecursiveOpenStruct.new
|
42
|
+
self.app_specific = nil # all unknown options will go here
|
43
|
+
self.build_artifacts_dir = nil
|
44
|
+
|
45
|
+
self.illuminator.clean = RecursiveOpenStruct.new
|
46
|
+
self.illuminator.task = RecursiveOpenStruct.new
|
47
|
+
self.illuminator.test = RecursiveOpenStruct.new
|
48
|
+
|
49
|
+
self.illuminator.test.tags = RecursiveOpenStruct.new
|
50
|
+
self.illuminator.test.retest = RecursiveOpenStruct.new
|
51
|
+
|
52
|
+
# name all the keys (just for visibiilty)
|
53
|
+
self.xcode.project_dir = nil
|
54
|
+
self.xcode.project = nil
|
55
|
+
self.xcode.app_name = nil
|
56
|
+
self.xcode.sdk = nil
|
57
|
+
self.xcode.workspace = nil
|
58
|
+
self.xcode.scheme = nil
|
59
|
+
self.xcode.environment_vars = nil
|
60
|
+
|
61
|
+
self.illuminator.entry_point = nil
|
62
|
+
self.illuminator.test.random_seed = nil
|
63
|
+
self.illuminator.test.tags.any = nil
|
64
|
+
self.illuminator.test.tags.all = nil
|
65
|
+
self.illuminator.test.tags.none = nil
|
66
|
+
self.illuminator.test.names = nil
|
67
|
+
self.illuminator.test.retest.attempts = nil
|
68
|
+
self.illuminator.test.retest.solo = nil
|
69
|
+
self.illuminator.clean.xcode = nil
|
70
|
+
self.illuminator.clean.derived = nil
|
71
|
+
self.illuminator.clean.artifacts = nil
|
72
|
+
self.illuminator.clean.no_delay = nil
|
73
|
+
self.illuminator.task.build = nil
|
74
|
+
self.illuminator.task.automate = nil
|
75
|
+
self.illuminator.task.set_sim = nil
|
76
|
+
self.illuminator.task.coverage = nil
|
77
|
+
self.illuminator.hardware_id = nil
|
78
|
+
|
79
|
+
self.simulator.device = nil
|
80
|
+
self.simulator.version = nil
|
81
|
+
self.simulator.language = nil
|
82
|
+
self.simulator.kill_after = nil
|
83
|
+
|
84
|
+
self.instruments.do_verbose = nil
|
85
|
+
self.instruments.timeout = nil
|
86
|
+
self.instruments.max_silence = nil
|
87
|
+
self.instruments.attempts = nil
|
88
|
+
self.instruments.app_location = nil # normally, this is where we build to
|
89
|
+
|
90
|
+
self.javascript.test_path = nil
|
91
|
+
self.javascript.implementation = nil
|
92
|
+
self.javascript.app_specific_config = nil
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/**
|
2
|
+
* This file is generated automatically.
|
3
|
+
* It provides global path information that would be problematic to introduce through config values
|
4
|
+
* (especially given that the config comes from a file)
|
5
|
+
*/
|
6
|
+
|
7
|
+
// Based on the way UIAutomation processes imports, this will be the first line of code to actually execute
|
8
|
+
UIALogger.logDebug("Illuminator has launched.");
|
9
|
+
|
10
|
+
var IlluminatorRootDirectory = "<%= @illuminator_root %>";
|
11
|
+
var IlluminatorScriptsDirectory = "<%= @illuminator_scripts %>";
|
12
|
+
var IlluminatorBuildArtifactsDirectory = "<%= @artifacts_root %>";
|
13
|
+
var IlluminatorInstrumentsOutputDirectory = "<%= @illuminator_instruments_root %>";
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#import "<%= @environment_file %>";
|
2
|
+
#import "<%= @illuminator_root %>/Preferences.js";
|
3
|
+
#import "<%= @illuminator_root %>/Extensions.js";
|
4
|
+
#import "<%= @illuminator_root %>/Base64.js";
|
5
|
+
#import "<%= @illuminator_root %>/Config.js";
|
6
|
+
#import "<%= @illuminator_root %>/AppMap.js";
|
7
|
+
#import "<%= @illuminator_root %>/Automator.js";
|
8
|
+
#import "<%= @illuminator_root %>/Bridge.js";
|
9
|
+
#import "<%= @illuminator_root %>/Illuminator.js";
|
10
|
+
#import "<%= @test_path %>";
|
11
|
+
|
12
|
+
/**
|
13
|
+
* IlluminatorGeneratedRunnerForInstruments.js is generated from
|
14
|
+
* IlluminatorGeneratedRunnerForInstruments.erb; it should not be hand-edited.
|
15
|
+
*
|
16
|
+
* This file is used to import your test definitions into the automation framework.
|
17
|
+
*/
|
18
|
+
|
19
|
+
IlluminatorIlluminate();
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# A class to hold the defintions of all automator tests, as defined in the (generated) automatorScenarios.json
|
4
|
+
class TestDefinitions
|
5
|
+
|
6
|
+
def initialize automator_settings_json_path
|
7
|
+
raw_defs = JSON.parse( IO.read(automator_settings_json_path) )
|
8
|
+
@in_order = raw_defs["scenarios"].dup
|
9
|
+
|
10
|
+
# save test defs for use later (as lookups)
|
11
|
+
@by_name = {}
|
12
|
+
@in_order.each { |scen| @by_name[scen["title"]] = scen }
|
13
|
+
end
|
14
|
+
|
15
|
+
def by_name name
|
16
|
+
@by_name[name].dup
|
17
|
+
end
|
18
|
+
|
19
|
+
def by_index idx
|
20
|
+
@in_order[idx].dup
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
class TestSuite
|
4
|
+
|
5
|
+
attr_reader :test_cases
|
6
|
+
attr_reader :implementation
|
7
|
+
|
8
|
+
def initialize(implementation)
|
9
|
+
@implementation = implementation
|
10
|
+
@test_cases = []
|
11
|
+
@case_lookup = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_test_case(class_name, name)
|
15
|
+
test = TestCase.new(@implementation, class_name, name)
|
16
|
+
@test_cases << test
|
17
|
+
@case_lookup[name] = test
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](test_case_name)
|
21
|
+
@case_lookup[test_case_name]
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: fix naming, some of these return test cases and some return arrays of names
|
25
|
+
|
26
|
+
def unstarted_tests
|
27
|
+
@test_cases.reject { |t| t.ran? }.map { |t| t.name }
|
28
|
+
end
|
29
|
+
|
30
|
+
def finished_tests
|
31
|
+
@test_cases.select { |t| t.ran? } .map { |t| t.name }
|
32
|
+
end
|
33
|
+
|
34
|
+
def all_tests
|
35
|
+
@test_cases.dup
|
36
|
+
end
|
37
|
+
|
38
|
+
def passed_tests
|
39
|
+
@test_cases.select { |t| t.passed? }
|
40
|
+
end
|
41
|
+
|
42
|
+
def unpassed_tests
|
43
|
+
@test_cases.reject { |t| t.passed? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_xml
|
47
|
+
output = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' << "\n"
|
48
|
+
|
49
|
+
output << "<testsuite>\n"
|
50
|
+
@test_cases.each { |test| output << test.to_xml }
|
51
|
+
output << "</testsuite>" << "\n"
|
52
|
+
output
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class TestCase
|
57
|
+
attr_reader :name
|
58
|
+
attr_reader :class_name
|
59
|
+
attr_reader :implementation
|
60
|
+
|
61
|
+
attr_accessor :stacktrace
|
62
|
+
|
63
|
+
def initialize(implementation, class_name, name)
|
64
|
+
@implementation = implementation
|
65
|
+
@class_name = class_name
|
66
|
+
@name = name
|
67
|
+
reset!
|
68
|
+
end
|
69
|
+
|
70
|
+
def <<(stdoutLine)
|
71
|
+
@stdout << stdoutLine
|
72
|
+
end
|
73
|
+
|
74
|
+
def reset!
|
75
|
+
@stdout = []
|
76
|
+
@stacktrace = ""
|
77
|
+
@fail_message = ""
|
78
|
+
@fail_tag = nil
|
79
|
+
@time_start = nil
|
80
|
+
@time_finish = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def start!
|
84
|
+
@time_start = Time.now
|
85
|
+
end
|
86
|
+
|
87
|
+
def pass!
|
88
|
+
@time_finish = Time.now
|
89
|
+
end
|
90
|
+
|
91
|
+
def fail message
|
92
|
+
@time_finish = Time.now
|
93
|
+
@fail_tag = "failure"
|
94
|
+
@fail_message = message
|
95
|
+
end
|
96
|
+
|
97
|
+
def error message
|
98
|
+
@time_finish = Time.now
|
99
|
+
@fail_tag = "error"
|
100
|
+
@fail_message = message
|
101
|
+
end
|
102
|
+
|
103
|
+
def ran?
|
104
|
+
not (@time_start.nil? or @time_finish.nil?)
|
105
|
+
end
|
106
|
+
|
107
|
+
def passed?
|
108
|
+
@fail_tag.nil?
|
109
|
+
end
|
110
|
+
|
111
|
+
# this is NOT the opposite of passed! this does not count errored tests
|
112
|
+
def failed?
|
113
|
+
@fail_tag == "failure"
|
114
|
+
end
|
115
|
+
|
116
|
+
def errored?
|
117
|
+
@fail_tag == "error"
|
118
|
+
end
|
119
|
+
|
120
|
+
def time
|
121
|
+
return 0 if @time_finish.nil?
|
122
|
+
@time_finish - @time_start
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_xml
|
126
|
+
attrs = {
|
127
|
+
"name" => @name,
|
128
|
+
"classname" => "#{@implementation}.#{@class_name}",
|
129
|
+
"time" => time,
|
130
|
+
}
|
131
|
+
|
132
|
+
output = " <testcase"
|
133
|
+
attrs.each { |key, value| output << " #{key}=#{value.to_s.encode(:xml => :attr)}" }
|
134
|
+
output << ">\n"
|
135
|
+
|
136
|
+
if not ran?
|
137
|
+
output << " <skipped />\n"
|
138
|
+
elsif (not @fail_tag.nil?)
|
139
|
+
fattrs = {
|
140
|
+
"message" => @fail_message,
|
141
|
+
}
|
142
|
+
|
143
|
+
output << " <#{@fail_tag}"
|
144
|
+
fattrs.each { |key, value| output << " #{key}=#{value.to_s.encode(:xml => :attr)}" }
|
145
|
+
output << ">#{@stacktrace.to_s.encode(:xml => :text)}" << "\n"
|
146
|
+
output << " </#{@fail_tag}>" << "\n"
|
147
|
+
end
|
148
|
+
|
149
|
+
output << " <system-out>#{@stdout.map { |m| m.encode(:xml => :text) }.join("\n")}" << "\n"
|
150
|
+
output << " </system-out>" << "\n"
|
151
|
+
|
152
|
+
output << " </testcase>" << "\n"
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
require_relative './build-artifacts'
|
4
|
+
require_relative './host-utils'
|
5
|
+
|
6
|
+
module Illuminator
|
7
|
+
class XcodeBuilder
|
8
|
+
attr_accessor :project
|
9
|
+
attr_accessor :configuration
|
10
|
+
attr_accessor :sdk
|
11
|
+
attr_accessor :arch
|
12
|
+
attr_accessor :scheme
|
13
|
+
attr_accessor :project_dir
|
14
|
+
attr_accessor :workspace
|
15
|
+
attr_accessor :destination
|
16
|
+
attr_accessor :xcconfig
|
17
|
+
attr_accessor :do_clean
|
18
|
+
attr_accessor :do_test
|
19
|
+
attr_accessor :do_build
|
20
|
+
attr_accessor :do_archive
|
21
|
+
attr_accessor :derived_data_is_artifact
|
22
|
+
|
23
|
+
attr_reader :exit_code
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@parameters = Hash.new
|
27
|
+
@environment_vars = Hash.new
|
28
|
+
@project_dir = nil
|
29
|
+
@do_clean = FALSE
|
30
|
+
@do_test = FALSE
|
31
|
+
@do_build = TRUE
|
32
|
+
@do_archive = FALSE
|
33
|
+
@exit_code = nil
|
34
|
+
|
35
|
+
@derived_data_is_artifact = FALSE
|
36
|
+
|
37
|
+
result_path = Illuminator::BuildArtifacts.instance.xcode
|
38
|
+
add_environment_variable('CONFIGURATION_BUILD_DIR', "'#{result_path}'")
|
39
|
+
add_environment_variable('CONFIGURATION_TEMP_DIR', "'#{result_path}'")
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_build_artifacts_root root_dir
|
43
|
+
Illuminator::BuildArtifacts.instance.set_root(root_dir)
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_parameter(parameter_name = '',parameter_value = '')
|
47
|
+
@parameters[parameter_name] = parameter_value
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_environment_variable(parameter_name = '',parameter_value = '')
|
51
|
+
@environment_vars[parameter_name] = parameter_value
|
52
|
+
end
|
53
|
+
|
54
|
+
def _assemble_config
|
55
|
+
# put standard parameters into parameters
|
56
|
+
key_defs = {
|
57
|
+
'project' => @project,
|
58
|
+
'configuration' => @configuration,
|
59
|
+
'sdk' => @sdk,
|
60
|
+
'arch' => @arch,
|
61
|
+
'scheme' => @scheme,
|
62
|
+
'destination' => @destination,
|
63
|
+
'workspace' => @workspace,
|
64
|
+
'xcconfig' => @xcconfig,
|
65
|
+
}
|
66
|
+
|
67
|
+
# since derived data can take quite a lot of disk space, don't automatically store it
|
68
|
+
# in build-specific directory
|
69
|
+
if @derived_data_is_artifact
|
70
|
+
key_defs['derivedDataPath'] = Illuminator::BuildArtifacts.instance.derived_data
|
71
|
+
end
|
72
|
+
|
73
|
+
key_defs.each do |key, value|
|
74
|
+
add_parameter(key, value) unless value.nil?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def _build_command
|
80
|
+
use_pipefail = false # debug option
|
81
|
+
_assemble_config
|
82
|
+
|
83
|
+
parameters = ''
|
84
|
+
environment_vars = ''
|
85
|
+
tasks = ''
|
86
|
+
|
87
|
+
@parameters.each { |name, value| parameters << " -#{name} \"#{value}\"" }
|
88
|
+
@environment_vars.each { |name, value| environment_vars << " #{name}=#{value}" }
|
89
|
+
|
90
|
+
tasks << ' clean' if @do_clean
|
91
|
+
tasks << ' build' if @do_build
|
92
|
+
tasks << ' archive' if @do_archive
|
93
|
+
tasks << ' test' if @do_test
|
94
|
+
|
95
|
+
command = ''
|
96
|
+
command << 'set -o pipefail && ' if use_pipefail
|
97
|
+
command << 'xcodebuild'
|
98
|
+
command << parameters << environment_vars << tasks
|
99
|
+
command << " | tee '#{logfile_path}'"
|
100
|
+
unless Illuminator::HostUtils.which("xcpretty").nil? # use xcpretty if available
|
101
|
+
command << " | xcpretty -c -r junit -o \"#{BuildArtifacts.instance.xcpretty_report_file}\""
|
102
|
+
end
|
103
|
+
command << ' && exit ${PIPESTATUS[0]}' unless use_pipefail
|
104
|
+
|
105
|
+
command
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
def logfile_path
|
110
|
+
log_file = File.join(Illuminator::BuildArtifacts.instance.console, 'xcodebuild.log')
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def _execute_build_command command
|
115
|
+
puts command.green
|
116
|
+
process = IO.popen(command) do |io|
|
117
|
+
io.each {|line| puts line}
|
118
|
+
io.close
|
119
|
+
end
|
120
|
+
|
121
|
+
ec = $?
|
122
|
+
@exit_code = ec.exitstatus
|
123
|
+
return @exit_code == 0
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
def build
|
128
|
+
command = _build_command
|
129
|
+
|
130
|
+
# switch to a directory (if desired) and build
|
131
|
+
directory = Dir.pwd
|
132
|
+
retval = nil
|
133
|
+
begin
|
134
|
+
Dir.chdir(@project_dir) unless @project_dir.nil?
|
135
|
+
retval = _execute_build_command command
|
136
|
+
ensure
|
137
|
+
Dir.chdir(directory) unless @project_dir.nil?
|
138
|
+
end
|
139
|
+
|
140
|
+
retval
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|