filecluster 0.0.3
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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +11 -0
- data/TODO +9 -0
- data/bin/fc-daemon +70 -0
- data/bin/fc-manage +102 -0
- data/bin/fc-setup-db +47 -0
- data/filecluster.gemspec +24 -0
- data/lib/daemon.rb +91 -0
- data/lib/daemon/base_thread.rb +13 -0
- data/lib/daemon/check_thread.rb +12 -0
- data/lib/daemon/global_daemon_thread.rb +60 -0
- data/lib/daemon/task_thread.rb +37 -0
- data/lib/fc/base.rb +82 -0
- data/lib/fc/db.rb +168 -0
- data/lib/fc/error.rb +12 -0
- data/lib/fc/item.rb +102 -0
- data/lib/fc/item_storage.rb +8 -0
- data/lib/fc/policy.rb +18 -0
- data/lib/fc/storage.rb +80 -0
- data/lib/fc/version.rb +3 -0
- data/lib/filecluster.rb +13 -0
- data/lib/manage.rb +4 -0
- data/lib/manage/policies.rb +70 -0
- data/lib/manage/show.rb +57 -0
- data/lib/manage/storages.rb +87 -0
- data/lib/utils.rb +76 -0
- data/test/base_test.rb +74 -0
- data/test/daemon_test.rb +98 -0
- data/test/db_test.rb +182 -0
- data/test/error_test.rb +15 -0
- data/test/functional_test.rb +97 -0
- data/test/helper.rb +17 -0
- data/test/item_test.rb +49 -0
- data/test/policy_test.rb +34 -0
- data/test/storage_test.rb +29 -0
- data/test/version_test.rb +7 -0
- metadata +199 -0
data/lib/fc/policy.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FC
|
4
|
+
class Policy < DbBase
|
5
|
+
set_table :policies, 'storages, copies'
|
6
|
+
|
7
|
+
def get_storages
|
8
|
+
FC::Storage.where("name IN (#{storages.split(',').map{|s| "'#{s}'"}.join(',')})")
|
9
|
+
end
|
10
|
+
|
11
|
+
# get available storage for object by size
|
12
|
+
def get_proper_storage(size, exclude = [])
|
13
|
+
get_storages.detect do |storage|
|
14
|
+
!exclude.include?(storage.name) && storage.up? && storage.size + size < storage.size_limit
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/fc/storage.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FC
|
4
|
+
class Storage < DbBase
|
5
|
+
set_table :storages, 'name, host, path, url, size, size_limit, check_time'
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :check_time_limit
|
9
|
+
end
|
10
|
+
@check_time_limit = 120 # ttl for up status check
|
11
|
+
|
12
|
+
def self.curr_host
|
13
|
+
@uname || @uname = `uname -n`.chomp
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(params = {})
|
17
|
+
path = (params['path'] || params[:path])
|
18
|
+
if path && !path.to_s.empty?
|
19
|
+
raise "Storage path must be like '/bla/bla../'" unless path.match(/^\/.*\/$/)
|
20
|
+
end
|
21
|
+
super params
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_check_time
|
25
|
+
self.check_time = Time.new.to_i
|
26
|
+
save
|
27
|
+
end
|
28
|
+
|
29
|
+
def check_time_delay
|
30
|
+
Time.new.to_i - check_time.to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def up?
|
34
|
+
Time.new.to_i - check_time.to_i <= self.class.check_time_limit
|
35
|
+
end
|
36
|
+
|
37
|
+
# copy local_path to storage
|
38
|
+
def copy_path(local_path, file_name)
|
39
|
+
cmd = self.class.curr_host == host ?
|
40
|
+
"cp -r #{local_path} #{self.path}#{file_name}" :
|
41
|
+
"scp -rB #{local_path} #{self.host}:#{self.path}#{file_name}"
|
42
|
+
r = `#{cmd} 2>&1`
|
43
|
+
raise r if $?.exitstatus != 0
|
44
|
+
end
|
45
|
+
|
46
|
+
# copy object to local_path
|
47
|
+
def copy_to_local(file_name, local_path)
|
48
|
+
cmd = self.class.curr_host == host ?
|
49
|
+
"cp -r #{self.path}#{file_name} #{local_path}" :
|
50
|
+
"scp -rB #{self.host}:#{self.path}#{file_name} #{local_path}"
|
51
|
+
r = `#{cmd} 2>&1`
|
52
|
+
raise r if $?.exitstatus != 0
|
53
|
+
end
|
54
|
+
|
55
|
+
# delete object from storage
|
56
|
+
def delete_file(file_name)
|
57
|
+
cmd = self.class.curr_host == host ?
|
58
|
+
"rm -rf #{self.path}#{file_name}" :
|
59
|
+
"ssh -oBatchMode=yes #{self.host} 'rm -rf #{self.path}#{file_name}'"
|
60
|
+
r = `#{cmd} 2>&1`
|
61
|
+
raise r if $?.exitstatus != 0
|
62
|
+
|
63
|
+
cmd = self.class.curr_host == host ?
|
64
|
+
"ls -la #{self.path}#{file_name}" :
|
65
|
+
"ssh -oBatchMode=yes #{self.host} 'ls -la #{self.path}#{file_name}'"
|
66
|
+
r = `#{cmd} 2>/dev/null`
|
67
|
+
raise "Path #{self.path}#{file_name} not deleted" unless r.empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
# return object size on storage
|
71
|
+
def file_size(file_name)
|
72
|
+
cmd = self.class.curr_host == host ?
|
73
|
+
"du -sb #{self.path}#{file_name}" :
|
74
|
+
"ssh -oBatchMode=yes #{self.host} 'du -sb #{self.path}#{file_name}'"
|
75
|
+
r = `#{cmd} 2>&1`
|
76
|
+
raise r if $?.exitstatus != 0
|
77
|
+
r.to_i
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/fc/version.rb
ADDED
data/lib/filecluster.rb
ADDED
data/lib/manage.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
def policies_list
|
2
|
+
policies = FC::Policy.where
|
3
|
+
if policies.size == 0
|
4
|
+
puts "No storages."
|
5
|
+
else
|
6
|
+
policies.each do |policy|
|
7
|
+
puts "##{policy.id} storages: #{policy.storages}, copies: #{policy.copies}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def policies_show
|
13
|
+
id = ARGV[2]
|
14
|
+
policy = FC::Policy.where('id = ?', id).first
|
15
|
+
if !policy
|
16
|
+
puts "Policy ##{id} not found."
|
17
|
+
else
|
18
|
+
count = FC::DB.connect.query("SELECT count(*) as cnt FROM #{FC::Item.table_name} WHERE policy_id = #{policy.id}").first['cnt']
|
19
|
+
puts %Q{Policy
|
20
|
+
ID: #{policy.id}
|
21
|
+
Storages: #{policy.storages}
|
22
|
+
Copies: #{policy.copies}
|
23
|
+
Items: #{count}}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def policies_add
|
28
|
+
puts "Add Policy"
|
29
|
+
storages = stdin_read_val('Storages')
|
30
|
+
copies = stdin_read_val('Copies').to_i
|
31
|
+
begin
|
32
|
+
policy = FC::Policy.new(:storages => storages, :copies => copies)
|
33
|
+
rescue Exception => e
|
34
|
+
puts "Error: #{e.message}"
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
puts %Q{\nPolicy
|
38
|
+
Storages: #{storages}
|
39
|
+
Copies: #{copies}}
|
40
|
+
s = Readline.readline("Continue? (y/n) ", false).strip.downcase
|
41
|
+
puts ""
|
42
|
+
if s == "y" || s == "yes"
|
43
|
+
begin
|
44
|
+
policy.save
|
45
|
+
rescue Exception => e
|
46
|
+
puts "Error: #{e.message}"
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
puts "ok"
|
50
|
+
else
|
51
|
+
puts "Canceled."
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def policies_rm
|
56
|
+
id = ARGV[2]
|
57
|
+
policy = FC::Policy.where('id = ?', id).first
|
58
|
+
if !policy
|
59
|
+
puts "Policy ##{id} not found."
|
60
|
+
else
|
61
|
+
s = Readline.readline("Continue? (y/n) ", false).strip.downcase
|
62
|
+
puts ""
|
63
|
+
if s == "y" || s == "yes"
|
64
|
+
policy.delete
|
65
|
+
puts "ok"
|
66
|
+
else
|
67
|
+
puts "Canceled."
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/manage/show.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
def show_current_host
|
2
|
+
puts "Current host: #{FC::Storage.curr_host}"
|
3
|
+
end
|
4
|
+
|
5
|
+
def show_global_daemon
|
6
|
+
r = FC::DB.connect.query("SELECT #{FC::DB.prefix}vars.*, UNIX_TIMESTAMP() as curr_time FROM #{FC::DB.prefix}vars WHERE name='global_daemon_host'").first
|
7
|
+
if r
|
8
|
+
puts "Global daemon run on #{r['val']}\nLast run #{r['curr_time']-r['time']} seconds ago."
|
9
|
+
else
|
10
|
+
puts "Global daemon is not runnning."
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def show_errors
|
15
|
+
count = ARGV[2] || 10
|
16
|
+
errors = FC::Error.where("1 ORDER BY id desc LIMIT #{count.to_i}")
|
17
|
+
if errors.size == 0
|
18
|
+
puts "No errors."
|
19
|
+
else
|
20
|
+
errors.each do |error|
|
21
|
+
puts "#{Time.at(error.time)} item_id: #{error.item_id}, item_storage_id: #{error.item_storage_id}, host: #{error.host}, message: #{error.message}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def show_host_info
|
27
|
+
host = ARGV[2] || FC::Storage.curr_host
|
28
|
+
storages = FC::Storage.where("host = ?", host)
|
29
|
+
if storages.size == 0
|
30
|
+
puts "No storages."
|
31
|
+
else
|
32
|
+
puts "Info for host #{host}"
|
33
|
+
storages.each do |storage|
|
34
|
+
counts = FC::DB.connect.query("SELECT status, count(*) as cnt FROM #{FC::ItemStorage.table_name} WHERE storage_name='#{Mysql2::Client.escape(storage.name)}' GROUP BY status")
|
35
|
+
str = "#{storage.name} #{size_to_human(storage.size)}/#{size_to_human(storage.size_limit)} "
|
36
|
+
str += "#{storage.up? ? colorize_string('UP', :green) : colorize_string('DOWN', :red)}"
|
37
|
+
str += " #{storage.check_time_delay} seconds ago" if storage.check_time
|
38
|
+
str += "\n"
|
39
|
+
counts.each do |r|
|
40
|
+
str += " Items storages #{r['status']}: #{r['cnt']}\n"
|
41
|
+
end
|
42
|
+
puts str
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def show_items_info
|
48
|
+
puts "Items by status:"
|
49
|
+
counts = FC::DB.connect.query("SELECT status, count(*) as cnt FROM #{FC::Item.table_name} WHERE 1 GROUP BY status")
|
50
|
+
counts.each do |r|
|
51
|
+
puts " #{r['status']}: #{r['cnt']}"
|
52
|
+
end
|
53
|
+
count = FC::DB.connect.query("SELECT count(*) as cnt FROM #{FC::Item.table_name} as i, #{FC::Policy.table_name} as p WHERE i.policy_id = p.id AND i.copies > 0 AND i.copies < p.copies AND i.status = 'ready'").first['cnt']
|
54
|
+
puts "Items to copy: #{count}"
|
55
|
+
count = FC::DB.connect.query("SELECT count(*) as cnt FROM #{FC::Item.table_name} as i, #{FC::Policy.table_name} as p WHERE i.policy_id = p.id AND i.copies > p.copies AND i.status = 'ready'").first['cnt']
|
56
|
+
puts "Items to delete: #{count}"
|
57
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
def storages_list
|
2
|
+
storages = FC::Storage.where("1 ORDER BY host")
|
3
|
+
if storages.size == 0
|
4
|
+
puts "No storages."
|
5
|
+
else
|
6
|
+
storages.each do |storage|
|
7
|
+
str = "#{colorize_string(storage.host, :yellow)} #{storage.name} #{size_to_human(storage.size)}/#{size_to_human(storage.size_limit)} "
|
8
|
+
str += "#{storage.up? ? colorize_string('UP', :green) : colorize_string('DOWN', :red)}"
|
9
|
+
str += " #{storage.check_time_delay} seconds ago" if storage.check_time
|
10
|
+
puts str
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def storages_show
|
16
|
+
name = ARGV[2]
|
17
|
+
storage = FC::Storage.where('name = ?', name).first
|
18
|
+
if !storage
|
19
|
+
puts "Storage #{name} not found."
|
20
|
+
else
|
21
|
+
count = FC::DB.connect.query("SELECT count(*) as cnt FROM #{FC::ItemStorage.table_name} WHERE storage_name='#{Mysql2::Client.escape(storage.name)}'").first['cnt']
|
22
|
+
puts %Q{Storage
|
23
|
+
Name: #{storage.name}
|
24
|
+
Host: #{storage.host}
|
25
|
+
Path: #{storage.path}
|
26
|
+
Url: #{storage.url}
|
27
|
+
Size: #{size_to_human storage.size}
|
28
|
+
Size limit: #{size_to_human storage.size_limit}
|
29
|
+
Check time: #{storage.check_time ? "#{Time.at(storage.check_time)} (#{storage.check_time_delay} seconds ago)" : ''}
|
30
|
+
Status: #{storage.up? ? colorize_string('UP', :green) : colorize_string('DOWN', :red)}
|
31
|
+
Items storages: #{count}}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def storages_add
|
36
|
+
host = FC::Storage.curr_host
|
37
|
+
puts "Add storage to host #{host}"
|
38
|
+
name = stdin_read_val('Name')
|
39
|
+
path = stdin_read_val('Path')
|
40
|
+
url = stdin_read_val('Url')
|
41
|
+
size_limit = human_to_size stdin_read_val('Size limit') {|val| "Size limit not is valid size." unless human_to_size(val)}
|
42
|
+
begin
|
43
|
+
storage = FC::Storage.new(:name => name, :host => host, :path => path, :url => url, :size_limit => size_limit)
|
44
|
+
size = storage.file_size('')
|
45
|
+
rescue Exception => e
|
46
|
+
puts "Error: #{e.message}"
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
puts %Q{\nStorage
|
50
|
+
Name: #{name}
|
51
|
+
Host: #{host}
|
52
|
+
Path: #{path}
|
53
|
+
Url: #{url}
|
54
|
+
Size: #{size_to_human size}
|
55
|
+
Size limit: #{size_to_human size_limit}}
|
56
|
+
s = Readline.readline("Continue? (y/n) ", false).strip.downcase
|
57
|
+
puts ""
|
58
|
+
if s == "y" || s == "yes"
|
59
|
+
storage.size = size
|
60
|
+
begin
|
61
|
+
storage.save
|
62
|
+
rescue Exception => e
|
63
|
+
puts "Error: #{e.message}"
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
puts "ok"
|
67
|
+
else
|
68
|
+
puts "Canceled."
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def storages_rm
|
73
|
+
name = ARGV[2]
|
74
|
+
storage = FC::Storage.where('name = ?', name).first
|
75
|
+
if !storage
|
76
|
+
puts "Storage #{name} not found."
|
77
|
+
else
|
78
|
+
s = Readline.readline("Continue? (y/n) ", false).strip.downcase
|
79
|
+
puts ""
|
80
|
+
if s == "y" || s == "yes"
|
81
|
+
storage.delete
|
82
|
+
puts "ok"
|
83
|
+
else
|
84
|
+
puts "Canceled."
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/utils.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
def option_parser_init(descriptions, text)
|
2
|
+
options = {}
|
3
|
+
optparse = OptionParser.new do |opts|
|
4
|
+
opts.banner = text
|
5
|
+
opts.separator "Options:"
|
6
|
+
|
7
|
+
descriptions.each_entry do |key, desc|
|
8
|
+
options[key] = desc[:default]
|
9
|
+
opts.on("-#{desc[:short]}", "--#{desc[:full]}#{desc[:no_val] ? '' : '='+desc[:full].upcase}", desc[:text]) {|s| options[key] = s }
|
10
|
+
end
|
11
|
+
opts.on_tail("-?", "--help", "Show this message") do
|
12
|
+
puts opts
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
opts.on_tail("-v", "--version", "Show version") do
|
16
|
+
puts FC::VERSION
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
end
|
20
|
+
optparse.parse!
|
21
|
+
options['optparse'] = optparse
|
22
|
+
options
|
23
|
+
end
|
24
|
+
|
25
|
+
def size_to_human(size)
|
26
|
+
return "0" if size == 0
|
27
|
+
units = %w{B KB MB GB TB}
|
28
|
+
e = (Math.log(size)/Math.log(1024)).floor
|
29
|
+
s = "%.2f" % (size.to_f / 1024**e)
|
30
|
+
s.sub(/\.?0*$/, units[e])
|
31
|
+
end
|
32
|
+
|
33
|
+
def human_to_size(size)
|
34
|
+
r = /^(\d+(\.\d+)?)\s*(.*)/
|
35
|
+
units = {'k' => 1024, 'm' => 1024*1024, 'g' => 1024*1024*1024, 't' => 1024*1024*1024*1024}
|
36
|
+
return nil unless matches = size.to_s.match(r)
|
37
|
+
unit = units[matches[3].to_s.strip.downcase[0]]
|
38
|
+
result = matches[1].to_f
|
39
|
+
result *= unit if unit
|
40
|
+
result.to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
def stdin_read_val(name)
|
44
|
+
while val = Readline.readline("#{name}: ", false).strip.downcase
|
45
|
+
if val.empty?
|
46
|
+
puts "Input non empty #{name}."
|
47
|
+
else
|
48
|
+
if block_given?
|
49
|
+
if err = yield(val)
|
50
|
+
puts err
|
51
|
+
else
|
52
|
+
return val
|
53
|
+
end
|
54
|
+
else
|
55
|
+
return val
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def colorize_string(str, color)
|
62
|
+
return str unless color
|
63
|
+
case color.to_s
|
64
|
+
when 'red'
|
65
|
+
color_code = 31
|
66
|
+
when 'green'
|
67
|
+
color_code = 32
|
68
|
+
when 'yellow'
|
69
|
+
color_code = 33
|
70
|
+
when 'pink'
|
71
|
+
color_code = 35
|
72
|
+
else
|
73
|
+
color_code = color.to_i
|
74
|
+
end
|
75
|
+
"\e[#{color_code}m#{str}\e[0m"
|
76
|
+
end
|
data/test/base_test.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class BaseTest < Test::Unit::TestCase
|
4
|
+
class << self
|
5
|
+
def shutdown
|
6
|
+
FC::DB.connect.query("DELETE FROM items")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@@policy_id ||= 1
|
12
|
+
@item = FC::Item.new(:name => 'test1', :tag => 'test tag', :dir => 0, :size => 100, :blabla => 'blabla', :policy_id => @@policy_id)
|
13
|
+
@@policy_id += 1
|
14
|
+
end
|
15
|
+
|
16
|
+
should "correct init" do
|
17
|
+
assert_raise(NoMethodError, 'Set not table field') { @item.blabla }
|
18
|
+
assert @item, 'Item not created'
|
19
|
+
assert_nil @item.id, 'Not nil id for new item'
|
20
|
+
end
|
21
|
+
|
22
|
+
should "correct add and save item" do
|
23
|
+
@item.save
|
24
|
+
assert id=@item.id, 'Nil id after save'
|
25
|
+
# double save check
|
26
|
+
@item.save
|
27
|
+
assert_equal id, @item.id, 'Changed id after double save'
|
28
|
+
@item.copies = 2
|
29
|
+
@item.save
|
30
|
+
assert_equal id, @item.id, 'Changed id after save with changes'
|
31
|
+
end
|
32
|
+
|
33
|
+
should "correct where" do
|
34
|
+
@item.save
|
35
|
+
ids = [@item.id]
|
36
|
+
item2 = FC::Item.new(:name => 'test2', :tag => 'test tag', :dir => 0, :size => 100, :blabla => 'blabla', :policy_id => 100)
|
37
|
+
item2.save
|
38
|
+
ids << item2.id
|
39
|
+
items = FC::Item.where("id IN (#{ids.join(',')})")
|
40
|
+
assert_same_elements items.map(&:id), ids, "Items by where load <> items by find"
|
41
|
+
end
|
42
|
+
|
43
|
+
should "correct reload item" do
|
44
|
+
@item.save
|
45
|
+
@item.name = 'new test'
|
46
|
+
@item.tag = 'new test tag'
|
47
|
+
@item.dir = '1'
|
48
|
+
@item.size = '777'
|
49
|
+
@item.reload
|
50
|
+
assert_same_elements ['test1', 'test tag', 0, 100], [@item.name, @item.tag, @item.dir, @item.size], 'Fields not restoted after reload'
|
51
|
+
end
|
52
|
+
|
53
|
+
should "correct update and load item" do
|
54
|
+
assert_raise(RuntimeError) { FC::Item.find(12454845) }
|
55
|
+
@item.save
|
56
|
+
@item.copies = 1
|
57
|
+
@item.save
|
58
|
+
@item.outer_id = 111
|
59
|
+
@item.save
|
60
|
+
loaded_item = FC::Item.find(@item.id)
|
61
|
+
assert_kind_of FC::Item, loaded_item, 'Load not FC::Item'
|
62
|
+
assert_equal @item.name, loaded_item.name, 'Saved item name <> loaded item name'
|
63
|
+
assert_equal @item.tag, loaded_item.tag, 'Saved item tag <> loaded item tag'
|
64
|
+
assert_equal @item.dir, loaded_item.dir, 'Saved item dir <> loaded item dir'
|
65
|
+
assert_equal @item.size, loaded_item.size, 'Saved item size <> loaded item size'
|
66
|
+
assert_equal @item.copies, loaded_item.copies, 'Saved item copies <> loaded item copies'
|
67
|
+
assert_equal @item.outer_id, loaded_item.outer_id, 'Saved item outer_id <> loaded item outer_id'
|
68
|
+
end
|
69
|
+
|
70
|
+
should "correct delete item" do
|
71
|
+
assert_nothing_raised { @item.delete }
|
72
|
+
assert_raise(RuntimeError, 'Item not deleted') { FC::Item.find(@item.id) }
|
73
|
+
end
|
74
|
+
end
|