├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Appraisals ├── Gemfile ├── HISTORY ├── LICENCE ├── Procfile.support ├── README.md ├── Rakefile ├── bin └── test ├── combustion.gemspec ├── exe └── combust ├── lib ├── combustion.rb └── combustion │ ├── application.rb │ ├── configurations │ ├── action_controller.rb │ ├── action_mailer.rb │ ├── active_record.rb │ └── active_storage.rb │ ├── database.rb │ ├── database │ ├── load_schema.rb │ ├── migrate.rb │ └── reset.rb │ ├── databases │ ├── base.rb │ ├── firebird.rb │ ├── mysql.rb │ ├── oracle.rb │ ├── postgresql.rb │ ├── sql_server.rb │ └── sqlite.rb │ ├── generator.rb │ └── version_gate.rb ├── spec ├── database_spec.rb ├── dummy │ ├── db │ │ └── migrate │ │ │ ├── 20150717075542_create_dummy_test_table.rb │ │ │ └── 20150717075543_create_dummy_test_table_in_another_db.rb │ ├── lib │ │ └── engine.rb │ └── spec │ │ └── internal │ │ ├── app │ │ ├── assets │ │ │ └── config │ │ │ │ └── manifest.js │ │ └── models │ │ │ ├── model.rb │ │ │ └── model_in_another_db.rb │ │ ├── config │ │ ├── database.yml │ │ └── routes.rb │ │ ├── db │ │ └── schema.rb │ │ └── log │ │ └── .gitignore └── spec_helper.rb └── templates ├── config.ru ├── database.yml ├── routes.rb ├── schema.rb └── storage.yml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2', '3.3' ] 13 | bundler: [ '1.17.3', '2.3.12' ] 14 | database: [ 'mysql2', 'postgresql', 'sqlite3', 'trilogy' ] 15 | exclude: 16 | - ruby: '2.6' 17 | bundler: '2.3.12' 18 | - ruby: '2.7' 19 | bundler: '1.17.3' 20 | - ruby: '3.0' 21 | bundler: '1.17.3' 22 | - ruby: '3.1' 23 | bundler: '1.17.3' 24 | - ruby: '3.2' 25 | bundler: '1.17.3' 26 | - ruby: '3.3' 27 | bundler: '1.17.3' 28 | - ruby: '2.6' 29 | database: 'trilogy' 30 | - ruby: '2.7' 31 | database: 'trilogy' 32 | 33 | services: 34 | postgres: 35 | image: postgres:13.5 36 | env: 37 | POSTGRES_USER: root 38 | POSTGRES_PASSWORD: combustion 39 | POSTGRES_DB: combustion 40 | ports: ['5432:5432'] 41 | # needed because the postgres container does not provide a healthcheck 42 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 43 | 44 | mysql: 45 | image: mysql:5.7 46 | env: 47 | MYSQL_ROOT_PASSWORD: combustion 48 | MYSQL_DATABASE: combustion 49 | ports: ['3306:3306'] 50 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 51 | 52 | steps: 53 | - name: Check out code 54 | uses: actions/checkout@v2 55 | - name: Set up ruby 56 | uses: ruby/setup-ruby@v1 57 | with: 58 | ruby-version: ${{ matrix.ruby }} 59 | bundler: ${{ matrix.bundler }} 60 | bundler-cache: true 61 | 62 | - name: Set up Bundler 63 | run: | 64 | export BUNDLER_VERSION=${{ matrix.bundler }} 65 | export BUNDLE_PATH=$PWD/vendor/bundle 66 | 67 | bundle _${{ matrix.bundler }}_ config set path $PWD/$BUNDLE_PATH 68 | bundle _${{ matrix.bundler }}_ install --jobs=4 --retry=3 69 | bundle _${{ matrix.bundler }}_ update 70 | - name: Set up Appraisal 71 | run: bundle exec appraisal update 72 | - name: Test 73 | env: 74 | CI: "true" 75 | DB_ADAPTER: ${{ matrix.database }} 76 | DB_USERNAME: root 77 | DB_PASSWORD: combustion 78 | DB_HOST: 127.0.0.1 79 | run: bundle exec appraisal rspec 80 | 81 | rubocop: 82 | runs-on: ubuntu-latest 83 | 84 | strategy: 85 | fail-fast: false 86 | 87 | steps: 88 | - name: Check out code 89 | uses: actions/checkout@v2 90 | - name: Set up ruby 91 | uses: ruby/setup-ruby@v1 92 | with: 93 | ruby-version: 2.7 94 | bundler: 2.3.12 95 | bundler-cache: true 96 | - name: Rubocop 97 | run: bundle exec rubocop 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .overmind.env 4 | .tool-versions 5 | data/* 6 | Gemfile.lock 7 | gemfiles/*.lock 8 | gemfiles/*.gemfile 9 | pkg/* 10 | .rubocop-*-yml 11 | .ruby-version 12 | .rvmrc 13 | spec/dummy/spec/internal/db/*.sqlite 14 | spec/dummy/spec/internal/combustion 15 | spec/dummy/spec/internal/test_another 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --tty 3 | --order random 4 | --require spec_helper.rb 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - https://gist.githubusercontent.com/pat/ba3b8ffb1901bfe5439b460943b6b019/raw/.rubocop.yml 3 | 4 | AllCops: 5 | Exclude: 6 | - 'gemfiles/*' 7 | - 'vendor/**/*' 8 | 9 | Bundler/DuplicatedGem: 10 | Enabled: false 11 | 12 | Gemspec/RequiredRubyVersion: 13 | Enabled: false 14 | 15 | Layout/DotPosition: 16 | EnforcedStyle: trailing 17 | 18 | Layout/CaseIndentation: 19 | EnforcedStyle: end 20 | 21 | Layout/EndAlignment: 22 | EnforcedStyleAlignWith: variable 23 | 24 | Layout/HeredocIndentation: 25 | Enabled: false 26 | 27 | Metrics/BlockLength: 28 | Exclude: 29 | - 'combustion.gemspec' 30 | - 'spec/**/*_spec.rb' 31 | 32 | Style/ClassAndModuleChildren: 33 | Enabled: false 34 | 35 | Style/Documentation: 36 | Enabled: false 37 | 38 | Style/MultilineTernaryOperator: 39 | Exclude: 40 | - 'spec/dummy/db/migrate/*.rb' 41 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if RUBY_VERSION.to_f < 3.0 4 | appraise "rails-5.0" do 5 | gem "rails", "~> 5.0.2" 6 | gem "mysql2", "~> 0.4.4" 7 | gem "pg", "< 1.0" 8 | end 9 | 10 | appraise "rails-5.1" do 11 | gem "rails", "~> 5.1.0" 12 | gem "mysql2", "~> 0.4.4" 13 | gem "pg", "< 1.0" 14 | end 15 | 16 | appraise "rails-5.2" do 17 | gem "rails", "~> 5.2.0" 18 | gem "mysql2", "~> 0.5.0" 19 | end 20 | end 21 | 22 | if RUBY_VERSION.to_f >= 2.5 && RUBY_VERSION.to_f < 3.1 23 | appraise "rails-6.0" do 24 | gem "rails", "~> 6.0.0" 25 | gem "mysql2", "~> 0.5.0" 26 | gem "sqlite3", "~> 1.4", "< 1.5.0" 27 | gem "activerecord-trilogy-adapter", "~> 3.1" 28 | end 29 | end 30 | 31 | if RUBY_VERSION.to_f >= 2.5 32 | appraise "rails-6.1" do 33 | gem "rails", "~> 6.1.0" 34 | gem "mysql2", "~> 0.5.0" 35 | gem "sqlite3", "~> 1.4", "< 1.6.0" 36 | gem "activerecord-trilogy-adapter", "~> 3.1" 37 | end 38 | end 39 | 40 | if RUBY_VERSION.to_f >= 2.7 41 | appraise "rails-7.0" do 42 | gem "rails", "~> 7.0.1" 43 | gem "mysql2", "~> 0.5.0" 44 | gem "sqlite3", "~> 1.4" 45 | gem "activerecord-trilogy-adapter", "~> 3.1" 46 | end 47 | 48 | appraise "rails-7.1" do 49 | gem "rails", "~> 7.1.2" 50 | gem "mysql2", "~> 0.5.0" 51 | gem "sqlite3", "~> 1.4" 52 | gem "trilogy", "~> 2.7" 53 | end 54 | end 55 | 56 | if RUBY_VERSION.to_f >= 3.1 57 | appraise "rails-edge" do 58 | gem "rails", :git => "https://github.com/rails/rails.git", :branch => "main" 59 | gem "mysql2", "~> 0.5.0" 60 | gem "sqlite3", "~> 1.4" 61 | gem "trilogy", "~> 2.7" 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "http://rubygems.org" 4 | 5 | gemspec 6 | 7 | if RUBY_VERSION.to_f < 3.0 8 | gem "sqlite3", "~> 1.3.13" 9 | else 10 | gem "sqlite3", "~> 1.4" 11 | end 12 | 13 | gem "rubocop", "~> 0.92" 14 | gem "rubocop-packaging", "~> 0.5" 15 | 16 | # Required for testing Rails 6.1 on MRI 3.1+ 17 | gem "net-smtp" if RUBY_VERSION.to_f > 3.0 18 | -------------------------------------------------------------------------------- /HISTORY: -------------------------------------------------------------------------------- 1 | 1.5.0 - July 7th 2024 2 | * Add support for Trilogy database adaptor (Andrew Kane). 3 | * Use load_defaults for Rails 7.2+ configuration support (Andrew Kane). 4 | 5 | 1.4.0 - January 13th 2024 6 | * Drop support for Ruby 2.4, 2.5. 7 | * Add support for Ruby 3.2, 3.3. 8 | * Avoid clear_active_connections! deprecation warning with Rails 7.1. 9 | * Avoid cache_format_version deprecation warning with Rails 7.1 (Matt Jankowski). 10 | * Check versions against direct dependency railties, instead of rails (Andrew Kane). 11 | 12 | 1.3.7 - June 1st 2022 13 | * Fix regression bug, ensure sprockets is loaded with Rails ~> 6.1.1. 14 | 15 | 1.3.6 - May 24th 2022 16 | * Update test matrix to cover Ruby 3.1. 17 | * Refactor version checking for historical Rails features. 18 | * Fix support for edge (7.1+) Rails (Jonathan Rochkind). 19 | 20 | 1.3.5 - November 25th 2021 21 | * Fixed typo when loading Action Cable (Mikhail Nelaev). 22 | 23 | 1.3.4 - November 21st 2021 24 | * Another fix for ActiveRecord settings to ensure Rails 7 configuration is run when needed. 25 | * Don't load Sprockets as part of the default (`:all`) set for Rails 7. 26 | 27 | 1.3.3 - September 22nd 2021 28 | * Fix order of ActiveRecord settings with Rails 7 so they don't run when ActiveRecord isn't required. 29 | 30 | 1.3.2 - September 18th 2021 31 | * Confirmed support for Ruby v3, Rails 6.1, and Rails 7.0.0.alpha2. 32 | 33 | 1.3.1 - October 1st 2020 34 | * Support Rails 6.1's likely database configuration structures. 35 | * Update documentation to reflect latest gem version (Josh Buker). 36 | * Remove git dependency in gemspec (Utkarsh Gupta). 37 | * Use keyword arguments when possible for YAML loading (Bishwa Hang Rai). 38 | 39 | 1.3.0 - April 20th 2020 40 | * Support ActiveJob, ActionCable, ActionText, ActionMailbox. 41 | 42 | 1.2.0 - April 18th 2020 43 | * Support ActiveStorage (Mikko Kokkonen). 44 | 45 | 1.1.2 - November 4th 2019 46 | * Generate an empty asset manifest file for Rails 6. 47 | 48 | 1.1.1 - August 3rd 2019 49 | * Fix for latest Rails 6.0 release candidate. 50 | * Test suite updates (Rails edge, sqlite3, Rubocop) 51 | 52 | 1.1.0 - February 13th 2019 53 | * Rails 6.0 support. 54 | 55 | 1.0.0 - October 3rd 2018 56 | * No functional changes. 57 | * Added documentation about database preparation options. 58 | 59 | 0.9.1 - April 25th 2018 60 | * Fix Rails 5.2 compatibility by not setting the deprecated secret_token configuration option. 61 | 62 | 0.9.0 - March 26th 2018 63 | * Only reset databases that are either custom or the current one (Moritz Winter). 64 | * More consistency around environment handling (rather than presuming it's always the test environment). 65 | 66 | 0.8.0 - February 1st 2018 67 | * Rails 5.2.0 support. 68 | * Requiring digest from std-lib directly. 69 | * Refactoring for cleaner code. 70 | 71 | 0.7.0 - June 20th 2017 72 | * Confirm support for MRI frozen string literals. 73 | * Hide migration output (Michael Grosser). 74 | 75 | 0.6.0 - March 28th 2017 76 | * Optionally disable database reset, schema loading, migrations (Sergey Kucher). 77 | * Allow for multiple test databases (Semyon Pupkov). 78 | 79 | 0.5.5 - July 23rd 2016 80 | * Add PostGIS support (Roland Koch). 81 | * MySQL parameter fix (Mathieu Leduc-Hamel). 82 | 83 | 0.5.4 - January 12th 2016 84 | * Remove silent_stream call for Rails 5.0 compatability (Bryan Ricker). 85 | * Fix duplicate migrations error (Semyon Pupkov). 86 | 87 | 0.5.3 - March 1st 2015 88 | * Use migrations from dependent gems (Korotaev Danil, Меркушин Михаил). 89 | 90 | 0.5.2 - July 18th 2014 91 | * Note MIT licence in gemspec (@ktdreyer). 92 | * Use local create/drop methods when resetting database (Bryan Ricker). 93 | * ERB is now supported in config/database.yml, matching Rails (@patorash). 94 | * The database now is configured before the application is loaded (@patorash). 95 | * Documentation and generated config.ru now specify explicly loading all Rails libraries by default (Pat Allan). 96 | 97 | 0.5.1 - July 25th 2013 98 | * Mass assignment errors raise exceptions instead of just being logged (Pat Allan). 99 | * whilelist_attributes is only set for Rails 3.2 apps (Philip Arndt). 100 | * Support ActiveRecord 3.0.x (Philip Arndt). 101 | * Allow custom application configuration (Pablo Herrero). 102 | 103 | 0.5.0 - May 1st 2013 104 | * whitelist_attributes is now set within configure_for_combustion, as it depends on which Railties are loaded. 105 | * Make sure Rails gems are loaded before the engine's gem (Pablo Herrero). 106 | * Fixed Rails version comparison (Josh Adam). 107 | 108 | 0.4.0 - March 16th 2013 109 | * Don't delete the SQLite test database if it doesn't exist (Michael Gee, Alexander Rozumiy). 110 | * Support for secret_key_base for Rails 4 apps (Philip Arndt). 111 | * eager_load is set to true when using the production environment (Philip Arndt). 112 | * whiny_nils is not set if using Rails 4 (Philip Arndt). 113 | * Mysql2 can raise Mysql::Error with JRuby (Philip Arndt). 114 | * Whitelist attributes typo is fixed (Geoff Hodgson). 115 | * Mass assignment checks are now turned on, so errors are raised. This matches Rails' defaults for test environments (Josh Adams). 116 | * Combustion no longer loads all of the Rails stack by default, just Railties and ActiveSupport. A combustion-rails gem will be created that uses all of Rails by default (Philip Arndt). 117 | * Combustion classes now all define the outer module, so each can be required by themselves, should you desire (Philip Arndt). 118 | 119 | 0.3.3 - December 23rd 2012 120 | * Removing version file - version number can just live in the gemspec. 121 | * Load ActionController and ActionMailer via their Railties, not non-existent Engines (Chris Beer). 122 | * SQL Schema support (Geoff Hodgson). 123 | * Documentation fixes (Philip Arndt, Inge Jørgensen, Erkan Yilmaz). 124 | * Include migrations from the internal app when migrating the database (Warren Seen). 125 | * Correctly drop the test database when using SQLite (Warren Seen). 126 | * Don't attempt to load sprockets for Rails 3.0.x (Alex Rozumey). 127 | 128 | 0.3.2 - March 3rd 2012 129 | * Tentative Rails 3.0 and migrations support. 130 | * Allow for different internal app directory. 131 | 132 | 0.3.1 - September 12th 2011 133 | * Allow for different versions of Capybara (Leo Liang). 134 | * Including Capybara::DSL instead of Capybara. 135 | * Require 3.1.0 or better. 136 | * Allow for JRuby in the database.yml template (Philip Arndt). 137 | 138 | 0.3.0 - September 2nd 2011 139 | * Simple generator to create a pretty minimal internal Rails app. 140 | 141 | 0.2.0 - August 30th 2011 142 | * Documentation. 143 | * Allow developers to choose which Rails modules they wish to be loaded. 144 | * Load Rails routes and Capybara helpers into RSpec examples when appropriate. 145 | 146 | 0.1.1 - August 24th 2011 147 | * Support assets properly by loading ActionView and Sprockets railties. 148 | 149 | 0.1.0 - August 19th 2011 150 | * First release, extracted from Dobro for HyperTiny. 151 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Pat Allan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Procfile.support: -------------------------------------------------------------------------------- 1 | postgres: postgres -D data/postgres -p ${POSTGRES_PORT:-5432} 2 | mysql: $(brew --prefix mysql@5.7)/bin/mysqld --datadir=$(PWD)/data/mysql --port ${MYSQL_PORT:-3306} --socket=mysql.sock 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Combustion 2 | 3 | Combustion is a library to help you test your Rails Engines in a simple and effective manner, instead of creating a full Rails application in your spec or test folder. 4 | 5 | It allows you to write your specs within the context of your engine, using only the parts of a Rails app you need. 6 | 7 | ## Usage 8 | 9 | Get the gem into either your gemspec or your Gemfile, depending on how you manage your engine's dependencies: 10 | 11 | ```ruby 12 | # gemspec 13 | gem.add_development_dependency 'combustion', '~> 1.3' 14 | 15 | # Gemfile 16 | gem 'combustion', '~> 1.3' 17 | ``` 18 | 19 | In your `spec_helper.rb`, get Combustion to set itself up - which has to happen before you introduce `rspec/rails` and - if being used - `capybara/rails`. Here's an example within context: 20 | 21 | ```ruby 22 | require 'bundler' 23 | 24 | Bundler.require :default, :development 25 | 26 | # If you're using all parts of Rails: 27 | Combustion.initialize! :all 28 | # Or, load just what you need: 29 | # Combustion.initialize! :active_record, :action_controller 30 | 31 | require 'rspec/rails' 32 | # If you're using Capybara: 33 | # require 'capybara/rails' 34 | 35 | RSpec.configure do |config| 36 | config.use_transactional_fixtures = true 37 | end 38 | ``` 39 | 40 | Please note that using `:all` as an argument for `Combustion.initialize!` will load all key parts of Rails that are considered essential for that version. For example: ActiveJob is only loaded for Rails 4.2 onwards, and Sprockets is only loaded for Rails 3.1-6.1 (as it is no longer part of the default set in 7.0). 41 | 42 | You'll also want to run the generator that creates a minimal set of files expected by Rails - run this in the directory of your engine: 43 | 44 | ```shell 45 | combust 46 | 47 | # or, if bundling with the git repo: 48 | bundle exec combust 49 | ``` 50 | 51 | Minitest support is considered to be experimental, but it's certainly working [for some](https://github.com/pat/combustion/issues/78). Comments on others' experiences are welcome! 52 | 53 | What Combustion is doing is setting up a Rails application at `spec/internal` - but you only need to add the files within that directory that you're going to use. Read on for some detail about what that involves. 54 | 55 | If you want to use Cucumber, I recommend starting with [these notes in issue #16](https://github.com/pat/combustion/issues/16) from Niklas Cathor. 56 | 57 | ### Configuring a different test app directory 58 | 59 | If you want your app to be located somewhere other than `spec/internal`, then make sure you configure it before you call `Combustion.initialize!`: 60 | 61 | ```ruby 62 | Combustion.path = 'spec/dummy' 63 | Combustion.initialize! :all 64 | ``` 65 | 66 | 67 | ### Configuring which Rails modules should be loaded. 68 | 69 | By default, Combustion doesn't come with any of the Rails stack. You can customise this though - just pass in what you'd like loaded to the `Combustion.initialize!` call: 70 | 71 | ```ruby 72 | Combustion.initialize! :active_record, :action_controller, 73 | :action_view, :sprockets 74 | ``` 75 | 76 | 77 | And then in your engine's Gemfile: 78 | 79 | ```ruby 80 | group :test do 81 | gem 'activerecord' 82 | gem 'actionpack' # action_controller, action_view 83 | gem 'sprockets' 84 | end 85 | ``` 86 | 87 | Make sure to specify the appropriate version that you want to use. 88 | 89 | ActiveSupport and Railties are always loaded, as they're an integral part of Rails. 90 | 91 | ### Using Models and ActiveRecord 92 | 93 | If you're using ActiveRecord, then there are two critical files within your internal Rails app at `spec/internal` that you'll need to modify: 94 | 95 | * config/database.yml 96 | * db/schema.rb 97 | 98 | Both follow the same structure as in any normal Rails application - and the schema file lets you avoid migrations, as it gets run whenever the test suite starts. Here's a quick sample (note that tables are overwritten if they already exist - this is necessary): 99 | 100 | ```ruby 101 | ActiveRecord::Schema.define do 102 | create_table(:pages, :force => true) do |t| 103 | t.string :name 104 | t.text :content 105 | t.timestamps 106 | end 107 | end 108 | ``` 109 | 110 | #### Disabling Database Preparation 111 | 112 | If you are preparing your own database manually or through different processes, you can disable different parts of the setup process by the following flags: `:database_reset`, `:load_schema`, and `:database_migrate`. All default to true. 113 | 114 | ```ruby 115 | Combustion.initialize! :active_record, 116 | :database_reset => false, 117 | :load_schema => false 118 | ``` 119 | 120 | ### Configuring Combustion to initialise the test db from a .sql file instead of schema.rb 121 | 122 | Name the file structure.sql and configure Combustion to use it before initialising: 123 | 124 | ```ruby 125 | Combustion.schema_format = :sql 126 | Combustion.initialize! :all 127 | ``` 128 | 129 | Any models that aren't provided by your engine should be located at `spec/internal/app/models`. 130 | 131 | ### Using ActionController and ActionView 132 | 133 | You'll only need to add controllers and views to your internal Rails app for whatever you're testing that your engine doesn't provide - this may be nothing at all, so perhaps you don't even need `spec/internal/app/views` or `spec/internal/app/controllers` directories. 134 | 135 | However, if you're doing any testing of your engine's controllers or views, then you're going to need routes set up for them - so modify `spec/internal/config/routes.rb` accordingly: 136 | 137 | ```ruby 138 | Rails.application.routes.draw do 139 | resources :pages 140 | end 141 | ``` 142 | 143 | Just like in a standard Rails app, if you have a mounted engine, then its routes are accessible through whatever it has been loaded as. 144 | 145 | ### Customizing Rails application settings 146 | 147 | If you would like to specify any Rails configuration parameter, you can do it without creating any environment file, simply passing a block to Combustion.initialize! like this: 148 | 149 | ```ruby 150 | Combustion.initialize! :all do 151 | config.active_record.whitelist_attributes = false 152 | end 153 | ``` 154 | 155 | Values given through the initialize! block will be set during Rails initialization process, exactly before the corresponding environment file inside `spec/internals/config/enviroments` is loaded (when that file exists), overriding Combustion's defaults. 156 | 157 | Parameters defined in, for instance, `spec/internals/config/environments/test.rb`, would override Combustion's defaults and also config settings passed to initialize!. 158 | 159 | ### Using other Rails-focused libraries 160 | 161 | Be aware that other gems may require parts of Rails when they're loaded, and this could cause some issues with Combustion's own setup. You may need to manage the loading yourself by setting `:require` to false in your Gemfile for the gem in question, and then requiring it manually in your spec_helper. View [issue #33](https://github.com/pat/combustion/issues/33) for an example with FactoryBot. 162 | 163 | ### Environment and Logging 164 | 165 | Your tests will execute within the test environment for the internal Rails app - and so logs are available at `spec/internal/log/test.log`. You should probably create that log directory so Rails doesn't complain. 166 | 167 | ### Rack it up 168 | 169 | Once you've got this set up, you can fire up your test environment quite easily with Rack - a `config.ru` file is provided by the generator. Just run `rackup` and visit [http://localhost:9292](http://localhost:9292). 170 | 171 | ### Get your test on! 172 | 173 | Now you're good to go - you can write specs within your engine's spec directory just like you were testing a full Rails application - models in `spec/models`, controllers in `spec/controllers`. If you bring Capybara into the mix, then the standard helpers from that will be loaded as well. 174 | 175 | ```ruby 176 | require 'spec_helper' 177 | 178 | describe Page do 179 | describe '#valid' do 180 | it 'requires a name' do 181 | # This is just an example. Go write your own tests! 182 | end 183 | end 184 | end 185 | ``` 186 | 187 | 188 | ## Compatibility 189 | 190 | The current test matrix covers MRI 2.4 to 3.1, and Rails 3.1 to 7.0. It will possibly work on older versions and other Ruby implementations as well. 191 | 192 | You can also use Combustion with multiple versions of Rails to test compatibility across them. [Appraisal](https://github.com/thoughtbot/appraisal) is a gem that can help with this, and a good starting reference is the [Thinking Sphinx](https://github.com/pat/thinking-sphinx) test suite, which runs against [multiple versions](https://github.com/pat/thinking-sphinx/blob/master/Appraisals) of Rails. 193 | 194 | ## Limitations and Known Issues 195 | 196 | Combustion is currently written with the expectation it'll be used with RSpec, but others have got it working with [Minitest](https://github.com/pat/combustion/issues/78). I'd love to make this more flexible - if you want to give it a shot before I get around to it, patches are very much welcome. 197 | 198 | I've not tried using this with Cucumber, but it should work in theory without too much hassle. Let me know if I'm wrong! 199 | 200 | ## Contributing 201 | 202 | Please note that this project now has a [Contributor Code of Conduct](http://contributor-covenant.org/version/1/0/0/). By participating in this project you agree to abide by its terms. 203 | 204 | Contributions are very much welcome - but keep in mind the following: 205 | 206 | * Keep patches in a separate branch 207 | * Don't mess with the version or history file. I'll take care of that when the patch is merged in. 208 | 209 | The tests are extremely minimal, and patches to extend the suite are especially welcome. 210 | 211 | ## Credits 212 | 213 | Copyright (c) 2011-2021, Combustion is developed and maintained by Pat Allan, and is released under the open MIT Licence. Many thanks to HyperTiny for encouraging its development, and [all who have contributed patches](https://github.com/pat/combustion/contributors). 214 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | require "rubocop/rake_task" 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | RuboCop::RakeTask.new(:rubocop) 9 | 10 | Rake::Task["default"].clear if Rake::Task.task_defined?("default") 11 | task :default => %i[ rubocop spec ] 12 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o allexport 4 | source .overmind.env 5 | set +o allexport 6 | 7 | DB_ADAPTER=postgresql DB_HOST=127.0.0.1 DB_PORT=${POSTGRES_PORT} bundle exec appraisal rspec 8 | DB_ADAPTER=mysql2 DB_USERNAME=${MYSQL_USERNAME} DB_PASSWORD=${MYSQL_PASSWORD} DB_HOST=127.0.0.1 DB_PORT=${MYSQL_PORT} bundle exec appraisal rspec 9 | DB_ADAPTER=sqlite3 bundle exec appraisal rspec 10 | -------------------------------------------------------------------------------- /combustion.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "combustion" 5 | s.version = "1.5.0" 6 | s.authors = ["Pat Allan"] 7 | s.email = ["pat@freelancing-gods.com"] 8 | s.homepage = "https://github.com/pat/combustion" 9 | s.summary = "Elegant Rails Engine Testing" 10 | s.description = "Test your Rails Engines without needing a full Rails app" 11 | s.license = "MIT" 12 | 13 | s.files = Dir["{exe,lib,templates}/**/*"] + %w[LICENCE README.md] 14 | s.test_files = 15 | Dir["spec/**/*"] + 16 | %w[.rspec Appraisals Gemfile Rakefile] - 17 | %w[ 18 | spec/dummy/spec/internal/log/development.log 19 | spec/dummy/spec/internal/log/test.log 20 | spec/dummy/spec/internal/test 21 | spec/dummy/spec/internal/test_another 22 | ] 23 | s.executables = ["combust"] 24 | s.bindir = "exe" 25 | s.require_paths = ["lib"] 26 | 27 | s.metadata = { 28 | "rubygems_mfa_required" => "true" 29 | } 30 | 31 | s.add_runtime_dependency "activesupport", ">= 3.0.0" 32 | s.add_runtime_dependency "railties", ">= 3.0.0" 33 | s.add_runtime_dependency "thor", ">= 0.14.6" 34 | 35 | s.add_development_dependency "appraisal", "~> 2.3" 36 | s.add_development_dependency "mysql2" 37 | s.add_development_dependency "pg" 38 | s.add_development_dependency "rails" 39 | s.add_development_dependency "rspec" 40 | s.add_development_dependency "rubocop", "~> 0.81.0" 41 | s.add_development_dependency "sqlite3" 42 | end 43 | -------------------------------------------------------------------------------- /exe/combust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- mode: ruby -*- 3 | # frozen_string_literal: true 4 | 5 | require "combustion" 6 | require "combustion/generator" 7 | 8 | Combustion::Generator.start 9 | -------------------------------------------------------------------------------- /lib/combustion.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails" 4 | require "active_support/dependencies" 5 | 6 | require "combustion/version_gate" 7 | 8 | module Combustion 9 | module Configurations 10 | end 11 | 12 | mattr_accessor :path, :schema_format, :setup_environment 13 | 14 | self.path = "/spec/internal" 15 | self.schema_format = :ruby 16 | 17 | MODULES = { 18 | :active_model => "active_model/railtie", 19 | :active_record => "active_record/railtie", 20 | :action_controller => "action_controller/railtie", 21 | :action_mailer => "action_mailer/railtie", 22 | :action_view => "action_view/railtie", 23 | :sprockets => "sprockets/railtie", 24 | :active_job => "active_job/railtie", 25 | :action_cable => "action_cable/engine", 26 | :active_storage => "active_storage/engine", 27 | :action_text => "action_text/engine", 28 | :action_mailbox => "action_mailbox/engine" 29 | }.freeze 30 | 31 | AVAILABLE_MODULES = begin 32 | keys = MODULES.keys 33 | rails_gate = VersionGate.new("railties") 34 | 35 | keys.delete(:sprockets) unless rails_gate.call(">= 3.1", "< 7.0") 36 | keys.delete(:active_job) unless rails_gate.call(">= 4.2") 37 | keys.delete(:action_cable) unless rails_gate.call(">= 5.0") 38 | keys.delete(:active_storage) unless rails_gate.call(">= 5.2") 39 | keys.delete(:action_text) unless rails_gate.call(">= 6.0") 40 | keys.delete(:action_mailbox) unless rails_gate.call(">= 6.0") 41 | 42 | keys 43 | end.freeze 44 | 45 | def self.initialize!(*modules, &block) 46 | self.setup_environment = block if block_given? 47 | 48 | options = modules.extract_options! 49 | modules = AVAILABLE_MODULES if modules == [:all] 50 | modules.each { |mod| require MODULES.fetch(mod, "#{mod}/railtie") } 51 | 52 | Bundler.require :default, Rails.env 53 | 54 | Combustion::Application.configure_for_combustion 55 | include_database modules, options 56 | Combustion::Application.initialize! 57 | include_rspec 58 | end 59 | 60 | def self.include_database(modules, options) 61 | return unless modules.map(&:to_s).include? "active_record" 62 | 63 | Combustion::Application.config.to_prepare do 64 | Combustion::Database.setup(options) 65 | end 66 | end 67 | 68 | def self.include_rspec 69 | return unless defined?(RSpec) && RSpec.respond_to?(:configure) 70 | 71 | RSpec.configure do |config| 72 | include_capybara_into config 73 | 74 | config.include Combustion::Application.routes.url_helpers 75 | if Combustion::Application.routes.respond_to?(:mounted_helpers) 76 | config.include Combustion::Application.routes.mounted_helpers 77 | end 78 | end 79 | end 80 | 81 | def self.include_capybara_into(config) 82 | return unless defined?(Capybara) 83 | 84 | config.include Capybara::RSpecMatchers if defined?(Capybara::RSpecMatchers) 85 | config.include Capybara::DSL if defined?(Capybara::DSL) 86 | return if defined?(Capybara::RSpecMatchers) || defined?(Capybara::DSL) 87 | 88 | config.include Capybara 89 | end 90 | end 91 | 92 | require "combustion/configurations/action_controller" 93 | require "combustion/configurations/action_mailer" 94 | require "combustion/configurations/active_record" 95 | require "combustion/configurations/active_storage" 96 | require "combustion/application" 97 | require "combustion/database" 98 | -------------------------------------------------------------------------------- /lib/combustion/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "securerandom" 4 | require "digest" 5 | Rails.env = ENV["RAILS_ENV"] || "test" 6 | 7 | module Combustion 8 | class Application < Rails::Application 9 | CONFIGURERS = [ 10 | Combustion::Configurations::ActiveRecord, 11 | Combustion::Configurations::ActionController, 12 | Combustion::Configurations::ActionMailer, 13 | Combustion::Configurations::ActiveStorage 14 | ].freeze 15 | 16 | rails_gate = VersionGate.new("railties") 17 | 18 | if rails_gate.call(">= 7.2.0.alpha") 19 | config.load_defaults Rails::VERSION::STRING.to_f 20 | end 21 | 22 | # Core Settings 23 | config.cache_classes = true 24 | config.consider_all_requests_local = true 25 | config.eager_load = Rails.env.production? 26 | 27 | config.secret_key_base = SecureRandom.hex if rails_gate.call(">= 4.0") 28 | config.whiny_nils = true if rails_gate.call("< 4") 29 | if rails_gate.call("< 5.2") 30 | config.secret_token = Digest::SHA1.hexdigest Time.now.to_s 31 | end 32 | if rails_gate.call("~> 7.1.0.alpha") 33 | config.active_support.cache_format_version = 7.1 34 | end 35 | 36 | # ActiveSupport Settings 37 | config.active_support.deprecation = :stderr 38 | 39 | # Some settings we're not sure if we want, so let's not load them by 40 | # default. Instead, wait for this method to be invoked (to get around 41 | # load-order complications). 42 | def self.configure_for_combustion 43 | config.root = File.expand_path File.join(Dir.pwd, Combustion.path) 44 | 45 | CONFIGURERS.each { |configurer| configurer.call config } 46 | 47 | config.assets.enabled = true if defined?(Sprockets) 48 | end 49 | 50 | initializer( 51 | :load_customized_environment_for_combustion, 52 | :before => :load_environment_config, 53 | :group => :all 54 | ) do 55 | next unless Combustion.setup_environment 56 | 57 | Combustion::Application.class_eval(&Combustion.setup_environment) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/combustion/configurations/action_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Configurations::ActionController 4 | def self.call(config) 5 | return unless defined?(ActionController::Railtie) 6 | 7 | config.action_dispatch.show_exceptions = false 8 | config.action_controller.perform_caching = false 9 | config.action_controller.allow_forgery_protection = false 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/combustion/configurations/action_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Configurations::ActionMailer 4 | def self.call(config) 5 | return unless defined?(ActionMailer::Railtie) 6 | 7 | config.action_mailer.delivery_method = :test 8 | config.action_mailer.default_url_options = {:host => "www.example.com"} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/combustion/configurations/active_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Configurations::ActiveRecord 4 | def self.call(config) 5 | return unless defined?(ActiveRecord::Railtie) 6 | 7 | if Combustion::VersionGate.call("activerecord", "~> 7.0.0") 8 | config.active_record.legacy_connection_handling = false 9 | end 10 | 11 | return unless ::ActiveRecord.constants.include?(:MassAssignmentSecurity) 12 | 13 | # Turn on ActiveRecord attribute whitelisting 14 | # This way the dummy app matches new rails apps re: this setting 15 | config.active_record.whitelist_attributes = true 16 | config.active_record.mass_assignment_sanitizer = :strict 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/combustion/configurations/active_storage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Configurations::ActiveStorage 4 | def self.call(config) 5 | return unless defined?(ActiveStorage::Engine) 6 | 7 | config.active_storage.service = :test 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/combustion/database.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Combustion 4 | module Databases 5 | end 6 | 7 | class Database 8 | DEFAULT_OPTIONS = { 9 | :database_reset => true, 10 | :load_schema => true, 11 | :database_migrate => true 12 | }.freeze 13 | 14 | def self.setup(options = {}) 15 | options = DEFAULT_OPTIONS.merge options 16 | 17 | Combustion::Database::Reset.call if options[:database_reset] 18 | Combustion::Database::LoadSchema.call if options[:load_schema] 19 | Combustion::Database::Migrate.call if options[:database_migrate] 20 | end 21 | end 22 | end 23 | 24 | require "combustion/databases/base" 25 | require "combustion/databases/firebird" 26 | require "combustion/databases/mysql" 27 | require "combustion/databases/oracle" 28 | require "combustion/databases/postgresql" 29 | require "combustion/databases/sql_server" 30 | require "combustion/databases/sqlite" 31 | 32 | require "combustion/database/load_schema" 33 | require "combustion/database/migrate" 34 | require "combustion/database/reset" 35 | -------------------------------------------------------------------------------- /lib/combustion/database/load_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Database::LoadSchema 4 | UnknownSchemaFormat = Class.new StandardError 5 | 6 | def self.call 7 | new.call 8 | end 9 | 10 | def call 11 | ActiveRecord::Schema.verbose = false 12 | 13 | case schema_format 14 | when :ruby 15 | load_ruby_schema 16 | when :sql 17 | load_sql_schema 18 | else 19 | raise UnknownSchemaFormat, "Unknown schema format: #{schema_format}" 20 | end 21 | end 22 | 23 | private 24 | 25 | def load_ruby_schema 26 | load Rails.root.join("db", "schema.rb") 27 | end 28 | 29 | def load_sql_schema 30 | ActiveRecord::Base.connection.execute( 31 | File.read(Rails.root.join("db", "structure.sql")) 32 | ) 33 | end 34 | 35 | def schema_format 36 | Combustion.schema_format 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/combustion/database/migrate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Database::Migrate 4 | def self.call 5 | new.call 6 | end 7 | 8 | def call 9 | ar_gate = Combustion::VersionGate.new("activerecord") 10 | 11 | if ar_gate.call(">= 5.2") 12 | migration_context.migrate 13 | elsif ar_gate.call(">= 3.1") 14 | migrator.migrate paths, nil 15 | else 16 | paths.each { |path| migrator.migrate path, nil } 17 | end 18 | end 19 | 20 | private 21 | 22 | def base_migration_paths 23 | if migrator.respond_to?(:migrations_paths) 24 | migrator.migrations_paths 25 | else 26 | Array("db/migrate/") 27 | end 28 | end 29 | 30 | def engine_migration_paths 31 | migration_paths = Rails.application.paths["db/migrate"].to_a 32 | 33 | if engine_paths_exist_in?(migration_paths) 34 | migration_paths 35 | else 36 | base_migration_paths + migration_paths 37 | end 38 | end 39 | 40 | def engine_path 41 | Rails.application.root.sub(::Combustion.path, "") 42 | end 43 | 44 | def engine_paths_exist_in?(paths) 45 | paths.include?(engine_path.join("db/migrate").to_s) 46 | end 47 | 48 | def migration_context 49 | if ActiveRecord::MigrationContext.instance_method(:initialize).arity <= 1 50 | ActiveRecord::MigrationContext.new paths 51 | else 52 | ActiveRecord::MigrationContext.new( 53 | paths, ActiveRecord::Base.connection.schema_migration 54 | ) 55 | end 56 | end 57 | 58 | def migrator 59 | @migrator ||= ActiveRecord::Migrator 60 | end 61 | 62 | def paths 63 | (engine_migration_paths + [File.join(Rails.root, "db/migrate")]).uniq 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/combustion/database/reset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Database::Reset 4 | # https://github.com/ruby/psych/pull/358/files#diff-fcdbfb11714f576f58ba9f866052bc79R322 5 | RUBY_VERSION_WITH_NEW_SAFE_LOAD_METHOD_SIGNATURE = "2.6.0" 6 | 7 | UnsupportedDatabase = Class.new StandardError 8 | 9 | OPERATOR_PATTERNS = { 10 | Combustion::Databases::MySQL => [/mysql/, /trilogy/], 11 | Combustion::Databases::PostgreSQL => [/postgres/, /postgis/], 12 | Combustion::Databases::SQLite => [/sqlite/], 13 | Combustion::Databases::SQLServer => [/sqlserver/], 14 | Combustion::Databases::Oracle => %w[ oci oracle ], 15 | Combustion::Databases::Firebird => %w[ firebird ] 16 | }.freeze 17 | 18 | RAILS_DEFAULT_ENVIRONMENTS = %w[ development production test ].freeze 19 | 20 | def self.call 21 | new.call 22 | end 23 | 24 | def initialize 25 | # TODO: remove when no longer support 2.5.8 26 | if RUBY_VERSION >= RUBY_VERSION_WITH_NEW_SAFE_LOAD_METHOD_SIGNATURE 27 | ActiveRecord::Base.configurations = YAML.safe_load( 28 | ERB.new(database_yaml).result, :aliases => true 29 | ) 30 | else 31 | ActiveRecord::Base.configurations = YAML.safe_load( 32 | ERB.new(database_yaml).result, [], [], true 33 | ) 34 | end 35 | end 36 | 37 | def call 38 | resettable_db_configs.each do |configuration| 39 | adapter = configuration[:adapter] || 40 | configuration[:url].split("://").first 41 | 42 | operator_class(adapter).new(configuration).reset 43 | end 44 | end 45 | 46 | private 47 | 48 | def database_yaml 49 | File.read "#{Rails.root}/config/database.yml" 50 | end 51 | 52 | def operator_class(adapter) 53 | klass = nil 54 | OPERATOR_PATTERNS.each do |operator, keys| 55 | klass = operator if keys.any? { |key| adapter[key] } 56 | end 57 | return klass if klass 58 | 59 | raise UnsupportedDatabase, "Unsupported database type: #{adapter}" 60 | end 61 | 62 | # All database configs except Rails default environments 63 | # that are not currently in use 64 | def resettable_db_configs 65 | if Combustion::VersionGate.call("activerecord", ">= 6.1") 66 | return resettable_db_configs_for_6_1 67 | end 68 | 69 | all_configurations = ActiveRecord::Base.configurations.to_h 70 | unused_environments = RAILS_DEFAULT_ENVIRONMENTS - [Rails.env.to_s] 71 | resettable_environments = all_configurations.keys - unused_environments 72 | 73 | all_configurations. 74 | select { |name| resettable_environments.include?(name) }. 75 | values. 76 | collect(&:with_indifferent_access) 77 | end 78 | 79 | def resettable_db_configs_for_6_1 80 | all_configurations = ActiveRecord::Base.configurations.configurations 81 | unused_environments = RAILS_DEFAULT_ENVIRONMENTS - [Rails.env.to_s] 82 | resettable_environments = all_configurations.collect(&:env_name).uniq - 83 | unused_environments 84 | 85 | all_configurations. 86 | select { |config| resettable_environments.include?(config.env_name) }. 87 | collect(&:configuration_hash) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/combustion/databases/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_support/core_ext/module/delegation" 4 | 5 | class Combustion::Databases::Base 6 | def initialize(configuration) 7 | @configuration = configuration 8 | end 9 | 10 | def reset 11 | drop 12 | create 13 | 14 | establish_connection Rails.env.to_sym 15 | end 16 | 17 | private 18 | 19 | attr_reader :configuration 20 | 21 | delegate :establish_connection, :connection, :to => :base 22 | 23 | def base 24 | ActiveRecord::Base 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/combustion/databases/firebird.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Databases::Firebird < Combustion::Databases::Base 4 | def reset 5 | establish_connection Rails.env.to_sym 6 | connection.recreate_database! 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/combustion/databases/mysql.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Databases::MySQL < Combustion::Databases::Base 4 | ACCESS_DENIED_ERROR = 10_145 5 | 6 | def reset 7 | establish_connection(configuration.merge(:database => nil)) 8 | 9 | super 10 | end 11 | 12 | private 13 | 14 | def charset 15 | configuration[:charset] || ENV["CHARSET"] || "utf8" 16 | end 17 | 18 | def charset_error 19 | return "" unless configuration[:charset] 20 | 21 | "(if you set the charset manually, make sure you have a matching collation)" 22 | end 23 | 24 | def collation 25 | configuration[:collation] || ENV["COLLATION"] || "utf8_unicode_ci" 26 | end 27 | 28 | def create 29 | connection.create_database configuration[:database], creation_options 30 | establish_connection configuration 31 | rescue error_class => error 32 | rescue_create_from error 33 | end 34 | 35 | def create_as_root(error) 36 | establish_connection configuration.merge( 37 | :database => nil, 38 | :username => "root", 39 | :password => request_password(error) 40 | ) 41 | 42 | connection.create_database configuration[:database], creation_options 43 | connection.execute grant_statement 44 | 45 | establish_connection configuration 46 | end 47 | 48 | def creation_options 49 | {:charset => charset, :collation => collation} 50 | end 51 | 52 | def drop 53 | connection.drop_database configuration[:database] 54 | end 55 | 56 | def error_class 57 | if configuration[:adapter][/jdbc/] 58 | # FIXME: After Jdbcmysql gives this class 59 | require "active_record/railties/jdbcmysql_error" 60 | ArJdbcMySQL::Error 61 | elsif configuration[:adapter][/mysql2/] && defined?(Mysql2) 62 | Mysql2::Error 63 | elsif configuration[:adapter][/trilogy/] && defined?(Trilogy) 64 | Trilogy::Error 65 | else 66 | Mysql::Error 67 | end 68 | end 69 | 70 | def grant_statement 71 | <<-SQL 72 | GRANT ALL PRIVILEGES ON #{configuration["database"]}.* 73 | TO '#{configuration[:username]}'@'localhost' 74 | IDENTIFIED BY '#{configuration[:password]}' WITH GRANT OPTION; 75 | SQL 76 | end 77 | 78 | def request_password(error) 79 | print <<-TXT.strip 80 | #{error.error}. 81 | Please provide the root password for your mysql installation 82 | > 83 | TXT 84 | 85 | $stdin.gets.strip 86 | end 87 | 88 | def rescue_create_from(error) 89 | if error.errno == ACCESS_DENIED_ERROR 90 | create_as_root(error) 91 | return 92 | end 93 | 94 | warn <<-TXT 95 | #{error.error} 96 | Couldn't create database for #{configuration.inspect}, charset: #{charset}, collation: #{collation} 97 | #{charset_error} 98 | TXT 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/combustion/databases/oracle.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Databases::Oracle < Combustion::Databases::Base 4 | def reset 5 | establish_connection Rails.env.to_sym 6 | connection.structure_drop.split(";\n\n").each do |ddl| 7 | connection.execute(ddl) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/combustion/databases/postgresql.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Databases::PostgreSQL < Combustion::Databases::Base 4 | def reset 5 | if Combustion::VersionGate.call("activerecord", ">= 7.1.0.alpha") 6 | base.connection_handler.clear_active_connections! 7 | else 8 | base.clear_active_connections! 9 | end 10 | 11 | establish_connection(postgres_configuration) 12 | 13 | super 14 | end 15 | 16 | private 17 | 18 | def create 19 | connection.create_database( 20 | configuration[:database], 21 | configuration.merge(:encoding => encoding) 22 | ) 23 | rescue StandardError => error 24 | warn error, *error.backtrace 25 | warn "Couldn't create database for #{configuration.inspect}" 26 | end 27 | 28 | def drop 29 | connection.drop_database(configuration[:database]) 30 | end 31 | 32 | def encoding 33 | configuration[:encoding] || ENV["CHARSET"] || "utf8" 34 | end 35 | 36 | def postgres_configuration 37 | configuration.merge( 38 | :database => "postgres", 39 | :schema_search_path => schema_search_path 40 | ) 41 | end 42 | 43 | def schema_search_path 44 | configuration[:adapter][/postgis/] ? "public, postgis" : "public" 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/combustion/databases/sql_server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Combustion::Databases::SQLServer < Combustion::Databases::Base 4 | def reset 5 | establish_connection configuration.merge(:database => "master") 6 | connection.recreate_database! configuration[:database] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/combustion/databases/sqlite.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "fileutils" 4 | require "pathname" 5 | 6 | class Combustion::Databases::SQLite < Combustion::Databases::Base 7 | private 8 | 9 | def create 10 | if exists? 11 | warn "#{config[:database]} already exists" 12 | return 13 | end 14 | 15 | establish_connection configuration 16 | connection 17 | rescue StandardError => error 18 | warn error, *error.backtrace 19 | warn "Couldn't create database for #{configuration.inspect}" 20 | end 21 | 22 | def drop 23 | FileUtils.rm_f file if exists? 24 | end 25 | 26 | def exists? 27 | File.exist? file 28 | end 29 | 30 | def file 31 | @file ||= path.absolute? ? path.to_s : File.join(Rails.root, path) 32 | end 33 | 34 | def path 35 | @path ||= Pathname.new configuration[:database] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/combustion/generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "thor/group" 4 | 5 | module Combustion 6 | class Generator < Thor::Group 7 | include Thor::Actions 8 | 9 | def self.source_root 10 | File.expand_path File.join(File.dirname(__FILE__), "..", "..") 11 | end 12 | 13 | def create_directories 14 | empty_directory "spec/internal" 15 | empty_directory "spec/internal/config" 16 | empty_directory "spec/internal/db" 17 | empty_directory "spec/internal/log" 18 | empty_directory "spec/internal/public" 19 | end 20 | 21 | def create_files 22 | template "templates/routes.rb", "spec/internal/config/routes.rb" 23 | template "templates/database.yml", "spec/internal/config/database.yml" 24 | template "templates/schema.rb", "spec/internal/db/schema.rb" 25 | template "templates/config.ru", "config.ru" 26 | template "templates/storage.yml", "spec/internal/config/storage.yml" 27 | 28 | create_file "spec/internal/app/assets/config/manifest.js" 29 | create_file "spec/internal/public/favicon.ico" 30 | create_file "spec/internal/log/.gitignore" do 31 | "*.log" 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/combustion/version_gate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubygems" 4 | 5 | module Combustion 6 | class VersionGate 7 | def self.call(name, *constraints) 8 | new(name).call(*constraints) 9 | end 10 | 11 | def initialize(name) 12 | @name = name 13 | end 14 | 15 | # Using matches_spec? instead of match? because the former returns true 16 | # even when the spec has an appropriate _pre-release_ version. 17 | def call(*constraints) 18 | return false if spec.nil? 19 | 20 | dependency(*constraints).matches_spec?(spec) 21 | end 22 | 23 | private 24 | 25 | attr_reader :name 26 | 27 | def dependency(*constraints) 28 | Gem::Dependency.new(name, *constraints) 29 | end 30 | 31 | def spec 32 | Gem.loaded_specs.fetch(name, nil) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/database_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Combustion::Database do 4 | it "creates dummy table from migration in base database" do 5 | expect(Model.connection.table_exists?("dummy_table")).to eq true 6 | expect(Model.connection.table_exists?("dummy_in_another_db")).to eq false 7 | end 8 | 9 | it "creates another dummy table from another database" do 10 | expect(ModelInAnotherDb.connection.table_exists?("dummy_table")). 11 | to eq false 12 | expect(ModelInAnotherDb.connection.table_exists?("dummy_in_another_db")). 13 | to eq true 14 | end 15 | 16 | it "returns test database for model with default connection" do 17 | if Combustion::VersionGate.call("activerecord", ">= 6.1") 18 | expect(Model.connection_db_config.database).to match(/combustion/) 19 | else 20 | expect(Model.connection_config[:database]).to match(/combustion/) 21 | end 22 | end 23 | 24 | it "returns test_another for model with connection to second database" do 25 | if Combustion::VersionGate.call("activerecord", ">= 6.1") 26 | expect(ModelInAnotherDb.connection_db_config.database). 27 | to match(/test_another/) 28 | else 29 | expect(ModelInAnotherDb.connection_config[:database]). 30 | to match(/test_another/) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20150717075542_create_dummy_test_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | superclass = ActiveRecord::VERSION::MAJOR < 5 ? 4 | ActiveRecord::Migration : ActiveRecord::Migration[4.2] 5 | class CreateDummyTestTable < superclass 6 | def change 7 | create_table "dummy_table" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20150717075543_create_dummy_test_table_in_another_db.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | superclass = ActiveRecord::VERSION::MAJOR < 5 ? 4 | ActiveRecord::Migration : ActiveRecord::Migration[4.2] 5 | class CreateDummyTestTableInAnotherDb < superclass 6 | def change 7 | create_table "dummy_in_another_db" 8 | end 9 | 10 | def connection 11 | ModelInAnotherDb.connection 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/lib/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dummy 4 | class Engine < ::Rails::Engine 5 | initializer :dummy, :before => :load_init_rb do |app| 6 | app.config.paths["db/migrate"].concat(config.paths["db/migrate"].expanded) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/spec/internal/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /spec/dummy/spec/internal/app/models/model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Model < ActiveRecord::Base 4 | self.table_name = "dummy_table" 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/spec/internal/app/models/model_in_another_db.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ModelInAnotherDb < ActiveRecord::Base 4 | self.table_name = "dummy_in_another_db" 5 | 6 | establish_connection :test_another 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/spec/internal/config/database.yml: -------------------------------------------------------------------------------- 1 | test: &defaults 2 | adapter: <%= ENV.fetch("DB_ADAPTER") %> 3 | database: combustion 4 | <% if ENV["DB_HOST"] %> 5 | host: <%= ENV["DB_HOST"] %> 6 | <% end %> 7 | 8 | <% if ENV["DB_USERNAME"] %> 9 | username: <%= ENV["DB_USERNAME"] %> 10 | <% end %> 11 | 12 | <% if ENV["DB_PASSWORD"] %> 13 | password: <%= ENV["DB_PASSWORD"] %> 14 | <% end %> 15 | 16 | <% if ENV["DB_PORT"] %> 17 | port: <%= ENV["DB_PORT"] %> 18 | <% end %> 19 | 20 | 21 | test_another: 22 | <<: *defaults 23 | database: test_another 24 | -------------------------------------------------------------------------------- /spec/dummy/spec/internal/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | # Add your own routes here, or remove this file if you don't have need for it. 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/spec/internal/db/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema.define do 4 | # Set up any tables you need to exist for your test suite that don't belong 5 | # in migrations. 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/spec/internal/log/.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "combustion" 4 | 5 | if Combustion::VersionGate.call("rails", "< 4.1") 6 | require "active_record" 7 | require "active_record/connection_adapters/mysql2_adapter" 8 | 9 | class ActiveRecord::ConnectionAdapters::Mysql2Adapter 10 | NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY" 11 | end 12 | end 13 | 14 | require File.expand_path("dummy/lib/engine.rb", __dir__) 15 | 16 | Dir.chdir(File.expand_path("dummy", __dir__)) do 17 | Combustion.initialize! :all 18 | end 19 | -------------------------------------------------------------------------------- /templates/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubygems" 4 | require "bundler" 5 | 6 | Bundler.require :default, :development 7 | 8 | Combustion.initialize! :all 9 | run Combustion::Application 10 | -------------------------------------------------------------------------------- /templates/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3 3 | database: db/combustion_test.sqlite 4 | -------------------------------------------------------------------------------- /templates/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | # Add your own routes here, or remove this file if you don't have need for it. 5 | end 6 | -------------------------------------------------------------------------------- /templates/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema.define do 4 | # Set up any tables you need to exist for your test suite that don't belong 5 | # in migrations. 6 | end 7 | -------------------------------------------------------------------------------- /templates/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | --------------------------------------------------------------------------------