From c8f98899af944b091350790032732bbea09f63b8 Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Sun, 30 Jun 2024 12:15:01 -0400 Subject: [PATCH] Real testing --- .rspec | 1 - Appraisals | 19 ++++ CHANGELOG.md | 5 + Gemfile | 12 +- README.md | 106 ++++++++++++------ Rakefile | 18 ++- bin/appraise | 24 ++++ bin/lib.sh | 65 +++++++++++ bin/run | 18 +++ bin/test | 29 +++++ bin/testall | 38 +++++++ bin/uninstall | 10 ++ compose/Dockerfile.ruby-3.0 | 15 +++ compose/Dockerfile.ruby-3.1 | 15 +++ compose/Dockerfile.ruby-3.2 | 15 +++ compose/Dockerfile.ruby-3.3 | 15 +++ docker-compose.yml | 39 +++++++ gemfiles/ar_6.1.gemfile | 15 +++ gemfiles/ar_6.1.gemfile.lock | 55 +++++++++ gemfiles/ar_7.0.gemfile | 15 +++ gemfiles/ar_7.0.gemfile.lock | 53 +++++++++ gemfiles/ar_7.1.gemfile | 15 +++ gemfiles/ar_7.1.gemfile.lock | 66 +++++++++++ gemfiles/ar_7.2.gemfile | 15 +++ gemfiles/ar_7.2.gemfile.lock | 64 +++++++++++ lib/otr-activerecord.rb | 12 +- lib/otr-activerecord/activerecord.rb | 65 +++++------ lib/otr-activerecord/compatibility_4.rb | 34 ------ lib/otr-activerecord/compatibility_7.rb | 11 -- lib/otr-activerecord/defaults.rb | 13 --- .../middleware/connection_management.rb | 8 +- .../middleware/query_cache.rb | 25 +---- .../{compatibility_6.rb => shim/v6.rb} | 8 +- .../{compatibility_5.rb => shim/v7.rb} | 12 +- lib/tasks/otr-activerecord.rake | 8 +- otr-activerecord.gemspec | 5 +- spec/otr-activerecord/activerecord_spec.rb | 27 ----- spec/spec_helper.rb | 7 -- test/configure_test.rb | 82 ++++++++++++++ test/connection_management_test.rb | 80 +++++++++++++ {spec => test}/fixtures/multi.yml | 0 {spec => test}/fixtures/simple.yml | 0 test/matrix | 19 ++++ test/query_cache_test.rb | 33 ++++++ test/rake_test.rb | 61 ++++++++++ test/support/active_record_connection.rb | 7 ++ test/support/active_record_models.rb | 7 ++ test/support/active_record_schema.rb | 19 ++++ test/support/test_app.rb | 100 +++++++++++++++++ test/test_helper.rb | 5 + 50 files changed, 1175 insertions(+), 215 deletions(-) delete mode 100644 .rspec create mode 100644 Appraisals create mode 100755 bin/appraise create mode 100644 bin/lib.sh create mode 100755 bin/run create mode 100755 bin/test create mode 100755 bin/testall create mode 100755 bin/uninstall create mode 100644 compose/Dockerfile.ruby-3.0 create mode 100644 compose/Dockerfile.ruby-3.1 create mode 100644 compose/Dockerfile.ruby-3.2 create mode 100644 compose/Dockerfile.ruby-3.3 create mode 100644 docker-compose.yml create mode 100644 gemfiles/ar_6.1.gemfile create mode 100644 gemfiles/ar_6.1.gemfile.lock create mode 100644 gemfiles/ar_7.0.gemfile create mode 100644 gemfiles/ar_7.0.gemfile.lock create mode 100644 gemfiles/ar_7.1.gemfile create mode 100644 gemfiles/ar_7.1.gemfile.lock create mode 100644 gemfiles/ar_7.2.gemfile create mode 100644 gemfiles/ar_7.2.gemfile.lock delete mode 100644 lib/otr-activerecord/compatibility_4.rb delete mode 100644 lib/otr-activerecord/compatibility_7.rb delete mode 100644 lib/otr-activerecord/defaults.rb rename lib/otr-activerecord/{compatibility_6.rb => shim/v6.rb} (77%) rename lib/otr-activerecord/{compatibility_5.rb => shim/v7.rb} (67%) delete mode 100644 spec/otr-activerecord/activerecord_spec.rb delete mode 100644 spec/spec_helper.rb create mode 100644 test/configure_test.rb create mode 100644 test/connection_management_test.rb rename {spec => test}/fixtures/multi.yml (100%) rename {spec => test}/fixtures/simple.yml (100%) create mode 100644 test/matrix create mode 100644 test/query_cache_test.rb create mode 100644 test/rake_test.rb create mode 100644 test/support/active_record_connection.rb create mode 100644 test/support/active_record_models.rb create mode 100644 test/support/active_record_schema.rb create mode 100644 test/support/test_app.rb create mode 100644 test/test_helper.rb diff --git a/.rspec b/.rspec deleted file mode 100644 index c99d2e7..0000000 --- a/.rspec +++ /dev/null @@ -1 +0,0 @@ ---require spec_helper diff --git a/Appraisals b/Appraisals new file mode 100644 index 0000000..c85701c --- /dev/null +++ b/Appraisals @@ -0,0 +1,19 @@ +appraise "ar-7.2" do + gem "activerecord", "~> 7.2.0.beta2" + gem "sqlite3", "~> 1.7.3" +end + +appraise "ar-7.1" do + gem "activerecord", "~> 7.1.1" + gem "sqlite3", "~> 1.7.3" +end + +appraise "ar-7.0" do + gem "activerecord", "~> 7.0.1" + gem "sqlite3", "~> 1.4.2" +end + +appraise "ar-6.1" do + gem "activerecord", "~> 6.1.4" + gem "sqlite3", "~> 1.4.2" +end diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e497e4..ed3bcb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 2.3.0 (?) +* Support for ActiveRecord 7.2 +* Support for multi-db config in `OTR::ActiveRecord.configure_from_hash!` +* Removal of support for ActiveRecord 4.x-6.0 + ### 2.2.0 (2023-10-19) * Support for ActiveRecord 7.1 diff --git a/Gemfile b/Gemfile index bfc08f7..1d846be 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,12 @@ - source 'https://rubygems.org' gemspec -gem 'activerecord', '>6.0' -gem 'rspec' -gem 'sqlite3' +gem 'rake' +gem 'appraisal', '~> 2.5' + +group :test do + gem 'minitest' +end + +gem "rack", "~> 3.1" diff --git a/README.md b/README.md index 9074290..3f7f30f 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,28 @@ An easy way to use ActiveRecord "off the rails." Works with Grape, Sinatra, plain old Rack, or even in a boring little script! The defaults are all very Railsy (`config/database.yml`, `db/seeds.rb`, `db/migrate`, etc.), but you can easily change them. (Formerly known as `grape-activerecord`.) Supports: +* ActiveRecord 7.1 * ActiveRecord 7.0 (new AR features, like encryption, not tested) -* ActiveRecord 6.x -* ActiveRecord 5.x -* ActiveRecord 4.2 +* ActiveRecord 6.1 +* See older versions of this library for older versions of ActiveRecord ## How to use #### 1. Add it to your Gemfile - gem "otr-activerecord" +```ruby +gem "otr-activerecord" +``` #### 2. Configure your database connection -After loading your gems, tell `OTR::ActiveRecord` about your database config using one of the following examples: +After loading your gems, tell `OTR::ActiveRecord` about your database config using *one* of the following examples: - OTR::ActiveRecord.configure_from_file! "config/database.yml" - OTR::ActiveRecord.configure_from_url! ENV['DATABASE_URL'] # e.g. postgres://user:pass@host/db - OTR::ActiveRecord.configure_from_hash!(adapter: "postgresql", host: "localhost", database: "db", username: "user", password: "pass", encoding: "utf8", pool: 10, timeout: 5000) +```ruby +OTR::ActiveRecord.configure_from_file! "config/database.yml" +OTR::ActiveRecord.configure_from_url! ENV['DATABASE_URL'] # e.g. postgres://user:pass@host/db +OTR::ActiveRecord.configure_from_hash!(adapter: "postgresql", host: "localhost", database: "db", username: "user", password: "pass", encoding: "utf8", pool: 10, timeout: 5000) +``` **Important note**: `configure_from_file!` won't work as expected if you have already `DATABASE_URL` set as part of your environment variables. This is because in ActiveRecord when that env variable is set it will merge its properties into the current connection configuration. @@ -28,57 +32,91 @@ This is because in ActiveRecord when that env variable is set it will merge its If you have a single database (most apps), use this helper: - OTR::ActiveRecord.establish_connection! +```ruby +OTR::ActiveRecord.establish_connection! +``` If you're using multiple databases, call your base class(es) instead: - MyBase.establish_connection :primary - MyBase.establish_connection :primary_replica - ... +```ruby +MyBase.establish_connection :primary +MyBase.establish_connection :primary_replica +... +``` #### 4. Enable middleware for Rack apps Add these middlewares in `config.ru`: - # Clean up database connections after every request (required) - use OTR::ActiveRecord::ConnectionManagement +```ruby +# Clean up database connections after every request (required) +use OTR::ActiveRecord::ConnectionManagement - # Enable ActiveRecord's QueryCache for every request (optional) - use OTR::ActiveRecord::QueryCache +# Enable ActiveRecord's QueryCache for every request (optional) +use OTR::ActiveRecord::QueryCache +``` #### 5. Import ActiveRecord tasks into your Rakefile This will give you most of the standard `db:` tasks you get in Rails. Add it to your `Rakefile`. - require "bundler/setup" - load "tasks/otr-activerecord.rake" +```ruby +require "bundler/setup" +load "tasks/otr-activerecord.rake" - namespace :db do - # Some db tasks require your app code to be loaded; they'll expect to find it here - task :environment do - require_relative "app" - end - end +namespace :db do + # Some db tasks require your app code to be loaded; they'll expect to find it here + task :environment do + require_relative "app" + end +end +``` Unlike in Rails, creating a new migration is also a rake task. Run `bundle exec rake -T` to get a full list of tasks. - bundle exec rake db:create_migration[create_widgets] - -## Examples - -Look under [/examples](https://github.com/jhollinger/otr-activerecord/tree/master/examples) for some example apps. +```bash +bundle exec rake db:create_migration[create_widgets] +``` ## Advanced options The defaults for db-related files like migrations, seeds, and fixtures are the same as Rails. If you want to override them, use the following options in your `Rakefile`: - OTR::ActiveRecord.db_dir = 'db' - OTR::ActiveRecord.migrations_paths = ['db/migrate'] - OTR::ActiveRecord.fixtures_path = 'test/fixtures' - OTR::ActiveRecord.seed_file = 'seeds.rb' +```ruby +OTR::ActiveRecord.db_dir = 'db' +OTR::ActiveRecord.migrations_paths = ['db/migrate'] +OTR::ActiveRecord.fixtures_path = 'test/fixtures' +OTR::ActiveRecord.seed_file = 'seeds.rb' +``` + +## Testing + +Testing is fully scripted under the `bin/` directory. Appraisal is used to test against various ActiveRecord versions, and Docker or Podman is used to test against various Ruby versions. The combinations to test are defined in [test/matrix](https://github.com/jhollinger/otr-activerecord/blob/main/test/matrix). + +```bash +# Run all tests +bin/testall + +# Filter tests +bin/testall ruby-3.3 +bin/testall ar-7.1 +bin/testall ruby-3.3 ar-7.1 + +# Run one specific line from test/matrix +bin/test ruby-3.3 ar-7.1 sqlite3 + +# Run a specific file +bin/test ruby-3.3 ar-7.1 sqlite3 test/configure_test.rb + +# Run a specific test +bin/test ruby-3.3 ar-7.1 sqlite3 N=test_configure_from_file + +# Use podman +PODMAN=1 bin/testall +``` ## License Licensed under the MIT License -Copyright 2016 Jordan Hollinger +Copyright 2024 Jordan Hollinger diff --git a/Rakefile b/Rakefile index 14cfe0b..17fc866 100644 --- a/Rakefile +++ b/Rakefile @@ -1,2 +1,18 @@ -require 'bundler' +require 'bundler/setup' +require 'minitest/test_task' + Bundler::GemHelper.install_tasks + +# Accepts files, dirs, N=test_name_or/pattern/, X=test_name_or/pattern/ +Minitest::TestTask.create(:test) do |t| + globs = ARGV[1..].map { |a| + if Dir.exist? a + "#{a}/**/*_test.rb" + elsif File.exist? a + a + end + }.compact + + t.libs << "test" << "lib" + t.test_globs = globs.any? ? globs : ["test/**/*_test.rb"] +end diff --git a/bin/appraise b/bin/appraise new file mode 100755 index 0000000..ee1ef75 --- /dev/null +++ b/bin/appraise @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +if [[ $# -lt 3 ]]; then + echo "Use: bin/appraise ruby-X ar-X [args]" + exit 1 +fi + +set -euo pipefail + +cd $(dirname $0)/../ + +ruby_version=$1 +ar_version=$2 +shift 2 + +args="$@" +exec env bin/run ${ruby_version} bash -c ' + BVER=$(tail -n 1 Gemfile.lock | sed "s/ //g") + if ! gem list bundler --exact | grep $BVER > /dev/null; then + gem install bundler -v $BVER + fi + bundle install && \ + bundle exec appraisal '${ar_version}' bundle install && \ + TEST_DATABASE_URL="'${TEST_DATABASE_URL-}'" bundle exec appraisal '${ar_version}" ${args[@]}" diff --git a/bin/lib.sh b/bin/lib.sh new file mode 100644 index 0000000..5e3256a --- /dev/null +++ b/bin/lib.sh @@ -0,0 +1,65 @@ +function podpose { + if [[ -n "${PODMAN-}" ]]; then + podman-compose "$@" + else + docker compose "$@" + fi +} + +function array_in_array { + args=("$@") + term_size=${args[0]} + terms=("${args[@]:1:$term_size}") + array=("${args[@]:$(($term_size+1))}") + + for x in ${terms[@]}; do + if ! in_array $x ${array[@]}; then + return 1 + fi + done + return 0 +} + +function in_array { + term=$1 + shift 1 + for x in $@; do [[ $term == $x ]] && return 0; done + return 1 +} + +function announce { + box="#############################################################" + printf "\n%s\n %s\n%s\n" $box "$@" $box +} + +function nyancat { + red='\e[31m' + green='\e[32m' + yellow='\e[33m' + blue='\e[34m' + bold='\033[1m' + normal='\e[0m' + + lines=( + "" + "+ o + o" + " + o + +" + "o +" + " o + + +" + "+ o o + o" + "${red}-_-_-_-_-_-_-_${normal},------, o " + "${yellow}_-_-_-_-_-_-_-${normal}| /\\_/\\ " + "${green}-_-_-_-_-_-_-${normal}~|__( ^ .^) + + " + "${blue}_-_-_-_-_-_-_-${normal}\"\" \"\" " + " + o o + o" + " + +" + "o o o o +" + " o +" + "+ + o o + " + "" + ) + + for line in "${lines[@]}"; do + printf "${line}\n" + done +} diff --git a/bin/run b/bin/run new file mode 100755 index 0000000..6191c9f --- /dev/null +++ b/bin/run @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# +# Use: bin/run service cmd [arg1...] +# + +set -euo pipefail + +cd $(dirname $0)/../ +source bin/lib.sh + +service=$1 +shift + + +podpose build $service +podpose up --no-start $service +podpose run --rm $service "$@" diff --git a/bin/test b/bin/test new file mode 100755 index 0000000..0c6b0e6 --- /dev/null +++ b/bin/test @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +if [[ $# -lt 3 ]]; then + echo "Use: bin/test ruby-X ar-X sqlite3|postgres-*|mysql-8 [args]" + exit 1 +fi + +set -euo pipefail + +cd $(dirname $0)/../ + +ruby_version=$1 +ar_version=$2 +db=$3 +shift 3 + +if [[ $db == sqlite* ]]; then + db_url="" +elif [[ $db == postgres-* ]]; then + db_url="postgresql://postgres@${db}:5432/postgres" +elif [[ $db == mysql-* ]]; then + db_url="mysql2://root:@${db}:3306/mysql" +else + echo "Unknown database '${db}'. Options are: sqlite, postgres-*, mysql-*" + exit 1 +fi + +export TEST_DATABASE_URL="$db_url" +exec bin/appraise ${ruby_version} ${ar_version} rake test "$@" diff --git a/bin/testall b/bin/testall new file mode 100755 index 0000000..2d49c0f --- /dev/null +++ b/bin/testall @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# +# Run every combination: +# bin/testall +# +# Run just the matching combinations: +# bin/testall ruby-3.0 +# + +set -euo pipefail + +cd $(dirname $0)/../ +source bin/lib.sh + +[[ $# -gt 0 ]] && filter=("$@") || filter=() + +# flatten matrix +matrix=() +while read ruby ar dbs; do + for db in $dbs; do + matrix+=("$ruby $ar $db") + done +done < <(awk '/^ *[^#]/' test/matrix) + +# run each row in the matrix +for row in "${matrix[@]}"; do + read ruby ar db <<< "$row" + if [[ ${#filter[@]} -eq 0 ]] || array_in_array ${#filter[@]} ${filter[@]} $ruby $ar $db; then + cmd="bin/test $ruby $ar $db" + announce "$cmd" + sleep 1 + $cmd + fi +done + +podpose stop +nyancat diff --git a/bin/uninstall b/bin/uninstall new file mode 100755 index 0000000..61d4991 --- /dev/null +++ b/bin/uninstall @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd $(dirname $0)/../ +source bin/lib.sh + +podpose stop +podpose down +podpose down --rmi=local --volumes diff --git a/compose/Dockerfile.ruby-3.0 b/compose/Dockerfile.ruby-3.0 new file mode 100644 index 0000000..76274e7 --- /dev/null +++ b/compose/Dockerfile.ruby-3.0 @@ -0,0 +1,15 @@ +FROM ruby:3.0-slim-bullseye +WORKDIR /srv + +ENV GEM_HOME /usr/local/bundle +ENV BUNDLE_PATH="$GEM_HOME" \ + BUNDLE_APP_CONFIG="$GEM_HOME" + +RUN \ + apt-get update -qq && \ + apt-get install -y --no-install-recommends \ + build-essential \ + libsqlite3-dev \ + git \ + && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/compose/Dockerfile.ruby-3.1 b/compose/Dockerfile.ruby-3.1 new file mode 100644 index 0000000..3d1c9ae --- /dev/null +++ b/compose/Dockerfile.ruby-3.1 @@ -0,0 +1,15 @@ +FROM ruby:3.1-slim-bookworm +WORKDIR /srv + +ENV GEM_HOME /usr/local/bundle +ENV BUNDLE_PATH="$GEM_HOME" \ + BUNDLE_APP_CONFIG="$GEM_HOME" + +RUN \ + apt-get update -qq && \ + apt-get install -y --no-install-recommends \ + build-essential \ + libsqlite3-dev \ + git \ + && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/compose/Dockerfile.ruby-3.2 b/compose/Dockerfile.ruby-3.2 new file mode 100644 index 0000000..81ddd24 --- /dev/null +++ b/compose/Dockerfile.ruby-3.2 @@ -0,0 +1,15 @@ +FROM ruby:3.2-slim-bookworm +WORKDIR /srv + +ENV GEM_HOME /usr/local/bundle +ENV BUNDLE_PATH="$GEM_HOME" \ + BUNDLE_APP_CONFIG="$GEM_HOME" + +RUN \ + apt-get update -qq && \ + apt-get install -y --no-install-recommends \ + build-essential \ + libsqlite3-dev \ + git \ + && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/compose/Dockerfile.ruby-3.3 b/compose/Dockerfile.ruby-3.3 new file mode 100644 index 0000000..d8974b5 --- /dev/null +++ b/compose/Dockerfile.ruby-3.3 @@ -0,0 +1,15 @@ +FROM ruby:3.3-slim-bookworm +WORKDIR /srv + +ENV GEM_HOME /usr/local/bundle +ENV BUNDLE_PATH="$GEM_HOME" \ + BUNDLE_APP_CONFIG="$GEM_HOME" + +RUN \ + apt-get update -qq && \ + apt-get install -y --no-install-recommends \ + build-essential \ + libsqlite3-dev \ + git \ + && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..97bbdea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +--- +services: + ruby-3.3: + build: + context: ./ + dockerfile: ./compose/Dockerfile.ruby-3.3 + volumes: + - ./:/srv + - ruby-3.3-gems:/usr/local/bundle + + ruby-3.2: + build: + context: ./ + dockerfile: ./compose/Dockerfile.ruby-3.2 + volumes: + - ./:/srv + - ruby-3.2-gems:/usr/local/bundle + + ruby-3.1: + build: + context: ./ + dockerfile: ./compose/Dockerfile.ruby-3.1 + volumes: + - ./:/srv + - ruby-3.1-gems:/usr/local/bundle + + ruby-3.0: + build: + context: ./ + dockerfile: ./compose/Dockerfile.ruby-3.0 + volumes: + - ./:/srv + - ruby-3.0-gems:/usr/local/bundle + +volumes: + ruby-3.3-gems: + ruby-3.2-gems: + ruby-3.1-gems: + ruby-3.0-gems: diff --git a/gemfiles/ar_6.1.gemfile b/gemfiles/ar_6.1.gemfile new file mode 100644 index 0000000..00ac7dc --- /dev/null +++ b/gemfiles/ar_6.1.gemfile @@ -0,0 +1,15 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rake" +gem "appraisal", "~> 2.5" +gem "rack", "~> 3.1" +gem "activerecord", "~> 6.1.4" +gem "sqlite3", "~> 1.4.2" + +group :test do + gem "minitest" +end + +gemspec path: "../" diff --git a/gemfiles/ar_6.1.gemfile.lock b/gemfiles/ar_6.1.gemfile.lock new file mode 100644 index 0000000..7a2618e --- /dev/null +++ b/gemfiles/ar_6.1.gemfile.lock @@ -0,0 +1,55 @@ +PATH + remote: .. + specs: + otr-activerecord (2.2.0) + activerecord (>= 6.0, < 7.2) + hashie-forbidden_attributes (~> 0.1) + +GEM + remote: https://rubygems.org/ + specs: + activemodel (6.1.7.8) + activesupport (= 6.1.7.8) + activerecord (6.1.7.8) + activemodel (= 6.1.7.8) + activesupport (= 6.1.7.8) + activesupport (6.1.7.8) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + appraisal (2.5.0) + bundler + rake + thor (>= 0.14.0) + concurrent-ruby (1.3.3) + hashie (5.0.0) + hashie-forbidden_attributes (0.1.1) + hashie (>= 3.0) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + minitest (5.24.0) + rack (3.1.4) + rake (13.2.1) + sqlite3 (1.4.4) + thor (1.3.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + zeitwerk (2.6.16) + +PLATFORMS + aarch64-linux + ruby + +DEPENDENCIES + activerecord (~> 6.1.4) + appraisal (~> 2.5) + minitest + otr-activerecord! + rack (~> 3.1) + rake + sqlite3 (~> 1.4.2) + +BUNDLED WITH + 2.5.11 diff --git a/gemfiles/ar_7.0.gemfile b/gemfiles/ar_7.0.gemfile new file mode 100644 index 0000000..5ddd406 --- /dev/null +++ b/gemfiles/ar_7.0.gemfile @@ -0,0 +1,15 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rake" +gem "appraisal", "~> 2.5" +gem "rack", "~> 3.1" +gem "activerecord", "~> 7.0.1" +gem "sqlite3", "~> 1.4.2" + +group :test do + gem "minitest" +end + +gemspec path: "../" diff --git a/gemfiles/ar_7.0.gemfile.lock b/gemfiles/ar_7.0.gemfile.lock new file mode 100644 index 0000000..ff1de06 --- /dev/null +++ b/gemfiles/ar_7.0.gemfile.lock @@ -0,0 +1,53 @@ +PATH + remote: .. + specs: + otr-activerecord (2.2.0) + activerecord (>= 6.0, < 7.2) + hashie-forbidden_attributes (~> 0.1) + +GEM + remote: https://rubygems.org/ + specs: + activemodel (7.0.8.4) + activesupport (= 7.0.8.4) + activerecord (7.0.8.4) + activemodel (= 7.0.8.4) + activesupport (= 7.0.8.4) + activesupport (7.0.8.4) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + appraisal (2.5.0) + bundler + rake + thor (>= 0.14.0) + concurrent-ruby (1.3.3) + hashie (5.0.0) + hashie-forbidden_attributes (0.1.1) + hashie (>= 3.0) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + minitest (5.24.0) + rack (3.1.4) + rake (13.2.1) + sqlite3 (1.4.4) + thor (1.3.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + +PLATFORMS + aarch64-linux + ruby + +DEPENDENCIES + activerecord (~> 7.0.1) + appraisal (~> 2.5) + minitest + otr-activerecord! + rack (~> 3.1) + rake + sqlite3 (~> 1.4.2) + +BUNDLED WITH + 2.5.11 diff --git a/gemfiles/ar_7.1.gemfile b/gemfiles/ar_7.1.gemfile new file mode 100644 index 0000000..b916fa4 --- /dev/null +++ b/gemfiles/ar_7.1.gemfile @@ -0,0 +1,15 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rake" +gem "appraisal", "~> 2.5" +gem "rack", "~> 3.1" +gem "activerecord", "~> 7.1.1" +gem "sqlite3", "~> 1.7.3" + +group :test do + gem "minitest" +end + +gemspec path: "../" diff --git a/gemfiles/ar_7.1.gemfile.lock b/gemfiles/ar_7.1.gemfile.lock new file mode 100644 index 0000000..1f6861b --- /dev/null +++ b/gemfiles/ar_7.1.gemfile.lock @@ -0,0 +1,66 @@ +PATH + remote: .. + specs: + otr-activerecord (2.2.0) + activerecord (>= 6.0, < 7.2) + hashie-forbidden_attributes (~> 0.1) + +GEM + remote: https://rubygems.org/ + specs: + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) + timeout (>= 0.4.0) + activesupport (7.1.3.4) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + appraisal (2.5.0) + bundler + rake + thor (>= 0.14.0) + base64 (0.2.0) + bigdecimal (3.1.8) + concurrent-ruby (1.3.3) + connection_pool (2.4.1) + drb (2.2.1) + hashie (5.0.0) + hashie-forbidden_attributes (0.1.1) + hashie (>= 3.0) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + minitest (5.24.0) + mutex_m (0.2.0) + rack (3.1.4) + rake (13.2.1) + sqlite3 (1.7.3-aarch64-linux) + sqlite3 (1.7.3-arm64-darwin) + thor (1.3.1) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + +PLATFORMS + aarch64-linux + arm64-darwin + +DEPENDENCIES + activerecord (~> 7.1.1) + appraisal (~> 2.5) + minitest + otr-activerecord! + rack (~> 3.1) + rake + sqlite3 (~> 1.7.3) + +BUNDLED WITH + 2.5.11 diff --git a/gemfiles/ar_7.2.gemfile b/gemfiles/ar_7.2.gemfile new file mode 100644 index 0000000..b0b6a72 --- /dev/null +++ b/gemfiles/ar_7.2.gemfile @@ -0,0 +1,15 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rake" +gem "appraisal", "~> 2.5" +gem "rack", "~> 3.1" +gem "activerecord", "~> 7.2.0.beta2" +gem "sqlite3", "~> 1.7.3" + +group :test do + gem "minitest" +end + +gemspec path: "../" diff --git a/gemfiles/ar_7.2.gemfile.lock b/gemfiles/ar_7.2.gemfile.lock new file mode 100644 index 0000000..e3dccaa --- /dev/null +++ b/gemfiles/ar_7.2.gemfile.lock @@ -0,0 +1,64 @@ +PATH + remote: .. + specs: + otr-activerecord (2.2.0) + activerecord (>= 6.0, < 7.3) + hashie-forbidden_attributes (~> 0.1) + +GEM + remote: https://rubygems.org/ + specs: + activemodel (7.2.0.beta2) + activesupport (= 7.2.0.beta2) + activerecord (7.2.0.beta2) + activemodel (= 7.2.0.beta2) + activesupport (= 7.2.0.beta2) + timeout (>= 0.4.0) + activesupport (7.2.0.beta2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0, >= 2.0.5) + appraisal (2.5.0) + bundler + rake + thor (>= 0.14.0) + base64 (0.2.0) + bigdecimal (3.1.8) + concurrent-ruby (1.3.3) + connection_pool (2.4.1) + drb (2.2.1) + hashie (5.0.0) + hashie-forbidden_attributes (0.1.1) + hashie (>= 3.0) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + minitest (5.24.0) + rack (3.1.4) + rake (13.2.1) + sqlite3 (1.7.3-aarch64-linux) + sqlite3 (1.7.3-arm64-darwin) + thor (1.3.1) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + +PLATFORMS + aarch64-linux + arm64-darwin + +DEPENDENCIES + activerecord (~> 7.2.0.beta2) + appraisal (~> 2.5) + minitest + otr-activerecord! + rack (~> 3.1) + rake + sqlite3 (~> 1.7.3) + +BUNDLED WITH + 2.5.11 diff --git a/lib/otr-activerecord.rb b/lib/otr-activerecord.rb index c9e31b3..4bcf803 100644 --- a/lib/otr-activerecord.rb +++ b/lib/otr-activerecord.rb @@ -1,7 +1,13 @@ require 'active_record' require 'hashie-forbidden_attributes' + require 'otr-activerecord/version' require 'otr-activerecord/activerecord' -require 'otr-activerecord/middleware/connection_management' -require 'otr-activerecord/middleware/query_cache' -require 'otr-activerecord/defaults' + +ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] ||= "true" + +OTR::ActiveRecord.db_dir = 'db' +OTR::ActiveRecord.migrations_paths = %w(db/migrate) +OTR::ActiveRecord.fixtures_path = 'test/fixtures' +OTR::ActiveRecord.seed_file = 'seeds.rb' +OTR::ActiveRecord.shim = OTR::ActiveRecord::Shim.new diff --git a/lib/otr-activerecord/activerecord.rb b/lib/otr-activerecord/activerecord.rb index 6f16ca3..0841afb 100644 --- a/lib/otr-activerecord/activerecord.rb +++ b/lib/otr-activerecord/activerecord.rb @@ -1,13 +1,19 @@ require 'erb' +require 'uri' +require 'yaml' # "Off the Rails" ActiveRecord configuration/integration for Grape, Sinatra, Rack, and any other kind of app module OTR # ActiveRecord configuration module module ActiveRecord - autoload :Compatibility4, 'otr-activerecord/compatibility_4' - autoload :Compatibility5, 'otr-activerecord/compatibility_5' - autoload :Compatibility6, 'otr-activerecord/compatibility_6' - autoload :Compatibility7, 'otr-activerecord/compatibility_7' + autoload :ConnectionManagement, 'otr-activerecord/middleware/connection_management' + autoload :QueryCache, 'otr-activerecord/middleware/query_cache' + autoload :Shim, + case ::ActiveRecord::VERSION::MAJOR + when 6 then 'otr-activerecord/shim/v6' + when 7 then 'otr-activerecord/shim/v7' + else raise "Unsupported ActiveRecord version" + end class << self # Relative path to the "db" dir @@ -19,19 +25,17 @@ class << self # Name of the seeds file in db_dir attr_accessor :seed_file # Internal compatibility layer across different major versions of AR - attr_accessor :_normalizer + attr_accessor :shim end # Connect to database with a Hash. Example: # {adapter: 'postgresql', host: 'localhost', database: 'db', username: 'user', password: 'pass', encoding: 'utf8', pool: 10, timeout: 5000} def self.configure_from_hash!(spec) - config = spec.stringify_keys.merge("migrations_paths" => ::OTR::ActiveRecord.migrations_paths) - ::ActiveRecord::Base.configurations = {rack_env.to_s => config} + ::ActiveRecord::Base.configurations = transform_config({rack_env.to_s => spec}) end # Connect to database with a DB URL. Example: "postgres://user:pass@localhost/db" def self.configure_from_url!(url) - require 'uri' uri = URI(url) spec = {"adapter" => uri.scheme} @@ -57,17 +61,9 @@ def self.configure_from_url!(url) # Connect to database with a yml file. Example: "config/database.yml" def self.configure_from_file!(path) - raise "#{path} does not exist!" unless File.file? path - result = load_yaml(path) - ::ActiveRecord::Base.configurations = begin - result.each do |_env, config| - if config.all? { |_, v| v.is_a?(Hash) } - config.each { |_, v| append_migration_path(v) } - else - append_migration_path(config) - end - end - end + yaml = ERB.new(File.read(path)).result + spec = YAML.safe_load(yaml, aliases: true) || {} + ::ActiveRecord::Base.configurations = transform_config spec end # Establish a connection to the given db (defaults to current rack env) @@ -75,29 +71,26 @@ def self.establish_connection!(db = rack_env) ::ActiveRecord::Base.establish_connection(db) end - def self.append_migration_path(config) - config['migrations_paths'] = ::OTR::ActiveRecord.migrations_paths unless config.key?('migrations_paths') - config - end - # The current Rack environment def self.rack_env (ENV['RACK_ENV'] || ENV['RAILS_ENV'] || ENV['APP_ENV'] || ENV['OTR_ENV'] || 'development').to_sym end - # Support old Psych versions - def self.load_yaml(path) - erb_result = ERB.new(File.read(path)).result - - result = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1') - YAML.safe_load(erb_result, aliases: true) - else - YAML.safe_load(erb_result, [], [], true) - end - - result || {} + def self.transform_config(spec) + fixup = ->(config) { + config = config.stringify_keys + config["migrations_paths"] ||= migrations_paths + config + } + spec.stringify_keys.transform_values { |config| + if config.all? { |_, v| v.is_a? Hash } + config.transform_values { |v| fixup.(v) } + else + fixup.(config) + end + } end - private_class_method :load_yaml + private_class_method :transform_config end end diff --git a/lib/otr-activerecord/compatibility_4.rb b/lib/otr-activerecord/compatibility_4.rb deleted file mode 100644 index 1f2c8c0..0000000 --- a/lib/otr-activerecord/compatibility_4.rb +++ /dev/null @@ -1,34 +0,0 @@ -module OTR - module ActiveRecord - # Compatibility layer for ActiveRecord 4 - class Compatibility4 - attr_reader :major_version - - # Compatibility layer for ActiveRecord 4 - def initialize - @major_version = 4 - ::ActiveRecord::Base.default_timezone = :utc - end - - # All db migration dir paths - def migrations_paths - OTR::ActiveRecord.migrations_paths - end - - # The dir in which to put new migrations - def migrations_path - OTR::ActiveRecord.migrations_paths[0] - end - - # Basename of migration classes - def migration_base_class_name - 'ActiveRecord::Migration' - end - - # Force RACK_ENV/RAILS_ENV to be 'test' when running any db:test:* tasks - def force_db_test_env? - true - end - end - end -end diff --git a/lib/otr-activerecord/compatibility_7.rb b/lib/otr-activerecord/compatibility_7.rb deleted file mode 100644 index 6c6842c..0000000 --- a/lib/otr-activerecord/compatibility_7.rb +++ /dev/null @@ -1,11 +0,0 @@ -module OTR - module ActiveRecord - # Compatibility layer for ActiveRecord 7 - class Compatibility7 < Compatibility6 - def initialize - @major_version = 7 - ::ActiveRecord.default_timezone = :utc - end - end - end -end diff --git a/lib/otr-activerecord/defaults.rb b/lib/otr-activerecord/defaults.rb deleted file mode 100644 index 9c42b4a..0000000 --- a/lib/otr-activerecord/defaults.rb +++ /dev/null @@ -1,13 +0,0 @@ -ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] ||= "true" - -OTR::ActiveRecord.db_dir = 'db' -OTR::ActiveRecord.migrations_paths = %w(db/migrate) -OTR::ActiveRecord.fixtures_path = 'test/fixtures' -OTR::ActiveRecord.seed_file = 'seeds.rb' -OTR::ActiveRecord._normalizer = - case ::ActiveRecord::VERSION::MAJOR - when 4 then OTR::ActiveRecord::Compatibility4.new - when 5 then OTR::ActiveRecord::Compatibility5.new - when 6 then OTR::ActiveRecord::Compatibility6.new - when 7 then OTR::ActiveRecord::Compatibility7.new - end diff --git a/lib/otr-activerecord/middleware/connection_management.rb b/lib/otr-activerecord/middleware/connection_management.rb index 11aa3ed..b4a34bb 100644 --- a/lib/otr-activerecord/middleware/connection_management.rb +++ b/lib/otr-activerecord/middleware/connection_management.rb @@ -13,13 +13,13 @@ def call(env) resp = @app.call env resp[2] = ::Rack::BodyProxy.new resp[2] do - ::ActiveRecord::Base.clear_active_connections! unless testing + ::ActiveRecord::Base.connection_handler.clear_active_connections! unless testing end resp - rescue Exception - ::ActiveRecord::Base.clear_active_connections! unless testing - raise + rescue => e + ::ActiveRecord::Base.connection_handler.clear_active_connections! unless testing + raise e end end end diff --git a/lib/otr-activerecord/middleware/query_cache.rb b/lib/otr-activerecord/middleware/query_cache.rb index 6d88dc3..8d04938 100644 --- a/lib/otr-activerecord/middleware/query_cache.rb +++ b/lib/otr-activerecord/middleware/query_cache.rb @@ -5,28 +5,15 @@ module ActiveRecord # class QueryCache def initialize(app) - @handler = case ::ActiveRecord::VERSION::MAJOR - when 4 then ::ActiveRecord::QueryCache.new(app) - when 5, 6, 7 then ActionDispatchHandler.new(app) - end + @app = app end def call(env) - @handler.call(env) - end - - class ActionDispatchHandler - def initialize(app) - @app = app - end - - def call(env) - state = nil - state = ::ActiveRecord::QueryCache.run - @app.call(env) - ensure - ::ActiveRecord::QueryCache.complete(state) if state - end + state = nil + state = ::ActiveRecord::QueryCache.run + @app.call(env) + ensure + ::ActiveRecord::QueryCache.complete(state) if state end end end diff --git a/lib/otr-activerecord/compatibility_6.rb b/lib/otr-activerecord/shim/v6.rb similarity index 77% rename from lib/otr-activerecord/compatibility_6.rb rename to lib/otr-activerecord/shim/v6.rb index 9f629ce..46e8219 100644 --- a/lib/otr-activerecord/compatibility_6.rb +++ b/lib/otr-activerecord/shim/v6.rb @@ -1,12 +1,8 @@ module OTR module ActiveRecord # Compatibility layer for ActiveRecord 6 - class Compatibility6 - attr_reader :major_version - - # Compatibility layer for ActiveRecord 6 + class Shim def initialize - @major_version = 6 ::ActiveRecord::Base.default_timezone = :utc end @@ -22,7 +18,7 @@ def migrations_path # Basename of migration classes def migration_base_class_name - version = "#{@major_version}.#{::ActiveRecord::VERSION::MINOR}" + version = "6.#{::ActiveRecord::VERSION::MINOR}" "ActiveRecord::Migration[#{version}]" end diff --git a/lib/otr-activerecord/compatibility_5.rb b/lib/otr-activerecord/shim/v7.rb similarity index 67% rename from lib/otr-activerecord/compatibility_5.rb rename to lib/otr-activerecord/shim/v7.rb index 7d75ef1..5764de6 100644 --- a/lib/otr-activerecord/compatibility_5.rb +++ b/lib/otr-activerecord/shim/v7.rb @@ -1,13 +1,9 @@ module OTR module ActiveRecord - # Compatibility layer for ActiveRecord 5 - class Compatibility5 - attr_reader :major_version - - # Compatibility layer for ActiveRecord 5 + # Compatibility layer for ActiveRecord 7 + class Shim def initialize - @major_version = 5 - ::ActiveRecord::Base.default_timezone = :utc + ::ActiveRecord.default_timezone = :utc end # All db migration dir paths @@ -22,7 +18,7 @@ def migrations_path # Basename of migration classes def migration_base_class_name - version = "5.#{::ActiveRecord::VERSION::MINOR}" + version = "7.#{::ActiveRecord::VERSION::MINOR}" "ActiveRecord::Migration[#{version}]" end diff --git a/lib/tasks/otr-activerecord.rake b/lib/tasks/otr-activerecord.rake index b9ff812..2f4171f 100644 --- a/lib/tasks/otr-activerecord.rake +++ b/lib/tasks/otr-activerecord.rake @@ -53,13 +53,13 @@ namespace :db do task :environment do ENV['RACK_ENV'] = 'test' end - end if OTR::ActiveRecord._normalizer.force_db_test_env? + end if OTR::ActiveRecord.shim.force_db_test_env? desc "Create a migration" task :create_migration, [:name] do |_, args| name, version = args[:name], Time.now.utc.strftime("%Y%m%d%H%M%S") - OTR::ActiveRecord._normalizer.migrations_paths.each do |directory| + OTR::ActiveRecord.shim.migrations_paths.each do |directory| next unless File.exist?(directory) migration_files = Pathname(directory).children @@ -69,12 +69,12 @@ namespace :db do end filename = "#{version}_#{name}.rb" - dirname = OTR::ActiveRecord._normalizer.migrations_path + dirname = OTR::ActiveRecord.shim.migrations_path path = File.join(dirname, filename) FileUtils.mkdir_p(dirname) File.write path, <<-MIGRATION.strip_heredoc - class #{name.camelize} < #{OTR::ActiveRecord._normalizer.migration_base_class_name} + class #{name.camelize} < #{OTR::ActiveRecord.shim.migration_base_class_name} def change end end diff --git a/otr-activerecord.gemspec b/otr-activerecord.gemspec index 0c2fe90..0a86326 100644 --- a/otr-activerecord.gemspec +++ b/otr-activerecord.gemspec @@ -4,7 +4,6 @@ require File.expand_path('../lib/otr-activerecord/version.rb', __FILE__) Gem::Specification.new do |gem| gem.name = 'otr-activerecord' gem.version = OTR::ActiveRecord::VERSION - gem.date = '2023-10-30' gem.description = 'Off The Rails ActiveRecord: Use ActiveRecord with Grape, Sinatra, Rack, or anything else! Formerly known as \'grape-activerecord\'.' gem.summary = 'Off The Rails: Use ActiveRecord with Grape, Sinatra, Rack, or anything else!' @@ -17,8 +16,8 @@ Gem::Specification.new do |gem| gem.files = Dir['lib/**/**'] + ['README.md', 'LICENSE'] - gem.required_ruby_version = '>= 2.1.0' + gem.required_ruby_version = '>= 3.0.0' - gem.add_runtime_dependency 'activerecord', ['>= 4.0', '< 7.2'] + gem.add_runtime_dependency 'activerecord', ['>= 6.0', '< 7.2'] gem.add_runtime_dependency 'hashie-forbidden_attributes', '~> 0.1' end diff --git a/spec/otr-activerecord/activerecord_spec.rb b/spec/otr-activerecord/activerecord_spec.rb deleted file mode 100644 index 0ddc3ac..0000000 --- a/spec/otr-activerecord/activerecord_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -RSpec.describe OTR::ActiveRecord do - describe '.configure_from_file!' do - context 'when simple configuration file is given' do - let(:config) { Bundler.root.join('spec/fixtures/simple.yml') } - - it 'configures active record' do - described_class.configure_from_file!(config) - t = ::ActiveRecord::Base.configurations['test'].with_indifferent_access - expect(t[:adapter]).to eq 'sqlite3' - expect(t[:database]).to eq 'tmp/simple.sqlite3' - expect(t[:migrations_paths]).to eq ['db/migrate'] - end - end - - context 'when configuration file with multiple roles given' do - let(:config) { Bundler.root.join('spec/fixtures/multi.yml') } - - it 'configures active record' do - described_class.configure_from_file!(config) - t = ::ActiveRecord::Base.configurations['test'].with_indifferent_access - expect(t[:adapter]).to eq 'sqlite3' - expect(t[:database]).to eq 'tmp/multi.sqlite3' - expect(t[:migrations_paths]).to eq ['db/migrate'] - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index f6f547a..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'otr-activerecord' - -RSpec.configure do |config| - config.color = true - config.tty = true - config.filter_run_when_matching :focus -end diff --git a/test/configure_test.rb b/test/configure_test.rb new file mode 100644 index 0000000..043f6a6 --- /dev/null +++ b/test/configure_test.rb @@ -0,0 +1,82 @@ +require 'test_helper' + +class ConfigureTest < Minitest::Test + def test_configure_from_file + OTR::ActiveRecord.configure_from_file! "test/fixtures/simple.yml" + + t = ::ActiveRecord::Base.configurations.find_db_config('test') + assert_equal "sqlite3", t.adapter + assert_equal "tmp/simple.sqlite3", t.database + assert_equal ['db/migrate'], t.migrations_paths + end + + def test_configure_from_file_with_multiple_roles + OTR::ActiveRecord.configure_from_file! "test/fixtures/multi.yml" + + t = ::ActiveRecord::Base.configurations.find_db_config('test') + assert_equal "sqlite3", t.adapter + assert_equal "tmp/multi.sqlite3", t.database + assert_equal ['db/migrate'], t.migrations_paths + end + + def test_configure_from_url + OTR::ActiveRecord.configure_from_url! "postgresql://foo:bar@my.host/mydb" + + t = ::ActiveRecord::Base.configurations.find_db_config('development') + assert_equal "postgresql", t.adapter + assert_equal "my.host", t.host + assert_equal "mydb", t.database + assert_equal ['db/migrate'], t.migrations_paths + end + + def test_configure_from_url_with_port + OTR::ActiveRecord.configure_from_url! "postgresql://foo:bar@my.host/mydb:5433" + + t = ::ActiveRecord::Base.configurations.find_db_config('development') + assert_equal "postgresql", t.adapter + assert_equal "my.host", t.host + assert_equal "mydb:5433", t.database + assert_equal ['db/migrate'], t.migrations_paths + end + + def test_configure_from_url_using_sqlite3 + OTR::ActiveRecord.configure_from_url! "sqlite3:///srv/db/mydb.sqlite3" + + t = ::ActiveRecord::Base.configurations.find_db_config('development') + assert_equal "sqlite3", t.adapter + assert_equal "/srv/db/mydb.sqlite3", t.database + assert_equal ['db/migrate'], t.migrations_paths + end + + def test_configure_from_url_using_sqlite3_memory + OTR::ActiveRecord.configure_from_url! "sqlite3::memory:" + + t = ::ActiveRecord::Base.configurations.find_db_config('development') + assert_equal "sqlite3", t.adapter + assert_equal ":memory:", t.database + assert_equal ['db/migrate'], t.migrations_paths + end + + def test_configure_from_hash + OTR::ActiveRecord.configure_from_hash!({adapter: "postgresql", host: "localhost", database: "db"}) + + t = ::ActiveRecord::Base.configurations.find_db_config('development') + assert_equal "postgresql", t.adapter + assert_equal "localhost", t.host + assert_equal "db", t.database + assert_equal ['db/migrate'], t.migrations_paths + end + + def test_configure_from_hash_with_multiple_roles + OTR::ActiveRecord.configure_from_hash!({ + primary: {adapter: "postgresql", host: "localhost", database: "primary"}, + reading: {adapter: "postgresql", host: "localhost", database: "reading"}, + }) + + t = ::ActiveRecord::Base.configurations.find_db_config('development') + assert_equal "postgresql", t.adapter + assert_equal "localhost", t.host + assert_equal "primary", t.database + assert_equal ['db/migrate'], t.migrations_paths + end +end diff --git a/test/connection_management_test.rb b/test/connection_management_test.rb new file mode 100644 index 0000000..af2af7a --- /dev/null +++ b/test/connection_management_test.rb @@ -0,0 +1,80 @@ +require 'test_helper' + +class ConnectionManagementTest < Minitest::Test + def setup + @db = Tempfile.create + OTR::ActiveRecord.configure_from_hash!({adapter: "sqlite3", database: @db.path}) + OTR::ActiveRecord.establish_connection! + Schema.load! + cat = Category.create!(name: "Foo") + Widget.create!(category_id: cat.id, name: "Spline") + end + + def teardown + ActiveRecord::Base.connection_handler.clear_all_connections! + ActiveRecord::Base.connection_pool.disconnect! + File.unlink @db.path + end + + def test_returns_resp + app = proc do |env| + body = "Testing" + [200, {"Content-Type" => "text/plain", "Content-Length" => body.size.to_s}, [body]] + end + + middleware = OTR::ActiveRecord::ConnectionManagement.new(app) + status, headers, body = middleware.call({}) + + assert_equal 200, status + assert_equal "text/plain", headers["Content-Type"] + assert_equal ["Testing"], body.to_a + end + + def test_with_no_queries + called = false + app = proc do |env| + called = true + body = "Testing" + [200, {"Content-Type" => "text/plain", "Content-Length" => body.size.to_s}, [body]] + end + + middleware = OTR::ActiveRecord::ConnectionManagement.new(app) + middleware.call({}) + assert called + end + + def test_with_queries + n = nil + app = proc do |env| + n = Widget.count + body = "Testing" + [200, {"Content-Type" => "text/plain", "Content-Length" => body.size.to_s}, [body]] + end + + middleware = OTR::ActiveRecord::ConnectionManagement.new(app) + middleware.call({}) + assert_equal 1, n + end + + def test_with_many_threads + app = proc do |env| + n = Widget.count + body = n.to_s + [200, {"Content-Type" => "text/plain", "Content-Length" => body.size.to_s}, [body]] + end + + n = 500 + middleware = OTR::ActiveRecord::ConnectionManagement.new(app) + results = n.times + .map { + Thread.new { + _, _, body = middleware.call({}) + body.close + body[0] + } + } + .map(&:value) + + assert_equal n.times.map { "1" }, results + end +end diff --git a/spec/fixtures/multi.yml b/test/fixtures/multi.yml similarity index 100% rename from spec/fixtures/multi.yml rename to test/fixtures/multi.yml diff --git a/spec/fixtures/simple.yml b/test/fixtures/simple.yml similarity index 100% rename from spec/fixtures/simple.yml rename to test/fixtures/simple.yml diff --git a/test/matrix b/test/matrix new file mode 100644 index 0000000..ca7c075 --- /dev/null +++ b/test/matrix @@ -0,0 +1,19 @@ +# +# Read by bin/testall to test various combinations of Ruby, ActiveRecord, and datbases +# + +ruby-3.3 ar-7.1 sqlite3 +ruby-3.3 ar-7.0 sqlite3 +ruby-3.3 ar-6.1 sqlite3 + +ruby-3.2 ar-7.1 sqlite3 +ruby-3.2 ar-7.0 sqlite3 +ruby-3.2 ar-6.1 sqlite3 + +ruby-3.1 ar-7.1 sqlite3 +ruby-3.1 ar-7.0 sqlite3 +ruby-3.1 ar-6.1 sqlite3 + +ruby-3.0 ar-7.1 sqlite3 +ruby-3.0 ar-7.0 sqlite3 +ruby-3.0 ar-6.1 sqlite3 diff --git a/test/query_cache_test.rb b/test/query_cache_test.rb new file mode 100644 index 0000000..18c8d7b --- /dev/null +++ b/test/query_cache_test.rb @@ -0,0 +1,33 @@ +require 'test_helper' + +class QueryCacheTest < Minitest::Test + def setup + @db = Tempfile.create + OTR::ActiveRecord.configure_from_hash!({adapter: "sqlite3", database: @db.path}) + OTR::ActiveRecord.establish_connection! + Schema.load! + @cat = Category.create!(name: "Foo") + Widget.create!(category_id: @cat.id, name: "Spline") + end + + def teardown + ActiveRecord::Base.connection_handler.clear_all_connections! + ActiveRecord::Base.connection_pool.disconnect! + File.unlink @db.path + end + + def test_returns_resp + app = proc do |env| + Widget.count + body = "Testing" + [200, {"Content-Type" => "text/plain", "Content-Length" => body.size.to_s}, [body]] + end + + middleware = OTR::ActiveRecord::QueryCache.new(app) + status, headers, body = middleware.call({}) + + assert_equal 200, status + assert_equal "text/plain", headers["Content-Type"] + assert_equal ["Testing"], body.to_a + end +end diff --git a/test/rake_test.rb b/test/rake_test.rb new file mode 100644 index 0000000..d165930 --- /dev/null +++ b/test/rake_test.rb @@ -0,0 +1,61 @@ +require 'test_helper' +require 'rake' +require 'json' + +class RakeTest < Minitest::Test + def self.test_order; :alpha; end + + def setup + Dir.chdir TestApp::DIR if TestApp.exists? + end + + def teardown + Dir.chdir Rake.application.original_dir + end + + def test_1_create + TestApp.delete if TestApp.exists? + TestApp.create + end + + def test_2_setup + skip unless TestApp.exists? + assert system "bundle install" + assert system "bundle exec rake db:setup" + widgets = JSON.parse(`bundle exec rake widgets:list`) + assert_equal ["Foo"], widgets.map { |w| w["name"] } + end + + def test_3_create_migration + skip unless TestApp.exists? + migration = `bundle exec rake db:create_migration[add_active]`.chomp + + assert File.exist? migration + assert_equal <<-STR.chomp, File.read(migration).chomp +class AddActive < ActiveRecord::Migration[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}] + def change + end +end + STR + end + + def test_4_run_migration + skip unless TestApp.exists? + migration = Dir.glob("db/migrate/*.rb")[0] + File.write migration, <<-STR +class AddActive < ActiveRecord::Migration[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}] + def change + add_column :widgets, :active, :boolean, default: true, null: false + end +end + STR + + assert system "bundle exec rake db:migrate db:test:prepare" + widgets = JSON.parse(`bundle exec rake widgets:list`) + assert_equal ["Foo:true"], widgets.map { |w| "#{w['name']}:#{w['active']}" } + end + + def test_5_teardown + TestApp.delete + end +end diff --git a/test/support/active_record_connection.rb b/test/support/active_record_connection.rb new file mode 100644 index 0000000..dd60882 --- /dev/null +++ b/test/support/active_record_connection.rb @@ -0,0 +1,7 @@ +require 'otr-activerecord' + +if ENV["TEST_DATABASE_URL"].to_s != "" + OTR::ActiveRecord.configure_from_url!(ENV["TEST_DATABASE_URL"]) +else + OTR::ActiveRecord.configure_from_hash!(adapter: "sqlite3", database: ":memory:", encoding: "utf8", pool: 5, timeout: 5000) +end diff --git a/test/support/active_record_models.rb b/test/support/active_record_models.rb new file mode 100644 index 0000000..3042c7f --- /dev/null +++ b/test/support/active_record_models.rb @@ -0,0 +1,7 @@ +class Category < ActiveRecord::Base + has_many :widgets +end + +class Widget < ActiveRecord::Base + belongs_to :category +end diff --git a/test/support/active_record_schema.rb b/test/support/active_record_schema.rb new file mode 100644 index 0000000..809c594 --- /dev/null +++ b/test/support/active_record_schema.rb @@ -0,0 +1,19 @@ +ActiveRecord::Base.logger = nil + +module Schema + def self.load! + ActiveRecord::Base.connection.instance_eval do + drop_table :categories if table_exists? :categories + create_table :categories do |t| + t.string :name, null: false + end + + drop_table :widgets if table_exists? :widgets + create_table :widgets do |t| + t.string :name, null: false + t.integer :category_id + end + add_index :widgets, :category_id + end + end +end diff --git a/test/support/test_app.rb b/test/support/test_app.rb new file mode 100644 index 0000000..13b3ad4 --- /dev/null +++ b/test/support/test_app.rb @@ -0,0 +1,100 @@ +require 'fileutils' + +module TestApp + DIR = "/tmp/otr-activerecord-test-app" + DB_DEV = "db/development.sqlite3" + DB_TEST = "db/test.sqlite3" + DB_PROD = "db/production.sqlite3" + + def self.delete + FileUtils.rm_r DIR + end + + def self.exists? + Dir.exist? DIR + end + + def self.create + FileUtils.mkdir_p DIR + FileUtils.mkdir_p File.join(DIR, "db", "migrate") + File.write File.join(DIR, "Gemfile"), gemfile + File.write File.join(DIR, "Rakefile"), rakefile + File.write File.join(DIR, "models.rb"), models + File.write File.join(DIR, "db", "schema.rb"), schemarb + File.write File.join(DIR, "db", "seeds.rb"), seedsrb + File.write File.join(DIR, "db", "config.yaml"), dbconfig + end + + def self.models + <<-STR + class Widget < ActiveRecord::Base + end + STR + end + + def self.gemfile + File + .readlines(File.join("gemfiles", "ar_#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}.gemfile")) + .select { |line| line =~ /source|rake|activerecord|sqlite/ } + .push("gem 'otr-activerecord', path: #{Rake.application.original_dir}") + .join("") + end + + def self.rakefile + <<-STR + require 'json' + require 'bundler/setup' + load 'tasks/otr-activerecord.rake' + + namespace :db do + task :environment do + require_relative "./models" + OTR::ActiveRecord.configure_from_file! "db/config.yaml" + OTR::ActiveRecord.establish_connection! + end + end + + namespace :widgets do + desc "List all widgets" + task :list => 'db:environment' do + widgets = Widget.order('name').to_a + puts widgets.map(&:as_json).to_json + end + end + STR + end + + def self.dbconfig + <<-STR + development: + adapter: sqlite3 + database: #{DB_DEV} + + test: + adapter: sqlite3 + database: #{DB_TEST} + + production: + adapter: sqlite3 + database: #{DB_PROD} + STR + end + + def self.schemarb + <<-STR + ActiveRecord::Schema.define(version: 2026_07_02_013351) do + create_table "widgets", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + end + STR + end + + def self.seedsrb + <<-STR + Widget.create!(name: "Foo") + STR + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..aa5d1b4 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,5 @@ +require 'otr-activerecord' +require 'rack' +require 'minitest/autorun' + +Dir.glob('./test/support/*.rb').each { |file| require file }