rspec-hermetic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +166 -0
- data/lib/rspec/hermetic/allowlist.rb +75 -0
- data/lib/rspec/hermetic/candidate_report.rb +47 -0
- data/lib/rspec/hermetic/change.rb +61 -0
- data/lib/rspec/hermetic/configuration.rb +93 -0
- data/lib/rspec/hermetic/corpus_evaluation.rb +124 -0
- data/lib/rspec/hermetic/diff.rb +63 -0
- data/lib/rspec/hermetic/evaluation.rb +184 -0
- data/lib/rspec/hermetic/evaluation_task.rb +53 -0
- data/lib/rspec/hermetic/forensic.rb +22 -0
- data/lib/rspec/hermetic/formatter.rb +93 -0
- data/lib/rspec/hermetic/minitest.rb +80 -0
- data/lib/rspec/hermetic/probe/base.rb +17 -0
- data/lib/rspec/hermetic/probe/constants.rb +87 -0
- data/lib/rspec/hermetic/probe/env.rb +15 -0
- data/lib/rspec/hermetic/probe/filesystem.rb +109 -0
- data/lib/rspec/hermetic/probe/globals.rb +31 -0
- data/lib/rspec/hermetic/probe/rails.rb +146 -0
- data/lib/rspec/hermetic/probe/randomness.rb +78 -0
- data/lib/rspec/hermetic/probe/resources.rb +110 -0
- data/lib/rspec/hermetic/probe/ruby_runtime.rb +37 -0
- data/lib/rspec/hermetic/probe/time.rb +54 -0
- data/lib/rspec/hermetic/probe.rb +38 -0
- data/lib/rspec/hermetic/resource_tracker.rb +111 -0
- data/lib/rspec/hermetic/restorer.rb +330 -0
- data/lib/rspec/hermetic/runner.rb +183 -0
- data/lib/rspec/hermetic/snapshot.rb +37 -0
- data/lib/rspec/hermetic/stable_value.rb +140 -0
- data/lib/rspec/hermetic/verdict.rb +39 -0
- data/lib/rspec/hermetic/verify_task.rb +65 -0
- data/lib/rspec/hermetic/version.rb +11 -0
- data/lib/rspec/hermetic.rb +59 -0
- data/sig/rspec/hermetic.rbs +112 -0
- metadata +117 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Hermetic
|
|
5
|
+
class Verdict
|
|
6
|
+
Result = Struct.new(:pollutions, :warnings, :ignored, keyword_init: true) do
|
|
7
|
+
def polluted?
|
|
8
|
+
pollutions.any?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def reportable?
|
|
12
|
+
polluted? || warnings.any?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(configuration)
|
|
17
|
+
@configuration = configuration
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(change_set, example_allowances: [])
|
|
21
|
+
pollutions = []
|
|
22
|
+
warnings = []
|
|
23
|
+
ignored = []
|
|
24
|
+
|
|
25
|
+
change_set.changes.each do |change|
|
|
26
|
+
if @configuration.allowlist.allowed?(change, example_allowances)
|
|
27
|
+
ignored << change
|
|
28
|
+
elsif @configuration.allowlist.append_only?(change) && change.append_only?
|
|
29
|
+
warnings << change
|
|
30
|
+
else
|
|
31
|
+
pollutions << change
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Result.new(pollutions: pollutions, warnings: warnings, ignored: ignored)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rake"
|
|
5
|
+
|
|
6
|
+
require_relative "../hermetic"
|
|
7
|
+
require_relative "candidate_report"
|
|
8
|
+
|
|
9
|
+
module RSpec
|
|
10
|
+
module Hermetic
|
|
11
|
+
class VerifyTask
|
|
12
|
+
extend ::Rake::DSL
|
|
13
|
+
|
|
14
|
+
DEFAULT_CANDIDATES = "tmp/rspec_hermetic_candidates.json"
|
|
15
|
+
|
|
16
|
+
def self.install(name = "hermetic:verify")
|
|
17
|
+
task_name = name.to_s
|
|
18
|
+
namespace_name, short_name = task_name.split(":", 2)
|
|
19
|
+
|
|
20
|
+
namespace namespace_name do
|
|
21
|
+
desc "Verify rspec-hermetic polluter/victim candidates by running victim alone and polluter before victim"
|
|
22
|
+
task short_name, [:candidates] do |_task, args|
|
|
23
|
+
new(args[:candidates] || ENV["HERMETIC_CANDIDATES"] || DEFAULT_CANDIDATES).run
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(candidates_path, output: $stdout)
|
|
29
|
+
@candidates_path = candidates_path
|
|
30
|
+
@output = output
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
candidates.each do |candidate|
|
|
35
|
+
polluter = candidate.fetch("polluter")
|
|
36
|
+
victim = candidate.fetch("victim")
|
|
37
|
+
verify_pair(polluter, victim)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def candidates
|
|
44
|
+
raise Errno::ENOENT, @candidates_path unless File.exist?(@candidates_path)
|
|
45
|
+
|
|
46
|
+
CandidateReport.new(@candidates_path).read
|
|
47
|
+
rescue Errno::ENOENT
|
|
48
|
+
raise Error, "candidate file not found: #{@candidates_path}"
|
|
49
|
+
rescue JSON::ParserError => error
|
|
50
|
+
raise Error, "invalid candidate JSON #{@candidates_path}: #{error.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def verify_pair(polluter, victim)
|
|
54
|
+
victim_status = run_rspec([victim])
|
|
55
|
+
pair_status = run_rspec([polluter, victim])
|
|
56
|
+
manifest = victim_status && !pair_status
|
|
57
|
+
@output.puts "#{manifest ? "MANIFEST" : "unconfirmed"} #{polluter} -> #{victim}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def run_rspec(locations)
|
|
61
|
+
system("bundle", "exec", "rspec", "--order", "defined", *locations)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hermetic/version"
|
|
4
|
+
require_relative "hermetic/configuration"
|
|
5
|
+
require_relative "hermetic/runner"
|
|
6
|
+
|
|
7
|
+
module RSpec
|
|
8
|
+
module Hermetic
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
attr_reader :runner
|
|
13
|
+
|
|
14
|
+
def configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure(rspec_config = nil)
|
|
19
|
+
yield configuration if block_given?
|
|
20
|
+
install!(rspec_config) if rspec_config
|
|
21
|
+
configuration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def install!(rspec_config)
|
|
25
|
+
@installed_config_ids ||= {}
|
|
26
|
+
return if @installed_config_ids[rspec_config.object_id]
|
|
27
|
+
|
|
28
|
+
@runner = Runner.new(configuration)
|
|
29
|
+
hook = proc { |example| ::RSpec::Hermetic.runner.call(example, self) }
|
|
30
|
+
|
|
31
|
+
if rspec_config.respond_to?(:hooks) && rspec_config.hooks.respond_to?(:register)
|
|
32
|
+
install_outer_around_hook(rspec_config, hook)
|
|
33
|
+
elsif rspec_config.respond_to?(:prepend_around)
|
|
34
|
+
rspec_config.prepend_around(:each, &hook)
|
|
35
|
+
else
|
|
36
|
+
rspec_config.around(:each, &hook)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@installed_config_ids[rspec_config.object_id] = true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def install_outer_around_hook(rspec_config, hook)
|
|
45
|
+
hook.__id__
|
|
46
|
+
if rspec_config.respond_to?(:add_hook_to_existing_matching_groups, true)
|
|
47
|
+
rspec_config.send(:add_hook_to_existing_matching_groups, [], :each) do |group|
|
|
48
|
+
group.hooks.register(:append, :around, :each, &hook)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
rspec_config.hooks.register(:append, :around, :each, &hook)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module Rspec
|
|
58
|
+
Hermetic = ::RSpec::Hermetic unless const_defined?(:Hermetic, false)
|
|
59
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module RSpec
|
|
2
|
+
module Hermetic
|
|
3
|
+
VERSION: String
|
|
4
|
+
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.configuration: () -> Configuration
|
|
9
|
+
def self.configure: (?untyped rspec_config) { (Configuration) -> untyped } -> Configuration
|
|
10
|
+
def self.install!: (untyped rspec_config) -> untyped
|
|
11
|
+
def self.configure_minitest: () { (Configuration) -> untyped } -> Configuration
|
|
12
|
+
|
|
13
|
+
class Configuration
|
|
14
|
+
attr_accessor probes: Array[Symbol]
|
|
15
|
+
attr_accessor on_pollution: Symbol
|
|
16
|
+
attr_accessor root_path: String
|
|
17
|
+
attr_accessor constant_namespaces: Array[untyped]
|
|
18
|
+
attr_accessor constant_exclude_patterns: Array[untyped]
|
|
19
|
+
attr_accessor constants_max_depth: Integer
|
|
20
|
+
attr_accessor constants_max_entries: Integer
|
|
21
|
+
attr_accessor constants_graph: bool
|
|
22
|
+
attr_accessor constants_graph_max_depth: Integer
|
|
23
|
+
attr_accessor constants_graph_max_nodes: Integer
|
|
24
|
+
attr_accessor filesystem_paths: Array[String]
|
|
25
|
+
attr_accessor filesystem_exclude_patterns: Array[String]
|
|
26
|
+
attr_accessor filesystem_content_hash_bytes: Integer
|
|
27
|
+
attr_accessor filesystem_max_entries: Integer
|
|
28
|
+
attr_accessor filesystem_max_depth: Integer?
|
|
29
|
+
attr_accessor forensic: bool
|
|
30
|
+
attr_accessor randomness_seed_probe: bool
|
|
31
|
+
attr_accessor resource_process_probe: bool
|
|
32
|
+
attr_accessor rails_config_paths: Array[String]
|
|
33
|
+
attr_accessor probe_sampling: Hash[Symbol, Integer]
|
|
34
|
+
attr_accessor report_probe_errors: bool
|
|
35
|
+
attr_accessor candidate_report_path: String?
|
|
36
|
+
attr_accessor auto_reset: untyped
|
|
37
|
+
attr_accessor track_resource_origins: bool
|
|
38
|
+
|
|
39
|
+
attr_reader allowlist: Allowlist
|
|
40
|
+
|
|
41
|
+
def initialize: () -> void
|
|
42
|
+
def allow: () { (Allowlist) -> untyped } -> untyped
|
|
43
|
+
def probe_interval: (Symbol) -> Integer
|
|
44
|
+
def auto_reset_probe?: (Symbol) -> bool
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Allowlist
|
|
48
|
+
def env: (*untyped patterns) -> untyped
|
|
49
|
+
def path: (*untyped patterns) -> untyped
|
|
50
|
+
def constant: (*untyped patterns) -> untyped
|
|
51
|
+
def append_only: (*untyped patterns) -> untyped
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class CandidateReport
|
|
55
|
+
def initialize: (String path) -> void
|
|
56
|
+
def append: (Hash[String, untyped] candidate) -> untyped
|
|
57
|
+
def read: () -> Array[Hash[String, untyped]]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class VerifyTask
|
|
61
|
+
DEFAULT_CANDIDATES: String
|
|
62
|
+
|
|
63
|
+
def self.install: (?String name) -> untyped
|
|
64
|
+
def initialize: (String candidates_path, ?output: untyped) -> void
|
|
65
|
+
def run: () -> untyped
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class Evaluation
|
|
69
|
+
def initialize: (?output_path: String) -> void
|
|
70
|
+
def run: () -> Hash[String, untyped]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class EvaluationTask
|
|
74
|
+
DEFAULT_OUTPUT: String
|
|
75
|
+
|
|
76
|
+
def self.install: (?String name) -> untyped
|
|
77
|
+
def self.install_corpus: (?String name) -> untyped
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class CorpusEvaluation
|
|
81
|
+
def initialize: (
|
|
82
|
+
output_path: String,
|
|
83
|
+
?project_path: String,
|
|
84
|
+
?baseline_command: String?,
|
|
85
|
+
hermetic_command: String,
|
|
86
|
+
?candidate_report_path: String,
|
|
87
|
+
?judgments_path: String?
|
|
88
|
+
) -> void
|
|
89
|
+
|
|
90
|
+
def run: () -> Hash[String, untyped]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class Restorer
|
|
94
|
+
def initialize: (Configuration configuration) -> void
|
|
95
|
+
def restore: (untyped change_set, untyped before_snapshot, ?context: untyped) -> untyped
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
module ResourceTracker
|
|
99
|
+
ORIGIN_IVAR: Symbol
|
|
100
|
+
|
|
101
|
+
def self.install!: () -> untyped
|
|
102
|
+
def self.start!: () -> untyped
|
|
103
|
+
def self.stop!: () -> untyped
|
|
104
|
+
def self.annotate: (untyped resource) -> untyped
|
|
105
|
+
def self.origin_for: (untyped resource) -> untyped
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
module Rspec
|
|
111
|
+
Hermetic: singleton(RSpec::Hermetic)
|
|
112
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rspec-hermetic
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec-core
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.10'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '3.10'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: minitest
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '5'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '7'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '5'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '7'
|
|
52
|
+
description: rspec-hermetic snapshots process state around each example and reports
|
|
53
|
+
leaked global state or resources.
|
|
54
|
+
email:
|
|
55
|
+
- t.yudai92@gmail.com
|
|
56
|
+
executables: []
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- LICENSE.txt
|
|
61
|
+
- README.md
|
|
62
|
+
- lib/rspec/hermetic.rb
|
|
63
|
+
- lib/rspec/hermetic/allowlist.rb
|
|
64
|
+
- lib/rspec/hermetic/candidate_report.rb
|
|
65
|
+
- lib/rspec/hermetic/change.rb
|
|
66
|
+
- lib/rspec/hermetic/configuration.rb
|
|
67
|
+
- lib/rspec/hermetic/corpus_evaluation.rb
|
|
68
|
+
- lib/rspec/hermetic/diff.rb
|
|
69
|
+
- lib/rspec/hermetic/evaluation.rb
|
|
70
|
+
- lib/rspec/hermetic/evaluation_task.rb
|
|
71
|
+
- lib/rspec/hermetic/forensic.rb
|
|
72
|
+
- lib/rspec/hermetic/formatter.rb
|
|
73
|
+
- lib/rspec/hermetic/minitest.rb
|
|
74
|
+
- lib/rspec/hermetic/probe.rb
|
|
75
|
+
- lib/rspec/hermetic/probe/base.rb
|
|
76
|
+
- lib/rspec/hermetic/probe/constants.rb
|
|
77
|
+
- lib/rspec/hermetic/probe/env.rb
|
|
78
|
+
- lib/rspec/hermetic/probe/filesystem.rb
|
|
79
|
+
- lib/rspec/hermetic/probe/globals.rb
|
|
80
|
+
- lib/rspec/hermetic/probe/rails.rb
|
|
81
|
+
- lib/rspec/hermetic/probe/randomness.rb
|
|
82
|
+
- lib/rspec/hermetic/probe/resources.rb
|
|
83
|
+
- lib/rspec/hermetic/probe/ruby_runtime.rb
|
|
84
|
+
- lib/rspec/hermetic/probe/time.rb
|
|
85
|
+
- lib/rspec/hermetic/resource_tracker.rb
|
|
86
|
+
- lib/rspec/hermetic/restorer.rb
|
|
87
|
+
- lib/rspec/hermetic/runner.rb
|
|
88
|
+
- lib/rspec/hermetic/snapshot.rb
|
|
89
|
+
- lib/rspec/hermetic/stable_value.rb
|
|
90
|
+
- lib/rspec/hermetic/verdict.rb
|
|
91
|
+
- lib/rspec/hermetic/verify_task.rb
|
|
92
|
+
- lib/rspec/hermetic/version.rb
|
|
93
|
+
- sig/rspec/hermetic.rbs
|
|
94
|
+
homepage: https://github.com/ydah/rspec-hermetic
|
|
95
|
+
licenses:
|
|
96
|
+
- MIT
|
|
97
|
+
metadata:
|
|
98
|
+
source_code_uri: https://github.com/ydah/rspec-hermetic
|
|
99
|
+
rubygems_mfa_required: 'true'
|
|
100
|
+
rdoc_options: []
|
|
101
|
+
require_paths:
|
|
102
|
+
- lib
|
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: 3.2.0
|
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '0'
|
|
113
|
+
requirements: []
|
|
114
|
+
rubygems_version: 4.0.6
|
|
115
|
+
specification_version: 4
|
|
116
|
+
summary: Detect RSpec examples that leave shared state polluted.
|
|
117
|
+
test_files: []
|