constable-rails 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 +88 -0
- data/LICENSE.txt +21 -0
- data/README.md +515 -0
- data/exe/constable +7 -0
- data/lib/constable/case.rb +336 -0
- data/lib/constable/cli.rb +475 -0
- data/lib/constable/cold_case/minitest.rb +342 -0
- data/lib/constable/cold_case/rspec.rb +334 -0
- data/lib/constable/cold_case.rb +280 -0
- data/lib/constable/config.rb +125 -0
- data/lib/constable/coverage.rb +951 -0
- data/lib/constable/diff.rb +212 -0
- data/lib/constable/dsl.rb +833 -0
- data/lib/constable/identity.rb +121 -0
- data/lib/constable/importer/modernizer.rb +860 -0
- data/lib/constable/importer/reopener.rb +468 -0
- data/lib/constable/importer.rb +51 -0
- data/lib/constable/investigation.rb +67 -0
- data/lib/constable/isolation.rb +171 -0
- data/lib/constable/jail.rb +399 -0
- data/lib/constable/log_router.rb +197 -0
- data/lib/constable/matchers.rb +834 -0
- data/lib/constable/order_audit.rb +130 -0
- data/lib/constable/rails_support.rb +213 -0
- data/lib/constable/railtie.rb +36 -0
- data/lib/constable/registry.rb +57 -0
- data/lib/constable/reporter.rb +625 -0
- data/lib/constable/result.rb +149 -0
- data/lib/constable/runner.rb +697 -0
- data/lib/constable/selection.rb +205 -0
- data/lib/constable/storage/adapter.rb +91 -0
- data/lib/constable/storage/mysql_adapter.rb +125 -0
- data/lib/constable/storage/postgres_adapter.rb +125 -0
- data/lib/constable/storage/sqlite_adapter.rb +84 -0
- data/lib/constable/storage.rb +847 -0
- data/lib/constable/version.rb +5 -0
- data/lib/constable/warrants.rb +290 -0
- data/lib/constable-rails.rb +16 -0
- data/lib/constable.rb +151 -0
- data/lib/generators/constable/base.rb +99 -0
- data/lib/generators/constable/channel/channel_generator.rb +20 -0
- data/lib/generators/constable/channel/templates/channel_case.rb.tt +29 -0
- data/lib/generators/constable/controller/controller_generator.rb +25 -0
- data/lib/generators/constable/controller/templates/controller_case.rb.tt +32 -0
- data/lib/generators/constable/generator/generator_generator.rb +31 -0
- data/lib/generators/constable/generator/templates/generator_case.rb.tt +28 -0
- data/lib/generators/constable/helper/helper_generator.rb +23 -0
- data/lib/generators/constable/helper/templates/helper_case.rb.tt +19 -0
- data/lib/generators/constable/import_generator.rb +137 -0
- data/lib/generators/constable/install_generator.rb +188 -0
- data/lib/generators/constable/integration/integration_generator.rb +27 -0
- data/lib/generators/constable/integration/templates/request_case.rb.tt +22 -0
- data/lib/generators/constable/job/job_generator.rb +20 -0
- data/lib/generators/constable/job/templates/job_case.rb.tt +33 -0
- data/lib/generators/constable/mailbox/mailbox_generator.rb +20 -0
- data/lib/generators/constable/mailbox/templates/mailbox_case.rb.tt +26 -0
- data/lib/generators/constable/mailer/mailer_generator.rb +32 -0
- data/lib/generators/constable/mailer/templates/mailer_case.rb.tt +34 -0
- data/lib/generators/constable/mailer/templates/preview.rb.tt +14 -0
- data/lib/generators/constable/model/model_generator.rb +31 -0
- data/lib/generators/constable/model/templates/model_case.rb.tt +37 -0
- data/lib/generators/constable/resource/resource_generator.rb +27 -0
- data/lib/generators/constable/scaffold/scaffold_generator.rb +42 -0
- data/lib/generators/constable/scaffold/templates/api_controller_case.rb.tt +54 -0
- data/lib/generators/constable/scaffold/templates/controller_case.rb.tt +70 -0
- data/lib/generators/constable/scaffold/templates/system_case.rb.tt +53 -0
- data/lib/generators/constable/system/system_generator.rb +20 -0
- data/lib/generators/constable/system/templates/system_case.rb.tt +18 -0
- data/lib/generators/constable/templates/authenticatable.rb.tt +31 -0
- data/lib/generators/constable/templates/case_helper.rb.tt +179 -0
- data/lib/generators/constable/templates/config.yml.tt +67 -0
- data/lib/generators/constable/templates/example_case.rb.tt +56 -0
- data/lib/generators/constable/templates/matchers.rb.tt +36 -0
- data/lib/generators/constable/templates/rubocop.yml.tt +12 -0
- metadata +209 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "ripper"
|
|
5
|
+
|
|
6
|
+
module Constable
|
|
7
|
+
# A test's identity is a content hash of its `investigate` block body, normalized so
|
|
8
|
+
# that formatting, comments and indentation don't affect it. Class name, description
|
|
9
|
+
# and file path are stored alongside purely as a display label.
|
|
10
|
+
#
|
|
11
|
+
# Rename the class, reword the description, move the file -- body untouched, hash
|
|
12
|
+
# untouched, full history carries over. Change what the test actually *does* and the
|
|
13
|
+
# hash changes, so history starts fresh. That is correct, not a limitation.
|
|
14
|
+
module Identity
|
|
15
|
+
LENGTH = 16
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# The stable key for a block of test code.
|
|
20
|
+
def for_block(block)
|
|
21
|
+
digest(normalize(source_for(block).to_s))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def for_source(source)
|
|
25
|
+
digest(normalize(source.to_s))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Cold cases have no `investigate` block to hash, so they're keyed by their file
|
|
29
|
+
# and example description instead. Renaming a cold-case file resets its history --
|
|
30
|
+
# an accepted tradeoff for tests that opted out of native rules.
|
|
31
|
+
def for_cold_case(file, description, root: Constable.root)
|
|
32
|
+
relative = file.to_s.delete_prefix("#{root}/")
|
|
33
|
+
digest("cold:#{relative}:#{description}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def digest(string)
|
|
37
|
+
Digest::SHA256.hexdigest(string)[0, LENGTH]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Extracts the literal source of a block. MRI can hand back the exact character
|
|
41
|
+
# range via the AST; anything else falls back to slicing the file by line numbers.
|
|
42
|
+
def source_for(block)
|
|
43
|
+
return nil unless block.respond_to?(:source_location)
|
|
44
|
+
|
|
45
|
+
ast_source(block) || line_source(block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ast_source(block)
|
|
49
|
+
return nil unless defined?(RubyVM::AbstractSyntaxTree)
|
|
50
|
+
|
|
51
|
+
node = RubyVM::AbstractSyntaxTree.of(block, keep_script_lines: true)
|
|
52
|
+
node&.source
|
|
53
|
+
rescue StandardError, NotImplementedError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def line_source(block)
|
|
58
|
+
file, line = block.source_location
|
|
59
|
+
return nil unless file && line && File.exist?(file)
|
|
60
|
+
|
|
61
|
+
lines = File.readlines(file)
|
|
62
|
+
# Without an AST we can't know where the block ends, so we scan forward balancing
|
|
63
|
+
# do/end and brace depth from the opening line.
|
|
64
|
+
slice = balanced_slice(lines, line - 1)
|
|
65
|
+
slice&.join
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whitespace-normalized, comment-free, wrapper-free form of a block body.
|
|
71
|
+
def normalize(source)
|
|
72
|
+
body = strip_wrapper(source)
|
|
73
|
+
tokens = lex(body)
|
|
74
|
+
return collapse(body) unless tokens
|
|
75
|
+
|
|
76
|
+
tokens.join(" ")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def lex(source)
|
|
80
|
+
lexed = Ripper.lex(source)
|
|
81
|
+
return nil if lexed.nil? || lexed.empty?
|
|
82
|
+
|
|
83
|
+
ignored = %i[on_comment on_sp on_ignored_nl on_nl on_embdoc on_embdoc_beg on_embdoc_end]
|
|
84
|
+
tokens = lexed.reject { |(_pos, type, _tok, _state)| ignored.include?(type) }
|
|
85
|
+
.map { |(_pos, _type, tok, _state)| tok.to_s.strip }
|
|
86
|
+
.reject(&:empty?)
|
|
87
|
+
tokens.empty? ? nil : tokens
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def strip_wrapper(source)
|
|
93
|
+
text = source.to_s.strip
|
|
94
|
+
if text.start_with?("do")
|
|
95
|
+
text = text.sub(/\Ado\b/, "").sub(/\bend\z/, "")
|
|
96
|
+
elsif text.start_with?("{")
|
|
97
|
+
text = text.sub(/\A\{/, "").sub(/\}\z/, "")
|
|
98
|
+
end
|
|
99
|
+
# Block parameters aren't part of what the test does.
|
|
100
|
+
text.sub(/\A\s*\|[^|]*\|/, "").strip
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def collapse(source)
|
|
104
|
+
source.to_s.gsub(/\s+/, " ").strip
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def balanced_slice(lines, start_index)
|
|
108
|
+
openers = /(\bdo\b|\{)/
|
|
109
|
+
depth = 0
|
|
110
|
+
collected = []
|
|
111
|
+
lines[start_index..]&.each do |line|
|
|
112
|
+
collected << line
|
|
113
|
+
stripped = line.sub(/#.*\z/, "")
|
|
114
|
+
depth += stripped.scan(openers).size
|
|
115
|
+
depth -= stripped.scan(/(\bend\b|\})/).size
|
|
116
|
+
return collected if depth <= 0 && collected.size.positive?
|
|
117
|
+
end
|
|
118
|
+
collected.empty? ? nil : collected
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|