revenc 0.1.2

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.
Files changed (44) hide show
  1. data/.document +5 -0
  2. data/.gitattributes +1 -0
  3. data/.gitignore +30 -0
  4. data/CLONING.rdoc +108 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +198 -0
  7. data/Rakefile +64 -0
  8. data/VERSION +1 -0
  9. data/bin/revenc +81 -0
  10. data/config/cucumber.yml +7 -0
  11. data/examples/rsync/encrypted_data/key/encfs6.xml +35 -0
  12. data/examples/rsync/revenc.conf +18 -0
  13. data/examples/rsync/scripts/passphrase +1 -0
  14. data/examples/rsync/unencrypted_data/test_file1.txt +1 -0
  15. data/examples/rsync/unencrypted_data/test_file2.txt +1 -0
  16. data/examples/simple/encfs6.xml +35 -0
  17. data/examples/simple/passphrase +1 -0
  18. data/examples/simple/unencrypted_data/test_file1.txt +1 -0
  19. data/examples/simple/unencrypted_data/test_file2.txt +1 -0
  20. data/features/app.feature +59 -0
  21. data/features/bin.feature +35 -0
  22. data/features/configuration.feature +98 -0
  23. data/features/copy.feature +169 -0
  24. data/features/generator.feature +15 -0
  25. data/features/mount.feature +133 -0
  26. data/features/step_definitions/.gitignore +0 -0
  27. data/features/step_definitions/revenc_steps.rb +64 -0
  28. data/features/support/aruba.rb +21 -0
  29. data/features/support/env.rb +4 -0
  30. data/features/support/hooks.rb +6 -0
  31. data/features/unmount.feature +58 -0
  32. data/lib/revenc/app.rb +128 -0
  33. data/lib/revenc/encfs_wrapper.rb +96 -0
  34. data/lib/revenc/errors.rb +78 -0
  35. data/lib/revenc/io.rb +265 -0
  36. data/lib/revenc/lockfile.rb +66 -0
  37. data/lib/revenc.rb +22 -0
  38. data/spec/revenc/error_spec.rb +50 -0
  39. data/spec/revenc/io_spec.rb +185 -0
  40. data/spec/revenc/lockfile_spec.rb +44 -0
  41. data/spec/spec.opts +2 -0
  42. data/spec/spec_helper.rb +16 -0
  43. data/spec/watchr.rb +142 -0
  44. metadata +179 -0
