rbbt-util 5.28.11 → 5.29.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -26,15 +26,25 @@ module Workflow
26
26
  workload
27
27
  end
28
28
 
29
+ def self.workload(jobs)
30
+ jobs.inject({}) do |acc,job|
31
+ Orchestrator.job_workload(job).each do |j,d|
32
+ acc[j] = d unless acc.keys.collect{|k| k.path }.include? j.path
33
+ end
34
+ acc
35
+ end
36
+ end
37
+
29
38
  def self.job_rules(rules, job)
30
39
  workflow = job.workflow.to_s
31
40
  task_name = job.task_name.to_s
41
+ defaults = rules["defaults"] || {}
32
42
 
33
- return IndiferentHash.setup(rules["defaults"]) unless rules[workflow]
34
- return IndiferentHash.setup(rules["defaults"]) unless rules[workflow][task_name]
43
+ return IndiferentHash.setup(defaults) unless rules[workflow]
44
+ return IndiferentHash.setup(defaults) unless rules[workflow][task_name]
35
45
 
36
46
  job_rules = IndiferentHash.setup(rules[workflow][task_name])
37
- rules["defaults"].each{|k,v| job_rules[k] = v if job_rules[k].nil? } if rules["defaults"]
47
+ defaults.each{|k,v| job_rules[k] = v if job_rules[k].nil? } if defaults
38
48
  job_rules
39
49
  end
40
50
 
@@ -169,12 +179,7 @@ module Workflow
169
179
  def process(rules, jobs)
170
180
  begin
171
181
 
172
- workload = jobs.inject({}) do |acc,job|
173
- Orchestrator.job_workload(job).each do |j,d|
174
- acc[j] = d unless acc.keys.collect{|k| k.path }.include? j.path
175
- end
176
- acc
177
- end
182
+ workload = Orchestrator.workload(jobs)
178
183
  all_jobs = workload.keys
179
184
 
180
185
  top_level_jobs = jobs.collect{|job| job.path }
@@ -22,11 +22,14 @@ class Step
22
22
  end
23
23
 
24
24
  def self.prov_report_msg(status, name, path, info = nil)
