tmptation 1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright © 2009 Martin Aumont (mynyml)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ Summary
2
+ -------
3
+ Tmptation provides classes that help safely manipulate temporary files and
4
+ directories. Especially useful for use in tests.
5
+
6
+ Features
7
+ --------
8
+ * garbage collection of all created tmp files and dirs
9
+ * safe deletion - will refuse to delete non-tmp paths (where tmp within `Dir.tmpdir`)
10
+
11
+ Examples
12
+ --------
13
+
14
+ # TmpFile is a subclass of Tempfile, with a few additions
15
+
16
+ file = TmpFile.new('name', 'contents')
17
+
18
+ file.path.exist? #=> true
19
+ file.closed? #=> false
20
+ file.read #=> "contents"
21
+
22
+ TmpFile.delete_all
23
+
24
+ file.path.exist? #=> false
25
+ file.closed? #=> true
26
+
27
+
28
+ # TmpDir is a subclass of Pathname, with a few additions
29
+
30
+ path = TmpDir.new
31
+ path.exist? #=> true
32
+
33
+ TmpDir.delete_all
34
+ path.exist? #=> false
35
+
36
+ Mixins
37
+ ------
38
+
39
+ Tmptation also contains two mixins, `SafeDeletable` and `SubclassTracking`.
40
+ They might be useful on their own. See the inline docs for more details.
41
+
42
+ Protip
43
+ ------
44
+
45
+ If you use Tmptation in specs, add `TmpFile.delete_all` and `TmpDir.delete_all`
46
+ to your global teardown method:
47
+
48
+ class MiniTest::Unit::TestCase
49
+ def teardown
50
+ TmpFile.delete_all
51
+ TmpDir.delete_all
52
+ end
53
+ end
54
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ task :default => :test
2
+
3
+ desc "Run tests"
4
+ task :test do
5
+ system "ruby -rubygems -I.:lib:test -e'%w( #{Dir['test/**/*_test.rb'].join(' ')} ).each {|p| require p }'"
6
+ end
7
+
data/lib/tmptation.rb ADDED
@@ -0,0 +1,171 @@
1
+ require 'pathname'
2
+ require 'tempfile'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+
6
+ module Tmptation
7
+ VERSION = 1.1
8
+
9
+ # Adds a #safe_delete method that will delete the object's associated path
10
+ # (either #path or #to_s, if it exists) only if it lives within the system's
11
+ # temporary directory (as defined by Dir.tmpdir)
12
+ #
13
+ # @example
14
+ #
15
+ # path = Pathname.new('~/Documents')
16
+ # path.extend(SafeDeletable)
17
+ #
18
+ # path.to_s #=> '~/Documents'
19
+ # path.safe_delete #=> raises UnsafeDelete
20
+ #
21
+ # # however:
22
+ #
23
+ # path = Pathname(Dir.mktmpdir).expand_path
24
+ #
25
+ # Dir.tmpdir #=> /var/folders/l8/l8EJIxZoHGGj+y1RvV0r6U+++TM/-Tmp-/
26
+ # path.to_s #=> /var/folders/l8/l8EJIxZoHGGj+y1RvV0r6U+++TM/-Tmp-/20101103-94996-1iywsjo
27
+ #
28
+ # path.exist? #=> true
29
+ # path.safe_delete
30
+ # path.exist? #=> false
31
+ #
32
+ module SafeDeletable
33
+ UnsafeDelete = Class.new(RuntimeError)
34
+
35
+ # Delete `#path` or `#to_s` if it exists, and only if it lives within
36
+ # `Dir.tmpdir`. If the path is a directory, it is deleted recursively.
37
+ #
38
+ # @raises SafeDeletable::UnsafeDelete if directory isn't within `Dir.tmpdir`
39
+ #
40
+ def safe_delete
41
+ path = self.respond_to?(:path) ? self.path : self.to_s
42
+ path = Pathname(path).expand_path
43
+
44
+ unless path.to_s.match(/^#{Regexp.escape(Dir.tmpdir)}/)
45
+ raise UnsafeDelete.new("refusing to remove non-tmp directory '#{path}'")
46
+ end
47
+ FileUtils.remove_entry_secure(path.to_s)
48
+ end
49
+ end
50
+
51
+ # Keep track of a class's instances
52
+ #
53
+ # @example
54
+ #
55
+ # class Foo
56
+ # include InstanceTracking
57
+ # end
58
+ #
59
+ # a, b, c = Foo.new, Foo.new, Foo.new
60
+ #
61
+ # [a,b,c] == Foo.instances #=> true
62
+ #
63
+ module InstanceTracking
64
+ def self.included(base)
65
+ base.class_eval do
66
+ extend ClassMethods
67
+ include InstanceMethods
68
+ end
69
+ end
70
+
71
+ module ClassMethods
72
+ def instances
73
+ @instances ||= []
74
+ end
75
+ end
76
+
77
+ module InstanceMethods
78
+ def initialize(*args)
79
+ super
80
+ self.class.instances << self
81
+ end
82
+ end
83
+ end
84
+
85
+ # Subclass of core lib's Tempfile that allows safely deleting all of its
86
+ # instances. It also provides a convenient way to add content to the file.
87
+ #
88
+ # @example
89
+ #
90
+ # file = TmpFile.new('name', 'contents')
91
+ #
92
+ # file.path.class #=> Pathname
93
+ # file.path.exist? #=> true
94
+ # file.closed? #=> false
95
+ # file.read #=> "contents"
96
+ #
97
+ # TmpFile.delete_all
98
+ #
99
+ # file.path.exist? #=> false
100
+ # file.closed? #=> true
101
+ #
102
+ class TmpFile < Tempfile
103
+ include SafeDeletable
104
+ include InstanceTracking
105
+
106
+ class << self
107
+
108
+ # Safe deletes and closes all instances
109
+ def delete_all
110
+ instances.each do |instance|
111
+ instance.safe_delete
112
+ instance.close
113
+ end
114
+ end
115
+ alias -@ delete_all
116
+ end
117
+
118
+ # @param name<String> optional
119
+ # prefix name of file
120
+ #
121
+ # @param body<String> optional
122
+ # contents of file
123
+ #
124
+ def initialize(name='anon', body='')
125
+ super(name)
126
+ self << body
127
+ self.rewind
128
+ end
129
+
130
+ # File's path as a Pathname
131
+ #
132
+ # @return path<Pathname>
133
+ #
134
+ def path
135
+ Pathname(super)
136
+ end
137
+ end
138
+
139
+ # Subclass of core lib's Pathname that allows safely deleting all of its
140
+ # instances.
141
+ #
142
+ # @example
143
+ #
144
+ # path = TmpDir.new
145
+ # path.exist? #=> true
146
+ #
147
+ # TmpDir.delete_all
148
+ # path.exist? #=> false
149
+ #
150
+ class TmpDir < Pathname
151
+ include SafeDeletable
152
+ include InstanceTracking
153
+
154
+ class << self
155
+
156
+ # Safe deletes and closes all instances
157
+ def delete_all
158
+ instances.each {|instance| instance.safe_delete }
159
+ end
160
+ alias -@ delete_all
161
+ end
162
+
163
+ # @param prefix<String> optional
164
+ # prefix of directory name
165
+ #
166
+ def initialize(prefix='TmpDir-')
167
+ super(Pathname(Dir.mktmpdir(prefix)).expand_path)
168
+ end
169
+ end
170
+ end
171
+
data/specs.watchr ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env watchr
2
+
3
+ # --------------------------------------------------
4
+ # Rules
5
+ # --------------------------------------------------
6
+ watch( '^test.*_test\.rb' ) {|m| ruby m[0] }
7
+ watch( '^lib/(.*)\.rb' ) {|m| ruby "test/#{m[1]}_test.rb" }
8
+
9
+ # --------------------------------------------------
10
+ # Signal Handling
11
+ # --------------------------------------------------
12
+ Signal.trap('QUIT') { ruby tests } # Ctrl-\
13
+ Signal.trap('INT' ) { abort("\n") } # Ctrl-C
14
+
15
+ # --------------------------------------------------
16
+ # Helpers
17
+ # --------------------------------------------------
18
+ def ruby(*paths)
19
+ paths = paths.flatten.select {|p| File.exist?(p) && !File.directory?(p) }.join(' ')
20
+ run "ruby #{gem_opt} -I.:lib:test -e'%w( #{paths} ).each {|p| require p }'" unless paths.empty?
21
+ end
22
+
23
+ def tests
24
+ Dir['test/**/*_test.rb']
25
+ end
26
+
27
+ def run( cmd )
28
+ puts cmd
29
+ system cmd
30
+ end
31
+
32
+ def gem_opt
33
+ defined?(Gem) ? "-rubygems" : ""
34
+ end
35
+
@@ -0,0 +1,256 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/spec'
3
+ require 'rr'
4
+
5
+ begin require 'ruby-debug'; rescue LoadError; end
6
+ begin require 'redgreen' ; rescue LoadError; end
7
+ begin require 'phocus' ; rescue LoadError; end
8
+
9
+ class MiniTest::Unit::TestCase
10
+ include RR::Adapters::TestUnit
11
+ end
12
+
13
+ require 'tmptation'
14
+ include Tmptation
15
+
16
+ describe Tmptation do
17
+ it "should have a version" do
18
+ assert_kind_of Float, Tmptation::VERSION
19
+ end
20
+ end
21
+
22
+ describe Tmptation::SafeDeletable do
23
+
24
+ it "should delete a tmp directory" do
25
+ begin
26
+ dir = Pathname(Dir.mktmpdir('SafeDeletable-')).expand_path
27
+ dir.extend(SafeDeletable)
28
+
29
+ assert dir.exist?
30
+ assert_match /^#{Regexp.quote(Dir.tmpdir)}/, dir.to_s
31
+
32
+ dir.safe_delete
33
+
34
+ refute dir.exist?
35
+ ensure
36
+ dir.rmdir if dir.directory?
37
+ end
38
+ end
39
+
40
+ it "should refuse to delete a non-tmp directory" do
41
+ begin
42
+ dir = Pathname(Dir.mktmpdir('SafeDeletable-')).expand_path
43
+ dir.extend(SafeDeletable)
44
+
45
+ stub(dir).to_s { '/not/a/tmp/dir' }
46
+ refute_match /^#{Regexp.quote(Dir.tmpdir)}/, dir.to_s
47
+
48
+ assert_raises(SafeDeletable::UnsafeDelete) { dir.safe_delete }
49
+ ensure
50
+ dir.rmdir if dir.directory?
51
+ end
52
+ end
53
+
54
+ it "should hanled relative paths" do
55
+ begin
56
+ dir = Pathname(Dir.mktmpdir('SafeDeletable-')).relative_path_from(Pathname(Dir.pwd))
57
+ dir.extend(SafeDeletable)
58
+
59
+ assert_match /^#{Regexp.quote(Dir.tmpdir)}/, dir.expand_path.to_s
60
+ assert dir.relative?
61
+
62
+ dir.safe_delete
63
+
64
+ refute dir.exist?
65
+ ensure
66
+ dir.rmdir if dir.directory?
67
+ end
68
+ end
69
+
70
+ it "should use an object's #path if it exists" do
71
+ begin
72
+ file = Tempfile.new('safe_deletable')
73
+ file.extend(SafeDeletable)
74
+
75
+ assert File.exist?(file.path)
76
+ assert_match /^#{Regexp.quote(Dir.tmpdir)}/, file.path.to_s
77
+
78
+ file.safe_delete
79
+
80
+ refute File.exist?(file.path)
81
+ ensure
82
+ file.delete if File.exist?(file)
83
+ end
84
+ end
85
+ end
86
+
87
+ describe Tmptation::InstanceTracking do
88
+
89
+ before do
90
+ TmpFile.instance_variable_set(:@instances, nil)
91
+ end
92
+
93
+ it "should keep track of class instances" do
94
+ klass = Class.new
95
+ klass.class_eval { include InstanceTracking }
96
+
97
+ assert_empty klass.instances
98
+
99
+ foo, bar = klass.new, klass.new
100
+ assert_equal [foo,bar], klass.instances
101
+ end
102
+ end
103
+
104
+ describe Tmptation::TmpFile do
105
+
106
+ before do
107
+ TmpFile.instance_variable_set(:@instances, nil)
108
+ end
109
+
110
+ it "should implement SafeDeletable" do
111
+ assert_includes TmpFile.included_modules, SafeDeletable
112
+ end
113
+
114
+ it "should implement InstanceTracking" do
115
+ assert_includes TmpFile.included_modules, InstanceTracking
116
+ end
117
+
118
+ it "should create a new temporary file on init" do
119
+ begin
120
+ foo = TmpFile.new
121
+
122
+ assert File.exist?(foo.path)
123
+ assert_match /^#{Regexp.quote(Dir.tmpdir)}/, foo.path.to_s
124
+ ensure
125
+ foo.delete if foo.path.exist?
126
+ end
127
+ end
128
+
129
+ it "should provide a path as Pathname" do
130
+ begin
131
+ foo = TmpFile.new
132
+ assert_kind_of Pathname, foo.path
133
+ ensure
134
+ foo.delete if foo.path.exist?
135
+ end
136
+ end
137
+
138
+ it "should allow setting a name and body on init" do
139
+ begin
140
+ foo = TmpFile.new('name', 'body')
141
+
142
+ assert_match /^name/, foo.path.basename.to_s
143
+ assert_equal 'body', foo.read
144
+ ensure
145
+ foo.delete if foo.path.exist?
146
+ end
147
+ end
148
+
149
+ it "should delete all instances" do
150
+ begin
151
+ foo, bar = TmpFile.new, TmpFile.new
152
+
153
+ assert foo.path.exist?
154
+ assert bar.path.exist?
155
+
156
+ TmpFile.delete_all
157
+
158
+ refute foo.path.exist?
159
+ refute bar.path.exist?
160
+ ensure
161
+ foo.delete if foo.path.exist?
162
+ bar.delete if bar.path.exist?
163
+ end
164
+ end
165
+
166
+ it "should use #safe_delete" do
167
+ begin
168
+ foo = TmpFile.new
169
+
170
+ mock(foo).safe_delete
171
+ TmpFile.delete_all
172
+
173
+ RR.verify
174
+ ensure
175
+ foo.delete if foo.path.exist?
176
+ end
177
+ end
178
+
179
+ it "should close files when deleting all instances" do
180
+ begin
181
+ foo = TmpFile.new
182
+ refute foo.closed?
183
+
184
+ TmpFile.delete_all
185
+ assert foo.closed?
186
+ ensure
187
+ foo.delete if foo.path.exist?
188
+ end
189
+ end
190
+ end
191
+
192
+ describe Tmptation::TmpDir do
193
+
194
+ before do
195
+ TmpDir.instance_variable_set(:@instances, nil)
196
+ end
197
+
198
+ it "should implement SafeDeletable" do
199
+ assert_includes TmpDir.included_modules, SafeDeletable
200
+ end
201
+
202
+ it "should implement InstanceTracking" do
203
+ assert_includes TmpFile.included_modules, InstanceTracking
204
+ end
205
+
206
+ it "should create a temporary directory on init" do
207
+ begin
208
+ foo = TmpDir.new
209
+
210
+ assert foo.exist?
211
+ assert_match /^#{Regexp.quote(Dir.tmpdir)}/, foo.to_s
212
+ ensure
213
+ foo.rmdir if foo.exist?
214
+ end
215
+ end
216
+
217
+ it "should allow setting a prefix on init" do
218
+ begin
219
+ foo = TmpDir.new('prefix-')
220
+ assert_match /^prefix/, foo.to_s.split('/').last
221
+ ensure
222
+ foo.rmdir if foo.exist?
223
+ end
224
+ end
225
+
226
+ it "should delete all instances" do
227
+ begin
228
+ foo, bar = TmpDir.new, TmpDir.new
229
+
230
+ assert foo.exist?
231
+ assert bar.exist?
232
+
233
+ TmpDir.delete_all
234
+
235
+ refute foo.exist?
236
+ refute bar.exist?
237
+ ensure
238
+ foo.rmdir if foo.exist?
239
+ bar.rmdir if foo.exist?
240
+ end
241
+ end
242
+
243
+ it "should use #safe_delete" do
244
+ begin
245
+ foo = TmpDir.new
246
+
247
+ mock(foo).safe_delete
248
+ TmpDir.delete_all
249
+
250
+ RR.verify
251
+ ensure
252
+ foo.rmdir if foo.exist?
253
+ end
254
+ end
255
+ end
256
+
data/tmptation.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ require 'lib/tmptation'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "tmptation"
5
+ s.version = Tmptation::VERSION
6
+ s.summary = "Classes that help safely manipulate temporary files and directories"
7
+ s.description = "Classes that help safely manipulate temporary files and directories."
8
+ s.author = "Martin Aumont"
9
+ s.email = "mynyml@gmail.com"
10
+ s.homepage = "http://github.com/mynyml/tmptation"
11
+ s.rubyforge_project = ""
12
+ s.require_path = "lib"
13
+ s.files = `git ls-files`.strip.split("\n")
14
+
15
+ s.add_development_dependency 'minitest'
16
+ s.add_development_dependency 'rr'
17
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tmptation
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 1
8
+ version: "1.1"
9
+ platform: ruby
10
+ authors:
11
+ - Martin Aumont
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2010-11-04 00:00:00 -07:00
17
+ default_executable:
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: minitest
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ segments:
27
+ - 0
28
+ version: "0"
29
+ type: :development
30
+ version_requirements: *id001
31
+ - !ruby/object:Gem::Dependency
32
+ name: rr
33
+ prerelease: false
34
+ requirement: &id002 !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ segments:
39
+ - 0
40
+ version: "0"
41
+ type: :development
42
+ version_requirements: *id002
43
+ description: Classes that help safely manipulate temporary files and directories.
44
+ email: mynyml@gmail.com
45
+ executables: []
46
+
47
+ extensions: []
48
+
49
+ extra_rdoc_files: []
50
+
51
+ files:
52
+ - .gitignore
53
+ - LICENSE
54
+ - README.md
55
+ - Rakefile
56
+ - lib/tmptation.rb
57
+ - specs.watchr
58
+ - test/tmptation_test.rb
59
+ - tmptation.gemspec
60
+ has_rdoc: true
61
+ homepage: http://github.com/mynyml/tmptation
62
+ licenses: []
63
+
64
+ post_install_message:
65
+ rdoc_options: []
66
+
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ segments:
74
+ - 0
75
+ version: "0"
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ requirements: []
84
+
85
+ rubyforge_project: ""
86
+ rubygems_version: 1.3.6
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: Classes that help safely manipulate temporary files and directories
90
+ test_files: []
91
+