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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "generators/constable/base"
4
+
5
+ module Constable
6
+ module Generators
7
+ # Invoked by `rails generate model Post title:string` (and by anything else that ends
8
+ # up at active_record:model -- `rails generate resource`, `rails generate scaffold`).
9
+ #
10
+ # test/cases/models/post_case.rb class PostCase < UnitCase
11
+ class ModelGenerator < Base
12
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
13
+
14
+ # Accepted and ignored, so `rails generate model --fixture` still runs for an app
15
+ # that passes it out of habit. Constable has no fixtures: a witness builds exactly
16
+ # what one investigation needs and throws it away with it, which is the same reason
17
+ # there is no before(:all).
18
+ class_option :fixture, type: :boolean
19
+
20
+ check_class_collision suffix: "Case"
21
+
22
+ def create_case_file
23
+ template "model_case.rb.tt", case_path("models", class_path, "#{file_name}_case.rb")
24
+ end
25
+
26
+ # A factory gem registered as the fixture replacement still gets its turn -- factories
27
+ # are a witness's business, not a fixture's, and Constable has no quarrel with them.
28
+ hook_for :fixture_replacement
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "case_helper"
4
+
5
+ <% module_namespacing do -%>
6
+ # Generated by `rails generate model <%= name %>`.
7
+ #
8
+ # :unit is the fastest tier Constable has, and the one thing it deliberately does not do
9
+ # is wrap each investigation in a transaction that gets rolled back afterwards. So build
10
+ # records here, don't save them -- anything that has to persist belongs in an
11
+ # :integration-tier case, where the rollback exists to clean up after it.
12
+ class <%= class_name %>Case < UnitCase
13
+ # Memoized per investigation and thrown away with it. Name it five times in one test
14
+ # and pay for it once; the next test builds its own.
15
+ witness(:<%= singular_name %>) { <%= new_record_expression %> }
16
+
17
+ <% if case_attributes.any? -%>
18
+ # The honest first assertion, and the one that earns its keep: the class loads, the
19
+ # table is there, and every column this generator just declared reads back. That is
20
+ # what catches a migration written but never run.
21
+ investigate "reads back the attributes it was generated with" do
22
+ <% case_attributes.each do |attribute| -%>
23
+ attest(<%= singular_name %>.<%= attribute.column_name %>).to eq(<%= attribute_value(attribute) %>)
24
+ <% end -%>
25
+ end
26
+
27
+ <% end -%>
28
+ # Then the first real one -- a validation, an association, a method this model owns:
29
+ #
30
+ # investigate "requires a <%= case_attributes.first&.column_name || "name" %>" do
31
+ # <%= singular_name %>.<%= case_attributes.first&.column_name || "name" %> = nil
32
+ #
33
+ # refute <%= singular_name %>.valid?
34
+ # attest(<%= singular_name %>.errors[:<%= case_attributes.first&.column_name || "name" %>]).to include("can't be blank")
35
+ # end
36
+ end
37
+ <% end -%>
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "generators/constable/model/model_generator"
4
+
5
+ module Constable
6
+ module Generators
7
+ # `rails generate resource Post title:string` is a model plus a routed controller, and
8
+ # Rails builds it that way: Rails::Generators::ResourceGenerator inherits from the
9
+ # model generator and hooks the controller separately. So the test-framework side of a
10
+ # resource is the model case -- the controller half arrives through constable:controller,
11
+ # invoked by the resource_controller hook.
12
+ #
13
+ # This subclass exists so that lookup resolves rather than printing
14
+ # "constable:resource [not found]" for anything that asks for the resource generator by
15
+ # name, and so it delegates exactly the way Rails' own does: same superclass, same
16
+ # output, no second copy of the template.
17
+ class ResourceGenerator < ModelGenerator
18
+ # generator_name is "resource", which would send the inherited source_root looking in
19
+ # a templates directory this generator does not have and does not need.
20
+ def self.source_root(path = nil)
21
+ return super if path
22
+
23
+ ModelGenerator.source_root
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "generators/constable/base"
4
+ require "rails/generators/resource_helpers"
5
+
6
+ module Constable
7
+ module Generators
8
+ # Invoked by `rails generate scaffold Post title:string`, via rails:scaffold_controller,
9
+ # which hooks :test_framework as :scaffold.
10
+ #
11
+ # test/cases/controllers/posts_controller_case.rb class PostsControllerCase < IntegrationCase
12
+ # test/cases/system/posts_case.rb class PostsCase < SystemCase
13
+ #
14
+ # The system case is written only when Rails asks for one (`--system-tests=true`),
15
+ # matching test_unit exactly: a browser test that nobody asked for is the slowest
16
+ # possible way to find that out.
17
+ class ScaffoldGenerator < Base
18
+ include ::Rails::Generators::ResourceHelpers
19
+
20
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
21
+
22
+ class_option :api, type: :boolean,
23
+ desc: "Generate cases for an API-only controller"
24
+ class_option :system_tests, type: :string,
25
+ desc: "Generate a system case (set to 'true' to enable)"
26
+
27
+ check_class_collision suffix: "ControllerCase"
28
+
29
+ def create_controller_case
30
+ template options.api? ? "api_controller_case.rb.tt" : "controller_case.rb.tt",
31
+ case_path("controllers", controller_class_path, "#{controller_file_name}_controller_case.rb")
32
+ end
33
+
34
+ def create_system_case
35
+ return if options.api?
36
+ return unless options[:system_tests] == "true"
37
+
38
+ template "system_case.rb.tt", case_path("system", class_path, "#{plural_file_name}_case.rb")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "case_helper"
4
+
5
+ <% module_namespacing do -%>
6
+ # Generated by `rails generate scaffold <%= name %> --api`. One investigation per action,
7
+ # every request and response in JSON -- no form pages, so no new/edit.
8
+ class <%= controller_class_name %>ControllerCase < IntegrationCase
9
+ <% if mountable_engine? -%>
10
+ include Engine.routes.url_helpers
11
+
12
+ <% end -%>
13
+ witness(:valid_attributes) { <%= attributes_literal %> }
14
+ witness(:existing_<%= singular_table_name %>) { <%= class_name %>.create!(valid_attributes) }
15
+
16
+ investigate "index responds successfully" do
17
+ get <%= index_helper(type: :url) %>, as: :json
18
+
19
+ attest(response).to have_http_status(:ok)
20
+ end
21
+
22
+ investigate "create saves a <%= human_name.downcase %>" do
23
+ assert_difference("<%= class_name %>.count", 1) do
24
+ post <%= index_helper(type: :url) %>, params: { <%= singular_table_name %>: valid_attributes }, as: :json
25
+ end
26
+
27
+ attest(response).to have_http_status(:created)
28
+ end
29
+
30
+ investigate "show responds successfully" do
31
+ get <%= show_helper("existing_#{singular_table_name}") %>, as: :json
32
+
33
+ attest(response).to have_http_status(:ok)
34
+ end
35
+
36
+ investigate "update responds successfully" do
37
+ patch <%= show_helper("existing_#{singular_table_name}") %>, params: { <%= singular_table_name %>: valid_attributes }, as: :json
38
+
39
+ attest(response).to have_http_status(:ok)
40
+ end
41
+
42
+ investigate "destroy removes the <%= human_name.downcase %>" do
43
+ # Named before the block on purpose: a witness is built the first time it is used, so
44
+ # creating it inside assert_difference would net out to zero.
45
+ record = existing_<%= singular_table_name %>
46
+
47
+ assert_difference("<%= class_name %>.count", -1) do
48
+ delete <%= show_helper("record") %>, as: :json
49
+ end
50
+
51
+ attest(response).to have_http_status(:no_content)
52
+ end
53
+ end
54
+ <% end -%>
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "case_helper"
4
+
5
+ <% module_namespacing do -%>
6
+ # Generated by `rails generate scaffold <%= name %>`. One investigation per action the
7
+ # scaffold just wrote, which is a floor rather than a suite: the case worth writing is the
8
+ # one about behavior this controller has that no scaffold does.
9
+ class <%= controller_class_name %>ControllerCase < IntegrationCase
10
+ <% if mountable_engine? -%>
11
+ include Engine.routes.url_helpers
12
+
13
+ <% end -%>
14
+ # :integration wraps every investigation in a transaction that is rolled back
15
+ # afterwards, so a witness may persist a record. It is still built fresh for each one --
16
+ # nothing here survives into the next investigation.
17
+ witness(:valid_attributes) { <%= attributes_literal %> }
18
+ witness(:existing_<%= singular_table_name %>) { <%= class_name %>.create!(valid_attributes) }
19
+
20
+ investigate "index responds successfully" do
21
+ get <%= index_helper(type: :url) %>
22
+
23
+ attest(response).to have_http_status(:ok)
24
+ end
25
+
26
+ investigate "new renders the form" do
27
+ get <%= new_helper %>
28
+
29
+ attest(response).to have_http_status(:ok)
30
+ end
31
+
32
+ investigate "create saves a <%= human_name.downcase %> and redirects to it" do
33
+ assert_difference("<%= class_name %>.count", 1) do
34
+ post <%= index_helper(type: :url) %>, params: { <%= singular_table_name %>: valid_attributes }
35
+ end
36
+
37
+ attest(response).to redirect_to(<%= show_helper("#{class_name}.last") %>)
38
+ end
39
+
40
+ investigate "show responds successfully" do
41
+ get <%= show_helper("existing_#{singular_table_name}") %>
42
+
43
+ attest(response).to have_http_status(:ok)
44
+ end
45
+
46
+ investigate "edit renders the form" do
47
+ get <%= edit_helper("existing_#{singular_table_name}") %>
48
+
49
+ attest(response).to have_http_status(:ok)
50
+ end
51
+
52
+ investigate "update redirects to the <%= human_name.downcase %>" do
53
+ patch <%= show_helper("existing_#{singular_table_name}") %>, params: { <%= singular_table_name %>: valid_attributes }
54
+
55
+ attest(response).to redirect_to(<%= show_helper("existing_#{singular_table_name}") %>)
56
+ end
57
+
58
+ investigate "destroy removes the <%= human_name.downcase %> and redirects to the index" do
59
+ # Named before the block on purpose: a witness is built the first time it is used, so
60
+ # creating it inside assert_difference would net out to zero.
61
+ record = existing_<%= singular_table_name %>
62
+
63
+ assert_difference("<%= class_name %>.count", -1) do
64
+ delete <%= show_helper("record") %>
65
+ end
66
+
67
+ attest(response).to redirect_to(<%= index_helper(type: :url) %>)
68
+ end
69
+ end
70
+ <% end -%>
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "case_helper"
4
+
5
+ <% module_namespacing do -%>
6
+ # Generated by `rails generate scaffold <%= name %> --system-tests=true`.
7
+ #
8
+ # System cases drive a real browser, so they are the slowest thing Constable runs and the
9
+ # easiest place in a suite to write a flaky test. Two rules cover most of it: never sleep
10
+ # (Capybara's finders already wait, and a bare sleep is a lint error anyway), and assert
11
+ # on what the page shows rather than on how long it took to show it.
12
+ class <%= class_name.pluralize %>Case < SystemCase
13
+ witness(:valid_attributes) { <%= attributes_literal %> }
14
+ witness(:existing_<%= singular_table_name %>) { <%= class_name %>.create!(valid_attributes) }
15
+
16
+ investigate "visiting the index" do
17
+ visit <%= index_helper(type: :url) %>
18
+
19
+ assert page.has_selector?("h1", text: "<%= human_name.pluralize %>")
20
+ end
21
+
22
+ investigate "creating a <%= human_name.downcase %>" do
23
+ visit <%= index_helper(type: :url) %>
24
+ click_on "New <%= human_name.downcase %>"
25
+
26
+ <% case_attributes.each do |attribute| -%>
27
+ <% if attribute.type == :boolean -%>
28
+ check "<%= attribute.human_name %>"
29
+ <% else -%>
30
+ fill_in "<%= attribute.human_name %>", with: <%= attribute_value(attribute) %>
31
+ <% end -%>
32
+ <% end -%>
33
+ click_on "Create <%= human_name %>"
34
+
35
+ attest(page.text).to include("<%= human_name %> was successfully created")
36
+ end
37
+
38
+ investigate "updating a <%= human_name.downcase %>" do
39
+ visit <%= show_helper("existing_#{singular_table_name}") %>
40
+ click_on "Edit this <%= human_name.downcase %>", match: :first
41
+ click_on "Update <%= human_name %>"
42
+
43
+ attest(page.text).to include("<%= human_name %> was successfully updated")
44
+ end
45
+
46
+ investigate "destroying a <%= human_name.downcase %>" do
47
+ visit <%= show_helper("existing_#{singular_table_name}") %>
48
+ click_on "Destroy this <%= human_name.downcase %>", match: :first
49
+
50
+ attest(page.text).to include("<%= human_name %> was successfully destroyed")
51
+ end
52
+ end
53
+ <% 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 system_test Posts`, which hooks :system_tests as :system.
8
+ #
9
+ # test/cases/system/posts_case.rb class PostsCase < SystemCase
10
+ class SystemGenerator < Base
11
+ check_class_collision suffix: "Case"
12
+
13
+ strips_suffix(/_test\z/i)
14
+
15
+ def create_case_file
16
+ template "system_case.rb.tt", case_path("system", class_path, "#{plural_file_name}_case.rb")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "case_helper"
4
+
5
+ <% module_namespacing do -%>
6
+ class <%= class_name.pluralize %>Case < SystemCase
7
+ # System cases drive a real browser, so they are the slowest thing Constable runs and
8
+ # the easiest place in a suite to write a flaky test. Two rules cover most of it: never
9
+ # sleep (Capybara's finders already wait, and a bare sleep is a lint error anyway), and
10
+ # assert on what the page shows rather than on how long it took to show it.
11
+ #
12
+ # investigate "visiting the index" do
13
+ # visit <%= plural_table_name %>_url
14
+ #
15
+ # assert page.has_selector?("h1", text: "<%= class_name.pluralize %>")
16
+ # end
17
+ end
18
+ <% end -%>
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared behavior is just a module.
4
+ #
5
+ # There is deliberately no shared-examples DSL in Constable. `include` already
6
+ # does that job: it composes, it respects ancestry, it shows up in `.ancestors`,
7
+ # your editor can jump to the definition, and there is no second set of scoping
8
+ # rules to learn on top of Ruby's own.
9
+ #
10
+ # Any case can pick this up directly:
11
+ #
12
+ # class SessionsCase < IntegrationCase
13
+ # include Authenticatable
14
+ #
15
+ # investigate "signs a user in" do
16
+ # sign_in(witnessed_user)
17
+ # attest(response).to redirect_to(root_path)
18
+ # end
19
+ # end
20
+ #
21
+ # Or a whole tier can, by adding `include Authenticatable` to IntegrationCase in
22
+ # test/case_helper.rb.
23
+ #
24
+ # One rule applies here exactly as it does inside a case: a support module must
25
+ # not hold mutable state across tests. Methods and constants, not @@class_vars.
26
+
27
+ module Authenticatable
28
+ def sign_in(user)
29
+ post session_path, params: { email: user.email, password: "password" }
30
+ end
31
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+ # test/case_helper.rb
5
+ #
6
+ # Constable's equivalent of RSpec's spec_helper.rb + rails_helper.rb, or of
7
+ # Minitest's test_helper.rb. `constable test` loads this file once, before any
8
+ # case file -- individual cases never require it themselves.
9
+ #
10
+ # Three things live here:
11
+ #
12
+ # 1. Booting the Rails test environment.
13
+ # 2. The per-tier base classes your cases inherit from.
14
+ # 3. Constable.configure -- configuration that is *code*.
15
+ #
16
+ # Configuration that is merely a *setting* (a number, a flag, a glob) lives in
17
+ # .constable/config.yml instead, so it can be overridden by a CLI flag for one
18
+ # run without editing Ruby.
19
+ # =============================================================================
20
+
21
+ ENV["RAILS_ENV"] ||= "test"
22
+
23
+ require_relative "../config/environment"
24
+ require "constable"
25
+
26
+ # A test suite that can reach production data is a suite that will eventually
27
+ # destroy some. This costs one string comparison per run.
28
+ abort("The Rails environment is running in production mode!") if Rails.env.production?
29
+
30
+ # Loads db/schema.rb into the test database when it has drifted behind your
31
+ # migrations -- the same thing `rails test` does before it runs anything. Without
32
+ # it the first case to touch the database fails with "no such table", which tells
33
+ # you nothing about the test you actually wrote.
34
+ if defined?(ActiveRecord::Migration)
35
+ begin
36
+ ActiveRecord::Migration.maintain_test_schema!
37
+ rescue ActiveRecord::PendingMigrationError => e
38
+ abort("#{e.message}\n\nRun: bin/rails db:migrate RAILS_ENV=test")
39
+ end
40
+ end
41
+
42
+ # -----------------------------------------------------------------------------
43
+ # Tiers are base classes, not magic.
44
+ #
45
+ # A tier says how much of the stack a case needs: :unit skips the database and
46
+ # the request stack entirely and is the fastest thing Constable can run,
47
+ # :integration gets a transactional database plus request helpers, :system
48
+ # additionally drives a browser.
49
+ #
50
+ # Rather than infer that from a file path, declare it once on a base class and
51
+ # let ordinary Ruby inheritance carry it down:
52
+ #
53
+ # class UsersController::CreatesUserCase < IntegrationCase
54
+ #
55
+ # Path-based inference (the `tiers:` section of .constable/config.yml) still
56
+ # works as a fallback for cases that don't inherit from one of these, but
57
+ # inheritance is the recommended pattern -- there is nothing to infer, nothing
58
+ # to surprise you, and no rule to learn that Ruby didn't already teach you.
59
+ #
60
+ # These are plain classes. Anything a whole tier needs belongs here: an
61
+ # ActiveJob test adapter, a factory module, `include Authenticatable` for every
62
+ # integration case.
63
+ #
64
+ # The two Constable::RailsSupport modules below are the bridge to Rails' own
65
+ # testing code. Constable does not reimplement `post users_path` or
66
+ # `click_on "Save"` -- Rails ships both as ordinary modules, and every
67
+ # reimplementation of them would be a slightly wrong one.
68
+ # -----------------------------------------------------------------------------
69
+
70
+ # No database, no request stack, no browser -- just Ruby. The fastest thing
71
+ # Constable can run, and where most of a suite should live.
72
+ class UnitCase < Constable::Case
73
+ tier :unit
74
+ end
75
+
76
+ # get/post/patch/delete, `response`, `follow_redirect!`, cookies, and every URL
77
+ # helper your routes define (`users_path`, `article_url(article)`). Each
78
+ # investigation runs inside a transaction that is rolled back afterwards.
79
+ class IntegrationCase < Constable::Case
80
+ include Constable::RailsSupport::Integration
81
+
82
+ tier :integration
83
+ end
84
+
85
+ # The browser instead of the request stack: Capybara's `visit`/`click_on`/
86
+ # `fill_in`/`page`, its assertions, your URL helpers pointed at the test server,
87
+ # and Rails' own `driven_by`. There is no `response` here -- a system case looks
88
+ # at the rendered page, which is the whole reason to pay for a browser.
89
+ class SystemCase < Constable::Case
90
+ # Capybara is not a Constable dependency. System cases are opt-in, and a suite
91
+ # with no browser tests shouldn't be made to install a browser driver to boot.
92
+ include Constable::RailsSupport::System if defined?(Capybara)
93
+
94
+ tier :system
95
+
96
+ # The default is Selenium driving Chrome, exactly as Rails' own system tests
97
+ # default. Change it here and every system case follows:
98
+ #
99
+ # driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
100
+ end
101
+
102
+ # -----------------------------------------------------------------------------
103
+ # Support files -- shared modules and custom matchers.
104
+ #
105
+ # Same role RSpec's spec/support/**/*.rb plays. Shared behavior *across* files
106
+ # is a plain module you `include`; there is deliberately no shared-examples DSL
107
+ # here, because Ruby's own composition tools already do that job with fewer
108
+ # rules to learn and more flexibility once the reuse stops being simple.
109
+ #
110
+ # Sorted on purpose: Dir[] returns filesystem order, which differs between your
111
+ # laptop and CI. A suite that loads its own support files in an unpredictable
112
+ # order has already lost the argument about determinism.
113
+ # -----------------------------------------------------------------------------
114
+
115
+ Dir[Rails.root.join("test/support/**/*.rb")].sort.each { |f| require f }
116
+
117
+ # -----------------------------------------------------------------------------
118
+ # Code-level configuration.
119
+ # -----------------------------------------------------------------------------
120
+
121
+ Constable.configure do |c|
122
+ # Workers default to `auto` -- processor count minus a little headroom, so the
123
+ # machine stays usable while the suite runs. Pin it only when a CI container
124
+ # lies about its core count.
125
+ # c.parallel_workers = 4
126
+
127
+ # One-time global setup, run once per process before the whole suite. This is
128
+ # for configuring the world -- drivers, adapters, formats -- and deliberately
129
+ # not for creating records that tests then share. See the note at the bottom
130
+ # of this file about why that distinction is the whole ballgame.
131
+ #
132
+ # c.before_suite do
133
+ # Capybara.default_driver = :rack_test
134
+ # Capybara.javascript_driver = :headless_chrome
135
+ # end
136
+ #
137
+ # c.after_suite do
138
+ # SomeExternalHarness.shut_down
139
+ # end
140
+
141
+ # Custom matchers can be defined here too, though test/support/matchers.rb is
142
+ # the better home once there is more than one of them:
143
+ #
144
+ # Constable::Matchers.define(:be_created) { |response| response.status == 201 }
145
+ end
146
+
147
+ # =============================================================================
148
+ # Two things are missing from this file on purpose.
149
+ #
150
+ # 1. There is no before(:all), no setup_once, no one-time fixture hook.
151
+ #
152
+ # A before(:all) block builds state once and hands the same objects to many
153
+ # tests. That is faster right up until one test mutates one of them -- and
154
+ # then you have a bug that appears only in a particular order, on a
155
+ # particular machine, in CI, roughly once a week. The ordering that produced
156
+ # it isn't written down anywhere, so it isn't reproducible, so it doesn't get
157
+ # fixed: it gets a retry, and the retry gets copied to the next test.
158
+ #
159
+ # Constable doesn't have the hook, so it can't have the bug. `briefing` runs
160
+ # before *every* investigation, against a fresh instance, inside a
161
+ # transaction that is rolled back afterwards. When that is genuinely too
162
+ # slow, the fix is a cheaper fixture -- build_stubbed instead of create, one
163
+ # record instead of five -- not state shared across tests.
164
+ #
165
+ # `c.before_suite` above is not a loophole. It runs once per process to
166
+ # configure the world, not to populate it.
167
+ #
168
+ # 2. `witness` memoizes per test, and never per process.
169
+ #
170
+ # witness(:user) { create(:user) } is memoized within a single
171
+ # investigation, so naming `user` five times in one test hits the database
172
+ # once. The moment that investigation finishes the memo is gone, and the
173
+ # next one builds its own.
174
+ #
175
+ # Caching across tests would be faster, and would reintroduce exactly the
176
+ # shared, mutable, order-dependent state that (1) exists to prevent. So the
177
+ # cache is scoped to the one place it is provably safe: inside a single test
178
+ # that owns the object outright.
179
+ # =============================================================================
@@ -0,0 +1,67 @@
1
+ # .constable/config.yml
2
+ #
3
+ # Settings, as opposed to code. Everything here is a number, a flag or a glob;
4
+ # anything that is Ruby -- custom matchers, tier base classes, one-time global
5
+ # setup -- lives in test/case_helper.rb instead.
6
+ #
7
+ # Every key Constable understands is present below at its default value, so this
8
+ # file doubles as the complete reference. Delete anything you haven't changed;
9
+ # the behavior is identical either way. CLI flags win over this file for the
10
+ # duration of a single run.
11
+
12
+ # Glob paths to run as cold cases: original RSpec/Minitest files, driven verbatim
13
+ # through their own real engine, with pass/fail/timing fed into Constable's
14
+ # reporting, flake history and CI gate alongside native cases.
15
+ #
16
+ # This is the zero-file-change adoption route. The alternative is one line per
17
+ # file -- inheriting from Constable::ColdCase::RSpec or ::Minitest -- which is
18
+ # more explicit but touches source. Either way nothing gets rewritten.
19
+ #
20
+ # Every cold-case file is reported as a warning on every run: one per file, not
21
+ # one per test. Not to nag, but so "12 tests not yet under native rules" is never
22
+ # something the suite quietly forgets to mention.
23
+ cold_cases: []
24
+ # cold_cases:
25
+ # - spec/controllers/**/*_spec.rb
26
+ # - test/legacy/**/*_test.rb
27
+
28
+ # The blotter: flake history, the jail docket, warrants.
29
+ #
30
+ # Constable owns this store outright. It is deliberately not the app's own
31
+ # database, for two reasons. Native cases run inside a transaction that gets
32
+ # rolled back, so writing "this test just failed" through that connection would
33
+ # roll the record back too. And :unit-tier runs skip booting the database
34
+ # entirely for speed -- requiring a live server to record a pass would undo that.
35
+ #
36
+ # SQLite in WAL mode handles the real workload (a handful of tables, one row per
37
+ # test per run) comfortably. Point it at Postgres or MySQL only if you genuinely
38
+ # need one queryable store shared across many CI machines -- and always at a
39
+ # separate database from the app's own, never its test connection.
40
+ storage:
41
+ adapter: sqlite # sqlite (default) | postgres | mysql
42
+ path: .constable/constable.sqlite3 # sqlite only
43
+ url: # postgres/mysql only, e.g.
44
+ # postgres://user:pass@host/constable_metadata
45
+
46
+ warrants: false # opt-in flaky detector -- rerun a failure in isolation before believing it
47
+ warrant_retries: 5 # reruns before declaring a warrant (flaky) or a genuine failure
48
+ auto_relink: false # auto-confirm high-confidence rename detection instead of suggesting it
49
+
50
+ parole_period: 10 # consecutive clean runs before a paroled test is auto-released
51
+
52
+ coverage: false # record coverage during test runs
53
+ coverage_threshold: 90 # diff-based -- only lines changed in the current diff are held to it
54
+ coverage_html: false # generate a browsable report by default
55
+
56
+ fail_on_warnings: false # CI: fail the build when the warning count doesn't trend down
57
+
58
+ parallel_workers: auto # or an explicit integer
59
+
60
+ # Fallback path-based tier inference, used only when a case doesn't inherit from
61
+ # a tiered base class. The base classes in test/case_helper.rb are the primary
62
+ # mechanism and always win -- this is here so an un-migrated file still lands in
63
+ # a sensible tier rather than the slowest one.
64
+ tiers:
65
+ unit: "test/cases/models/**/*"
66
+ integration: "test/cases/controllers/**/*"
67
+ system: "test/cases/system/**/*"