data/lib/revenc/app.rb ADDED
@@ -0,0 +1,128 @@
1
+ require 'configatron'
2
+ require 'term/ansicolor'
3
+
4
+ class String
5
+ include Term::ANSIColor
6
+ end
7
+
8
+ module Revenc
9
+
10
+ AVAILABLE_ACTIONS = %w[mount unmount copy]
11
+
12
+ class App
13
+
14
+ def initialize(base_dir, options={})
15
+ @base_dir = base_dir
16
+ @options = options
17
+ if @options[:verbose]
18
+ puts "base_dir: #{@base_dir}".cyan
19
+ puts "options: #{@options.inspect}".cyan
20
+ end
21
+ configure(options)
22
+ end
23
+
24
+ def run
25
+ begin
26
+
27
+ if action_argument_required?
28
+ action = ARGV.shift
29
+ unless AVAILABLE_ACTIONS.include?(action)
30
+ if action.nil?
31
+ puts "revenc action required"
32
+ else
33
+ puts "revenc invalid action: #{action}"
34
+ end
35
+ puts "revenc --help for more information"
36
+ exit 1
37
+ end
38
+ puts "revenc run action: #{action}".cyan if @options[:verbose]
39
+ raise "action #{action} not implemented" unless respond_to?(action)
40
+ result = send(action)
41
+ else
42
+ #
43
+ # default action if action_argument_required? is false
44
+ #
45
+ result = 0
46
+ end
47
+
48
+ exit(result ? 0 : 1)
49
+
50
+ rescue SystemExit => e
51
+ # This is the normal exit point, exit code from the send result
52
+ # or exit from another point in the system
53
+ puts "revenc run system exit: #{e}, status code: #{e.status}".green if @options[:verbose]
54
+ exit(e.status)
55
+ rescue Exception => e
56
+ STDERR.puts("revenc command failed, error(s) follow:")
57
+ STDERR.puts("#{e.message}".red)
58
+ STDERR.puts(e.backtrace.join("\n")) if @options[:verbose]
59
+ exit(1)
60
+ end
61
+ end
62
+
63
+ #
64
+ # app commands start
65
+ #
66
+ # TODO: Add status command, use encfsctl
67
+
68
+ def mount
69
+ EncfsWrapper.new(@base_dir, @options).mount(ARGV.shift, ARGV.shift)
70
+ end
71
+
72
+ def unmount
73
+ EncfsWrapper.new(@base_dir, @options).unmount(ARGV.shift)
74
+ end
75
+
76
+ def copy
77
+ EncfsWrapper.new(@base_dir, @options).copy(ARGV.shift, ARGV.shift)
78
+ end
79
+
80
+ #
81
+ # app commands end
82
+ #
83
+
84
+ private
85
+
86
+ # true if application requires an action to be specified on the command line
87
+ def action_argument_required?
88
+ !AVAILABLE_ACTIONS.empty?
89
+ end
90
+
91
+ # read options for YAML config with ERB processing and initialize configatron
92
+ def configure(options)
93
+ # TODO: read ~/.revenc.conf before looking in the current folder for revenc.conf, read BOTH files
94
+ config = @options[:config]
95
+ config = File.join(@base_dir, 'revenc.conf') unless config
96
+ if File.exists?(config)
97
+ # load configatron options from the config file
98
+ puts "loading config file: #{config}".cyan if @options[:verbose]
99
+ configatron.configure_from_yaml(config)
100
+ else
101
+ # user specified a config file?
102
+ raise "config file not found" if @options[:config]
103
+ # no error if user did not specify config file
104
+ puts "#{config} not found".yellow if @options[:verbose]
105
+ end
106
+
107
+ #
108
+ # set defaults, these will NOT override setting read from YAML
109
+ #
110
+ configatron.mount.source.set_default(:name, nil)
111
+ configatron.mount.mountpoint.set_default(:name, nil)
112
+ configatron.mount.passphrasefile.set_default(:name, 'passphrase')
113
+ configatron.mount.keyfile.set_default(:name, 'encfs6.xml')
114
+ configatron.mount.set_default(:cmd, nil)
115
+ configatron.mount.set_default(:executable, nil)
116
+
117
+ configatron.unmount.mountpoint.set_default(:name, nil)
118
+ configatron.unmount.set_default(:cmd, nil)
119
+ configatron.unmount.set_default(:executable, nil)
120
+
121
+ configatron.copy.source.set_default(:name, nil)
122
+ configatron.copy.destination.set_default(:name, nil)
123
+ configatron.copy.set_default(:cmd, nil)
124
+ configatron.copy.set_default(:executable, nil)
125
+ end
126
+
127
+ end
128
+ end
@@ -0,0 +1,96 @@
1
+ module Revenc
2
+
3
+ class EncfsWrapper
4
+
5
+ def initialize(base_dir, options)
6
+ raise ArgumentError, "Options should be a hash" unless options.is_a?(Hash)
7
+ @base_dir = base_dir
8
+ @options = options
9
+ end
10
+
11
+ def mount(source=nil, mount_point_folder=nil)
12
+
13
+ # add params from config file if not specified
14
+ source = configatron.mount.source.name unless source
15
+ mount_point_folder = configatron.mount.mountpoint.name unless mount_point_folder
16
+
17
+ # sanity check params
18
+ raise "source folder not specified" unless source
19
+ raise "mountpoint not specified" unless mount_point_folder
20
+
21
+ mount_point_options = @options.merge(:passphrasefile => configatron.mount.passphrasefile.name)
22
+ mount_point_options = mount_point_options.merge(:keyfile => configatron.mount.keyfile.name)
23
+ mount_point_options = mount_point_options.merge(:cmd => configatron.mount.cmd) if configatron.mount.cmd
24
+ mount_point_options = mount_point_options.merge(:executable => configatron.mount.executable) if configatron.mount.executable
25
+
26
+ mount_point = MountPoint.new(mount_point_folder, source, mount_point_options)
27
+
28
+ if @options[:verbose]
29
+ puts "mount: source=#{mount_point.source.name}".cyan
30
+ puts "mount: mountpoint=#{mount_point.name}".cyan
31
+ puts "mount: passphrasefile=#{mount_point.passphrasefile.name}".cyan
32
+ puts "mount: keyfile=#{mount_point.keyfile.name}".cyan
33
+ puts "mount: cmd=#{mount_point.cmd}".cyan
34
+ puts "mount: executable=#{mount_point.executable}".cyan
35
+ end
36
+
37
+ mount_point.execute
38
+ end
39
+
40
+ def unmount(foldername = nil)
41
+
42
+ # add param from config file if not specified, try specific unmount
43
+ foldername = configatron.unmount.mountpoint.name unless foldername
44
+ # fallback to mount.mountpoint if specified
45
+ foldername = configatron.mount.mountpoint.name unless foldername
46
+
47
+ # sanity check params
48
+ raise "mountpoint not specified" unless foldername
49
+
50
+ unmount_point_options = @options || {}
51
+ unmount_point_options = unmount_point_options.merge(:cmd => configatron.unmount.cmd) if configatron.umount.cmd
52
+ unmount_point_options = unmount_point_options.merge(:executable => configatron.unmount.executable) if configatron.umount.executable
53
+ unmount_point = UnmountPoint.new(foldername, unmount_point_options)
54
+
55
+ if @options[:verbose]
56
+ puts "unmount: mountpoint=#{unmount_point.mountpoint.name}".cyan
57
+ puts "unmount: cmd=#{unmount_point.cmd}".cyan
58
+ puts "unmount: executable=#{unmount_point.executable}".cyan
59
+ end
60
+
61
+ unmount_point.execute
62
+ end
63
+
64
+ def copy(source=nil, destination=nil)
65
+
66
+ # add params from config file if not specified
67
+ source = configatron.copy.source.name unless source
68
+ # fallback
69
+ source = configatron.mount.mountpoint.name unless source
70
+ destination = configatron.copy.destination.name unless destination
71
+
72
+ # sanity check params
73
+ raise "source folder not specified" unless source
74
+ raise "destination not specified" unless destination
75
+
76
+ copy_options = @options || {}
77
+ copy_options = copy_options.merge(:cmd => configatron.copy.cmd) if configatron.copy.cmd
78
+ copy_options = copy_options.merge(:executable => configatron.copy.executable) if configatron.copy.executable
79
+ copy_options = copy_options.merge(:mountpoint => configatron.mount.mountpoint.name) if configatron.mount.mountpoint.name
80
+
81
+ copy_folder = CopySourceFolder.new( source, destination, copy_options)
82
+
83
+ if @options[:verbose]
84
+ puts "copy: source=#{copy_folder.name}".cyan
85
+ puts "copy: destination=#{copy_folder.destination.name}".cyan
86
+ puts "copy: cmd=#{copy_folder.cmd}".cyan
87
+ puts "copy: executable=#{copy_folder.executable}".cyan
88
+ end
89
+
90
+ copy_folder.execute
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+
@@ -0,0 +1,78 @@
1
+ module Revenc
2
+
3
+ class Errors
4
+ include Enumerable
5
+
6
+ def initialize
7
+ @errors ={}
8
+ end
9
+
10
+ # add errors, error_on can be a symbol or object instance
11
+ def add(error_on, message = "Unknown error")
12
+
13
+ # humanize error_on
14
+ if error_on.is_a?(Symbol)
15
+ error_on_str = error_on.to_s
16
+ else
17
+ error_on_str = underscore(error_on.class.name)
18
+ end
19
+ error_on_str = error_on_str.gsub(/\//, '_')
20
+ error_on_str = error_on_str.gsub(/_/, ' ')
21
+ error_on_str = error_on_str.gsub(/^revenc/, '').strip
22
+ #error_on_str = error_on_str.capitalize
23
+
24
+ @errors[error_on_str] ||= []
25
+ @errors[error_on_str] << message.to_s
26
+ end
27
+
28
+ def empty?
29
+ @errors.empty?
30
+ end
31
+
32
+ def clear
33
+ @errors = {}
34
+ end
35
+
36
+ def each
37
+ @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
38
+ end
39
+
40
+ def size
41
+ @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
42
+ end
43
+
44
+ alias_method :count, :size
45
+ alias_method :length, :size
46
+
47
+ def messages
48
+ messages = []
49
+
50
+ @errors.each_key do |attr|
51
+ @errors[attr].each do |message|
52
+ next unless message
53
+ attr_name = attr.to_s
54
+ messages << attr_name + ' ' + message
55
+ end
56
+ end
57
+
58
+ messages
59
+ end
60
+
61
+ def to_sentences
62
+ messages.join("\n")
63
+ end
64
+
65
+ private
66
+
67
+ # from ActiveSupport
68
+ def underscore(camel_cased_word)
69
+ camel_cased_word.to_s.gsub(/::/, '/').
70
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
71
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
72
+ tr("-", "_").
73
+ downcase
74
+ end
75
+
76
+ end
77
+
78
+ end
data/lib/revenc/io.rb ADDED
@@ -0,0 +1,265 @@
1
+ module Revenc
2
+
3
+ class BasicAction
4
+
5
+ def initialize(name=nil, options={})
6
+ raise ArgumentError, "Options should be a hash" unless options.is_a?(Hash)
7
+ @name = name
8
+ @options = options
9
+ end
10
+
11
+ def name
12
+ @name
13
+ end
14
+
15
+ def name=(value)
16
+ @name = value
17
+ end
18
+
19
+ def options
20
+ @options ||= {}
21
+ end
22
+
23
+ def options=(value)
24
+ @options = value
25
+ end
26
+
27
+ def errors
28
+ @errors ||= Errors.new
29
+ end
30
+
31
+ def exists?
32
+ false
33
+ end
34
+
35
+ def validate
36
+ end
37
+
38
+ def valid?
39
+ errors.clear
40
+ validate
41
+ errors.empty?
42
+ end
43
+
44
+ def cmd
45
+ return nil unless @cmd
46
+ # process ERB
47
+ render(@cmd)
48
+ end
49
+
50
+ def cmd=(value)
51
+ @cmd = value
52
+ end
53
+
54
+ private
55
+
56
+ def system_cmd(cmd=nil)
57
+ raise "ERROR: cmd not given" unless cmd
58
+ return true if options[:dry_run]
59
+ system cmd
60
+ end
61
+
62
+ # Runs the YAML file through ERB
63
+ def render(value, b = binding)
64
+ ERB.new(value).result(b)
65
+ end
66
+
67
+ end
68
+
69
+ class FileSystemEntity < BasicAction
70
+
71
+ # return fully qualified name
72
+ def name
73
+ return @name unless (@name && File.exists?(@name))
74
+ File.expand_path(@name)
75
+ end
76
+
77
+ def exists?
78
+ File.exists?(name) if name
79
+ end
80
+
81
+ end
82
+
83
+ class TextFile < FileSystemEntity
84
+
85
+
86
+ def empty?
87
+ return true unless exists?
88
+
89
+ contents = nil
90
+ File.open(@name, "r") do |f|
91
+ contents = f.read
92
+ end
93
+ contents.empty?
94
+ end
95
+
96
+ def validate
97
+ errors.add(self, "filename not specified") if @name.nil?
98
+ errors.add(self, "not found") unless exists?
99
+ end
100
+ end
101
+
102
+ class PassphraseFile < TextFile
103
+
104
+ def initialize(name='passphrase', options={})
105
+ super name, options
106
+ end
107
+
108
+ def validate
109
+ super
110
+ errors.add(self, "is empty") if empty?
111
+ end
112
+ end
113
+
114
+ class KeyFile < TextFile
115
+
116
+ def initialize(name='encfs6.xml', options={})
117
+ super name, options
118
+ end
119
+
120
+ def validate
121
+ super
122
+ errors.add(self, "is empty") if exists? && empty?
123
+ end
124
+ end
125
+
126
+ class FileFolder < FileSystemEntity
127
+
128
+ def exists?
129
+ !@name.nil? && File.directory?(@name)
130
+ end
131
+
132
+ def empty?
133
+ return true unless exists?
134
+ Dir.entries(@name).sort == [".", ".."].sort
135
+ end
136
+
137
+ def validate
138
+ errors.add(self, "not found") unless exists?
139
+ end
140
+ end
141
+
142
+ class SourceFolder < FileFolder
143
+
144
+ def validate
145
+ super
146
+ errors.add(self, "is empty") if exists? && empty?
147
+ end
148
+ end
149
+
150
+ class ActionFolder < FileFolder
151
+ attr_accessor :passphrasefile
152
+ attr_accessor :keyfile
153
+
154
+ def initialize(name=nil, options={})
155
+ super
156
+ @passphrasefile = PassphraseFile.new(options[:passphrasefile])
157
+ @keyfile = KeyFile.new(options[:keyfile])
158
+ @cmd = options[:cmd]
159
+ @executable = options[:executable]
160
+ end
161
+
162
+ def validate
163
+ super
164
+ errors.add(self, "executable filename not specified") unless @executable
165
+ errors.add(self, "#{@executable} executable not found") if executable.empty?
166
+ errors.add(self, "cmd not specified") unless cmd
167
+ end
168
+
169
+ def executable
170
+ return nil unless @executable
171
+ result = `which #{@executable}`
172
+ result.strip
173
+ end
174
+
175
+ # run the action if valid and return true if successful
176
+ def execute
177
+ raise errors.to_sentences unless valid?
178
+
179
+ # default failing command
180
+ result = false
181
+
182
+ # protect command from recursion
183
+ mutex = Revenc::Mutex.new
184
+ lock_sucessful = mutex.execute do
185
+ result = system_cmd(cmd)
186
+ end
187
+
188
+ raise "action failed, lock file present" unless lock_sucessful
189
+ result
190
+ end
191
+ end
192
+
193
+ class MountPoint < ActionFolder
194
+ attr_accessor :source
195
+
196
+ def initialize(name=nil, source_folder_name=nil, options={})
197
+ super name, options
198
+ @source = SourceFolder.new(source_folder_name)
199
+ @cmd = options[:cmd] || "cat <%= passphrasefile.name %> | ENCFS6_CONFIG=<%= keyfile.name %> \
200
+ <%= executable %> --stdinpass --reverse <%= source.name %> <%= mountpoint.name %> -- -o ro"
201
+ @executable = options[:executable] || 'encfs'
202
+ end
203
+
204
+ # allow clarity in config files, instead of <%= name %> you can use <%= mountpoint.name %>
205
+ def mountpoint
206
+ self
207
+ end
208
+
209
+ def validate
210
+ super
211
+ errors.add(self, "is not empty") unless empty?
212
+ errors.add(self, source.errors.to_sentences) unless source.valid?
213
+ errors.add(self, keyfile.errors.to_sentences) unless keyfile.valid?
214
+ errors.add(self, passphrasefile.errors.to_sentences) unless passphrasefile.valid?
215
+ end
216
+
217
+ end
218
+
219
+ class UnmountPoint < ActionFolder
220
+
221
+ def initialize(name=nil, options={})
222
+ super
223
+ @cmd = options[:cmd] || "<%= executable %> -u <%= mountpoint.name %>"
224
+ @executable = options[:executable] || 'fusermount'
225
+ end
226
+
227
+ # allow clarity in config files, instead of <%= name %> you can use <%= mountpoint.name %>
228
+ def mountpoint
229
+ self
230
+ end
231
+
232
+ end
233
+
234
+ class DestinationPoint < BasicAction
235
+ end
236
+
237
+ class CopySourceFolder < ActionFolder
238
+ attr_accessor :destination
239
+ attr_accessor :mountpoint
240
+
241
+ def initialize(name=nil, destination_name=nil, options={})
242
+ super name, options
243
+ @destination = DestinationPoint.new(destination_name)
244
+ @mountpoint = MountPoint.new(options[:mountpoint])
245
+ @cmd = options[:cmd] || "<%= executable %> -r <%= source.name %> <%= destination.name %>"
246
+ @executable = options[:executable] || 'cp'
247
+ end
248
+
249
+ # allow clarity in config files, instead of <%= name %> you can use <%= source.name %>
250
+ def source
251
+ self
252
+ end
253
+
254
+ def validate
255
+ super
256
+ errors.add(self, "is empty") if exists? && empty?
257
+ errors.add(self, "mountpoint not found") if (mountpoint.name && !mountpoint.exists?)
258
+ errors.add(self, "mountpoint is empty") if (mountpoint.name && mountpoint.exists? && mountpoint.empty?)
259
+ errors.add(self, destination.errors.to_sentences) unless destination.valid?
260
+ end
261
+
262
+ end
263
+
264
+ end
265
+
@@ -0,0 +1,66 @@
1
+ module Revenc
2
+
3
+ class Lockfile
4
+ attr_accessor :lockfile
5
+
6
+ def initialize(lockfile=nil)
7
+ raise ArgumentError, "lockfile not specified" unless lockfile
8
+ @lockfile = lockfile
9
+ end
10
+
11
+ def locked?
12
+ return false unless File.exists?(lockfile)
13
+ result = false
14
+ open(lockfile, 'w') do |f|
15
+ # exclusive non-blocking lock
16
+ result = !lock(f, File::LOCK_EX | File::LOCK_NB)
17
+ end
18
+ result
19
+ end
20
+
21
+ # flock, get a file lock
22
+ #
23
+ # Typical usage:
24
+ #
25
+ # open('output', 'w') do |f|
26
+ # flock(f, File::LOCK_EX) do |f|
27
+ # f << "write to file"
28
+ # end
29
+ # end
30
+ def lock(file, mode)
31
+ result = file.flock(mode)
32
+ if result
33
+ begin
34
+ yield file if block_given?
35
+ ensure
36
+ file.flock(File::LOCK_UN)
37
+ end
38
+ end
39
+ return result
40
+ end
41
+ end
42
+
43
+ class Mutex < Lockfile
44
+
45
+ def initialize(lockfile='revenc.lck')
46
+ super lockfile
47
+ end
48
+
49
+ def execute
50
+ result = false
51
+ begin
52
+ open(@lockfile, 'w') do |f|
53
+ # exclusive non-blocking lock
54
+ result = lock(f, File::LOCK_EX | File::LOCK_NB) do |f|
55
+ yield if block_given?
56
+ end
57
+ end
58
+ ensure
59
+ # clean up but only if we have a positive result meaning we wrote the lockfile
60
+ FileUtils.rm(@lockfile) if (result && File.exists?(@lockfile))
61
+ end
62
+ result
63
+ end
64
+ end
65
+
66
+ end
data/lib/revenc.rb ADDED
@@ -0,0 +1,22 @@
1
+ $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
2
+
3
+ require 'rubygems'
4
+
5
+ module Revenc
6
+
7
+ # return the contents of the VERSION file
8
+ # VERSION format: 0.0.0
9
+ def self.version
10
+ version_info_file = File.join(File.dirname(__FILE__), *%w[.. VERSION])
11
+ File.open(version_info_file, "r") do |f|
12
+ f.read
13
+ end
14
+ end
15
+
16
+ end
17
+
18
+ require 'revenc/app'
19
+ require 'revenc/io'
20
+ require 'revenc/errors'
21
+ require 'revenc/lockfile'
22
+ require 'revenc/encfs_wrapper'
@@ -0,0 +1,50 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Revenc::Errors do
4
+
5
+ before(:each) do
6
+ @error_obj = Revenc::Errors.new
7
+ end
8
+
9
+ it "should be empty when created" do
10
+ @error_obj.should be_empty
11
+ end
12
+
13
+ it "should return a count of the errors" do
14
+ @error_obj.should be_empty
15
+ @error_obj.add(:test_error1)
16
+ @error_obj.add(:test_error2)
17
+ @error_obj.should_not be_empty
18
+ @error_obj.size.should == 2
19
+ end
20
+
21
+ it "should clear the errors" do
22
+ @error_obj.should be_empty
23
+ @error_obj.add(:test_error1)
24
+ @error_obj.add(:test_error2)
25
+ @error_obj.should_not be_empty
26
+ @error_obj.size.should be(2)
27
+ @error_obj.clear
28
+ @error_obj.should be_empty
29
+ @error_obj.size.should be(0)
30
+ end
31
+
32
+ it "should return the errors in full sentences for errors on symbol" do
33
+ @error_obj.should be_empty
34
+ @error_obj.add(:test_error1, "Error no 1")
35
+ @error_obj.add(:test_error2, "Error no 2")
36
+ @error_obj.size.should be(2)
37
+ @error_obj.to_sentences.should == "test error1 Error no 1\ntest error2 Error no 2"
38
+ end
39
+
40
+ it "should return the errors in full sentences for errors on class names" do
41
+ @error_obj.should be_empty
42
+ @error_obj.add(@error_obj, "error no 1")
43
+ @error_obj.add(@error_obj, "error no 2")
44
+ @error_obj.size.should be(2)
45
+ @error_obj.to_sentences.should == "errors error no 1\nerrors error no 2"
46
+ end
47
+
48
+ end
49
+
50
+