computable 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +44 -0
- data/.travis.yml +5 -0
- data/README.md +3 -0
- data/Rakefile +1 -1
- data/computable.gemspec +3 -1
- data/lib/computable/version.rb +1 -1
- data/lib/computable.rb +190 -65
- data/test/helper.rb +16 -0
- data/test/test_candy.rb +8 -5
- data/test/test_debug.rb +40 -0
- data/test/test_expired_recalc.rb +40 -0
- data/test/test_format.rb +6 -3
- data/test/test_freeze.rb +6 -3
- data/test/test_input_value.rb +6 -3
- data/test/test_parallel.rb +126 -0
- data/test/test_recursion_detection.rb +6 -3
- data/test/test_unknown.rb +47 -0
- data.tar.gz.sig +0 -0
- metadata +48 -18
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5aa6e8a15124c51c77b6849974a4a8bec2badd58ee90532eabae5d974d2e1342
|
4
|
+
data.tar.gz: 47e2605924a75967629be1e6c9f1fa263f5c577771ddad3ffc05660a300734d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f394e585c1da2f6270ae4bfd2e520506738603ee07a5a7ddf1485a2aba061bf74bd40d61a4c4a3be74a8a89d279f97eb792179411cbbfe2b154b6cd4f62c9a0e
|
7
|
+
data.tar.gz: 28a0950d0fcd04a80eb865a62997f4cd5f8a577cfe8cb1e2854b672bb51a95051b61798dc50d3f74a0dcabe5706be3454908becba91405b2a7e1e07bcd0376c4
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -0,0 +1,44 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
job_test:
|
7
|
+
name: CI
|
8
|
+
strategy:
|
9
|
+
fail-fast: false
|
10
|
+
matrix:
|
11
|
+
include:
|
12
|
+
- os: windows
|
13
|
+
ruby: "head"
|
14
|
+
- os: ubuntu
|
15
|
+
ruby: "head"
|
16
|
+
- os: ubuntu
|
17
|
+
ruby: "3.1"
|
18
|
+
- os: ubuntu
|
19
|
+
ruby: "2.5"
|
20
|
+
- os: ubuntu
|
21
|
+
ruby: "truffleruby"
|
22
|
+
NO_TIMING_TESTS: true
|
23
|
+
- os: ubuntu
|
24
|
+
ruby: "jruby"
|
25
|
+
- os: macos
|
26
|
+
ruby: "head"
|
27
|
+
NO_TIMING_TESTS: true
|
28
|
+
|
29
|
+
runs-on: ${{ matrix.os }}-latest
|
30
|
+
env:
|
31
|
+
NO_TIMING_TESTS: ${{ matrix.NO_TIMING_TESTS }}
|
32
|
+
|
33
|
+
steps:
|
34
|
+
|
35
|
+
- uses: actions/checkout@v2
|
36
|
+
- name: Set up Ruby
|
37
|
+
uses: ruby/setup-ruby@v1
|
38
|
+
with:
|
39
|
+
ruby-version: ${{ matrix.ruby }}
|
40
|
+
|
41
|
+
- run: bundle install
|
42
|
+
|
43
|
+
- name: Run tests
|
44
|
+
run: bundle exec rake test
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
[](https://github.com/larskanis/computable/actions/workflows/ci.yml)
|
2
|
+
[](https://app.travis-ci.com/larskanis/computable)
|
3
|
+
|
1
4
|
# Computable
|
2
5
|
|
3
6
|
TODO: Write a gem description
|
data/Rakefile
CHANGED
@@ -3,5 +3,5 @@ require "bundler/gem_tasks"
|
|
3
3
|
task :gem => :build
|
4
4
|
|
5
5
|
task :test do
|
6
|
-
sh "ruby -w -W2 -I. -Ilib -e \"#{Dir["test/test_*.rb"].map{|f| "require '#{f}';"}.join}\" -- -v"
|
6
|
+
sh "ruby -w -W2 -I. -Ilib -e \"#{Dir["test/test_*.rb"].map{|f| "require '#{f}';"}.join}\" -- -v #{ENV["TESTOPT"]}"
|
7
7
|
end
|
data/computable.gemspec
CHANGED
@@ -12,12 +12,14 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.summary = %q{Define computation tasks with automatic caching and dependency tracking.}
|
13
13
|
spec.homepage = "https://github.com/larskanis/computable"
|
14
14
|
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.4"
|
15
16
|
|
16
17
|
spec.files = `git ls-files`.split($/)
|
17
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
20
|
spec.require_paths = ["lib"]
|
20
21
|
|
21
|
-
spec.add_development_dependency "bundler", "
|
22
|
+
spec.add_development_dependency "bundler", ">= 1.4", "< 3.0"
|
22
23
|
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "minitest", "~> 5.6"
|
23
25
|
end
|
data/lib/computable/version.rb
CHANGED
data/lib/computable.rb
CHANGED
@@ -16,29 +16,41 @@ class Computable
|
|
16
16
|
end
|
17
17
|
|
18
18
|
class Variable
|
19
|
-
attr_accessor :name, :calc_method, :used_for, :expired_from, :value, :value_calced, :count
|
20
|
-
|
19
|
+
attr_accessor :name, :calc_method, :used_for, :expired_from, :value, :value_calced, :count, :in_process, :recalc_error
|
20
|
+
|
21
|
+
def initialize(name, calc_method, comp, mutex)
|
21
22
|
@name = name
|
22
23
|
@calc_method = calc_method
|
24
|
+
@comp = comp
|
25
|
+
@mutex = mutex
|
23
26
|
@used_for = {}
|
24
27
|
@expired_from = {}
|
25
28
|
@count = 0
|
26
29
|
@value = Unknown
|
30
|
+
@in_process = false
|
31
|
+
@recalc_error = nil
|
27
32
|
end
|
28
33
|
|
29
34
|
def inspect
|
30
|
-
|
35
|
+
has = @recalc_error ? "error!" : "value:#{Unknown!=value}"
|
36
|
+
"<Variable #{name} used_for:#{used_for.keys} expired_from:#{expired_from.keys} has #{has} value_calced:#{value_calced.inspect}>"
|
31
37
|
end
|
32
38
|
|
33
39
|
def calc!
|
34
40
|
self.count += 1
|
35
|
-
|
41
|
+
self.value_calced = true
|
42
|
+
@mutex.unlock
|
43
|
+
begin
|
44
|
+
calc_method.call(self)
|
45
|
+
ensure
|
46
|
+
@mutex.lock
|
47
|
+
end
|
36
48
|
end
|
37
49
|
|
38
50
|
def expire_value
|
39
51
|
return if used_for.empty?
|
40
52
|
|
41
|
-
puts "expire #{inspect}" if
|
53
|
+
puts "expire #{inspect}" if @comp.computable_debug
|
42
54
|
used_for.each do |name2, v2|
|
43
55
|
if v2.value_calced && !v2.expired_from[name]
|
44
56
|
v2.expire_value
|
@@ -50,7 +62,7 @@ class Computable
|
|
50
62
|
def revoke_expire
|
51
63
|
return if used_for.empty?
|
52
64
|
|
53
|
-
puts "revoke expire #{inspect}" if
|
65
|
+
puts "revoke expire #{inspect}" if @comp.computable_debug
|
54
66
|
used_for.each do |name2, v2|
|
55
67
|
if v2.value_calced && v2.expired_from.delete(name) && v2.expired_from.empty?
|
56
68
|
v2.revoke_expire
|
@@ -58,49 +70,177 @@ class Computable
|
|
58
70
|
end
|
59
71
|
end
|
60
72
|
|
73
|
+
def process_recalced_value(recalced_value, err)
|
74
|
+
if err
|
75
|
+
self.recalc_error = err
|
76
|
+
self.value = Unknown
|
77
|
+
used_for.clear
|
78
|
+
elsif self.value == recalced_value
|
79
|
+
revoke_expire
|
80
|
+
else
|
81
|
+
self.recalc_error = nil
|
82
|
+
self.value = recalced_value
|
83
|
+
used_for.clear
|
84
|
+
end
|
85
|
+
expired_from.clear
|
86
|
+
end
|
87
|
+
|
61
88
|
def recalc_value
|
62
89
|
return if !value_calced || expired_from.empty?
|
63
90
|
|
64
|
-
puts "recalc #{inspect}" if
|
91
|
+
puts "recalc #{inspect}" if @comp.computable_debug
|
65
92
|
expired_from.each do |name2, v2|
|
66
93
|
v2.recalc_value
|
67
94
|
end
|
68
95
|
|
69
96
|
unless expired_from.empty?
|
70
|
-
|
71
|
-
|
72
|
-
|
97
|
+
begin
|
98
|
+
recalced_value = self.calc!
|
99
|
+
rescue Exception => err
|
100
|
+
end
|
101
|
+
process_recalced_value(recalced_value, err)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def new_worker(from_workers, to_workers)
|
106
|
+
Thread.new do
|
107
|
+
while v = to_workers.pop
|
108
|
+
puts "recalc parallel #{v.inspect}" if @comp.computable_debug
|
109
|
+
err = nil
|
110
|
+
begin
|
111
|
+
recalced_value = v.calc_method.call(v)
|
112
|
+
rescue Exception => err
|
113
|
+
end
|
114
|
+
from_workers.push([v, recalced_value, err])
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def recalc_parallel(max_threads)
|
120
|
+
workers = []
|
121
|
+
from_workers = Queue.new
|
122
|
+
to_workers = Queue.new
|
123
|
+
|
124
|
+
master_loop(max_threads, workers, from_workers, to_workers)
|
125
|
+
|
126
|
+
to_workers.close
|
127
|
+
workers.each { |t| t.join }
|
128
|
+
end
|
129
|
+
|
130
|
+
def master_loop(max_threads, workers, from_workers, to_workers)
|
131
|
+
num_working = 0
|
132
|
+
loop do
|
133
|
+
if num_working == max_threads || !(node = find_recalcable)
|
134
|
+
#
|
135
|
+
# maxed out or no nodes available -- wait for results
|
136
|
+
#
|
137
|
+
return if num_working == 0
|
138
|
+
|
139
|
+
puts "recalc join" if @comp.computable_debug
|
140
|
+
@mutex.unlock
|
141
|
+
begin
|
142
|
+
node, recalced_value, err = from_workers.pop
|
143
|
+
ensure
|
144
|
+
@mutex.lock
|
145
|
+
end
|
146
|
+
node.in_process = false
|
147
|
+
num_working -= 1
|
148
|
+
|
149
|
+
if err
|
150
|
+
# Add the backtrace of the caller to the small in-thread backtrace for better debugging
|
151
|
+
err.set_backtrace(err.backtrace + caller)
|
152
|
+
end
|
153
|
+
|
154
|
+
node.process_recalced_value(recalced_value, err)
|
73
155
|
else
|
74
|
-
|
75
|
-
|
156
|
+
#
|
157
|
+
# not maxed out and found a node -- compute it
|
158
|
+
#
|
159
|
+
if (max_threads && workers.size < max_threads) ||
|
160
|
+
(!max_threads && num_working == workers.size)
|
161
|
+
workers << new_worker(from_workers, to_workers)
|
162
|
+
end
|
163
|
+
node.in_process = true
|
164
|
+
node.count += 1
|
165
|
+
node.value_calced = true
|
166
|
+
num_working += 1
|
167
|
+
to_workers.push(node)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def find_recalcable
|
173
|
+
if !value_calced || expired_from.empty? || in_process
|
174
|
+
nil
|
175
|
+
elsif expired_from.all?{ |_, v2| !v2.value_calced || v2.expired_from.empty? }
|
176
|
+
self
|
177
|
+
else
|
178
|
+
expired_from.each do |_, v2|
|
179
|
+
node = v2.find_recalcable and return node
|
76
180
|
end
|
181
|
+
nil
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def assign_value(value)
|
186
|
+
unless self.value == value
|
187
|
+
expire_value
|
77
188
|
expired_from.clear
|
189
|
+
used_for.clear
|
190
|
+
self.value = value
|
191
|
+
end
|
192
|
+
self.value_calced = false
|
193
|
+
end
|
194
|
+
|
195
|
+
def query_value(kaller)
|
196
|
+
if kaller
|
197
|
+
v2 = used_for[kaller.name]
|
198
|
+
if v2
|
199
|
+
if Unknown==value && Unknown==v2.value && value_calced && v2.value_calced
|
200
|
+
raise RecursionDetected, "#{v2.name} depends on #{name}, but #{name} could not be computed without #{v2.name}"
|
201
|
+
end
|
202
|
+
else
|
203
|
+
used_for[kaller.name] = kaller
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
max_threads = @comp.computable_max_threads
|
208
|
+
if !max_threads || max_threads > 0
|
209
|
+
recalc_parallel(max_threads)
|
210
|
+
else
|
211
|
+
recalc_value
|
78
212
|
end
|
213
|
+
|
214
|
+
raise recalc_error if recalc_error
|
215
|
+
self.value = calc! if Unknown==value
|
216
|
+
self.value
|
79
217
|
end
|
80
218
|
end
|
81
219
|
|
82
|
-
|
83
|
-
|
84
|
-
|
220
|
+
def computable_debug=(v)
|
221
|
+
@computable_debug = v
|
222
|
+
end
|
223
|
+
def computable_debug
|
224
|
+
@computable_debug
|
225
|
+
end
|
226
|
+
|
227
|
+
def computable_max_threads=(v)
|
228
|
+
@computable_max_threads = v
|
85
229
|
end
|
86
|
-
def
|
87
|
-
|
230
|
+
def computable_max_threads
|
231
|
+
@computable_max_threads
|
88
232
|
end
|
89
233
|
|
90
|
-
def computable_display_dot(
|
234
|
+
def computable_display_dot(**kwargs)
|
91
235
|
IO.popen("dot -Tpng | display -", "w") do |fd|
|
92
|
-
fd.puts computable_to_dot(
|
236
|
+
fd.puts computable_to_dot(**kwargs)
|
93
237
|
end
|
94
238
|
end
|
95
239
|
|
96
|
-
def computable_to_dot(
|
97
|
-
rankdir = params.delete(:rankdir){ "TB" }
|
98
|
-
multiline = params.delete(:multiline){ true }
|
99
|
-
raise ArgumentError, "invalid params #{params.inspect}" unless params.empty?
|
100
|
-
|
240
|
+
def computable_to_dot(rankdir: "TB", multiline: true)
|
101
241
|
dot = "digraph #{self.class.name.inspect} {\n"
|
102
242
|
dot << "graph [ dpi = 45, rankdir=#{rankdir} ];\n"
|
103
|
-
@
|
243
|
+
@computable_variables.each do |name, v|
|
104
244
|
col = case
|
105
245
|
when !v.value_calced then "color = red,"
|
106
246
|
when !v.used_for.empty? then "color = green,"
|
@@ -120,82 +260,67 @@ class Computable
|
|
120
260
|
end
|
121
261
|
|
122
262
|
def initialize
|
123
|
-
@
|
124
|
-
@
|
263
|
+
@computable_debug = false
|
264
|
+
@computable_max_threads = 0
|
265
|
+
@computable_variables = {}
|
266
|
+
@computable_caller = nil
|
267
|
+
@computable_mutex = Mutex.new
|
125
268
|
end
|
126
269
|
|
127
270
|
|
128
271
|
def self.verify_format(name, value, format)
|
129
|
-
if format && value
|
272
|
+
if format && !(Unknown==value) && !(format === value)
|
130
273
|
raise InvalidFormat, "variable '#{name}': value #{value.inspect} is not in format #{format.inspect}"
|
131
274
|
end
|
132
275
|
end
|
133
276
|
|
134
|
-
def self.calc_value name, format=nil,
|
135
|
-
freeze = params.delete(:freeze){ true }
|
136
|
-
raise ArgumentError, "invalid params #{params.inspect}" unless params.empty?
|
137
|
-
|
277
|
+
def self.calc_value name, format=nil, freeze: true, &block
|
138
278
|
calc_method_id = "calc_#{name}".intern
|
139
279
|
define_method(calc_method_id, &block)
|
140
280
|
|
141
281
|
calc_method2_id = "calc_#{name}_with_tracking".intern
|
142
282
|
define_method(calc_method2_id) do |v|
|
143
283
|
begin
|
144
|
-
|
145
|
-
|
284
|
+
old_caller = Thread.current.thread_variable_get("Computable #{object_id}")
|
285
|
+
Thread.current.thread_variable_set("Computable #{self.object_id}", v)
|
146
286
|
begin
|
147
|
-
puts "do calc #{v.inspect}" if
|
287
|
+
puts "do calc #{v.inspect}" if @computable_debug
|
148
288
|
res = send(calc_method_id)
|
149
289
|
Computable.verify_format(name, res, format)
|
150
290
|
res.freeze if freeze
|
151
291
|
res
|
152
292
|
ensure
|
153
|
-
|
293
|
+
Thread.current.thread_variable_set("Computable #{self.object_id}", old_caller)
|
154
294
|
end
|
155
295
|
end
|
156
296
|
end
|
157
297
|
|
158
298
|
define_method("#{name}=") do |value|
|
159
299
|
Computable.verify_format(name, value, format)
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
v.expire_value
|
166
|
-
v.expired_from.clear
|
167
|
-
v.used_for.clear
|
300
|
+
@computable_mutex.synchronize do
|
301
|
+
v = @computable_variables[name]
|
302
|
+
puts "set #{name}: #{value.inspect} #{v.inspect}" if @computable_debug
|
303
|
+
v = @computable_variables[name] = Variable.new(name, method(calc_method2_id), self, @computable_mutex) unless v
|
304
|
+
|
168
305
|
value.freeze if freeze
|
169
|
-
v.value
|
306
|
+
v.assign_value(value)
|
170
307
|
end
|
171
|
-
v.value_calced = false
|
172
308
|
end
|
173
309
|
|
174
310
|
define_method(name) do
|
175
|
-
|
176
|
-
|
177
|
-
|
311
|
+
@computable_mutex.synchronize do
|
312
|
+
v = @computable_variables[name]
|
313
|
+
puts "called #{name} #{v.inspect}" if @computable_debug
|
314
|
+
v = @computable_variables[name] = Variable.new(name, method(calc_method2_id), self, @computable_mutex) unless v
|
178
315
|
|
179
|
-
|
180
|
-
|
181
|
-
if v2
|
182
|
-
if v.value==Unknown && v2.value==Unknown && v.value_calced && v2.value_calced
|
183
|
-
raise RecursionDetected, "#{v2.name} depends on #{name}, but #{name} could not be computed without #{v2.name}"
|
184
|
-
end
|
185
|
-
else
|
186
|
-
v.used_for[@caller.name] = @caller
|
187
|
-
end
|
316
|
+
kaller = Thread.current.thread_variable_get("Computable #{object_id}")
|
317
|
+
v.query_value(kaller)
|
188
318
|
end
|
189
|
-
|
190
|
-
v.recalc_value
|
191
|
-
|
192
|
-
v.value = v.calc! if v.value==Unknown
|
193
|
-
v.value
|
194
319
|
end
|
195
320
|
end
|
196
321
|
|
197
|
-
def self.input_value name, format=nil,
|
198
|
-
calc_value name, format,
|
322
|
+
def self.input_value name, format=nil, **kwargs
|
323
|
+
calc_value name, format, **kwargs do
|
199
324
|
raise UndefinedValue, "input variable '#{name}' is not assigned"
|
200
325
|
end
|
201
326
|
end
|
data/test/helper.rb
ADDED
data/test/test_candy.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
|
2
|
-
require 'computable'
|
1
|
+
require_relative 'helper'
|
3
2
|
|
4
|
-
class TestCandy <
|
3
|
+
class TestCandy < Minitest::Test
|
5
4
|
class MyBuilder < Computable
|
6
5
|
attr_reader :counters
|
7
6
|
|
@@ -10,9 +9,9 @@ class TestCandy < MiniTest::Unit::TestCase
|
|
10
9
|
@counters = {}
|
11
10
|
end
|
12
11
|
|
13
|
-
def self.counted_value name, format=nil,
|
12
|
+
def self.counted_value name, format=nil, **kwargs, &block
|
14
13
|
define_method "#{name}_counted", &block
|
15
|
-
calc_value name, format do
|
14
|
+
calc_value name, format, **kwargs do
|
16
15
|
@counters[name] ||= 0
|
17
16
|
@counters[name] += 1
|
18
17
|
send "#{name}_counted"
|
@@ -102,3 +101,7 @@ class TestCandy < MiniTest::Unit::TestCase
|
|
102
101
|
assert_equal [2,2,2,1,2,2,1,1], @b.counters.values_at(:top, :l0, :r0, :l1, :r1, :bot, :fl, :fr)
|
103
102
|
end
|
104
103
|
end
|
104
|
+
|
105
|
+
class TestCandyParallel < TestCandy
|
106
|
+
include Helper::EnableParallel
|
107
|
+
end
|
data/test/test_debug.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class TestDebug < Minitest::Test
|
4
|
+
class MyBuilder < Computable
|
5
|
+
input_value :inp
|
6
|
+
calc_value :cal do
|
7
|
+
inp * 2
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@b = MyBuilder.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def teardown
|
16
|
+
# @b.computable_display_dot
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_enable_debug
|
20
|
+
refute @b.computable_debug
|
21
|
+
@b.computable_debug = true
|
22
|
+
assert @b.computable_debug
|
23
|
+
@b.computable_debug = false
|
24
|
+
refute @b.computable_debug
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_prints_debug
|
28
|
+
@b.computable_debug = true
|
29
|
+
|
30
|
+
assert_output(/set inp/){ @b.inp = 3 }
|
31
|
+
cal = nil
|
32
|
+
assert_output(/do calc/){ cal = @b.cal }
|
33
|
+
|
34
|
+
assert_equal 6, cal
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class TestDebugParallel < TestDebug
|
39
|
+
include Helper::EnableParallel
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class TestExpiredRecalc < Minitest::Test
|
4
|
+
class MyBuilder < Computable
|
5
|
+
input_value :enable
|
6
|
+
|
7
|
+
calc_value :a do
|
8
|
+
b if enable
|
9
|
+
end
|
10
|
+
|
11
|
+
calc_value :b do
|
12
|
+
raise StandardError, "not enabled" unless enable
|
13
|
+
:x
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup
|
18
|
+
@b = MyBuilder.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def teardown
|
22
|
+
# @b.computable_display_dot
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_on_off
|
26
|
+
@b.enable = true
|
27
|
+
assert_equal :x, @b.a
|
28
|
+
|
29
|
+
@b.enable = false
|
30
|
+
assert_nil @b.a # this shouldn't raise an error although b is internally recalced
|
31
|
+
assert_raises(StandardError){ @b.b }
|
32
|
+
|
33
|
+
@b.enable = true
|
34
|
+
assert_equal :x, @b.a
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class TestExpiredRecalcParallel < TestExpiredRecalc
|
39
|
+
include Helper::EnableParallel
|
40
|
+
end
|
data/test/test_format.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
|
2
|
-
require 'computable'
|
1
|
+
require_relative 'helper'
|
3
2
|
|
4
|
-
class TestFormat <
|
3
|
+
class TestFormat < Minitest::Test
|
5
4
|
class MyBuilder < Computable
|
6
5
|
def self.verify_uniq_array(a)
|
7
6
|
a.uniq.length == a.length
|
@@ -66,3 +65,7 @@ class TestFormat < MiniTest::Unit::TestCase
|
|
66
65
|
assert_raises(Computable::InvalidFormat){ @b.sqrt }
|
67
66
|
end
|
68
67
|
end
|
68
|
+
|
69
|
+
class TestFormatParallel < TestFormat
|
70
|
+
include Helper::EnableParallel
|
71
|
+
end
|
data/test/test_freeze.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
|
2
|
-
require 'computable'
|
1
|
+
require_relative 'helper'
|
3
2
|
|
4
|
-
class TestFreeze <
|
3
|
+
class TestFreeze < Minitest::Test
|
5
4
|
class MyBuilder < Computable
|
6
5
|
input_value :no_freeze1, String, freeze: false
|
7
6
|
calc_value :no_freeze2, String, freeze: false do
|
@@ -34,3 +33,7 @@ class TestFreeze < MiniTest::Unit::TestCase
|
|
34
33
|
assert @b.freeze2.frozen?, "should be frozen"
|
35
34
|
end
|
36
35
|
end
|
36
|
+
|
37
|
+
class TestFreezeParallel < TestFreeze
|
38
|
+
include Helper::EnableParallel
|
39
|
+
end
|
data/test/test_input_value.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
|
2
|
-
require 'computable'
|
1
|
+
require_relative 'helper'
|
3
2
|
|
4
|
-
class TestInputValue <
|
3
|
+
class TestInputValue < Minitest::Test
|
5
4
|
class MyBuilder < Computable
|
6
5
|
input_value :i
|
7
6
|
|
@@ -46,3 +45,7 @@ class TestInputValue < MiniTest::Unit::TestCase
|
|
46
45
|
assert_equal 6, @b.o
|
47
46
|
end
|
48
47
|
end
|
48
|
+
|
49
|
+
class TestInputValueParallel < TestInputValue
|
50
|
+
include Helper::EnableParallel
|
51
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class TestParallel < Minitest::Test
|
4
|
+
class MyBuilder < Computable
|
5
|
+
input_value :i1, Integer
|
6
|
+
input_value :i2, Integer
|
7
|
+
|
8
|
+
def self.p(idx)
|
9
|
+
calc_value "p#{idx}" do
|
10
|
+
sleep 0.1
|
11
|
+
i1
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.q(idx)
|
16
|
+
calc_value "q#{idx}" do
|
17
|
+
sleep 0.1
|
18
|
+
send("p#{idx}") + i2
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
0.upto(4) do |idx|
|
23
|
+
p idx
|
24
|
+
q idx
|
25
|
+
end
|
26
|
+
|
27
|
+
calc_value :o do
|
28
|
+
0.upto(3).map do |idx|
|
29
|
+
send("q#{idx}")
|
30
|
+
end.inject(:+)
|
31
|
+
end
|
32
|
+
|
33
|
+
input_value :offs
|
34
|
+
|
35
|
+
100.times do |idx|
|
36
|
+
calc_value "m0-#{idx}" do
|
37
|
+
#sleep 0.001
|
38
|
+
idx + offs
|
39
|
+
end
|
40
|
+
calc_value "m1-#{idx}" do
|
41
|
+
#sleep 0.001
|
42
|
+
idx + offs
|
43
|
+
end
|
44
|
+
calc_value "m-#{idx}" do
|
45
|
+
send("m#{offs}-#{idx}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
calc_value "n" do
|
50
|
+
100.times.sum do |idx|
|
51
|
+
send("m-#{idx}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
class MyError < RuntimeError
|
57
|
+
end
|
58
|
+
|
59
|
+
input_value :enable_error
|
60
|
+
calc_value :raise_error do
|
61
|
+
raise MyError if enable_error
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def setup
|
66
|
+
@b = MyBuilder.new
|
67
|
+
end
|
68
|
+
|
69
|
+
def teardown
|
70
|
+
# @b.computable_display_dot
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_many_recalcs
|
74
|
+
@b.computable_max_threads = 10
|
75
|
+
@b.offs = 0
|
76
|
+
assert_equal 4950, @b.n
|
77
|
+
|
78
|
+
#@b.computable_debug = true
|
79
|
+
@b.offs = 1
|
80
|
+
assert_equal 5050, @b.n
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_error
|
84
|
+
@b.enable_error = false
|
85
|
+
assert_nil @b.raise_error
|
86
|
+
@b.enable_error = true
|
87
|
+
assert_raises(MyBuilder::MyError){ @b.raise_error }
|
88
|
+
assert_raises(MyBuilder::MyError){ @b.raise_error }
|
89
|
+
@b.enable_error = false
|
90
|
+
assert_nil @b.raise_error
|
91
|
+
end
|
92
|
+
|
93
|
+
unless ENV["NO_TIMING_TESTS"]=="true"
|
94
|
+
def diff_time
|
95
|
+
st = Time.now
|
96
|
+
res = yield
|
97
|
+
[Time.now - st, res]
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_full_parallel
|
101
|
+
@b.computable_max_threads = nil
|
102
|
+
@b.i1, @b.i2 = 2, 3
|
103
|
+
dt, res = diff_time{ @b.o }
|
104
|
+
assert_in_delta 0.8, dt, 0.05
|
105
|
+
assert_equal 20, res
|
106
|
+
|
107
|
+
@b.i1, @b.i2 = 4, 5
|
108
|
+
dt, res = diff_time{ @b.o }
|
109
|
+
assert_in_delta 0.2, dt, 0.05
|
110
|
+
assert_equal 36, res
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_2_threads
|
114
|
+
@b.computable_max_threads = 2
|
115
|
+
@b.i1, @b.i2 = 2, 3
|
116
|
+
dt, res = diff_time{ @b.o }
|
117
|
+
assert_in_delta 0.8, dt, 0.05
|
118
|
+
assert_equal 20, res
|
119
|
+
|
120
|
+
@b.i1, @b.i2 = 4, 5
|
121
|
+
dt, res = diff_time{ @b.o }
|
122
|
+
assert_in_delta 0.4, dt, 0.05
|
123
|
+
assert_equal 36, res
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
|
-
|
2
|
-
require 'computable'
|
1
|
+
require_relative 'helper'
|
3
2
|
|
4
|
-
class TestRecursionDetection <
|
3
|
+
class TestRecursionDetection < Minitest::Test
|
5
4
|
class MyBuilder < Computable
|
6
5
|
calc_value :recursion1 do
|
7
6
|
recursion1
|
@@ -34,3 +33,7 @@ class TestRecursionDetection < MiniTest::Unit::TestCase
|
|
34
33
|
assert_raises(Computable::RecursionDetected){ @b.recursion2 }
|
35
34
|
end
|
36
35
|
end
|
36
|
+
|
37
|
+
class TestRecursionDetectionParallel < TestRecursionDetection
|
38
|
+
include Helper::EnableParallel
|
39
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class TestUnknown < Minitest::Test
|
4
|
+
class MyTruth
|
5
|
+
def ==(obj)
|
6
|
+
true
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class MyFault
|
11
|
+
def ==(obj)
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class MyBuilder < Computable
|
17
|
+
input_value :v1
|
18
|
+
|
19
|
+
calc_value :v2 do
|
20
|
+
v1
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def setup
|
25
|
+
@b = MyBuilder.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def teardown
|
29
|
+
# @b.computable_display_dot
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_eq_true
|
33
|
+
@b.v1 = v = MyTruth.new
|
34
|
+
assert_same @b.v1, v
|
35
|
+
assert_same @b.v2, v
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_eq_false
|
39
|
+
@b.v1 = v = MyFault.new
|
40
|
+
assert_same @b.v1, v
|
41
|
+
assert_same @b.v2, v
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class TestUnknownParallel < TestUnknown
|
46
|
+
include Helper::EnableParallel
|
47
|
+
end
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: computable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lars Kanis
|
@@ -10,9 +10,9 @@ bindir: bin
|
|
10
10
|
cert_chain:
|
11
11
|
- |
|
12
12
|
-----BEGIN CERTIFICATE-----
|
13
|
-
|
13
|
+
MIIDLjCCAhagAwIBAgIBCTANBgkqhkiG9w0BAQsFADA9MQ4wDAYDVQQDDAVrYW5p
|
14
14
|
czEXMBUGCgmSJomT8ixkARkWB2NvbWNhcmQxEjAQBgoJkiaJk/IsZAEZFgJkZTAe
|
15
|
-
|
15
|
+
Fw0yMTA0MDcxMzQzNTZaFw0yMjA0MDcxMzQzNTZaMD0xDjAMBgNVBAMMBWthbmlz
|
16
16
|
MRcwFQYKCZImiZPyLGQBGRYHY29tY2FyZDESMBAGCgmSJomT8ixkARkWAmRlMIIB
|
17
17
|
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApop+rNmg35bzRugZ21VMGqI6
|
18
18
|
HGzPLO4VHYncWn/xmgPU/ZMcZdfj6MzIaZJ/czXyt4eHpBk1r8QOV3gBXnRXEjVW
|
@@ -20,32 +20,37 @@ cert_chain:
|
|
20
20
|
lJi4+ENAVT4MpqHEAGB8yFoPC0GqiOHQsdHxQV3P3c2OZqG+yJey74QtwA2tLcLn
|
21
21
|
Q53c63+VLGsOjODl1yPn/2ejyq8qWu6ahfTxiIlSar2UbwtaQGBDFdb2CXgEufXT
|
22
22
|
L7oaPxlmj+Q2oLOfOnInd2Oxop59HoJCQPsg8f921J43NCQGA8VHK6paxIRDLQID
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
MlGgwc//cCsBG8sa
|
23
|
+
AQABozkwNzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQUvgTdT7fe
|
24
|
+
x17ugO3IOsjEJwW7KP4wDQYJKoZIhvcNAQELBQADggEBAGCQhS4TBqUG1bSY5gw5
|
25
|
+
emj2GNePHFNlXTZ/W0/7FlnXQz/LyBZeYmy4AIHcdY0w9xsu3bPNGk8kLBkHgK3Y
|
26
|
+
l/yWiUK0NYRI3K3yI2EoTfrHPDT8XIgBPeUUGv5Nje+SUYMQWsfYWKo3+vLEG64a
|
27
|
+
n1xP+1+g2Za39WCS5LwnDWMiIk47NnxR9yXErKd0Iau/Q/IarYsHFX6kWWmlMoln
|
28
|
+
W1lMomCcOJFwIPnsy6aqq7YfS0YcqyHjcvs1h5k3zPaIRWhoPlQivniMVMa3Txh+
|
29
|
+
NEF/4atY64rruzkyfxGEcrFFOHJIkWnWQjRGaiZdgULxf7ira2gEFvV/ZtamqJWF
|
30
|
+
c+I=
|
32
31
|
-----END CERTIFICATE-----
|
33
|
-
date:
|
32
|
+
date: 2022-03-25 00:00:00.000000000 Z
|
34
33
|
dependencies:
|
35
34
|
- !ruby/object:Gem::Dependency
|
36
35
|
name: bundler
|
37
36
|
requirement: !ruby/object:Gem::Requirement
|
38
37
|
requirements:
|
39
|
-
- - "
|
38
|
+
- - ">="
|
40
39
|
- !ruby/object:Gem::Version
|
41
40
|
version: '1.4'
|
41
|
+
- - "<"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '3.0'
|
42
44
|
type: :development
|
43
45
|
prerelease: false
|
44
46
|
version_requirements: !ruby/object:Gem::Requirement
|
45
47
|
requirements:
|
46
|
-
- - "
|
48
|
+
- - ">="
|
47
49
|
- !ruby/object:Gem::Version
|
48
50
|
version: '1.4'
|
51
|
+
- - "<"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
49
54
|
- !ruby/object:Gem::Dependency
|
50
55
|
name: rake
|
51
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -60,6 +65,20 @@ dependencies:
|
|
60
65
|
- - ">="
|
61
66
|
- !ruby/object:Gem::Version
|
62
67
|
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: minitest
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '5.6'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '5.6'
|
63
82
|
description: Define computation tasks with automatic caching and dependency tracking.
|
64
83
|
email:
|
65
84
|
- lars@greiz-reinsdorf.de
|
@@ -67,7 +86,9 @@ executables: []
|
|
67
86
|
extensions: []
|
68
87
|
extra_rdoc_files: []
|
69
88
|
files:
|
89
|
+
- ".github/workflows/ci.yml"
|
70
90
|
- ".gitignore"
|
91
|
+
- ".travis.yml"
|
71
92
|
- Gemfile
|
72
93
|
- LICENSE.txt
|
73
94
|
- README.md
|
@@ -75,11 +96,16 @@ files:
|
|
75
96
|
- computable.gemspec
|
76
97
|
- lib/computable.rb
|
77
98
|
- lib/computable/version.rb
|
99
|
+
- test/helper.rb
|
78
100
|
- test/test_candy.rb
|
101
|
+
- test/test_debug.rb
|
102
|
+
- test/test_expired_recalc.rb
|
79
103
|
- test/test_format.rb
|
80
104
|
- test/test_freeze.rb
|
81
105
|
- test/test_input_value.rb
|
106
|
+
- test/test_parallel.rb
|
82
107
|
- test/test_recursion_detection.rb
|
108
|
+
- test/test_unknown.rb
|
83
109
|
homepage: https://github.com/larskanis/computable
|
84
110
|
licenses:
|
85
111
|
- MIT
|
@@ -92,21 +118,25 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
118
|
requirements:
|
93
119
|
- - ">="
|
94
120
|
- !ruby/object:Gem::Version
|
95
|
-
version: '
|
121
|
+
version: '2.4'
|
96
122
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
123
|
requirements:
|
98
124
|
- - ">="
|
99
125
|
- !ruby/object:Gem::Version
|
100
126
|
version: '0'
|
101
127
|
requirements: []
|
102
|
-
|
103
|
-
rubygems_version: 2.2.0
|
128
|
+
rubygems_version: 3.3.7
|
104
129
|
signing_key:
|
105
130
|
specification_version: 4
|
106
131
|
summary: Define computation tasks with automatic caching and dependency tracking.
|
107
132
|
test_files:
|
133
|
+
- test/helper.rb
|
108
134
|
- test/test_candy.rb
|
135
|
+
- test/test_debug.rb
|
136
|
+
- test/test_expired_recalc.rb
|
109
137
|
- test/test_format.rb
|
110
138
|
- test/test_freeze.rb
|
111
139
|
- test/test_input_value.rb
|
140
|
+
- test/test_parallel.rb
|
112
141
|
- test/test_recursion_detection.rb
|
142
|
+
- test/test_unknown.rb
|
metadata.gz.sig
CHANGED
Binary file
|