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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +133 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/exe/pinspec +8 -0
- data/lib/pinspec/analyzer/app_profile_reader.rb +348 -0
- data/lib/pinspec/analyzer/factory_registry.rb +288 -0
- data/lib/pinspec/analyzer/inflector.rb +85 -0
- data/lib/pinspec/analyzer/schema_reader.rb +444 -0
- data/lib/pinspec/analyzer/source.rb +56 -0
- data/lib/pinspec/analyzer/target_parser.rb +710 -0
- data/lib/pinspec/cli.rb +585 -0
- data/lib/pinspec/emit/namer.rb +103 -0
- data/lib/pinspec/emit/spec_writer.rb +504 -0
- data/lib/pinspec/emit/stability_filter.rb +183 -0
- data/lib/pinspec/errors.rb +85 -0
- data/lib/pinspec/inputs/boundary.rb +112 -0
- data/lib/pinspec/inputs/corpus.rb +148 -0
- data/lib/pinspec/inputs/hydrator.rb +197 -0
- data/lib/pinspec/inputs/redactor.rb +138 -0
- data/lib/pinspec/inputs/sample_runner.rb +98 -0
- data/lib/pinspec/inputs/sampler.rb +187 -0
- data/lib/pinspec/report/summary.rb +348 -0
- data/lib/pinspec/runner/capture.rb +127 -0
- data/lib/pinspec/runner/probe_generator.rb +662 -0
- data/lib/pinspec/runner/sandbox.rb +121 -0
- data/lib/pinspec/setup/context_builder.rb +471 -0
- data/lib/pinspec/setup/dependency_resolver.rb +236 -0
- data/lib/pinspec/tags.rb +103 -0
- data/lib/pinspec/types.rb +497 -0
- data/lib/pinspec/validate/mutation_adapter.rb +108 -0
- data/lib/pinspec/validate/pin_scorer.rb +164 -0
- data/lib/pinspec/verify/verifier.rb +149 -0
- data/lib/pinspec/version.rb +8 -0
- data/lib/pinspec.rb +17 -0
- data/templates/factory_build.rb +54 -0
- data/templates/serializer.rb +243 -0
- data/templates/spec_support.rb +83 -0
- 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
|