scarpe-components 0.1.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +22 -0
- data/lib/scarpe/components/base64.rb +29 -0
- data/lib/scarpe/components/file_helpers.rb +65 -0
- data/lib/scarpe/{logger.rb → components/modular_logger.rb} +7 -2
- data/lib/scarpe/components/print_logger.rb +43 -0
- data/lib/scarpe/components/promises.rb +454 -0
- data/lib/scarpe/components/segmented_file_loader.rb +170 -0
- data/lib/scarpe/components/unit_test_helpers.rb +217 -0
- data/lib/scarpe/components/version.rb +7 -0
- metadata +12 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7067a7b6f0e71d041289334dccf5c0edaae3db689a710450c1d8f585888f74d
|
4
|
+
data.tar.gz: c73105b84ae8468779ca93c216e7f618213a79086e2c0d7a7470bdece885c2b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d0be001e64c94d3e82dcfc7dca65cbfe373edfd219905716075cfcb3655b0da9a9fec76cf2323cec7d151b83581b3762d7b7c81d51ba2b34dcc10451b33c4db
|
7
|
+
data.tar.gz: 78a734af216bf4c7275483258d20508a985782442a1b6700632905d26b6dfccf0a66ca3c752202b41f32893094450681fdfae7e0d832ee08ba43812b300b8427
|
data/Gemfile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gemspec
|
6
|
+
|
7
|
+
gem "lacci", path: "../lacci"
|
8
|
+
|
9
|
+
gem "rake", "~> 13.0"
|
10
|
+
|
11
|
+
gem "nokogiri"
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem "minitest", "~> 5.0"
|
15
|
+
gem "minitest-reporters"
|
16
|
+
end
|
17
|
+
|
18
|
+
group :development do
|
19
|
+
gem "debug"
|
20
|
+
gem "rubocop", "~> 1.21"
|
21
|
+
gem "rubocop-shopify"
|
22
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
class Scarpe; end
|
7
|
+
module Scarpe::Components; end
|
8
|
+
class Scarpe
|
9
|
+
module Components::Base64
|
10
|
+
def valid_url?(string)
|
11
|
+
uri = URI.parse(string)
|
12
|
+
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
13
|
+
rescue URI::InvalidURIError, URI::BadURIError
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def encode_file_to_base64(image_filename)
|
18
|
+
directory_path = File.dirname(__FILE__, 5)
|
19
|
+
|
20
|
+
image_path = File.join(directory_path, image_filename)
|
21
|
+
|
22
|
+
image_data = File.binread(image_path)
|
23
|
+
|
24
|
+
encoded_data = ::Base64.strict_encode64(image_data)
|
25
|
+
|
26
|
+
encoded_data
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
|
5
|
+
# These can be used for unit tests, but also more generally.
|
6
|
+
|
7
|
+
module Scarpe::Components::FileHelpers
|
8
|
+
# Create a temporary file with the given prefix and contents.
|
9
|
+
# Execute the block of code with it in place. Make sure
|
10
|
+
# it gets cleaned up afterward.
|
11
|
+
#
|
12
|
+
# @param prefix [String] the prefix passed to Tempfile to identify this file on disk
|
13
|
+
# @param contents [String] the file contents that should be written to Tempfile
|
14
|
+
# @param dir [String] the directory to create the tempfile in
|
15
|
+
# @yield The code to execute with the tempfile present
|
16
|
+
# @yieldparam the path of the new tempfile
|
17
|
+
def with_tempfile(prefix, contents, dir: Dir.tmpdir)
|
18
|
+
t = Tempfile.new(prefix, dir)
|
19
|
+
t.write(contents)
|
20
|
+
t.flush # Make sure the contents are written out
|
21
|
+
|
22
|
+
yield(t.path)
|
23
|
+
ensure
|
24
|
+
t.close
|
25
|
+
t.unlink
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create multiple tempfiles, with given contents, in given
|
29
|
+
# directories, and execute the block in that context.
|
30
|
+
# When the block is finished, make sure all tempfiles are
|
31
|
+
# deleted.
|
32
|
+
#
|
33
|
+
# Pass an array of arrays, where each array is of the form:
|
34
|
+
# [prefix, contents, (optional)dir]
|
35
|
+
#
|
36
|
+
# I don't love inlining with_tempfile's contents into here.
|
37
|
+
# But calling it iteratively or recursively was difficult
|
38
|
+
# when I tried it the obvious ways.
|
39
|
+
#
|
40
|
+
# This method should be equivalent to calling with_tempfile
|
41
|
+
# once for each entry in the array, in a set of nested
|
42
|
+
# blocks.
|
43
|
+
#
|
44
|
+
# @param tf_specs [Array<Array>] The array of tempfile prefixes, contents and directories
|
45
|
+
# @yield The code to execute with those tempfiles present
|
46
|
+
# @yieldparam An array of paths to tempfiles, in the same order as tf_specs
|
47
|
+
def with_tempfiles(tf_specs, &block)
|
48
|
+
tempfiles = []
|
49
|
+
tf_specs.each do |prefix, contents, dir|
|
50
|
+
dir ||= Dir.tmpdir
|
51
|
+
t = Tempfile.new(prefix, dir)
|
52
|
+
tempfiles << t
|
53
|
+
t.write(contents)
|
54
|
+
t.flush # Make sure the contents are written out
|
55
|
+
end
|
56
|
+
|
57
|
+
args = tempfiles.map(&:path)
|
58
|
+
yield(args)
|
59
|
+
ensure
|
60
|
+
tempfiles.each do |t|
|
61
|
+
t.close
|
62
|
+
t.unlink
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -5,10 +5,12 @@ require "json"
|
|
5
5
|
|
6
6
|
require "shoes/log"
|
7
7
|
|
8
|
-
#
|
8
|
+
# Requires the logging gem
|
9
9
|
|
10
|
+
class Scarpe; end
|
11
|
+
module Scarpe::Components; end
|
10
12
|
class Scarpe
|
11
|
-
class
|
13
|
+
class Components::ModularLogImpl
|
12
14
|
include Shoes::Log # for constants
|
13
15
|
|
14
16
|
def logger_for_component(component)
|
@@ -106,3 +108,6 @@ class Scarpe
|
|
106
108
|
end
|
107
109
|
end
|
108
110
|
end
|
111
|
+
|
112
|
+
#Shoes::Log.instance = Scarpe::Components::ModularLogImpl.new
|
113
|
+
#Shoes::Log.configure_logger(Shoes::Log::DEFAULT_LOG_CONFIG)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shoes/log"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
class Scarpe; end
|
7
|
+
module Scarpe::Components; end
|
8
|
+
class Scarpe::Components::PrintLogImpl
|
9
|
+
include Shoes::Log # for constants
|
10
|
+
|
11
|
+
class PrintLogger
|
12
|
+
def initialize(component_name)
|
13
|
+
@comp_name = component_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def error(msg)
|
17
|
+
puts "#{@comp_name} error: #{msg}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def warn(msg)
|
21
|
+
puts "#{@comp_name} warn: #{msg}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def debug(msg)
|
25
|
+
puts "#{@comp_name} debug: #{msg}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def info(msg)
|
29
|
+
puts "#{@comp_name} info: #{msg}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def logger_for_component(component)
|
34
|
+
PrintLogger.new(component.to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
def configure_logger(log_config)
|
38
|
+
# For now, ignore
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
#Shoes::Log.instance = Scarpe::PrintLogImpl.new
|
43
|
+
#Shoes::Log.configure_logger(Shoes::Log::DEFAULT_LOG_CONFIG)
|
@@ -0,0 +1,454 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Scarpe; end
|
4
|
+
module Scarpe::Components; end
|
5
|
+
class Scarpe
|
6
|
+
# Scarpe::Promise is a promises library, but one with no form of built-in
|
7
|
+
# concurrency. Instead, promise callbacks are executed synchronously.
|
8
|
+
# Even execution is usually synchronous, but can also be handled manually
|
9
|
+
# for forms of execution not controlled in Ruby (like Webview.)
|
10
|
+
#
|
11
|
+
# Funny thing... We need promises as an API concept since we have a JS event
|
12
|
+
# loop doing its thing, and we need to respond to actions that it takes.
|
13
|
+
# But there's not really a Ruby implementation of Promises *without* an
|
14
|
+
# attached form of concurrency. So here we are, writing our own :-/
|
15
|
+
#
|
16
|
+
# In theory you could probably write some kind of "no-op thread pool"
|
17
|
+
# for the ruby-concurrency gem, pass it manually to every promise we
|
18
|
+
# created and then raise an exception any time we tried to do something
|
19
|
+
# in the background. That's probably more code than writing our own, though,
|
20
|
+
# and we'd be fighting it constantly.
|
21
|
+
#
|
22
|
+
# This class is inspired by concurrent-ruby [Promise](https://ruby-concurrency.github.io/concurrent-ruby/1.1.5/Concurrent/Promise.html)
|
23
|
+
# which is inspired by Javascript Promises, which is what we actually need
|
24
|
+
# for our use case. We can't easily tell when our WebView begins processing
|
25
|
+
# our request, which removes the :processing state. This can be used for
|
26
|
+
# executing JS, but also generally waiting on events.
|
27
|
+
#
|
28
|
+
# We don't fully control ordering here, so it *is* conceivable that a
|
29
|
+
# child waiting on a parent can be randomly fulfilled, even if we didn't
|
30
|
+
# expect it. We don't consider that an error. Similarly, we'll call
|
31
|
+
# on_scheduled callbacks if a promise is fulfilled, even though we
|
32
|
+
# never explicitly scheduled it. If a promise is *rejected* without
|
33
|
+
# ever being scheduled, we won't call those callbacks.
|
34
|
+
class Promise
|
35
|
+
include Shoes::Log
|
36
|
+
|
37
|
+
# The unscheduled promise state means it's waiting on a parent promise that
|
38
|
+
# hasn't completed yet. The pending state means it's waiting to execute.
|
39
|
+
# Fulfilled means it has completed successfully and returned a value,
|
40
|
+
# while rejected means it has failed, normally producing a reason.
|
41
|
+
PROMISE_STATES = [:unscheduled, :pending, :fulfilled, :rejected]
|
42
|
+
|
43
|
+
# The state of the promise, which should be one of PROMISE_STATES
|
44
|
+
attr_reader :state
|
45
|
+
|
46
|
+
# The parent promises of this promise, sometimes an empty array
|
47
|
+
attr_reader :parents
|
48
|
+
|
49
|
+
# If the promise is fulfilled, this is the value returned
|
50
|
+
attr_reader :returned_value
|
51
|
+
|
52
|
+
# If the promise is rejected, this is the reason, sometimes an exception
|
53
|
+
attr_reader :reason
|
54
|
+
|
55
|
+
# Create a promise and then instantly fulfill it.
|
56
|
+
def self.fulfilled(return_val = nil, parents: [], &block)
|
57
|
+
p = Promise.new(parents: parents, &block)
|
58
|
+
p.fulfilled!(return_val)
|
59
|
+
p
|
60
|
+
end
|
61
|
+
|
62
|
+
# Create a promise and then instantly reject it.
|
63
|
+
def self.rejected(reason = nil, parents: [])
|
64
|
+
p = Promise.new(parents: parents)
|
65
|
+
p.rejected!(reason)
|
66
|
+
p
|
67
|
+
end
|
68
|
+
|
69
|
+
# Fulfill the promise, setting the returned_value to value
|
70
|
+
def fulfilled!(value = nil)
|
71
|
+
set_state(:fulfilled, value)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Reject the promise, setting the reason to reason
|
75
|
+
def rejected!(reason = nil)
|
76
|
+
set_state(:rejected, reason)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Create a new promise with this promise as a parent. It runs the
|
80
|
+
# specified code in block when scheduled.
|
81
|
+
def then(&block)
|
82
|
+
Promise.new(parents: [self], &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# The Promise.new method, along with all the various handlers,
|
86
|
+
# are pretty raw. They'll do what promises do, but they're not
|
87
|
+
# the prettiest. However, they ensure that guarantees are made
|
88
|
+
# and so on, so they're great as plumbing under the syntactic
|
89
|
+
# sugar above.
|
90
|
+
#
|
91
|
+
# Note that the state passed in may not be the actual initial
|
92
|
+
# state. If a parent is rejected, the state will become
|
93
|
+
# rejected. If no parents are waiting or failed then a state
|
94
|
+
# of nil or :unscheduled will become :pending.
|
95
|
+
#
|
96
|
+
# @param state [Symbol] One of PROMISE_STATES for the initial state
|
97
|
+
# @param parents [Array] A list of promises that must be fulfilled before this one is scheduled
|
98
|
+
# @yield A block that executes when this promise is scheduled - when its parents, if any, are all fulfilled
|
99
|
+
def initialize(state: nil, parents: [], &scheduler)
|
100
|
+
log_init("Promise")
|
101
|
+
|
102
|
+
# These are as initially specified, and effectively immutable
|
103
|
+
@state = state
|
104
|
+
@parents = parents
|
105
|
+
|
106
|
+
# These are what we're waiting on, and will be
|
107
|
+
# removed as time goes forward.
|
108
|
+
@waiting_on = parents.select { |p| !p.complete? }
|
109
|
+
@on_fulfilled = []
|
110
|
+
@on_rejected = []
|
111
|
+
@on_scheduled = []
|
112
|
+
@scheduler = scheduler
|
113
|
+
@executor = nil
|
114
|
+
@returned_value = nil
|
115
|
+
@reason = nil
|
116
|
+
|
117
|
+
if complete?
|
118
|
+
# Did we start out already fulfilled or rejected?
|
119
|
+
# If so, we can skip a lot of fiddly checking.
|
120
|
+
# Don't need a scheduler or to care about parents
|
121
|
+
# or anything.
|
122
|
+
@waiting_on = []
|
123
|
+
@scheduler = nil
|
124
|
+
elsif @parents.any? { |p| p.state == :rejected }
|
125
|
+
@state = :rejected
|
126
|
+
@waiting_on = []
|
127
|
+
@scheduler = nil
|
128
|
+
elsif @state == :pending
|
129
|
+
# Did we get an explicit :pending? Then we don't need
|
130
|
+
# to schedule ourselves, or care about the scheduler
|
131
|
+
# in general.
|
132
|
+
@scheduler = nil
|
133
|
+
elsif @state.nil? || @state == :unscheduled
|
134
|
+
# If no state was given or we're unscheduled, we'll
|
135
|
+
# wait until our parents have all completed to
|
136
|
+
# schedule ourselves.
|
137
|
+
|
138
|
+
if @waiting_on.empty?
|
139
|
+
# No further dependencies, we can schedule ourselves
|
140
|
+
@state = :pending
|
141
|
+
|
142
|
+
# We have no on_scheduled handlers yet, but this will
|
143
|
+
# call and clear the scheduler.
|
144
|
+
call_handlers_for(:pending)
|
145
|
+
else
|
146
|
+
# We're still waiting on somebody, no scheduling yet
|
147
|
+
@state = :unscheduled
|
148
|
+
@waiting_on.each do |dep|
|
149
|
+
dep.on_fulfilled { parent_fulfilled!(dep) }
|
150
|
+
dep.on_rejected { parent_rejected!(dep) }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Return true if the Promise is either fulfilled or rejected.
|
157
|
+
#
|
158
|
+
# @return [Boolean] true if the promise is fulfilled or rejected
|
159
|
+
def complete?
|
160
|
+
@state == :fulfilled || @state == :rejected
|
161
|
+
end
|
162
|
+
|
163
|
+
# Return true if the promise is already fulfilled.
|
164
|
+
#
|
165
|
+
# @return [Boolean] true if the promise is fulfilled
|
166
|
+
def fulfilled?
|
167
|
+
@state == :fulfilled
|
168
|
+
end
|
169
|
+
|
170
|
+
# Return true if the promise is already rejected.
|
171
|
+
#
|
172
|
+
# @return [Boolean] true if the promise is rejected
|
173
|
+
def rejected?
|
174
|
+
@state == :rejected
|
175
|
+
end
|
176
|
+
|
177
|
+
# An inspect method to give slightly smaller output, for ease of reading in irb
|
178
|
+
def inspect
|
179
|
+
"#<Scarpe::Promise:#{object_id} " +
|
180
|
+
"@state=#{@state.inspect} @parents=#{@parents.inspect} " +
|
181
|
+
"@waiting_on=#{@waiting_on.inspect} @on_fulfilled=#{@on_fulfilled.size} " +
|
182
|
+
"@on_rejected=#{@on_rejected.size} @on_scheduled=#{@on_scheduled.size} " +
|
183
|
+
"@scheduler=#{@scheduler ? "Y" : "N"} @executor=#{@executor ? "Y" : "N"} " +
|
184
|
+
"@returned_value=#{@returned_value.inspect} @reason=#{@reason.inspect}" +
|
185
|
+
">"
|
186
|
+
end
|
187
|
+
|
188
|
+
# These promises are mostly designed for external execution.
|
189
|
+
# You could put together your own thread-pool, or use RPC,
|
190
|
+
# a WebView, a database or similar source of external calculation.
|
191
|
+
# But in many cases it's reasonable to execute locally.
|
192
|
+
# In those cases, you can register an executor which will be
|
193
|
+
# called when the promise is ready to execute but has not yet
|
194
|
+
# done so. Registering an executor on a promise that is
|
195
|
+
# already fulfilled is an error. Registering an executor on
|
196
|
+
# a promise that has already rejected is a no-op.
|
197
|
+
def to_execute(&block)
|
198
|
+
case @state
|
199
|
+
when :fulfilled
|
200
|
+
# Should this be a no-op instead?
|
201
|
+
raise "Registering an executor on an already fulfilled promise means it will never run!"
|
202
|
+
when :rejected
|
203
|
+
return
|
204
|
+
when :unscheduled
|
205
|
+
@executor = block # save for later
|
206
|
+
when :pending
|
207
|
+
@executor = block
|
208
|
+
call_executor
|
209
|
+
else
|
210
|
+
raise "Internal error, illegal state!"
|
211
|
+
end
|
212
|
+
|
213
|
+
self
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
# set_state looks at the old and new states of the promise. It calls handlers and updates tracking
|
219
|
+
# data accordingly.
|
220
|
+
def set_state(new_state, value_or_reason = nil)
|
221
|
+
old_state = @state
|
222
|
+
|
223
|
+
# First, filter out illegal input
|
224
|
+
unless PROMISE_STATES.include?(old_state)
|
225
|
+
raise "Internal Promise error! Internal state was #{old_state.inspect}! Legal states: #{PROMISE_STATES.inspect}"
|
226
|
+
end
|
227
|
+
|
228
|
+
unless PROMISE_STATES.include?(new_state)
|
229
|
+
raise "Internal Promise error! Internal state was set to #{new_state.inspect}! " +
|
230
|
+
"Legal states: #{PROMISE_STATES.inspect}"
|
231
|
+
end
|
232
|
+
|
233
|
+
if new_state != :fulfilled && new_state != :rejected && !value_or_reason.nil?
|
234
|
+
raise "Internal promise error! Non-completed state transitions should not specify a value or reason!"
|
235
|
+
end
|
236
|
+
|
237
|
+
# Here's our state-transition grid for what we're doing here.
|
238
|
+
# "From" state is on the left, "to" state is on top.
|
239
|
+
#
|
240
|
+
# U P F R
|
241
|
+
#
|
242
|
+
# U - 1 . .
|
243
|
+
# P X - . .
|
244
|
+
# F X X - X
|
245
|
+
# R X X X -
|
246
|
+
#
|
247
|
+
# - Change from same to same, no effect
|
248
|
+
# X Illegal for one reason or another, raise error
|
249
|
+
# . Great, no problem, run handlers but not @scheduler or @executor
|
250
|
+
# 1 Interesting case - if we have an executor, actually change to a *different* state instead
|
251
|
+
|
252
|
+
# Transitioning from our state to our same state? No-op.
|
253
|
+
return if new_state == old_state
|
254
|
+
|
255
|
+
# Transitioning to any *different* state after being fulfilled or rejected? Nope. Those states are final.
|
256
|
+
if complete?
|
257
|
+
raise "Internal Promise error! Trying to change state from #{old_state.inspect} to #{new_state.inspect}!"
|
258
|
+
end
|
259
|
+
|
260
|
+
if old_state == :pending && new_state == :unscheduled
|
261
|
+
raise "Can't change state from :pending to :unscheduled! Scheduling is not reversible!"
|
262
|
+
end
|
263
|
+
|
264
|
+
# The next three checks should all be followed by calling handlers for the newly-changed state.
|
265
|
+
# See call_handlers_for below.
|
266
|
+
|
267
|
+
# Okay, we're getting scheduled.
|
268
|
+
if old_state == :unscheduled && new_state == :pending
|
269
|
+
@state = new_state
|
270
|
+
call_handlers_for(new_state)
|
271
|
+
|
272
|
+
# It's not impossible for the scheduler to do something that fulfills or rejects the promise.
|
273
|
+
# In that case it *also* called the appropriate handlers. Let's get out of here.
|
274
|
+
return if @state == :fulfilled || @state == :rejected
|
275
|
+
|
276
|
+
if @executor
|
277
|
+
# In this case we're still pending, but we have a synchronous executor. Let's do it.
|
278
|
+
call_executor
|
279
|
+
end
|
280
|
+
|
281
|
+
return
|
282
|
+
end
|
283
|
+
|
284
|
+
# Setting to rejected calls the rejected handlers. But no scheduling ever occurs, so on_scheduled handlers
|
285
|
+
# will never be called.
|
286
|
+
if new_state == :rejected
|
287
|
+
@state = :rejected
|
288
|
+
@reason = value_or_reason
|
289
|
+
call_handlers_for(new_state)
|
290
|
+
end
|
291
|
+
|
292
|
+
# If we go straight from :unscheduled to :fulfilled we *will* run the on_scheduled callbacks,
|
293
|
+
# because we pretend the scheduling *did* occur at some point. Normally that'll be no callbacks,
|
294
|
+
# of course.
|
295
|
+
#
|
296
|
+
# Fun-but-unfortunate trivia: you *can* fulfill a promise before all its parents are fulfilled.
|
297
|
+
# If you do, the unfinished parents will result in nil arguments to the on_fulfilled handler,
|
298
|
+
# because we have no other value to provide. The scheduler callback will never be called, but
|
299
|
+
# the on_scheduled callbacks, if any, will be.
|
300
|
+
if new_state == :fulfilled
|
301
|
+
@state = :fulfilled
|
302
|
+
@returned_value = value_or_reason
|
303
|
+
call_handlers_for(new_state)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# This private method calls handlers for a new state, removing those handlers
|
308
|
+
# since they have now been called. This interacts subtly with set_state()
|
309
|
+
# above, particularly in the case of fulfilling a promise without it ever being
|
310
|
+
# properly scheduled.
|
311
|
+
#
|
312
|
+
# The rejected handlers will be cleared if the promise is fulfilled and vice-versa.
|
313
|
+
# After rejection, no on_fulfilled handler should ever be called and vice-versa.
|
314
|
+
#
|
315
|
+
# When we go from :unscheduled to :pending, the scheduler, if any, should be
|
316
|
+
# called and cleared. That should *not* happen when going from :unscheduled to
|
317
|
+
# :fulfilled.
|
318
|
+
def call_handlers_for(state)
|
319
|
+
case state
|
320
|
+
when :fulfilled
|
321
|
+
@on_scheduled.each { |h| h.call(*@parents.map(&:returned_value)) }
|
322
|
+
@on_fulfilled.each { |h| h.call(*@parents.map(&:returned_value)) }
|
323
|
+
@on_scheduled = @on_rejected = @on_fulfilled = []
|
324
|
+
@scheduler = @executor = nil
|
325
|
+
when :rejected
|
326
|
+
@on_rejected.each { |h| h.call(*@parents.map(&:returned_value)) }
|
327
|
+
@on_fulfilled = @on_scheduled = @on_rejected = []
|
328
|
+
@scheduler = @executor = nil
|
329
|
+
when :pending
|
330
|
+
# A scheduler can get an exception. If so, treat it as rejection
|
331
|
+
# and the exception as the provided reason.
|
332
|
+
if @scheduler
|
333
|
+
begin
|
334
|
+
@scheduler.call(*@parents.map(&:returned_value))
|
335
|
+
rescue => e
|
336
|
+
@log.error("Error while running scheduler! #{e.full_message}")
|
337
|
+
rejected!(e)
|
338
|
+
end
|
339
|
+
@scheduler = nil
|
340
|
+
end
|
341
|
+
@on_scheduled.each { |h| h.call(*@parents.map(&:returned_value)) }
|
342
|
+
@on_scheduled = []
|
343
|
+
else
|
344
|
+
raise "Internal error! Trying to call handlers for #{state.inspect}!"
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def parent_fulfilled!(parent)
|
349
|
+
@waiting_on.delete(parent)
|
350
|
+
|
351
|
+
# Last parent? If so, schedule ourselves.
|
352
|
+
if @waiting_on.empty? && !self.complete?
|
353
|
+
# This will result in :pending if there's no executor,
|
354
|
+
# or fulfilled/rejected if there is an executor.
|
355
|
+
set_state(:pending)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def parent_rejected!(parent)
|
360
|
+
@waiting_on = []
|
361
|
+
|
362
|
+
unless self.complete?
|
363
|
+
# If our parent was rejected and we were waiting on them,
|
364
|
+
# now we're rejected too.
|
365
|
+
set_state(:rejected)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def call_executor
|
370
|
+
raise("Internal error! Should not call_executor with no executor!") unless @executor
|
371
|
+
|
372
|
+
begin
|
373
|
+
result = @executor.call(*@parents.map(&:returned_value))
|
374
|
+
fulfilled!(result)
|
375
|
+
rescue => e
|
376
|
+
@log.error("Error running executor! #{e.full_message}")
|
377
|
+
rejected!(e)
|
378
|
+
end
|
379
|
+
ensure
|
380
|
+
@executor = nil
|
381
|
+
end
|
382
|
+
|
383
|
+
public
|
384
|
+
|
385
|
+
# Register a handler to be called when the promise is fulfilled.
|
386
|
+
# If called on a fulfilled promise, the handler will be called immediately.
|
387
|
+
#
|
388
|
+
# @yield Handler to be called on fulfilled
|
389
|
+
# @return [Scarpe::Promise] self
|
390
|
+
def on_fulfilled(&handler)
|
391
|
+
unless handler
|
392
|
+
raise "You must pass a block to on_fulfilled!"
|
393
|
+
end
|
394
|
+
|
395
|
+
case @state
|
396
|
+
when :fulfilled
|
397
|
+
handler.call(*@parents.map(&:returned_value))
|
398
|
+
when :pending, :unscheduled
|
399
|
+
@on_fulfilled << handler
|
400
|
+
when :rejected
|
401
|
+
# Do nothing
|
402
|
+
end
|
403
|
+
|
404
|
+
self
|
405
|
+
end
|
406
|
+
|
407
|
+
# Register a handler to be called when the promise is rejected.
|
408
|
+
# If called on a rejected promise, the handler will be called immediately.
|
409
|
+
#
|
410
|
+
# @yield Handler to be called on rejected
|
411
|
+
# @return [Scarpe::Promise] self
|
412
|
+
def on_rejected(&handler)
|
413
|
+
unless handler
|
414
|
+
raise "You must pass a block to on_rejected!"
|
415
|
+
end
|
416
|
+
|
417
|
+
case @state
|
418
|
+
when :rejected
|
419
|
+
handler.call(*@parents.map(&:returned_value))
|
420
|
+
when :pending, :unscheduled
|
421
|
+
@on_rejected << handler
|
422
|
+
when :fulfilled
|
423
|
+
# Do nothing
|
424
|
+
end
|
425
|
+
|
426
|
+
self
|
427
|
+
end
|
428
|
+
|
429
|
+
# Register a handler to be called when the promise is scheduled.
|
430
|
+
# If called on a promise that was scheduled earlier, the handler
|
431
|
+
# will be called immediately.
|
432
|
+
#
|
433
|
+
# @yield Handler to be called on scheduled
|
434
|
+
# @return [Scarpe::Promise] self
|
435
|
+
def on_scheduled(&handler)
|
436
|
+
unless handler
|
437
|
+
raise "You must pass a block to on_scheduled!"
|
438
|
+
end
|
439
|
+
|
440
|
+
# Add a pending handler or call it now
|
441
|
+
case @state
|
442
|
+
when :fulfilled, :pending
|
443
|
+
handler.call(*@parents.map(&:returned_value))
|
444
|
+
when :unscheduled
|
445
|
+
@on_scheduled << handler
|
446
|
+
when :rejected
|
447
|
+
# Do nothing
|
448
|
+
end
|
449
|
+
|
450
|
+
self
|
451
|
+
end
|
452
|
+
end
|
453
|
+
Components::Promise = Promise
|
454
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "scarpe/components/file_helpers"
|
4
|
+
|
5
|
+
module Scarpe::Components
|
6
|
+
class SegmentedFileLoader
|
7
|
+
include Scarpe::Components::FileHelpers
|
8
|
+
|
9
|
+
# Add a new segment type (e.g. "catscradle") with a different
|
10
|
+
# file handler.
|
11
|
+
#
|
12
|
+
# @param type [String] the new name for this segment type
|
13
|
+
# @param handler [Object] an object that will be called as obj.call(filename) - often a proc
|
14
|
+
# @return <void>
|
15
|
+
def add_segment_type(type, handler)
|
16
|
+
if segment_type_hash.key?(type)
|
17
|
+
raise "Segment type #{type.inspect} already exists!"
|
18
|
+
end
|
19
|
+
|
20
|
+
segment_type_hash[type] = handler
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return an Array of segment type labels, such as "code" and "app_test".
|
24
|
+
#
|
25
|
+
# @return Array<String> the segment type labels
|
26
|
+
def segment_types
|
27
|
+
segment_type_hash.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
# Load a .sca file with an optional YAML frontmatter prefix and
|
31
|
+
# multiple file sections which can be treated differently.
|
32
|
+
#
|
33
|
+
# The file loader acts like a proc, being called with .call()
|
34
|
+
# and returning true or false for whether it has handled the
|
35
|
+
# file load. This allows chaining loaders in order and the
|
36
|
+
# first loader to recognise a file will run it.
|
37
|
+
#
|
38
|
+
# @param path [String] the file or directory to treat as a Scarpe app
|
39
|
+
# @return [Boolean] return true if the file is loaded as a segmented Scarpe app file
|
40
|
+
def call(path)
|
41
|
+
return false unless path.end_with?(".scas")
|
42
|
+
|
43
|
+
file_load(path)
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
# Segment type handlers can call this to perform an operation after the load
|
48
|
+
# has completed. This is important for ordering, and because loading a Shoes
|
49
|
+
# app often doesn't return. So to have a later section (e.g. tests, additional
|
50
|
+
# data) do something that affects Shoes app loading (e.g. set an env var,
|
51
|
+
# affect the display service) it's important that app loading take place later
|
52
|
+
# in the sequence.
|
53
|
+
def after_load(&block)
|
54
|
+
@after_load ||= []
|
55
|
+
@after_load << block
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def gen_name(segmap)
|
61
|
+
ctr = (1..10_000).detect { |i| !segmap.key?("%5d" % i) }
|
62
|
+
"%5d" % ctr
|
63
|
+
end
|
64
|
+
|
65
|
+
def tokenize_segments(contents)
|
66
|
+
require "yaml" # Only load when needed
|
67
|
+
require "English"
|
68
|
+
|
69
|
+
segments = contents.split(/\n-{5,}/)
|
70
|
+
front_matter = {}
|
71
|
+
|
72
|
+
# The very first segment can start with front matter, or with a divider, or with no divider.
|
73
|
+
if segments[0].start_with?("---\n") || segments[0] == "---"
|
74
|
+
# We have YAML front matter at the start. All later segments will have a divider.
|
75
|
+
front_matter = YAML.load segments[0]
|
76
|
+
front_matter ||= {} # If the front matter is just the three dashes it returns nil
|
77
|
+
segments = segments[1..-1]
|
78
|
+
elsif segments[0].start_with?("-----")
|
79
|
+
# We have a divider at the start. Great! We're already well set up for this case.
|
80
|
+
elsif segments.size == 1
|
81
|
+
# No front matter, no divider, a single unnamed segment. No more parsing needed.
|
82
|
+
return [{}, { "" => segments[0] }]
|
83
|
+
else
|
84
|
+
# No front matter, no divider before the first segment, multiple segments.
|
85
|
+
# We'll add an artificial divider to the first segment for uniformity.
|
86
|
+
segments = ["-----\n" + segments[0]] + segments[1..-1]
|
87
|
+
end
|
88
|
+
|
89
|
+
segmap = {}
|
90
|
+
segments.each do |segment|
|
91
|
+
if segment =~ /\A-* +(.*?)\n/
|
92
|
+
# named segment with separator
|
93
|
+
segmap[::Regexp.last_match(1)] = ::Regexp.last_match.post_match
|
94
|
+
elsif segment =~ /\A-* *\n/
|
95
|
+
# unnamed segment with separator
|
96
|
+
segmap[gen_name(segmap)] = ::Regexp.last_match.post_match
|
97
|
+
else
|
98
|
+
raise "Internal error when parsing segments in segmented app file! seg: #{segment.inspect}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
[front_matter, segmap]
|
103
|
+
end
|
104
|
+
|
105
|
+
def file_load(path)
|
106
|
+
contents = File.read(path)
|
107
|
+
|
108
|
+
front_matter, segmap = tokenize_segments(contents)
|
109
|
+
|
110
|
+
if segmap.empty?
|
111
|
+
raise "Illegal segmented Scarpe file: must have at least one code segment, not just front matter!"
|
112
|
+
end
|
113
|
+
|
114
|
+
if front_matter[:segments]
|
115
|
+
if front_matter[:segments].size != segmap.size
|
116
|
+
raise "Number of front matter :segments must equal number of file segments!"
|
117
|
+
end
|
118
|
+
else
|
119
|
+
if segmap.size > 2
|
120
|
+
raise "Segmented files with more than two segments have to specify what they're for!"
|
121
|
+
end
|
122
|
+
|
123
|
+
# Set to default of shoes code only or shoes code and app test code.
|
124
|
+
front_matter[:segments] = segmap.size == 2 ? ["shoes", "app_test"] : ["shoes"]
|
125
|
+
end
|
126
|
+
|
127
|
+
# Match up front_matter[:segments] with the segments, or use the default of shoes and app_test.
|
128
|
+
|
129
|
+
sth = segment_type_hash
|
130
|
+
sv = segmap.values
|
131
|
+
|
132
|
+
tf_specs = []
|
133
|
+
front_matter[:segments].each.with_index do |seg_type, idx|
|
134
|
+
unless sth.key?(seg_type)
|
135
|
+
raise "Unrecognized segment type #{seg_type.inspect}! No matching segment type available!"
|
136
|
+
end
|
137
|
+
|
138
|
+
tf_specs << ["scarpe_#{seg_type}_segment_contents", sv[idx]]
|
139
|
+
end
|
140
|
+
|
141
|
+
with_tempfiles(tf_specs) do |filenames|
|
142
|
+
filenames.each.with_index do |filename, idx|
|
143
|
+
seg_name = front_matter[:segments][idx]
|
144
|
+
sth[seg_name].call(filename)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Need to call @after_load hooks while tempfiles still exist
|
148
|
+
if @after_load && !@after_load.empty?
|
149
|
+
@after_load.each(&:call)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# The hash of segment type labels mapped to handlers which will be called.
|
155
|
+
# Normal client code shouldn't ever call this.
|
156
|
+
#
|
157
|
+
# @return Hash<String, Object> the name/handler pairs
|
158
|
+
def segment_type_hash
|
159
|
+
@segment_handlers ||= {
|
160
|
+
"shoes" => proc { |seg_file| after_load { load seg_file } },
|
161
|
+
"app_test" => proc { |seg_file| ENV["SCARPE_APP_TEST"] = seg_file },
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# You can add additional segment types to the segmented file loader
|
168
|
+
# loader = Scarpe::Components::SegmentedFileLoader.new
|
169
|
+
# loader.add_segment_type "capybara", proc { |seg_file| load_file_as_capybara(seg_file) }
|
170
|
+
# Shoes.add_file_loader loader
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
require "json"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
require "scarpe/components/file_helpers"
|
8
|
+
|
9
|
+
module Scarpe::Test; end
|
10
|
+
|
11
|
+
# We want test failures set up once *total*, not per Minitest::Test. So an instance var
|
12
|
+
# doesn't do it.
|
13
|
+
ALREADY_SET_UP_LOGGED_TEST_FAILURES = { setup: false }
|
14
|
+
|
15
|
+
# General helpers for general usage.
|
16
|
+
# Helpers here should *not* use Webview-specific functionality.
|
17
|
+
# The intention is that these are helpers for various Scarpe display
|
18
|
+
# services that do *not* necessarily use Webview.
|
19
|
+
|
20
|
+
module Scarpe::Test::Helpers
|
21
|
+
# Very useful for tests
|
22
|
+
include Scarpe::Components::FileHelpers
|
23
|
+
|
24
|
+
# Temporarily set env vars for the block of code inside. The old environment
|
25
|
+
# variable values will be restored after the block finishes.
|
26
|
+
#
|
27
|
+
# @param envs [Hash<String,String>] A hash of environment variable names and values
|
28
|
+
def with_env_vars(envs)
|
29
|
+
old_env = {}
|
30
|
+
envs.each do |k, v|
|
31
|
+
old_env[k] = ENV[k]
|
32
|
+
ENV[k] = v
|
33
|
+
end
|
34
|
+
yield
|
35
|
+
ensure
|
36
|
+
old_env.each { |k, v| ENV[k] = v }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# This test will save extensive logs in case of test failure.
|
41
|
+
# Note that it defines setup/teardown methods. If you want
|
42
|
+
# multiple setup/teardowns from multiple places to happen you
|
43
|
+
# may need to explictly call (e.g. with logged_test_setup/teardown)
|
44
|
+
# to ensure everything you want happens.
|
45
|
+
module Scarpe::Test::LoggedTest
|
46
|
+
def self.included(includer)
|
47
|
+
class << includer
|
48
|
+
attr_accessor :logger_dir
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def file_id
|
53
|
+
"#{self.class.name}_#{self.name}"
|
54
|
+
end
|
55
|
+
|
56
|
+
# This should be called by the test during setup to make sure that
|
57
|
+
# failure logs will be saved if this test fails. It makes sure the
|
58
|
+
# log config will save all logs from all sources, but keeps a copy
|
59
|
+
# of the old log config to restore after the test is finished.
|
60
|
+
#
|
61
|
+
# @return [void]
|
62
|
+
def logged_test_setup
|
63
|
+
# Make sure test failures will be saved at the end of the run.
|
64
|
+
# Delete stale test failures and logging only the *first* time this is called.
|
65
|
+
set_up_test_failures
|
66
|
+
|
67
|
+
@normal_log_config = Shoes::Log.current_log_config
|
68
|
+
Shoes::Log.configure_logger(log_config_for_test)
|
69
|
+
|
70
|
+
Shoes::Log.logger("LoggedScarpeTest").info("Test: #{self.class.name}##{self.name}")
|
71
|
+
end
|
72
|
+
|
73
|
+
# If you include this module and don't override setup/teardown, everything will
|
74
|
+
# work fine. But if you need more setup/teardown steps, you can do that too.
|
75
|
+
#
|
76
|
+
# The setup method guarantees that just including this module will do setup
|
77
|
+
# automatically. If you override it, be sure to call `super` or `logged_test_setup`.
|
78
|
+
#
|
79
|
+
# @return [void]
|
80
|
+
def setup
|
81
|
+
logged_test_setup
|
82
|
+
end
|
83
|
+
|
84
|
+
# After the test has finished, this will restore the old log configuration.
|
85
|
+
# It will also save the logfiles, but only if the test failed, not if it
|
86
|
+
# succeeded or was skipped.
|
87
|
+
#
|
88
|
+
# @return [void]
|
89
|
+
def logged_test_teardown
|
90
|
+
# Restore previous log config
|
91
|
+
Shoes::Log.configure_logger(@normal_log_config)
|
92
|
+
|
93
|
+
if self.failure
|
94
|
+
save_failure_logs
|
95
|
+
else
|
96
|
+
remove_unsaved_logs
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Make sure that, by default, #logged_test_teardown will be called for teardown.
|
101
|
+
# If a class overrides teardown, it should also call `super` or `logged_test_teardown`
|
102
|
+
# to make sure this still happens.
|
103
|
+
#
|
104
|
+
# @return [void]
|
105
|
+
def teardown
|
106
|
+
logged_test_teardown
|
107
|
+
end
|
108
|
+
|
109
|
+
# Set additional LoggedTest configuration for specific logs to separate or save.
|
110
|
+
# This is normally going to be display-service-specific log components.
|
111
|
+
# Note that this only really works with the modular logger or another logger
|
112
|
+
# that does something useful with the log config. The simple print logger
|
113
|
+
# doesn't do a lot with it.
|
114
|
+
def extra_log_config=(additional_log_config)
|
115
|
+
@additional_log_config = additional_log_config
|
116
|
+
end
|
117
|
+
|
118
|
+
# This is the log config that LoggedTests use. It makes sure all components keep all
|
119
|
+
# logs, but also splits the logs into several different files for later ease of scanning.
|
120
|
+
#
|
121
|
+
# TODO: this shouldn't directly include any Webview entries like WebviewAPI or
|
122
|
+
# CatsCradle. Those should be overridden in Webview.
|
123
|
+
#
|
124
|
+
# @return [Hash] the log config
|
125
|
+
def log_config_for_test
|
126
|
+
{
|
127
|
+
"default" => ["debug", "logger/test_failure_#{file_id}.log"],
|
128
|
+
"DisplayService" => ["debug", "logger/test_failure_events_#{file_id}.log"],
|
129
|
+
}.merge(@additional_log_config || {})
|
130
|
+
end
|
131
|
+
|
132
|
+
# The list of logfiles that should be saved. Normally this is called internally by the
|
133
|
+
# class, not externally from elsewhere.
|
134
|
+
#
|
135
|
+
# This could be a lot simpler except I want to only update the file list in one place,
|
136
|
+
# log_config_for_test(). Having a single spot should (I hope) make it a lot friendlier to
|
137
|
+
# add more logfiles for different components, logged API objects, etc.
|
138
|
+
def saved_log_files
|
139
|
+
lc = log_config_for_test
|
140
|
+
log_outfiles = lc.values.map { |_level, loc| loc }
|
141
|
+
log_outfiles.select { |s| s.start_with?("logger/") }.map { |s| s.delete_prefix("logger/") }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Make sure that test failure logs will be noticed, and a message will be printed,
|
145
|
+
# if any logged tests fail. This needs to be called at least once in any Minitest-enabled
|
146
|
+
# process using logged tests.
|
147
|
+
#
|
148
|
+
# @return [void]
|
149
|
+
def set_up_test_failures
|
150
|
+
return if ALREADY_SET_UP_LOGGED_TEST_FAILURES[:setup]
|
151
|
+
|
152
|
+
log_dir = self.class.logger_dir
|
153
|
+
raise("Must set logger directory!") unless log_dir
|
154
|
+
raise("Can't find logger directory!") unless File.directory?(log_dir)
|
155
|
+
|
156
|
+
ALREADY_SET_UP_LOGGED_TEST_FAILURES[:setup] = true
|
157
|
+
# Delete stale test failures, if any, before starting the first failure-logged test
|
158
|
+
Dir["#{log_dir}/test_failure*.log"].each { |fn| File.unlink(fn) }
|
159
|
+
|
160
|
+
Minitest.after_run do
|
161
|
+
# Print test failure notice to console
|
162
|
+
unless Dir["#{log_dir}/test_failure*.out.log"].empty?
|
163
|
+
puts "Some tests have failed! See #{log_dir}/test_failure*.out.log for test logs!"
|
164
|
+
end
|
165
|
+
|
166
|
+
# Remove un-saved test logs
|
167
|
+
Dir["#{log_dir}/test_failure*.log"].each do |f|
|
168
|
+
next if f.include?(".out.log")
|
169
|
+
|
170
|
+
File.unlink(f) if File.exist?(f)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Failure log output location for a given file path. This is normally used internally to this
|
176
|
+
# class, not externally.
|
177
|
+
#
|
178
|
+
# @return [String] the output path
|
179
|
+
def logfail_out_loc(filepath)
|
180
|
+
# Add a .out prefix before final .log
|
181
|
+
out_loc = filepath.gsub(%r{.log\Z}, ".out.log")
|
182
|
+
|
183
|
+
if out_loc == filepath
|
184
|
+
raise "Something is wrong! Could not figure out failure-log output path for #{filepath.inspect}!"
|
185
|
+
end
|
186
|
+
|
187
|
+
if File.exist?(out_loc)
|
188
|
+
raise "Duplicate test file #{out_loc.inspect}? This file should *not* already exist!"
|
189
|
+
end
|
190
|
+
|
191
|
+
out_loc
|
192
|
+
end
|
193
|
+
|
194
|
+
# Save the failure logs in the appropriate place(s). This is normally used internally, not externally.
|
195
|
+
#
|
196
|
+
# @return [void]
|
197
|
+
def save_failure_logs
|
198
|
+
saved_log_files.each do |log_file|
|
199
|
+
full_loc = File.expand_path("#{self.class.logger_dir}/#{log_file}")
|
200
|
+
# TODO: we'd like to skip 0-length logfiles. But also Logging doesn't flush. For now, ignore.
|
201
|
+
next unless File.exist?(full_loc)
|
202
|
+
|
203
|
+
FileUtils.mv full_loc, logfail_out_loc(full_loc)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Remove unsaved failure logs. This is normally used internally, not externally.
|
208
|
+
#
|
209
|
+
# @return [void]
|
210
|
+
def remove_unsaved_logs
|
211
|
+
Dir["#{self.class.logger_dir}/test_failure*.log"].each do |f|
|
212
|
+
next if f.include?(".out.log") # Don't delete saved logs
|
213
|
+
|
214
|
+
File.unlink(f)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scarpe-components
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marco Concetto Rudilosso
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2023-08-
|
12
|
+
date: 2023-08-29 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description:
|
15
15
|
email:
|
@@ -19,9 +19,17 @@ executables: []
|
|
19
19
|
extensions: []
|
20
20
|
extra_rdoc_files: []
|
21
21
|
files:
|
22
|
+
- Gemfile
|
22
23
|
- README.md
|
23
24
|
- Rakefile
|
24
|
-
- lib/scarpe/
|
25
|
+
- lib/scarpe/components/base64.rb
|
26
|
+
- lib/scarpe/components/file_helpers.rb
|
27
|
+
- lib/scarpe/components/modular_logger.rb
|
28
|
+
- lib/scarpe/components/print_logger.rb
|
29
|
+
- lib/scarpe/components/promises.rb
|
30
|
+
- lib/scarpe/components/segmented_file_loader.rb
|
31
|
+
- lib/scarpe/components/unit_test_helpers.rb
|
32
|
+
- lib/scarpe/components/version.rb
|
25
33
|
homepage: https://github.com/scarpe-team/scarpe
|
26
34
|
licenses:
|
27
35
|
- MIT
|
@@ -44,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
52
|
- !ruby/object:Gem::Version
|
45
53
|
version: '0'
|
46
54
|
requirements: []
|
47
|
-
rubygems_version: 3.4.
|
55
|
+
rubygems_version: 3.4.10
|
48
56
|
signing_key:
|
49
57
|
specification_version: 4
|
50
58
|
summary: Reusable components for Scarpe display libraries
|