pups 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +126 -0
- data/Rakefile +6 -0
- data/bin/pups +9 -0
- data/lib/pups.rb +23 -0
- data/lib/pups/cli.rb +36 -0
- data/lib/pups/command.rb +17 -0
- data/lib/pups/config.rb +115 -0
- data/lib/pups/exec_command.rb +116 -0
- data/lib/pups/file_command.rb +37 -0
- data/lib/pups/merge_command.rb +48 -0
- data/lib/pups/replace_command.rb +43 -0
- data/lib/pups/runit.rb +40 -0
- data/lib/pups/version.rb +3 -0
- data/pups.gemspec +26 -0
- data/test/config_test.rb +55 -0
- data/test/exec_command_test.rb +113 -0
- data/test/file_command_test.rb +27 -0
- data/test/merge_command_test.rb +58 -0
- data/test/replace_command_test.rb +112 -0
- data/test/test_helper.rb +4 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 055f645c7b240abd8d9d8206029043d1097611c8
|
4
|
+
data.tar.gz: 9a0c6803b35b3813058ed89c33f937a959dc9fcb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2b64563d45db0ec09bb16465e09d81ca4ef39ac92dee8c1b6dd894a24ea4eda5ecc08e6890391e57e3653365f29a39fdcf17399ee383b7bcb9e7e5978ec44b52
|
7
|
+
data.tar.gz: 0069a7d13b2070240fc23b17befc9dc2a34d1a51c6e7573b575acf892d22251fbf1bd4c44a94820cc86c3d1dbdb5999e0b048fec6dc528a2ce44f0a81d62f9f7
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Sam Saffron
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# pups
|
2
|
+
|
3
|
+
Simple yaml based bootstrapper
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'pups'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install pups
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
pups is a small library that allows you to automate the process of creating Unix images.
|
22
|
+
|
23
|
+
Example:
|
24
|
+
|
25
|
+
```
|
26
|
+
# somefile.yaml
|
27
|
+
params:
|
28
|
+
hello: hello world
|
29
|
+
|
30
|
+
run:
|
31
|
+
exec: /bin/bash -c 'echo $hello >>> hello'
|
32
|
+
```
|
33
|
+
|
34
|
+
Running: `pups somefile.yaml` will execute the shell script resulting in a file called "hello" with the "hello world" contents
|
35
|
+
|
36
|
+
### Features:
|
37
|
+
|
38
|
+
####Execution
|
39
|
+
|
40
|
+
Run multiple commands in one path:
|
41
|
+
|
42
|
+
```
|
43
|
+
run:
|
44
|
+
exec:
|
45
|
+
cd: some/path
|
46
|
+
cmd:
|
47
|
+
- echo 1
|
48
|
+
- echo 2
|
49
|
+
```
|
50
|
+
|
51
|
+
Run commands in the background (for services etc)
|
52
|
+
|
53
|
+
```
|
54
|
+
run:
|
55
|
+
exec:
|
56
|
+
cmd: /usr/bin/sshd
|
57
|
+
background: true
|
58
|
+
```
|
59
|
+
|
60
|
+
Suppress exceptions on certain commands
|
61
|
+
|
62
|
+
```
|
63
|
+
run:
|
64
|
+
exec:
|
65
|
+
cmd: /test
|
66
|
+
raise_on_faile: false
|
67
|
+
```
|
68
|
+
|
69
|
+
####Replacements:
|
70
|
+
|
71
|
+
```
|
72
|
+
run:
|
73
|
+
replace:
|
74
|
+
filename: "/etc/redis/redis.conf"
|
75
|
+
from: /^pidfile.*$/
|
76
|
+
to: ""
|
77
|
+
```
|
78
|
+
|
79
|
+
Will substitued the regex with blank, removing the pidfile line
|
80
|
+
|
81
|
+
```
|
82
|
+
run:
|
83
|
+
- replace:
|
84
|
+
filename: "/etc/nginx/conf.d/discourse.conf"
|
85
|
+
from: /upstream[^\}]+\}/m
|
86
|
+
to: "upstream discourse {
|
87
|
+
server 127.0.0.1:3000;
|
88
|
+
}"
|
89
|
+
```
|
90
|
+
|
91
|
+
Multiline replace using regex
|
92
|
+
|
93
|
+
####Merge yaml files:
|
94
|
+
|
95
|
+
```
|
96
|
+
home: /var/www/my_app
|
97
|
+
params:
|
98
|
+
database_yml:
|
99
|
+
production:
|
100
|
+
username: discourse
|
101
|
+
password: foo
|
102
|
+
|
103
|
+
run:
|
104
|
+
- merge: $home/config/database.yml $database_yml
|
105
|
+
|
106
|
+
```
|
107
|
+
|
108
|
+
Will merge the yaml file with the inline contents
|
109
|
+
|
110
|
+
####A common environment
|
111
|
+
|
112
|
+
```
|
113
|
+
env:
|
114
|
+
MY_ENV: 1
|
115
|
+
```
|
116
|
+
|
117
|
+
All executions will get this environment set up
|
118
|
+
|
119
|
+
|
120
|
+
## Contributing
|
121
|
+
|
122
|
+
1. Fork it
|
123
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
124
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
125
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
126
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/pups
ADDED
data/lib/pups.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
require "pups/version"
|
5
|
+
require "pups/config"
|
6
|
+
require "pups/command"
|
7
|
+
require "pups/exec_command"
|
8
|
+
require "pups/merge_command"
|
9
|
+
require "pups/replace_command"
|
10
|
+
require "pups/file_command"
|
11
|
+
|
12
|
+
require "pups/runit"
|
13
|
+
|
14
|
+
module Pups
|
15
|
+
def self.log
|
16
|
+
# at the moment docker likes this
|
17
|
+
@logger ||= Logger.new(STDERR)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.log=(logger)
|
21
|
+
@logger = logger
|
22
|
+
end
|
23
|
+
end
|
data/lib/pups/cli.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class Pups::Cli
|
2
|
+
|
3
|
+
def self.usage
|
4
|
+
puts "Usage: pups FILE or pups --stdin"
|
5
|
+
exit 1
|
6
|
+
end
|
7
|
+
def self.run(args)
|
8
|
+
if args.length != 1
|
9
|
+
usage
|
10
|
+
end
|
11
|
+
|
12
|
+
Pups.log.info("Loading #{args[0]}")
|
13
|
+
if args[0] == "--stdin"
|
14
|
+
conf = STDIN.readlines.join
|
15
|
+
split = conf.split("_FILE_SEPERATOR_")
|
16
|
+
|
17
|
+
conf = nil
|
18
|
+
split.each do |data|
|
19
|
+
current = YAML.load(data.strip)
|
20
|
+
if conf
|
21
|
+
conf = Pups::MergeCommand.deep_merge(conf, current, :merge_arrays)
|
22
|
+
else
|
23
|
+
conf = current
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
config = Pups::Config.new(conf)
|
28
|
+
else
|
29
|
+
config = Pups::Config.load_file(args[0])
|
30
|
+
end
|
31
|
+
config.run
|
32
|
+
|
33
|
+
ensure
|
34
|
+
Pups::ExecCommand.terminate_async
|
35
|
+
end
|
36
|
+
end
|
data/lib/pups/command.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Pups::Command
|
2
|
+
|
3
|
+
def self.run(command,params)
|
4
|
+
case command
|
5
|
+
when String then self.from_str(command,params).run
|
6
|
+
when Hash then self.from_hash(command,params).run
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.interpolate_params(cmd,params)
|
11
|
+
Pups::Config.interpolate_params(cmd,params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def interpolate_params(cmd)
|
15
|
+
Pups::Command.interpolate_params(cmd,@params)
|
16
|
+
end
|
17
|
+
end
|
data/lib/pups/config.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
class Pups::Config
|
2
|
+
|
3
|
+
attr_reader :config, :params
|
4
|
+
|
5
|
+
def self.load_file(config_file)
|
6
|
+
new YAML.load_file(config_file)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.load_config(config)
|
10
|
+
new YAML.load(config)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
validate!(@config)
|
16
|
+
@params = @config["params"]
|
17
|
+
@params ||= {}
|
18
|
+
ENV.each do |k,v|
|
19
|
+
@params["$ENV_#{k}"] = v
|
20
|
+
end
|
21
|
+
inject_hooks
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate!(conf)
|
25
|
+
# raise proper errors if nodes are missing etc
|
26
|
+
end
|
27
|
+
|
28
|
+
def inject_hooks
|
29
|
+
return unless hooks = @config["hooks"]
|
30
|
+
|
31
|
+
run = @config["run"]
|
32
|
+
|
33
|
+
positions = {}
|
34
|
+
run.each do |row|
|
35
|
+
if Hash === row
|
36
|
+
command = row.first
|
37
|
+
if Hash === command[1]
|
38
|
+
hook = command[1]["hook"]
|
39
|
+
positions[hook] = row if hook
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
hooks.each do |full, list|
|
45
|
+
|
46
|
+
offset = nil
|
47
|
+
name = nil
|
48
|
+
|
49
|
+
if full =~ /^after_/
|
50
|
+
name = full[6..-1]
|
51
|
+
offset = 1
|
52
|
+
end
|
53
|
+
|
54
|
+
if full =~ /^before_/
|
55
|
+
name = full[7..-1]
|
56
|
+
offset = 0
|
57
|
+
end
|
58
|
+
|
59
|
+
index = run.index(positions[name])
|
60
|
+
|
61
|
+
if index && index >= 0
|
62
|
+
run.insert(index + offset, *list)
|
63
|
+
else
|
64
|
+
Pups.log.info "Skipped missing #{full} hook"
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def run
|
71
|
+
run_commands
|
72
|
+
rescue => e
|
73
|
+
puts
|
74
|
+
puts
|
75
|
+
puts "FAILED"
|
76
|
+
puts "-" * 20
|
77
|
+
puts "#{e.class}: #{e}"
|
78
|
+
puts "Location of failure: #{e.backtrace[0]}"
|
79
|
+
if @last_command
|
80
|
+
puts "#{@last_command[:command]} failed with the params #{@last_command[:params].inspect}"
|
81
|
+
end
|
82
|
+
exit 1
|
83
|
+
end
|
84
|
+
|
85
|
+
def run_commands
|
86
|
+
@config["run"].each do |item|
|
87
|
+
item.each do |k,v|
|
88
|
+
type = case k
|
89
|
+
when "exec" then Pups::ExecCommand
|
90
|
+
when "merge" then Pups::MergeCommand
|
91
|
+
when "replace" then Pups::ReplaceCommand
|
92
|
+
when "file" then Pups::FileCommand
|
93
|
+
else raise SyntaxError.new("Invalid run command #{k}")
|
94
|
+
end
|
95
|
+
|
96
|
+
@last_command = { command: k, params: v }
|
97
|
+
type.run(v, @params)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def interpolate_params(cmd)
|
103
|
+
self.class.interpolate_params(cmd,@params)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.interpolate_params(cmd, params)
|
107
|
+
return unless cmd
|
108
|
+
processed = cmd.dup
|
109
|
+
params.each do |k,v|
|
110
|
+
processed.gsub!("$#{k}", v.to_s)
|
111
|
+
end
|
112
|
+
processed
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
class Pups::ExecCommand < Pups::Command
|
4
|
+
attr_reader :commands, :cd
|
5
|
+
attr_accessor :background, :raise_on_fail, :stdin, :stop_signal
|
6
|
+
|
7
|
+
def self.terminate_async(opts={})
|
8
|
+
|
9
|
+
return unless defined? @@asyncs
|
10
|
+
|
11
|
+
Pups.log.info("Terminating async processes")
|
12
|
+
|
13
|
+
@@asyncs.each do |async|
|
14
|
+
Pups.log.info("Sending #{async[:stop_signal]} to #{async[:command]} pid: #{async[:pid]}")
|
15
|
+
Process.kill(async[:stop_signal],async[:pid]) rescue nil
|
16
|
+
end
|
17
|
+
|
18
|
+
@@asyncs.map do |async|
|
19
|
+
Thread.new do
|
20
|
+
begin
|
21
|
+
Timeout.timeout(opts[:wait] || 10) do
|
22
|
+
Process.wait(async[:pid]) rescue nil
|
23
|
+
end
|
24
|
+
rescue Timeout::Error
|
25
|
+
Pups.log.info("#{async[:command]} pid:#{async[:pid]} did not terminate cleanly, forcing termination!")
|
26
|
+
begin
|
27
|
+
Process.kill("KILL",async[:pid])
|
28
|
+
Process.wait(async[:pid])
|
29
|
+
rescue Errno::ESRCH
|
30
|
+
rescue Errno::ECHILD
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end.each(&:join)
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.from_hash(hash, params)
|
40
|
+
cmd = new(params, hash["cd"])
|
41
|
+
|
42
|
+
case c = hash["cmd"]
|
43
|
+
when String then cmd.add(c)
|
44
|
+
when Array then c.each{|i| cmd.add(i)}
|
45
|
+
end
|
46
|
+
|
47
|
+
cmd.background = hash["background"]
|
48
|
+
cmd.stop_signal = hash["stop_signal"] || "TERM"
|
49
|
+
cmd.raise_on_fail = hash["raise_on_fail"] if hash.key? "raise_on_fail"
|
50
|
+
cmd.stdin = interpolate_params(hash["stdin"], params)
|
51
|
+
|
52
|
+
cmd
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.from_str(str, params)
|
56
|
+
cmd = new(params)
|
57
|
+
cmd.add(str)
|
58
|
+
cmd
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(params, cd = nil)
|
62
|
+
@commands = []
|
63
|
+
@params = params
|
64
|
+
@cd = interpolate_params(cd)
|
65
|
+
@raise_on_fail = true
|
66
|
+
end
|
67
|
+
|
68
|
+
def add(cmd)
|
69
|
+
@commands << process_params(cmd)
|
70
|
+
end
|
71
|
+
|
72
|
+
def run
|
73
|
+
commands.each do |command|
|
74
|
+
Pups.log.info("> #{command}")
|
75
|
+
pid = spawn(command)
|
76
|
+
Pups.log.info(@result.readlines.join("\n")) if @result
|
77
|
+
pid
|
78
|
+
end
|
79
|
+
rescue
|
80
|
+
raise if @raise_on_fail
|
81
|
+
end
|
82
|
+
|
83
|
+
def spawn(command)
|
84
|
+
if background
|
85
|
+
pid = Process.spawn(command)
|
86
|
+
(@@asyncs ||= []) << {pid: pid, command: command, stop_signal: (stop_signal || "TERM")}
|
87
|
+
Thread.new do
|
88
|
+
Process.wait(pid)
|
89
|
+
@@asyncs.delete_if{|async| async[:pid] == pid}
|
90
|
+
end
|
91
|
+
return pid
|
92
|
+
end
|
93
|
+
|
94
|
+
IO.popen(command, "w+") do |f|
|
95
|
+
if stdin
|
96
|
+
# need a way to get stdout without blocking
|
97
|
+
Pups.log.info(stdin)
|
98
|
+
f.write stdin
|
99
|
+
f.close
|
100
|
+
else
|
101
|
+
Pups.log.info(f.readlines.join)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
raise RuntimeError.new("#{command} failed with return #{$?.inspect}") unless $? == 0
|
106
|
+
|
107
|
+
nil
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
def process_params(cmd)
|
112
|
+
processed = interpolate_params(cmd)
|
113
|
+
@cd ? "cd #{cd} && #{processed}" : processed
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Pups::FileCommand < Pups::Command
|
2
|
+
attr_accessor :path, :contents, :params, :type, :chmod
|
3
|
+
|
4
|
+
def self.from_hash(hash, params)
|
5
|
+
command = new
|
6
|
+
command.path = hash["path"]
|
7
|
+
command.contents = hash["contents"]
|
8
|
+
command.chmod = hash["chmod"]
|
9
|
+
command.params = params
|
10
|
+
|
11
|
+
command
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@params = {}
|
16
|
+
@type = :bash
|
17
|
+
end
|
18
|
+
|
19
|
+
def params=(p)
|
20
|
+
@params = p
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
path = interpolate_params(@path)
|
25
|
+
|
26
|
+
`mkdir -p #{File.dirname(path)}`
|
27
|
+
File.open(path, "w") do |f|
|
28
|
+
f.write(interpolate_params(contents))
|
29
|
+
end
|
30
|
+
if @chmod
|
31
|
+
`chmod #{@chmod} #{path}`
|
32
|
+
end
|
33
|
+
Pups.log.info("File > #{path} chmod: #{@chmod}")
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Pups::MergeCommand < Pups::Command
|
2
|
+
attr_reader :filename
|
3
|
+
attr_reader :merge_hash
|
4
|
+
|
5
|
+
def self.from_str(command, params)
|
6
|
+
new(command,params)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.parse_command(command)
|
10
|
+
split = command.split(" ")
|
11
|
+
raise ArgumentError.new("Invalid merge command #{command}") unless split[-1][0] == "$"
|
12
|
+
|
13
|
+
[split[0..-2].join(" ") , split[-1][1..-1]]
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(command, params)
|
17
|
+
@params = params
|
18
|
+
|
19
|
+
filename, target_param = Pups::MergeCommand.parse_command(command)
|
20
|
+
@filename = interpolate_params(filename)
|
21
|
+
@merge_hash = params[target_param]
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
merged = self.class.deep_merge(YAML.load_file(@filename), @merge_hash)
|
26
|
+
File.open(@filename,"w"){|f| f.write(merged.to_yaml) }
|
27
|
+
Pups.log.info("Merge: #{@filename} with: \n#{@merge_hash.inspect}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.deep_merge(first,second, *args)
|
31
|
+
args ||= []
|
32
|
+
merge_arrays = args.include? :merge_arrays
|
33
|
+
|
34
|
+
merger = proc { |key, v1, v2|
|
35
|
+
if Hash === v1 && Hash === v2
|
36
|
+
v1.merge(v2, &merger)
|
37
|
+
elsif Array === v1 && Array === v2
|
38
|
+
merge_arrays ? v1 + v2 : v2
|
39
|
+
elsif NilClass === v2
|
40
|
+
v1
|
41
|
+
else
|
42
|
+
v2
|
43
|
+
end
|
44
|
+
}
|
45
|
+
first.merge(second, &merger)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Pups::ReplaceCommand < Pups::Command
|
2
|
+
attr_accessor :text, :from, :to, :filename, :direction, :global
|
3
|
+
|
4
|
+
def self.from_hash(hash, params)
|
5
|
+
replacer = new(params)
|
6
|
+
replacer.from = guess_replace_type(hash["from"])
|
7
|
+
replacer.to = guess_replace_type(hash["to"])
|
8
|
+
replacer.text = File.read(hash["filename"])
|
9
|
+
replacer.filename = hash["filename"]
|
10
|
+
replacer.direction = hash["direction"].to_sym if hash["direction"]
|
11
|
+
replacer.global = hash["global"].to_s == "true"
|
12
|
+
replacer
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.guess_replace_type(item)
|
16
|
+
# evaling to get all the regex flags easily
|
17
|
+
item[0] == "/" ? eval(item) : item
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(params)
|
21
|
+
@params = params
|
22
|
+
end
|
23
|
+
|
24
|
+
def replaced_text
|
25
|
+
new_to = to
|
26
|
+
if String === to
|
27
|
+
new_to = interpolate_params(to)
|
28
|
+
end
|
29
|
+
if global
|
30
|
+
text.gsub(from,new_to)
|
31
|
+
elsif direction == :reverse
|
32
|
+
index = text.rindex(from)
|
33
|
+
text[0..index-1] << text[index..-1].sub(from,new_to)
|
34
|
+
else
|
35
|
+
text.sub(from,new_to)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def run
|
40
|
+
Pups.log.info("Replacing #{from.to_s} with #{to.to_s} in #{filename}")
|
41
|
+
File.open(filename, "w"){|f| f.write replaced_text }
|
42
|
+
end
|
43
|
+
end
|
data/lib/pups/runit.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
class Pups::Runit
|
2
|
+
|
3
|
+
attr_accessor :env, :exec, :cd, :name
|
4
|
+
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
def setup
|
11
|
+
`mkdir -p /etc/service/#{name}`
|
12
|
+
run = "/etc/service/#{name}/run"
|
13
|
+
File.open(run, "w") do |f|
|
14
|
+
f.write(run_script)
|
15
|
+
end
|
16
|
+
`chmod +x #{run}`
|
17
|
+
end
|
18
|
+
|
19
|
+
def run_script
|
20
|
+
"#!/bin/bash
|
21
|
+
exec 2>&1
|
22
|
+
#{env_script}
|
23
|
+
#{cd_script}
|
24
|
+
#{exec}
|
25
|
+
"
|
26
|
+
end
|
27
|
+
|
28
|
+
def cd_script
|
29
|
+
"cd #{@cd}" if @cd
|
30
|
+
end
|
31
|
+
|
32
|
+
def env_script
|
33
|
+
if @env
|
34
|
+
@env.map do |k,v|
|
35
|
+
"export #{k}=#{v}"
|
36
|
+
end.join("\n")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/lib/pups/version.rb
ADDED
data/pups.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pups/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pups"
|
8
|
+
spec.version = Pups::VERSION
|
9
|
+
spec.authors = ["Sam Saffron"]
|
10
|
+
spec.email = ["sam.saffron@gmail.com"]
|
11
|
+
spec.description = %q{Process orchestrator}
|
12
|
+
spec.summary = %q{Process orchestrator}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "minitest"
|
24
|
+
spec.add_development_dependency "guard"
|
25
|
+
spec.add_development_dependency "guard-minitest"
|
26
|
+
end
|
data/test/config_test.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class ConfigTest < MiniTest::Test
|
6
|
+
|
7
|
+
def test_config_from_env
|
8
|
+
ENV["HELLO"] = "world"
|
9
|
+
config = Config.new({})
|
10
|
+
assert_equal("world", config.params["$ENV_HELLO"])
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_integration
|
14
|
+
|
15
|
+
f = Tempfile.new("test")
|
16
|
+
f.close
|
17
|
+
|
18
|
+
config = <<YAML
|
19
|
+
params:
|
20
|
+
run: #{f.path}
|
21
|
+
run:
|
22
|
+
- exec: echo hello world >> #{f.path}
|
23
|
+
YAML
|
24
|
+
|
25
|
+
Config.new(YAML.load(config)).run
|
26
|
+
assert_equal("hello world", File.read(f.path).strip)
|
27
|
+
|
28
|
+
ensure
|
29
|
+
f.unlink
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_hooks
|
33
|
+
yaml = <<YAML
|
34
|
+
run:
|
35
|
+
- exec: 1
|
36
|
+
- exec:
|
37
|
+
hook: middle
|
38
|
+
cmd: 2
|
39
|
+
- exec: 3
|
40
|
+
hooks:
|
41
|
+
after_middle:
|
42
|
+
- exec: 2.1
|
43
|
+
before_middle:
|
44
|
+
- exec: 1.9
|
45
|
+
YAML
|
46
|
+
|
47
|
+
config = Config.load_config(yaml).config
|
48
|
+
assert_equal({ "exec" => 1.9 }, config["run"][1])
|
49
|
+
assert_equal({ "exec" => 2.1 }, config["run"][3])
|
50
|
+
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class ExecCommandTest < MiniTest::Test
|
6
|
+
|
7
|
+
def from_str(str, params={})
|
8
|
+
ExecCommand.from_str(str, params).commands
|
9
|
+
end
|
10
|
+
|
11
|
+
def from_hash(hash, params={})
|
12
|
+
ExecCommand.from_hash(hash, params).commands
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_simple_str_command
|
16
|
+
assert_equal(["do_something"],
|
17
|
+
from_str("do_something"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_simple_str_command_with_param
|
21
|
+
assert_equal(["hello world"],
|
22
|
+
from_str("hello $bob", {"bob" => "world"}))
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_nested_command
|
26
|
+
assert_equal(["first"],
|
27
|
+
from_hash("cmd" => "first"))
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_multi_commands
|
31
|
+
assert_equal(["first","second"],
|
32
|
+
from_hash("cmd" => ["first","second"]))
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_multi_commands_with_home
|
36
|
+
assert_equal(["cd /home/sam && first",
|
37
|
+
"cd /home/sam && second"],
|
38
|
+
from_hash("cmd" => ["first","second"],
|
39
|
+
"cd" => "/home/sam"))
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_exec_works
|
43
|
+
ExecCommand.from_str("ls",{}).run
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_fails_for_bad_command
|
47
|
+
assert_raises(Errno::ENOENT) do
|
48
|
+
ExecCommand.from_str("boom",{}).run
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_backgroud_task_do_not_fail
|
53
|
+
cmd = ExecCommand.new({})
|
54
|
+
cmd.background = true
|
55
|
+
cmd.add("sleep 10 && exit 1")
|
56
|
+
cmd.run
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_raise_on_fail
|
60
|
+
cmd = ExecCommand.new({})
|
61
|
+
cmd.add("chgrp -a")
|
62
|
+
cmd.raise_on_fail = false
|
63
|
+
cmd.run
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_stdin
|
67
|
+
|
68
|
+
`touch test_file`
|
69
|
+
cmd = ExecCommand.new({})
|
70
|
+
cmd.add("read test ; echo $test > test_file")
|
71
|
+
cmd.stdin = "hello"
|
72
|
+
cmd.run
|
73
|
+
|
74
|
+
assert_equal("hello\n", File.read("test_file"))
|
75
|
+
|
76
|
+
ensure
|
77
|
+
File.delete("test_file")
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_fails_for_non_zero_exit
|
81
|
+
assert_raises(RuntimeError) do
|
82
|
+
ExecCommand.from_str("chgrp -a",{}).run
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def test_can_terminate_async
|
88
|
+
cmd = ExecCommand.new({})
|
89
|
+
cmd.background = true
|
90
|
+
pid = cmd.spawn("sleep 10 && exit 1")
|
91
|
+
ExecCommand.terminate_async
|
92
|
+
assert_raises(Errno::ECHILD) do
|
93
|
+
Process.waitpid(pid,Process::WNOHANG)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_can_terminate_rogues
|
98
|
+
cmd = ExecCommand.new({})
|
99
|
+
cmd.background = true
|
100
|
+
pid = cmd.spawn("trap \"echo TERM && sleep 100\" TERM ; sleep 100")
|
101
|
+
# we need to give bash enough time to trap
|
102
|
+
sleep 0.01
|
103
|
+
|
104
|
+
ExecCommand.terminate_async(wait: 0.1)
|
105
|
+
|
106
|
+
assert_raises(Errno::ECHILD) do
|
107
|
+
Process.waitpid(pid,Process::WNOHANG)
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class FileCommandTest < MiniTest::Test
|
6
|
+
|
7
|
+
def test_simple_file_creation
|
8
|
+
tmp = Tempfile.new("test")
|
9
|
+
tmp.write("x")
|
10
|
+
tmp.close
|
11
|
+
|
12
|
+
|
13
|
+
cmd = FileCommand.new
|
14
|
+
cmd.path = tmp.path
|
15
|
+
cmd.contents = "hello $world"
|
16
|
+
cmd.params = {"world" => "world"}
|
17
|
+
cmd.run
|
18
|
+
|
19
|
+
assert_equal("hello world",
|
20
|
+
File.read(tmp.path))
|
21
|
+
ensure
|
22
|
+
tmp.close
|
23
|
+
tmp.unlink
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class MergeCommandTest < MiniTest::Test
|
6
|
+
def test_deep_merge_arrays
|
7
|
+
a = {a: {a: ["hi",1]}}
|
8
|
+
b = {a: {a: ["hi",2]}}
|
9
|
+
c = {a: {}}
|
10
|
+
|
11
|
+
d = Pups::MergeCommand.deep_merge(a,b,:merge_arrays)
|
12
|
+
d = Pups::MergeCommand.deep_merge(d,c,:merge_arrays)
|
13
|
+
|
14
|
+
assert_equal(["hi", 1,"hi", 2], d[:a][:a])
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_merges
|
18
|
+
|
19
|
+
source = <<YAML
|
20
|
+
user:
|
21
|
+
name: "bob"
|
22
|
+
password: "xyz"
|
23
|
+
YAML
|
24
|
+
|
25
|
+
f = Tempfile.new("test")
|
26
|
+
f.write source
|
27
|
+
f.close
|
28
|
+
|
29
|
+
merge = <<YAML
|
30
|
+
user:
|
31
|
+
name: "bob2"
|
32
|
+
YAML
|
33
|
+
|
34
|
+
MergeCommand.from_str("#{f.path} $yaml", {"yaml" => YAML.load(merge) }).run
|
35
|
+
|
36
|
+
changed = YAML.load_file(f.path)
|
37
|
+
|
38
|
+
assert_equal({"user" => {
|
39
|
+
"name" => "bob2",
|
40
|
+
"password" => "xyz"
|
41
|
+
}}, changed)
|
42
|
+
|
43
|
+
def test_deep_merge_nil
|
44
|
+
a = {param: {venison: "yes please"}}
|
45
|
+
b = {param: nil}
|
46
|
+
|
47
|
+
r1 = Pups::MergeCommand.deep_merge(a,b)
|
48
|
+
r2 = Pups::MergeCommand.deep_merge(b,a)
|
49
|
+
|
50
|
+
assert_equal({venison: "yes please"}, r1[:param])
|
51
|
+
assert_equal({venison: "yes please"}, r2[:param])
|
52
|
+
end
|
53
|
+
|
54
|
+
ensure
|
55
|
+
f.unlink
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class ReplaceCommandTest < MiniTest::Test
|
6
|
+
|
7
|
+
def test_simple
|
8
|
+
command = ReplaceCommand.new({})
|
9
|
+
command.text = "hello world"
|
10
|
+
command.from = /he[^o]+o/
|
11
|
+
command.to = "world"
|
12
|
+
|
13
|
+
assert_equal("world world", command.replaced_text)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_reverse
|
17
|
+
source = <<SCR
|
18
|
+
1 one thousand 1
|
19
|
+
1 one thousand 1
|
20
|
+
1 one thousand 1
|
21
|
+
SCR
|
22
|
+
|
23
|
+
f = Tempfile.new("test")
|
24
|
+
f.write source
|
25
|
+
f.close
|
26
|
+
|
27
|
+
hash = {
|
28
|
+
"filename" => f.path,
|
29
|
+
"from" => "/one t.*d/",
|
30
|
+
"to" => "hello world",
|
31
|
+
"direction" => "reverse"
|
32
|
+
}
|
33
|
+
|
34
|
+
command = ReplaceCommand.from_hash(hash, {})
|
35
|
+
|
36
|
+
assert_equal("1 one thousand 1\n1 one thousand 1\n1 hello world 1\n", command.replaced_text)
|
37
|
+
ensure
|
38
|
+
f.unlink
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_global
|
42
|
+
source = <<SCR
|
43
|
+
one
|
44
|
+
one
|
45
|
+
one
|
46
|
+
SCR
|
47
|
+
|
48
|
+
f = Tempfile.new("test")
|
49
|
+
f.write source
|
50
|
+
f.close
|
51
|
+
|
52
|
+
hash = {
|
53
|
+
"filename" => f.path,
|
54
|
+
"from" => "/one/",
|
55
|
+
"to" => "two",
|
56
|
+
"global" => "true"
|
57
|
+
}
|
58
|
+
|
59
|
+
command = ReplaceCommand.from_hash(hash, {})
|
60
|
+
|
61
|
+
assert_equal("two\ntwo\ntwo\n", command.replaced_text)
|
62
|
+
ensure
|
63
|
+
f.unlink
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_replace_with_env
|
68
|
+
source = "123"
|
69
|
+
|
70
|
+
f = Tempfile.new("test")
|
71
|
+
f.write source
|
72
|
+
f.close
|
73
|
+
|
74
|
+
hash = {
|
75
|
+
"filename" => f.path,
|
76
|
+
"from" => "123",
|
77
|
+
"to" => "hello $hellos"
|
78
|
+
}
|
79
|
+
|
80
|
+
command = ReplaceCommand.from_hash(hash, {"hello" => "world"})
|
81
|
+
assert_equal("hello worlds", command.replaced_text)
|
82
|
+
|
83
|
+
ensure
|
84
|
+
f.unlink
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_parse
|
88
|
+
|
89
|
+
source = <<SCR
|
90
|
+
this {
|
91
|
+
is a test
|
92
|
+
}
|
93
|
+
SCR
|
94
|
+
|
95
|
+
f = Tempfile.new("test")
|
96
|
+
f.write source
|
97
|
+
f.close
|
98
|
+
|
99
|
+
hash = {
|
100
|
+
"filename" => f.path,
|
101
|
+
"from" => "/this[^\}]+\}/m",
|
102
|
+
"to" => "hello world"
|
103
|
+
}
|
104
|
+
|
105
|
+
command = ReplaceCommand.from_hash(hash, {})
|
106
|
+
|
107
|
+
assert_equal("hello world", command.replaced_text.strip)
|
108
|
+
ensure
|
109
|
+
f.unlink
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pups
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sam Saffron
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard-minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Process orchestrator
|
84
|
+
email:
|
85
|
+
- sam.saffron@gmail.com
|
86
|
+
executables:
|
87
|
+
- pups
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- Gemfile
|
93
|
+
- Guardfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/pups
|
98
|
+
- lib/pups.rb
|
99
|
+
- lib/pups/cli.rb
|
100
|
+
- lib/pups/command.rb
|
101
|
+
- lib/pups/config.rb
|
102
|
+
- lib/pups/exec_command.rb
|
103
|
+
- lib/pups/file_command.rb
|
104
|
+
- lib/pups/merge_command.rb
|
105
|
+
- lib/pups/replace_command.rb
|
106
|
+
- lib/pups/runit.rb
|
107
|
+
- lib/pups/version.rb
|
108
|
+
- pups.gemspec
|
109
|
+
- test/config_test.rb
|
110
|
+
- test/exec_command_test.rb
|
111
|
+
- test/file_command_test.rb
|
112
|
+
- test/merge_command_test.rb
|
113
|
+
- test/replace_command_test.rb
|
114
|
+
- test/test_helper.rb
|
115
|
+
homepage: ''
|
116
|
+
licenses:
|
117
|
+
- MIT
|
118
|
+
metadata: {}
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 2.2.2
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: Process orchestrator
|
139
|
+
test_files:
|
140
|
+
- test/config_test.rb
|
141
|
+
- test/exec_command_test.rb
|
142
|
+
- test/file_command_test.rb
|
143
|
+
- test/merge_command_test.rb
|
144
|
+
- test/replace_command_test.rb
|
145
|
+
- test/test_helper.rb
|