derail_specs 0.2.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 (73) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +25 -0
  4. data/.rubocop_todo.yml +47 -0
  5. data/.tool-versions +1 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/Gemfile +13 -0
  9. data/Gemfile.lock +122 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +85 -0
  12. data/Rakefile +12 -0
  13. data/bin/console +15 -0
  14. data/bin/setup +8 -0
  15. data/derail_specs.gemspec +34 -0
  16. data/example/.gitignore +23 -0
  17. data/example/.ruby-version +1 -0
  18. data/example/Gemfile +15 -0
  19. data/example/Gemfile.lock +159 -0
  20. data/example/README.md +24 -0
  21. data/example/Rakefile +8 -0
  22. data/example/app/controllers/application_controller.rb +4 -0
  23. data/example/app/models/application_record.rb +5 -0
  24. data/example/app/views/layouts/application.html.erb +15 -0
  25. data/example/bin/bundle +118 -0
  26. data/example/bin/rails +6 -0
  27. data/example/bin/rake +6 -0
  28. data/example/bin/setup +35 -0
  29. data/example/config/application.rb +40 -0
  30. data/example/config/boot.rb +5 -0
  31. data/example/config/credentials.yml.enc +1 -0
  32. data/example/config/database.yml +25 -0
  33. data/example/config/environment.rb +7 -0
  34. data/example/config/environments/development.rb +62 -0
  35. data/example/config/environments/production.rb +98 -0
  36. data/example/config/environments/test.rb +51 -0
  37. data/example/config/initializers/application_controller_renderer.rb +9 -0
  38. data/example/config/initializers/backtrace_silencers.rb +10 -0
  39. data/example/config/initializers/content_security_policy.rb +29 -0
  40. data/example/config/initializers/cookies_serializer.rb +7 -0
  41. data/example/config/initializers/derail_specs.rb +3 -0
  42. data/example/config/initializers/filter_parameter_logging.rb +8 -0
  43. data/example/config/initializers/inflections.rb +17 -0
  44. data/example/config/initializers/mime_types.rb +5 -0
  45. data/example/config/initializers/permissions_policy.rb +12 -0
  46. data/example/config/initializers/wrap_parameters.rb +16 -0
  47. data/example/config/locales/en.yml +33 -0
  48. data/example/config/puma.rb +45 -0
  49. data/example/config/routes.rb +5 -0
  50. data/example/config.ru +8 -0
  51. data/example/public/404.html +67 -0
  52. data/example/public/422.html +67 -0
  53. data/example/public/500.html +66 -0
  54. data/example/public/apple-touch-icon-precomposed.png +0 -0
  55. data/example/public/apple-touch-icon.png +0 -0
  56. data/example/public/favicon.ico +0 -0
  57. data/example/public/robots.txt +1 -0
  58. data/example/tests.sh +4 -0
  59. data/lib/derail_specs/boot.rb +28 -0
  60. data/lib/derail_specs/railtie.rb +7 -0
  61. data/lib/derail_specs/server/app.rb +15 -0
  62. data/lib/derail_specs/server/checker.rb +43 -0
  63. data/lib/derail_specs/server/middleware.rb +67 -0
  64. data/lib/derail_specs/server/puma.rb +32 -0
  65. data/lib/derail_specs/server/timer.rb +20 -0
  66. data/lib/derail_specs/server.rb +117 -0
  67. data/lib/derail_specs/transaction.rb +84 -0
  68. data/lib/derail_specs/version.rb +5 -0
  69. data/lib/derail_specs.rb +24 -0
  70. data/lib/generators/derail_specs/install_generator.rb +16 -0
  71. data/lib/generators/templates/config/initializers/derail_specs.rb +5 -0
  72. data/lib/tasks/derail_specs.rake +5 -0
  73. metadata +145 -0
