pinspec 0.2.0 → 0.3.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.
@@ -18,7 +18,7 @@ module Pinspec
18
18
  Result = Data.define(:spec_path, :support_paths, :pinned_cases, :aspects)
19
19
 
20
20
  def initialize(app_root:, target:, plan:, corpus:, stability:, fk_map:, force: false,
21
- only_aspect: nil, spec_dir: nil, namer: nil)
21
+ only_aspect: nil, spec_dir: nil)
22
22
  @app_root = app_root
23
23
  @target = target
24
24
  @plan = plan
@@ -28,7 +28,18 @@ module Pinspec
28
28
  @force = force
29
29
  @only_aspect = only_aspect
30
30
  @spec_dir = spec_dir || SPEC_DIR
31
- @namer = namer
31
+ end
32
+
33
+ # The class half of a pin's filename, so a caller can find the pin for a class
34
+ # without knowing which method it froze.
35
+ def self.class_stem(class_name)
36
+ class_name.to_s
37
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
38
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
39
+ .downcase
40
+ .tr("/", "_")
41
+ .tr(":", "_")
42
+ .squeeze("_")
32
43
  end
33
44
 
34
45
  def spec_path
@@ -101,7 +112,7 @@ module Pinspec
101
112
  end
102
113
 
103
114
  def file_stem
104
- underscore(@target.class_name).tr("/", "_") + "_" + @target.method_name.to_s.gsub(/[^a-z0-9_]/i, "")
115
+ self.class.class_stem(@target.class_name) + "_" + @target.method_name.to_s.gsub(/[^a-z0-9_]/i, "")
105
116
  end
106
117
 
107
118
  def render
@@ -206,8 +217,13 @@ module Pinspec
206
217
  fingerprint = @plan.env_fingerprint
207
218
 
208
219
  <<~RUBY
220
+ # travel_to and travel_back live here. rspec-rails includes them only when a
221
+ # suite opts in, and a pin must not depend on that.
222
+ include ActiveSupport::Testing::TimeHelpers
223
+
209
224
  before do
210
225
  #{clock_guard(fingerprint)} pinspec_clear_sinks
226
+ PinspecFactory.reset_sequences!
211
227
 
