filecluster 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|