cotta 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.
- data/README +75 -0
- data/lib/cotta.rb +12 -0
- data/lib/cotta/cotta.rb +37 -0
- data/lib/cotta/cotta_dir.rb +184 -0
- data/lib/cotta/cotta_file.rb +253 -0
- data/lib/cotta/file_factory.rb +117 -0
- data/lib/cotta/file_not_found_error.rb +13 -0
- data/lib/cotta/impl/command_error.rb +13 -0
- data/lib/cotta/impl/command_interface.rb +46 -0
- data/lib/cotta/impl/command_runner.rb +55 -0
- data/lib/cotta/impl/cotta_pathname.rb +9 -0
- data/lib/cotta/impl/in_memory_system.rb +295 -0
- data/lib/cotta/impl/physical_system.rb +90 -0
- data/lib/cotta/version +1 -0
- data/test/cotta/command_interface_spec.rb +42 -0
- data/test/cotta/command_runner_spec.rb +24 -0
- data/test/cotta/content.txt +3 -0
- data/test/cotta/cotta_dir_behaviors.rb +202 -0
- data/test/cotta/cotta_dir_in_memory_spec.rb +23 -0
- data/test/cotta/cotta_dir_physical_spec.rb +14 -0
- data/test/cotta/cotta_file_behaviors.rb +139 -0
- data/test/cotta/cotta_file_in_memory_spec.rb +15 -0
- data/test/cotta/cotta_file_physical_spec.rb +47 -0
- data/test/cotta/cotta_zip_support_spec.rb +53 -0
- data/test/cotta/example_spec.rb +20 -0
- data/test/cotta/file_factory_spec.rb +54 -0
- data/test/cotta/file_system_behaviors.rb +163 -0
- data/test/cotta/in_memory_system_spec.rb +24 -0
- data/test/cotta/logo.gif +0 -0
- data/test/cotta/pathname_spec.rb +20 -0
- data/test/cotta/physical_system_spec.rb +34 -0
- data/test/cotta/physical_system_stub.rb +122 -0
- data/test/cotta/tar_test.tar +0 -0
- data/test/test.rb +19 -0
- data/test/ts_cotta.rb +6 -0
- metadata +89 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
module Cotta
|
2
|
+
# The file factory of Cotta files that handles creation of the CottaFile and CottaDir
|
3
|
+
# instances. This class also can be used to start command lines
|
4
|
+
class FileFactory
|
5
|
+
# file system that is backing the current file factory
|
6
|
+
attr_reader :system
|
7
|
+
# attr_accessor :command_interface
|
8
|
+
|
9
|
+
# Creates an instance of the file factory with the given system
|
10
|
+
def initialize(system=PhysicalSystem.new)
|
11
|
+
@system = system
|
12
|
+
# @command_interface = CommandInterface.new
|
13
|
+
end
|
14
|
+
|
15
|
+
=begin not for 1.0 release
|
16
|
+
# Invoke the command line through the backing system
|
17
|
+
def shell(command_line, &block)
|
18
|
+
@system.shell(command_line, &block)
|
19
|
+
end
|
20
|
+
=end
|
21
|
+
# Returns the current directory
|
22
|
+
def pwd
|
23
|
+
dir(@system.pwd)
|
24
|
+
end
|
25
|
+
|
26
|
+
=begin not for 1.0 release
|
27
|
+
# Starts the process. Unlike shell method, this method does not
|
28
|
+
# collect the output, thus suitable for starting a server in Ruby
|
29
|
+
# and leave it running for a long time
|
30
|
+
def start(command_line)
|
31
|
+
@system.shell(command_line) do |io|
|
32
|
+
if (block_given?)
|
33
|
+
yield io
|
34
|
+
else
|
35
|
+
while (line = io.gets)
|
36
|
+
puts line
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def command_interface=(value)
|
43
|
+
if (value)
|
44
|
+
@command_interface = value
|
45
|
+
else
|
46
|
+
@command_interface = CommandInterface.new
|
47
|
+
end
|
48
|
+
end
|
49
|
+
=end
|
50
|
+
|
51
|
+
# Returns a CottaDir instance with the given path
|
52
|
+
def dir(path)
|
53
|
+
return nil unless path
|
54
|
+
return CottaDir.new(self, Pathname.new(path))
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the FileFactory instance that represents the
|
58
|
+
# physical file system
|
59
|
+
def self::physical
|
60
|
+
PHYSICAL
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return the FileFactory instance that represents an
|
64
|
+
# in-memory file system
|
65
|
+
def self::in_memory
|
66
|
+
FileFactory.new(InMemorySystem.new)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a CottDirectory with the PhysicalSystem
|
70
|
+
def self::dir(path)
|
71
|
+
return nil unless path
|
72
|
+
return physical.dir(File.expand_path(path))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns a CottaFile in the current system with the path
|
76
|
+
def file(path)
|
77
|
+
return nil unless path
|
78
|
+
return CottaFile.new(self, Pathname.new(path))
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a CottaFile with the PhysicalSystem and the given path
|
82
|
+
def self::file(path)
|
83
|
+
return nil unless path
|
84
|
+
return physical.file(File.expand_path(path))
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns a CottaDir with the PhysicalSystem and is the parent of the given file path
|
88
|
+
def self::parent_dir(path)
|
89
|
+
return file(path).parent
|
90
|
+
end
|
91
|
+
|
92
|
+
# Creates the entry given a path. This will return either
|
93
|
+
# CottaFile or CottaDirectory depending by checking the path
|
94
|
+
# passed in, which means that if neither a directory nor a file
|
95
|
+
# exists with this name it will raise an error
|
96
|
+
def entry(path)
|
97
|
+
entry_instance = file(path)
|
98
|
+
unless (entry_instance.exists?)
|
99
|
+
entry_instance = dir(path)
|
100
|
+
raise "#{path} exists as niether file nor directory" unless entry_instance.exists?
|
101
|
+
end
|
102
|
+
return entry_instance
|
103
|
+
end
|
104
|
+
|
105
|
+
=begin not for release 1.0
|
106
|
+
def environment!(variable)
|
107
|
+
@system.environment!(variable)
|
108
|
+
end
|
109
|
+
|
110
|
+
def environment(variable, default = '')
|
111
|
+
@system.environment(variable, default)
|
112
|
+
end
|
113
|
+
=end
|
114
|
+
|
115
|
+
end
|
116
|
+
PHYSICAL = FileFactory.new(PhysicalSystem.new)
|
117
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Cotta
|
2
|
+
# Error class that will be thrown if
|
3
|
+
# a system command exit with an error
|
4
|
+
# code
|
5
|
+
class CommandError < StandardError
|
6
|
+
attr_reader :proc_status, :output
|
7
|
+
|
8
|
+
def initialize(proc_status, output)
|
9
|
+
@proc_status = proc_status
|
10
|
+
@output = output
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Cotta
|
2
|
+
# A command line interface tha can
|
3
|
+
# be stubbed out by InMemorySystem
|
4
|
+
class CommandInterface
|
5
|
+
def initialize(io = SystemIo.new)
|
6
|
+
@io = io
|
7
|
+
end
|
8
|
+
|
9
|
+
def puts(content)
|
10
|
+
@io.puts(content)
|
11
|
+
end
|
12
|
+
|
13
|
+
def prompt(question)
|
14
|
+
puts(question)
|
15
|
+
gets
|
16
|
+
end
|
17
|
+
|
18
|
+
def prompt_for_choice(question, candidates)
|
19
|
+
puts(question)
|
20
|
+
0.upto(candidates.size - 1) do |index|
|
21
|
+
puts "[#{index + 1}] #{candidates[index]}"
|
22
|
+
end
|
23
|
+
answer = gets.to_i
|
24
|
+
seletion = nil
|
25
|
+
if answer > 0 && answer <= candidates.size
|
26
|
+
selection = candidates[answer - 1]
|
27
|
+
end
|
28
|
+
return selection
|
29
|
+
end
|
30
|
+
|
31
|
+
def gets
|
32
|
+
@io.gets
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class SystemIo
|
38
|
+
def puts(content)
|
39
|
+
$stdout.puts(content)
|
40
|
+
end
|
41
|
+
|
42
|
+
def gets
|
43
|
+
$stdin.gets
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
=begin
|
2
|
+
if RUBY_PLATFORM =~ /mswin|mingw/
|
3
|
+
begin
|
4
|
+
require 'win32/open3'
|
5
|
+
rescue LoadError
|
6
|
+
warn "You must 'gem install win32-open3' to use Cotta on Windows"
|
7
|
+
exit 1
|
8
|
+
end
|
9
|
+
else
|
10
|
+
require 'open3'
|
11
|
+
end
|
12
|
+
=end
|
13
|
+
|
14
|
+
module Cotta
|
15
|
+
|
16
|
+
# Command runner
|
17
|
+
class CommandRunner
|
18
|
+
attr_reader :outputs
|
19
|
+
|
20
|
+
def initialize(command)
|
21
|
+
@command = command
|
22
|
+
end
|
23
|
+
|
24
|
+
# executs the command. If a closure is
|
25
|
+
# given, it will be called with the io to the process
|
26
|
+
# If not, it will print out the log pre-fixed with a random number for the process,
|
27
|
+
# collects the output and return the output when the process finishes.
|
28
|
+
# This method will also raise CommandError if the process fails.
|
29
|
+
def execute
|
30
|
+
id = rand(10000)
|
31
|
+
puts "[#{id}]$> #{@command}"
|
32
|
+
output = nil
|
33
|
+
IO.popen(@command) do |io|
|
34
|
+
if (block_given?)
|
35
|
+
output = yield io
|
36
|
+
else
|
37
|
+
output = load_output(id, io)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
last_process = $?
|
41
|
+
raise CommandError.new(last_process, output), @command unless last_process.exitstatus == 0
|
42
|
+
return output
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def load_output(id, io)
|
47
|
+
output = ''
|
48
|
+
while (line = io.gets) do
|
49
|
+
puts "[#{id}] #{line}"
|
50
|
+
output << line
|
51
|
+
end
|
52
|
+
return output
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,295 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Cotta
|
4
|
+
class InMemorySystem
|
5
|
+
|
6
|
+
attr_reader :executed_commands, :pwd
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@executed_commands = []
|
10
|
+
@content = ""
|
11
|
+
@file_system = Hash.new
|
12
|
+
@file_system[nil] = DirectoryContent.new('')
|
13
|
+
@file_system[Pathname.new('/')] = DirectoryContent.new('/')
|
14
|
+
@file_system[Pathname.new('.')] = DirectoryContent.new('.')
|
15
|
+
@output_map = Hash.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def shell(command)
|
19
|
+
@executed_commands.push(command)
|
20
|
+
result = @output_map[command]
|
21
|
+
raise "#{command} not found in expectation" unless result
|
22
|
+
return result
|
23
|
+
end
|
24
|
+
|
25
|
+
def output_for_command(command, output)
|
26
|
+
@output_map[command] = output
|
27
|
+
end
|
28
|
+
|
29
|
+
def expect_command(command)
|
30
|
+
Entry.new(command)
|
31
|
+
end
|
32
|
+
|
33
|
+
def dir_exists?(pathname)
|
34
|
+
content = path_content(pathname)
|
35
|
+
return !content.nil? && content.directory?
|
36
|
+
end
|
37
|
+
|
38
|
+
def file_exists?(pathname)
|
39
|
+
content = path_content(pathname)
|
40
|
+
return !content.nil? && content.file?
|
41
|
+
end
|
42
|
+
|
43
|
+
def dir_stat(pathname)
|
44
|
+
check_dir_exists(pathname)
|
45
|
+
path_content(pathname).stat
|
46
|
+
end
|
47
|
+
|
48
|
+
def file_stat(pathname)
|
49
|
+
check_file_exists(pathname)
|
50
|
+
path_content(pathname).stat
|
51
|
+
end
|
52
|
+
|
53
|
+
def list(pathname)
|
54
|
+
check_dir_exists(pathname)
|
55
|
+
content = path_content(pathname)
|
56
|
+
return content.children.collect {|item| item.name}
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_dir_exists(pathname)
|
60
|
+
raise Errno::ENOENT, pathname unless dir_exists? pathname
|
61
|
+
end
|
62
|
+
|
63
|
+
def check_file_exists(pathname)
|
64
|
+
raise Errno::ENOENT, pathname unless file_exists? pathname
|
65
|
+
end
|
66
|
+
|
67
|
+
def mkdir(pathname)
|
68
|
+
path_content!(pathname.cotta_parent).add(create_dir(pathname))
|
69
|
+
end
|
70
|
+
|
71
|
+
def io(*args)
|
72
|
+
file_content = retrieve_file_content(args[0], args[1])
|
73
|
+
return StringIO.new(file_content.content, *args[1, args.size - 1])
|
74
|
+
end
|
75
|
+
|
76
|
+
def copy(source, target)
|
77
|
+
copy_file(source, target)
|
78
|
+
end
|
79
|
+
|
80
|
+
def copy_file(source, target)
|
81
|
+
file_content = retrieve_file_content(source, 'r').content
|
82
|
+
create_file(target).content = file_content.clone
|
83
|
+
end
|
84
|
+
|
85
|
+
def move(source, target)
|
86
|
+
move_file(source, target)
|
87
|
+
end
|
88
|
+
|
89
|
+
def move_file(source, target)
|
90
|
+
copy(source, target)
|
91
|
+
delete_file(source)
|
92
|
+
end
|
93
|
+
|
94
|
+
def copy_dir(source, target)
|
95
|
+
check_dir_exists(source)
|
96
|
+
mkdir(target)
|
97
|
+
path_content(source).children.each do |item|
|
98
|
+
item.copy_to_dir(self, source, target)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def move_dir(source, target)
|
103
|
+
copy_dir(source, target)
|
104
|
+
delete_dir(source)
|
105
|
+
end
|
106
|
+
|
107
|
+
def delete_file(pathname)
|
108
|
+
raise Errno::ENOENT.new(pathname) unless file_exists? pathname
|
109
|
+
delete_entry(pathname)
|
110
|
+
end
|
111
|
+
|
112
|
+
def delete_dir(pathname)
|
113
|
+
raise Errno::ENOENT.new(pathname) unless dir_exists? pathname
|
114
|
+
delete_entry(pathname)
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_s
|
118
|
+
return 'InMemorySystem'
|
119
|
+
end
|
120
|
+
|
121
|
+
def chdir(path)
|
122
|
+
last_pwd = @pwd
|
123
|
+
@pwd = path.to_s
|
124
|
+
result = 0
|
125
|
+
if (block_given?)
|
126
|
+
begin
|
127
|
+
result = yield
|
128
|
+
ensure
|
129
|
+
@pwd = last_pwd
|
130
|
+
end
|
131
|
+
end
|
132
|
+
result
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
def gather_paths_to_create(pathname)
|
137
|
+
paths = Array.new
|
138
|
+
path_to_create = pathname
|
139
|
+
while (! dir_exists?(path_to_create))
|
140
|
+
paths.push(path_to_create)
|
141
|
+
path_to_create = path_to_create.cotta_parent
|
142
|
+
end
|
143
|
+
return paths
|
144
|
+
end
|
145
|
+
|
146
|
+
def create_dir(pathname)
|
147
|
+
content = DirectoryContent.new(pathname.basename.to_s)
|
148
|
+
@file_system[pathname] = content
|
149
|
+
return content
|
150
|
+
end
|
151
|
+
|
152
|
+
def create_file(pathname)
|
153
|
+
content = FileContent.new(pathname.basename.to_s)
|
154
|
+
parent_dir = pathname.cotta_parent
|
155
|
+
path_content(parent_dir).add(content)
|
156
|
+
@file_system[pathname] = content
|
157
|
+
return content
|
158
|
+
end
|
159
|
+
|
160
|
+
def path_content(pathname)
|
161
|
+
content = path_content!(pathname)
|
162
|
+
if (content.nil? && pathname.cotta_parent.nil?)
|
163
|
+
mkdir(pathname)
|
164
|
+
content = @file_system[pathname]
|
165
|
+
end
|
166
|
+
return content
|
167
|
+
end
|
168
|
+
|
169
|
+
def path_content!(pathname)
|
170
|
+
@file_system[pathname]
|
171
|
+
end
|
172
|
+
|
173
|
+
def retrieve_file_content(pathname, options)
|
174
|
+
file_content = path_content(pathname)
|
175
|
+
if (file_content.nil?)
|
176
|
+
if (options =~ /r/)
|
177
|
+
raise Errno::ENOENT.new(pathname)
|
178
|
+
end
|
179
|
+
file_content = create_file(pathname)
|
180
|
+
end
|
181
|
+
if (options =~ /w/)
|
182
|
+
file_content.touch
|
183
|
+
end
|
184
|
+
return file_content
|
185
|
+
end
|
186
|
+
|
187
|
+
def delete_entry(pathname)
|
188
|
+
@file_system.delete pathname
|
189
|
+
@file_system[pathname.cotta_parent].delete(pathname.basename.to_s)
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
class DirectoryContent
|
195
|
+
attr_reader :name, :children, :stat
|
196
|
+
|
197
|
+
def initialize(name)
|
198
|
+
@name = name
|
199
|
+
@children = Array.new
|
200
|
+
@stat = ContentStat.new(self)
|
201
|
+
end
|
202
|
+
|
203
|
+
def file?
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
|
207
|
+
def directory?
|
208
|
+
return true
|
209
|
+
end
|
210
|
+
|
211
|
+
def add(content)
|
212
|
+
@children.push(content)
|
213
|
+
end
|
214
|
+
|
215
|
+
def delete(name)
|
216
|
+
@children.delete_if {|file_content| file_content.name == name}
|
217
|
+
end
|
218
|
+
|
219
|
+
def copy_to_dir(system, parent_dir, target_dir)
|
220
|
+
source_path = parent_dir.join(name)
|
221
|
+
target_path = target_dir.join(name)
|
222
|
+
system.copy_dir(source_path, target_path)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
class FileContent
|
227
|
+
attr_reader :name, :content, :stat
|
228
|
+
attr_writer :content
|
229
|
+
|
230
|
+
def initialize(name)
|
231
|
+
@name = name
|
232
|
+
@content = ''
|
233
|
+
@stat = ContentStat.new(self)
|
234
|
+
end
|
235
|
+
|
236
|
+
def file?
|
237
|
+
return true
|
238
|
+
end
|
239
|
+
|
240
|
+
def directory?
|
241
|
+
return false
|
242
|
+
end
|
243
|
+
|
244
|
+
def size
|
245
|
+
content.size
|
246
|
+
end
|
247
|
+
|
248
|
+
def copy_to_dir(system, parent_dir, target_dir)
|
249
|
+
target_path = target_dir.join(name)
|
250
|
+
source_path = parent_dir.join(name)
|
251
|
+
system.copy_file(source_path, target_path)
|
252
|
+
end
|
253
|
+
|
254
|
+
def touch
|
255
|
+
@stat.touch
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
class ContentStat
|
260
|
+
attr_reader :mtime
|
261
|
+
|
262
|
+
def initialize(content)
|
263
|
+
@content = content
|
264
|
+
@mtime = Time.new
|
265
|
+
end
|
266
|
+
|
267
|
+
def mode
|
268
|
+
'10777'
|
269
|
+
end
|
270
|
+
|
271
|
+
def size
|
272
|
+
@content.size
|
273
|
+
end
|
274
|
+
|
275
|
+
def file?
|
276
|
+
@content.file?
|
277
|
+
end
|
278
|
+
|
279
|
+
def directory?
|
280
|
+
@content.directory?
|
281
|
+
end
|
282
|
+
|
283
|
+
def touch
|
284
|
+
@mtime = Time.new
|
285
|
+
end
|
286
|
+
|
287
|
+
def writable?
|
288
|
+
true
|
289
|
+
end
|
290
|
+
|
291
|
+
def <=> stat
|
292
|
+
mtime <=> stat.mtime
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|