recheck 0.0.1 → 0.4.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 +4 -4
- data/exe/recheck +8 -0
- data/lib/recheck/checkers.rb +33 -0
- data/lib/recheck/cli.rb +47 -0
- data/lib/recheck/commands.rb +311 -0
- data/lib/recheck/count_stats.rb +66 -0
- data/lib/recheck/reporters.rb +271 -0
- data/lib/recheck/results.rb +21 -0
- data/lib/recheck/runner.rb +165 -0
- data/lib/recheck/version.rb +5 -0
- data/lib/recheck.rb +20 -0
- data/template/recheck_helper.rb +18 -0
- data/template/site/dns_checker.rb +31 -0
- data/template/site/tls_checker.rb +51 -0
- data/template/site/whois_checker.rb +25 -0
- data/vendor/optimist.rb +1361 -0
- metadata +66 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d3abb8f0a8b65c31746964627a98cd5e713f77370797153268465e16a515646
|
4
|
+
data.tar.gz: 767cc703b8a396e486f2350d9f67e39c7d6b572b749dba5e1a9ef48e6394da7c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7509ef3cc281c76f022fd1c2599242a4cc5b758f8feb2975de6d70b8fa9756419d45d23740e16487b44b8088506ae8f5a11fc62bf25d77ec38255553d8d044f3
|
7
|
+
data.tar.gz: ec42bd74543f4ea2a8d884528005d3da2a2f302e1a987f16e15631f324d901f5e817da143151116a836a59b0e3c98aa81f438ccf0c67f29038411559a01737cf
|
data/exe/recheck
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Recheck
|
2
|
+
module Checker
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
def checker_classes
|
6
|
+
@@checker_classes ||= Set.new
|
7
|
+
@@checker_classes
|
8
|
+
end
|
9
|
+
|
10
|
+
def inherited(klass)
|
11
|
+
register klass
|
12
|
+
end
|
13
|
+
|
14
|
+
# Call if you don't want to inherit from Recheck::Checker
|
15
|
+
def register klass
|
16
|
+
checker_classes << klass
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Reflect for a list of queries to run. Override this if you don't want to start all your
|
21
|
+
# query methods with `query` or you are metaprogramming query methods at runtime.
|
22
|
+
def self.query_methods
|
23
|
+
public_instance_methods(false).select { |m| m.to_s.start_with?("query") }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Reflect for a list of checks to run. Override this if you don't want to start all your
|
27
|
+
# check methods with `check` or you are metaprogramming check methods at runtime.
|
28
|
+
def self.check_methods
|
29
|
+
public_instance_methods(false).select { |m| m.to_s.start_with?("check") }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/recheck/cli.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Recheck
|
2
|
+
class Cli
|
3
|
+
EXIT_CODE = {
|
4
|
+
no_errors: 0, # all checks passed
|
5
|
+
any_errors: 1, # any check returns fail or threw an exception
|
6
|
+
load_error: 2, # error loading checker/reporter
|
7
|
+
recheck_error: 3 # recheck itself encountered an error
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
COMMANDS = {
|
11
|
+
reporters: "List available reporters",
|
12
|
+
run: "Run checks",
|
13
|
+
setup: "Set up a new check suite in the current directory"
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize(argv: [])
|
17
|
+
@argv = argv
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
global_options = Recheck::Optimist.options(@argv) do
|
22
|
+
version "recheck v#{Recheck::VERSION}"
|
23
|
+
|
24
|
+
banner "Usage:"
|
25
|
+
banner " recheck [global options] [<command> [options]]"
|
26
|
+
|
27
|
+
banner "\nGlobal options:"
|
28
|
+
opt :version, "Print version and exit", short: :v
|
29
|
+
opt :help, "Print help", short: :h
|
30
|
+
stop_on COMMANDS.keys.map(&:to_s)
|
31
|
+
|
32
|
+
banner "\nCommands:"
|
33
|
+
COMMANDS.each { |c, desc| banner format(" %-10s %s", c, desc) }
|
34
|
+
end
|
35
|
+
|
36
|
+
command = global_options[:_leftovers].shift&.to_sym || :help
|
37
|
+
Recheck::Optimist.die "unknown command '#{command}'" unless COMMANDS.include? command
|
38
|
+
|
39
|
+
Recheck::Command.const_get(command.to_s.split("_").map(&:capitalize).join("")).new(argv: global_options[:_leftovers]).run
|
40
|
+
|
41
|
+
exit EXIT_CODE[:no_errors]
|
42
|
+
rescue Interrupt
|
43
|
+
puts "\nOperation cancelled by user."
|
44
|
+
exit EXIT_CODE[:recheck_error]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,311 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "erb"
|
5
|
+
|
6
|
+
module Recheck
|
7
|
+
module Command
|
8
|
+
class Reporters
|
9
|
+
def initialize(argv: [])
|
10
|
+
@options = Optimist.options(argv) do
|
11
|
+
banner "recheck list_reporters: load and list reporters"
|
12
|
+
opt :location, "Show source location", short: :l, type: :boolean, default: false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
puts "Available reporters (add yours to recheck/reporter/):\n"
|
18
|
+
Recheck::Reporter::Base.subclasses.each do |reporter_class|
|
19
|
+
help = reporter_class.respond_to?(:help) ? reporter_class.help : nil
|
20
|
+
help ||= "No help avalable"
|
21
|
+
puts "#{reporter_class.name} #{help}"
|
22
|
+
puts %( #{Object.const_source_location(reporter_class.to_s).join(":")}) if @options[:location]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end # Reporters
|
26
|
+
|
27
|
+
class Run
|
28
|
+
def initialize(argv: [])
|
29
|
+
@options = {
|
30
|
+
reporters: []
|
31
|
+
}
|
32
|
+
@file_patterns = []
|
33
|
+
|
34
|
+
@argv = argv
|
35
|
+
@options = Optimist.options(@argv) do
|
36
|
+
banner "recheck run: run the suite"
|
37
|
+
opt :reporter, "<reporter>[:ARGS], can use multiple times", short: :r, multi: true, default: ["Recheck::Reporter::Default"]
|
38
|
+
end
|
39
|
+
@files_created = []
|
40
|
+
|
41
|
+
@file_patterns = @options[:_leftovers]
|
42
|
+
end
|
43
|
+
|
44
|
+
def run
|
45
|
+
checkers = load_checkers
|
46
|
+
reporters = load_reporters(@options[:reporter])
|
47
|
+
|
48
|
+
total_counts = Runner.new(checkers:, reporters:).run
|
49
|
+
rescue Interrupt
|
50
|
+
puts "\nOperation interrupted by user."
|
51
|
+
rescue => e
|
52
|
+
puts "\nAn error occurred in Recheck:"
|
53
|
+
puts e.full_message(highlight: false)
|
54
|
+
exit Cli::EXIT_CODE[:recheck_error]
|
55
|
+
# ensure
|
56
|
+
# puts "ensure"
|
57
|
+
# exit Cli::EXIT_CODE[total_counts&.all_pass? ? :no_errors : :any_errors]
|
58
|
+
end
|
59
|
+
|
60
|
+
def load_checkers
|
61
|
+
files = if @file_patterns.empty?
|
62
|
+
Dir.glob("recheck/**/*.rb").sort
|
63
|
+
else
|
64
|
+
check_missing_files
|
65
|
+
@file_patterns.flat_map do |pattern|
|
66
|
+
if File.directory?(pattern)
|
67
|
+
Dir.glob(File.join(pattern, "**/*.rb"))
|
68
|
+
else
|
69
|
+
Dir.glob(pattern)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
files.each do |file|
|
75
|
+
# bug: if the file has a syntax error, Ruby silently exits instead of raising SyntaxError
|
76
|
+
require File.expand_path(file)
|
77
|
+
rescue LoadError => e
|
78
|
+
puts "Loading #{file} threw an exception: #{e.class}: #{e.message}, #{e.backtrace.first}"
|
79
|
+
unless file.start_with?("recheck/")
|
80
|
+
puts "that filename doesn't start with \"recheck/\", did you give the name of a model instead of its checker?"
|
81
|
+
end
|
82
|
+
exit Cli::EXIT_CODE[:load_error]
|
83
|
+
end
|
84
|
+
|
85
|
+
if Recheck::Checker::Base.checker_classes.empty?
|
86
|
+
error = "No checks detected." +
|
87
|
+
(@file_patterns.empty? ? " Did you run `bundle exec recheck setup`?" : "")
|
88
|
+
warn error
|
89
|
+
exit Cli::EXIT_CODE[:load_error]
|
90
|
+
end
|
91
|
+
|
92
|
+
Recheck::Checker::Base.checker_classes.map(&:new)
|
93
|
+
end
|
94
|
+
|
95
|
+
def check_missing_files
|
96
|
+
missing_files = @file_patterns.reject { |pattern| Dir.glob(pattern).any? }
|
97
|
+
unless missing_files.empty?
|
98
|
+
puts "Error: The following files do not exist:"
|
99
|
+
missing_files.each { |file| puts file }
|
100
|
+
exit Cli::EXIT_CODE[:load_error]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def load_reporters(reporters)
|
105
|
+
reporters.map { |option|
|
106
|
+
class_name, arg = option.split(/(?<!:):(?!:)/, 2)
|
107
|
+
resolve_reporter_class(class_name).new(arg:)
|
108
|
+
}
|
109
|
+
rescue ArgumentError => e
|
110
|
+
puts "Bad argument to Reporter (#{e.backtrace.first}): #{e.message}"
|
111
|
+
exit Cli::EXIT_CODE[:load_error]
|
112
|
+
rescue LoadError => e
|
113
|
+
puts "Loading #{file} threw an exception: #{e.class}: #{e.message}, #{e.backtrace.first}"
|
114
|
+
exit Cli::EXIT_CODE[:load_error]
|
115
|
+
end
|
116
|
+
|
117
|
+
def resolve_reporter_class(reporter_name)
|
118
|
+
[Object, Recheck::Reporter].each do |namespace|
|
119
|
+
return namespace.const_get(reporter_name)
|
120
|
+
rescue NameError
|
121
|
+
next
|
122
|
+
end
|
123
|
+
puts "Error: Reporter class '#{reporter_name}' not found globally or in Recheck::Reporter."
|
124
|
+
exit Cli::EXIT_CODE[:load_error]
|
125
|
+
end
|
126
|
+
end # Run
|
127
|
+
|
128
|
+
class Setup
|
129
|
+
def initialize(argv: [])
|
130
|
+
@argv = argv
|
131
|
+
@options = Optimist.options(@argv) do
|
132
|
+
banner "recheck setup: create a check suite"
|
133
|
+
end
|
134
|
+
@files_created = []
|
135
|
+
end
|
136
|
+
|
137
|
+
def run
|
138
|
+
create_helper
|
139
|
+
create_reporter_dir
|
140
|
+
create_site_checks
|
141
|
+
setup_model_checks
|
142
|
+
run_linter
|
143
|
+
vcs_message
|
144
|
+
end
|
145
|
+
|
146
|
+
def run_linter
|
147
|
+
if (linter_command = detect_linter)
|
148
|
+
puts "Detected linter, running `#{linter_command}` on created files..."
|
149
|
+
system("#{linter_command} #{@files_created.join(" ")}")
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def detect_linter
|
154
|
+
return "bundle exec standardrb --fix-unsafely recheck" if File.exist?(".standard.yml") || gemfile_includes?("standard")
|
155
|
+
return "bundle exec rubocop --autocorrect-all recheck" if File.exist?(".rubocop.yml") || gemfile_includes?("rubocop")
|
156
|
+
nil
|
157
|
+
end
|
158
|
+
|
159
|
+
def gemfile_includes?(gem_name)
|
160
|
+
File.readlines("Gemfile").any? { |line| line.include?(gem_name) }
|
161
|
+
rescue Errno::ENOENT
|
162
|
+
false
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def create_helper
|
168
|
+
copy_template("#{template_dir}/recheck_helper.rb", "recheck/recheck_helper.rb")
|
169
|
+
end
|
170
|
+
|
171
|
+
def create_reporter_dir
|
172
|
+
FileUtils.mkdir_p("recheck/reporter")
|
173
|
+
end
|
174
|
+
|
175
|
+
def create_site_checks
|
176
|
+
Dir.glob("#{template_dir}/site/*.rb").each do |filename|
|
177
|
+
copy_template(filename, "recheck/site/#{File.basename(filename)}")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
ModelFile = Data.define(:path, :class_name, :readonly, :pk_info) do
|
182
|
+
def checker_path
|
183
|
+
"recheck/model/#{underscore(class_name.gsub("::", "/"))}_checker.rb"
|
184
|
+
end
|
185
|
+
|
186
|
+
def underscore(string)
|
187
|
+
string.gsub("::", "/")
|
188
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
189
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
190
|
+
.tr("-", "_")
|
191
|
+
.downcase
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# warning: the rails gem overrides this method because it can
|
196
|
+
# provide better checks with the rails env loaded
|
197
|
+
def setup_model_checks
|
198
|
+
puts "Scanning for ActiveRecord models..."
|
199
|
+
model_files.each do |mf|
|
200
|
+
if mf.readonly
|
201
|
+
puts " #{mf.path} -> Skipped (readonly model, likely a view or uneditable record)"
|
202
|
+
elsif mf.pk_info.nil?
|
203
|
+
puts " #{mf.path} -> Skipped (model without primary key, unable to report on it)"
|
204
|
+
else
|
205
|
+
FileUtils.mkdir_p(File.dirname(mf.checker_path))
|
206
|
+
File.write(mf.checker_path, model_check_content(mf.class_name, mf.pk_info))
|
207
|
+
@files_created << mf.checker_path
|
208
|
+
puts " #{mf.path} -> #{mf.checker_path}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def model_files
|
214
|
+
search_paths = Dir.glob("**/*.rb").reject { |f| f.start_with?(%r{db/migrate/|vendor/}) }
|
215
|
+
search_paths.map do |path|
|
216
|
+
content = File.read(path)
|
217
|
+
if content.match?(/class\s+\w+(::\w+)*\s+<\s+(ApplicationRecord|ActiveRecord::Base)/) &&
|
218
|
+
!content.match?(/^\s+self\.abstract_class\s*=\s*true/)
|
219
|
+
class_name = extract_class_name(content)
|
220
|
+
readonly = readonly_model?(content)
|
221
|
+
pk_info = extract_primary_key_info(content)
|
222
|
+
ModelFile.new(path, class_name, readonly, pk_info)
|
223
|
+
end
|
224
|
+
end.compact
|
225
|
+
rescue Errno::ENOENT => e
|
226
|
+
puts "Error reading file: #{e.message}"
|
227
|
+
[]
|
228
|
+
end
|
229
|
+
|
230
|
+
PrimaryKeyInfo = Data.define(:query_method, :fetch_id_code) do
|
231
|
+
def compound?
|
232
|
+
fetch_id_code.include?(" + ")
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def extract_primary_key_info(content)
|
237
|
+
if content.match?(/self\.primary_key\s*=/)
|
238
|
+
pk_definition = content.match(/self\.primary_key\s*=\s*(.+)$/)[1].strip
|
239
|
+
if pk_definition.start_with?("[")
|
240
|
+
keys = parse_array(pk_definition)
|
241
|
+
fetch_id_code = keys.map { |key| "record.#{key}" }.join(' + "-" + ')
|
242
|
+
else
|
243
|
+
key = parse_symbol_or_string(pk_definition)
|
244
|
+
fetch_id_code = "record.#{key}.to_s"
|
245
|
+
end
|
246
|
+
query_method = ".all"
|
247
|
+
elsif content.match?(/self\.primary_key\s*=\s*nil/)
|
248
|
+
return nil
|
249
|
+
else
|
250
|
+
fetch_id_code = "record.id.to_s"
|
251
|
+
query_method = ".find_each"
|
252
|
+
end
|
253
|
+
PrimaryKeyInfo.new(query_method: query_method, fetch_id_code: fetch_id_code)
|
254
|
+
end
|
255
|
+
|
256
|
+
def parse_array(str)
|
257
|
+
str.gsub(/[\[\]]/, "").split(",").map { |item| parse_symbol_or_string(item.strip) }
|
258
|
+
end
|
259
|
+
|
260
|
+
def parse_symbol_or_string(str)
|
261
|
+
if str.start_with?(":")
|
262
|
+
str[1..].to_sym
|
263
|
+
elsif str.start_with?('"', "'")
|
264
|
+
str[1..-2]
|
265
|
+
else
|
266
|
+
str
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def active_record_model?(content)
|
271
|
+
content.match?(/class\s+\w+(::\w+)*\s+<\s+(ApplicationRecord|ActiveRecord::Base)/) &&
|
272
|
+
!content.match?(/^\s+self\.abstract_class\s*=\s*true/) &&
|
273
|
+
!readonly_model?(content)
|
274
|
+
end
|
275
|
+
|
276
|
+
def readonly_model?(content)
|
277
|
+
content.match?(/^\s*def\s+readonly\?\s*true\s*end/) ||
|
278
|
+
content.match?(/^\s*def\s+readonly\?\s*$\s*true\s*end/m)
|
279
|
+
end
|
280
|
+
|
281
|
+
def extract_class_name(content)
|
282
|
+
content.match(/class\s+(\w+(::\w+)*)\s+</)[1]
|
283
|
+
end
|
284
|
+
|
285
|
+
def copy_template(from, to)
|
286
|
+
content = File.read(from)
|
287
|
+
FileUtils.mkdir_p(File.dirname(to))
|
288
|
+
File.write(to, content)
|
289
|
+
@files_created << to
|
290
|
+
puts "Created: #{to}"
|
291
|
+
rescue Errno::ENOENT => e
|
292
|
+
puts "Error creating file: #{e.message}"
|
293
|
+
end
|
294
|
+
|
295
|
+
def model_check_content(class_name, pk_info)
|
296
|
+
template = File.read("#{template_dir}/active_record_model_check.rb.erb")
|
297
|
+
ERB.new(template).result(binding)
|
298
|
+
end
|
299
|
+
|
300
|
+
# surely there's a better way to find the gem's root
|
301
|
+
def template_dir
|
302
|
+
File.join(File.expand_path("../..", __dir__), "template")
|
303
|
+
end
|
304
|
+
|
305
|
+
def vcs_message
|
306
|
+
puts
|
307
|
+
puts "Run `git add --all` and `git commit` to checkpoint this setup, then `bundle exec recheck run` to check for the first time."
|
308
|
+
end
|
309
|
+
end # Setup
|
310
|
+
end
|
311
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Recheck
|
2
|
+
class CountStats
|
3
|
+
attr_reader :counts, :queries
|
4
|
+
|
5
|
+
Recheck::RESULT_TYPES.each do |type|
|
6
|
+
define_method(type) do
|
7
|
+
@counts[type]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@counts = RESULT_TYPES.map { [it, 0] }.to_h
|
13
|
+
@queries = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def <<(other)
|
17
|
+
@queries += other.queries
|
18
|
+
@counts.merge!(other.counts) { |type, self_v, other_v| self_v + other_v }
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def all_pass?
|
23
|
+
@counts.slice(*Recheck::ERROR_TYPES).all? { |type, count| count.zero? }
|
24
|
+
end
|
25
|
+
|
26
|
+
def all_zero?
|
27
|
+
@counts.all? { |type, count| count.zero? }
|
28
|
+
end
|
29
|
+
|
30
|
+
def any_errors?
|
31
|
+
!all_pass?
|
32
|
+
end
|
33
|
+
|
34
|
+
def increment(type)
|
35
|
+
if type == :queries
|
36
|
+
@queries += 1
|
37
|
+
elsif !@counts.include? type
|
38
|
+
raise ArgumentError, "Unkown type #{type}"
|
39
|
+
else
|
40
|
+
@counts[type] += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def reached_blanket_failure?
|
45
|
+
pass == 0 && (fail == 20 || exception == 20)
|
46
|
+
end
|
47
|
+
|
48
|
+
def summary
|
49
|
+
"#{queries} #{(queries == 1) ? "query" : "queries"}, " + (
|
50
|
+
[:pass, :fail] +
|
51
|
+
[
|
52
|
+
:exception,
|
53
|
+
:blanket,
|
54
|
+
:no_query_methods,
|
55
|
+
:no_queries,
|
56
|
+
:no_check_methods,
|
57
|
+
:no_checks
|
58
|
+
].filter { |type| @counts[type].nonzero? }
|
59
|
+
).map { |type| "#{@counts[type]} #{type}" }.join(", ")
|
60
|
+
end
|
61
|
+
|
62
|
+
def total
|
63
|
+
@counts.values.sum
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|