scarpe-components 0.1.0 → 0.2.2
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/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
|