25
- parts = path.sub(/\{.*/,'').sub(/#{Regexp.quote(name)}$/,'').split "/"
25
+ parts = path.sub(/\{.*/,'').split "/"
26
26
 
27
+ parts.pop
28
+
27
29
  task = Log.color(:yellow, parts.pop)
28
30
  workflow = Log.color(:magenta, parts.pop)
29
- if status.to_s == 'noinfo' and parts.last != 'jobs'
31
+ #if status.to_s == 'noinfo' && parts.last != 'jobs'
32
+ if ! Workflow.job_path?(path)
30
33
  task, status, workflow = Log.color(:yellow, info[:task_name]), Log.color(:green, "file"), Log.color(:magenta, "-")
31
34
  end
32
35
 
@@ -0,0 +1,141 @@
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
+ Queue a job in Marenostrum
11
+
12
+ $ rbbt mnl [options]
13
+
14
+ -h--help Print this help
15
+ -d--done Done jobs only
16
+ -e--error Error jobs only
17
+ -a--aborted SLURM aboted jobs
18
+ -r--running Running jobs only
19
+ -q--queued Queued jobs only
20
+ -j--job* Job ids
21
+ -s--search* Regular expression
22
+ -t--tail* Show the last lines of the STDERR
23
+ EOF
24
+
25
+ if options[:help]
26
+ if defined? rbbt_usage
27
+ rbbt_usage
28
+ else
29
+ puts SOPT.doc
30
+ end
31
+ exit 0
32
+ end
33
+
34
+ Log.severity = 4
35
+ done, error, running, queued, aborted, jobid, search, tail = options.values_at :done, :error, :running, :queued, :aborted, :job, :search, :tail
36
+
37
+ workdir = File.expand_path('~/rbbt-slurm')
38
+ Path.setup(workdir)
39
+
40
+ running_jobs = begin
41
+ CMD.cmd('squeue').read.split("\n").collect{|l| l.to_i.to_s}
42
+ rescue
43
+ Log.warn "Cannot determine if jobs are running, they will seem to be all alive (Job ID in green)"
44
+ $norunningjobs = true
45
+ []
46
+ end
47
+
48
+ count = 0
49
+ workdir.glob("**/command.slurm").sort_by{|f| File.mtime(f)}.each do |fcmd|
50
+ dir = File.dirname(fcmd)
51
+
52
+ if m = Open.read(fcmd).match(/#CMD: (.*)/)
53
+ cmd = m[1]
54
+ else
55
+ cmd = nil
56
+ end
57
+
58
+ if m = Open.read(fcmd).match(/# Run command\n(.*?)\n/im)
59
+ exe = m[1]
60
+ else
61
+ exe = nil
62
+ end
63
+
64
+ if m = Open.read(fcmd).match(/^CONTAINER_DIR=(.*)/)
65
+ container_home = m[1]
66
+ else
67
+ container_home = nil
68
+ end
69
+
70
+
71
+ if File.exists?(fid = File.join(dir, 'job.id'))
72
+ id = Open.read(fid).chomp
73
+ else
74
+ id = nil
75
+ end
76
+
77
+ if File.exists?(fstatus = File.join(dir, 'exit.status'))
78
+ exit_status = Open.read(fstatus).to_i
79
+ else
80
+ exit_status = nil
81
+ end
82
+
83
+ if File.exists?(fstatus = File.join(dir, 'job.status'))
84
+ nodes = Open.read(fstatus).split("\n").last.split(/\s+/).last.split(",")
85
+ else
86
+ nodes = []
87
+ end
88
+
89
+ if File.exists?(File.join(dir, 'std.out'))
90
+ outt = File.mtime File.join(dir, 'std.out')
91
+ errt = File.mtime File.join(dir, 'std.err')
92
+ time_diff = Time.now - [outt, errt].max
93
+ end
94
+
95
+ fdep = File.join(dir, 'dependencies.list')
96
+ deps = Open.read(fdep).split("\n") if File.exists?(fdep)
97
+
98
+ if done || error || aborted || running || queued || jobid || search
99
+ select = false
100
+ select = true if done && exit_status == 0
101
+ select = true if error && exit_status && exit_status != 0
102
+ select = true if aborted && (exit_status.nil? && ! running_jobs.include?(id))
103
+ select = true if queued && deps && (running_jobs & deps).any?
104
+ select = true if running && (exit_status.nil? && running_jobs.include?(id)) && (!deps || (running_jobs & deps).empty?)
105
+ select = true if jobid && jobid.split(",").include?(id)
106
+ select = true if search && cmd.match(/#{search}/)
107
+ next unless select
108
+ end
109
+
110
+
111
+ puts Log.color :blue, dir
112
+ puts Log.color(:magenta, "Creation: ") << File.mtime(File.join(dir, 'command.slurm')).to_s
113
+ puts Log.color(:magenta, "Done: ") << File.mtime(File.join(dir, 'exit.status')).to_s if File.exist?(File.join(dir, 'exit.status'))
114
+ puts Log.color(:magenta, "Exec: ") << (exe || "Missing")
115
+ puts Log.color(:magenta, "CMD: ") << (Log.color(:yellow, cmd) || "Missing")
116
+ puts Log.color(:magenta, "HOME: ") << Log.color(:yellow, container_home) if container_home
117
+ puts Log.color(:magenta, "Job ID: ") << (exit_status ? (exit_status == 0 ? Log.color(:green, "Done") : Log.color(:red, "Error")) : (running_jobs.include?(id) || $norunningjobs ? Log.color(:green, id) : Log.color(:red, id) ))
118
+ puts Log.color(:magenta, "Dependencies: ") << deps * ", " if deps
119
+ puts Log.color(:magenta, "Nodes: ") << nodes * ", "
120
+ puts Log.color(:magenta, "Output: ") << File.exists?(File.join(dir, 'std.out')).to_s << (id.nil? ? "" : " (last update " + Misc.format_seconds(time_diff) + " ago)")
121
+
122
+ if tail && File.exists?(File.join(dir, 'std.err'))
123
+ if exit_status && exit_status != 0
124
+ puts Log.color(:magenta, "First error or exception found: ")
125
+ puts CMD.cmd("grep -i -w 'error\\|[a-z]*exception' #{File.join(dir, 'std.err')} -A #{tail.to_i} |head -n #{tail.to_i}", :no_fail => true).read
126
+ elsif exit_status
127
+ puts Log.color(:magenta, "Completed jobs: ")
128
+ puts CMD.cmd("grep -i -w 'Completed step' #{File.join(dir, 'std.err')} | grep -v 'Retrying dep.' | tail -n #{tail.to_i}", :no_fail => true).read
129
+ else
130
+ puts Log.color(:magenta, "Log tail: ")
131
+ puts CMD.cmd("tail -n #{tail.to_i} #{File.join(dir, 'std.err')}").read
132
+ end
133
+ end
134
+
135
+ count += 1
136
+
137
+ end
138
+
139
+ puts
140
+ puts "Found #{count} jobs"
141
+
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rbbt/util/simpleopt'
4
+ require 'rbbt/workflow'
5
+ require 'rbbt/workflow/usage'
6
+ require 'rbbt/hpc'
7
+ require 'rbbt/hpc/orchestrate'
8
+ require 'time'
9
+
10
+ $slurm_options = SOPT.get <<EOF
11
+ -dr--dry_run Print only the template
12
+ -cj--clean_job Clean job
13
+ --drbbt* Use development version of rbbt
14
+ -sing--singularity Use Singularity
15
+ -ug--user_group* Use alternative user group for group project directory
16
+ -c--contain* Contain in directory (using Singularity)
17
+ -s--sync* Contain in directory and sync jobs
18
+ -e--exclusive Make exclusive use of the node
19
+ -hm--highmem Make use of highmem cores
20
+ -wc--wipe_container* Wipe the jobs from the contain directory
21
+ -CS--contain_and_sync Contain and sync to default locations
22
+ -ci--copy_image When using a container directory, copy image there
23
+ -t--tail Tail the logs
24
+ -q--queue* Queue
25
+ -t--task_cpus* Tasks
26
+ -W--workflows* Additional workflows
27
+ -tm--time* Time
28
+ -R--orchestration_rules* Orchestration rules
29
+ -rmb--remove_slurm_basedir Remove the SLURM working directory (command, STDIN, exit status, ...)
30
+ EOF
31
+
32
+ class Step
33
+ def run(*args)
34
+ if done?
35
+ self.load
36
+ else
37
+ begin
38
+ Log.debug "Issuing SLURM job for #{self.path}"
39
+ HPC::SLURM.orchestrate_job(self, SOPT::GOT_OPTIONS.merge($slurm_options))
40
+ rescue HPC::SBATCH
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ ARGV.concat ["-W", $slurm_options[:workflows]] if $slurm_options[:workflows]
47
+ load Rbbt.share.rbbt_commands.workflow.task.find
@@ -9,8 +9,9 @@ require 'time'
9
9
  $slurm_options = SOPT.get <<EOF
10
10
  -dr--dry_run Print only the template
11
11
  -cj--clean_job Clean job
12
- --drbbt Use development version of rbbt
12
+ --drbbt* Use development version of rbbt
13
13
  -sing--singularity Use Singularity
14
+ -ug--user_group* Use alternative user group for group project directory
14
15
  -c--contain* Contain in directory (using Singularity)
15
16
  -s--sync* Contain in directory and sync jobs
16
17
  -e--exclusive Make exclusive use of the node
@@ -21,8 +22,9 @@ $slurm_options = SOPT.get <<EOF
21
22
  -t--tail Tail the logs
22
23
  -q--queue* Queue
23
24
  -t--task_cpus* Tasks
25
+ -W--workflows* Additional workflows
24
26
  -tm--time* Time
25
- -S--server* SLURM login node
27
+ -rmb--remove_slurm_basedir Remove the SLURM working directory (command, STDIN, exit status, ...)
26
28
  EOF
27
29
 
28
30
  class Step
@@ -30,9 +32,14 @@ class Step
30
32
  if done?
31
33
  self.load
32
34
  else
33
- Marenostrum::SLURM.run_job(self, SOPT::GOT_OPTIONS.merge($slurm_options))
35
+ begin
36
+ Log.debug "Issuing SLURM job for #{self.path}"
37
+ HPC::SLURM.run_job(self, SOPT::GOT_OPTIONS.merge($slurm_options))
38
+ rescue HPC::SBATCH
39
+ end
34
40
  end
35
41
  end
36
42
  end
37
43
 
44
+ ARGV.concat ["-W", $slurm_options[:workflows]] if $slurm_options[:workflows]
38
45
  load Rbbt.share.rbbt_commands.workflow.task.find
@@ -17,6 +17,7 @@ $ rbbt workflow info <job-result>
17
17
  -a--all Print all info entries
18
18
  -r--recursive Print recursive input values
19
19
  -o--original Print original object
20
+ -w--width* Screen width
20
21
  EOF
21
22
 
22
23
  SOPT.usage if options[:help]
@@ -24,6 +25,7 @@ SOPT.usage if options[:help]
24
25
  file = ARGV.shift
25
26
  all = options.delete :all
26
27
  recursive = options.delete :recursive
28
+ width = (options.delete(:width) || 80).to_i
27
29
 
28
30
  def get_step(file)
29
31
  file = file.sub(/\.(info|files)/,'')
@@ -86,6 +88,7 @@ pid = info[:pid]
86
88
  exception = info[:exception]
87
89
  rest = info.keys - [:inputs, :dependencies, :status, :time_elapsed, :messages, :backtrace, :exception, :pid, :archived_info]
88
90
 
91
+
89
92
  puts Log.color(:magenta, "File") << ": " << step.path
90
93
  puts Log.color(:magenta, "Status") << ": " << status_msg(status) << ((step.aborted? || step.error?) && step.recoverable_error? ? " (recoverable)" : "" ) << (step.dirty? ? " (dirty)" : "")
91
94
  puts Log.color(:magenta, "Pid") << ": " << pid_msg(pid, status.to_s == "done")
@@ -96,14 +99,14 @@ if inputs and inputs.any?
96
99
  inputs.each do |input,value|
97
100
  case value
98
101
  when nil
99
- puts Misc.format_definition_list_item(" " + input.to_s, 'nil', 80, 20, :blue)
102
+ puts Misc.format_definition_list_item(" " + input.to_s, 'nil', width, 20, :blue)
100
103
  when Array
101
- puts Misc.format_definition_list_item(" " + input.to_s, (value.length > 6 ? value[0..5]*"\n" << "\n" << "..." : value * "\n" ), 80, 20, :blue)
104
+ puts Misc.format_definition_list_item(" " + input.to_s, (value.length > 6 ? value[0..5]*"\n" << "\n" << "..." : value * "\n" ), width, 20, :blue)
102
105
  when TrueClass, FalseClass
103
- puts Misc.format_definition_list_item(" " + input.to_s, value.to_s, 80, 20, :blue)
106
+ puts Misc.format_definition_list_item(" " + input.to_s, value.to_s, width, 20, :blue)
104
107
  else
105
108
  text = value.to_s.split("\n")[0..5].compact * "\n\n"
106
- puts Misc.format_definition_list_item(" " + input.to_s, text, 80, 20, :blue)
109
+ puts Misc.format_definition_list_item(" " + input.to_s, text, width, 20, :blue)
107
110
  end
108
111
  end
109
112
  end
@@ -162,16 +165,16 @@ if recursive
162
165
  inputs.each do |input,value|
163
166
  case value
164
167
  when nil
165
- puts Misc.format_definition_list_item(" " << input.to_s, 'nil', 80, 20, :blue)
168
+ puts Misc.format_definition_list_item(" " << input.to_s, 'nil', width, 20, :blue)
166
169
  when Array
167
- puts Misc.format_definition_list_item(" " << input.to_s, (value.length > 6 ? (value[0..5])*"\n\n" << "\n\n" << "..." : value * "\n\n" ), 80, 20, :blue).gsub("\n\n","\n")
170
+ puts Misc.format_definition_list_item(" " << input.to_s, (value.length > 6 ? (value[0..5])*"\n\n" << "\n\n" << "..." : value * "\n\n" ), width, 20, :blue).gsub("\n\n","\n")
168
171
  when TrueClass, FalseClass
169
- puts Misc.format_definition_list_item(" " << input.to_s, value.to_s, 80, 20, :blue)
172
+ puts Misc.format_definition_list_item(" " << input.to_s, value.to_s, width, 20, :blue)
170
173
  else
171
- lines = value.to_s.split("\n").collect{|l| l.length >= 60 ? l[0..45] + " ..." : l }
174
+ lines = value.to_s.split("\n").collect{|l| l.length >= width - 5 ? l[0..width - 5] + " ..." : l }
172
175
  text = lines[0..5].compact * "\n\n"
173
176
  text << "\n\n...\n\n" if lines.length > 6
174
- puts Misc.format_definition_list_item(" " << input.to_s, text, 80, 20, :blue).gsub("\n\n","\n")
177
+ puts Misc.format_definition_list_item(" " << input.to_s, text, width, 20, :blue).gsub("\n\n","\n")
175
178
  end
176
179
  end
177
180
  end
@@ -156,6 +156,15 @@ for this dependency
156
156
  Open.read(file).reverse
157
157
  end
158
158
 
159
+ task :create_file => :text do |file|
160
+ Open.write(file('a'), "A")
161
+ Open.write(file('b'), "B")
162
+ "DONE"
163
+ end
164
+
165
+ dep_task :reverse_step_file, TestWF, :reverse_file do |jobname, options, dependencies|
166
+ dep = dependencies.flatten.first
167
+ end
159
168
 
160
169
  end
161
170
 
@@ -377,9 +386,9 @@ class TestWorkflow < Test::Unit::TestCase
377
386
  job.run
378
387
  Misc.with_env "RBBT_UPDATE", 'true' do
379
388
  assert job.checks.select{|d| d.task_name.to_s == "t1" }.any?
380
- job = TestWF.job(:t3)
381
- job.step(:t1).clean
382
- assert job.checks.select{|d| d.task_name.to_s == "t1" }.empty?
389
+ #job = TestWF.job(:t3)
390
+ #job.step(:t1).clean
391
+ #assert job.checks.select{|d| d.task_name.to_s == "t1" }.empty?
383
392
  job = TestWF.job(:t3).recursive_clean
384
393
  job.run
385
394
  assert job.checks.select{|d| d.task_name.to_s == "t1" }.any?
@@ -418,7 +427,7 @@ class TestWorkflow < Test::Unit::TestCase
418
427
  TmpFile.with_file do |dir|
419
428
  Path.setup(dir)
420
429
  Step.save_job_inputs(job, dir)
421
- assert_equal Dir.glob(dir + "/*"), [dir.file.find + '.read']
430
+ assert_equal Dir.glob(dir + "/*"), [dir.file.find + '.yaml']
422
431
  inputs = Workflow.load_inputs(dir, [:file], :file => :file)
423
432
  assert_equal inputs, {:file => 'code'}
424
433
  end
@@ -440,4 +449,27 @@ class TestWorkflow < Test::Unit::TestCase
440
449
  end
441
450
  end
442
451
  end
452
+
453
+ def test_input_step_file_check
454
+ job = TestWF.job(:t3).recursive_clean
455
+ job.run
456
+ Misc.with_env "RBBT_UPDATE", 'true' do
457
+ assert job.checks.select{|d| d.task_name.to_s == "t1" }.any?
458
+ #job = TestWF.job(:t3)
459
+ #job.step(:t1).clean
460
+ #assert job.checks.select{|d| d.task_name.to_s == "t1" }.empty?
461
+ job = TestWF.job(:t3).recursive_clean
462
+ job.run
463
+ assert job.checks.select{|d| d.task_name.to_s == "t1" }.any?
464
+ job = TestWF.job(:t3)
465
+ sleep 1
466
+ Open.touch job.step(:t1).path
467
+ Misc.with_env "RBBT_UPDATE", "false" do
468
+ assert job.updated?
469
+ end
470
+ Misc.with_env "RBBT_UPDATE", "true" do
471
+ assert ! job.updated?
472
+ end
473
+ end
474
+ end
443
475
  end
@@ -566,7 +566,7 @@ row2,CC
566
566
  tsv3.keys.each{|k| tsv3[k] = nil if tsv3[k] == ""}
567
567
  end
568
568
 
569
- assert_equal tsv1.attach(tsv2, :complete => true).attach(tsv3, :complete => true)["row1"], [nil, "B", nil]
569
+ assert_equal [nil, "B", nil], tsv1.attach(tsv2, :complete => true).attach(tsv3, :complete => true)["row1"]
570
570
  end
571
571
 
572
572
  def test_attach_index_both_non_key
@@ -597,10 +597,10 @@ A Id3
597
597
 
598
598
  tsv1 = tsv2 = nil
599
599
 
600
- tsv1 = Rbbt.tmp.test.test1.data.tsv :double, :sep => /\s+/
601
- tsv2 = Rbbt.tmp.test.test2.data.tsv :double, :sep => /\s+/
600
+ tsv1 = Rbbt.tmp.test.test1.data.produce(true).tsv :double, :sep => /\s+/
601
+ tsv2 = Rbbt.tmp.test.test2.data.produce(true).tsv :double, :sep => /\s+/
602
602
 
603
- tsv2.identifiers = Rbbt.tmp.test.test2.identifiers.produce.find #.to_s
603
+ tsv2.identifiers = Rbbt.tmp.test.test2.identifiers.produce(true).produce.find #.to_s
604
604
 
605
605
  tsv1.attach tsv2, :fields => ["ValueE"] #, :persist_input => true
606
606
  Log.tsv tsv1
@@ -627,12 +627,92 @@ E B
627
627
 
628
628
  tsv1 = tsv2 = nil
629
629
 
630
- tsv1 = Rbbt.tmp.test.test1.data.tsv :double, :sep => /\s+/
631
- tsv2 = Rbbt.tmp.test.test2.data.tsv :double, :sep => /\s+/
630
+ tsv1 = Rbbt.tmp.test.test1.data.produce(true).tsv :double, :sep => /\s+/
631
+ tsv2 = Rbbt.tmp.test.test2.data.produce(true).tsv :double, :sep => /\s+/
632
632
 
633
633
  tsv1.attach tsv2, :fields => ["ValueE"] #, :persist_input => true
634
634
  Log.tsv tsv1
635
635
 
636
636
  end
637
+
638
+ def test_attach_complete
639
+ content1 =<<-EOF
640
+ #: :sep=/\\s+/
641
+ #Id ValueA
642
+ row1 a|aa|aaa
643
+ row2 A
644
+ EOF
645
+
646
+ content2 =<<-EOF
647
+ #: :sep=/\\s+/
648
+ #Id ValueB
649
+ row1 b
650
+ row3 C
651
+ EOF
652
+ Rbbt.claim Rbbt.tmp.test.test1.data, :string, content1
653
+ Rbbt.claim Rbbt.tmp.test.test2.data, :string, content2
654
+
655
+ tsv1 = tsv2 = nil
656
+
657
+ tsv1 = Rbbt.tmp.test.test1.data.produce(true).tsv :double, :sep => /\s+/
658
+ tsv2 = Rbbt.tmp.test.test2.data.produce(true).tsv :double, :sep => /\s+/
659
+
660
+ tsv1.attach tsv2, :complete => true
661
+ assert_equal [[], ["C"]], tsv1["row3"]
662
+
663
+ tsv1 = Rbbt.tmp.test.test1.data.produce(true).tsv :double, :sep => /\s+/
664
+ tsv2 = Rbbt.tmp.test.test2.data.produce(true).tsv :double, :sep => /\s+/
665
+
666
+ ppp tsv1.attach tsv2, :complete => ["AA"]
667
+ tsv1.attach tsv2, :complete => ["AA"]
668
+ assert_equal [["AA"], ["C"]], tsv1["row3"]
669
+ end
670
+
671
+ def test_attach_complete_identifiers
672
+ content1 =<<-EOF
673
+ #: :sep=/\\s+/
674
+ #Id ValueA
675
+ row1 a|aa|aaa
676
+ row2 A
677
+ EOF
678
+
679
+ content2 =<<-EOF
680
+ #: :sep=/\\s+/
681
+ #Id2 ValueB
682
+ ROW_1 b
683
+ ROW_2 C
684
+ EOF
685
+
686
+ identifiers =<<-EOF
687
+ #: :sep=/\\s+/
688
+ #Id Id2
689
+ row1 ROW_1
690
+ row2 ROW_2
691
+ row3 ROW_3
692
+ EOF
693
+ Rbbt.claim Rbbt.tmp.test.test1.data, :string, content1
694
+ Rbbt.claim Rbbt.tmp.test.test2.data, :string, content2
695
+ Rbbt.claim Rbbt.tmp.test.identifiers.data, :string, identifiers
696
+
697
+ tsv1 = tsv2 = nil
698
+
699
+ tsv1 = Rbbt.tmp.test.test1.data.produce(true).tsv :double, :sep => /\s+/
700
+ tsv2 = Rbbt.tmp.test.test2.data.produce(true).tsv :double, :sep => /\s+/
701
+ ids = Rbbt.tmp.test.identifiers.data.produce(true).tsv :double, :sep => /\s+/
702
+
703
+ tsv1.identifiers = ids
704
+
705
+ tsv1.attach tsv2
706
+ assert_equal [["A"], ["C"]], tsv1["row2"]
707
+
708
+ tsv1 = Rbbt.tmp.test.test1.data.produce(true).tsv :double, :sep => /\s+/
709
+ tsv2 = Rbbt.tmp.test.test2.data.produce(true).tsv :double, :sep => /\s+/
710
+ ids = Rbbt.tmp.test.identifiers.data.produce(true).tsv :double, :sep => /\s+/
711
+
712
+ tsv1.identifiers = ids
713
+
714
+ tsv1.attach tsv2, :complete => true
715
+ assert_equal [["A"], ["C"]], tsv1["row2"]
716
+ end
637
717
  end
638
718