212
228
  travel_to Time.parse(#{PINSPEC_EPOCH.inspect})
213
229
  srand(#{PINSPEC_SEED})
@@ -254,11 +270,16 @@ module Pinspec
254
270
  end
255
271
 
256
272
  def create_call(payload)
273
+ attrs = (payload[:attrs] || {}).merge(assoc_arguments(payload))
274
+
257
275
  if payload[:factory]
258
- return "PinspecFactory.create(:#{payload[:factory]})"
276
+ # The probe passes these to the factory too. Dropping them here built a
277
+ # different world in the emitted spec than the one that was captured.
278
+ return "PinspecFactory.create(:#{payload[:factory]})" if attrs.empty?
279
+
280
+ return "PinspecFactory.create(:#{payload[:factory]}, #{render_attrs(attrs)})"
259
281
  end
260
282
 
261
- attrs = (payload[:attrs] || {}).merge(assoc_arguments(payload))
262
283
  "#{payload[:model]}.create!(#{render_attrs(attrs)})"
263
284
  end
264
285
 
@@ -321,9 +342,6 @@ module Pinspec
321
342
  end
322
343
 
323
344
  def description_for(input_case, observation)
324
- named = descriptions[input_case.id]
325
- return named if named
326
-
327
345
  outcome =
328
346
  case observation["status"]
329
347
  when "raised" then "raises #{observation.dig('error', 'class')}"
@@ -333,20 +351,6 @@ module Pinspec
333
351
  "#{@target.method_name} #{outcome} (#{input_case.id}, #{input_case.origin})"
334
352
  end
335
353
 
336
- def descriptions
337
- return @descriptions if defined?(@descriptions)
338
-
339
- @descriptions =
340
- if @namer&.enabled?
341
- pairs = pinned.filter_map do |verdict|
342
- input_case = @corpus.cases.find { |c| c.id == verdict.case_id }
343
- [input_case, verdict.observation] if input_case
344
- end
345
- @namer.describe(pairs)
346
- else
347
- {}
348
- end
349
- end
350
354
 
351
355
  def value_shape(value)
352
356
  case value && value["t"]
@@ -75,10 +75,6 @@ module Pinspec
75
75
  self.exit_code = 10
76
76
  end
77
77
 
78
- class EnvironmentRefused < Error
79
- self.exit_code = 11
80
- end
81
-
82
78
  class ConfigInvalid < Error
83
79
  self.exit_code = 13
84
80
  end
@@ -19,11 +19,10 @@ module Pinspec
19
19
  DECIMAL_HINTS = %w[BigDecimal Decimal].freeze
20
20
 
21
21
  class << self
22
- def values_for(param, column: nil, default_first: true)
22
+ def values_for(param, column: nil)
23
23
  declared = default_value(param)
24
24
  edges = edges_for(param, column)
25
-
26
- ordered = default_first ? [declared, *edges] : [*edges, declared]
25
+ ordered = [declared, *edges]
27
26
 
28
27
  present(ordered).map { |value| encode(value, param) }.uniq
29
28
  end
@@ -48,7 +48,7 @@ module Pinspec
48
48
  unless status.success?
49
49
  raise ProbeFailure,
50
50
  "the sampler exited #{status.exitstatus} in #{@app_root}.\n" \
51
- "#{stderr.to_s.lines.last(12).join}"
51
+ "#{excerpt(stderr)}"
52
52
  end
53
53
 
54
54
  parse(stdout, stderr)
@@ -63,6 +63,20 @@ module Pinspec
63
63
  path
64
64
  end
65
65
 
66
+ # A boot failure says WHAT went wrong on its first line and where on the rest.
67
+ # Showing only the last N lines shows the bottom of a backtrace - gem_prelude,
68
+ # rubygems.rb - which is identical for every failure and identifies none of them.
69
+ # Diagnosing a real application's failure took three commands and a read of
70
+ # pinspec's own internals because of that.
71
+ def excerpt(text, head: 6, tail: 6)
72
+ lines = text.to_s.lines
73
+ return lines.join if lines.size <= head + tail
74
+
75
+ lines.first(head).join +
76
+ " ... #{lines.size - head - tail} more line(s) ...\n" +
77
+ lines.last(tail).join
78
+ end
79
+
66
80
  def command
67
81
  return ["rails", "runner", SCRIPT_PATH] unless File.file?(File.join(@app_root, "Gemfile"))
68
82
 
@@ -80,8 +94,7 @@ module Pinspec
80
94
 
81
95
  if json.nil?
82
96
  raise ProbeFailure,
83
- "the sampler produced no rows.\nstdout: #{stdout.to_s.lines.last(8).join}" \
84
- "stderr: #{stderr.to_s.lines.last(8).join}"
97
+ "the sampler produced no rows.\nstdout: #{excerpt(stdout)}\nstderr: #{excerpt(stderr)}"
85
98
  end
86
99
 
87
100
  parsed = JSON.parse(json)
@@ -7,41 +7,9 @@ module Pinspec
7
7
  class Sampler
8
8
  SCRIPT_PATH = "tmp/pinspec/sampler.rb"
9
9
 
10
- ENV_PREFERENCE = %w[development test].freeze
11
-
12
- PRODUCTION_HINTS = [
13
- /(?<![a-z])prod(uction)?(?![a-z])/i,
14
- /(?<![a-z])live(?![a-z])/i,
15
- /(?<![a-z])master(?![a-z])/i
16
- ].freeze
17
-
18
10
  MAX_DISTINCT_STATUSES = 6
19
11
 
20
12
  class << self
21
- def choose_env(available:, counts: {}, override: nil)
22
- return override if override
23
-
24
- populated = ENV_PREFERENCE.find do |env|
25
- available.include?(env) && counts.fetch(env, 0).positive?
26
- end
27
-
28
- populated || ENV_PREFERENCE.find { |env| available.include?(env) } || available.first
29
- end
30
-
31
- def production_like?(name)
32
- PRODUCTION_HINTS.any? { |hint| name.to_s.match?(hint) }
33
- end
34
-
35
- def guard_production!(name, confirmed: false)
36
- return unless production_like?(name)
37
- return if confirmed
38
-
39
- raise EnvironmentRefused,
40
- "#{name.inspect} looks like a production database. pinspec only ever " \
41
- "SELECTs, but it will not read production without being told to: " \
42
- "re-run with --sample-db pointed somewhere else, or confirm explicitly."
43
- end
44
-
45
13
  def script_for(requests)
46
14
  new(requests).script
47
15
  end
@@ -185,6 +185,7 @@ module Pinspec
185
185
 
186
186
  def coverage_caveats
187
187
  caveats = []
188
+ caveats << vacuous_caveat
188
189
  caveats << seq_caveat
189
190
  caveats << truncated_caveat
190
191
  caveats << quarantine_caveat
@@ -203,6 +204,35 @@ module Pinspec
203
204
  MD
204
205
  end
205
206
 
207
+ # The honest half of pin strength, and the free half. A case that returned an
208
+ # empty collection, wrote no rows, enqueued nothing and sent nothing did run -
209
+ # but almost any change to the target would still satisfy it. Measured on real
210
+ # code, these are exactly the pins that score weak or worthless.
211
+ def vacuous_caveat
212
+ return nil if @stability.nil?
213
+
214
+ count = @stability.stable.count { |verdict| vacuous?(verdict.observation) }
215
+ return nil if count.zero?
216
+
217
+ "#{count} pinned case(s) observed nothing happening: an empty or absent return " \
218
+ "value, no rows written, no jobs, no mail. The pin holds, but almost any " \
219
+ "change to the target would still satisfy it. Give the target a world with " \
220
+ "something in it - `--sample` reads real rows - before relying on these."
221
+ end
222
+
223
+ EMPTY_RETURNS = [[], {}, nil, false, ""].freeze
224
+
225
+ def vacuous?(observation)
226
+ return false unless observation["status"] == "returned"
227
+ return false unless observation["enqueued_jobs"].to_a.empty?
228
+ return false unless observation["mail_deliveries"].to_a.empty?
229
+ return false unless observation["db_delta"].to_h.values.all? { |n| n.to_i.zero? }
230
+
231
+ EMPTY_RETURNS.include?(Tags.decode(observation["return_value"]))
232
+ rescue StandardError
233
+ false
234
+ end
235
+
206
236
  def seq_caveat
207
237
  count = rendered_observations.scan('"seq"').size
208
238
  return nil if count.zero?
@@ -17,7 +17,9 @@ module Pinspec
17
17
  @target_file = target
18
18
  @method = method
19
19
  @max_cases = max_cases
20
- @boots = boots
20
+ # Two is the floor, not the default. One boot has nothing to compare against,
21
+ # so every case would be called stable by virtue of never being checked.
22
+ @boots = [boots.to_i, 2].max
21
23
  @compare_sql = compare_sql
22
24
  @sandbox_env = sandbox_env
23
25
  @output_dir = output_dir || File.join(app_root, Sandbox::PROBE_DIR)
@@ -574,6 +574,7 @@ module Pinspec
574
574
  begin
575
575
  pinspec_with_isolation do
576
576
  begin
577
+ PinspecFactory.reset_sequences!
577
578
  pinspec_run_steps(records, refs)
578
579
  rescue StandardError => setup_error
579
580
  observation["status"] = "setup_error"
@@ -638,7 +639,9 @@ module Pinspec
638
639
  # Between cases: anything memoized in a cache or a CurrentAttributes slot
639
640
  # would otherwise leak into the next case.
640
641
  Rails.cache.clear if defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
641
- ActiveSupport::CurrentAttributes.reset_all if defined?(ActiveSupport::CurrentAttributes)
642
+ if defined?(ActiveSupport::CurrentAttributes) && ActiveSupport::CurrentAttributes.respond_to?(:reset_all)
643
+ ActiveSupport::CurrentAttributes.reset_all
644
+ end
642
645
  pinspec_clear_sinks
643
646
  PinspecClock.travel_back
644
647
  end
@@ -20,8 +20,6 @@ module Pinspec
20
20
  "probe-#{pid}-#{object_id || rand(1 << 24)}.rb"
21
21
  end
22
22
 
23
- DEFAULT_TIMEOUT = 600
24
-
25
23
  FORCED_ENV = {
26
24
  "DISABLE_SPRING" => "1",
27
25
  "TZ" => "UTC",
@@ -36,11 +34,9 @@ module Pinspec
36
34
  end
37
35
  end
38
36
 
39
- def initialize(app_root:, probe_source:, timeout: DEFAULT_TIMEOUT, runner: nil, env: {})
37
+ def initialize(app_root:, probe_source:, env: {})
40
38
  @app_root = app_root
41
39
  @probe_source = probe_source
42
- @timeout = timeout
43
- @runner = runner
44
40
  @env = env
45
41
  end
46
42
 
@@ -80,24 +76,40 @@ module Pinspec
80
76
  .merge(@env)
81
77
 
82
78
  stdout, stderr, status = Open3.capture3(env, *command, chdir: @app_root)
79
+ stdout = stdout.to_s.dup.force_encoding(Encoding::UTF_8).scrub
80
+ stderr = stderr.to_s.dup.force_encoding(Encoding::UTF_8).scrub
83
81
 
84
82
  unless status.success?
85
83
  raise ProbeFailure,
86
84
  "the probe exited #{status.exitstatus} in #{@app_root}.\n" \
87
85
  "#{runtime.note ? "#{runtime.note}\n\n" : ''}" \
88
- "#{stderr.to_s.lines.last(15).join}"
86
+ "#{excerpt(stderr)}"
89
87
  end
90
88
 
91
89
  parse(stdout, stderr, run)
92
90
  end
93
91
 
92
+ # A boot failure says WHAT went wrong on its first line and where on the rest.
93
+ # Showing only the last N lines shows the bottom of a backtrace - gem_prelude,
94
+ # rubygems.rb - which is identical for every failure and identifies none of them.
95
+ # Diagnosing a real application's failure took three commands and a read of
96
+ # pinspec's own internals because of that.
97
+ def excerpt(text, head: 6, tail: 6)
98
+ lines = text.to_s.lines
99
+ return lines.join if lines.size <= head + tail
100
+
101
+ lines.first(head).join +
102
+ " ... #{lines.size - head - tail} more line(s) ...\n" +
103
+ lines.last(tail).join
104
+ end
105
+
94
106
  def parse(stdout, stderr, run)
95
107
  json = stdout.to_s.lines.reverse.find { |line| line.strip.start_with?("{") }
96
108
 
97
109
  if json.nil?
98
110
  raise ProbeFailure,
99
111
  "the probe produced no observations.\n" \
100
- "stdout: #{stdout.to_s.lines.last(10).join}\nstderr: #{stderr.to_s.lines.last(10).join}"
112
+ "stdout: #{excerpt(stdout)}\nstderr: #{excerpt(stderr)}"
101
113
  end
102
114
 
103
115
  parsed = JSON.parse(json)
@@ -114,8 +126,6 @@ module Pinspec
114
126
  end
115
127
 
116
128
  def runner_command
117
- return @runner if @runner
118
-
119
129
  relative = File.join(PROBE_DIR, File.basename(probe_path))
120
130
 
121
131
  if File.file?(File.join(@app_root, "Gemfile"))
@@ -5,8 +5,6 @@ require "digest"
5
5
  module Pinspec
6
6
  module Setup
7
7
  class ContextBuilder
8
- MAX_GENERATIONS = 3
9
-
10
8
  class << self
11
9
  def build(target:, profile:, imports: [], generation: 1, tz: nil)
12
10
  new(target: target, profile: profile, imports: imports, generation: generation, tz: tz).build
@@ -33,8 +31,11 @@ module Pinspec
33
31
  refuse_attachment_targets!
34
32
 
35
33
  environment_steps
36
- record_steps
34
+ # Imports first: create_record_for skips a table whose ref already exists, so
35
+ # building factory rows first meant every binding pointed at the factory
36
+ # record and the sampled row - the whole point of --sample - went unused.
37
37
  import_steps
38
+ record_steps
38
39
  context_steps
39
40
  subject_step
40
41
 
@@ -146,11 +147,13 @@ module Pinspec
146
147
  @resolver.table_for_type_hint(param.type_hint)
147
148
  end
148
149
 
150
+ # Only the hints that actually occur. Measured over 841 real service files, these
151
+ # ten are the only ones ever produced; the other thirty-one fired zero times and
152
+ # none of them has a Boundary value, so a hint that did somehow reach the list
153
+ # would be exempted from the refusal and then passed as nil - manufacturing the
154
+ # exact fiction the refusal exists to prevent.
149
155
  NON_MODEL_HINTS = %w[
150
- String Symbol Integer Float Numeric Decimal BigDecimal Boolean TrueClass
151
- FalseClass Hash Array Range Time Date DateTime Proc Class Module Object
152
- NilClass Params Param Options Option Attributes Attribute Id Ids Name
153
- Amount Quantity Count Total Price Rate Percentage Status State Kind Type
156
+ String Symbol Integer Float Boolean Hash Array Time Date Object
154
157
  ].freeze
155
158
 
156
159
  def refuse_unresolvable_model_param!(param)
data/lib/pinspec/types.rb CHANGED
@@ -1,19 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pinspec
4
- PARAM_KINDS = %i[req opt rest keyreq key keyrest].freeze
5
4
 
6
5
  OPTIONAL_PARAM_KINDS = %i[opt key rest keyrest].freeze
7
6
  POSITIONAL_PARAM_KINDS = %i[req opt rest].freeze
8
7
 
9
- CONSTRUCTION_KINDS = %i[
10
- new
11
- class_method
12
- interactor
13
- dry_initializer
14
- struct
15
- model_instance
16
- ].freeze
17
8
 
18
9
  Param = Data.define(:name, :kind, :default_source, :type_hint) do
19
10
  def positional?
@@ -49,7 +40,8 @@ module Pinspec
49
40
  :takes_block,
50
41
  :source_range,
51
42
  :referenced_constants,
52
- :clock_sites
43
+ :clock_sites,
44
+ :construction_source
53
45
  ) do
54
46
  def singleton?
55
47
  construction_kind == :class_method
@@ -365,38 +357,6 @@ module Pinspec
365
357
  def dsl_module
366
358
  legacy_dsl ? "FactoryGirl" : "FactoryBot"
367
359
  end
368
-
369
- def traits_for(name)
370
- ancestry(name).flat_map(&:traits)
371
- end
372
-
373
- def ancestry(name)
374
- chain = []
375
- seen = []
376
- current = factory(name)
377
-
378
- while current && !seen.include?(current.name)
379
- seen << current.name
380
- chain.unshift(current)
381
- current = current.parent && factory(current.parent)
382
- end
383
-
384
- chain
385
- end
386
-
387
- def attributes_for(name, traits: [])
388
- chain = ancestry(name)
389
- merged = {}
390
-
391
- chain.each { |f| f.attributes.each { |a| merged[a.name] = a } }
392
-
393
- Array(traits).each do |trait_name|
394
- found = chain.reverse.filter_map { |f| f.trait(trait_name) }.first
395
- found&.attributes&.each { |a| merged[a.name] = a }
396
- end
397
-
398
- merged.values
399
- end
400
360
  end
401
361
 
402
362
  Column = Data.define(
@@ -450,13 +410,7 @@ module Pinspec
450
410
  end
451
411
 
452
412
  SkippedStatement = Data.define(:kind, :table, :column, :references, :file, :line, :relevant) do
453
- def relevant?
454
- relevant == true
455
- end
456
413
 
457
- def tables_touched
458
- ([table] + Array(references)).compact.uniq
459
- end
460
414
 
461
415
  def to_s
462
416
  subject = column ? "#{table}.#{column}" : table
@@ -486,12 +440,5 @@ module Pinspec
486
440
  tables.flat_map { |t| t.columns.select(&:unknown_type?).map { |c| [t.name, c] } }
487
441
  end
488
442
 
489
- def annotate_relevance(planned_tables)
490
- wanted = Array(planned_tables).map(&:to_s)
491
-
492
- with(skipped_statements: skipped_statements.map do |statement|
493
- statement.with(relevant: statement.tables_touched.any? { |t| wanted.include?(t) })
494
- end)
495
- end
496
443
  end
497
444
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
4
+ require "pathname"
3
5
  require "json"
4
6
  require "open3"
5
7
 
@@ -8,6 +10,8 @@ module Pinspec
8
10
  class Verifier
9
11
  CONFIGS = %i[isolated hostile neighbored].freeze
10
12
 
13
+ NEIGHBOUR_DIR = "tmp/pinspec"
14
+
11
15
  HOSTILE_TZ_CANDIDATES = ["Etc/GMT+8", "Etc/GMT-6"].freeze
12
16
 
13
17
  Outcome = Data.define(:config, :status, :diagnosis, :detail, :examples, :failures) do
@@ -52,6 +56,7 @@ module Pinspec
52
56
  def run(config)
53
57
  args, extra_env = arguments_for(config)
54
58
  stdout, stderr, _status = Open3.capture3(environment.merge(extra_env), *args, chdir: @app_root)
59
+ FileUtils.rm_f(@neighbour) if @neighbour
55
60
 
56
61
  summary = parse(stdout)
57
62
  return failure(config, stdout, stderr, summary) if summary.nil? || summary["failure_count"].to_i.positive?
@@ -72,7 +77,7 @@ module Pinspec
72
77
  end
73
78
 
74
79
  def arguments_for(config)
75
- relative = @spec_path.sub("#{@app_root}/", "")
80
+ relative = relative_spec_path
76
81
  base = ["bundle", "exec", "rspec", "--format", "json"]
77
82
 
78
83
  case config
@@ -81,10 +86,40 @@ module Pinspec
81
86
  when :hostile
82
87
  [base + ["--seed", "7", relative], { "TZ" => hostile_tz, "LANG" => "C", "LC_ALL" => "C" }]
83
88
  when :neighbored
84
- [base + [relative, relative], {}]
89
+ # A COPY alongside the original, because RSpec loads a given path once
90
+ # however many times it is named - so passing it twice re-ran nothing, and
91
+ # this configuration silently checked exactly what :isolated checks.
92
+ [base + [relative, neighbour_of(relative)], {}]
85
93
  end
86
94
  end
87
95
 
96
+ # Pathname does this properly: a string sub leaves an absolute path untouched
97
+ # when it does not start with the app root, which is exactly the case when
98
+ # someone runs `pinspec verify spec/foo_spec.rb` from inside their app.
99
+ def relative_spec_path
100
+ Pathname.new(File.expand_path(@spec_path))
101
+ .relative_path_from(Pathname.new(File.expand_path(@app_root)))
102
+ .to_s
103
+ rescue ArgumentError
104
+ @spec_path
105
+ end
106
+
107
+ # Written into the app's tmp, with its relative requires rewritten to point back
108
+ # at the support files the original sits beside. Removed again after the run -
109
+ # pinspec does not leave files in somebody's repository.
110
+ def neighbour_of(relative)
111
+ source = File.expand_path(relative, @app_root)
112
+ target = File.join(@app_root, NEIGHBOUR_DIR, "neighbour_#{File.basename(relative)}")
113
+ support = File.dirname(source)
114
+
115
+ FileUtils.mkdir_p(File.dirname(target))
116
+ File.write(target, Analyzer::Source.read(source)
117
+ .gsub('require_relative "support/', %(require_relative "#{support}/support/)))
118
+ @neighbour = target
119
+
120
+ File.join(NEIGHBOUR_DIR, File.basename(target))
121
+ end
122
+
88
123
  def hostile_tz
89
124
  HOSTILE_TZ_CANDIDATES.find { |zone| zone != @captured_tz } || HOSTILE_TZ_CANDIDATES.first
90
125
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pinspec
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
 
6
6
  PROBE_VERSION = 3
7
7
  SERIALIZER_VERSION = 3
@@ -4,12 +4,16 @@
4
4
  module PinspecFactory
5
5
  MAX_ATTEMPTS = 8
6
6
 
7
- def self.attempts
8
- @attempts ||= {}
9
- end
10
-
11
- def self.reset_attempts!
12
- @attempts = {}
7
+ # factory_bot sequences are process-global and monotonic, so a pin whose world uses
8
+ # one is only reproducible while nothing else in the process built that factory
9
+ # first. Running the same pin twice in one process produced INV-1 then INV-2 and the
10
+ # second run failed against its own snapshot. Both hosts rewind before every case,
11
+ # so a case always sees the same sequence values.
12
+ def self.reset_sequences!
13
+ mod = defined?(FactoryBot) ? FactoryBot : (defined?(FactoryGirl) ? FactoryGirl : nil)
14
+ return unless mod.respond_to?(:rewind_sequences)
15
+
16
+ mod.rewind_sequences
13
17
  end
14
18
 
15
19
  def self.default_module
@@ -31,7 +35,6 @@ module PinspecFactory
31
35
  factory_module.create(name.to_sym, attrs)
32
36
  end
33
37
 
34
- attempts[name.to_s] = attempt
35
38
  return record
36
39
  rescue StandardError => e
37
40
  raise unless retryable?(e)
@@ -40,7 +43,6 @@ module PinspecFactory
40
43
  end
41
44
  end
42
45
 
43
- attempts[name.to_s] = MAX_ATTEMPTS
44
46
  raise last_error
45
47
  end
46
48
 
@@ -48,7 +50,4 @@ module PinspecFactory
48
50
  defined?(ActiveRecord::RecordInvalid) && error.is_a?(ActiveRecord::RecordInvalid)
49
51
  end
50
52
 
51
- def self.fragile
52
- attempts.select { |_name, count| count > 1 }
53
- end
54
53
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pinspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rehan Munir
@@ -75,6 +75,7 @@ files:
75
75
  - exe/pinspec
76
76
  - lib/pinspec.rb
77
77
  - lib/pinspec/analyzer/app_profile_reader.rb
78
+ - lib/pinspec/analyzer/discovery.rb
78
79
  - lib/pinspec/analyzer/factory_registry.rb
79
80
  - lib/pinspec/analyzer/inflector.rb
80
81
  - lib/pinspec/analyzer/schema_reader.rb
@@ -83,7 +84,6 @@ files:
83
84
  - lib/pinspec/batch.rb
84
85
  - lib/pinspec/cli.rb
85
86
  - lib/pinspec/config.rb
86
- - lib/pinspec/emit/namer.rb
87
87
  - lib/pinspec/emit/spec_writer.rb
88
88
  - lib/pinspec/emit/stability_filter.rb
89
89
  - lib/pinspec/errors.rb