pinspec 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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +133 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +183 -0
  5. data/exe/pinspec +8 -0
  6. data/lib/pinspec/analyzer/app_profile_reader.rb +348 -0
  7. data/lib/pinspec/analyzer/factory_registry.rb +288 -0
  8. data/lib/pinspec/analyzer/inflector.rb +85 -0
  9. data/lib/pinspec/analyzer/schema_reader.rb +444 -0
  10. data/lib/pinspec/analyzer/source.rb +56 -0
  11. data/lib/pinspec/analyzer/target_parser.rb +710 -0
  12. data/lib/pinspec/cli.rb +585 -0
  13. data/lib/pinspec/emit/namer.rb +103 -0
  14. data/lib/pinspec/emit/spec_writer.rb +504 -0
  15. data/lib/pinspec/emit/stability_filter.rb +183 -0
  16. data/lib/pinspec/errors.rb +85 -0
  17. data/lib/pinspec/inputs/boundary.rb +112 -0
  18. data/lib/pinspec/inputs/corpus.rb +148 -0
  19. data/lib/pinspec/inputs/hydrator.rb +197 -0
  20. data/lib/pinspec/inputs/redactor.rb +138 -0
  21. data/lib/pinspec/inputs/sample_runner.rb +98 -0
  22. data/lib/pinspec/inputs/sampler.rb +187 -0
  23. data/lib/pinspec/report/summary.rb +348 -0
  24. data/lib/pinspec/runner/capture.rb +127 -0
  25. data/lib/pinspec/runner/probe_generator.rb +662 -0
  26. data/lib/pinspec/runner/sandbox.rb +121 -0
  27. data/lib/pinspec/setup/context_builder.rb +471 -0
  28. data/lib/pinspec/setup/dependency_resolver.rb +236 -0
  29. data/lib/pinspec/tags.rb +103 -0
  30. data/lib/pinspec/types.rb +497 -0
  31. data/lib/pinspec/validate/mutation_adapter.rb +108 -0
  32. data/lib/pinspec/validate/pin_scorer.rb +164 -0
  33. data/lib/pinspec/verify/verifier.rb +149 -0
  34. data/lib/pinspec/version.rb +8 -0
  35. data/lib/pinspec.rb +17 -0
  36. data/templates/factory_build.rb +54 -0
  37. data/templates/serializer.rb +243 -0
  38. data/templates/spec_support.rb +83 -0
  39. metadata +134 -0
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+
6
+ module Pinspec
7
+ module Runner
8
+ class Capture
9
+ OUTPUT = "observations.json"
10
+
11
+ Result = Data.define(:target, :plan, :corpus, :runs, :stability, :probe_source, :output_path)
12
+
13
+ def initialize(app_root:, target:, method:, max_cases: Inputs::Corpus::DEFAULT_MAX_CASES,
14
+ boots: 2, compare_sql: false, sandbox_env: {}, output_dir: nil,
15
+ sample: false, sample_env: nil, redact: true)
16
+ @app_root = app_root
17
+ @target_file = target
18
+ @method = method
19
+ @max_cases = max_cases
20
+ @boots = boots
21
+ @compare_sql = compare_sql
22
+ @sandbox_env = sandbox_env
23
+ @output_dir = output_dir || File.join(app_root, Sandbox::PROBE_DIR)
24
+ @sample = sample
25
+ @sample_env = sample_env
26
+ @redact = redact
27
+ end
28
+
29
+ def run
30
+ target_profile = Analyzer::TargetParser.parse(@target_file, @method)
31
+ app_profile = Analyzer::AppProfileReader.read(@app_root)
32
+
33
+ plan = Setup::ContextBuilder.build(target: target_profile, profile: app_profile, tz: probe_tz)
34
+ imports = @sample ? sample_imports(plan, app_profile, target_profile) : []
35
+ if imports.any?
36
+ plan = Setup::ContextBuilder.build(target: target_profile, profile: app_profile,
37
+ imports: imports, tz: probe_tz)
38
+ end
39
+
40
+ corpus = Inputs::Corpus.build(
41
+ target: target_profile, plan: plan,
42
+ schema: app_profile.schema, max_cases: @max_cases
43
+ )
44
+
45
+ probe = ProbeGenerator.generate(
46
+ plan: plan, corpus: corpus,
47
+ fk_map: app_profile.schema.fk_map, target: target_profile
48
+ )
49
+
50
+ runs = Sandbox.new(app_root: @app_root, probe_source: probe, env: @sandbox_env)
51
+ .capture(boots: @boots)
52
+
53
+ stability = Emit::StabilityFilter.new(compare_sql: @compare_sql).filter(runs)
54
+
55
+ Result.new(
56
+ target: target_profile, plan: plan, corpus: corpus, runs: runs,
57
+ stability: stability, probe_source: probe, output_path: write_observations(plan, runs, stability)
58
+ )
59
+ end
60
+
61
+ def probe_tz
62
+ @sandbox_env["TZ"] || Sandbox::FORCED_ENV.fetch("TZ")
63
+ end
64
+
65
+ private
66
+
67
+ def sample_imports(plan, app_profile, target_profile)
68
+ tables = plan.record_steps.map { |step| app_profile.schema.table(table_of(step, app_profile)) }
69
+ .compact.map(&:name).uniq
70
+ return [] if tables.empty?
71
+
72
+ requests = tables.map do |table|
73
+ { table: table, status_column: status_column_for(app_profile.schema.table(table)), limit: 3 }
74
+ end
75
+
76
+ result = Inputs::SampleRunner.new(app_root: @app_root, env: @sample_env || @sandbox_env).fetch(requests)
77
+
78
+ return [] if result.empty?
79
+
80
+ root = tables.first
81
+ hydrator = Inputs::Hydrator.new(
82
+ schema: app_profile.schema,
83
+ redactor: Inputs::Redactor.new(enabled: @redact),
84
+ factories: app_profile.factories
85
+ )
86
+ clusters, = hydrator.hydrate(root, result.rows_for(root).first(1), result.all_rows,
87
+ target_source: Analyzer::Source.read(target_profile.file_path))
88
+ clusters
89
+ end
90
+
91
+ def table_of(step, app_profile)
92
+ model = step.payload[:model] || step.payload[:factory].to_s
93
+ Setup::DependencyResolver.new(app_profile.schema, app_profile.factories).table_for(model)&.name ||
94
+ Setup::DependencyResolver.new(app_profile.schema, app_profile.factories).table_for(model.to_s.capitalize)&.name
95
+ end
96
+
97
+ def status_column_for(table)
98
+ return nil if table.nil?
99
+
100
+ %w[status state kind].find { |name| table.column(name) }
101
+ end
102
+
103
+ def write_observations(plan, runs, stability)
104
+ FileUtils.mkdir_p(@output_dir)
105
+ path = File.join(@output_dir, OUTPUT)
106
+
107
+ File.write(path, JSON.pretty_generate(
108
+ "pinspec_probe_version" => PROBE_VERSION,
109
+ "serializer" => SERIALIZER_VERSION,
110
+ "plan_id" => plan.plan_id,
111
+ "isolation" => plan.isolation.to_s,
112
+ "runs" => runs.map { |run| { "run" => run.run, "env" => run.env } },
113
+ "compared_fields" => stability.compared_fields,
114
+ "stable" => stability.stable.map { |verdict| verdict.case_id },
115
+ "unstable" => stability.unstable.map do |verdict|
116
+ { "case_id" => verdict.case_id, "cause" => verdict.cause.to_s, "diff" => verdict.diff }
117
+ end,
118
+ "observations" => runs.map do |run|
119
+ { "run" => run.run, "observations" => run.observations }
120
+ end
121
+ ))
122
+
123
+ path
124
+ end
125
+ end
126
+ end
127
+ end