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,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/base"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
module Generators
|
|
7
|
+
# Invoked by `rails generate generator Awesome`.
|
|
8
|
+
#
|
|
9
|
+
# test/cases/generators/awesome_generator_case.rb class AwesomeGeneratorCase < UnitCase
|
|
10
|
+
class GeneratorGenerator < Base
|
|
11
|
+
check_class_collision suffix: "GeneratorCase"
|
|
12
|
+
|
|
13
|
+
class_option :namespace, type: :boolean, default: true,
|
|
14
|
+
desc: "Namespace generator under lib/generators/name"
|
|
15
|
+
|
|
16
|
+
def create_case_file
|
|
17
|
+
template "generator_case.rb.tt", case_path("generators", class_path, "#{file_name}_generator_case.rb")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def generator_path
|
|
23
|
+
if options[:namespace]
|
|
24
|
+
File.join("generators", regular_class_path, file_name, "#{file_name}_generator")
|
|
25
|
+
else
|
|
26
|
+
File.join("generators", regular_class_path, "#{file_name}_generator")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
require "<%= generator_path %>"
|
|
5
|
+
|
|
6
|
+
<% module_namespacing do -%>
|
|
7
|
+
class <%= class_name %>GeneratorCase < UnitCase
|
|
8
|
+
# Rails' generator harness is a module, so it composes into a case rather than needing
|
|
9
|
+
# a base class of its own. Wire it up here:
|
|
10
|
+
#
|
|
11
|
+
# include Rails::Generators::Testing::Behaviour
|
|
12
|
+
# include FileUtils
|
|
13
|
+
#
|
|
14
|
+
# self.generator_class = <%= class_name %>Generator
|
|
15
|
+
# self.destination_root = Rails.root.join("tmp/generators")
|
|
16
|
+
#
|
|
17
|
+
# briefing { prepare_destination }
|
|
18
|
+
#
|
|
19
|
+
# A generator is one of the few things whose test is genuinely about files on disk, so
|
|
20
|
+
# assert on what it wrote and on what is inside it:
|
|
21
|
+
#
|
|
22
|
+
# investigate "writes the file it promises" do
|
|
23
|
+
# run_generator ["Post"]
|
|
24
|
+
#
|
|
25
|
+
# attest(File.read(File.join(destination_root, "app/post.rb"))).to include("class Post")
|
|
26
|
+
# end
|
|
27
|
+
end
|
|
28
|
+
<% end -%>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/base"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
module Generators
|
|
7
|
+
# Invoked by `rails generate helper Posts` (and by `rails generate controller`, which
|
|
8
|
+
# hooks the helper generator alongside the test framework).
|
|
9
|
+
#
|
|
10
|
+
# test/cases/helpers/posts_helper_case.rb class PostsHelperCase < UnitCase
|
|
11
|
+
#
|
|
12
|
+
# Rails' own test_unit generator writes nothing here at all. A helper is the cheapest
|
|
13
|
+
# thing in a Rails app to test -- a module, a method, a return value -- so the file is
|
|
14
|
+
# worth having, even when it starts out with the module included and nothing to call.
|
|
15
|
+
class HelperGenerator < Base
|
|
16
|
+
check_class_collision suffix: "HelperCase"
|
|
17
|
+
|
|
18
|
+
def create_case_file
|
|
19
|
+
template "helper_case.rb.tt", case_path("helpers", class_path, "#{file_name}_helper_case.rb")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= class_name %>HelperCase < UnitCase
|
|
7
|
+
# A helper is a plain module, so a helper case is a plain unit case: include it and call
|
|
8
|
+
# the method. No request, no database, no browser -- which is why this is the fastest
|
|
9
|
+
# tier and the one where a helper belongs.
|
|
10
|
+
include <%= class_name %>Helper
|
|
11
|
+
|
|
12
|
+
# `rails generate helper <%= name %>` writes an empty module, so there is nothing to
|
|
13
|
+
# call yet. The first method it gains gets an investigation like this:
|
|
14
|
+
#
|
|
15
|
+
# investigate "formats a badge number" do
|
|
16
|
+
# attest(badge_number(1)).to eq("No. 1")
|
|
17
|
+
# end
|
|
18
|
+
end
|
|
19
|
+
<% end -%>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require "constable"
|
|
5
|
+
|
|
6
|
+
module Constable
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails generate constable:import --from=rspec`
|
|
9
|
+
#
|
|
10
|
+
# A thin front door onto Constable::Importer, for people who reach for
|
|
11
|
+
# `rails generate` before `constable import`. The two are the same operation;
|
|
12
|
+
# this one exists so the install step and the import step are spelled the
|
|
13
|
+
# same way.
|
|
14
|
+
#
|
|
15
|
+
# The default strategy is "reopen": files become Constable::ColdCase::*
|
|
16
|
+
# subclasses (a superclass swap, or a zero-file-change glob in config) and run
|
|
17
|
+
# immediately, verbatim, through their own engine. Nothing is rewritten and
|
|
18
|
+
# nothing can be broken by a bad conversion. The AST rewrite into the native
|
|
19
|
+
# DSL is a separate, opt-in step -- `constable modernize PATH` -- taken one
|
|
20
|
+
# file at a time once the suite is already green under Constable.
|
|
21
|
+
class ImportGenerator < Rails::Generators::Base
|
|
22
|
+
source_root File.expand_path("templates", __dir__)
|
|
23
|
+
|
|
24
|
+
SOURCES = %w[rspec minitest].freeze
|
|
25
|
+
STRATEGIES = %w[reopen modernize].freeze
|
|
26
|
+
|
|
27
|
+
desc <<~DESC
|
|
28
|
+
Imports an existing RSpec or Minitest suite as cold cases. The default "reopen"
|
|
29
|
+
strategy changes no test code: files run verbatim through their own engine, with
|
|
30
|
+
pass/fail/timing merged into Constable's reporting, flake history and CI gate.
|
|
31
|
+
|
|
32
|
+
Run with --dry-run first to see exactly which files would be touched.
|
|
33
|
+
DESC
|
|
34
|
+
|
|
35
|
+
class_option :from, type: :string, default: "rspec",
|
|
36
|
+
desc: "Which suite to import: #{SOURCES.join(" | ")}"
|
|
37
|
+
class_option :strategy, type: :string, default: "reopen",
|
|
38
|
+
desc: "#{STRATEGIES.join(" | ")} -- reopen is verbatim, modernize rewrites"
|
|
39
|
+
class_option :dry_run, type: :boolean, default: false,
|
|
40
|
+
desc: "Report what would happen without writing anything"
|
|
41
|
+
|
|
42
|
+
def import
|
|
43
|
+
validate_options!
|
|
44
|
+
report(invoke_importer)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def validate_options!
|
|
50
|
+
unless SOURCES.include?(options[:from])
|
|
51
|
+
raise Thor::Error, "--from must be one of #{SOURCES.join(", ")} (got #{options[:from].inspect})"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return if STRATEGIES.include?(options[:strategy])
|
|
55
|
+
|
|
56
|
+
raise Thor::Error, "--strategy must be one of #{STRATEGIES.join(", ")} (got #{options[:strategy].inspect})"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def importer_arguments
|
|
60
|
+
{
|
|
61
|
+
from: options[:from].to_sym,
|
|
62
|
+
config: Constable.config,
|
|
63
|
+
dry_run: options[:dry_run],
|
|
64
|
+
strategy: options[:strategy].to_sym
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The importer is built independently of this generator, so the call site
|
|
69
|
+
# checks the contract instead of assuming it. A signature that has drifted
|
|
70
|
+
# should read as "the importer changed shape, here is how" -- not as an
|
|
71
|
+
# ArgumentError from three frames down with no indication of whose fault
|
|
72
|
+
# it is.
|
|
73
|
+
def invoke_importer
|
|
74
|
+
importer = load_importer!
|
|
75
|
+
assert_signature!(importer)
|
|
76
|
+
importer.run(**importer_arguments)
|
|
77
|
+
rescue ArgumentError => e
|
|
78
|
+
raise Thor::Error, <<~MSG.chomp
|
|
79
|
+
Constable::Importer.run rejected the arguments this generator passes
|
|
80
|
+
(#{importer_arguments.keys.map(&:inspect).join(", ")}): #{e.message}
|
|
81
|
+
|
|
82
|
+
The generator and the importer have drifted apart. Fix the call in
|
|
83
|
+
lib/generators/constable/import_generator.rb to match Importer.run.
|
|
84
|
+
MSG
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def load_importer!
|
|
88
|
+
importer = Constable::Importer
|
|
89
|
+
return importer if importer.respond_to?(:run)
|
|
90
|
+
|
|
91
|
+
raise Thor::Error, "Constable::Importer does not respond to .run -- this build of the gem is incomplete."
|
|
92
|
+
rescue LoadError, NameError => e
|
|
93
|
+
raise Thor::Error, "Constable::Importer could not be loaded (#{e.class}: #{e.message})."
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def assert_signature!(importer)
|
|
97
|
+
parameters = importer.method(:run).parameters
|
|
98
|
+
return if parameters.any? { |type, _| type == :keyrest } # **opts swallows anything
|
|
99
|
+
|
|
100
|
+
accepted = parameters.filter_map { |type, name| name if %i[key keyreq].include?(type) }
|
|
101
|
+
missing = importer_arguments.keys - accepted
|
|
102
|
+
required = parameters.filter_map { |type, name| name if type == :keyreq }
|
|
103
|
+
unfilled = required - importer_arguments.keys
|
|
104
|
+
|
|
105
|
+
return if missing.empty? && unfilled.empty?
|
|
106
|
+
|
|
107
|
+
lines = ["Constable::Importer.run has a different signature than this generator expects.",
|
|
108
|
+
" generator passes: #{importer_arguments.keys.map(&:inspect).join(", ")}",
|
|
109
|
+
" importer accepts: #{accepted.map(&:inspect).join(", ")}"]
|
|
110
|
+
lines << " not accepted: #{missing.map(&:inspect).join(", ")}" unless missing.empty?
|
|
111
|
+
lines << " required, unset: #{unfilled.map(&:inspect).join(", ")}" unless unfilled.empty?
|
|
112
|
+
lines << "Fix the call in lib/generators/constable/import_generator.rb to match Importer.run."
|
|
113
|
+
|
|
114
|
+
raise Thor::Error, lines.join("\n")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The importer owns the detail of what it did; this only makes sure the run
|
|
118
|
+
# says something, since a generator that prints nothing reads as a generator
|
|
119
|
+
# that did nothing.
|
|
120
|
+
def report(result)
|
|
121
|
+
return if options[:quiet]
|
|
122
|
+
|
|
123
|
+
say ""
|
|
124
|
+
if options[:dry_run]
|
|
125
|
+
say " Dry run -- nothing was written.", :yellow
|
|
126
|
+
else
|
|
127
|
+
say " Imported #{options[:from]} as cold cases (strategy: #{options[:strategy]}).", :green
|
|
128
|
+
end
|
|
129
|
+
say " #{result}" if result.is_a?(String) && !result.empty?
|
|
130
|
+
say ""
|
|
131
|
+
say " Every cold-case file is reported as a warning on every run -- one per file,"
|
|
132
|
+
say " not one per test -- until it is either modernized or deliberately kept."
|
|
133
|
+
say ""
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rails generator machinery is required *here*, not from lib/constable.rb.
|
|
4
|
+
# `require "constable"` has to work in a process with no Rails app at all --
|
|
5
|
+
# that is the entire point of the :unit tier -- so railties is only ever pulled
|
|
6
|
+
# in by the two files that genuinely cannot exist without it.
|
|
7
|
+
require "rails/generators/base"
|
|
8
|
+
require "constable"
|
|
9
|
+
|
|
10
|
+
module Constable
|
|
11
|
+
module Generators
|
|
12
|
+
# `rails generate constable:install`
|
|
13
|
+
#
|
|
14
|
+
# Writes the six things a Constable suite needs to run, and nothing else:
|
|
15
|
+
#
|
|
16
|
+
# test/case_helper.rb the boot file + per-tier base classes
|
|
17
|
+
# test/support/matchers.rb custom matcher examples
|
|
18
|
+
# test/support/authenticatable.rb the "shared behavior is a module" example
|
|
19
|
+
# test/cases/example_case.rb a worked case so `constable test` does something
|
|
20
|
+
# .constable/config.yml every setting, at its default, as a reference
|
|
21
|
+
# .rubocop.yml the linter, merged into yours if you have one
|
|
22
|
+
#
|
|
23
|
+
# Plus the optional :cold_case Gemfile group, but only when there is actually
|
|
24
|
+
# an RSpec or Minitest suite in the repo to import. Installing the gem into a
|
|
25
|
+
# greenfield app shouldn't add two dependencies for a migration that will
|
|
26
|
+
# never happen.
|
|
27
|
+
class InstallGenerator < Rails::Generators::Base
|
|
28
|
+
source_root File.expand_path("templates", __dir__)
|
|
29
|
+
|
|
30
|
+
desc <<~DESC
|
|
31
|
+
Sets up Constable: test/case_helper.rb with the per-tier base classes, test/support/
|
|
32
|
+
with matcher and shared-module examples, an example case, the full .constable/config.yml
|
|
33
|
+
reference, a .rubocop.yml wired to rubocop-constable, and -- only when this repo already
|
|
34
|
+
has RSpec or Minitest files to import -- the optional :cold_case Gemfile group.
|
|
35
|
+
|
|
36
|
+
--force, --pretend, --quiet and --skip come from Rails and behave as they do everywhere.
|
|
37
|
+
DESC
|
|
38
|
+
|
|
39
|
+
class_option :skip_gemfile, type: :boolean, default: false,
|
|
40
|
+
desc: "Don't append the optional :cold_case group to the Gemfile"
|
|
41
|
+
class_option :skip_rubocop, type: :boolean, default: false,
|
|
42
|
+
desc: "Don't create or modify .rubocop.yml"
|
|
43
|
+
class_option :skip_example, type: :boolean, default: false,
|
|
44
|
+
desc: "Don't write the example case under test/cases/"
|
|
45
|
+
class_option :skip_support, type: :boolean, default: false,
|
|
46
|
+
desc: "Don't write the example files under test/support/"
|
|
47
|
+
|
|
48
|
+
COLD_CASE_GROUP = <<~RUBY
|
|
49
|
+
# Cold cases: your existing RSpec/Minitest files, run verbatim through their own
|
|
50
|
+
# real engine, with results merged into Constable's reporting and CI gate. These
|
|
51
|
+
# two gems are needed only for as long as cold cases exist -- delete the group
|
|
52
|
+
# once the suite is fully modernized and both dependencies drop out with it.
|
|
53
|
+
group :cold_case do
|
|
54
|
+
gem "rspec-rails"
|
|
55
|
+
gem "minitest"
|
|
56
|
+
end
|
|
57
|
+
RUBY
|
|
58
|
+
|
|
59
|
+
RUBOCOP_EXTENSION = "rubocop-constable"
|
|
60
|
+
|
|
61
|
+
def create_case_helper
|
|
62
|
+
template "case_helper.rb.tt", "test/case_helper.rb"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def create_support_files
|
|
66
|
+
return if options[:skip_support]
|
|
67
|
+
|
|
68
|
+
template "matchers.rb.tt", "test/support/matchers.rb"
|
|
69
|
+
template "authenticatable.rb.tt", "test/support/authenticatable.rb"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def create_config
|
|
73
|
+
template "config.yml.tt", ".constable/config.yml"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def create_example_case
|
|
77
|
+
return if options[:skip_example]
|
|
78
|
+
|
|
79
|
+
template "example_case.rb.tt", "test/cases/example_case.rb"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The :cold_case group is appended only when there is something to import,
|
|
83
|
+
# and only once no matter how many times the generator runs. Re-running an
|
|
84
|
+
# installer is normal (a new option, a regenerated helper) and must never
|
|
85
|
+
# leave a Gemfile with the same group in it twice.
|
|
86
|
+
def add_cold_case_gemfile_group
|
|
87
|
+
return if options[:skip_gemfile]
|
|
88
|
+
|
|
89
|
+
unless File.exist?(File.join(destination_root, "Gemfile"))
|
|
90
|
+
say_status :skip, "Gemfile not found -- add the :cold_case group by hand before importing", :yellow
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
unless legacy_suite_present?
|
|
95
|
+
say_status :skip, "Gemfile (no RSpec or Minitest files here to import as cold cases)", :blue
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if gemfile_contents.match?(/^\s*group\s+:cold_case\b/)
|
|
100
|
+
say_status :identical, "Gemfile (:cold_case group already present)", :blue
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
append_to_file "Gemfile", "\n#{COLD_CASE_GROUP}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# An app that already lints has opinions in .rubocop.yml worth more than
|
|
108
|
+
# ours. Merge into it; never clobber it.
|
|
109
|
+
def configure_rubocop
|
|
110
|
+
return if options[:skip_rubocop]
|
|
111
|
+
|
|
112
|
+
path = File.join(destination_root, ".rubocop.yml")
|
|
113
|
+
return template("rubocop.yml.tt", ".rubocop.yml") unless File.exist?(path)
|
|
114
|
+
|
|
115
|
+
existing = File.read(path)
|
|
116
|
+
if existing.include?(RUBOCOP_EXTENSION)
|
|
117
|
+
say_status :identical, ".rubocop.yml (already requires #{RUBOCOP_EXTENSION})", :blue
|
|
118
|
+
return
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
create_file ".rubocop.yml", merge_rubocop(existing), force: true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def print_next_steps
|
|
125
|
+
return if options[:quiet]
|
|
126
|
+
|
|
127
|
+
say ""
|
|
128
|
+
say " Constable is installed.", :green
|
|
129
|
+
say ""
|
|
130
|
+
say " constable test run everything changed since the merge-base"
|
|
131
|
+
say " constable test --full run the whole suite (this is what CI does)"
|
|
132
|
+
say ""
|
|
133
|
+
say " test/case_helper.rb is worth reading before you write a case -- it explains"
|
|
134
|
+
say " the tier base classes, and the two things Constable deliberately doesn't have."
|
|
135
|
+
say ""
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def gemfile_contents
|
|
141
|
+
File.read(File.join(destination_root, "Gemfile"))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# "Is there anything here to import?" -- any RSpec or Minitest file at all.
|
|
145
|
+
# Native Constable cases are named *_case.rb, so the example this generator
|
|
146
|
+
# just wrote can never be mistaken for a legacy suite.
|
|
147
|
+
def legacy_suite_present?
|
|
148
|
+
Dir.glob(File.join(destination_root, "{spec,test}/**/*_{spec,test}.rb")).any?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# A textual merge, not a YAML round trip: parsing and re-emitting someone's
|
|
152
|
+
# .rubocop.yml would silently eat every comment in it, and comments in a
|
|
153
|
+
# lint config are usually the reason a rule is there at all.
|
|
154
|
+
def merge_rubocop(existing)
|
|
155
|
+
lines = existing.lines
|
|
156
|
+
|
|
157
|
+
# `require:` heading an indented list -- append one more item to the end
|
|
158
|
+
# of it, so load order for everything already there is unchanged.
|
|
159
|
+
index = lines.index { |line| line.match?(/\Arequire:[ \t]*(#.*)?$/) }
|
|
160
|
+
if index
|
|
161
|
+
last = index
|
|
162
|
+
indent = " "
|
|
163
|
+
((index + 1)...lines.length).each do |i|
|
|
164
|
+
item = lines[i].match(/\A([ \t]+)-[ \t]/)
|
|
165
|
+
break unless item || lines[i].match?(/\A[ \t]*#/)
|
|
166
|
+
next unless item
|
|
167
|
+
|
|
168
|
+
last = i
|
|
169
|
+
indent = item[1]
|
|
170
|
+
end
|
|
171
|
+
lines.insert(last + 1, "#{indent}- #{RUBOCOP_EXTENSION}\n")
|
|
172
|
+
return lines.join
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# `require: some-extension` on one line -- promote it to a list of both.
|
|
176
|
+
index = lines.index { |line| line.match?(/\Arequire:[ \t]*[^\s#]/) }
|
|
177
|
+
if index
|
|
178
|
+
current = lines[index].sub(/\Arequire:[ \t]*/, "").sub(/[ \t]*#.*\z/m, "").strip
|
|
179
|
+
lines[index] = "require:\n - #{current}\n - #{RUBOCOP_EXTENSION}\n"
|
|
180
|
+
return lines.join
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# No `require:` at all -- append the whole snippet, existing file intact.
|
|
184
|
+
"#{existing.chomp}\n\n#{File.read(File.join(self.class.source_root, "rubocop.yml.tt"))}"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/base"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
module Generators
|
|
7
|
+
# Invoked by `rails generate integration_test Checkout`, which hooks :integration_tool
|
|
8
|
+
# as :integration -- so the command Rails calls integration_test resolves to a
|
|
9
|
+
# generator that has to be called IntegrationGenerator.
|
|
10
|
+
#
|
|
11
|
+
# test/cases/controllers/checkout_case.rb class CheckoutCase < IntegrationCase
|
|
12
|
+
#
|
|
13
|
+
# It lands under test/cases/controllers because that is the directory the `tiers:`
|
|
14
|
+
# globs in Constable::Config::DEFAULTS map to :integration. A request case is exactly
|
|
15
|
+
# what that directory is for -- driving the real stack from the outside, which is the
|
|
16
|
+
# level Constable's own spec example works at.
|
|
17
|
+
class IntegrationGenerator < Base
|
|
18
|
+
check_class_collision suffix: "Case"
|
|
19
|
+
|
|
20
|
+
strips_suffix(/_test\z/i)
|
|
21
|
+
|
|
22
|
+
def create_case_file
|
|
23
|
+
template "request_case.rb.tt", case_path("controllers", class_path, "#{file_name}_case.rb")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= class_name %>Case < IntegrationCase
|
|
7
|
+
# A request case drives the whole stack from the outside -- routes, middleware,
|
|
8
|
+
# controller, view -- which is why it is :integration tier and gets a transaction rolled
|
|
9
|
+
# back around every investigation.
|
|
10
|
+
#
|
|
11
|
+
# The generator does not know which route this covers. Once you do, it reads like this:
|
|
12
|
+
#
|
|
13
|
+
# witness(:<%= singular_name %>) { <%= class_name.singularize %>.create!(name: "MyString") }
|
|
14
|
+
#
|
|
15
|
+
# investigate "GET /<%= plural_file_name %> lists every <%= singular_name %>" do
|
|
16
|
+
# get <%= plural_file_name %>_url
|
|
17
|
+
#
|
|
18
|
+
# attest(response).to have_http_status(:ok)
|
|
19
|
+
# attest(response.body).to include(<%= singular_name %>.name)
|
|
20
|
+
# end
|
|
21
|
+
end
|
|
22
|
+
<% end -%>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/base"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
module Generators
|
|
7
|
+
# Invoked by `rails generate job CleanUp`.
|
|
8
|
+
#
|
|
9
|
+
# test/cases/jobs/clean_up_job_case.rb class CleanUpJobCase < IntegrationCase
|
|
10
|
+
class JobGenerator < Base
|
|
11
|
+
check_class_collision suffix: "JobCase"
|
|
12
|
+
|
|
13
|
+
strips_suffix(/_job\z/i)
|
|
14
|
+
|
|
15
|
+
def create_case_file
|
|
16
|
+
template "job_case.rb.tt", case_path("jobs", class_path, "#{file_name}_job_case.rb")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= class_name %>JobCase < IntegrationCase
|
|
7
|
+
# `rails generate job <%= name %>` writes an empty #perform, so there is nothing to
|
|
8
|
+
# assert on yet. When there is, a job has two halves worth testing and they are two
|
|
9
|
+
# different investigations.
|
|
10
|
+
#
|
|
11
|
+
# What perform actually does -- run it directly, assert on the effect:
|
|
12
|
+
#
|
|
13
|
+
# investigate "marks the record as processed" do
|
|
14
|
+
# record = <%= class_name %>.create!
|
|
15
|
+
#
|
|
16
|
+
# <%= class_name %>Job.perform_now(record)
|
|
17
|
+
#
|
|
18
|
+
# attest(record.reload).to be_processed
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# That it gets enqueued at all -- this one needs ActiveJob's test adapter, which is a
|
|
22
|
+
# line in the IntegrationCase base class in test/case_helper.rb (or here, if only jobs
|
|
23
|
+
# want it):
|
|
24
|
+
#
|
|
25
|
+
# include ActiveJob::TestHelper
|
|
26
|
+
#
|
|
27
|
+
# investigate "goes on the queue it declares" do
|
|
28
|
+
# assert_difference("enqueued_jobs.size", 1) do
|
|
29
|
+
# <%= class_name %>Job.perform_later
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
end
|
|
33
|
+
<% end -%>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/base"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
module Generators
|
|
7
|
+
# Invoked by `rails generate mailbox Forwards`.
|
|
8
|
+
#
|
|
9
|
+
# test/cases/mailboxes/forwards_mailbox_case.rb class ForwardsMailboxCase < IntegrationCase
|
|
10
|
+
class MailboxGenerator < Base
|
|
11
|
+
check_class_collision suffix: "MailboxCase"
|
|
12
|
+
|
|
13
|
+
strips_suffix(/_mailbox\z/i)
|
|
14
|
+
|
|
15
|
+
def create_case_file
|
|
16
|
+
template "mailbox_case.rb.tt", case_path("mailboxes", class_path, "#{file_name}_mailbox_case.rb")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= class_name %>MailboxCase < IntegrationCase
|
|
7
|
+
# ActionMailbox's helpers for building an inbound email are a plain module, so they
|
|
8
|
+
# compose into a case like any other shared behavior. One line, here or in the
|
|
9
|
+
# IntegrationCase base class in test/case_helper.rb:
|
|
10
|
+
#
|
|
11
|
+
# include ActionMailbox::TestHelper
|
|
12
|
+
#
|
|
13
|
+
# Then the investigation worth writing is about what the mailbox did with the message,
|
|
14
|
+
# not that it received one:
|
|
15
|
+
#
|
|
16
|
+
# witness(:inbound) do
|
|
17
|
+
# create_inbound_email_from_mail(to: "<%= file_name %>@example.com", subject: "Hello")
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# investigate "files the message against its sender" do
|
|
21
|
+
# inbound.route
|
|
22
|
+
#
|
|
23
|
+
# attest(inbound.reload).to be_delivered
|
|
24
|
+
# end
|
|
25
|
+
end
|
|
26
|
+
<% end -%>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/base"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
module Generators
|
|
7
|
+
# Invoked by `rails generate mailer Notifier welcome`.
|
|
8
|
+
#
|
|
9
|
+
# test/cases/mailers/notifier_mailer_case.rb class NotifierMailerCase < IntegrationCase
|
|
10
|
+
# test/mailers/previews/notifier_mailer_preview.rb the /rails/mailers preview
|
|
11
|
+
class MailerGenerator < Base
|
|
12
|
+
argument :actions, type: :array, default: [], banner: "method method"
|
|
13
|
+
|
|
14
|
+
strips_suffix(/_mailer\z/i)
|
|
15
|
+
|
|
16
|
+
def check_class_collision
|
|
17
|
+
class_collisions "#{class_name}MailerCase", "#{class_name}MailerPreview"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_case_file
|
|
21
|
+
template "mailer_case.rb.tt", case_path("mailers", class_path, "#{file_name}_mailer_case.rb")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A preview is a development tool, not a case, and ActionMailer looks for it under
|
|
25
|
+
# test/mailers/previews by default. Moving it somewhere more Constable-shaped would
|
|
26
|
+
# buy nothing and break /rails/mailers, so it stays exactly where Rails put it.
|
|
27
|
+
def create_preview_file
|
|
28
|
+
template "preview.rb.tt", File.join("test/mailers/previews", class_path, "#{file_name}_mailer_preview.rb")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= class_name %>MailerCase < IntegrationCase
|
|
7
|
+
<% if actions.empty? -%>
|
|
8
|
+
# `rails generate mailer <%= name %>` ran with no methods, so there is nothing to send
|
|
9
|
+
# yet. When there is, an investigation reads like this:
|
|
10
|
+
#
|
|
11
|
+
# investigate "welcome" do
|
|
12
|
+
# mail = <%= class_name %>Mailer.welcome
|
|
13
|
+
#
|
|
14
|
+
# attest(mail.subject).to eq("Welcome")
|
|
15
|
+
# attest(mail.to).to eq(["to@example.org"])
|
|
16
|
+
# attest(mail.body.encoded).to include("Hi")
|
|
17
|
+
# end
|
|
18
|
+
<% else -%>
|
|
19
|
+
# Rails wrote these headers into the generated views; they are placeholders and every
|
|
20
|
+
# one of these assertions is meant to be rewritten as the mailer becomes real.
|
|
21
|
+
<% actions.each_with_index do |action, index| -%>
|
|
22
|
+
<%= "\n" unless index.zero? -%>
|
|
23
|
+
investigate "<%= action %>" do
|
|
24
|
+
mail = <%= class_name %>Mailer.<%= action %>
|
|
25
|
+
|
|
26
|
+
attest(mail.subject).to eq(<%= action.to_s.humanize.inspect %>)
|
|
27
|
+
attest(mail.to).to eq(["to@example.org"])
|
|
28
|
+
attest(mail.from).to eq(["from@example.com"])
|
|
29
|
+
attest(mail.body.encoded).to include("Hi")
|
|
30
|
+
end
|
|
31
|
+
<% end -%>
|
|
32
|
+
<% end -%>
|
|
33
|
+
end
|
|
34
|
+
<% end -%>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %>_mailer
|
|
5
|
+
class <%= class_name %>MailerPreview < ActionMailer::Preview
|
|
6
|
+
<% actions.each_with_index do |action, index| -%>
|
|
7
|
+
<%= "\n" unless index.zero? -%>
|
|
8
|
+
# Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>_mailer/<%= action %>
|
|
9
|
+
def <%= action %>
|
|
10
|
+
<%= class_name %>Mailer.<%= action %>
|
|
11
|
+
end
|
|
12
|
+
<% end -%>
|
|
13
|
+
end
|
|
14
|
+
<% end -%>
|