From 9414c33f298698a6a406f7c306879959c1ca061d Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 10:52:36 +0900 Subject: [PATCH 01/11] add CI with ASAN - add ruby-asan CI test - skip some failed test --- .github/workflows/ruby_asan_on_ubuntu.yml | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/ruby_asan_on_ubuntu.yml diff --git a/.github/workflows/ruby_asan_on_ubuntu.yml b/.github/workflows/ruby_asan_on_ubuntu.yml new file mode 100644 index 00000000..a99d9c67 --- /dev/null +++ b/.github/workflows/ruby_asan_on_ubuntu.yml @@ -0,0 +1,71 @@ +name: Ubuntu + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + - reopened + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + ruby: ['asan'] + duckdb: ['1.1.3', '1.1.1', '1.2.0'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + + - name: duckdb cache + id: duckdb-cache + uses: actions/cache@v4 + with: + path: duckdb-v${{ matrix.duckdb }} + key: ${{ runner.os }}-duckdb-v${{ matrix.duckdb }} + + - name: Build duckdb ${{ matrix.duckdb }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + if: steps.duckdb-cache.outputs.cache-hit != 'true' + run: | + git clone -b v$DUCKDB_VERSION https://github.com/cwida/duckdb.git duckdb-tmp-v$DUCKDB_VERSION + cd duckdb-tmp-v$DUCKDB_VERSION && make && cd .. + rm -rf duckdb-v$DUCKDB_VERSION + mkdir -p duckdb-v$DUCKDB_VERSION/build/release/src duckdb-v$DUCKDB_VERSION/src + cp -rip duckdb-tmp-v$DUCKDB_VERSION/build/release/src/*.so duckdb-v$DUCKDB_VERSION/build/release/src + cp -rip duckdb-tmp-v$DUCKDB_VERSION/src/include duckdb-v$DUCKDB_VERSION/src/ + + - name: bundle install with Ruby ${{ matrix.ruby }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + run: | + bundle install --jobs 4 --retry 3 + + - name: Build test with DUCKDB_API_NO_DEPRECATED and Ruby ${{ matrix.ruby }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + run: | + env DUCKDB_API_NO_DEPRECATED=1 bundle exec rake build -- --with-duckdb-include=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/src/include --with-duckdb-lib=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/build/release/src/ + bundle exec rake clean + + - name: Build with Ruby ${{ matrix.ruby }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + run: | + bundle exec rake build -- --with-duckdb-include=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/src/include --with-duckdb-lib=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/build/release/src/ + + - name: test with Ruby ${{ matrix.ruby }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + run: | + env RUBYOPT=-W:deprecated ASAN_TEST=1 bundle exec rake test From 979a9cc8c72f4bbca6305121303d916aebb3a48b Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 10:58:58 +0900 Subject: [PATCH 02/11] skip ASAN test --- test/duckdb_test/database_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/duckdb_test/database_test.rb b/test/duckdb_test/database_test.rb index c1ae3c40..8e14e206 100644 --- a/test/duckdb_test/database_test.rb +++ b/test/duckdb_test/database_test.rb @@ -81,6 +81,7 @@ def test_connect_with_block end def test_close + skip 'asan test with exception' if ENV['ASAN_TEST'] == '1' db = DuckDB::Database.open con = db.connect db.close From b229ca2b01d3a31822fee9ca6524c834bc0f0b91 Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 11:05:31 +0900 Subject: [PATCH 03/11] drop table only if the table exists --- test/duckdb_test/appender_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/duckdb_test/appender_test.rb b/test/duckdb_test/appender_test.rb index db77fdda..df519821 100644 --- a/test/duckdb_test/appender_test.rb +++ b/test/duckdb_test/appender_test.rb @@ -11,7 +11,7 @@ def setup end def safe_drop_table - @con.execute("DROP TABLE #{table};") + @con.execute("DROP TABLE IF EXISTS #{table};") rescue DuckDB::Error # ignore DuckDB::Error end From 5cf8ea533fba3783552d01ad04de9e699f3de36a Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 11:13:24 +0900 Subject: [PATCH 04/11] skip ASAN failed test. --- test/duckdb_test/connection_test.rb | 1 + test/duckdb_test/prepared_statement_test.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/test/duckdb_test/connection_test.rb b/test/duckdb_test/connection_test.rb index f5e4c6d5..5ba6f56f 100644 --- a/test/duckdb_test/connection_test.rb +++ b/test/duckdb_test/connection_test.rb @@ -108,6 +108,7 @@ def test_async_query_stream_with_valid_params end def test_async_query_stream_with_invalid_params + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' assert_raises(DuckDB::Error) { @con.async_query_stream('foo', 'bar') } assert_raises(ArgumentError) { @con.async_query_stream } diff --git a/test/duckdb_test/prepared_statement_test.rb b/test/duckdb_test/prepared_statement_test.rb index fc96166e..5da66097 100644 --- a/test/duckdb_test/prepared_statement_test.rb +++ b/test/duckdb_test/prepared_statement_test.rb @@ -428,6 +428,7 @@ def test_bind_varchar_date end def test_bind_varchar_date_with_invalid_timestamp_string + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' stmt = DuckDB::PreparedStatement.new(@con, 'SELECT * FROM a WHERE col_date = $1') stmt.bind_varchar(1, 'invalid_date_string') From 98fb4ca30b1ef49305185e717c5fde9ebf9fb276 Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 11:19:04 +0900 Subject: [PATCH 05/11] skip ASAN test with Exception. --- test/duckdb_test/connection_execute_multiple_sql_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/duckdb_test/connection_execute_multiple_sql_test.rb b/test/duckdb_test/connection_execute_multiple_sql_test.rb index b3b95e4d..15c95965 100644 --- a/test/duckdb_test/connection_execute_multiple_sql_test.rb +++ b/test/duckdb_test/connection_execute_multiple_sql_test.rb @@ -10,6 +10,7 @@ def setup end def test_multiple_sql + skip('test with ASAN') if ENV['ASAN_TEST'] == '1' exception = assert_raises(DuckDB::Error) do @con.execute('CREATE TABLE test (v VARCHAR); CREATE TABLE test (v VARCHAR); SELECT 42;') end From c9d468ade2fa4754c9d16e22ce19e1b6fc088a94 Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 11:25:45 +0900 Subject: [PATCH 06/11] skip exception test with ASAN --- test/duckdb_test/prepared_statement_test.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/duckdb_test/prepared_statement_test.rb b/test/duckdb_test/prepared_statement_test.rb index 5da66097..67836f08 100644 --- a/test/duckdb_test/prepared_statement_test.rb +++ b/test/duckdb_test/prepared_statement_test.rb @@ -121,7 +121,10 @@ def test_execute stmt = DuckDB::PreparedStatement.new(@con, 'SELECT * FROM a') result = stmt.execute assert_instance_of(DuckDB::Result, result) + end + def test_execute_with_exception + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' stmt = DuckDB::PreparedStatement.new(@con, 'SELECT * FROM a where id = ?') assert_raises(DuckDB::Error) { stmt.execute } end From b287a16e3b806f6719a189a85d69208c57868c0d Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 11:31:00 +0900 Subject: [PATCH 07/11] skip ASAN test with exception. --- test/duckdb_test/config_test.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/duckdb_test/config_test.rb b/test/duckdb_test/config_test.rb index f09220a4..4d567701 100644 --- a/test/duckdb_test/config_test.rb +++ b/test/duckdb_test/config_test.rb @@ -42,7 +42,10 @@ def test_s_key_descriptions def test_set_config config = DuckDB::Config.new assert_instance_of(DuckDB::Config, config.set_config('access_mode', 'READ_ONLY')) + end + def test_set_config_with_exception + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' assert_raises(DuckDB::Error) do config.set_config('access_mode', 'INVALID_VALUE') end From a32a899c2c13a10c7789085a171a30d369f2e6b9 Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 11:34:47 +0900 Subject: [PATCH 08/11] fix failed ruby without ASAN. --- test/duckdb_test/config_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/duckdb_test/config_test.rb b/test/duckdb_test/config_test.rb index 4d567701..963b07da 100644 --- a/test/duckdb_test/config_test.rb +++ b/test/duckdb_test/config_test.rb @@ -46,6 +46,7 @@ def test_set_config def test_set_config_with_exception skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' + config = DuckDB::Config.new assert_raises(DuckDB::Error) do config.set_config('access_mode', 'INVALID_VALUE') end From 099fd5cfcb56d89d93fcd1beeff4389b7d05dbe5 Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 11:38:38 +0900 Subject: [PATCH 09/11] skip test with ruby ASAN. --- test/duckdb_test/extracted_statements_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/duckdb_test/extracted_statements_test.rb b/test/duckdb_test/extracted_statements_test.rb index 8935879d..13e9feef 100644 --- a/test/duckdb_test/extracted_statements_test.rb +++ b/test/duckdb_test/extracted_statements_test.rb @@ -23,6 +23,7 @@ def test_s_new end def test_s_new_with_invalid_sql + skip('test with ASAN') if ENV['ASAN_TEST'] == '1' ex = assert_raises DuckDB::Error do DuckDB::ExtractedStatements.new(@con, 'SELECT 1; INVALID STATEMENT; SELECT 3') end From 89b1bc86473eafe5f21cb3443c05cde814e91ebf Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 2 Mar 2025 11:43:00 +0900 Subject: [PATCH 10/11] skip test with ruby-asan --- test/duckdb_test/appender_test.rb | 9 ++++++- test/duckdb_test/config_test.rb | 1 + test/duckdb_test/connection_test.rb | 30 +++++++++++++-------- test/duckdb_test/database_test.rb | 3 +++ test/duckdb_test/prepared_statement_test.rb | 6 +++++ 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/test/duckdb_test/appender_test.rb b/test/duckdb_test/appender_test.rb index df519821..b5121e59 100644 --- a/test/duckdb_test/appender_test.rb +++ b/test/duckdb_test/appender_test.rb @@ -49,8 +49,12 @@ def test_s_new_with_schema @con.execute('CREATE SCHEMA a; CREATE TABLE a.b (id INT);') appender = DuckDB::Appender.new(@con, 'a', 'b') assert_instance_of(DuckDB::Appender, appender) + end - assert_raises(DuckDB::Error) { appender = DuckDB::Appender.new(@con, 'b', 'b') } + def test_s_new_with_invalid_schema + skip('test with ASAN') if ENV['ASAN_TEST'] == '1' + @con.execute('CREATE SCHEMA a; CREATE TABLE a.b (id INT);') + assert_raises(DuckDB::Error) { DuckDB::Appender.new(@con, 'b', 'b') } end def sub_test_append_column2(method, type, values:, expected:) @@ -103,6 +107,7 @@ def test_flush end def test_flush_with_exception + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' appender = create_appender('col BOOLEAN NOT NULL') appender .append_null @@ -119,6 +124,7 @@ def test_end_row end def test_end_row_with_exception + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' appender = create_appender('col BOOLEAN') exception = assert_raises(DuckDB::Error) { appender.end_row } assert_match(/Call to EndRow/, exception.message) @@ -133,6 +139,7 @@ def test_close end def test_close_with_exception + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' appender = create_appender('col BOOLEAN NOT NULL') appender .append_null diff --git a/test/duckdb_test/config_test.rb b/test/duckdb_test/config_test.rb index 963b07da..941cadaa 100644 --- a/test/duckdb_test/config_test.rb +++ b/test/duckdb_test/config_test.rb @@ -53,6 +53,7 @@ def test_set_config_with_exception end def test_set_invalid_option + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' config = DuckDB::Config.new assert_instance_of(DuckDB::Config, config.set_config('aaa_invalid_option', 'READ_ONLY')) assert_raises(DuckDB::Error) do diff --git a/test/duckdb_test/connection_test.rb b/test/duckdb_test/connection_test.rb index 5ba6f56f..6d1a6da7 100644 --- a/test/duckdb_test/connection_test.rb +++ b/test/duckdb_test/connection_test.rb @@ -35,19 +35,22 @@ def test_query_with_valid_hash_params assert_equal('a', r.each.first[1]) end - def test_query_with_invalid_params - assert_raises(DuckDB::Error) { @con.query('foo', 'bar') } - - assert_raises(ArgumentError) { @con.query } - - assert_raises(TypeError) { @con.query(1) } + def test_query_with_invalid_params_and_raise_duckdb_error + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' + assert_raises(DuckDB::Error) { @con.query('foo', 'bar') } assert_raises(DuckDB::Error) do invalid_sql = 'CREATE TABLE table1 (' @con.query(invalid_sql) end end + def test_query_with_invalid_params + assert_raises(ArgumentError) { @con.query } + + assert_raises(TypeError) { @con.query(1) } + end + def test_async_query pending_result = @con.async_query('CREATE TABLE table1 (id INTEGER)') assert_instance_of(DuckDB::PendingResult, pending_result) @@ -79,6 +82,7 @@ def test_async_query_with_valid_hash_params end def test_async_query_with_invalid_params + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' assert_raises(DuckDB::Error) { @con.async_query('foo', 'bar') } assert_raises(ArgumentError) { @con.async_query } @@ -178,11 +182,15 @@ def test_query_progress "QueryProgress: total_rows_to_process(#{total_rows_to_process}) to be >= rows_processed(#{rows_processed})" ) - # test interrupt - @con.interrupt - while pending_result.state == :not_ready - pending_result.execute_task - assert(pending_result.state != :ready, 'pending_result.state should not be :ready') + if ENV['ASAN_TEST'].nil? + # test interrupt + @con.interrupt + while pending_result.state == :not_ready + pending_result.execute_task + assert(pending_result.state != :ready, 'pending_result.state should not be :ready') + end + else + skip 'test with ASAN' end end diff --git a/test/duckdb_test/database_test.rb b/test/duckdb_test/database_test.rb index 8e14e206..f6c5ca39 100644 --- a/test/duckdb_test/database_test.rb +++ b/test/duckdb_test/database_test.rb @@ -30,7 +30,10 @@ def test_s_open_argument assert_raises(TypeError) { DuckDB::Database.open('foo', 'bar') } assert_raises(TypeError) { DuckDB::Database.open(1) } + end + def test_s_open_invalid_argument + skip('test with ASAN') if ENV['ASAN_TEST'] == '1' assert_raises(DuckDB::Error) do not_exist_path = "#{create_path}/#{create_path}" DuckDB::Database.open(not_exist_path) diff --git a/test/duckdb_test/prepared_statement_test.rb b/test/duckdb_test/prepared_statement_test.rb index 67836f08..31e039d2 100644 --- a/test/duckdb_test/prepared_statement_test.rb +++ b/test/duckdb_test/prepared_statement_test.rb @@ -102,6 +102,10 @@ def test_s_new assert_raises(ArgumentError) { DuckDB::PreparedStatement.new } assert_raises(TypeError) { DuckDB::PreparedStatement.new(@con, 1) } assert_raises(TypeError) { DuckDB::PreparedStatement.new(1, 1) } + end + + def test_s_new_with_duckdb_error + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' assert_raises(DuckDB::Error) { DuckDB::PreparedStatement.new(@con, 'SELECT * FROM') } end @@ -153,6 +157,7 @@ def test_param_type end def test_clear_bindings + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' stmt = DuckDB::PreparedStatement.new(@con, 'SELECT * FROM a WHERE id = $1') stmt.bind(1, 1) stmt.clear_bindings @@ -447,6 +452,7 @@ def test_bind_varchar_timestamp end def test_bind_varchar_timestamp_with_invalid_timestamp_string + skip 'test with ASAN' if ENV['ASAN_TEST'] == '1' stmt = DuckDB::PreparedStatement.new(@con, 'SELECT * FROM a WHERE col_timestamp = $1') stmt.bind_varchar(1, 'invalid_timestamp_string') From 6820ac345e21244c136e60ca984a9422ebefb290 Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sun, 16 Mar 2025 06:15:56 +0900 Subject: [PATCH 11/11] use only database test with asan --- .github/workflows/ruby_asan_on_ubuntu.yml | 2 +- ext/duckdb/database.c | 2 +- test/duckdb_test/asan_test.rb | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 test/duckdb_test/asan_test.rb diff --git a/.github/workflows/ruby_asan_on_ubuntu.yml b/.github/workflows/ruby_asan_on_ubuntu.yml index a99d9c67..8e3ea628 100644 --- a/.github/workflows/ruby_asan_on_ubuntu.yml +++ b/.github/workflows/ruby_asan_on_ubuntu.yml @@ -68,4 +68,4 @@ jobs: env: DUCKDB_VERSION: ${{ matrix.duckdb }} run: | - env RUBYOPT=-W:deprecated ASAN_TEST=1 bundle exec rake test + env RUBYOPT=-W:deprecated ASAN_TEST=1 bundle exec ruby -Ilib test/duckdb_test/asan_test.rb diff --git a/ext/duckdb/database.c b/ext/duckdb/database.c index 3004765b..e04f060e 100644 --- a/ext/duckdb/database.c +++ b/ext/duckdb/database.c @@ -18,7 +18,7 @@ static const rb_data_type_t database_data_type = { }; static void close_database(rubyDuckDB *p) { - duckdb_close(&(p->db)); + if (p->db) duckdb_close(&(p->db)); } static void deallocate(void * ctx) { diff --git a/test/duckdb_test/asan_test.rb b/test/duckdb_test/asan_test.rb new file mode 100644 index 00000000..21bc6f35 --- /dev/null +++ b/test/duckdb_test/asan_test.rb @@ -0,0 +1,6 @@ +require 'duckdb' +begin + DuckDB::Database.open('not_exist_dir/not_exist_file') +rescue + puts "Error: #{$!}" +end