rbbt-util 5.32.23 → 5.32.27
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/bin/rbbt +2 -3
- data/bin/rbbt_find.rb +74 -0
- data/lib/rbbt/hpc/slurm.rb +1 -0
- data/lib/rbbt/tsv/excel.rb +16 -8
- data/lib/rbbt/tsv/manipulate.rb +10 -0
- data/lib/rbbt/workflow/definition.rb +1 -1
- data/lib/rbbt/workflow/examples.rb +0 -65
- data/lib/rbbt/workflow/step/accessor.rb +0 -70
- data/lib/rbbt/workflow/step/dependencies.rb +8 -2
- data/lib/rbbt/workflow/step/save_load_inputs.rb +162 -0
- data/lib/rbbt/workflow/step.rb +2 -1
- data/lib/rbbt/workflow/task.rb +2 -2
- data/lib/rbbt/workflow.rb +20 -14
- data/share/rbbt_commands/tsv/read_excel +2 -2
- data/share/rbbt_commands/workflow/task +8 -4
- data/test/rbbt/tsv/test_excel.rb +38 -4
- data/test/rbbt/workflow/step/test_dependencies.rb +14 -13
- data/test/rbbt/workflow/step/test_save_load_inputs.rb +46 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ffa357551766aa6ae6da35c586949c0ff05814171304ca5a874689418c7c48e2
|
|
4
|
+
data.tar.gz: 5e1e419fcf846fc8ead12a77e6713f198a74d516c0262c34b4d86380a58af092
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6afd86e7698e5b7e4588412ed678497c825242616bc75d2fa2ab835bc325a8a58826da164dea16cd5b4bbf81697ac5201bb347af220bab5ce238f8e0ce4edcb
|
|
7
|
+
data.tar.gz: 3e86fe7c2d84cc7a04cdd348f03a6323b5467723182057ec028b5d611b3dd200fce41949f1502ca59b0a295b5664f1d6ba259d27a196039f873261e9faf7a1a6
|
data/bin/rbbt
CHANGED
|
@@ -262,9 +262,8 @@ rescue ParameterException
|
|
|
262
262
|
puts
|
|
263
263
|
exit_status = -1
|
|
264
264
|
exit exit_status
|
|
265
|
-
rescue SystemExit
|
|
266
|
-
|
|
267
|
-
exit_status = $!.exit_status
|
|
265
|
+
rescue SystemExit,CmdStop
|
|
266
|
+
exit_status = $!.status
|
|
268
267
|
exit exit_status
|
|
269
268
|
rescue Exception
|
|
270
269
|
Log.exception $!
|
data/bin/rbbt_find.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'rbbt-util'
|
|
4
|
+
require 'rbbt/util/simpleopt'
|
|
5
|
+
|
|
6
|
+
$0 = "rbbt #{$previous_commands*" "} #{ File.basename(__FILE__) }" if $previous_commands
|
|
7
|
+
|
|
8
|
+
options = SOPT.setup <<EOF
|
|
9
|
+
|
|
10
|
+
Find a path
|
|
11
|
+
|
|
12
|
+
$ #{$0} [options] [<subpath>] <path>
|
|
13
|
+
|
|
14
|
+
Use - to read from STDIN
|
|
15
|
+
|
|
16
|
+
-h--help Print this help
|
|
17
|
+
-w--workflows Workflow to load
|
|
18
|
+
-s--search_path* Workflow to load
|
|
19
|
+
-l--list List contents of resolved directories
|
|
20
|
+
-n--nocolor Don't color output
|
|
21
|
+
EOF
|
|
22
|
+
if options[:help]
|
|
23
|
+
if defined? rbbt_usage
|
|
24
|
+
rbbt_usage
|
|
25
|
+
else
|
|
26
|
+
puts SOPT.doc
|
|
27
|
+
end
|
|
28
|
+
exit 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
subpath, path = ARGV
|
|
32
|
+
path, subpath = subpath, nil if path.nil?
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
require 'rbbt/workflow'
|
|
36
|
+
workflow = Workflow.require_workflow subpath
|
|
37
|
+
subpath = workflow.libdir
|
|
38
|
+
rescue
|
|
39
|
+
Log.exception $!
|
|
40
|
+
end if subpath && subpath =~ /^[A-Z][a-zA-Z]+$/
|
|
41
|
+
|
|
42
|
+
path = subpath ? Path.setup(subpath)[path] : Path.setup(path)
|
|
43
|
+
|
|
44
|
+
search_path = options[:search_path].to_sym if options.include? :search_path
|
|
45
|
+
nocolor = options[:nocolor]
|
|
46
|
+
|
|
47
|
+
found = if search_path
|
|
48
|
+
[path.find(search_path)]
|
|
49
|
+
else
|
|
50
|
+
path.find_all
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
found.each do |path|
|
|
54
|
+
if options[:list] && File.directory?(path)
|
|
55
|
+
puts Log.color :blue, path
|
|
56
|
+
path.glob("*").each do |subpath|
|
|
57
|
+
if nocolor
|
|
58
|
+
puts subpath
|
|
59
|
+
else
|
|
60
|
+
color = File.directory?(subpath) ? :blue : nil
|
|
61
|
+
puts " " << Log.color(color, subpath)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
if nocolor
|
|
66
|
+
puts path
|
|
67
|
+
else
|
|
68
|
+
color = File.exists?(path) ? (File.directory?(path) ? :blue : nil) : :red
|
|
69
|
+
puts Log.color color, path
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
data/lib/rbbt/hpc/slurm.rb
CHANGED
data/lib/rbbt/tsv/excel.rb
CHANGED
|
@@ -177,7 +177,7 @@ module TSV
|
|
|
177
177
|
|
|
178
178
|
sheet ||= "0"
|
|
179
179
|
workbook = RubyXL::Parser.parse file
|
|
180
|
-
if sheet &&
|
|
180
|
+
if sheet && sheet =~ /^\d+$/
|
|
181
181
|
sheet = workbook.worksheets.collect{|s| s.sheet_name }[sheet.to_i]
|
|
182
182
|
end
|
|
183
183
|
sheet_name = sheet
|
|
@@ -185,7 +185,9 @@ module TSV
|
|
|
185
185
|
|
|
186
186
|
TmpFile.with_file :extension => Misc.sanitize_filename(sheet_name.to_s) do |filename|
|
|
187
187
|
|
|
188
|
-
sheet =
|
|
188
|
+
sheet = sheet_name ? workbook[sheet_name] : workbook.worksheets.first
|
|
189
|
+
|
|
190
|
+
raise "No sheet #{sheet_name} found" if sheet.nil?
|
|
189
191
|
|
|
190
192
|
rows = []
|
|
191
193
|
|
|
@@ -217,21 +219,27 @@ module TSV
|
|
|
217
219
|
end
|
|
218
220
|
|
|
219
221
|
def self.write(tsv, file, options = {})
|
|
220
|
-
sheet = Misc.process_options options, :sheet
|
|
222
|
+
sheet, add_sheet = Misc.process_options options, :sheet, :add_sheet
|
|
221
223
|
|
|
222
224
|
fields, rows = TSV._excel_data(tsv, options)
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
if Open.exists?(file) && add_sheet
|
|
227
|
+
book = RubyXL::Parser.parse file
|
|
228
|
+
sheet1 = book.add_worksheet(sheet)
|
|
229
|
+
else
|
|
230
|
+
book = RubyXL::Workbook.new
|
|
231
|
+
sheet1 = book.worksheets.first
|
|
232
|
+
sheet1.sheet_name = sheet if sheet
|
|
233
|
+
end
|
|
227
234
|
|
|
228
235
|
fields.each_with_index do |e,i|
|
|
229
236
|
sheet1.add_cell(0, i, e)
|
|
230
|
-
end
|
|
237
|
+
end if fields
|
|
231
238
|
|
|
232
239
|
rows.each_with_index do |cells,i|
|
|
240
|
+
i += 1 if fields
|
|
233
241
|
cells.each_with_index do |e,j|
|
|
234
|
-
sheet1.add_cell(i
|
|
242
|
+
sheet1.add_cell(i, j, e)
|
|
235
243
|
end
|
|
236
244
|
end
|
|
237
245
|
|
data/lib/rbbt/tsv/manipulate.rb
CHANGED
|
@@ -356,6 +356,16 @@ module TSV
|
|
|
356
356
|
elems.sort_by{|k,v| v}.collect{|k,v| k}
|
|
357
357
|
end
|
|
358
358
|
|
|
359
|
+
def subset(keys)
|
|
360
|
+
new = TSV.setup({}, :key_field => key_field, :fields => fields, :type => type, :filename => filename, :identifiers => identifiers)
|
|
361
|
+
self.with_unnamed do
|
|
362
|
+
keys.each do |k|
|
|
363
|
+
new[k] = self[k]
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
new
|
|
367
|
+
end
|
|
368
|
+
|
|
359
369
|
def select(method = nil, invert = false, &block)
|
|
360
370
|
new = TSV.setup({}, :key_field => key_field, :fields => fields, :type => type, :filename => filename, :identifiers => identifiers)
|
|
361
371
|
|
|
@@ -76,7 +76,7 @@ module Workflow
|
|
|
76
76
|
extension :dep_task unless @extension
|
|
77
77
|
returns workflow.tasks[oname].result_description if workflow.tasks.include?(oname) unless @result_description
|
|
78
78
|
task name do
|
|
79
|
-
raise RbbtException, "
|
|
79
|
+
raise RbbtException, "dep_task does not have any dependencies" if dependencies.empty?
|
|
80
80
|
Step.wait_for_jobs dependencies.select{|d| d.streaming? }
|
|
81
81
|
dep = dependencies.last
|
|
82
82
|
dep.join
|
|
@@ -25,71 +25,6 @@ module Workflow
|
|
|
25
25
|
end.compact
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def self.load_inputs(dir, input_names, input_types)
|
|
29
|
-
inputs = {}
|
|
30
|
-
if File.exists?(dir) && ! File.directory?(dir)
|
|
31
|
-
Log.debug "Loading inputs from #{dir}, not a directory trying as tar.gz"
|
|
32
|
-
tarfile = dir
|
|
33
|
-
digest = CMD.cmd("md5sum '#{tarfile}'").read.split(" ").first
|
|
34
|
-
tmpdir = Rbbt.tmp.input_bundle[digest].find
|
|
35
|
-
Misc.untar(tarfile, tmpdir) unless File.exists? tmpdir
|
|
36
|
-
files = tmpdir.glob("*")
|
|
37
|
-
if files.length == 1 && File.directory?(files.first)
|
|
38
|
-
tmpdir = files.first
|
|
39
|
-
end
|
|
40
|
-
load_inputs(tmpdir, input_names, input_types)
|
|
41
|
-
else
|
|
42
|
-
dir = Path.setup(dir.dup)
|
|
43
|
-
input_names.each do |input|
|
|
44
|
-
file = dir[input].find
|
|
45
|
-
file = dir.glob(input.to_s + ".*").reject{|f| f =~ /\.md5$/}.first if file.nil? or not file.exists?
|
|
46
|
-
Log.debug "Trying #{ input }: #{file}"
|
|
47
|
-
next unless file and file.exists?
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case input_types[input]
|
|
51
|
-
when :file
|
|
52
|
-
Log.debug "Pointing #{ input } to #{file}"
|
|
53
|
-
if file =~ /\.yaml/
|
|
54
|
-
inputs[input.to_sym] = YAML.load(Open.read(file))
|
|
55
|
-
else
|
|
56
|
-
if File.symlink?(file)
|
|
57
|
-
link_target = File.expand_path(File.readlink(file), File.dirname(file))
|
|
58
|
-
inputs[input.to_sym] = link_target
|
|
59
|
-
else
|
|
60
|
-
inputs[input.to_sym] = Open.realpath(file)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
when :text
|
|
64
|
-
Log.debug "Reading #{ input } from #{file}"
|
|
65
|
-
inputs[input.to_sym] = Open.read(file)
|
|
66
|
-
when :array
|
|
67
|
-
Log.debug "Reading array #{ input } from #{file}"
|
|
68
|
-
inputs[input.to_sym] = Open.read(file).split("\n")
|
|
69
|
-
when :tsv
|
|
70
|
-
Log.debug "Opening tsv #{ input } from #{file}"
|
|
71
|
-
inputs[input.to_sym] = TSV.open(file)
|
|
72
|
-
when :boolean
|
|
73
|
-
inputs[input.to_sym] = (file.read.strip == 'true')
|
|
74
|
-
else
|
|
75
|
-
Log.debug "Loading #{ input } from #{file}"
|
|
76
|
-
inputs[input.to_sym] = file.read.strip
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
end
|
|
80
|
-
inputs = IndiferentHash.setup(inputs)
|
|
81
|
-
|
|
82
|
-
dir.glob("*#*").each do |od|
|
|
83
|
-
name = File.basename(od)
|
|
84
|
-
value = Open.read(od)
|
|
85
|
-
Log.debug "Loading override dependency #{ name } as #{value}"
|
|
86
|
-
inputs[name] = value.chomp
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
inputs
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
28
|
def example_inputs(task_name, example)
|
|
94
29
|
inputs = {}
|
|
95
30
|
IndiferentHash.setup(inputs)
|
|
@@ -86,76 +86,6 @@ class Step
|
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
def self.save_inputs(inputs, input_types, dir)
|
|
90
|
-
inputs.each do |name,value|
|
|
91
|
-
type = input_types[name]
|
|
92
|
-
type = type.to_s if type
|
|
93
|
-
path = File.join(dir, name.to_s)
|
|
94
|
-
|
|
95
|
-
Log.debug "Saving job input #{name} (#{type}) into #{path}"
|
|
96
|
-
case
|
|
97
|
-
when Step === value
|
|
98
|
-
Open.ln_s(value.path, path)
|
|
99
|
-
when type.to_s == "file"
|
|
100
|
-
if String === value && File.exists?(value)
|
|
101
|
-
value = File.expand_path(value)
|
|
102
|
-
Open.ln_s(value, path)
|
|
103
|
-
else
|
|
104
|
-
value = value.collect{|v| v = "#{v}" if Path === v; v }if Array === value
|
|
105
|
-
value = "#{value}" if Path === value
|
|
106
|
-
Open.write(path + '.yaml', value.to_yaml)
|
|
107
|
-
end
|
|
108
|
-
when Array === value
|
|
109
|
-
Open.write(path, value.collect{|v| Step === v ? v.path : v.to_s} * "\n")
|
|
110
|
-
when IO === value
|
|
111
|
-
if value.filename && String === value.filename && File.exists?(value.filename)
|
|
112
|
-
Open.ln_s(value.filename, path)
|
|
113
|
-
else
|
|
114
|
-
Open.write(path, value)
|
|
115
|
-
end
|
|
116
|
-
else
|
|
117
|
-
Open.write(path, value.to_s)
|
|
118
|
-
end
|
|
119
|
-
end.any?
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def self.save_job_inputs(job, dir, options = nil)
|
|
123
|
-
options = IndiferentHash.setup options.dup if options
|
|
124
|
-
|
|
125
|
-
task_name = Symbol === job.overriden ? job.overriden : job.task_name
|
|
126
|
-
workflow = job.workflow
|
|
127
|
-
workflow = Kernel.const_get workflow if String === workflow
|
|
128
|
-
if workflow
|
|
129
|
-
task_info = IndiferentHash.setup(workflow.task_info(task_name))
|
|
130
|
-
input_types = IndiferentHash.setup(task_info[:input_types])
|
|
131
|
-
task_inputs = IndiferentHash.setup(task_info[:inputs])
|
|
132
|
-
input_defaults = IndiferentHash.setup(task_info[:input_defaults])
|
|
133
|
-
else
|
|
134
|
-
task_info = IndiferentHash.setup({})
|
|
135
|
-
input_types = IndiferentHash.setup({})
|
|
136
|
-
task_inputs = IndiferentHash.setup({})
|
|
137
|
-
input_defaults = IndiferentHash.setup({})
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
inputs = IndiferentHash.setup({})
|
|
141
|
-
real_inputs = job.real_inputs || job.info[:real_inputs]
|
|
142
|
-
job.recursive_inputs.zip(job.recursive_inputs.fields).each do |value,name|
|
|
143
|
-
next unless task_inputs.include? name.to_sym
|
|
144
|
-
next unless real_inputs.include? name.to_sym
|
|
145
|
-
next if options && ! options.include?(name)
|
|
146
|
-
next if value.nil?
|
|
147
|
-
next if input_defaults[name] == value
|
|
148
|
-
inputs[name] = value
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
if options && options.include?('override_dependencies')
|
|
152
|
-
inputs.merge!(:override_dependencies => open[:override_dependencies])
|
|
153
|
-
input_types = IndiferentHash.setup(input_types.merge(:override_dependencies => :array))
|
|
154
|
-
end
|
|
155
|
-
save_inputs(inputs, input_types, dir)
|
|
156
|
-
|
|
157
|
-
inputs.keys
|
|
158
|
-
end
|
|
159
89
|
|
|
160
90
|
def name
|
|
161
91
|
@name ||= path.sub(/.*\/#{Regexp.quote task_name.to_s}\/(.*)/, '\1')
|
|
@@ -254,7 +254,12 @@ class Step
|
|
|
254
254
|
when :bootstrap
|
|
255
255
|
cpus = rest.nil? ? nil : rest.first
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
if cpus.nil?
|
|
258
|
+
keys = ['bootstrap'] + list.collect{|d| [d.task_name, d.task_signature] }.flatten.uniq
|
|
259
|
+
cpus = config('dep_cpus', *keys, :default => [5, list.length / 2].min)
|
|
260
|
+
elsif Symbol === cpus
|
|
261
|
+
cpus = config('dep_cpus', cpus, :default => [5, list.length / 2].min)
|
|
262
|
+
end
|
|
258
263
|
|
|
259
264
|
respawn = rest && rest.include?(:respawn)
|
|
260
265
|
respawn = false if rest && rest.include?(:norespawn)
|
|
@@ -369,7 +374,8 @@ class Step
|
|
|
369
374
|
next unless step.dependencies and step.dependencies.any?
|
|
370
375
|
(step.dependencies + step.input_dependencies).each do |step_dep|
|
|
371
376
|
next unless step.dependencies.include?(step_dep)
|
|
372
|
-
next if step_dep.done? or step_dep.running? or
|
|
377
|
+
next if step_dep.done? or step_dep.running? or
|
|
378
|
+
(ComputeDependency === step_dep and (step_dep.compute == :nodup or step_dep.compute == :ignore))
|
|
373
379
|
dep_step[step_dep.path] ||= []
|
|
374
380
|
dep_step[step_dep.path] << step
|
|
375
381
|
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
module Workflow
|
|
2
|
+
def self.load_inputs(dir, input_names, input_types)
|
|
3
|
+
inputs = {}
|
|
4
|
+
if File.exists?(dir) && ! File.directory?(dir)
|
|
5
|
+
Log.debug "Loading inputs from #{dir}, not a directory trying as tar.gz"
|
|
6
|
+
tarfile = dir
|
|
7
|
+
digest = CMD.cmd("md5sum '#{tarfile}'").read.split(" ").first
|
|
8
|
+
tmpdir = Rbbt.tmp.input_bundle[digest].find
|
|
9
|
+
Misc.untar(tarfile, tmpdir) unless File.exists? tmpdir
|
|
10
|
+
files = tmpdir.glob("*")
|
|
11
|
+
if files.length == 1 && File.directory?(files.first)
|
|
12
|
+
tmpdir = files.first
|
|
13
|
+
end
|
|
14
|
+
load_inputs(tmpdir, input_names, input_types)
|
|
15
|
+
else
|
|
16
|
+
dir = Path.setup(dir.dup)
|
|
17
|
+
input_names.each do |input|
|
|
18
|
+
file = dir[input].find
|
|
19
|
+
file = dir.glob(input.to_s + ".*").reject{|f| f =~ /\.md5$/}.first if file.nil? or not (File.symlink?(file) || file.exists?)
|
|
20
|
+
Log.debug "Trying #{ input }: #{file}"
|
|
21
|
+
next unless file and (File.symlink?(file) || file.exists?)
|
|
22
|
+
|
|
23
|
+
type = input_types[input]
|
|
24
|
+
|
|
25
|
+
type = :io if file.split(".").last == 'as_io'
|
|
26
|
+
|
|
27
|
+
case type
|
|
28
|
+
when :io
|
|
29
|
+
inputs[input.to_sym] = Open.open(Open.realpath(file))
|
|
30
|
+
when :file, :binary
|
|
31
|
+
Log.debug "Pointing #{ input } to #{file}"
|
|
32
|
+
if file =~ /\.yaml/
|
|
33
|
+
inputs[input.to_sym] = YAML.load(Open.read(file))
|
|
34
|
+
else
|
|
35
|
+
if File.symlink?(file)
|
|
36
|
+
link_target = File.expand_path(File.readlink(file), File.dirname(file))
|
|
37
|
+
inputs[input.to_sym] = link_target
|
|
38
|
+
else
|
|
39
|
+
inputs[input.to_sym] = Open.realpath(file)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
when :text
|
|
43
|
+
Log.debug "Reading #{ input } from #{file}"
|
|
44
|
+
inputs[input.to_sym] = Open.read(file)
|
|
45
|
+
when :array
|
|
46
|
+
Log.debug "Reading array #{ input } from #{file}"
|
|
47
|
+
inputs[input.to_sym] = Open.read(file).split("\n")
|
|
48
|
+
when :tsv
|
|
49
|
+
Log.debug "Opening tsv #{ input } from #{file}"
|
|
50
|
+
inputs[input.to_sym] = TSV.open(file)
|
|
51
|
+
when :boolean
|
|
52
|
+
inputs[input.to_sym] = (file.read.strip == 'true')
|
|
53
|
+
else
|
|
54
|
+
Log.debug "Loading #{ input } from #{file}"
|
|
55
|
+
inputs[input.to_sym] = file.read.strip
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
inputs = IndiferentHash.setup(inputs)
|
|
60
|
+
|
|
61
|
+
dir.glob("*#*").each do |od|
|
|
62
|
+
name = File.basename(od)
|
|
63
|
+
value = Open.read(od)
|
|
64
|
+
Log.debug "Loading override dependency #{ name } as #{value}"
|
|
65
|
+
inputs[name] = value.chomp
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
inputs
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def task_inputs_from_directory(task_name, directory)
|
|
73
|
+
task_info = self.task_info(task_name)
|
|
74
|
+
Workflow.load_inputs(directory, task_info[:inputs], task_info[:input_types])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def job_for_directory_inputs(task_name, directory, jobname = nil)
|
|
78
|
+
inputs = task_inputs_from_directory(task_name, directory)
|
|
79
|
+
job(task_name, jobname, inputs)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class Step
|
|
85
|
+
def self.save_inputs(inputs, input_types, input_options, dir)
|
|
86
|
+
inputs.each do |name,value|
|
|
87
|
+
type = input_types[name]
|
|
88
|
+
type = type.to_s if type
|
|
89
|
+
path = File.join(dir, name.to_s)
|
|
90
|
+
|
|
91
|
+
path = path + '.as_io' if (IO === value || Step === value) && ! (input_options[name] && input_options[name][:nofile])
|
|
92
|
+
Log.debug "Saving job input #{name} (#{type}) into #{path}"
|
|
93
|
+
|
|
94
|
+
case
|
|
95
|
+
when IO === value
|
|
96
|
+
Open.write(path, value.to_s)
|
|
97
|
+
when Step === value
|
|
98
|
+
Open.ln_s(value.path, path)
|
|
99
|
+
when type.to_s == "file"
|
|
100
|
+
if String === value && File.exists?(value)
|
|
101
|
+
value = File.expand_path(value)
|
|
102
|
+
Open.ln_s(value, path)
|
|
103
|
+
else
|
|
104
|
+
value = value.collect{|v| v = "#{v}" if Path === v; v }if Array === value
|
|
105
|
+
value = "#{value}" if Path === value
|
|
106
|
+
Open.write(path + '.yaml', value.to_yaml)
|
|
107
|
+
end
|
|
108
|
+
when Array === value
|
|
109
|
+
Open.write(path, value.collect{|v| Step === v ? v.path : v.to_s} * "\n")
|
|
110
|
+
when IO === value
|
|
111
|
+
if value.filename && String === value.filename && File.exists?(value.filename)
|
|
112
|
+
Open.ln_s(value.filename, path)
|
|
113
|
+
else
|
|
114
|
+
Open.write(path, value)
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
Open.write(path, value.to_s)
|
|
118
|
+
end
|
|
119
|
+
end.any?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.save_job_inputs(job, dir, options = nil)
|
|
123
|
+
options = IndiferentHash.setup options.dup if options
|
|
124
|
+
|
|
125
|
+
task_name = Symbol === job.overriden ? job.overriden : job.task_name
|
|
126
|
+
workflow = job.workflow
|
|
127
|
+
workflow = Kernel.const_get workflow if String === workflow
|
|
128
|
+
if workflow
|
|
129
|
+
task_info = IndiferentHash.setup(workflow.task_info(task_name))
|
|
130
|
+
input_types = IndiferentHash.setup(task_info[:input_types])
|
|
131
|
+
input_options = IndiferentHash.setup(task_info[:input_options])
|
|
132
|
+
task_inputs = IndiferentHash.setup(task_info[:inputs])
|
|
133
|
+
input_defaults = IndiferentHash.setup(task_info[:input_defaults])
|
|
134
|
+
else
|
|
135
|
+
task_info = IndiferentHash.setup({})
|
|
136
|
+
input_types = IndiferentHash.setup({})
|
|
137
|
+
task_inputs = IndiferentHash.setup({})
|
|
138
|
+
task_options = IndiferentHash.setup({})
|
|
139
|
+
input_defaults = IndiferentHash.setup({})
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
inputs = IndiferentHash.setup({})
|
|
143
|
+
real_inputs = job.real_inputs || job.info[:real_inputs]
|
|
144
|
+
job.recursive_inputs.zip(job.recursive_inputs.fields).each do |value,name|
|
|
145
|
+
next unless task_inputs.include? name.to_sym
|
|
146
|
+
next unless real_inputs.include? name.to_sym
|
|
147
|
+
next if options && ! options.include?(name)
|
|
148
|
+
next if value.nil?
|
|
149
|
+
next if input_defaults[name] == value
|
|
150
|
+
inputs[name] = value
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if options && options.include?('override_dependencies')
|
|
154
|
+
inputs.merge!(:override_dependencies => open[:override_dependencies])
|
|
155
|
+
input_types = IndiferentHash.setup(input_types.merge(:override_dependencies => :array))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
save_inputs(inputs, input_types, input_options, dir)
|
|
159
|
+
|
|
160
|
+
inputs.keys
|
|
161
|
+
end
|
|
162
|
+
end
|
data/lib/rbbt/workflow/step.rb
CHANGED
|
@@ -6,6 +6,7 @@ require 'rbbt/workflow/step/accessor'
|
|
|
6
6
|
require 'rbbt/workflow/step/prepare'
|
|
7
7
|
require 'rbbt/workflow/step/status'
|
|
8
8
|
require 'rbbt/workflow/step/info'
|
|
9
|
+
require 'rbbt/workflow/step/save_load_inputs'
|
|
9
10
|
|
|
10
11
|
class Step
|
|
11
12
|
attr_accessor :clean_name, :path, :task, :workflow, :inputs, :dependencies, :bindings
|
|
@@ -321,7 +322,7 @@ class Step
|
|
|
321
322
|
def load
|
|
322
323
|
res = begin
|
|
323
324
|
@result = nil if IO === @result && @result.closed?
|
|
324
|
-
if @result && @path != @result
|
|
325
|
+
if @result && @path != @result && ! StreamArray === @result
|
|
325
326
|
res = @result
|
|
326
327
|
else
|
|
327
328
|
join if not done?
|
data/lib/rbbt/workflow/task.rb
CHANGED
|
@@ -90,11 +90,11 @@ module Task
|
|
|
90
90
|
|
|
91
91
|
maps = (Array === dep and Hash === dep.last) ? dep.last.keys : []
|
|
92
92
|
raise "Dependency task not found: #{dep}" if task.nil?
|
|
93
|
-
next if seen.include? [wf, task.name]
|
|
93
|
+
next if seen.include? [wf, task.name, maps]
|
|
94
94
|
|
|
95
95
|
task.workflow = wf if wf
|
|
96
96
|
|
|
97
|
-
seen << [wf, task.name]
|
|
97
|
+
seen << [wf, task.name, maps]
|
|
98
98
|
new_inputs = task.inputs - maps
|
|
99
99
|
next unless new_inputs.any?
|
|
100
100
|
if task_inputs[task].nil?
|
data/lib/rbbt/workflow.rb
CHANGED
|
@@ -45,14 +45,10 @@ module Workflow
|
|
|
45
45
|
load_remote_tasks(Rbbt.root.etc.remote_tasks.find) if Rbbt.root.etc.remote_tasks.exists?
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def self.require_remote_workflow(wf_name, url)
|
|
49
|
-
require 'rbbt/workflow/remote_workflow'
|
|
50
|
-
eval "Object::#{wf_name} = RemoteWorkflow.new '#{ url }', '#{wf_name}'"
|
|
51
|
-
end
|
|
52
48
|
|
|
53
49
|
def self.require_remote_workflow(wf_name, url)
|
|
54
50
|
require 'rbbt/workflow/remote_workflow'
|
|
55
|
-
eval "Object::#{wf_name} = RemoteWorkflow.new '#{ url }', '#{wf_name}'"
|
|
51
|
+
eval "Object::#{wf_name.split("+").first} = RemoteWorkflow.new '#{ url }', '#{wf_name}'"
|
|
56
52
|
end
|
|
57
53
|
|
|
58
54
|
def self.load_workflow_libdir(filename)
|
|
@@ -134,9 +130,10 @@ module Workflow
|
|
|
134
130
|
end
|
|
135
131
|
|
|
136
132
|
def self.require_local_workflow(wf_name)
|
|
133
|
+
|
|
137
134
|
filename = local_workflow_filename(wf_name)
|
|
138
135
|
|
|
139
|
-
if filename and File.exist?
|
|
136
|
+
if filename and File.exist?(filename)
|
|
140
137
|
load_workflow_file filename
|
|
141
138
|
else
|
|
142
139
|
return false
|
|
@@ -194,14 +191,23 @@ module Workflow
|
|
|
194
191
|
end
|
|
195
192
|
|
|
196
193
|
Log.high{"Loading workflow #{wf_name}"}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
194
|
+
|
|
195
|
+
first = nil
|
|
196
|
+
wf_name.split("+").each do |wf_name|
|
|
197
|
+
require_local_workflow(wf_name) or
|
|
198
|
+
(Workflow.autoinstall and `rbbt workflow install #{Misc.snake_case(wf_name)} || rbbt workflow install #{wf_name}` and require_local_workflow(wf_name)) or raise("Workflow not found or could not be loaded: #{ wf_name }")
|
|
199
|
+
|
|
200
|
+
workflow = begin
|
|
201
|
+
Misc.string2const Misc.camel_case(wf_name.split("+").first)
|
|
202
|
+
rescue
|
|
203
|
+
Workflow.workflows.last || true
|
|
204
|
+
end
|
|
205
|
+
workflow.load_documentation
|
|
206
|
+
|
|
207
|
+
first ||= workflow
|
|
208
|
+
end
|
|
209
|
+
return first
|
|
210
|
+
|
|
205
211
|
workflow
|
|
206
212
|
end
|
|
207
213
|
|
|
@@ -22,7 +22,7 @@ Use - to read from STDIN
|
|
|
22
22
|
-h--help Print this help
|
|
23
23
|
-s--sheet* Sheet to extract
|
|
24
24
|
-skip--skip_rows* Initial rows to skip
|
|
25
|
-
|
|
25
|
+
-o--original Dump the rows without parsing them into TSV
|
|
26
26
|
EOF
|
|
27
27
|
if options[:help]
|
|
28
28
|
if defined? rbbt_usage
|
|
@@ -39,5 +39,5 @@ raise ParameterException, "No excel file given" if excelfile.nil?
|
|
|
39
39
|
|
|
40
40
|
options[:zipped] ||= true if options[:merge]
|
|
41
41
|
require 'rbbt/tsv/excel'
|
|
42
|
-
puts TSV.excel(excelfile, options)
|
|
42
|
+
puts TSV.excel(excelfile, options.merge(:text => options[:original]))
|
|
43
43
|
|
|
@@ -104,7 +104,7 @@ def fix_options(workflow, task, job_options)
|
|
|
104
104
|
elsif input_options[name] and input_options[name][:stream] and value == "-"
|
|
105
105
|
STDIN
|
|
106
106
|
else
|
|
107
|
-
if Array === value
|
|
107
|
+
if Array === value || IO === value
|
|
108
108
|
value
|
|
109
109
|
else
|
|
110
110
|
array_separator = $array_separator
|
|
@@ -137,6 +137,8 @@ def fix_options(workflow, task, job_options)
|
|
|
137
137
|
TSV.open(STDIN, :unnamed => true, :sep => $field_separator, :sep2 => ($array_separator || "|"))
|
|
138
138
|
when (Misc.is_filename?(value) and String)
|
|
139
139
|
TSV.open(value, :unnamed => true, :sep => $field_separator, :sep2 => ($array_separator || "|"))
|
|
140
|
+
when IO
|
|
141
|
+
TSV.open(value, :unnamed => true, :sep => $field_separator, :sep2 => ($array_separator || "|"))
|
|
140
142
|
else
|
|
141
143
|
TSV.open(StringIO.new(value), :unnamed => true, :sep => $field_separator, :sep2 => ($array_separator || "|"))
|
|
142
144
|
end
|
|
@@ -287,8 +289,8 @@ else
|
|
|
287
289
|
puts
|
|
288
290
|
puts $!.message
|
|
289
291
|
puts
|
|
290
|
-
|
|
291
|
-
exit
|
|
292
|
+
|
|
293
|
+
exit -1
|
|
292
294
|
end
|
|
293
295
|
end
|
|
294
296
|
|
|
@@ -572,7 +574,9 @@ when Step
|
|
|
572
574
|
exit! 0
|
|
573
575
|
else
|
|
574
576
|
res.join
|
|
575
|
-
|
|
577
|
+
Open.open(res.path, :mode => 'rb') do |io|
|
|
578
|
+
Misc.consume_stream(io, false, out)
|
|
579
|
+
end if Open.exist?(res.path) || Open.remote?(res.path) || Open.ssh?(res.path)
|
|
576
580
|
end
|
|
577
581
|
else
|
|
578
582
|
if Array === res
|
data/test/rbbt/tsv/test_excel.rb
CHANGED
|
@@ -2,7 +2,7 @@ require File.join(File.expand_path(File.dirname(__FILE__)), '../..', 'test_helpe
|
|
|
2
2
|
require 'rbbt/tsv/excel'
|
|
3
3
|
|
|
4
4
|
class TestExcel < Test::Unit::TestCase
|
|
5
|
-
def
|
|
5
|
+
def test_xls
|
|
6
6
|
content =<<-EOF
|
|
7
7
|
#Id ValueA ValueB OtherID
|
|
8
8
|
row1 a|aa|aaa b Id1|Id2
|
|
@@ -19,7 +19,7 @@ row2 A B Id3
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def
|
|
22
|
+
def test_xlsx
|
|
23
23
|
content =<<-EOF
|
|
24
24
|
#Id ValueA ValueB OtherID
|
|
25
25
|
row1 a|aa|aaa b Id1|Id2
|
|
@@ -36,7 +36,7 @@ row2 A B Id3
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def
|
|
39
|
+
def test_excel
|
|
40
40
|
content =<<-EOF
|
|
41
41
|
#Id ValueA ValueB OtherID
|
|
42
42
|
row1 a|aa|aaa b Id1|Id2
|
|
@@ -63,7 +63,7 @@ row2 A B Id3
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
def
|
|
66
|
+
def test_excel_sheets
|
|
67
67
|
content =<<-EOF
|
|
68
68
|
#Id ValueA ValueB OtherID
|
|
69
69
|
row1 a|aa|aaa b Id1|Id2
|
|
@@ -133,5 +133,39 @@ row2 A B Id3
|
|
|
133
133
|
end
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
|
+
|
|
137
|
+
def test_excel_multi_sheets
|
|
138
|
+
content =<<-EOF
|
|
139
|
+
#Id ValueA ValueB OtherID
|
|
140
|
+
row1 a|aa|aaa b Id1|Id2
|
|
141
|
+
row2 A B Id3
|
|
142
|
+
EOF
|
|
143
|
+
|
|
144
|
+
TmpFile.with_file(content) do |filename|
|
|
145
|
+
tsv1 = TSV.open(filename, :sep => /\s+/)
|
|
146
|
+
tsv2 = tsv1.annotate(tsv1.dup)
|
|
147
|
+
tsv3 = tsv1.annotate(tsv1.dup)
|
|
148
|
+
|
|
149
|
+
tsv2["row2"] = [["AA"], ["BB"], ["Id4"]]
|
|
150
|
+
tsv3["row2"] = [["AAA"], ["BBB"], ["Id5"]]
|
|
151
|
+
|
|
152
|
+
TmpFile.with_file(nil, false, :extension => 'xlsx') do |excelfile|
|
|
153
|
+
tsv1.xlsx(excelfile, :sheet => "S1")
|
|
154
|
+
tsv2.xlsx(excelfile, :sheet => "S2", :add_sheet => true)
|
|
155
|
+
workbook = RubyXL::Parser.parse excelfile
|
|
156
|
+
|
|
157
|
+
assert_equal %w(S1 S2), workbook.worksheets.collect{|s| s.sheet_name}
|
|
158
|
+
|
|
159
|
+
new = TSV.excel(excelfile, :sheet => "S1")
|
|
160
|
+
assert_equal %w(row1 row2), new.keys.sort
|
|
161
|
+
assert_equal %w(A), new["row2"]["ValueA"]
|
|
162
|
+
|
|
163
|
+
new = TSV.excel(excelfile, :sheet => "S2")
|
|
164
|
+
assert_equal %w(row1 row2), new.keys.sort
|
|
165
|
+
assert_equal %w(AA), new["row2"]["ValueA"]
|
|
166
|
+
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
136
170
|
end
|
|
137
171
|
|
|
@@ -203,20 +203,21 @@ class TestWorkflowDependency < Test::Unit::TestCase
|
|
|
203
203
|
size = 100000
|
|
204
204
|
content = (1..size).to_a.collect{|num| "Line #{num}" } * "\n"
|
|
205
205
|
last_line = nil
|
|
206
|
-
Log.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
206
|
+
Log.with_severity 0 do
|
|
207
|
+
TmpFile.with_file(content) do |input_file|
|
|
208
|
+
begin
|
|
209
|
+
job = DepWorkflow.job(:s3, "TEST", :input_file => input_file)
|
|
210
|
+
job.recursive_clean
|
|
211
|
+
job.run(:stream)
|
|
212
|
+
io = TSV.get_stream job
|
|
213
|
+
while line = io.gets
|
|
214
|
+
last_line = line.strip
|
|
215
|
+
end
|
|
216
|
+
io.join if io.respond_to? :join
|
|
217
|
+
rescue Exception
|
|
218
|
+
job.abort
|
|
219
|
+
raise $!
|
|
215
220
|
end
|
|
216
|
-
io.join if io.respond_to? :join
|
|
217
|
-
rescue Exception
|
|
218
|
-
job.abort
|
|
219
|
-
raise $!
|
|
220
221
|
end
|
|
221
222
|
end
|
|
222
223
|
assert last_line.include? "Line #{size}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), '../../..', 'test_helper.rb')
|
|
2
|
+
require 'rbbt/workflow/step/save_load_inputs'
|
|
3
|
+
|
|
4
|
+
ENV["RBBT_DEBUG_JOB_HASH"] = true.to_s
|
|
5
|
+
require 'rbbt/workflow'
|
|
6
|
+
module TestSaveLoadWF
|
|
7
|
+
extend Workflow
|
|
8
|
+
|
|
9
|
+
task :number => :integer do
|
|
10
|
+
10
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task :list => :array do
|
|
14
|
+
(0..10).to_a.collect{|e| e.to_s}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
input :list, :array
|
|
18
|
+
input :number, :integer
|
|
19
|
+
task :reverse => :array do |list|
|
|
20
|
+
list.reverse
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
dep :list
|
|
24
|
+
dep :number
|
|
25
|
+
dep :reverse, :list => :list, :number => :number
|
|
26
|
+
task :prefix => :array do
|
|
27
|
+
step(:reverse).run.collect{|e| "A-#{e}" }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class TestSaveLoad < Test::Unit::TestCase
|
|
32
|
+
def test_save
|
|
33
|
+
Log.with_severity 0 do
|
|
34
|
+
job = TestSaveLoadWF.job(:prefix)
|
|
35
|
+
job.recursive_clean
|
|
36
|
+
job = TestSaveLoadWF.job(:prefix)
|
|
37
|
+
TmpFile.with_file do |directory|
|
|
38
|
+
Step.save_job_inputs(job.step(:reverse), directory)
|
|
39
|
+
job.produce
|
|
40
|
+
newjob = TestSaveLoadWF.job_for_directory_inputs(:reverse, directory)
|
|
41
|
+
assert_equal job.rec_dependencies.last.path, newjob.path
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rbbt-util
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.32.
|
|
4
|
+
version: 5.32.27
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Miguel Vazquez
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-
|
|
11
|
+
date: 2021-12-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -172,6 +172,7 @@ executables:
|
|
|
172
172
|
- rbbt_Rutil.rb
|
|
173
173
|
- rbbt
|
|
174
174
|
- rbbt_dangling_locks.rb
|
|
175
|
+
- rbbt_find.rb
|
|
175
176
|
extensions: []
|
|
176
177
|
extra_rdoc_files:
|
|
177
178
|
- LICENSE
|
|
@@ -183,6 +184,7 @@ files:
|
|
|
183
184
|
- bin/rbbt_Rutil.rb
|
|
184
185
|
- bin/rbbt_dangling_locks.rb
|
|
185
186
|
- bin/rbbt_exec.rb
|
|
187
|
+
- bin/rbbt_find.rb
|
|
186
188
|
- bin/rbbt_query.rb
|
|
187
189
|
- etc/app.d/base.rb
|
|
188
190
|
- etc/app.d/entities.rb
|
|
@@ -343,6 +345,7 @@ files:
|
|
|
343
345
|
- lib/rbbt/workflow/step/info.rb
|
|
344
346
|
- lib/rbbt/workflow/step/prepare.rb
|
|
345
347
|
- lib/rbbt/workflow/step/run.rb
|
|
348
|
+
- lib/rbbt/workflow/step/save_load_inputs.rb
|
|
346
349
|
- lib/rbbt/workflow/step/status.rb
|
|
347
350
|
- lib/rbbt/workflow/task.rb
|
|
348
351
|
- lib/rbbt/workflow/usage.rb
|
|
@@ -548,6 +551,7 @@ files:
|
|
|
548
551
|
- test/rbbt/util/test_simpleopt.rb
|
|
549
552
|
- test/rbbt/util/test_tmpfile.rb
|
|
550
553
|
- test/rbbt/workflow/step/test_dependencies.rb
|
|
554
|
+
- test/rbbt/workflow/step/test_save_load_inputs.rb
|
|
551
555
|
- test/rbbt/workflow/test_doc.rb
|
|
552
556
|
- test/rbbt/workflow/test_remote_workflow.rb
|
|
553
557
|
- test/rbbt/workflow/test_schedule.rb
|
|
@@ -590,6 +594,7 @@ test_files:
|
|
|
590
594
|
- test/rbbt/workflow/test_schedule.rb
|
|
591
595
|
- test/rbbt/workflow/test_step.rb
|
|
592
596
|
- test/rbbt/workflow/step/test_dependencies.rb
|
|
597
|
+
- test/rbbt/workflow/step/test_save_load_inputs.rb
|
|
593
598
|
- test/rbbt/workflow/test_task.rb
|
|
594
599
|
- test/rbbt/resource/test_path.rb
|
|
595
600
|
- test/rbbt/util/test_colorize.rb
|