memfs 0.5.0 → 1.0.0
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.hound.yml +2 -0
- data/.rspec +1 -1
- data/.rubocop.yml +1 -0
- data/.ruby-style.yml +335 -0
- data/.travis.yml +6 -3
- data/CHANGELOG.md +12 -1
- data/README.md +6 -5
- data/lib/memfs.rb +29 -2
- data/lib/memfs/dir.rb +6 -6
- data/lib/memfs/fake/directory.rb +5 -2
- data/lib/memfs/fake/file.rb +4 -5
- data/lib/memfs/fake/file/content.rb +0 -2
- data/lib/memfs/file.rb +27 -20
- data/lib/memfs/file/stat.rb +1 -1
- data/lib/memfs/file_system.rb +3 -3
- data/lib/memfs/io.rb +190 -154
- data/lib/memfs/version.rb +1 -1
- data/memfs.gemspec +4 -1
- data/spec/fileutils_spec.rb +23 -18
- data/spec/memfs/dir_spec.rb +20 -9
- data/spec/memfs/fake/directory_spec.rb +5 -1
- data/spec/memfs/fake/entry_spec.rb +5 -5
- data/spec/memfs/fake/file/content_spec.rb +1 -1
- data/spec/memfs/fake/file_spec.rb +1 -1
- data/spec/memfs/fake/symlink_spec.rb +2 -2
- data/spec/memfs/file/stat_spec.rb +6 -3
- data/spec/memfs/file_spec.rb +140 -57
- data/spec/memfs/file_system_spec.rb +5 -4
- data/spec/memfs_spec.rb +39 -2
- data/spec/spec_helper.rb +23 -21
- metadata +22 -6
- data/.rubocop.yml +0 -29
data/lib/memfs/dir.rb
CHANGED
@@ -47,7 +47,7 @@ module MemFs
|
|
47
47
|
class << self; alias_method :pwd, :getwd; end
|
48
48
|
|
49
49
|
def self.glob(patterns, flags = 0)
|
50
|
-
patterns = [*patterns]
|
50
|
+
patterns = [*patterns].map(&:to_s)
|
51
51
|
list = fs.paths.select do |path|
|
52
52
|
patterns.any? do |pattern|
|
53
53
|
File.fnmatch?(pattern, path, flags | GLOB_FLAGS)
|
@@ -143,11 +143,11 @@ module MemFs
|
|
143
143
|
|
144
144
|
private
|
145
145
|
|
146
|
-
if defined?(File::FNM_EXTGLOB)
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
146
|
+
GLOB_FLAGS = if defined?(File::FNM_EXTGLOB)
|
147
|
+
File::FNM_EXTGLOB | File::FNM_PATHNAME
|
148
|
+
else
|
149
|
+
File::FNM_PATHNAME
|
150
|
+
end
|
151
151
|
|
152
152
|
attr_accessor :entry, :max_seek, :state
|
153
153
|
|
data/lib/memfs/fake/directory.rb
CHANGED
@@ -19,7 +19,7 @@ module MemFs
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def find(path)
|
22
|
-
path = path.sub(
|
22
|
+
path = path.sub(%r{\A/+}, '').sub(%r{/+\z}, '')
|
23
23
|
parts = path.split('/', 2)
|
24
24
|
|
25
25
|
if entry_names.include?(path)
|
@@ -44,7 +44,10 @@ module MemFs
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def paths
|
47
|
-
[path] + entries.reject { |p| %w[. ..].include?(p) }
|
47
|
+
[path] + entries.reject { |p| %w[. ..].include?(p) }
|
48
|
+
.values
|
49
|
+
.map(&:paths)
|
50
|
+
.flatten
|
48
51
|
end
|
49
52
|
|
50
53
|
def remove_entry(entry)
|
data/lib/memfs/fake/file.rb
CHANGED
@@ -33,11 +33,10 @@ module MemFs
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def type
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
end
|
36
|
+
return 'blockSpecial' if block_device
|
37
|
+
return 'characterSpecial' if character_device
|
38
|
+
|
39
|
+
'file'
|
41
40
|
end
|
42
41
|
end
|
43
42
|
end
|
data/lib/memfs/file.rb
CHANGED
@@ -3,15 +3,12 @@ require 'memfs/filesystem_access'
|
|
3
3
|
require 'memfs/io'
|
4
4
|
|
5
5
|
module MemFs
|
6
|
-
class File
|
6
|
+
class File < IO
|
7
7
|
extend FilesystemAccess
|
8
8
|
extend SingleForwardable
|
9
|
-
extend IO::ClassMethods
|
10
9
|
|
11
10
|
include Enumerable
|
12
11
|
include FilesystemAccess
|
13
|
-
include OriginalFile::Constants
|
14
|
-
include IO::InstanceMethods
|
15
12
|
|
16
13
|
ALT_SEPARATOR = nil
|
17
14
|
|
@@ -22,9 +19,9 @@ module MemFs
|
|
22
19
|
'w+' => CREAT | TRUNC | RDWR,
|
23
20
|
'a' => CREAT | APPEND | WRONLY,
|
24
21
|
'a+' => CREAT | APPEND | RDWR
|
25
|
-
}
|
22
|
+
}.freeze
|
26
23
|
|
27
|
-
SEPARATOR = '/'
|
24
|
+
SEPARATOR = '/'.freeze
|
28
25
|
SUCCESS = 0
|
29
26
|
|
30
27
|
@umask = nil
|
@@ -58,18 +55,24 @@ module MemFs
|
|
58
55
|
:writable_real?,
|
59
56
|
:zero?
|
60
57
|
].each do |query_method|
|
61
|
-
|
62
|
-
|
63
|
-
|
58
|
+
# def directory?(path)
|
59
|
+
# stat_query(path, :directory?)
|
60
|
+
# end
|
61
|
+
define_singleton_method(query_method) do |path|
|
62
|
+
stat_query(path, query_method)
|
63
|
+
end
|
64
64
|
end
|
65
65
|
|
66
66
|
[
|
67
67
|
:world_readable?,
|
68
68
|
:world_writable?
|
69
69
|
].each do |query_method|
|
70
|
-
|
71
|
-
|
72
|
-
|
70
|
+
# def directory?(path)
|
71
|
+
# stat_query(path, :directory?, false)
|
72
|
+
# end
|
73
|
+
define_singleton_method(query_method) do |path|
|
74
|
+
stat_query(path, query_method, false)
|
75
|
+
end
|
73
76
|
end
|
74
77
|
|
75
78
|
def self.absolute_path(path, dir_string = fs.pwd)
|
@@ -233,21 +236,27 @@ module MemFs
|
|
233
236
|
|
234
237
|
def initialize(filename, mode = File::RDONLY, *perm_and_or_opt)
|
235
238
|
opt = perm_and_or_opt.last.is_a?(Hash) ? perm_and_or_opt.pop : {}
|
236
|
-
|
237
|
-
if perm_and_or_opt.
|
239
|
+
perm_and_or_opt.shift
|
240
|
+
if perm_and_or_opt.any?
|
238
241
|
fail ArgumentError, 'wrong number of arguments (4 for 1..3)'
|
239
242
|
end
|
240
243
|
|
241
244
|
@path = filename
|
242
|
-
@external_encoding = opt[:external_encoding] &&
|
245
|
+
@external_encoding = opt[:external_encoding] &&
|
246
|
+
Encoding.find(opt[:external_encoding])
|
243
247
|
|
244
248
|
self.closed = false
|
245
249
|
self.opening_mode = str_to_mode_int(mode)
|
246
250
|
|
247
251
|
fs.touch(filename) if create_file?
|
248
252
|
|
249
|
-
self.entry = fs.find(filename)
|
253
|
+
self.entry = fs.find!(filename)
|
254
|
+
# FIXME: this is an ugly way to ensure a symlink has a target
|
255
|
+
entry.dereferenced
|
250
256
|
|
257
|
+
if entry.respond_to?(:pos=)
|
258
|
+
entry.pos = 0
|
259
|
+
end
|
251
260
|
entry.content.clear if truncate_file?
|
252
261
|
end
|
253
262
|
|
@@ -289,8 +298,6 @@ module MemFs
|
|
289
298
|
File.truncate(path, integer)
|
290
299
|
end
|
291
300
|
|
292
|
-
private
|
293
|
-
|
294
301
|
def self.dereference_name(path)
|
295
302
|
entry = fs.find(path)
|
296
303
|
if entry
|
@@ -323,13 +330,13 @@ module MemFs
|
|
323
330
|
|
324
331
|
def self.stat_query(path, query, force_boolean = true)
|
325
332
|
response = fs.find(path) && stat(path).public_send(query)
|
326
|
-
force_boolean ? !!
|
333
|
+
force_boolean ? !!response : response
|
327
334
|
end
|
328
335
|
private_class_method :stat_query
|
329
336
|
|
330
337
|
def self.lstat_query(path, query)
|
331
338
|
response = fs.find(path) && lstat(path).public_send(query)
|
332
|
-
!!
|
339
|
+
!!response
|
333
340
|
end
|
334
341
|
private_class_method :lstat_query
|
335
342
|
end
|
data/lib/memfs/file/stat.rb
CHANGED
data/lib/memfs/file_system.rb
CHANGED
@@ -15,15 +15,15 @@ module MemFs
|
|
15
15
|
File.basename(path)
|
16
16
|
end
|
17
17
|
|
18
|
-
def chdir(path
|
18
|
+
def chdir(path)
|
19
19
|
destination = find_directory!(path)
|
20
20
|
|
21
21
|
previous_directory = working_directory
|
22
22
|
self.working_directory = destination
|
23
23
|
|
24
|
-
|
24
|
+
yield if block_given?
|
25
25
|
ensure
|
26
|
-
self.working_directory = previous_directory if
|
26
|
+
self.working_directory = previous_directory if block_given?
|
27
27
|
end
|
28
28
|
|
29
29
|
def clear!
|
data/lib/memfs/io.rb
CHANGED
@@ -2,203 +2,239 @@ require 'forwardable'
|
|
2
2
|
require 'memfs/filesystem_access'
|
3
3
|
|
4
4
|
module MemFs
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
5
|
+
class IO
|
6
|
+
extend SingleForwardable
|
7
|
+
include OriginalFile::Constants
|
8
|
+
|
9
|
+
(OriginalIO.constants - OriginalFile::Constants.constants)
|
10
|
+
.each do |const_name|
|
11
|
+
self.const_set(const_name, OriginalIO.const_get(const_name))
|
12
|
+
end
|
13
|
+
|
14
|
+
def_delegators :original_io_class,
|
15
|
+
:copy_stream
|
16
|
+
|
17
|
+
def self.read(path, *args)
|
18
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
19
|
+
options = {
|
20
|
+
mode: File::RDONLY,
|
21
|
+
encoding: nil,
|
22
|
+
open_args: nil
|
23
|
+
}.merge(options)
|
24
|
+
open_args = options[:open_args] ||
|
25
|
+
[options[:mode], encoding: options[:encoding]]
|
26
|
+
|
27
|
+
length, offset = args
|
28
|
+
|
29
|
+
file = open(path, *open_args)
|
30
|
+
file.seek(offset || 0)
|
31
|
+
file.read(length)
|
32
|
+
ensure
|
33
|
+
file.close if file
|
21
34
|
end
|
22
35
|
|
23
|
-
|
24
|
-
|
25
|
-
:close_on_exec
|
26
|
-
|
27
|
-
def <<(object)
|
28
|
-
fail IOError, 'not opened for writing' unless writable?
|
36
|
+
def self.write(path, string, offset = 0, open_args = nil)
|
37
|
+
open_args ||= [File::WRONLY, encoding: nil]
|
29
38
|
|
30
|
-
|
39
|
+
offset = 0 if offset.nil?
|
40
|
+
unless offset.respond_to?(:to_int)
|
41
|
+
fail TypeError, "no implicit conversion from #{offset.class}"
|
31
42
|
end
|
43
|
+
offset = offset.to_int
|
32
44
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
:noreuse,
|
37
|
-
:normal,
|
38
|
-
:random,
|
39
|
-
:sequential,
|
40
|
-
:willneed
|
41
|
-
]
|
42
|
-
unless advice_types.include?(advice_type)
|
43
|
-
fail NotImplementedError, "Unsupported advice: #{advice_type.inspect}"
|
44
|
-
end
|
45
|
-
nil
|
45
|
+
if offset > 0
|
46
|
+
fail NotImplementedError,
|
47
|
+
'MemFs::IO.write with offset not yet supported.'
|
46
48
|
end
|
47
49
|
|
48
|
-
|
49
|
-
|
50
|
-
|
50
|
+
file = open(path, *open_args)
|
51
|
+
file.seek(offset)
|
52
|
+
file.write(string)
|
53
|
+
ensure
|
54
|
+
file.close if file
|
55
|
+
end
|
51
56
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
57
|
+
def self.original_io_class
|
58
|
+
MemFs::OriginalIO
|
59
|
+
end
|
60
|
+
private_class_method :original_io_class
|
57
61
|
|
58
|
-
|
59
|
-
|
60
|
-
end
|
62
|
+
attr_writer :autoclose,
|
63
|
+
:close_on_exec
|
61
64
|
|
62
|
-
|
63
|
-
|
64
|
-
end
|
65
|
+
def <<(object)
|
66
|
+
fail IOError, 'not opened for writing' unless writable?
|
65
67
|
|
66
|
-
|
67
|
-
|
68
|
-
end
|
68
|
+
content << object.to_s
|
69
|
+
end
|
69
70
|
|
70
|
-
|
71
|
-
|
72
|
-
|
71
|
+
def advise(advice_type, _offset = 0, _len = 0)
|
72
|
+
advice_types = [
|
73
|
+
:dontneed,
|
74
|
+
:noreuse,
|
75
|
+
:normal,
|
76
|
+
:random,
|
77
|
+
:sequential,
|
78
|
+
:willneed
|
79
|
+
]
|
80
|
+
unless advice_types.include?(advice_type)
|
81
|
+
fail NotImplementedError, "Unsupported advice: #{advice_type.inspect}"
|
82
|
+
end
|
83
|
+
nil
|
84
|
+
end
|
73
85
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
alias_method :eof, :eof?
|
78
|
-
|
79
|
-
def external_encoding
|
80
|
-
if writable?
|
81
|
-
@external_encoding
|
82
|
-
else
|
83
|
-
@external_encoding ||= Encoding.default_external
|
84
|
-
end
|
85
|
-
end
|
86
|
+
def autoclose?
|
87
|
+
defined?(@autoclose) ? !!@autoclose : true
|
88
|
+
end
|
86
89
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
end
|
90
|
+
def binmode
|
91
|
+
@binmode = true
|
92
|
+
@external_encoding = Encoding::ASCII_8BIT
|
93
|
+
self
|
94
|
+
end
|
93
95
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
content.each_byte { |byte| block.call(byte) }
|
98
|
-
self
|
99
|
-
end
|
100
|
-
alias_method :bytes, :each_byte
|
96
|
+
def binmode?
|
97
|
+
defined?(@binmode) ? @binmode : false
|
98
|
+
end
|
101
99
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
content.each_char { |char| block.call(char) }
|
106
|
-
self
|
107
|
-
end
|
108
|
-
alias_method :chars, :each_char
|
100
|
+
def close
|
101
|
+
self.closed = true
|
102
|
+
end
|
109
103
|
|
110
|
-
|
111
|
-
|
112
|
-
|
104
|
+
def closed?
|
105
|
+
closed
|
106
|
+
end
|
113
107
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
108
|
+
def close_on_exec?
|
109
|
+
defined?(@close_on_exec) ? !!@close_on_exec : true
|
110
|
+
end
|
111
|
+
|
112
|
+
def eof?
|
113
|
+
pos >= content.size
|
114
|
+
end
|
115
|
+
alias_method :eof, :eof?
|
120
116
|
|
121
|
-
|
122
|
-
|
117
|
+
def external_encoding
|
118
|
+
if writable?
|
119
|
+
@external_encoding
|
120
|
+
else
|
121
|
+
@external_encoding ||= Encoding.default_external
|
123
122
|
end
|
123
|
+
end
|
124
124
|
|
125
|
-
|
126
|
-
|
125
|
+
def each(sep = $/)
|
126
|
+
return to_enum(__callee__) unless block_given?
|
127
|
+
fail IOError, 'not opened for reading' unless readable?
|
128
|
+
content.each_line(sep) { |line| yield(line) }
|
129
|
+
self
|
130
|
+
end
|
127
131
|
|
128
|
-
|
129
|
-
|
132
|
+
def each_byte
|
133
|
+
return to_enum(__callee__) unless block_given?
|
134
|
+
fail IOError, 'not opened for reading' unless readable?
|
135
|
+
content.each_byte { |byte| yield(byte) }
|
136
|
+
self
|
137
|
+
end
|
138
|
+
alias_method :bytes, :each_byte
|
130
139
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
140
|
+
def each_char
|
141
|
+
return to_enum(__callee__) unless block_given?
|
142
|
+
fail IOError, 'not opened for reading' unless readable?
|
143
|
+
content.each_char { |char| yield(char) }
|
144
|
+
self
|
145
|
+
end
|
146
|
+
alias_method :chars, :each_char
|
138
147
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
when ::IO::SEEK_END then content.to_s.length + amount
|
143
|
-
when ::IO::SEEK_SET then amount
|
144
|
-
end
|
148
|
+
def pos
|
149
|
+
entry.pos
|
150
|
+
end
|
145
151
|
|
146
|
-
|
152
|
+
def print(*objs)
|
153
|
+
objs << $_ if objs.empty?
|
154
|
+
self << objs.join($,) << $\.to_s
|
155
|
+
nil
|
156
|
+
end
|
147
157
|
|
148
|
-
|
149
|
-
|
150
|
-
|
158
|
+
def printf(format_string, *objs)
|
159
|
+
print format_string % objs
|
160
|
+
end
|
151
161
|
|
152
|
-
|
153
|
-
|
154
|
-
end
|
162
|
+
def puts(text)
|
163
|
+
fail IOError, 'not opened for writing' unless writable?
|
155
164
|
|
156
|
-
|
157
|
-
|
165
|
+
content.puts text
|
166
|
+
end
|
158
167
|
|
159
|
-
|
168
|
+
def read(length = nil, buffer = '')
|
169
|
+
unless entry
|
170
|
+
fail(Errno::ENOENT, path)
|
160
171
|
end
|
172
|
+
default = length ? nil : ''
|
173
|
+
content.read(length, buffer) || default
|
174
|
+
end
|
161
175
|
|
162
|
-
|
176
|
+
def seek(amount, whence = ::IO::SEEK_SET)
|
177
|
+
new_pos = case whence
|
178
|
+
when ::IO::SEEK_CUR then entry.pos + amount
|
179
|
+
when ::IO::SEEK_END then content.to_s.length + amount
|
180
|
+
when ::IO::SEEK_SET then amount
|
181
|
+
end
|
163
182
|
|
164
|
-
|
165
|
-
:entry,
|
166
|
-
:opening_mode
|
183
|
+
fail Errno::EINVAL, path if new_pos.nil? || new_pos < 0
|
167
184
|
|
168
|
-
|
185
|
+
entry.pos = new_pos
|
186
|
+
0
|
187
|
+
end
|
169
188
|
|
170
|
-
|
171
|
-
|
172
|
-
|
189
|
+
def stat
|
190
|
+
File.stat(path)
|
191
|
+
end
|
173
192
|
|
174
|
-
|
175
|
-
|
176
|
-
end
|
193
|
+
def write(string)
|
194
|
+
fail IOError, 'not opened for writing' unless writable?
|
177
195
|
|
178
|
-
|
179
|
-
|
180
|
-
(opening_mode | File::RDONLY).zero?
|
181
|
-
end
|
196
|
+
content.write(string.to_s)
|
197
|
+
end
|
182
198
|
|
183
|
-
|
184
|
-
return mode unless mode.is_a?(String)
|
199
|
+
private
|
185
200
|
|
186
|
-
|
187
|
-
|
188
|
-
|
201
|
+
attr_accessor :closed,
|
202
|
+
:entry,
|
203
|
+
:opening_mode
|
189
204
|
|
190
|
-
|
191
|
-
|
192
|
-
|
205
|
+
attr_reader :path
|
206
|
+
|
207
|
+
def content
|
208
|
+
entry.content
|
209
|
+
end
|
193
210
|
|
194
|
-
|
195
|
-
|
211
|
+
def create_file?
|
212
|
+
(opening_mode & File::CREAT).nonzero?
|
213
|
+
end
|
214
|
+
|
215
|
+
def readable?
|
216
|
+
(opening_mode & File::RDWR).nonzero? ||
|
217
|
+
(opening_mode | File::RDONLY).zero?
|
218
|
+
end
|
219
|
+
|
220
|
+
def str_to_mode_int(mode)
|
221
|
+
return mode unless mode.is_a?(String)
|
222
|
+
|
223
|
+
unless mode =~ /\A([rwa]\+?)([bt])?(:bom)?(\|.+)?\z/
|
224
|
+
fail ArgumentError, "invalid access mode #{mode}"
|
196
225
|
end
|
197
226
|
|
198
|
-
|
199
|
-
|
227
|
+
mode_str = $~[1]
|
228
|
+
File::MODE_MAP[mode_str]
|
229
|
+
end
|
230
|
+
|
231
|
+
def truncate_file?
|
232
|
+
(opening_mode & File::TRUNC).nonzero?
|
233
|
+
end
|
234
|
+
|
235
|
+
def writable?
|
236
|
+
(opening_mode & File::WRONLY).nonzero? ||
|
200
237
|
(opening_mode & File::RDWR).nonzero?
|
201
|
-
end
|
202
238
|
end
|
203
239
|
end
|
204
240
|
end
|