libis-workflow-activerecord 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +57 -0
  3. data/.travis.yml +50 -0
  4. data/Gemfile +3 -0
  5. data/README.md +13 -0
  6. data/Rakefile +85 -0
  7. data/db/config.yml +71 -0
  8. data/db/migrate/001_create_work_items_table.rb +18 -0
  9. data/db/migrate/002_create_jobs_table.rb +27 -0
  10. data/db/schema.rb +72 -0
  11. data/db/setup_db.rb +14 -0
  12. data/db/travis.config.yml +4 -0
  13. data/lib/libis-workflow-activerecord.rb +1 -0
  14. data/lib/libis/workflow/activerecord.rb +25 -0
  15. data/lib/libis/workflow/activerecord/base.rb +79 -0
  16. data/lib/libis/workflow/activerecord/config.rb +30 -0
  17. data/lib/libis/workflow/activerecord/helpers/hash_serializer.rb +32 -0
  18. data/lib/libis/workflow/activerecord/helpers/property_helper.rb +40 -0
  19. data/lib/libis/workflow/activerecord/helpers/status_serializer.rb +30 -0
  20. data/lib/libis/workflow/activerecord/job.rb +73 -0
  21. data/lib/libis/workflow/activerecord/run.rb +99 -0
  22. data/lib/libis/workflow/activerecord/version.rb +7 -0
  23. data/lib/libis/workflow/activerecord/work_item.rb +90 -0
  24. data/lib/libis/workflow/activerecord/worker.rb +20 -0
  25. data/lib/libis/workflow/activerecord/workflow.rb +40 -0
  26. data/libis-workflow-activerecord.gemspec +42 -0
  27. data/spec/db_env.sh +5 -0
  28. data/spec/db_start.sh +5 -0
  29. data/spec/items.rb +3 -0
  30. data/spec/items/test_dir_item.rb +18 -0
  31. data/spec/items/test_file_item.rb +29 -0
  32. data/spec/items/test_item.rb +6 -0
  33. data/spec/items/test_run.rb +8 -0
  34. data/spec/job_spec.rb +10 -0
  35. data/spec/run_spec.rb +4 -0
  36. data/spec/spec_helper.rb +37 -0
  37. data/spec/tasks/camelize_name.rb +13 -0
  38. data/spec/tasks/checksum_tester.rb +34 -0
  39. data/spec/tasks/collect_files.rb +52 -0
  40. data/spec/test_job.rb +6 -0
  41. data/spec/test_workflow.rb +6 -0
  42. data/spec/test_workitem.rb +7 -0
  43. data/spec/workflow_spec.rb +208 -0
  44. data/spec/workitem_spec.rb +366 -0
  45. metadata +245 -0
