illuminator 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|