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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +88 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +515 -0
  5. data/exe/constable +7 -0
  6. data/lib/constable/case.rb +336 -0
  7. data/lib/constable/cli.rb +475 -0
  8. data/lib/constable/cold_case/minitest.rb +342 -0
  9. data/lib/constable/cold_case/rspec.rb +334 -0
  10. data/lib/constable/cold_case.rb +280 -0
  11. data/lib/constable/config.rb +125 -0
  12. data/lib/constable/coverage.rb +951 -0
  13. data/lib/constable/diff.rb +212 -0
  14. data/lib/constable/dsl.rb +833 -0
  15. data/lib/constable/identity.rb +121 -0
  16. data/lib/constable/importer/modernizer.rb +860 -0
  17. data/lib/constable/importer/reopener.rb +468 -0
  18. data/lib/constable/importer.rb +51 -0
  19. data/lib/constable/investigation.rb +67 -0
  20. data/lib/constable/isolation.rb +171 -0
  21. data/lib/constable/jail.rb +399 -0
  22. data/lib/constable/log_router.rb +197 -0
  23. data/lib/constable/matchers.rb +834 -0
  24. data/lib/constable/order_audit.rb +130 -0
  25. data/lib/constable/rails_support.rb +213 -0
  26. data/lib/constable/railtie.rb +36 -0
  27. data/lib/constable/registry.rb +57 -0
  28. data/lib/constable/reporter.rb +625 -0
  29. data/lib/constable/result.rb +149 -0
  30. data/lib/constable/runner.rb +697 -0
  31. data/lib/constable/selection.rb +205 -0
  32. data/lib/constable/storage/adapter.rb +91 -0
  33. data/lib/constable/storage/mysql_adapter.rb +125 -0
  34. data/lib/constable/storage/postgres_adapter.rb +125 -0
  35. data/lib/constable/storage/sqlite_adapter.rb +84 -0
  36. data/lib/constable/storage.rb +847 -0
  37. data/lib/constable/version.rb +5 -0
  38. data/lib/constable/warrants.rb +290 -0
  39. data/lib/constable-rails.rb +16 -0
  40. data/lib/constable.rb +151 -0
  41. data/lib/generators/constable/base.rb +99 -0
  42. data/lib/generators/constable/channel/channel_generator.rb +20 -0
  43. data/lib/generators/constable/channel/templates/channel_case.rb.tt +29 -0
  44. data/lib/generators/constable/controller/controller_generator.rb +25 -0
  45. data/lib/generators/constable/controller/templates/controller_case.rb.tt +32 -0
  46. data/lib/generators/constable/generator/generator_generator.rb +31 -0
  47. data/lib/generators/constable/generator/templates/generator_case.rb.tt +28 -0
  48. data/lib/generators/constable/helper/helper_generator.rb +23 -0
  49. data/lib/generators/constable/helper/templates/helper_case.rb.tt +19 -0
  50. data/lib/generators/constable/import_generator.rb +137 -0
  51. data/lib/generators/constable/install_generator.rb +188 -0
  52. data/lib/generators/constable/integration/integration_generator.rb +27 -0
  53. data/lib/generators/constable/integration/templates/request_case.rb.tt +22 -0
  54. data/lib/generators/constable/job/job_generator.rb +20 -0
  55. data/lib/generators/constable/job/templates/job_case.rb.tt +33 -0
  56. data/lib/generators/constable/mailbox/mailbox_generator.rb +20 -0
  57. data/lib/generators/constable/mailbox/templates/mailbox_case.rb.tt +26 -0
  58. data/lib/generators/constable/mailer/mailer_generator.rb +32 -0
  59. data/lib/generators/constable/mailer/templates/mailer_case.rb.tt +34 -0
  60. data/lib/generators/constable/mailer/templates/preview.rb.tt +14 -0
  61. data/lib/generators/constable/model/model_generator.rb +31 -0
  62. data/lib/generators/constable/model/templates/model_case.rb.tt +37 -0
  63. data/lib/generators/constable/resource/resource_generator.rb +27 -0
  64. data/lib/generators/constable/scaffold/scaffold_generator.rb +42 -0
  65. data/lib/generators/constable/scaffold/templates/api_controller_case.rb.tt +54 -0
  66. data/lib/generators/constable/scaffold/templates/controller_case.rb.tt +70 -0
  67. data/lib/generators/constable/scaffold/templates/system_case.rb.tt +53 -0
  68. data/lib/generators/constable/system/system_generator.rb +20 -0
  69. data/lib/generators/constable/system/templates/system_case.rb.tt +18 -0
  70. data/lib/generators/constable/templates/authenticatable.rb.tt +31 -0
  71. data/lib/generators/constable/templates/case_helper.rb.tt +179 -0
  72. data/lib/generators/constable/templates/config.yml.tt +67 -0
  73. data/lib/generators/constable/templates/example_case.rb.tt +56 -0
  74. data/lib/generators/constable/templates/matchers.rb.tt +36 -0
  75. data/lib/generators/constable/templates/rubocop.yml.tt +12 -0
  76. 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