@@ -0,0 +1,20 @@
1
+ require 'libis/workflow/worker'
2
+ require 'sidekiq'
3
+
4
+ module Libis
5
+ module Workflow
6
+ module ActiveRecord
7
+
8
+ class Worker < Libis::Workflow::Worker
9
+
10
+ def get_job(job_config)
11
+ job_name = job_config.delete(:name)
12
+ job = ::Libis::Workflow::ActiveRecord::Job.find(name: job_name).first
13
+ raise RuntimeError.new "Workflow #{job_name} not found" unless job.is_a? ::Libis::Workflow::ActiveRecord::Job
14
+ job
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ require 'libis/workflow/activerecord'
2
+ require 'libis/workflow/base/workflow'
3
+ require 'libis/tools/config_file'
4
+ require 'libis/tools/extend/hash'
5
+
6
+ module Libis
7
+ module Workflow
8
+ module ActiveRecord
9
+
10
+ class Workflow < ::ActiveRecord::Base
11
+ include ::Libis::Workflow::Base::Workflow
12
+ include ::Libis::Workflow::ActiveRecord::Base
13
+
14
+ # noinspection RailsParamDefResolve
15
+ has_many :jobs,
16
+ -> {order('id')},
17
+ class_name: Libis::Workflow::ActiveRecord::Job.to_s,
18
+ foreign_key: :workflow_id,
19
+ autosave: true
20
+
21
+ def self.from_hash(hash)
22
+ self.create_from_hash(hash, [:name]) do |item, cfg|
23
+ item.configure(cfg.merge('name' => item.name))
24
+ cfg.clear
25
+ end
26
+ end
27
+
28
+ def self.load(file_or_hash)
29
+ config = Libis::Tools::ConfigFile.new
30
+ config << file_or_hash
31
+ return nil if config.empty?
32
+ workflow = self.new
33
+ workflow.configure(config.to_hash.key_symbols_to_strings(recursive: true))
34
+ workflow
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'libis/workflow/activerecord/version'
7
+ require 'date'
8
+
9
+ Gem::Specification.new do |spec|
10
+ spec.name = 'libis-workflow-activerecord'
11
+ spec.version = ::Libis::Workflow::ActiveRecord::VERSION
12
+ spec.date = Date.today.to_s
13
+
14
+ spec.summary = %q{ActiveRecord persistence for the LIBIS Workflow framework.}
15
+ spec.description = %q{Class implementations that use ActiveRecord storage for the LIBIS Workflow framework.}
16
+
17
+ spec.author = 'Kris Dekeyser'
18
+ spec.email = 'kris.dekeyser@libis.be'
19
+ spec.homepage = 'https://github.com/libis/workflow-activerecord'
20
+ spec.license = 'MIT'
21
+
22
+ spec.platform = Gem::Platform::JAVA if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
23
+
24
+ spec.files = `git ls-files -z`.split("\0")
25
+ spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
26
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
27
+
28
+ spec.require_paths = ['lib']
29
+
30
+ spec.add_runtime_dependency 'libis-workflow', '~> 2.0'
31
+ spec.add_runtime_dependency 'activerecord', '~> 5'
32
+
33
+ spec.add_development_dependency 'bundler', '~> 1.6'
34
+ spec.add_development_dependency 'rake'
35
+ spec.add_development_dependency 'rspec'
36
+ spec.add_development_dependency 'awesome_print'
37
+ spec.add_development_dependency 'database_cleaner'
38
+ spec.add_development_dependency 'coveralls'
39
+ spec.add_development_dependency 'sqlite3'
40
+ spec.add_runtime_dependency 'pg'
41
+
42
+ end
data/spec/db_env.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ export DB_HOST="kahana.mongohq.com:10053"
3
+ export DB_HOST="localhost:27017"
4
+ export DB_USER="test"
5
+ export DB_PASS="abc123"
data/spec/db_start.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ cd `dirname $0`
3
+ mkdir -p ./data/db
4
+ /opt/mongodb/bin/mongod --dbpath ./data/db
5
+
data/spec/items.rb ADDED
@@ -0,0 +1,3 @@
1
+ require_relative 'items/test_dir_item'
2
+ require_relative 'items/test_file_item'
3
+ require_relative 'items/test_run'
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+ require 'libis/workflow'
3
+ require_relative 'test_item'
4
+
5
+ class TestDirItem < TestItem
6
+
7
+ include ::Libis::Workflow::Base::DirItem
8
+
9
+ def name=(dir)
10
+ raise RuntimeError, "'#{dir}' is not a directory" unless File.directory? dir
11
+ super dir
12
+ end
13
+
14
+ def name
15
+ self.properties['name'] || super
16
+ end
17
+
18
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ require 'libis-tools'
3
+ require 'libis-workflow'
4
+
5
+ require_relative 'test_item'
6
+
7
+ class TestFileItem < TestItem
8
+
9
+ include ::Libis::Workflow::Base::FileItem
10
+
11
+ def filename=(file)
12
+ raise RuntimeError, "'#{file}' is not a file" unless File.file? file
13
+ set_checksum :SHA256, ::Libis::Tools::Checksum.hexdigest(file, :SHA256)
14
+ super file
15
+ end
16
+
17
+ def name
18
+ self.properties['name'] || super
19
+ end
20
+
21
+ def filesize
22
+ properties['size']
23
+ end
24
+
25
+ def fixity_check(checksum)
26
+ properties['checksum'] == checksum
27
+ end
28
+
29
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ require 'libis/workflow/activerecord/work_item'
4
+
5
+ class TestItem < Libis::Workflow::ActiveRecord::WorkItem
6
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+ require 'libis-workflow-activerecord'
3
+
4
+ class TestRun <::Libis::Workflow::ActiveRecord::Run
5
+
6
+ def name; 'TestRun'; end
7
+
8
+ end
data/spec/job_spec.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rspec'
2
+ require_relative 'spec_helper'
3
+
4
+ require 'libis-workflow-activerecord'
5
+ require_relative 'test_job'
6
+
7
+ describe 'TestJob' do
8
+
9
+ context
10
+ end
data/spec/run_spec.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'rspec'
2
+ require_relative 'spec_helper'
3
+
4
+ require 'libis-workflow-activerecord'
@@ -0,0 +1,37 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ require 'bundler/setup'
5
+ Bundler.setup
6
+
7
+ require 'rspec'
8
+ require 'libis-workflow-activerecord'
9
+
10
+ require 'database_cleaner'
11
+ require 'stringio'
12
+
13
+ RSpec.configure do |cfg|
14
+
15
+ cfg.before :suite do
16
+ # noinspection RubyResolve
17
+ ::Libis::Workflow::ActiveRecord.configure do |cfg|
18
+ cfg.logger.appenders =
19
+ ::Logging::Appenders.string_io('StringIO', layout: ::Libis::Tools::Config.get_log_formatter)
20
+ cfg.itemdir = File.join(File.dirname(__FILE__), 'items')
21
+ cfg.taskdir = File.join(File.dirname(__FILE__), 'tasks')
22
+ cfg.workdir = File.join(File.dirname(__FILE__), 'work')
23
+ cfg.database_connect 'db/config.yml', :test
24
+ end
25
+ DatabaseCleaner.clean_with :truncation
26
+ end
27
+
28
+ cfg.before :each do
29
+ # DatabaseCleaner.strategy = :transaction
30
+ # DatabaseCleaner.start
31
+ end
32
+
33
+ cfg.after :each do
34
+ # DatabaseCleaner.clean
35
+ end
36
+
37
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ require 'backports/rails/string'
3
+ require 'libis-workflow'
4
+
5
+ class CamelizeName < ::Libis::Workflow::Task
6
+
7
+ def process(item)
8
+ return unless (item.is_a?(TestFileItem) || item.is_a?(TestDirItem))
9
+ item.properties['name'] = item.name.camelize
10
+ item.save!
11
+ end
12
+
13
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+ require 'libis/tools/checksum'
3
+
4
+ require 'libis/exceptions'
5
+ require 'libis-workflow'
6
+
7
+ class ChecksumTester < ::Libis::Workflow::Task
8
+
9
+ parameter checksum_type: nil,
10
+ description: 'Checksum type to use.',
11
+ constraint: ::Libis::Tools::Checksum::CHECKSUM_TYPES.map {|x| x.to_s}
12
+ parameter checksum_file: nil, description: 'File with checksums of the files.'
13
+
14
+ def process(item)
15
+ return unless item.is_a? TestFileItem
16
+
17
+ checksum_type = parameter(:checksum_type)
18
+
19
+ if checksum_type.nil?
20
+ ::Libis::Tools::Checksum::CHECKSUM_TYPES.each do |x|
21
+ test_checksum(item, x) if item.checksum(x)
22
+ end
23
+ else
24
+ test_checksum(item, checksum_type)
25
+ end
26
+ end
27
+
28
+ def test_checksum(item, checksum_type)
29
+ checksum = ::Libis::Tools::Checksum.hexdigest(item.fullpath, checksum_type.to_sym)
30
+ return if item.checksum(checksum_type) == checksum
31
+ raise ::Libis::WorkflowError, "Checksum test #{checksum_type} failed for #{item.filepath}"
32
+ end
33
+
34
+ end
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ require 'libis/exceptions'
3
+
4
+ require_relative '../items'
5
+
6
+ class CollectFiles < ::Libis::Workflow::Task
7
+
8
+ parameter location: '.',
9
+ description: 'Dir location to start scanning for files.'
10
+ parameter subdirs: false,
11
+ description: 'Look for files in subdirs too.'
12
+ parameter selection: nil,
13
+ description: 'Only select files that match the given regular expression. Ignored if empty.'
14
+
15
+ def process(item)
16
+ if item.is_a? TestRun
17
+ add_item(item, parameter(:location))
18
+ elsif item.is_a? TestDirItem
19
+ collect_files(item, item.fullpath)
20
+ end
21
+ end
22
+
23
+ def collect_files(item, dir)
24
+ glob_string = dir
25
+ glob_string = File.join(glob_string, '**') if parameter(:subdirs)
26
+ glob_string = File.join(glob_string, '*')
27
+
28
+ Dir.glob(glob_string).select do |x|
29
+ parameter(:selection) && !parameter(:selection).empty? ? x =~ Regexp.new(parameter(:selection)) : true
30
+ end.sort.each do |file|
31
+ next if %w'. ..'.include? file
32
+ add_item(item, file)
33
+ end
34
+ end
35
+
36
+ def add_item(item, file)
37
+ child = if File.file?(file)
38
+ TestFileItem.new
39
+ elsif File.directory?(file)
40
+ TestDirItem.new
41
+ else
42
+ error 'Bad file type encountered: %s', file
43
+ nil
44
+ end
45
+ unless child
46
+ return
47
+ end
48
+ child.filename = file
49
+ item << child
50
+ end
51
+
52
+ end
data/spec/test_job.rb ADDED
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ require 'libis/workflow/activerecord/job'
4
+
5
+ class TestJob < ::Libis::Workflow::ActiveRecord::Job
6
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ require 'libis/workflow/activerecord/workflow'
4
+
5
+ class TestWorkflow < ::Libis::Workflow::ActiveRecord::Workflow
6
+ end
@@ -0,0 +1,7 @@
1
+ require 'libis/workflow/activerecord/work_item'
2
+
3
+ class TestWorkItem < ::Libis::Workflow::ActiveRecord::WorkItem
4
+ property_field :abc,
5
+ reader: lambda {|value| value.blank? ? :xyz : value.to_sym},
6
+ writer: lambda {|value| value.blank? ? nil : value.to_s}
7
+ end
@@ -0,0 +1,208 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rspec'
4
+ require 'stringio'
5
+
6
+ require 'libis-workflow-activerecord'
7
+
8
+ require_relative 'spec_helper'
9
+ require_relative 'test_job'
10
+ require_relative 'test_workflow'
11
+ require_relative 'items'
12
+
13
+ require 'awesome_print'
14
+
15
+ describe 'TestWorkflow' do
16
+
17
+ before :each do
18
+ ::Libis::Workflow::ActiveRecord::Config.logger.appenders.first.clear
19
+ end
20
+
21
+ dirname = File.absolute_path(File.join(File.dirname(__FILE__), 'items'))
22
+
23
+ let(:logoutput) { ::Libis::Workflow::ActiveRecord::Config.logger.appenders.first.sio }
24
+
25
+ let(:workflow) {
26
+ wf = TestWorkflow.find_or_initialize_by(name: 'TestWorkflow')
27
+ wf.configure(
28
+ 'name' => 'TestWorkflow',
29
+ 'description' => 'Workflow for testing',
30
+ 'tasks' => [
31
+ {'class' => 'CollectFiles', 'recursive' => true},
32
+ {
33
+ 'name' => 'ProcessFiles',
34
+ 'subitems' => true,
35
+ 'tasks' => [
36
+ {'class' => 'ChecksumTester', 'recursive' => true},
37
+ {'class' => 'CamelizeName', 'recursive' => true}
38
+ ]
39
+ }
40
+ ],
41
+ 'input' => {
42
+ 'dirname' => {'default' => '.', 'propagate_to' => 'CollectFiles#location'},
43
+ 'checksum_type' => {'default' => 'SHA1', 'propagate_to' => 'ProcessFiles/ChecksumTester'}
44
+ }
45
+ )
46
+ wf.save!
47
+ wf
48
+ }
49
+ let(:job) {
50
+ job = TestJob.from_hash(
51
+ 'name' => 'TestJob',
52
+ 'description' => 'Job for testing',
53
+ 'workflow' => 'TestWorkflow',
54
+ 'run_object' => 'TestRun',
55
+ 'input' => {'dirname' => dirname, 'checksum_type' => 'SHA256'},
56
+ 'log_to_file' => false,
57
+ 'log_each_run' => false
58
+ )
59
+
60
+ # job = TestJob.find_or_initialize_by(name: 'TestJob')
61
+ # job.configure(
62
+ # 'name' => 'TestJob',
63
+ # 'description' => 'Job for testing',
64
+ # 'workflow' => workflow,
65
+ # 'run_object' => 'TestRun',
66
+ # 'input' => {'dirname' => dirname, 'checksum_type' => 'SHA256'},
67
+ # 'log_to_file' => false
68
+ # )
69
+
70
+ # noinspection RubyResolve
71
+ job.runs.each { |run| run.destroy! }
72
+ job.save!
73
+ job
74
+ }
75
+
76
+ let(:run) { job.execute }
77
+
78
+ it 'should contain three tasks' do
79
+
80
+ expect(workflow.config['tasks'].size).to eq 2
81
+ expect(workflow.config['tasks'].first['class']).to eq 'CollectFiles'
82
+ expect(workflow.config['tasks'].last['name']).to eq 'ProcessFiles'
83
+
84
+ end
85
+
86
+ it 'find workflow' do
87
+ workflow
88
+ wf = TestWorkflow.first
89
+ expect(wf.nil?).to eq false
90
+ expect(wf.name).to eq 'TestWorkflow'
91
+ expect(wf.description).to eq 'Workflow for testing'
92
+ expect(wf.input.count).to eq 2
93
+ expect(wf.input[:dirname][:default]).to eq '.'
94
+ expect(wf.config['tasks'].count).to eq 2
95
+ expect(wf.config['tasks'][0]['class']).to eq 'CollectFiles'
96
+ expect(wf.config['tasks'][0]['recursive']).to eq true
97
+ expect(wf.config['tasks'][1]['name']).to eq 'ProcessFiles'
98
+ expect(wf.config['tasks'][1]['subitems']).to eq true
99
+ expect(wf.config['tasks'][1]['tasks'].count).to eq 2
100
+ expect(wf.config['tasks'][1]['tasks'][0]['class']).to eq 'ChecksumTester'
101
+ expect(wf.config['tasks'][1]['tasks'][0]['recursive']).to eq true
102
+ expect(wf.config['tasks'][1]['tasks'][1]['class']).to eq 'CamelizeName'
103
+ expect(wf.config['tasks'][1]['tasks'][1]['recursive']).to eq true
104
+ end
105
+
106
+ it 'should camelize the workitem name' do
107
+
108
+ expect(run.options['CollectFiles']['location']).to eq dirname
109
+
110
+ expect(run.items.size).to eq 1
111
+ expect(run.items.count).to eq 1
112
+ expect(run.size).to eq 1
113
+ expect(run.count).to eq 1
114
+ expect(run.items.first.class).to eq TestDirItem
115
+ expect(run.first.class).to eq TestDirItem
116
+ expect(run.first.items.size).to eq 4
117
+ expect(run.first.items.count).to eq 4
118
+ expect(run.first.size).to eq 4
119
+ expect(run.first.count).to eq 4
120
+ expect(run.first.items.first.class).to eq TestFileItem
121
+ expect(run.first.first.class).to eq TestFileItem
122
+
123
+ run.items.first.each_with_index do |x, i|
124
+ expect(x.name).to eq %w'TestDirItem.rb TestFileItem.rb TestItem.rb TestRun.rb'[i]
125
+ end
126
+ end
127
+
128
+ it 'should return expected debug output' do
129
+
130
+ run
131
+
132
+ sample_out = <<STR
133
+ Run - TestRun : Ingest run started.
134
+ Run - TestRun : Running subtask (1/2): CollectFiles
135
+ CollectFiles - TestRun : Processing subitem (1/1): items
136
+ CollectFiles - items : Processing subitem (1/4): test_dir_item.rb
137
+ CollectFiles - items : Processing subitem (2/4): test_file_item.rb
138
+ CollectFiles - items : Processing subitem (3/4): test_item.rb
139
+ CollectFiles - items : Processing subitem (4/4): test_run.rb
140
+ CollectFiles - items : 4 of 4 subitems passed
141
+ CollectFiles - TestRun : 1 of 1 subitems passed
142
+ Run - TestRun : Running subtask (2/2): ProcessFiles
143
+ ProcessFiles - TestRun : Running subtask (1/2): ChecksumTester
144
+ ProcessFiles/ChecksumTester - TestRun : Processing subitem (1/1): items
145
+ ProcessFiles/ChecksumTester - items : Processing subitem (1/4): test_dir_item.rb
146
+ ProcessFiles/ChecksumTester - items : Processing subitem (2/4): test_file_item.rb
147
+ ProcessFiles/ChecksumTester - items : Processing subitem (3/4): test_item.rb
148
+ ProcessFiles/ChecksumTester - items : Processing subitem (4/4): test_run.rb
149
+ ProcessFiles/ChecksumTester - items : 4 of 4 subitems passed
150
+ ProcessFiles/ChecksumTester - TestRun : 1 of 1 subitems passed
151
+ ProcessFiles - TestRun : Running subtask (2/2): CamelizeName
152
+ ProcessFiles/CamelizeName - TestRun : Processing subitem (1/1): items
153
+ ProcessFiles/CamelizeName - Items : Processing subitem (1/4): test_dir_item.rb
154
+ ProcessFiles/CamelizeName - Items : Processing subitem (2/4): test_file_item.rb
155
+ ProcessFiles/CamelizeName - Items : Processing subitem (3/4): test_item.rb
156
+ ProcessFiles/CamelizeName - Items : Processing subitem (4/4): test_run.rb
157
+ ProcessFiles/CamelizeName - Items : 4 of 4 subitems passed
158
+ ProcessFiles/CamelizeName - TestRun : 1 of 1 subitems passed
159
+ ProcessFiles - TestRun : Done
160
+ Run - TestRun : Done
161
+ STR
162
+
163
+ sample_out = sample_out.lines.to_a
164
+ output = logoutput.string.lines
165
+
166
+ # puts output
167
+
168
+ expect(run.status_log.count).to eq 5
169
+ item = run.items.first
170
+ expect(item.status_log.count).to eq 3
171
+
172
+ expect(output.count).to eq sample_out.count
173
+ output.each_with_index do |o, i|
174
+ expect(o.strip).to match(/#{Regexp.escape sample_out[i].strip}$/)
175
+ end
176
+
177
+ end
178
+
179
+ # noinspection RubyResolve
180
+ it 'find run' do
181
+ run
182
+ my_job = TestJob.first
183
+ expect(my_job).to eq job
184
+ expect(my_job.runs.all.count).to eq 1
185
+ my_run = my_job.runs.all.first
186
+ expect(my_run).to eq run
187
+ expect(my_run.status).to eq :DONE
188
+ end
189
+
190
+ # noinspection RubyResolve
191
+ it 'find first item' do
192
+ item = run.items.first
193
+ expect(item.nil?).to eq false
194
+ expect(item.is_a? TestDirItem).to eq true
195
+ expect(item.properties['name']).to eq 'Items'
196
+ end
197
+
198
+ it 'move item in relation' do
199
+ item = run.items.first
200
+ sub_item = item.items.first
201
+ expect(run.size).to eq 1
202
+ expect(item.size).to eq 4
203
+ run.move_item(sub_item)
204
+ expect(run.size).to eq 2
205
+ expect(item.size).to eq 3
206
+ end
207
+
208
+ end