@@ -0,0 +1,32 @@
1
+ module DerailSpecs
2
+ class Server
3
+ module Puma
4
+ def self.create(app, port, host)
5
+ require "rack/handler/puma"
6
+
7
+ # If we just run the Puma Rack handler it installs signal handlers which prevent us from being able to interrupt tests.
8
+ # Therefore construct and run the Server instance ourselves.
9
+ # Rack::Handler::Puma.run(app, { Host: host, Port: port, Threads: "0:4", workers: 0, daemon: false }.merge(options))
10
+ default_options = { Host: host, Port: port, Threads: "0:4", workers: 0, daemon: false }
11
+ options = default_options # .merge(options)
12
+
13
+ conf = Rack::Handler::Puma.config(app, options)
14
+ conf.clamp
15
+ events = ::Puma::Events.stdio
16
+
17
+ puma_ver = Gem::Version.new(::Puma::Const::PUMA_VERSION)
18
+ require_relative "patches/puma_ssl" if (Gem::Version.new("4.0.0")...Gem::Version.new("4.1.0")).cover? puma_ver
19
+
20
+ events.log "Starting Puma..."
21
+ events.log "* Version #{::Puma::Const::PUMA_VERSION} , codename: #{::Puma::Const::CODE_NAME}"
22
+ events.log "* Min threads: #{conf.options[:min_threads]}, max threads: #{conf.options[:max_threads]}"
23
+
24
+ ::Puma::Server.new(conf.app, events, conf.options).tap do |s|
25
+ s.binder.parse conf.options[:binds], s.events
26
+ s.min_threads = conf.options[:min_threads]
27
+ s.max_threads = conf.options[:max_threads]
28
+ end.run.join
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ module DerailSpecs
2
+ class Server
3
+ class Timer
4
+ def initialize(expire_in)
5
+ @start = current
6
+ @expire_in = expire_in
7
+ end
8
+
9
+ def expired?
10
+ current - @start >= @expire_in
11
+ end
12
+
13
+ private
14
+
15
+ def current
16
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "net/http"
5
+ require "rack"
6
+ require_relative "server/middleware"
7
+ require_relative "server/checker"
8
+ require_relative "server/timer"
9
+ require_relative "server/puma"
10
+ require_relative "server/app"
11
+
12
+ # Tons of this server configuration stuff is copied from:
13
+ # https://github.com/testdouble/cypress-rails
14
+ module DerailSpecs
15
+ class Server
16
+ class << self
17
+ def ports
18
+ @ports ||= {}
19
+ end
20
+ end
21
+
22
+ attr_reader :app, :host, :port
23
+
24
+ def initialize(reportable_errors: [Exception], extra_middleware: [])
25
+ @app = Server::App
26
+ @extra_middleware = extra_middleware
27
+ @server_thread = nil # suppress warnings
28
+ @host = DerailSpecs.configuration.host
29
+ @reportable_errors = reportable_errors
30
+ @port = DerailSpecs.configuration.port
31
+ @port ||= Server.ports[port_key]
32
+ @port ||= find_available_port(host)
33
+ @checker = Checker.new(@host, @port)
34
+ end
35
+
36
+ def reset_error!
37
+ middleware.clear_error
38
+ end
39
+
40
+ def error
41
+ middleware.error
42
+ end
43
+
44
+ def using_ssl?
45
+ @checker.ssl?
46
+ end
47
+
48
+ def responsive?
49
+ return false if @server_thread&.join(0)
50
+
51
+ res = @checker.request { |http| http.get("/__identify__") }
52
+
53
+ return res.body == app.object_id.to_s if res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPRedirection)
54
+ rescue SystemCallError, Net::ReadTimeout, OpenSSL::SSL::SSLError
55
+ false
56
+ end
57
+
58
+ def wait_for_pending_requests
59
+ timer = Timer.new(60)
60
+ while pending_requests?
61
+ raise "Requests did not finish in 60 seconds: #{middleware.pending_requests}" if timer.expired?
62
+
63
+ sleep 0.01
64
+ end
65
+ end
66
+
67
+ def boot
68
+ unless responsive?
69
+ Server.ports[port_key] = port
70
+
71
+ @server_thread = Thread.new do
72
+ Puma.create(middleware, port, host)
73
+ end
74
+
75
+ timer = Timer.new(60)
76
+ until responsive?
77
+ raise "Rack application timed out during boot" if timer.expired?
78
+
79
+ @server_thread.join(0.1)
80
+ end
81
+ end
82
+
83
+ self
84
+ end
85
+
86
+ private
87
+
88
+ def middleware
89
+ @middleware ||= Middleware.new(app, @reportable_errors, @extra_middleware)
90
+ end
91
+
92
+ def port_key
93
+ app.object_id # as opposed to middleware.object_id if multiple instances
94
+ end
95
+
96
+ def pending_requests?
97
+ middleware.pending_requests?
98
+ end
99
+
100
+ def find_available_port(host)
101
+ server = TCPServer.new(host, 0)
102
+ port = server.addr[1]
103
+ server.close
104
+
105
+ # Workaround issue where some platforms (mac, ???) when passed a host
106
+ # of '0.0.0.0' will return a port that is only available on one of the
107
+ # ip addresses that resolves to, but the next binding to that port requires
108
+ # that port to be available on all ips
109
+ server = TCPServer.new(host, port)
110
+ port
111
+ rescue Errno::EADDRINUSE
112
+ retry
113
+ ensure
114
+ server&.close
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,84 @@
1
+ module DerailSpecs
2
+ class Transaction
3
+ def self.instance
4
+ @instance ||= new
5
+ end
6
+
7
+ def self.begin
8
+ instance.begin
9
+ end
10
+
11
+ def begin
12
+ @connections = gather_connections
13
+ @connections.each do |connection|
14
+ connection.begin_transaction joinable: false, _lazy: false
15
+ connection.pool.lock_thread = true
16
+ end
17
+
18
+ # When connections are established in the future, begin a transaction too
19
+ @connection_subscriber = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
20
+ if payload.key?(:spec_name) && (spec_name = payload[:spec_name])
21
+ setup_shared_connection_pool
22
+
23
+ begin
24
+ ActiveRecord::Base.connection_handler.retrieve_connection(spec_name)
25
+ rescue ActiveRecord::ConnectionNotEstablished
26
+ connection = nil
27
+ end
28
+
29
+ if connection && !@connections.include?(connection)
30
+ connection.begin_transaction joinable: false, _lazy: false
31
+ connection.pool.lock_thread = true
32
+ @connections << connection
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def self.rollback
39
+ instance.rollback
40
+ end
41
+
42
+ def rollback
43
+ return unless @connections.present?
44
+
45
+ ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
46
+
47
+ @connections.each do |connection|
48
+ connection.rollback_transaction if connection.transaction_open?
49
+ connection.pool.lock_thread = false
50
+ end
51
+ @connections.clear
52
+
53
+ ActiveRecord::Base.clear_active_connections!
54
+ end
55
+
56
+ def self.reset
57
+ instance.reset
58
+ end
59
+
60
+ def reset
61
+ rollback
62
+ self.begin
63
+ end
64
+
65
+ def gather_connections
66
+ setup_shared_connection_pool
67
+
68
+ ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
69
+ end
70
+
71
+ # Shares the writing connection pool with connections on
72
+ # other handlers.
73
+ #
74
+ # In an application with a primary and replica the test fixtures
75
+ # need to share a connection pool so that the reading connection
76
+ # can see data in the open transaction on the writing connection.
77
+ def setup_shared_connection_pool
78
+ @legacy_saved_pool_configs ||= Hash.new { |hash, key| hash[key] = {} }
79
+ @saved_pool_configs ||= Hash.new { |hash, key| hash[key] = {} }
80
+
81
+ ActiveRecord::TestFixtures.instance_method(:setup_shared_connection_pool).bind_call(self)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DerailSpecs
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "derail_specs/version"
4
+
5
+ module DerailSpecs
6
+ class Error < StandardError; end
7
+
8
+ def self.configuration
9
+ @configuration ||= Struct
10
+ .new(:command, :host, :port, keyword_init: true)
11
+ .new(
12
+ host: '127.0.0.1',
13
+ port: 3001,
14
+ )
15
+ end
16
+
17
+ def self.configure
18
+ yield(configuration)
19
+ end
20
+ end
21
+
22
+ require 'derail_specs/boot'
23
+ require 'derail_specs/transaction'
24
+ require 'derail_specs/railtie'
@@ -0,0 +1,16 @@
1
+ require 'rails/generators'
2
+
3
+ module DerailSpecs
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("../templates", __dir__)
7
+
8
+ def copy_config
9
+ copy_file(
10
+ "config/initializers/derail_specs.rb",
11
+ "config/initializers/derail_specs.rb",
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ DerailSpecs.configure do |config|
2
+ config.command = './tests.sh'
3
+ config.host = '127.0.0.1'
4
+ config.port = 3001
5
+ end
@@ -0,0 +1,5 @@
1
+ namespace :derail_specs do
2
+ task run: :environment do
3
+ DerailSpecs::Boot.new.run
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: derail_specs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Piechowski
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-09-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: puma
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.8.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.8.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 5.2.0
41
+ description:
42
+ email:
43
+ - alex@piechowski.io
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rspec"
49
+ - ".rubocop.yml"
50
+ - ".rubocop_todo.yml"
51
+ - ".tool-versions"
52
+ - CHANGELOG.md
53
+ - CODE_OF_CONDUCT.md
54
+ - Gemfile
55
+ - Gemfile.lock
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - bin/console
60
+ - bin/setup
61
+ - derail_specs.gemspec
62
+ - example/.gitignore
63
+ - example/.ruby-version
64
+ - example/Gemfile
65
+ - example/Gemfile.lock
66
+ - example/README.md
67
+ - example/Rakefile
68
+ - example/app/controllers/application_controller.rb
69
+ - example/app/models/application_record.rb
70
+ - example/app/views/layouts/application.html.erb
71
+ - example/bin/bundle
72
+ - example/bin/rails
73
+ - example/bin/rake
74
+ - example/bin/setup
75
+ - example/config.ru
76
+ - example/config/application.rb
77
+ - example/config/boot.rb
78
+ - example/config/credentials.yml.enc
79
+ - example/config/database.yml
80
+ - example/config/environment.rb
81
+ - example/config/environments/development.rb
82
+ - example/config/environments/production.rb
83
+ - example/config/environments/test.rb
84
+ - example/config/initializers/application_controller_renderer.rb
85
+ - example/config/initializers/backtrace_silencers.rb
86
+ - example/config/initializers/content_security_policy.rb
87
+ - example/config/initializers/cookies_serializer.rb
88
+ - example/config/initializers/derail_specs.rb
89
+ - example/config/initializers/filter_parameter_logging.rb
90
+ - example/config/initializers/inflections.rb
91
+ - example/config/initializers/mime_types.rb
92
+ - example/config/initializers/permissions_policy.rb
93
+ - example/config/initializers/wrap_parameters.rb
94
+ - example/config/locales/en.yml
95
+ - example/config/puma.rb
96
+ - example/config/routes.rb
97
+ - example/public/404.html
98
+ - example/public/422.html
99
+ - example/public/500.html
100
+ - example/public/apple-touch-icon-precomposed.png
101
+ - example/public/apple-touch-icon.png
102
+ - example/public/favicon.ico
103
+ - example/public/robots.txt
104
+ - example/tests.sh
105
+ - lib/derail_specs.rb
106
+ - lib/derail_specs/boot.rb
107
+ - lib/derail_specs/railtie.rb
108
+ - lib/derail_specs/server.rb
109
+ - lib/derail_specs/server/app.rb
110
+ - lib/derail_specs/server/checker.rb
111
+ - lib/derail_specs/server/middleware.rb
112
+ - lib/derail_specs/server/puma.rb
113
+ - lib/derail_specs/server/timer.rb
114
+ - lib/derail_specs/transaction.rb
115
+ - lib/derail_specs/version.rb
116
+ - lib/generators/derail_specs/install_generator.rb
117
+ - lib/generators/templates/config/initializers/derail_specs.rb
118
+ - lib/tasks/derail_specs.rake
119
+ homepage: https://github.com/roshreview/derail_specs
120
+ licenses:
121
+ - MIT
122
+ metadata:
123
+ homepage_uri: https://github.com/roshreview/derail_specs
124
+ source_code_uri: https://github.com/roshreview/derail_specs
125
+ changelog_uri: https://github.com/roshreview/derail_specs/blob/main/CHANGELOG.md
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: 2.6.0
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubygems_version: 3.2.22
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Rails test server for external tests.
145
+ test_files: []