eyes_selenium 6.12.10 → 6.12.11
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 +4 -4
- data/CHANGELOG.md +25 -0
- data/eyes_selenium.gemspec +2 -5
- data/lib/applitools/eyes_selenium/version.rb +1 -1
- data/lib/applitools/selenium/browsers_info.rb +2 -0
- data/lib/applitools/selenium/concerns/selenium_eyes.rb +0 -33
- data/lib/applitools/selenium/driver.rb +1 -1
- data/lib/applitools/selenium/eyes.rb +1 -5
- data/lib/applitools/selenium/selenium_eyes.rb +15 -315
- data/lib/applitools/selenium/visual_grid/visual_grid_runner.rb +33 -92
- data/lib/eyes_selenium.rb +0 -2
- metadata +9 -64
- data/lib/applitools/selenium/css_parser/find_embedded_resources.rb +0 -102
- data/lib/applitools/selenium/dom_capture/dom_capture.rb +0 -172
- data/lib/applitools/selenium/dom_capture/dom_capture_script.rb +0 -611
- data/lib/applitools/selenium/external_css_resources.rb +0 -32
- data/lib/applitools/selenium/visual_grid/dom_snapshot_script.rb +0 -198
- data/lib/applitools/selenium/visual_grid/eyes_connector.rb +0 -170
- data/lib/applitools/selenium/visual_grid/render_info.rb +0 -24
- data/lib/applitools/selenium/visual_grid/render_request.rb +0 -24
- data/lib/applitools/selenium/visual_grid/render_requests.rb +0 -12
- data/lib/applitools/selenium/visual_grid/render_task.rb +0 -311
- data/lib/applitools/selenium/visual_grid/resource_cache.rb +0 -69
- data/lib/applitools/selenium/visual_grid/running_test.rb +0 -271
- data/lib/applitools/selenium/visual_grid/thread_pool.rb +0 -95
- data/lib/applitools/selenium/visual_grid/vg_match_window_data.rb +0 -187
- data/lib/applitools/selenium/visual_grid/vg_region.rb +0 -16
- data/lib/applitools/selenium/visual_grid/vg_resource.rb +0 -77
- data/lib/applitools/selenium/visual_grid/vg_task.rb +0 -53
- data/lib/applitools/selenium/visual_grid/visual_grid_eyes.rb +0 -494
- data/lib/applitools/selenium/visual_grid/web_element_region.rb +0 -16
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require 'thread'
|
|
3
|
-
|
|
4
|
-
module Applitools
|
|
5
|
-
module Selenium
|
|
6
|
-
class VGThreadPool
|
|
7
|
-
extend Forwardable
|
|
8
|
-
attr_accessor :concurrency
|
|
9
|
-
def_delegator 'Applitools::EyesLogger', :logger
|
|
10
|
-
|
|
11
|
-
def initialize(concurrency = 10)
|
|
12
|
-
self.concurrency = concurrency
|
|
13
|
-
@stopped = true
|
|
14
|
-
@thread_group = ThreadGroup.new
|
|
15
|
-
@semaphore = Mutex.new
|
|
16
|
-
@next_task_block = nil
|
|
17
|
-
@watchdog = nil
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def on_next_task_needed(&block)
|
|
21
|
-
@next_task_block = block if block_given?
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def start
|
|
25
|
-
@semaphore.synchronize { @stopped = false }
|
|
26
|
-
init_or_renew_threads
|
|
27
|
-
@watchdog = Thread.new do
|
|
28
|
-
catch(:exit) do
|
|
29
|
-
loop do
|
|
30
|
-
throw :exit if stopped?
|
|
31
|
-
sleep 5
|
|
32
|
-
init_or_renew_threads
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def stop
|
|
39
|
-
@watchdog.exit
|
|
40
|
-
@semaphore.synchronize do
|
|
41
|
-
@stopped = true
|
|
42
|
-
end
|
|
43
|
-
@thread_group.list.each(&:join)
|
|
44
|
-
stopped?
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def stopped?
|
|
48
|
-
@semaphore.synchronize { @stopped }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def next_task
|
|
54
|
-
@semaphore.synchronize do
|
|
55
|
-
return @next_task_block.call if @next_task_block && @next_task_block.respond_to?(:call)
|
|
56
|
-
end
|
|
57
|
-
nil
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def init_or_renew_threads
|
|
61
|
-
one_concurrency = 1 # Thread's moved to universal server
|
|
62
|
-
(one_concurrency - @thread_group.list.count).times do
|
|
63
|
-
logger.debug 'starting new thread (task worker)'
|
|
64
|
-
next_thread
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def next_thread
|
|
69
|
-
thread = Thread.new do
|
|
70
|
-
begin
|
|
71
|
-
catch(:exit) do
|
|
72
|
-
loop do
|
|
73
|
-
throw :exit if stopped?
|
|
74
|
-
task_to_run = next_task
|
|
75
|
-
if task_to_run && task_to_run.respond_to?(:call)
|
|
76
|
-
logger.debug "Executing new task... #{task_to_run.name}"
|
|
77
|
-
task_to_run.call
|
|
78
|
-
logger.debug 'Done!'
|
|
79
|
-
else
|
|
80
|
-
sleep 0.5
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
Applitools::EyesLogger.logger.info 'Worker is stopped'
|
|
85
|
-
rescue => e
|
|
86
|
-
Applitools::EyesLogger.logger.error "Failed to execute task - #{task_to_run.name}"
|
|
87
|
-
Applitools::EyesLogger.logger.error e.message
|
|
88
|
-
Applitools::EyesLogger.logger.error e.backtrace.join(' ')
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
@thread_group.add thread
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Applitools
|
|
4
|
-
module Selenium
|
|
5
|
-
class VgMatchWindowData < Applitools::MatchWindowData
|
|
6
|
-
CONVERT_COORDINATES = proc do |region, selector_regions|
|
|
7
|
-
begin
|
|
8
|
-
offset_region = selector_regions.last
|
|
9
|
-
new_location = region.location.offset_negative(Applitools::Location.new(offset_region['x'].to_i, offset_region['y'].to_i))
|
|
10
|
-
region.location = new_location
|
|
11
|
-
rescue
|
|
12
|
-
Applitools::EyesLogger.error("Failed to convert coordinates for #{region}")
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
class RegionCoordinatesError < ::Applitools::EyesError
|
|
16
|
-
attr_accessor :region
|
|
17
|
-
def initialize(region, message)
|
|
18
|
-
super(message)
|
|
19
|
-
self.region = region
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
attr_accessor :target, :selector_regions
|
|
23
|
-
def read_target(target, driver, selector_regions)
|
|
24
|
-
self.target = target
|
|
25
|
-
self.selector_regions = selector_regions
|
|
26
|
-
# options
|
|
27
|
-
target_options_to_read.each do |field|
|
|
28
|
-
a_value = target.options[field.to_sym]
|
|
29
|
-
send("#{field}=", a_value) unless a_value.nil?
|
|
30
|
-
end
|
|
31
|
-
# ignored regions
|
|
32
|
-
if target.respond_to? :ignored_regions
|
|
33
|
-
target.ignored_regions.each do |r|
|
|
34
|
-
@need_convert_ignored_regions_coordinates = true unless @need_convert_ignored_regions_coordinates
|
|
35
|
-
case r
|
|
36
|
-
when Proc
|
|
37
|
-
region, padding_proc = r.call(driver, true)
|
|
38
|
-
region = selector_regions[target.regions[region]]
|
|
39
|
-
retrieved_region = Applitools::Region.new(region['x'], region['y'], region['width'], region['height'])
|
|
40
|
-
@ignored_regions << padding_proc.call(retrieved_region) if padding_proc.is_a? Proc
|
|
41
|
-
when Applitools::Region
|
|
42
|
-
@ignored_regions << r
|
|
43
|
-
when Applitools::Selenium::VGRegion
|
|
44
|
-
region = target.regions.key?(r.region) ? selector_regions[target.regions[r.region]] : r.region
|
|
45
|
-
raise RegionCoordinatesError.new(r, region['error']) if region['error']
|
|
46
|
-
retrieved_region = Applitools::Region.new(region['x'], region['y'], region['width'], region['height'])
|
|
47
|
-
@ignored_regions << if r.padding_proc.is_a?(Proc)
|
|
48
|
-
r.padding_proc.call(retrieved_region)
|
|
49
|
-
else
|
|
50
|
-
retrieved_region
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
if target.respond_to? :layout_regions
|
|
57
|
-
@layout_regions = obtain_regions_coordinates(target.layout_regions, driver)
|
|
58
|
-
@need_convert_layout_regions_coordinates = true unless @layout_regions.empty?
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
if target.respond_to? :content_regions
|
|
62
|
-
@content_regions = obtain_regions_coordinates(target.content_regions, driver)
|
|
63
|
-
@need_convert_content_regions_coordinates = true unless @content_regions.empty?
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
if target.respond_to? :strict_regions
|
|
67
|
-
@strict_regions = obtain_regions_coordinates(target.strict_regions, driver)
|
|
68
|
-
@need_convert_strict_regions_coordinates = true unless @strict_regions.empty?
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
if target.respond_to? :accessibility_regions
|
|
72
|
-
@accessibility_regions = obtain_regions_coordinates(target.accessibility_regions, driver)
|
|
73
|
-
@need_convert_accessibility_regions_coordinates = true unless @accessibility_regions.empty?
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# # floating regions
|
|
77
|
-
return unless target.respond_to? :floating_regions
|
|
78
|
-
target.floating_regions.each do |r|
|
|
79
|
-
case r
|
|
80
|
-
when Proc
|
|
81
|
-
region, padding_proc = r.call(driver, true)
|
|
82
|
-
region = selector_regions[target.regions[region]]
|
|
83
|
-
retrieved_region = Applitools::Region.new(region['x'], region['y'], region['width'], region['height'])
|
|
84
|
-
floating_region = padding_proc.call(retrieved_region) if padding_proc.is_a? Proc
|
|
85
|
-
raise Applitools::EyesError.new "Wrong floating region: #{region.class}" unless
|
|
86
|
-
floating_region.is_a? Applitools::FloatingRegion
|
|
87
|
-
@floating_regions << floating_region
|
|
88
|
-
@need_convert_floating_regions_coordinates = true
|
|
89
|
-
when Applitools::FloatingRegion
|
|
90
|
-
@floating_regions << r
|
|
91
|
-
@need_convert_floating_regions_coordinates = true
|
|
92
|
-
when Applitools::Selenium::VGRegion
|
|
93
|
-
region = r.region
|
|
94
|
-
region = selector_regions[target.regions[region]]
|
|
95
|
-
raise RegionCoordinatesError.new(r, region['error']) if region['error']
|
|
96
|
-
retrieved_region = Applitools::Region.new(region['x'], region['y'], region['width'], region['height'])
|
|
97
|
-
floating_region = r.padding_proc.call(retrieved_region) if r.padding_proc.is_a? Proc
|
|
98
|
-
raise Applitools::EyesError.new "Wrong floating region: #{region.class}" unless
|
|
99
|
-
floating_region.is_a? Applitools::FloatingRegion
|
|
100
|
-
@floating_regions << floating_region
|
|
101
|
-
@need_convert_floating_regions_coordinates = true
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def obtain_regions_coordinates(regions, driver)
|
|
107
|
-
result = []
|
|
108
|
-
regions.each do |r|
|
|
109
|
-
case r
|
|
110
|
-
when Proc
|
|
111
|
-
region = r.call(driver)
|
|
112
|
-
region = selector_regions[target.regions[region]]
|
|
113
|
-
result << Applitools::Region.new(region['x'], region['y'], region['width'], region['height'])
|
|
114
|
-
when Applitools::Region
|
|
115
|
-
result << r
|
|
116
|
-
when Applitools::Selenium::VGRegion
|
|
117
|
-
region = r.region
|
|
118
|
-
region = selector_regions[target.regions[region]]
|
|
119
|
-
raise RegionCoordinatesError.new(r, region['error']) if region['error']
|
|
120
|
-
retrieved_region = Applitools::Region.new(region['x'], region['y'], region['width'], region['height'])
|
|
121
|
-
result_region = if r.padding_proc.is_a? Proc
|
|
122
|
-
r.padding_proc.call(retrieved_region)
|
|
123
|
-
else
|
|
124
|
-
retrieved_region
|
|
125
|
-
end
|
|
126
|
-
result << result_region
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
result
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def convert_ignored_regions_coordinates
|
|
133
|
-
return unless @need_convert_ignored_regions_coordinates
|
|
134
|
-
if target.convert_coordinates_block.is_a?(Proc)
|
|
135
|
-
@ignored_regions.each { |r| target.convert_coordinates_block.call(r, selector_regions)}
|
|
136
|
-
end
|
|
137
|
-
self.ignored_regions = @ignored_regions.map(&:with_padding).map(&:to_hash)
|
|
138
|
-
@need_convert_ignored_regions_coordinates = false
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def convert_floating_regions_coordinates
|
|
142
|
-
return unless @need_convert_floating_regions_coordinates
|
|
143
|
-
if target.convert_coordinates_block.is_a?(Proc)
|
|
144
|
-
@floating_regions.each { |r| target.convert_coordinates_block.call(r, selector_regions)}
|
|
145
|
-
end
|
|
146
|
-
self.floating_regions = @floating_regions
|
|
147
|
-
@need_convert_floating_regions_coordinates = false
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def convert_layout_regions_coordinates
|
|
151
|
-
return unless @need_convert_layout_regions_coordinates
|
|
152
|
-
if target.convert_coordinates_block.is_a?(Proc)
|
|
153
|
-
@layout_regions.each { |r| target.convert_coordinates_block.call(r, selector_regions)}
|
|
154
|
-
end
|
|
155
|
-
self.layout_regions = @layout_regions
|
|
156
|
-
@need_convert_layout_regions_coordinates = false
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def convert_strict_regions_coordinates
|
|
160
|
-
return unless @need_convert_strict_regions_coordinates
|
|
161
|
-
if target.convert_coordinates_block.is_a?(Proc)
|
|
162
|
-
@strict_regions.each { |r| target.convert_coordinates_block.call(r, selector_regions)}
|
|
163
|
-
end
|
|
164
|
-
self.strict_regions = @strict_regions
|
|
165
|
-
@need_convert_strict_regions_coordinates = false
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def convert_content_regions_coordinates
|
|
169
|
-
return unless @need_convert_content_regions_coordinates
|
|
170
|
-
if target.convert_coordinates_block.is_a?(Proc)
|
|
171
|
-
@content_regions.each { |r| target.convert_coordinates_block.call(r, selector_regions)}
|
|
172
|
-
end
|
|
173
|
-
self.content_regions = @content_regions
|
|
174
|
-
@need_convert_content_regions_coordinates = false
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def convert_accessibility_regions_coordinates
|
|
178
|
-
return unless @need_convert_accessibility_regions_coordinates
|
|
179
|
-
if target.convert_coordinates_block.is_a?(Proc)
|
|
180
|
-
@accessibility_regions.each { |r| target.convert_coordinates_block.call(r, selector_regions)}
|
|
181
|
-
end
|
|
182
|
-
self.accessibility_regions = @accessibility_regions
|
|
183
|
-
@need_convert_accessibility_regions_coordinates = false
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
end
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
module Applitools
|
|
3
|
-
module Selenium
|
|
4
|
-
class VGRegion
|
|
5
|
-
attr_accessor :region, :padding_proc
|
|
6
|
-
def initialize(region, padding_proc)
|
|
7
|
-
self.region = region
|
|
8
|
-
self.padding_proc = padding_proc
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def to_s
|
|
12
|
-
region.inspect
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'base64'
|
|
4
|
-
require 'digest'
|
|
5
|
-
# require 'nokogiri'
|
|
6
|
-
|
|
7
|
-
module Applitools
|
|
8
|
-
module Selenium
|
|
9
|
-
class VGResource
|
|
10
|
-
include Applitools::Jsonable
|
|
11
|
-
json_fields :contentType, :hash, :hashFormat, :errorStatusCode
|
|
12
|
-
attr_accessor :url, :content, :handle_discovered_resources_block
|
|
13
|
-
alias content_type contentType
|
|
14
|
-
alias content_type= contentType=
|
|
15
|
-
|
|
16
|
-
class << self
|
|
17
|
-
def parse_blob_from_script(blob, options = {})
|
|
18
|
-
return new(
|
|
19
|
-
blob['url'],
|
|
20
|
-
"application/X-error-response-#{blob['errorStatusCode']}",
|
|
21
|
-
blob['value'] || ''
|
|
22
|
-
).tap {|r| r.error_status_code = blob['errorStatusCode']} if blob['errorStatusCode']
|
|
23
|
-
content = Base64.decode64(blob['value'])
|
|
24
|
-
new(blob['url'], blob['type'], content, options)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def parse_response(url, response, options = {})
|
|
28
|
-
return new(url, 'application/empty-response', '') unless response.status == 200
|
|
29
|
-
new(url, response.headers['Content-Type'], response.body, options)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def initialize(url, content_type, content, options = {})
|
|
34
|
-
self.handle_discovered_resources_block = options[:on_resources_fetched] if
|
|
35
|
-
options[:on_resources_fetched].is_a? Proc
|
|
36
|
-
self.url = URI(url)
|
|
37
|
-
self.content_type = content_type
|
|
38
|
-
self.content = content
|
|
39
|
-
self.hash = Digest::SHA256.hexdigest(content)
|
|
40
|
-
self.hashFormat = 'sha256'
|
|
41
|
-
lookup_for_resources
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def on_resources_fetched(block)
|
|
45
|
-
self.handle_discovered_resources_block = block
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def lookup_for_resources
|
|
49
|
-
lookup_for_css_resources
|
|
50
|
-
lookup_for_svg_resources
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def lookup_for_css_resources
|
|
54
|
-
return unless %r{^text/css}i =~ content_type && handle_discovered_resources_block
|
|
55
|
-
parser = Applitools::Selenium::CssParser::FindEmbeddedResources.new(content)
|
|
56
|
-
handle_discovered_resources_block.call(parser.imported_css + parser.fonts + parser.images, url)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def lookup_for_svg_resources
|
|
60
|
-
return unless %r{^image/svg\+xml} =~ content_type && handle_discovered_resources_block
|
|
61
|
-
attrs = []
|
|
62
|
-
# attrs = Nokogiri::XML(content)
|
|
63
|
-
# .xpath("//@*[namespace-uri(.) = 'http://www.w3.org/1999/xlink'] | //@href")
|
|
64
|
-
# .select { |a| a.name == 'href' }
|
|
65
|
-
# .map(&:value)
|
|
66
|
-
# .select { |a| /^(?!#).*/.match(a) }
|
|
67
|
-
handle_discovered_resources_block.call(attrs, url)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def stringify
|
|
71
|
-
url.to_s + content_type.to_s + hash
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
private :lookup_for_svg_resources, :lookup_for_css_resources
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require 'securerandom'
|
|
3
|
-
module Applitools
|
|
4
|
-
module Selenium
|
|
5
|
-
class VGTask
|
|
6
|
-
attr_accessor :name, :uuid
|
|
7
|
-
def initialize(name, &block)
|
|
8
|
-
self.name = name
|
|
9
|
-
@block_to_run = block if block_given?
|
|
10
|
-
@callback = []
|
|
11
|
-
@error_callback = []
|
|
12
|
-
@completed_callback = []
|
|
13
|
-
self.uuid = SecureRandom.uuid
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def on_task_succeeded(&block)
|
|
17
|
-
@callback.push block if block_given?
|
|
18
|
-
self
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def on_task_error(&block)
|
|
22
|
-
@error_callback.push block if block_given?
|
|
23
|
-
self
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def on_task_completed(&block)
|
|
27
|
-
@completed_callback.push block if block_given?
|
|
28
|
-
self
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def call
|
|
32
|
-
return unless @block_to_run.respond_to? :call
|
|
33
|
-
begin
|
|
34
|
-
res = @block_to_run.call
|
|
35
|
-
@callback.each do |cb|
|
|
36
|
-
cb.call(res) if cb.respond_to? :call
|
|
37
|
-
end
|
|
38
|
-
rescue StandardError => e
|
|
39
|
-
Applitools::EyesLogger.logger.error 'Failed to execute task!'
|
|
40
|
-
Applitools::EyesLogger.logger.error e.message
|
|
41
|
-
Applitools::EyesLogger.logger.error e.backtrace.join('\n\t')
|
|
42
|
-
@error_callback.each do |ecb|
|
|
43
|
-
ecb.call(e) if ecb.respond_to? :call
|
|
44
|
-
end
|
|
45
|
-
ensure
|
|
46
|
-
@completed_callback.each do |ccb|
|
|
47
|
-
ccb.call if ccb.respond_to? :call
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|