diff --git a/.github/workflows/ruby_asan_on_ubuntu.yml b/.github/workflows/ruby_asan_on_ubuntu.yml new file mode 100644 index 00000000..8e3ea628 --- /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 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/appender_test.rb b/test/duckdb_test/appender_test.rb index db77fdda..b5121e59 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 @@ -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/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 diff --git a/test/duckdb_test/config_test.rb b/test/duckdb_test/config_test.rb index f09220a4..941cadaa 100644 --- a/test/duckdb_test/config_test.rb +++ b/test/duckdb_test/config_test.rb @@ -42,13 +42,18 @@ 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' + config = DuckDB::Config.new assert_raises(DuckDB::Error) do config.set_config('access_mode', 'INVALID_VALUE') end 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_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 diff --git a/test/duckdb_test/connection_test.rb b/test/duckdb_test/connection_test.rb index f5e4c6d5..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 } @@ -108,6 +112,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 } @@ -177,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 c1ae3c40..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) @@ -81,6 +84,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 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 diff --git a/test/duckdb_test/prepared_statement_test.rb b/test/duckdb_test/prepared_statement_test.rb index fc96166e..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 @@ -121,7 +125,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 @@ -150,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 @@ -428,6 +436,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') @@ -443,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')