diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0503441..ba6ab49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,21 @@ jobs: MYSQL_PASSWORD: with_advisory_pass MYSQL_DATABASE: with_advisory_lock_test MYSQL_ROOT_HOST: '%' + mariadb: + image: mariadb:12 + ports: + - 3306 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: with_advisory_lock_trilogy_test + MARIADB_USER: with_advisory + MARIADB_PASSWORD: with_advisory_pass + MARIADB_ROOT_HOST: '%' + options: >- + --health-cmd "healthcheck.sh --su-mysql --connect --innodb_initialized" + --health-interval 10s + --health-timeout 5s + --health-retries 5 strategy: fail-fast: false matrix: @@ -48,6 +63,11 @@ jobs: - 7.2 - "8.0" - "8.1" + - "edge" + exclude: + # TruffleRuby doesn't support Rails edge yet + - ruby: 'truffleruby' + rails: "edge" env: ACTIVERECORD_VERSION: ${{ matrix.rails }} RAILS_ENV: test @@ -62,10 +82,14 @@ jobs: bundler-cache: true rubygems: latest + - name: Setup test databases + timeout-minutes: 5 env: DATABASE_URL_PG: postgres://with_advisory:with_advisory_pass@localhost:${{ job.services.postgres.ports[5432] }}/with_advisory_lock_test DATABASE_URL_MYSQL: mysql2://with_advisory:with_advisory_pass@127.0.0.1:${{ job.services.mysql.ports[3306] }}/with_advisory_lock_test + # Trilogy doesn't support TruffleRuby + DATABASE_URL_TRILOGY: ${{ matrix.ruby != 'truffleruby' && format('trilogy://with_advisory:with_advisory_pass@127.0.0.1:{0}/with_advisory_lock_trilogy_test', job.services.mariadb.ports[3306]) || '' }} run: | cd test/dummy bundle exec rake db:test:prepare @@ -74,5 +98,7 @@ jobs: env: DATABASE_URL_PG: postgres://with_advisory:with_advisory_pass@localhost:${{ job.services.postgres.ports[5432] }}/with_advisory_lock_test DATABASE_URL_MYSQL: mysql2://with_advisory:with_advisory_pass@127.0.0.1:${{ job.services.mysql.ports[3306] }}/with_advisory_lock_test + # Trilogy doesn't support TruffleRuby + DATABASE_URL_TRILOGY: ${{ matrix.ruby != 'truffleruby' && format('trilogy://with_advisory:with_advisory_pass@127.0.0.1:{0}/with_advisory_lock_trilogy_test', job.services.mariadb.ports[3306]) || '' }} WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} run: bin/rails test diff --git a/Gemfile b/Gemfile index c2de31e..bd2a962 100644 --- a/Gemfile +++ b/Gemfile @@ -11,12 +11,17 @@ gem 'benchmark' gem 'logger' gem 'ostruct' -activerecord_version = ENV.fetch('ACTIVERECORD_VERSION', '7.2') - -gem 'activerecord', "~> #{activerecord_version}.0" +activerecord_version = ENV.fetch('ACTIVERECORD_VERSION', '8.1') + +if activerecord_version == 'edge' + gem 'activerecord', github: 'rails/rails', branch: 'main' + gem 'railties', github: 'rails/rails', branch: 'main' +else + gem 'activerecord', "~> #{activerecord_version}.0" + gem 'railties' +end gem 'dotenv' -gem 'railties' platforms :ruby do gem 'mysql2' diff --git a/docker-compose.yml b/docker-compose.yml index 26fca3e..d919e49 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,18 +2,28 @@ services: pg: image: postgres:17-alpine environment: - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: with_advisory_lock_test ports: - "5433:5432" mysql: image: mysql:8 environment: - MYSQL_USER: ${DB_USER} - MYSQL_PASSWORD: ${DB_PASSWORD} - MYSQL_DATABASE: ${DB_NAME} + MYSQL_USER: test + MYSQL_PASSWORD: test + MYSQL_DATABASE: with_advisory_lock_test MYSQL_RANDOM_ROOT_PASSWORD: "yes" MYSQL_ROOT_HOST: '%' ports: - "3366:3306" + mariadb: + image: mariadb:12 + environment: + MARIADB_USER: test + MARIADB_PASSWORD: test + MARIADB_DATABASE: with_advisory_lock_test_trilogy + MARIADB_RANDOM_ROOT_PASSWORD: "yes" + MARIADB_ROOT_HOST: '%' + ports: + - "3368:3306" diff --git a/lib/with_advisory_lock/mysql_advisory.rb b/lib/with_advisory_lock/mysql_advisory.rb index 02e35ce..db79804 100644 --- a/lib/with_advisory_lock/mysql_advisory.rb +++ b/lib/with_advisory_lock/mysql_advisory.rb @@ -16,12 +16,15 @@ def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seco # MySQL's GET_LOCK already provides native timeout support, making the blocking # parameter redundant. MySQL doesn't have separate try/blocking functions like PostgreSQL. - # MySQL GET_LOCK supports native timeout: - # - timeout_seconds = nil: wait indefinitely (-1) + # MySQL/MariaDB GET_LOCK supports native timeout: + # - timeout_seconds = nil: wait indefinitely # - timeout_seconds = 0: try once, no wait (0) # - timeout_seconds > 0: wait up to timeout_seconds + # + # Note: MySQL accepts -1 for infinite wait, but MariaDB does not. + # Using a large value (1 year) for cross-compatibility. mysql_timeout = case timeout_seconds - when nil then -1 + when nil then 31_536_000 # 1 year in seconds when 0 then 0 else timeout_seconds.to_i end diff --git a/lib/with_advisory_lock/postgresql_advisory.rb b/lib/with_advisory_lock/postgresql_advisory.rb index 9c9b2b3..6f43130 100644 --- a/lib/with_advisory_lock/postgresql_advisory.rb +++ b/lib/with_advisory_lock/postgresql_advisory.rb @@ -19,10 +19,20 @@ def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seco advisory_try_lock_function(transaction, shared) end execute_advisory(function, lock_keys, lock_name, blocking: blocking) + rescue ActiveRecord::Deadlocked + # Rails 8.2+ raises ActiveRecord::Deadlocked directly for PostgreSQL deadlocks + # When using blocking locks, treat deadlocks as lock acquisition failure + return false if blocking + + raise rescue ActiveRecord::StatementInvalid => e # PostgreSQL deadlock detection raises PG::TRDeadlockDetected (SQLSTATE 40P01) - # When using blocking locks, treat deadlocks as lock acquisition failure - if blocking && (e.cause.is_a?(PG::TRDeadlockDetected) || e.message.include?('deadlock detected')) + # When using blocking locks, treat deadlocks as lock acquisition failure. + # Rails 8.2+ may also retry after deadlock and get "current transaction is aborted" + # when the transaction was rolled back by PostgreSQL's deadlock detection. + if blocking && (e.cause.is_a?(PG::TRDeadlockDetected) || + e.message.include?('deadlock detected') || + e.message =~ ERROR_MESSAGE_REGEX) false else raise @@ -117,13 +127,23 @@ def advisory_unlock_function(shared) end def execute_advisory(function, lock_keys, lock_name, blocking: false) + sql = prepare_sql(function, lock_keys, lock_name) if blocking - # Blocking locks return void - if the query executes successfully, the lock was acquired - query_value(prepare_sql(function, lock_keys, lock_name)) + # Blocking locks return void - if the query executes successfully, the lock was acquired. + # Rails 8.2+ uses lazy transaction materialization. We must use materialize_transactions: true + # to ensure the transaction is started on the database before acquiring the lock, + # otherwise the lock won't actually block other connections. + if respond_to?(:internal_exec_query, true) + # Rails < 8.2 + query_value(sql) + else + # Rails 8.2+ - use query_all with materialize_transactions: true + send(:query_all, sql, 'AdvisoryLock', materialize_transactions: true) + end true else # Non-blocking try locks return boolean - result = query_value(prepare_sql(function, lock_keys, lock_name)) + result = query_value(sql) LOCK_RESULT_VALUES.include?(result) end end diff --git a/test/dummy/app/models/trilogy_label.rb b/test/dummy/app/models/trilogy_label.rb new file mode 100644 index 0000000..53dd907 --- /dev/null +++ b/test/dummy/app/models/trilogy_label.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class TrilogyLabel < TrilogyRecord + self.table_name = 'trilogy_labels' +end diff --git a/test/dummy/app/models/trilogy_record.rb b/test/dummy/app/models/trilogy_record.rb new file mode 100644 index 0000000..228692b --- /dev/null +++ b/test/dummy/app/models/trilogy_record.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class TrilogyRecord < ActiveRecord::Base + self.abstract_class = true + establish_connection :trilogy +end diff --git a/test/dummy/app/models/trilogy_tag.rb b/test/dummy/app/models/trilogy_tag.rb new file mode 100644 index 0000000..06edba2 --- /dev/null +++ b/test/dummy/app/models/trilogy_tag.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class TrilogyTag < TrilogyRecord + self.table_name = 'trilogy_tags' + + after_save do + TrilogyTagAudit.create(tag_name: name) + TrilogyLabel.create(name: name) + end +end diff --git a/test/dummy/app/models/trilogy_tag_audit.rb b/test/dummy/app/models/trilogy_tag_audit.rb new file mode 100644 index 0000000..e8dfdee --- /dev/null +++ b/test/dummy/app/models/trilogy_tag_audit.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class TrilogyTagAudit < TrilogyRecord + self.table_name = 'trilogy_tag_audits' +end diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index 01fda01..9814dc6 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -14,6 +14,17 @@ module TestSystemApp class Application < Rails::Application config.load_defaults [Rails::VERSION::MAJOR, Rails::VERSION::MINOR].join('.') config.eager_load = true + + # Ignore trilogy models when DATABASE_URL_TRILOGY is not set (e.g., TruffleRuby) + unless ENV['DATABASE_URL_TRILOGY'] && !ENV['DATABASE_URL_TRILOGY'].empty? + config.autoload_lib(ignore: %w[]) + initializer 'ignore_trilogy_models', before: :set_autoload_paths do |app| + trilogy_models = %w[trilogy_record trilogy_tag trilogy_tag_audit trilogy_label] + trilogy_models.each do |model| + Rails.autoloaders.main.ignore(Rails.root.join('app', 'models', "#{model}.rb")) + end + end + end config.serve_static_files = false config.public_file_server.enabled = false config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml index 8c73631..c0a07e6 100644 --- a/test/dummy/config/database.yml +++ b/test/dummy/config/database.yml @@ -11,3 +11,11 @@ test: url: "<%= ENV['DATABASE_URL_MYSQL'] %>" properties: allowPublicKeyRetrieval: true +<% if ENV['DATABASE_URL_TRILOGY'] && !ENV['DATABASE_URL_TRILOGY'].empty? %> + trilogy: + <<: *default + url: "<%= ENV['DATABASE_URL_TRILOGY'] %>" + adapter: trilogy + properties: + allowPublicKeyRetrieval: true +<% end %> diff --git a/test/dummy/db/trilogy_schema.rb b/test/dummy/db/trilogy_schema.rb new file mode 100644 index 0000000..fbfa1e7 --- /dev/null +++ b/test/dummy/db/trilogy_schema.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +ActiveRecord::Schema.define(version: 1) do + create_table 'trilogy_tags', force: true do |t| + t.string 'name' + end + + create_table 'trilogy_tag_audits', id: false, force: true do |t| + t.string 'tag_name' + end + + create_table 'trilogy_labels', id: false, force: true do |t| + t.string 'name' + end +end \ No newline at end of file diff --git a/test/dummy/lib/tasks/db.rake b/test/dummy/lib/tasks/db.rake index c5a7655..23acd47 100644 --- a/test/dummy/lib/tasks/db.rake +++ b/test/dummy/lib/tasks/db.rake @@ -4,37 +4,29 @@ namespace :db do namespace :test do desc 'Load schema for all databases' task prepare: :environment do - # Load schema for primary database + # Setup PostgreSQL database ActiveRecord::Base.establish_connection(:primary) - ActiveRecord::Schema.define(version: 1) do - create_table 'tags', force: true do |t| - t.string 'name' - end + load Rails.root.join('db', 'schema.rb') + puts 'PostgreSQL database schema loaded' - create_table 'tag_audits', id: false, force: true do |t| - t.string 'tag_name' - end - - create_table 'labels', id: false, force: true do |t| - t.string 'name' - end - end - - # Load schema for secondary database + # Setup MySQL database ActiveRecord::Base.establish_connection(:secondary) - ActiveRecord::Schema.define(version: 1) do - create_table 'mysql_tags', force: true do |t| - t.string 'name' - end + load Rails.root.join('db', 'secondary_schema.rb') + puts 'MySQL database schema loaded' - create_table 'mysql_tag_audits', id: false, force: true do |t| - t.string 'tag_name' - end - - create_table 'mysql_labels', id: false, force: true do |t| - t.string 'name' - end + # Setup Trilogy database (MariaDB) - optional, not supported on TruffleRuby + if ENV['DATABASE_URL_TRILOGY'] && !ENV['DATABASE_URL_TRILOGY'].empty? + ActiveRecord::Base.establish_connection(:trilogy) + load Rails.root.join('db', 'trilogy_schema.rb') + puts 'Trilogy database schema loaded' + else + puts 'Skipping Trilogy database (DATABASE_URL_TRILOGY not set)' end + + puts 'All test databases prepared successfully' + rescue StandardError => e + puts "Error preparing test databases: #{e.message}" + raise e end end end diff --git a/test/sanity_check_test.rb b/test/sanity_check_test.rb index 4917fed..a2932d3 100644 --- a/test/sanity_check_test.rb +++ b/test/sanity_check_test.rb @@ -43,7 +43,7 @@ class SanityCheckTest < GemTestCase assert_equal 'Mysql2', MysqlLabel.connection.adapter_name end - test 'can write to both databases in same test' do + test 'can write to PostgreSQL and MySQL databases in same test' do # Create records in both databases pg_tag = Tag.create!(name: 'test-pg') mysql_tag = MysqlTag.create!(name: 'test-mysql') @@ -61,3 +61,50 @@ class SanityCheckTest < GemTestCase mysql_tag.destroy end end + +if GemTestCase.trilogy_available? + class TrilogySanityCheckTest < GemTestCase + test 'Trilogy database is isolated from PostgreSQL and MySQL' do + # Create tags in all databases + pg_tag = Tag.create!(name: 'pg-isolation-test') + mysql_tag = MysqlTag.create!(name: 'mysql-isolation-test') + trilogy_tag = TrilogyTag.create!(name: 'trilogy-isolation-test') + + # Verify Trilogy tag exists only in Trilogy + assert TrilogyTag.exists?(name: 'trilogy-isolation-test') + assert_not Tag.exists?(name: 'trilogy-isolation-test') + assert_not MysqlTag.exists?(name: 'trilogy-isolation-test') + + # Verify PostgreSQL tag doesn't exist in Trilogy + assert_not TrilogyTag.exists?(name: 'pg-isolation-test') + + # Verify MySQL tag doesn't exist in Trilogy + assert_not TrilogyTag.exists?(name: 'mysql-isolation-test') + + # Clean up + pg_tag.destroy + mysql_tag.destroy + trilogy_tag.destroy + end + + test 'Trilogy models use Trilogy adapter' do + assert_equal 'Trilogy', TrilogyTag.connection.adapter_name + assert_equal 'Trilogy', TrilogyTagAudit.connection.adapter_name + assert_equal 'Trilogy', TrilogyLabel.connection.adapter_name + end + + test 'can write to all three databases in same test' do + pg_tag = Tag.create!(name: 'test-pg') + mysql_tag = MysqlTag.create!(name: 'test-mysql') + trilogy_tag = TrilogyTag.create!(name: 'test-trilogy') + + assert pg_tag.persisted? + assert mysql_tag.persisted? + assert trilogy_tag.persisted? + + pg_tag.destroy + mysql_tag.destroy + trilogy_tag.destroy + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 09a6358..46e673b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,10 +19,19 @@ class GemTestCase < ActiveSupport::TestCase parallelize(workers: 1) def self.startup - # Validate environment variables when tests actually start running + # Validate required environment variables %w[DATABASE_URL_PG DATABASE_URL_MYSQL].each do |var| abort "Missing required environment variable: #{var}" if ENV[var].nil? || ENV[var].empty? end + + # Trilogy is optional (not supported on TruffleRuby) + if ENV['DATABASE_URL_TRILOGY'].nil? || ENV['DATABASE_URL_TRILOGY'].empty? + puts 'DATABASE_URL_TRILOGY not set, skipping Trilogy tests' + end + end + + def self.trilogy_available? + ENV['DATABASE_URL_TRILOGY'] && !ENV['DATABASE_URL_TRILOGY'].empty? end # Override in test classes to clean only the tables you need diff --git a/test/with_advisory_lock/blocking_test.rb b/test/with_advisory_lock/blocking_test.rb index 0b79476..bfd7eef 100644 --- a/test/with_advisory_lock/blocking_test.rb +++ b/test/with_advisory_lock/blocking_test.rb @@ -113,6 +113,21 @@ def setup end end +if GemTestCase.trilogy_available? + class TrilogyBlockingTest < GemTestCase + include BlockingTestCases + + def model_class + TrilogyTag + end + + def setup + super + TrilogyTag.delete_all + end + end +end + # Deadlock test requires non-transactional mode to work properly class PostgreSQLDeadlockTest < GemTestCase self.use_transactional_tests = false diff --git a/test/with_advisory_lock/concern_test.rb b/test/with_advisory_lock/concern_test.rb index 87c6aab..e5b4bc1 100644 --- a/test/with_advisory_lock/concern_test.rb +++ b/test/with_advisory_lock/concern_test.rb @@ -40,6 +40,16 @@ def model_class end end +if GemTestCase.trilogy_available? + class TrilogyConcernTest < GemTestCase + include ConcernTestCases + + def model_class + TrilogyTag + end + end +end + # This test is adapter-agnostic, so we only need to test it once class ActiveRecordQueryCacheTest < GemTestCase self.use_transactional_tests = false diff --git a/test/with_advisory_lock/lock_test.rb b/test/with_advisory_lock/lock_test.rb index de39246..ff8fecf 100644 --- a/test/with_advisory_lock/lock_test.rb +++ b/test/with_advisory_lock/lock_test.rb @@ -215,3 +215,49 @@ def setup end end end + +if GemTestCase.trilogy_available? + class TrilogyLockTest < GemTestCase + include LockTestCases + + def model_class + TrilogyTag + end + + def setup + super + TrilogyTag.delete_all + end + + test 'uses database timeout for Trilogy' do + assert model_class.connection.supports_database_timeout? + end + + test 'trilogy uses native timeout instead of polling' do + # This test verifies that Trilogy bypasses Ruby-level polling + # when timeout is specified, relying on GET_LOCK's native timeout + lock_name = 'trilogy_timeout_test' + + # Hold a lock in another connection - need to use the same prefixed name as the gem + other_conn = model_class.connection_pool.checkout + lock_keys = other_conn.lock_keys_for(lock_name) + other_conn.query_value("SELECT GET_LOCK(#{other_conn.quote(lock_keys.first)}, 0)") + + begin + # Attempt to acquire with a short timeout - should fail quickly + elapsed = Benchmark.realtime do + result = model_class.with_advisory_lock(lock_name, timeout_seconds: 1) { 'success' } + + # Should return false and complete within reasonable time (< 3 seconds) + # If it were using Ruby polling, it would take longer + assert_not result + end + + assert elapsed < 3.0, "Expected quick timeout, but took #{elapsed} seconds" + ensure + other_conn.query_value("SELECT RELEASE_LOCK(#{other_conn.quote(lock_keys.first)})") + model_class.connection_pool.checkin(other_conn) + end + end + end +end diff --git a/test/with_advisory_lock/multi_adapter_test.rb b/test/with_advisory_lock/multi_adapter_test.rb index 1ac1083..6659399 100644 --- a/test/with_advisory_lock/multi_adapter_test.rb +++ b/test/with_advisory_lock/multi_adapter_test.rb @@ -6,12 +6,42 @@ class MultiAdapterIsolationTest < GemTestCase test 'postgresql and mysql adapters do not overlap' do lock_name = 'multi-adapter-lock' + # PostgreSQL lock doesn't block MySQL Tag.with_advisory_lock(lock_name) do assert MysqlTag.with_advisory_lock(lock_name, timeout_seconds: 0) { true } end + # MySQL lock doesn't block PostgreSQL MysqlTag.with_advisory_lock(lock_name) do assert Tag.with_advisory_lock(lock_name, timeout_seconds: 0) { true } end end end + +if GemTestCase.trilogy_available? + class TrilogyMultiAdapterIsolationTest < GemTestCase + test 'trilogy adapter does not overlap with postgresql or mysql' do + lock_name = 'multi-adapter-lock' + + # PostgreSQL lock doesn't block Trilogy + Tag.with_advisory_lock(lock_name) do + assert TrilogyTag.with_advisory_lock(lock_name, timeout_seconds: 0) { true } + end + + # Trilogy lock doesn't block PostgreSQL + TrilogyTag.with_advisory_lock(lock_name) do + assert Tag.with_advisory_lock(lock_name, timeout_seconds: 0) { true } + end + + # MySQL lock doesn't block Trilogy + MysqlTag.with_advisory_lock(lock_name) do + assert TrilogyTag.with_advisory_lock(lock_name, timeout_seconds: 0) { true } + end + + # Trilogy lock doesn't block MySQL + TrilogyTag.with_advisory_lock(lock_name) do + assert MysqlTag.with_advisory_lock(lock_name, timeout_seconds: 0) { true } + end + end + end +end diff --git a/test/with_advisory_lock/parallelism_test.rb b/test/with_advisory_lock/parallelism_test.rb index 06620c7..9cfa580 100644 --- a/test/with_advisory_lock/parallelism_test.rb +++ b/test/with_advisory_lock/parallelism_test.rb @@ -99,3 +99,13 @@ def model_class MysqlTag end end + +if GemTestCase.trilogy_available? + class TrilogyParallelismTest < GemTestCase + include ParallelismTestCases + + def model_class + TrilogyTag + end + end +end diff --git a/test/with_advisory_lock/shared_test.rb b/test/with_advisory_lock/shared_test.rb index 0f9d8b4..c7293a8 100644 --- a/test/with_advisory_lock/shared_test.rb +++ b/test/with_advisory_lock/shared_test.rb @@ -124,3 +124,31 @@ class MySQLSharedLocksTest < GemTestCase assert_match(/shared locks are not supported/, exception.message) end end + +if GemTestCase.trilogy_available? + class TrilogySharedLocksTest < GemTestCase + self.use_transactional_tests = false + + test 'does not allow two exclusive locks' do + one = SharedTestWorker.new(TrilogyTag, false) + assert_predicate(one, :locked?) + + two = SharedTestWorker.new(TrilogyTag, false) + refute(two.locked?) + + one.cleanup! + two.cleanup! + end + + test 'raises an error when attempting to use a shared lock' do + one = SharedTestWorker.new(TrilogyTag, true) + assert_equal(false, one.locked?) + + exception = assert_raises(ArgumentError) do + one.cleanup! + end + + assert_match(/shared locks are not supported/, exception.message) + end + end +end diff --git a/test/with_advisory_lock/thread_test.rb b/test/with_advisory_lock/thread_test.rb index bfc00d7..bf7843c 100644 --- a/test/with_advisory_lock/thread_test.rb +++ b/test/with_advisory_lock/thread_test.rb @@ -81,3 +81,13 @@ def model_class MysqlTag end end + +if GemTestCase.trilogy_available? + class TrilogyThreadTest < GemTestCase + include ThreadTestCases + + def model_class + TrilogyTag + end + end +end diff --git a/test/with_advisory_lock/transaction_test.rb b/test/with_advisory_lock/transaction_test.rb index b33decf..51d4c80 100644 --- a/test/with_advisory_lock/transaction_test.rb +++ b/test/with_advisory_lock/transaction_test.rb @@ -73,3 +73,41 @@ class MySQLTransactionScopingTest < GemTestCase assert_match(/#{Regexp.escape('require an active transaction')}/, exception.message) end end + +if GemTestCase.trilogy_available? + class TrilogyTransactionScopingTest < GemTestCase + self.use_transactional_tests = false + + test 'raises an error when attempting to use transaction level locks' do + TrilogyTag.transaction do + exception = assert_raises(ArgumentError) do + TrilogyTag.with_advisory_lock 'test', transaction: true do + raise 'Trilogy transaction realm is forbidden!' + end + end + + assert_match(/#{Regexp.escape('not supported')}/, exception.message) + end + end + + test 'session locks work within transactions' do + lock_acquired = false + TrilogyTag.transaction do + TrilogyTag.with_advisory_lock 'test' do + lock_acquired = true + end + end + assert lock_acquired + end + + test 'raises an error when attempting to use transaction level locks outside a transaction' do + exception = assert_raises(ArgumentError) do + TrilogyTag.with_advisory_lock 'test', transaction: true do + raise 'Trilogy gates are closed!' + end + end + + assert_match(/#{Regexp.escape('require an active transaction')}/, exception.message) + end + end +end