mutineer-mongoid 2.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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1194 -0
  3. data/LICENSE +21 -0
  4. data/README.md +659 -0
  5. data/bin/mutineer +6 -0
  6. data/lib/mutineer/baseline.rb +185 -0
  7. data/lib/mutineer/changed_lines.rb +136 -0
  8. data/lib/mutineer/child_stdout.rb +35 -0
  9. data/lib/mutineer/cli.rb +681 -0
  10. data/lib/mutineer/config.rb +372 -0
  11. data/lib/mutineer/coverage_map.rb +1366 -0
  12. data/lib/mutineer/daemon_backend.rb +351 -0
  13. data/lib/mutineer/daemon_client.rb +407 -0
  14. data/lib/mutineer/daemon_server.rb +351 -0
  15. data/lib/mutineer/external_backend.rb +288 -0
  16. data/lib/mutineer/file_swap.rb +194 -0
  17. data/lib/mutineer/isolation.rb +321 -0
  18. data/lib/mutineer/job_plan.rb +352 -0
  19. data/lib/mutineer/kill_channel.rb +284 -0
  20. data/lib/mutineer/kill_matrix.rb +88 -0
  21. data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
  22. data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
  23. data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +145 -0
  24. data/lib/mutineer/minitest_integration.rb +137 -0
  25. data/lib/mutineer/mongoid_failures.rb +82 -0
  26. data/lib/mutineer/mongoid_worker_db.rb +97 -0
  27. data/lib/mutineer/mutant_id.rb +126 -0
  28. data/lib/mutineer/mutation.rb +27 -0
  29. data/lib/mutineer/mutator_registry.rb +109 -0
  30. data/lib/mutineer/mutators/arithmetic.rb +35 -0
  31. data/lib/mutineer/mutators/array_literal.rb +44 -0
  32. data/lib/mutineer/mutators/base.rb +105 -0
  33. data/lib/mutineer/mutators/boolean_connector.rb +52 -0
  34. data/lib/mutineer/mutators/boolean_literal.rb +56 -0
  35. data/lib/mutineer/mutators/chain_link.rb +132 -0
  36. data/lib/mutineer/mutators/collection_method.rb +44 -0
  37. data/lib/mutineer/mutators/comparison.rb +35 -0
  38. data/lib/mutineer/mutators/condition_forcing.rb +134 -0
  39. data/lib/mutineer/mutators/condition_negation.rb +49 -0
  40. data/lib/mutineer/mutators/literal_mutation.rb +51 -0
  41. data/lib/mutineer/mutators/negation_removal.rb +44 -0
  42. data/lib/mutineer/mutators/operand_removal.rb +58 -0
  43. data/lib/mutineer/mutators/operator_assignment.rb +113 -0
  44. data/lib/mutineer/mutators/range_literal.rb +54 -0
  45. data/lib/mutineer/mutators/regex_literal.rb +72 -0
  46. data/lib/mutineer/mutators/return_nil.rb +72 -0
  47. data/lib/mutineer/mutators/safe_navigation.rb +76 -0
  48. data/lib/mutineer/mutators/statement_removal.rb +33 -0
  49. data/lib/mutineer/mutators/string_literal.rb +34 -0
  50. data/lib/mutineer/orphan_guard.rb +28 -0
  51. data/lib/mutineer/pairing.rb +298 -0
  52. data/lib/mutineer/parser.rb +45 -0
  53. data/lib/mutineer/progress.rb +46 -0
  54. data/lib/mutineer/project.rb +506 -0
  55. data/lib/mutineer/project_path.rb +57 -0
  56. data/lib/mutineer/rails_worker_db.rb +212 -0
  57. data/lib/mutineer/reporter.rb +926 -0
  58. data/lib/mutineer/result.rb +240 -0
  59. data/lib/mutineer/runner.rb +418 -0
  60. data/lib/mutineer/statement_lines.rb +183 -0
  61. data/lib/mutineer/subject.rb +38 -0
  62. data/lib/mutineer/test_runners/minitest.rb +22 -0
  63. data/lib/mutineer/test_runners/rspec.rb +330 -0
  64. data/lib/mutineer/test_runners.rb +30 -0
  65. data/lib/mutineer/version.rb +6 -0
  66. data/lib/mutineer/worker_pool.rb +147 -0
  67. data/lib/mutineer.rb +48 -0
  68. metadata +143 -0
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Negation-removal mutator (Tier-2).
8
+ #
9
+ # Removes the `!` or `not` of a negation, one mutation per occurrence:
10
+ # `!x` becomes `x` and `not x` becomes ` x`. The mutant survives when no
11
+ # test depends on the negated value.
12
+ class NegationRemoval < Base
13
+ # Visits call nodes and emits negation-removal mutations.
14
+ #
15
+ # @param node [Prism::CallNode] call node to inspect.
16
+ # @return [void]
17
+ def visit_call_node(node)
18
+ emit(node)
19
+ super # nested negations (!!x) each get their own mutation
20
+ end
21
+
22
+ private
23
+
24
+ # Emits a mutation when the node is a prefix `!` or `not`.
25
+ #
26
+ # The explicit form `x.!` is skipped: without its message, `x.` does
27
+ # not parse.
28
+ #
29
+ # @param node [Prism::CallNode] call node to inspect.
30
+ # @return [void]
31
+ def emit(node)
32
+ return unless node.name == :! && node.call_operator_loc.nil?
33
+
34
+ loc = node.message_loc
35
+ @mutations << Mutation.new(
36
+ start_offset: loc.start_offset,
37
+ end_offset: loc.end_offset,
38
+ replacement: "",
39
+ operator: :negation_removal
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Operand-removal mutator (Tier-2).
8
+ #
9
+ # Replaces a boolean expression with one of its operands, two mutations
10
+ # per `&&`, `||`, `and` or `or`: `a && b` becomes `(a)` and `(b)`. The
11
+ # parentheses keep the precedence of `and` and `or`. A surviving mutant
12
+ # shows that no test needs the operand that was removed.
13
+ class OperandRemoval < Base
14
+ # Node types for jumps. A jump in a value context does not parse, so
15
+ # the operator never keeps a jump alone.
16
+ JUMPS = [Prism::ReturnNode, Prism::BreakNode, Prism::NextNode, Prism::RedoNode, Prism::RetryNode].freeze
17
+
18
+ # Visits `and` nodes.
19
+ #
20
+ # @param node [Prism::AndNode] node to inspect.
21
+ # @return [void]
22
+ def visit_and_node(node)
23
+ emit(node)
24
+ super # nested connectors (a && b && c) each get their own mutations
25
+ end
26
+
27
+ # Visits `or` nodes.
28
+ #
29
+ # @param node [Prism::OrNode] node to inspect.
30
+ # @return [void]
31
+ def visit_or_node(node)
32
+ emit(node)
33
+ super
34
+ end
35
+
36
+ private
37
+
38
+ # Emits one mutation that keeps the left operand, then one that keeps
39
+ # the right operand. The order is fixed, because the two mutants share
40
+ # a token and their occurrence ordinals tell them apart.
41
+ #
42
+ # @param node [Prism::AndNode, Prism::OrNode] connector node.
43
+ # @return [void]
44
+ def emit(node)
45
+ [[node.left, node.right], [node.right, node.left]].each do |kept, removed|
46
+ next if JUMPS.any? { |type| kept.is_a?(type) } || heredoc?(removed)
47
+
48
+ @mutations << Mutation.new(
49
+ start_offset: node.location.start_offset,
50
+ end_offset: node.location.end_offset,
51
+ replacement: "(#{kept.slice})",
52
+ operator: :operand_removal
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Operator-assignment mutator (Tier-2).
8
+ #
9
+ # Swaps the operator of a compound assignment, one mutation per
10
+ # occurrence: `t += x` -> `t -= x`. Prism parses `t += x` as its own
11
+ # node, not as a CallNode, so the arithmetic mutator never sees it.
12
+ #
13
+ # The mapping is the same as for arithmetic. Other compound operators
14
+ # (`||=`, `&&=`, `|=`, `<<=` and the rest) are not changed.
15
+ class OperatorAssignment < Base
16
+ # Token replacements for compound arithmetic operators.
17
+ REPLACEMENTS = {
18
+ :+ => "-=", :- => "+=", :* => "/=", :/ => "*=", :% => "*=", :** => "*="
19
+ }.freeze
20
+
21
+ # Visits `t += x`.
22
+ #
23
+ # @param node [Prism::LocalVariableOperatorWriteNode] node to inspect.
24
+ # @return [void]
25
+ def visit_local_variable_operator_write_node(node)
26
+ emit(node)
27
+ super # the value can hold its own compound assignment
28
+ end
29
+
30
+ # Visits `@t += x`.
31
+ #
32
+ # @param node [Prism::InstanceVariableOperatorWriteNode] node to inspect.
33
+ # @return [void]
34
+ def visit_instance_variable_operator_write_node(node)
35
+ emit(node)
36
+ super
37
+ end
38
+
39
+ # Visits `@@t += x`.
40
+ #
41
+ # @param node [Prism::ClassVariableOperatorWriteNode] node to inspect.
42
+ # @return [void]
43
+ def visit_class_variable_operator_write_node(node)
44
+ emit(node)
45
+ super
46
+ end
47
+
48
+ # Visits `$t += x`.
49
+ #
50
+ # @param node [Prism::GlobalVariableOperatorWriteNode] node to inspect.
51
+ # @return [void]
52
+ def visit_global_variable_operator_write_node(node)
53
+ emit(node)
54
+ super
55
+ end
56
+
57
+ # Visits `T += x`.
58
+ #
59
+ # @param node [Prism::ConstantOperatorWriteNode] node to inspect.
60
+ # @return [void]
61
+ def visit_constant_operator_write_node(node)
62
+ emit(node)
63
+ super
64
+ end
65
+
66
+ # Visits `A::T += x` and `::T += x`.
67
+ #
68
+ # @param node [Prism::ConstantPathOperatorWriteNode] node to inspect.
69
+ # @return [void]
70
+ def visit_constant_path_operator_write_node(node)
71
+ emit(node)
72
+ super
73
+ end
74
+
75
+ # Visits `a.b += x` and `a&.b += x`.
76
+ #
77
+ # @param node [Prism::CallOperatorWriteNode] node to inspect.
78
+ # @return [void]
79
+ def visit_call_operator_write_node(node)
80
+ emit(node)
81
+ super
82
+ end
83
+
84
+ # Visits `a[i] += x`.
85
+ #
86
+ # @param node [Prism::IndexOperatorWriteNode] node to inspect.
87
+ # @return [void]
88
+ def visit_index_operator_write_node(node)
89
+ emit(node)
90
+ super
91
+ end
92
+
93
+ private
94
+
95
+ # Emits a mutation when the node's operator has a replacement.
96
+ #
97
+ # @param node [Prism::Node] a node with `binary_operator` and `binary_operator_loc`.
98
+ # @return [void]
99
+ def emit(node)
100
+ replacement = REPLACEMENTS[node.binary_operator]
101
+ return unless replacement
102
+
103
+ loc = node.binary_operator_loc
104
+ @mutations << Mutation.new(
105
+ start_offset: loc.start_offset,
106
+ end_offset: loc.end_offset,
107
+ replacement: replacement,
108
+ operator: :operator_assignment
109
+ )
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Range mutator (Tier-2).
8
+ #
9
+ # Swaps `..` with `...` and `...` with `..`, one mutation per range. The
10
+ # `..` -> `...` mutant survives when no test checks the last element.
11
+ # Flip-flops are FlipFlopNode, not RangeNode, so this mutator skips them.
12
+ #
13
+ # Endless ranges (`1..`, `1..nil`) are skipped. `(1..)` and `(1...)` give
14
+ # the same result for slicing, `include?`, `===`, `size` and patterns, so
15
+ # the mutant is equivalent and no normal test can kill it.
16
+ class RangeLiteral < Base
17
+ # Maps each range operator to its opposite.
18
+ SWAPS = { ".." => "...", "..." => ".." }.freeze
19
+
20
+ # Visits range nodes and emits range mutations.
21
+ #
22
+ # @param node [Prism::RangeNode] range node to inspect.
23
+ # @return [void]
24
+ def visit_range_node(node)
25
+ emit(node) unless endless?(node)
26
+ super # nested ranges ((1..2)...(3..4)) each get their own mutation
27
+ end
28
+
29
+ private
30
+
31
+ # Whether the range has no end: `1..` or `1..nil`.
32
+ #
33
+ # @param node [Prism::RangeNode] range node to inspect.
34
+ # @return [Boolean]
35
+ def endless?(node)
36
+ node.right.nil? || node.right.is_a?(Prism::NilNode)
37
+ end
38
+
39
+ # Emits the mutation that swaps the range operator.
40
+ #
41
+ # @param node [Prism::RangeNode] range node to mutate.
42
+ # @return [void]
43
+ def emit(node)
44
+ loc = node.operator_loc
45
+ @mutations << Mutation.new(
46
+ start_offset: loc.start_offset,
47
+ end_offset: loc.end_offset,
48
+ replacement: SWAPS.fetch(loc.slice),
49
+ operator: :range
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Regular-expression literal mutator (Tier-2).
8
+ #
9
+ # Conservative, clearly-safe textual swaps inside a plain regex literal:
10
+ # drop a leading +^+ anchor, drop a trailing +$+ anchor, and swap the +
11
+ # and * quantifiers. Interpolated regexes use a different node type and are
12
+ # never visited. Escaped tokens (preceded by a backslash) are left alone.
13
+ class RegexLiteral < Base
14
+ # Visits regular-expression literals.
15
+ #
16
+ # @param node [Prism::RegularExpressionNode] node to inspect.
17
+ # @return [void]
18
+ def visit_regular_expression_node(node)
19
+ loc = node.content_loc
20
+ content = @source.byteslice(loc.start_offset...loc.end_offset)
21
+ scan(content, loc.start_offset)
22
+ super
23
+ end
24
+
25
+ private
26
+
27
+ # Scans the pattern bytes and emits anchor/quantifier mutations.
28
+ #
29
+ # @param content [String] regex pattern between the slashes.
30
+ # @param base [Integer] byte offset of the pattern start in source.
31
+ # @return [void]
32
+ def scan(content, base)
33
+ emit(base, base + 1, "") if content.start_with?("^")
34
+ emit(base + content.bytesize - 1, base + content.bytesize, "") if trailing_anchor?(content)
35
+
36
+ escaped = false
37
+ offset = base
38
+ content.each_char do |ch|
39
+ unless escaped
40
+ emit(offset, offset + 1, "*") if ch == "+"
41
+ emit(offset, offset + 1, "+") if ch == "*"
42
+ end
43
+ escaped = ch == "\\" && !escaped
44
+ offset += ch.bytesize
45
+ end
46
+ end
47
+
48
+ # Whether the pattern ends with an unescaped +$+ anchor.
49
+ #
50
+ # @param content [String] regex pattern.
51
+ # @return [Boolean] true when a trailing anchor is droppable.
52
+ def trailing_anchor?(content)
53
+ content.end_with?("$") && !content.end_with?("\\$")
54
+ end
55
+
56
+ # Emits a regex mutation.
57
+ #
58
+ # @param start_offset [Integer] byte start.
59
+ # @param end_offset [Integer] byte end.
60
+ # @param replacement [String] replacement text.
61
+ # @return [void]
62
+ def emit(start_offset, end_offset, replacement)
63
+ @mutations << Mutation.new(
64
+ start_offset: start_offset,
65
+ end_offset: end_offset,
66
+ replacement: replacement,
67
+ operator: :regex
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Return-nil mutator.
8
+ #
9
+ # Replaces explicit return values and final expressions with nil.
10
+ class ReturnNil < Base
11
+ # Collects return-nil mutations for a subject.
12
+ #
13
+ # @param subject [Mutineer::Subject] subject to inspect.
14
+ # @param source [String] full source text.
15
+ # @return [Array<Mutineer::Mutation>] collected mutations.
16
+ def mutations_for(subject, source)
17
+ @source = source
18
+ @mutations = []
19
+ @body = subject.def_node.body
20
+ if @body
21
+ @body.accept(self) # rule 1 (explicit return nodes in this body)
22
+ final_expression_nil(@body) # rule 2 (method's final expression)
23
+ end
24
+ drop_dangling_heredocs
25
+ @mutations
26
+ end
27
+
28
+ # Visits return nodes.
29
+ #
30
+ # @param node [Prism::ReturnNode] node to inspect.
31
+ # @return [void]
32
+ def visit_return_node(node)
33
+ args = node.arguments
34
+ if args
35
+ values = args.arguments
36
+ unless values.size == 1 && values.first.is_a?(Prism::NilNode)
37
+ emit(args.location)
38
+ end
39
+ end
40
+ super
41
+ end
42
+
43
+ private
44
+
45
+ # Mutates a method's final expression to nil when eligible.
46
+ #
47
+ # @param body [Prism::Node] method body node.
48
+ # @return [void]
49
+ def final_expression_nil(body)
50
+ return unless body.is_a?(Prism::StatementsNode)
51
+
52
+ last = body.body.last
53
+ return if last.nil? || last.is_a?(Prism::ReturnNode) || last.is_a?(Prism::NilNode)
54
+
55
+ emit(last.location)
56
+ end
57
+
58
+ # Emits a return-nil mutation.
59
+ #
60
+ # @param loc [Prism::Location] source location.
61
+ # @return [void]
62
+ def emit(loc)
63
+ @mutations << Mutation.new(
64
+ start_offset: loc.start_offset,
65
+ end_offset: loc.end_offset,
66
+ replacement: "nil",
67
+ operator: :return_nil
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Safe-navigation mutator (Tier-2).
8
+ #
9
+ # Replaces `&.` with `.`, one mutation per occurrence. The mutant raises
10
+ # NoMethodError on a nil receiver, so it survives when no test passes nil.
11
+ class SafeNavigation < Base
12
+ # Visits call nodes and emits safe-navigation mutations.
13
+ #
14
+ # @param node [Prism::CallNode] call node to inspect.
15
+ # @return [void]
16
+ def visit_call_node(node)
17
+ emit(node)
18
+ super # chained calls (a&.b&.c) each get their own mutation
19
+ end
20
+
21
+ # Visits `a&.b += 1`, which Prism parses as its own node, not a CallNode.
22
+ #
23
+ # @param node [Prism::CallOperatorWriteNode] node to inspect.
24
+ # @return [void]
25
+ def visit_call_operator_write_node(node)
26
+ emit(node)
27
+ super
28
+ end
29
+
30
+ # Visits `a&.b ||= 1`.
31
+ #
32
+ # @param node [Prism::CallOrWriteNode] node to inspect.
33
+ # @return [void]
34
+ def visit_call_or_write_node(node)
35
+ emit(node)
36
+ super
37
+ end
38
+
39
+ # Visits `a&.b &&= 1`.
40
+ #
41
+ # @param node [Prism::CallAndWriteNode] node to inspect.
42
+ # @return [void]
43
+ def visit_call_and_write_node(node)
44
+ emit(node)
45
+ super
46
+ end
47
+
48
+ # Visits a call used as an assignment target, as in `for a&.b in list`.
49
+ #
50
+ # @param node [Prism::CallTargetNode] node to inspect.
51
+ # @return [void]
52
+ def visit_call_target_node(node)
53
+ emit(node)
54
+ super
55
+ end
56
+
57
+ private
58
+
59
+ # Emits a mutation when the node's call operator is `&.`.
60
+ #
61
+ # @param node [Prism::Node] a node with `safe_navigation?` and `call_operator_loc`.
62
+ # @return [void]
63
+ def emit(node)
64
+ return unless node.safe_navigation?
65
+
66
+ loc = node.call_operator_loc
67
+ @mutations << Mutation.new(
68
+ start_offset: loc.start_offset,
69
+ end_offset: loc.end_offset,
70
+ replacement: ".",
71
+ operator: :safe_navigation
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # Statement-removal mutator.
8
+ #
9
+ # Replaces each non-final method statement with `nil`.
10
+ class StatementRemoval < Base
11
+ # Visits statement nodes and emits removals.
12
+ #
13
+ # @param node [Prism::StatementsNode] statement list to inspect.
14
+ # @return [void]
15
+ def visit_statements_node(node)
16
+ stmts = node.body
17
+ if stmts.length >= 2
18
+ stmts[0...-1].each do |stmt|
19
+ loc = stmt.location
20
+ @mutations << Mutation.new(
21
+ start_offset: loc.start_offset,
22
+ end_offset: loc.end_offset,
23
+ replacement: "nil",
24
+ operator: :statement_removal
25
+ )
26
+ end
27
+ end
28
+ # Nested statement lists (if/begin/while bodies) are separate StatementsNodes.
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Mutineer
6
+ module Mutators
7
+ # String literal mutator (Tier-2).
8
+ #
9
+ # Empties a non-empty string literal and fills an empty one. Only plain
10
+ # quoted strings are touched; interpolated strings, heredocs and %-literals
11
+ # are skipped for safety (they re-parse unpredictably).
12
+ class StringLiteral < Base
13
+ # Visits string literals.
14
+ #
15
+ # @param node [Prism::StringNode] node to inspect.
16
+ # @return [void]
17
+ def visit_string_node(node)
18
+ # Only plain "..." / '...' quotes. opening is nil for
19
+ # interpolation parts and %w[] elements; heredocs/%-literals use a
20
+ # different opening token. Skipping them keeps mutants re-parseable.
21
+ if %w[" '].include?(node.opening)
22
+ loc = node.content_loc
23
+ @mutations << Mutation.new(
24
+ start_offset: loc.start_offset,
25
+ end_offset: loc.end_offset,
26
+ replacement: node.unescaped.empty? ? "mutineer" : "",
27
+ operator: :string_literal
28
+ )
29
+ end
30
+ super
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutineer
4
+ # Ends a forked child's process group once the process that forked it is
5
+ # gone (#101). A mutant or capture child leads its own process group, so a
6
+ # parent that dies, or is killed by a client that gave up on it, does not
7
+ # take the child's tests with it; without this, a hung test would run on and
8
+ # keep its worker database open. Needs no other part of Mutineer, so the
9
+ # app-side daemon can load it.
10
+ module OrphanGuard
11
+ # Seconds between checks. A forked child gets no signal on parent death.
12
+ POLL = 0.5
13
+
14
+ # Starts the watchdog in the current (child) process.
15
+ #
16
+ # @param parent [Integer] the parent's pid, read in the parent before
17
+ # `fork`, so a parent that dies before this call is still noticed.
18
+ # @return [Thread]
19
+ def self.start(parent)
20
+ Thread.new do
21
+ sleep(POLL) while Process.ppid == parent
22
+ # Group 0 only when this child leads its own group: a failed setpgid
23
+ # leaves it in the parent's group, which must not be killed.
24
+ Process.kill(:KILL, Process.getpgrp == Process.pid ? 0 : Process.pid)
25
+ end
26
+ end
27
+ end
28
+ end