assassin 0.4.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.
data/LICENSE ADDED
@@ -0,0 +1 @@
1
+ Ruby
@@ -0,0 +1,44 @@
1
+ NAME
2
+ ----
3
+ assassin.rb
4
+
5
+ SYNOPSIS
6
+ --------
7
+ no zombies ever, not even on `exit!` or `kill -9`
8
+
9
+ USAGE
10
+ -----
11
+ pipe = IO.popen 'program-that-must-not-be-zombied'
12
+
13
+ Assassin.at_exit_kill(pipe.pid)
14
+
15
+ also see lib/assassin.rb and test/assassin_test.rb
16
+
17
+ DESCRIPTION
18
+ -----------
19
+ assassin.rb is a small (~ 60 loc), simple, reliable methodology of ensuring
20
+ that child processes are *always* cleaned up, regardless of the manner in
21
+ which the parent program is shut down.
22
+
23
+ the basic concept it to generate, start, and detach another script that
24
+ monitors the parent process and, when that process no longer exists, ensures
25
+ through escalation of signals that a child process is shut down and does not
26
+ become a zombie.
27
+
28
+ this becomes espcially important for libraries which spawn processes, such
29
+ as via `IO.popen` that need to ensure those children are cleaned up, but
30
+ which cannot control whether client code may call `exit`. this approach
31
+ also handles being `kill -9`d - something no `at_exit{}` handler can
32
+ promise.
33
+
34
+ INSTALL
35
+ -------
36
+ gem install assassin
37
+
38
+ URI
39
+ ---
40
+ http://github.com/ahoward/assassin
41
+
42
+ HISTORY
43
+ -------
44
+ - 0.4.2. initial release
@@ -0,0 +1,394 @@
1
+ This.rubyforge_project = 'codeforpeople'
2
+ This.author = "Ara T. Howard"
3
+ This.email = "ara.t.howard@gmail.com"
4
+ This.homepage = "https://github.com/ahoward/#{ This.lib }"
5
+
6
+ task :license do
7
+ open('LICENSE', 'w'){|fd| fd.puts "same as ruby's"}
8
+ end
9
+
10
+ task :default do
11
+ puts((Rake::Task.tasks.map{|task| task.name.gsub(/::/,':')} - ['default']).sort)
12
+ end
13
+
14
+ task :test do
15
+ run_tests!
16
+ end
17
+
18
+ namespace :test do
19
+ task(:unit){ run_tests!(:unit) }
20
+ task(:functional){ run_tests!(:functional) }
21
+ task(:integration){ run_tests!(:integration) }
22
+ end
23
+
24
+ def run_tests!(which = nil)
25
+ which ||= '**'
26
+ test_dir = File.join(This.dir, "test")
27
+ test_glob ||= File.join(test_dir, "#{ which }/**_test.rb")
28
+ test_rbs = Dir.glob(test_glob).sort
29
+
30
+ div = ('=' * 119)
31
+ line = ('-' * 119)
32
+
33
+ test_rbs.each_with_index do |test_rb, index|
34
+ testno = index + 1
35
+ command = "#{ This.ruby } -w -I ./lib -I ./test/lib #{ test_rb }"
36
+
37
+ puts
38
+ say(div, :color => :cyan, :bold => true)
39
+ say("@#{ testno } => ", :bold => true, :method => :print)
40
+ say(command, :color => :cyan, :bold => true)
41
+ say(line, :color => :cyan, :bold => true)
42
+
43
+ system(command)
44
+
45
+ say(line, :color => :cyan, :bold => true)
46
+
47
+ status = $?.exitstatus
48
+
49
+ if status.zero?
50
+ say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print)
51
+ say("SUCCESS", :color => :green, :bold => true)
52
+ else
53
+ say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print)
54
+ say("FAILURE", :color => :red, :bold => true)
55
+ end
56
+ say(line, :color => :cyan, :bold => true)
57
+
58
+ exit(status) unless status.zero?
59
+ end
60
+ end
61
+
62
+
63
+ task :gemspec do
64
+ ignore_extensions = ['git', 'svn', 'tmp', /sw./, 'bak', 'gem']
65
+ ignore_directories = ['pkg']
66
+ ignore_files = ['test/log']
67
+
68
+ shiteless =
69
+ lambda do |list|
70
+ list.delete_if do |entry|
71
+ next unless test(?e, entry)
72
+ extension = File.basename(entry).split(%r/[.]/).last
73
+ ignore_extensions.any?{|ext| ext === extension}
74
+ end
75
+ list.delete_if do |entry|
76
+ next unless test(?d, entry)
77
+ dirname = File.expand_path(entry)
78
+ ignore_directories.any?{|dir| File.expand_path(dir) == dirname}
79
+ end
80
+ list.delete_if do |entry|
81
+ next unless test(?f, entry)
82
+ filename = File.expand_path(entry)
83
+ ignore_files.any?{|file| File.expand_path(file) == filename}
84
+ end
85
+ end
86
+
87
+ lib = This.lib
88
+ object = This.object
89
+ version = This.version
90
+ files = shiteless[Dir::glob("**/**")]
91
+ executables = shiteless[Dir::glob("bin/*")].map{|exe| File.basename(exe)}
92
+ #has_rdoc = true #File.exist?('doc')
93
+ test_files = "test/#{ lib }.rb" if File.file?("test/#{ lib }.rb")
94
+ summary = object.respond_to?(:summary) ? object.summary : "summary: #{ lib } kicks the ass"
95
+ description = object.respond_to?(:description) ? object.description : "description: #{ lib } kicks the ass"
96
+ license = object.respond_to?(:license) ? object.license : "same as ruby's"
97
+
98
+ if This.extensions.nil?
99
+ This.extensions = []
100
+ extensions = This.extensions
101
+ %w( Makefile configure extconf.rb ).each do |ext|
102
+ extensions << ext if File.exists?(ext)
103
+ end
104
+ end
105
+ extensions = [extensions].flatten.compact
106
+
107
+ if This.dependencies.nil?
108
+ dependencies = []
109
+ else
110
+ case This.dependencies
111
+ when Hash
112
+ dependencies = This.dependencies.values
113
+ when Array
114
+ dependencies = This.dependencies
115
+ end
116
+ end
117
+
118
+ template =
119
+ if test(?e, 'gemspec.erb')
120
+ Template{ IO.read('gemspec.erb') }
121
+ else
122
+ Template {
123
+ <<-__
124
+ ## <%= lib %>.gemspec
125
+ #
126
+
127
+ Gem::Specification::new do |spec|
128
+ spec.name = <%= lib.inspect %>
129
+ spec.version = <%= version.inspect %>
130
+ spec.platform = Gem::Platform::RUBY
131
+ spec.summary = <%= lib.inspect %>
132
+ spec.description = <%= description.inspect %>
133
+ spec.license = <%= license.inspect %>
134
+
135
+ spec.files =\n<%= files.sort.pretty_inspect %>
136
+ spec.executables = <%= executables.inspect %>
137
+
138
+ spec.require_path = "lib"
139
+
140
+ spec.test_files = <%= test_files.inspect %>
141
+
142
+ <% dependencies.each do |lib_version| %>
143
+ spec.add_dependency(*<%= Array(lib_version).flatten.inspect %>)
144
+ <% end %>
145
+
146
+ spec.extensions.push(*<%= extensions.inspect %>)
147
+
148
+ spec.rubyforge_project = <%= This.rubyforge_project.inspect %>
149
+ spec.author = <%= This.author.inspect %>
150
+ spec.email = <%= This.email.inspect %>
151
+ spec.homepage = <%= This.homepage.inspect %>
152
+ end
153
+ __
154
+ }
155
+ end
156
+
157
+ Fu.mkdir_p(This.pkgdir)
158
+ gemspec = "#{ lib }.gemspec"
159
+ open(gemspec, "w"){|fd| fd.puts(template)}
160
+ This.gemspec = gemspec
161
+ end
162
+
163
+ task :gem => [:clean, :gemspec] do
164
+ Fu.mkdir_p(This.pkgdir)
165
+ before = Dir['*.gem']
166
+ cmd = "gem build #{ This.gemspec }"
167
+ `#{ cmd }`
168
+ after = Dir['*.gem']
169
+ gem = ((after - before).first || after.first) or abort('no gem!')
170
+ Fu.mv(gem, This.pkgdir)
171
+ This.gem = File.join(This.pkgdir, File.basename(gem))
172
+ end
173
+
174
+ task :readme do
175
+ samples = ''
176
+ prompt = '~ > '
177
+ lib = This.lib
178
+ version = This.version
179
+
180
+ Dir['sample*/*'].sort.each do |sample|
181
+ samples << "\n" << " <========< #{ sample } >========>" << "\n\n"
182
+
183
+ cmd = "cat #{ sample }"
184
+ samples << Util.indent(prompt + cmd, 2) << "\n\n"
185
+ samples << Util.indent(`#{ cmd }`, 4) << "\n"
186
+
187
+ cmd = "ruby #{ sample }"
188
+ samples << Util.indent(prompt + cmd, 2) << "\n\n"
189
+
190
+ cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -I ./lib #{ sample })'"
191
+ samples << Util.indent(`#{ cmd } 2>&1`, 4) << "\n"
192
+ end
193
+
194
+ template =
195
+ if test(?e, 'README.erb')
196
+ Template{ IO.read('README.erb') }
197
+ else
198
+ Template {
199
+ <<-__
200
+ NAME
201
+ #{ lib }
202
+
203
+ DESCRIPTION
204
+
205
+ INSTALL
206
+ gem install #{ lib }
207
+
208
+ SAMPLES
209
+ #{ samples }
210
+ __
211
+ }
212
+ end
213
+
214
+ open("README", "w"){|fd| fd.puts template}
215
+ end
216
+
217
+
218
+ task :clean do
219
+ Dir[File.join(This.pkgdir, '**/**')].each{|entry| Fu.rm_rf(entry)}
220
+ end
221
+
222
+
223
+ task :release => [:clean, :gemspec, :gem] do
224
+ gems = Dir[File.join(This.pkgdir, '*.gem')].flatten
225
+ raise "which one? : #{ gems.inspect }" if gems.size > 1
226
+ raise "no gems?" if gems.size < 1
227
+
228
+ cmd = "gem push #{ This.gem }"
229
+ puts cmd
230
+ puts
231
+ system(cmd)
232
+ abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero?
233
+
234
+ cmd = "rubyforge login && rubyforge add_release #{ This.rubyforge_project } #{ This.lib } #{ This.version } #{ This.gem }"
235
+ puts cmd
236
+ puts
237
+ system(cmd)
238
+ abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero?
239
+ end
240
+
241
+
242
+
243
+
244
+
245
+ BEGIN {
246
+ # support for this rakefile
247
+ #
248
+ $VERBOSE = nil
249
+
250
+ require 'ostruct'
251
+ require 'erb'
252
+ require 'fileutils'
253
+ require 'rbconfig'
254
+ require 'pp'
255
+
256
+ # fu shortcut
257
+ #
258
+ Fu = FileUtils
259
+
260
+ # cache a bunch of stuff about this rakefile/environment
261
+ #
262
+ This = OpenStruct.new
263
+
264
+ This.file = File.expand_path(__FILE__)
265
+ This.dir = File.dirname(This.file)
266
+ This.pkgdir = File.join(This.dir, 'pkg')
267
+
268
+ # grok lib
269
+ #
270
+ lib = ENV['LIB']
271
+ unless lib
272
+ lib = File.basename(Dir.pwd).sub(/[-].*$/, '')
273
+ end
274
+ This.lib = lib
275
+
276
+ # grok version
277
+ #
278
+ version = ENV['VERSION']
279
+ unless version
280
+ require "./lib/#{ This.lib }"
281
+ This.name = lib.capitalize
282
+ This.object = eval(This.name)
283
+ version = This.object.send(:version)
284
+ end
285
+ This.version = version
286
+
287
+ # see if dependencies are export by the module
288
+ #
289
+ if This.object.respond_to?(:dependencies)
290
+ This.dependencies = This.object.dependencies
291
+ end
292
+
293
+ # we need to know the name of the lib an it's version
294
+ #
295
+ abort('no lib') unless This.lib
296
+ abort('no version') unless This.version
297
+
298
+ # discover full path to this ruby executable
299
+ #
300
+ c = Config::CONFIG
301
+ bindir = c["bindir"] || c['BINDIR']
302
+ ruby_install_name = c['ruby_install_name'] || c['RUBY_INSTALL_NAME'] || 'ruby'
303
+ ruby_ext = c['EXEEXT'] || ''
304
+ ruby = File.join(bindir, (ruby_install_name + ruby_ext))
305
+ This.ruby = ruby
306
+
307
+ # some utils
308
+ #
309
+ module Util
310
+ def indent(s, n = 2)
311
+ s = unindent(s)
312
+ ws = ' ' * n
313
+ s.gsub(%r/^/, ws)
314
+ end
315
+
316
+ def unindent(s)
317
+ indent = nil
318
+ s.each_line do |line|
319
+ next if line =~ %r/^\s*$/
320
+ indent = line[%r/^\s*/] and break
321
+ end
322
+ indent ? s.gsub(%r/^#{ indent }/, "") : s
323
+ end
324
+ extend self
325
+ end
326
+
327
+ # template support
328
+ #
329
+ class Template
330
+ def initialize(&block)
331
+ @block = block
332
+ @template = block.call.to_s
333
+ end
334
+ def expand(b=nil)
335
+ ERB.new(Util.unindent(@template)).result((b||@block).binding)
336
+ end
337
+ alias_method 'to_s', 'expand'
338
+ end
339
+ def Template(*args, &block) Template.new(*args, &block) end
340
+
341
+ # colored console output support
342
+ #
343
+ This.ansi = {
344
+ :clear => "\e[0m",
345
+ :reset => "\e[0m",
346
+ :erase_line => "\e[K",
347
+ :erase_char => "\e[P",
348
+ :bold => "\e[1m",
349
+ :dark => "\e[2m",
350
+ :underline => "\e[4m",
351
+ :underscore => "\e[4m",
352
+ :blink => "\e[5m",
353
+ :reverse => "\e[7m",
354
+ :concealed => "\e[8m",
355
+ :black => "\e[30m",
356
+ :red => "\e[31m",
357
+ :green => "\e[32m",
358
+ :yellow => "\e[33m",
359
+ :blue => "\e[34m",
360
+ :magenta => "\e[35m",
361
+ :cyan => "\e[36m",
362
+ :white => "\e[37m",
363
+ :on_black => "\e[40m",
364
+ :on_red => "\e[41m",
365
+ :on_green => "\e[42m",
366
+ :on_yellow => "\e[43m",
367
+ :on_blue => "\e[44m",
368
+ :on_magenta => "\e[45m",
369
+ :on_cyan => "\e[46m",
370
+ :on_white => "\e[47m"
371
+ }
372
+ def say(phrase, *args)
373
+ options = args.last.is_a?(Hash) ? args.pop : {}
374
+ options[:color] = args.shift.to_s.to_sym unless args.empty?
375
+ keys = options.keys
376
+ keys.each{|key| options[key.to_s.to_sym] = options.delete(key)}
377
+
378
+ color = options[:color]
379
+ bold = options.has_key?(:bold)
380
+
381
+ parts = [phrase]
382
+ parts.unshift(This.ansi[color]) if color
383
+ parts.unshift(This.ansi[:bold]) if bold
384
+ parts.push(This.ansi[:clear]) if parts.size > 1
385
+
386
+ method = options[:method] || :puts
387
+
388
+ Kernel.send(method, parts.join)
389
+ end
390
+
391
+ # always run out of the project dir
392
+ #
393
+ Dir.chdir(This.dir)
394
+ }
@@ -0,0 +1,40 @@
1
+ ## assassin.gemspec
2
+ #
3
+
4
+ Gem::Specification::new do |spec|
5
+ spec.name = "assassin"
6
+ spec.version = "0.4.2"
7
+ spec.platform = Gem::Platform::RUBY
8
+ spec.summary = "assassin"
9
+ spec.description = "no zombies ever, not even on `exit!` or `kill -9`"
10
+ spec.license = "same as ruby's"
11
+
12
+ spec.files =
13
+ ["LICENSE",
14
+ "README.md",
15
+ "Rakefile",
16
+ "assassin.gemspec",
17
+ "lib",
18
+ "lib/assassin.rb",
19
+ "test",
20
+ "test/assassin_test.rb",
21
+ "test/child.rb",
22
+ "test/lib",
23
+ "test/lib/testing.rb",
24
+ "test/parent.rb"]
25
+
26
+ spec.executables = []
27
+
28
+ spec.require_path = "lib"
29
+
30
+ spec.test_files = nil
31
+
32
+
33
+
34
+ spec.extensions.push(*[])
35
+
36
+ spec.rubyforge_project = "codeforpeople"
37
+ spec.author = "Ara T. Howard"
38
+ spec.email = "ara.t.howard@gmail.com"
39
+ spec.homepage = "https://github.com/ahoward/assassin"
40
+ end
@@ -0,0 +1,89 @@
1
+ require 'tmpdir'
2
+ require 'securerandom'
3
+
4
+ class Assassin
5
+ Version = '0.4.2' unless defined?(Version)
6
+
7
+ def Assassin.version
8
+ Version
9
+ end
10
+
11
+ def Assassin.description
12
+ 'no zombies ever, not even on `exit!` or `kill -9`'
13
+ end
14
+
15
+ def Assassin.at_exit_kill(*args, &block)
16
+ new(*args, &block)
17
+ end
18
+
19
+ def Assassin.ate(*args, &block)
20
+ new(*args, &block)
21
+ end
22
+
23
+ attr_accessor :parent_pid
24
+ attr_accessor :child_pid
25
+ attr_accessor :pid
26
+ attr_accessor :path
27
+
28
+ def initialize(child_pid, options = {})
29
+ @child_pid = child_pid.to_s.to_i
30
+ @parent_pid = Process.pid
31
+ @options = Assassin.options_for(options)
32
+ @pid, @path = Assassin.generate(@child_pid, @options)
33
+ end
34
+
35
+ def Assassin.options_for(options)
36
+ options.inject({}){|h, kv| k,v = kv; h.update(k.to_s.to_sym => v)}
37
+ end
38
+
39
+ def Assassin.generate(child_pid, options = {})
40
+ path = File.join(Dir.tmpdir, "assassin-#{ child_pid }-#{ SecureRandom.uuid }.rb")
41
+ script = Assassin.script_for(child_pid, options)
42
+ IO.binwrite(path, script)
43
+ pid = Process.spawn "ruby #{ path }"
44
+ [pid, path]
45
+ end
46
+
47
+ def Assassin.script_for(child_pid, options = {})
48
+ parent_pid = Process.pid
49
+
50
+ script = <<-__
51
+ Process.daemon
52
+
53
+ require 'fileutils'
54
+ at_exit{ FileUtils.rm_f(__FILE__) }
55
+
56
+ parent_pid = #{ parent_pid }
57
+ child_pid = #{ child_pid }
58
+
59
+ m = 24*60*60
60
+ n = 42
61
+
62
+ m.times do
63
+ begin
64
+ Process.kill(0, parent_pid)
65
+ rescue Object => e
66
+ if e.is_a?(Errno::ESRCH)
67
+ n.times do
68
+ begin
69
+ Process.kill(15, child_pid) rescue nil
70
+ sleep(rand + rand)
71
+ Process.kill(9, child_pid) rescue nil
72
+ sleep(rand + rand)
73
+ Process.kill(0, child_pid)
74
+ rescue Errno::ESRCH
75
+ break
76
+ end
77
+ end
78
+ end
79
+
80
+ exit
81
+ end
82
+
83
+ sleep(1)
84
+ end
85
+ __
86
+
87
+ return script
88
+ end
89
+ end
@@ -0,0 +1,100 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'testing'
3
+ require 'assassin'
4
+ require 'yaml'
5
+
6
+ Testing Assassin do
7
+ testing 'that not using assassin.rb leaks zombie processes' do
8
+ assert do
9
+ parent_pid, child_pid = `#{ TESTDIR }/parent.rb`.scan(/\d+/).map(&:to_i)
10
+
11
+ parent_was_killed = false
12
+ child_was_killed = false
13
+
14
+ 10.times do
15
+ begin
16
+ Process.kill(0, parent_pid)
17
+ sleep(rand)
18
+ rescue Errno::ESRCH
19
+ parent_was_killed = true
20
+ break
21
+ end
22
+ end
23
+
24
+ 10.times do
25
+ begin
26
+ Process.kill(0, child_pid)
27
+ sleep(rand)
28
+ rescue Errno::ESRCH
29
+ child_was_killed = true
30
+ break
31
+ end
32
+ end
33
+
34
+ cleanup!(parent_pid, child_pid)
35
+
36
+ parent_was_killed && !child_was_killed
37
+ end
38
+ end
39
+
40
+ testing 'that __using__ assassin.rb does __not__ leak zombie processes' do
41
+ assert do
42
+ parent_pid, child_pid = `#{ TESTDIR }/parent.rb assassin`.scan(/\d+/).map(&:to_i)
43
+
44
+ parent_was_killed = false
45
+ child_was_killed = false
46
+
47
+ 10.times do
48
+ begin
49
+ Process.kill(0, parent_pid)
50
+ sleep(rand)
51
+ rescue Errno::ESRCH
52
+ parent_was_killed = true
53
+ break
54
+ end
55
+ end
56
+
57
+ 10.times do
58
+ begin
59
+ Process.kill(0, child_pid)
60
+ sleep(rand)
61
+ rescue Errno::ESRCH
62
+ child_was_killed = true
63
+ break
64
+ end
65
+ end
66
+
67
+ cleanup!(parent_pid, child_pid)
68
+
69
+ parent_was_killed && child_was_killed
70
+ end
71
+ end
72
+
73
+ protected
74
+
75
+ def cleanup!(*pids)
76
+ pids.flatten.compact.each do |pid|
77
+ pid = pid.to_s.to_i
78
+ begin
79
+ Process.kill(9, pid) rescue nil
80
+ Process.kill(-9, pid) rescue nil
81
+ `kill -9 #{ pid } 2>&1`
82
+ rescue
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+
89
+
90
+
91
+
92
+
93
+ BEGIN {
94
+ TESTDIR = File.dirname(File.expand_path(__FILE__))
95
+ TESTLIBDIR = File.join(TESTDIR, 'lib')
96
+ ROOTDIR = File.dirname(TESTDIR)
97
+ LIBDIR = File.join(ROOTDIR, 'lib')
98
+ $LOAD_PATH.push(LIBDIR)
99
+ $LOAD_PATH.push(TESTLIBDIR)
100
+ }
@@ -0,0 +1,9 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ #
4
+ STDOUT.sync = true
5
+ STDERR.sync = true
6
+ puts Process.pid rescue nil
7
+
8
+ #
9
+ loop{ sleep }
@@ -0,0 +1,74 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # simple testing support
3
+ #
4
+ require 'test/unit'
5
+
6
+ def Testing(*args, &block)
7
+ Class.new(Test::Unit::TestCase) do
8
+ eval("This=self")
9
+
10
+ def self.slug_for(*args)
11
+ string = args.flatten.compact.join('-')
12
+ words = string.to_s.scan(%r/\w+/)
13
+ words.map!{|word| word.gsub %r/[^0-9a-zA-Z_-]/, ''}
14
+ words.delete_if{|word| word.nil? or word.strip.empty?}
15
+ words.join('-').downcase
16
+ end
17
+
18
+ def This.testing_subclass_count
19
+ @testing_subclass_count ||= 1
20
+ ensure
21
+ @testing_subclass_count += 1
22
+ end
23
+
24
+ slug = slug_for(*args).gsub(%r/-/,'_')
25
+ name = ['TESTING', '%03d' % This.testing_subclass_count, slug].delete_if{|part| part.empty?}.join('_')
26
+ name = name.upcase!
27
+ const_set(:Name, name)
28
+ def self.name() const_get(:Name) end
29
+
30
+ def self.testno()
31
+ '%05d' % (@testno ||= 0)
32
+ ensure
33
+ @testno += 1
34
+ end
35
+
36
+ def self.testing(*args, &block)
37
+ define_method("test_#{ testno }_#{ slug_for(*args) }", &block)
38
+ end
39
+
40
+ alias_method '__assert__', 'assert'
41
+
42
+ def assert(*args, &block)
43
+ if block
44
+ label = "assert(#{ args.join ' ' })"
45
+ result = nil
46
+ assert_nothing_raised{ result = block.call }
47
+ __assert__(result, label)
48
+ result
49
+ else
50
+ result = args.shift
51
+ label = "assert(#{ args.join ' ' })"
52
+ __assert__(result, label)
53
+ result
54
+ end
55
+ end
56
+
57
+ def subclass_of exception
58
+ class << exception
59
+ def ==(other) super or self > other end
60
+ end
61
+ exception
62
+ end
63
+
64
+ alias_method '__assert_raises__', 'assert_raises'
65
+
66
+ def assert_raises(*args, &block)
67
+ args.push(subclass_of(Exception)) if args.empty?
68
+ __assert_raises__(*args, &block)
69
+ end
70
+
71
+ module_eval(&block)
72
+ self
73
+ end
74
+ end
@@ -0,0 +1,25 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ #
4
+ STDOUT.sync = true
5
+ STDERR.sync = true
6
+ puts Process.pid
7
+
8
+ #
9
+ TESTDIR = File.dirname(__FILE__)
10
+ ROOTDIR = File.dirname(TESTDIR)
11
+ LIBDIR = File.join(ROOTDIR, 'lib')
12
+
13
+ #
14
+ child_program = "#{ TESTDIR }/child.rb"
15
+ child = IO.popen(child_program)
16
+ puts child.pid
17
+
18
+ #
19
+ if ARGV.first.to_s =~ /assassin/
20
+ require "#{ LIBDIR }/assassin.rb"
21
+ Assassin.at_exit_kill(child.pid)
22
+ end
23
+
24
+ #
25
+ exit!
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: assassin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ara T. Howard
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-10-17 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: no zombies ever, not even on `exit!` or `kill -9`
15
+ email: ara.t.howard@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - Rakefile
23
+ - assassin.gemspec
24
+ - lib/assassin.rb
25
+ - test/assassin_test.rb
26
+ - test/child.rb
27
+ - test/lib/testing.rb
28
+ - test/parent.rb
29
+ homepage: https://github.com/ahoward/assassin
30
+ licenses:
31
+ - same as ruby's
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project: codeforpeople
50
+ rubygems_version: 1.8.23.2
51
+ signing_key:
52
+ specification_version: 3
53
+ summary: assassin
54
+ test_files: []
55
+ has_rdoc: