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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +166 -0
  4. data/lib/rspec/hermetic/allowlist.rb +75 -0
  5. data/lib/rspec/hermetic/candidate_report.rb +47 -0
  6. data/lib/rspec/hermetic/change.rb +61 -0
  7. data/lib/rspec/hermetic/configuration.rb +93 -0
  8. data/lib/rspec/hermetic/corpus_evaluation.rb +124 -0
  9. data/lib/rspec/hermetic/diff.rb +63 -0
  10. data/lib/rspec/hermetic/evaluation.rb +184 -0
  11. data/lib/rspec/hermetic/evaluation_task.rb +53 -0
  12. data/lib/rspec/hermetic/forensic.rb +22 -0
  13. data/lib/rspec/hermetic/formatter.rb +93 -0
  14. data/lib/rspec/hermetic/minitest.rb +80 -0
  15. data/lib/rspec/hermetic/probe/base.rb +17 -0
  16. data/lib/rspec/hermetic/probe/constants.rb +87 -0
  17. data/lib/rspec/hermetic/probe/env.rb +15 -0
  18. data/lib/rspec/hermetic/probe/filesystem.rb +109 -0
  19. data/lib/rspec/hermetic/probe/globals.rb +31 -0
  20. data/lib/rspec/hermetic/probe/rails.rb +146 -0
  21. data/lib/rspec/hermetic/probe/randomness.rb +78 -0
  22. data/lib/rspec/hermetic/probe/resources.rb +110 -0
  23. data/lib/rspec/hermetic/probe/ruby_runtime.rb +37 -0
  24. data/lib/rspec/hermetic/probe/time.rb +54 -0
  25. data/lib/rspec/hermetic/probe.rb +38 -0
  26. data/lib/rspec/hermetic/resource_tracker.rb +111 -0
  27. data/lib/rspec/hermetic/restorer.rb +330 -0
  28. data/lib/rspec/hermetic/runner.rb +183 -0
  29. data/lib/rspec/hermetic/snapshot.rb +37 -0
  30. data/lib/rspec/hermetic/stable_value.rb +140 -0
  31. data/lib/rspec/hermetic/verdict.rb +39 -0
  32. data/lib/rspec/hermetic/verify_task.rb +65 -0
  33. data/lib/rspec/hermetic/version.rb +11 -0
  34. data/lib/rspec/hermetic.rb +59 -0
  35. data/sig/rspec/hermetic.rbs +112 -0
  36. 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module Hermetic
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
8
+
9
+ module Rspec
10
+ Hermetic = ::RSpec::Hermetic unless const_defined?(:Hermetic, false)
11
+ 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: []