diff --git a/.editorconfig b/.editorconfig index 8949f97e..883ecf7c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,7 @@ trim_trailing_whitespace=true insert_final_newline=true end_of_line=lf charset=utf-8 + +[*.yml] +indent_style=space +indent_size=2 diff --git a/.github/workflows/dub.yml b/.github/workflows/dub.yml index 33d18666..befa5fb7 100644 --- a/.github/workflows/dub.yml +++ b/.github/workflows/dub.yml @@ -9,7 +9,10 @@ on: schedule: - cron: '30 7 1 * *' push: + branches: + - master pull_request: + workflow_dispatch: jobs: build: diff --git a/.github/workflows/integration-testing.yml b/.github/workflows/integration-testing.yml index 1cce8a16..3df59d36 100644 --- a/.github/workflows/integration-testing.yml +++ b/.github/workflows/integration-testing.yml @@ -9,7 +9,10 @@ on: schedule: - cron: '30 7 1 * *' push: + branches: + - master pull_request: + workflow_dispatch: jobs: # mysql8-tests: @@ -41,37 +44,37 @@ jobs: # --health-retries 4 # steps: - # - uses: actions/checkout@v2 - - # - name: Install ${{ matrix.compiler }} - # uses: dlang-community/setup-dlang@v1 - # with: - # compiler: ${{ matrix.compiler }} - - # - name: Install dependencies on Ubuntu - # if: startsWith(matrix.os, 'ubuntu') - # run: sudo apt-get update && sudo apt-get install libevent-dev -y - - # ## Turns out the unittest-vibe-ut tried to connect to an actualy MySQL on 172.18.0.1 so it's not - # ## actually a unit test at all. It's an integration test and should be pulled out from the main - # ## codebase into a separate sub module - # - name: Run unittest-vibe-ut - # env: - # MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} - # run: | - # echo "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" > testConnectionStr.txt - # dub run -c unittest-vibe-ut -- -t - - # - name: Build The Example Project - # working-directory: ./examples/homePage - # run: dub build - - # - name: Run Example (MySQL 8) - # working-directory: ./examples/homePage - # env: - # MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} - # run: | - # ./example "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" + # - uses: actions/checkout@v2 + + # - name: Install ${{ matrix.compiler }} + # uses: dlang-community/setup-dlang@v1 + # with: + # compiler: ${{ matrix.compiler }} + + # - name: Set up test connection string + # env: + # MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} + # run: | + # echo "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" > testConnectionStr.txt + + # - name: Run unittests with Vibe.d + # run: | + # dub run ":integration-tests-vibe" + + # - name: Run unittests with Phobos + # run: | + # dub run ":integration-tests-phobos" + + # - name: Build The Example Project + # working-directory: ./examples/homePage + # run: dub build + + # - name: Run Example (MySQL 8) + # working-directory: ./examples/homePage + # env: + # MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} + # run: | + # ./example "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" mysql57-tests: name: MySQL 5.7 Tests ${{ matrix.compiler }} @@ -113,19 +116,25 @@ jobs: with: compiler: ${{ matrix.compiler }} - - name: Install dependencies on Ubuntu - if: startsWith(matrix.os, 'ubuntu') - run: sudo apt-get update && sudo apt-get install libevent-dev -y - - ## Turns out the unittest-vibe-ut tried to connect to an actualy MySQL on 172.18.0.1 so it's not - ## actually a unit test at all. It's an integration test and should be pulled out from the main - ## codebase into a separate sub module - - name: Run unittest-vibe-ut + - name: Set up test connection string env: MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} run: | echo "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" > testConnectionStr.txt - dub run -c unittest-vibe-ut -- -t + + - name: Run unittests with Vibe.d + run: | + dub run ":integration-tests-vibe" + + - name: Run unittests with Phobos + run: | + dub run ":integration-tests-phobos" + + - name: Run test connection utility + env: + MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} + run: | + dub run ":testconn" -- "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" - name: Build The Example Project working-directory: ./examples/homePage @@ -178,19 +187,25 @@ jobs: with: compiler: ${{ matrix.compiler }} - - name: Install dependencies on Ubuntu - if: startsWith(matrix.os, 'ubuntu') - run: sudo apt-get update && sudo apt-get install libevent-dev -y - - ## Turns out the unittest-vibe-ut tried to connect to an actualy MySQL on 172.18.0.1 so it's not - ## actually a unit test at all. It's an integration test and should be pulled out from the main - ## codebase into a separate sub module - - name: Run unittest-vibe-ut + - name: Set up test connection string env: MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} run: | echo "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" > testConnectionStr.txt - dub run -c unittest-vibe-ut -- -t + + - name: Run unittests with Vibe.d + run: | + dub run ":integration-tests-vibe" + + - name: Run unittests with Phobos + run: | + dub run ":integration-tests-phobos" + + - name: Run test connection utility + env: + MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} + run: | + dub run ":testconn" -- "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" - name: Build The Example Project working-directory: ./examples/homePage @@ -201,4 +216,4 @@ jobs: env: MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} run: | - ./example "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" \ No newline at end of file + ./example "host=localhost;port=$MYSQL_PORT;user=testuser;pwd=passw0rd;db=testdb" diff --git a/README.md b/README.md index 1aa95ede..b7ce422a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ MySQL native [![GitHub - Builds](https://github.com/mysql-d/mysql-native/actions/workflows/dub.yml/badge.svg)](https://github.com/mysql-d/mysql-native/actions/workflows/dub.yml) [![GitHub - Integration Tests](https://github.com/mysql-d/mysql-native/actions/workflows/integration-testing.yml/badge.svg)](https://github.com/mysql-d/mysql-native/actions/workflows/integration-testing.yml) +*NOTE: we are in the process of migrating to github actions. Documentation will +eventually be generated using github actions, and stored on github. This README +is in flux at the moment, and may contain outdated information* + A [Boost-licensed](http://www.boost.org/LICENSE_1_0.txt) native [D](http://dlang.org) client driver for MySQL and MariaDB. @@ -89,7 +93,7 @@ void main(string[] args) "SELECT * FROM `tablename` WHERE `name`=? OR `name`=?", "Bob", "Bobby"); bobs.close(); // Skip them - + Row[] rs = conn.query( // Same SQL as above, but only prepared once and is reused! "SELECT * FROM `tablename` WHERE `name`=? OR `name`=?", "Bob", "Ann").array; // Get ALL the rows at once @@ -119,26 +123,41 @@ Additional notes This requires MySQL server v4.1.1 or later, or a MariaDB server. Older versions of MySQL server are obsolete, use known-insecure authentication, -and are not supported by this package. +and are not supported by this package. Currently the github actions tests use +MySQL 5.7 and MariaDB 10. MySQL 8 is supported with `mysql_native_password` +authentication, but is not currently tested. Expect this to change in the future. Normally, MySQL clients connect to a server on the same machine via a Unix socket on *nix systems, and through a named pipe on Windows. Neither of these conventions is currently supported. TCP is used for all connections. -For historical reference, see the [old homepage](http://britseyeview.com/software/mysqln/) -for the original release of this project. Note, however, that version has -become out-of-date. +Unfortunately, the original home page of Steve Teale's mysqln is no longer +available. You can see an archive on the [Internet Archive wayback +machine](https://web.archive.org/web/20120323165808/http://britseyeview.com/software/mysqln) Developers - How to run the test suite -------------------------------------- -This package contains various unittests and integration tests. To run them, -run `run-tests`. +Unittests that do not require an actual server are located in the library +codebase. You can run just these tests using `dub test`. + +Unittests that require a working server are all located in the +[integration-tests](integration-tests) subpackage. Due to a [dub +issue](https://github.com/dlang/dub/issues/2136), the integration tests are run +using the [integration-tests-phobos](integration-tests-phobos) and +[integration-tests-vibe](integration-tests-vibe) subpackages. At some point, if this dub issue +is fixed, they will simply become configurations in the main integration-tests +repository. You can run these directly from the main repository folder by +issuing the commands: + +```sh +dub run :integration-tests-phobos +dub run :integration-tests-vibe +``` +This will also run the library tests as well as the integration tests. -The first time you run `run-tests`, it will automatically create a -file `testConnectionStr.txt` in project's base diretory and then exit. -This file is deliberately not contained in the source repository -because it's specific to your system. +The first time you run an integration test, the file `testConnectionStr.txt` +will be created in your current directory Open the `testConnectionStr.txt` file and verify the connection settings inside, modifying them as needed, and if necessary, creating a test user and @@ -148,6 +167,9 @@ The tests will completely clobber anything inside the db schema provided, but they will ONLY modify that one db schema. No other schema will be modified in any way. -After you've configured the connection string, run `run-tests` again -and their tests will be compiled and run, first using Phobos sockets, -then using Vibe sockets. +After you've configured the connection string, run the integration tests again. + +The integration tests use +[unit-threaded](https://code.dlang.org/packages/unit-threaded) which allows for +running individual named tests. Use this for running specific tests instead of +the whole suite. diff --git a/dub.sdl b/dub.sdl index ad7ae6ae..a07b0373 100644 --- a/dub.sdl +++ b/dub.sdl @@ -1,65 +1,22 @@ name "mysql-native" description "A native MySQL driver implementation based on Steve Teale's original" license "BSL-1.0" -copyright "Copyright (c) 2011-2019 Steve Teale, James W. Oliphant, Simen Endsjø, Sönke Ludwig, Sergey Shamov, and Nick Sabalausky" -authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" +copyright "Copyright (c) 2011-2021 Steve Teale, James W. Oliphant, Simen Endsjø, Sönke Ludwig, Sergey Shamov, Nick Sabalausky, and Steven Schveighoffer" +authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" "Steven Schveighoffer" dependency "vibe-core" version="~>1.16.0" optional=true toolchainRequirements frontend=">=2.068" -sourcePaths "source/" -importPaths "source/" - -configuration "application" { - targetType "executable" - versions "VibeCustomMain" -} +subPackage "./integration-tests" +subPackage "./integration-tests-vibe" +subPackage "./integration-tests-phobos" +subPackage "./testconn" configuration "library" { - targetType "library" - excludedSourceFiles "source/app.d" } -// Do not use this. Use "run_tests" insetad. configuration "unittest" { - excludedSourceFiles "source/app.d" - preBuildCommands \ - "echo \"ERROR: Don't use 'dub test' to test mysql-native. Use 'run_tests' instead.\"" \ - "echo Bailing..." \ - "mkdir" // Generate error to halt build -} - -// Run with: dub test -c unittest-vibe -configuration "unittest-vibe" { - targetType "executable" - targetPath "bin/" - targetName "mysqln-tests-vibe" - excludedSourceFiles "source/app.d" - - dependency "vibe-core" version="~>1.16.0" optional=false - - // mainSourceFile "source/mysql/package.d" debugVersions "MYSQLN_TESTS" -} - -// Run with: dub run -c unittest-vibe-ut -- -t -configuration "unittest-vibe-ut" { - targetType "executable" - targetPath "bin/" - targetName "mysqln-tests-vibe" - excludedSourceFiles "source/app.d" - sourceFiles "bin/ut.d" - importPaths "bin/" - buildOptions "unittests" - - dependency "vibe-core" version="~>1.16.0" optional=false - - dependency "unit-threaded" version="~>1.0.15" - - debugVersions "MYSQLN_TESTS" - versions "MYSQLN_TESTS_NO_MAIN" - versions "unitUnthreaded" - - preBuildCommands "dub run unit-threaded -c gen_ut_main -- -f bin/ut.d" + targetType "sourceLibrary" } diff --git a/integration-tests-phobos/dub.sdl b/integration-tests-phobos/dub.sdl new file mode 100644 index 00000000..617c21e7 --- /dev/null +++ b/integration-tests-phobos/dub.sdl @@ -0,0 +1,8 @@ +name "integration-tests-phobos" +description "Phobos tests for mysql-native" +license "BSL-1.0" +copyright "Copyright (c) 2011-2021 Steve Teale, James W. Oliphant, Simen Endsjø, Sönke Ludwig, Sergey Shamov, Nick Sabalausky, and Steven Schveighoffer" +authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" "Steven Schveighoffer" + +dependency "mysql-native:integration-tests" path="../" +targetType "executable" diff --git a/integration-tests-phobos/source/dummy.d b/integration-tests-phobos/source/dummy.d new file mode 100644 index 00000000..e69de29b diff --git a/integration-tests-vibe/dub.sdl b/integration-tests-vibe/dub.sdl new file mode 100644 index 00000000..28a78bb7 --- /dev/null +++ b/integration-tests-vibe/dub.sdl @@ -0,0 +1,9 @@ +name "integration-tests-vibe" +description "Vibe tests for mysql-native" +license "BSL-1.0" +copyright "Copyright (c) 2011-2021 Steve Teale, James W. Oliphant, Simen Endsjø, Sönke Ludwig, Sergey Shamov, Nick Sabalausky, and Steven Schveighoffer" +authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" "Steven Schveighoffer" + +dependency "mysql-native:integration-tests" path="../" +dependency "vibe-core" version="~>1.16.0" +targetType "executable" diff --git a/integration-tests-vibe/source/dummy.d b/integration-tests-vibe/source/dummy.d new file mode 100644 index 00000000..e69de29b diff --git a/integration-tests/README.md b/integration-tests/README.md new file mode 100644 index 00000000..474a6bcc --- /dev/null +++ b/integration-tests/README.md @@ -0,0 +1,22 @@ +Integration Tests for MySQL Native +================================== + +This sub-project is intended for proving the functionality of the project against a database instance. + +See the instructions in the [main README](../README.md#developers---how-to-run-the-test-suite) on how to use this subpackage. + +## Docker image + +A docker-compose.yml is supplied for convenience when testing locally. It's preconfigured to use the same username/password that is used by default. + +To run tests on your machine, presuming docker is installed, simply run: + +``` +$ docker-compose up --detach +``` + +Once you are finished, tear down the docker instance + +``` +$ docker-compose down +``` diff --git a/integration-tests/docker-compose.yml b/integration-tests/docker-compose.yml new file mode 100644 index 00000000..4b1cb171 --- /dev/null +++ b/integration-tests/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.7' +services: + mysql: + # Don't use latest (MySQL Server 8.0) as we cannot currently support it + image: mysql:5.7 + restart: always + ports: ['3306:3306'] + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=mysqln_testdb + - MYSQL_USER=mysqln_test + - MYSQL_PASSWORD=pass123 diff --git a/integration-tests/dub.sdl b/integration-tests/dub.sdl new file mode 100644 index 00000000..ea06b269 --- /dev/null +++ b/integration-tests/dub.sdl @@ -0,0 +1,15 @@ +name "integration-tests" +description "Test harness for integration tests" +license "BSL-1.0" +copyright "Copyright (c) 2011-2021 Steve Teale, James W. Oliphant, Simen Endsjø, Sönke Ludwig, Sergey Shamov, Nick Sabalausky, and Steven Schveighoffer" +authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" "Steven Schveighoffer" + +dependency "mysql-native" path="../" +subConfiguration "mysql-native" "unittest" +dependency "unit-threaded" version="~>1.0.15" +debugVersions "MYSQLN_TESTS" +versions "unitUnthreaded" +targetType "library" + +// this is needed to make the unittests compile, even though there's a warning +buildOptions "unittests" "debugMode" "debugInfo" diff --git a/integration-tests/source/app.d b/integration-tests/source/app.d new file mode 100644 index 00000000..a79fda82 --- /dev/null +++ b/integration-tests/source/app.d @@ -0,0 +1,23 @@ +import mysql.test.common; +import mysql.test.integration; +import mysql.test.regression; +import mysql.maintests; +import mysql.protocol.packet_helpers; +import mysql.connection; +import mysql.escape; + +import unit_threaded; + +// manual unit-threaded main function +int main(string[] args) +{ + return args.runTests!( + "mysql.maintests", + "mysql.test.common", + "mysql.test.integration", + "mysql.test.regression", + "mysql.protocol.packet_helpers", + "mysql.connection", + "mysql.escape" + ); +} diff --git a/integration-tests/source/mysql/maintests.d b/integration-tests/source/mysql/maintests.d new file mode 100644 index 00000000..61e5f107 --- /dev/null +++ b/integration-tests/source/mysql/maintests.d @@ -0,0 +1,1348 @@ +module mysql.maintests; +import mysql.test.common; +import mysql; +import mysql.protocol.constants; + +import std.exception; +import std.variant; +import std.typecons; +import std.array; +import std.algorithm; + +// mysql.commands +@("columnSpecial") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import std.range; + import mysql.test.common; + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `columnSpecial`"); + cn.exec("CREATE TABLE `columnSpecial` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below + auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + auto data = alph.cycle.take(totalSize).array; + cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(string)data)~"\")"); + + // Common stuff + int chunkSize; + immutable selectSQL = "SELECT `data` FROM `columnSpecial`"; + ubyte[] received; + bool lastValueOfFinished; + void receiver(const(ubyte)[] chunk, bool finished) + { + assert(lastValueOfFinished == false); + + if(finished) + assert(chunk.length == chunkSize); + else + assert(chunk.length < chunkSize); // Not always true in general, but true in this unittest + + received ~= chunk; + lastValueOfFinished = finished; + } + + // Sanity check + auto value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + + // Use ColumnSpecialization with sql string, + // and totalSize as a multiple of chunkSize + { + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + value = cn.queryValue(selectSQL, [columnSpecial]); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } + + // Use ColumnSpecialization with sql string, + // and totalSize as a non-multiple of chunkSize + { + chunkSize = 64; + assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + value = cn.queryValue(selectSQL, [columnSpecial]); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } + + // Use ColumnSpecialization with prepared statement, + // and totalSize as a multiple of chunkSize + { + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + auto prepared = cn.prepare(selectSQL); + prepared.columnSpecials = [columnSpecial]; + value = cn.queryValue(prepared); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } +} + +// Test what happens when queryRowTuple receives no rows +@("queryRowTuple_noRows") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.test.common : scopedCn, createCn; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `queryRowTuple_noRows`"); + cn.exec("CREATE TABLE `queryRowTuple_noRows` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable selectSQL = "SELECT * FROM `queryRowTuple_noRows`"; + int queryTupleResult; + assertThrown!MYX(cn.queryRowTuple(selectSQL, queryTupleResult)); +} + +@("execOverloads") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import mysql.connection; + import mysql.test.common; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `execOverloads`"); + cn.exec("CREATE TABLE `execOverloads` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable prepareSQL = "INSERT INTO `execOverloads` VALUES (?, ?)"; + + // Do the inserts, using exec + + // exec: const(char[]) sql + assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1); + assert(cn.exec(prepareSQL, 2, "bb") == 1); + assert(cn.exec(prepareSQL, [Variant(3), Variant("cc")]) == 1); + + // exec: prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(4, "dd"); + assert(cn.exec(prepared) == 1); + + assert(cn.exec(prepared, 5, "ee") == 1); + assert(prepared.getArg(0) == 5); + assert(prepared.getArg(1) == "ee"); + + assert(cn.exec(prepared, [Variant(6), Variant("ff")]) == 1); + assert(prepared.getArg(0) == 6); + assert(prepared.getArg(1) == "ff"); + + // exec: bcPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(7, "gg"); + assert(cn.exec(bcPrepared) == 1); + assert(bcPrepared.getArg(0) == 7); + assert(bcPrepared.getArg(1) == "gg"); + + // Check results + auto rows = cn.query("SELECT * FROM `execOverloads`").array(); + assert(rows.length == 7); + + assert(rows[0].length == 2); + assert(rows[1].length == 2); + assert(rows[2].length == 2); + assert(rows[3].length == 2); + assert(rows[4].length == 2); + assert(rows[5].length == 2); + assert(rows[6].length == 2); + + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + assert(rows[1][0] == 2); + assert(rows[1][1] == "bb"); + assert(rows[2][0] == 3); + assert(rows[2][1] == "cc"); + assert(rows[3][0] == 4); + assert(rows[3][1] == "dd"); + assert(rows[4][0] == 5); + assert(rows[4][1] == "ee"); + assert(rows[5][0] == 6); + assert(rows[5][1] == "ff"); + assert(rows[6][0] == 7); + assert(rows[6][1] == "gg"); +} + +@("queryOverloads") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import mysql.connection; + import mysql.test.common; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `queryOverloads`"); + cn.exec("CREATE TABLE `queryOverloads` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `queryOverloads` VALUES (1, \"aa\"), (2, \"bb\"), (3, \"cc\")"); + + immutable prepareSQL = "SELECT * FROM `queryOverloads` WHERE `i`=? AND `s`=?"; + + // Test query + { + Row[] rows; + + // String sql + rows = cn.query("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + + rows = cn.query(prepareSQL, 2, "bb").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == "bb"); + + rows = cn.query(prepareSQL, [Variant(3), Variant("cc")]).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 3); + assert(rows[0][1] == "cc"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + rows = cn.query(prepared).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + + rows = cn.query(prepared, 2, "bb").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == "bb"); + + rows = cn.query(prepared, [Variant(3), Variant("cc")]).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 3); + assert(rows[0][1] == "cc"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(1, "aa"); + rows = cn.query(bcPrepared).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + } + + // Test queryRow + { + Nullable!Row row; + + // String sql + row = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); + assert(!row.isNull); + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + + row = cn.queryRow(prepareSQL, 2, "bb"); + assert(!row.isNull); + assert(row.length == 2); + assert(row[0] == 2); + assert(row[1] == "bb"); + + row = cn.queryRow(prepareSQL, [Variant(3), Variant("cc")]); + assert(!row.isNull); + assert(row.length == 2); + assert(row[0] == 3); + assert(row[1] == "cc"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + row = cn.queryRow(prepared); + assert(!row.isNull); + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + + row = cn.queryRow(prepared, 2, "bb"); + assert(!row.isNull); + assert(row.length == 2); + assert(row[0] == 2); + assert(row[1] == "bb"); + + row = cn.queryRow(prepared, [Variant(3), Variant("cc")]); + assert(!row.isNull); + assert(row.length == 2); + assert(row[0] == 3); + assert(row[1] == "cc"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(1, "aa"); + row = cn.queryRow(bcPrepared); + assert(!row.isNull); + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + } + + // Test queryRowTuple + { + int i; + string s; + + // String sql + cn.queryRowTuple("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"", i, s); + assert(i == 1); + assert(s == "aa"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(2, "bb"); + cn.queryRowTuple(prepared, i, s); + assert(i == 2); + assert(s == "bb"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(3, "cc"); + cn.queryRowTuple(bcPrepared, i, s); + assert(i == 3); + assert(s == "cc"); + } + + // Test queryValue + { + Nullable!Variant value; + + // String sql + value = cn.queryValue("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); + assert(!value.isNull); + assert(value.get.type != typeid(typeof(null))); + assert(value.get == 1); + + value = cn.queryValue(prepareSQL, 2, "bb"); + assert(!value.isNull); + assert(value.get.type != typeid(typeof(null))); + assert(value.get == 2); + + value = cn.queryValue(prepareSQL, [Variant(3), Variant("cc")]); + assert(!value.isNull); + assert(value.get.type != typeid(typeof(null))); + assert(value.get == 3); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + value = cn.queryValue(prepared); + assert(!value.isNull); + assert(value.get.type != typeid(typeof(null))); + assert(value.get == 1); + + value = cn.queryValue(prepared, 2, "bb"); + assert(!value.isNull); + assert(value.get.type != typeid(typeof(null))); + assert(value.get == 2); + + value = cn.queryValue(prepared, [Variant(3), Variant("cc")]); + assert(!value.isNull); + assert(value.get.type != typeid(typeof(null))); + assert(value.get == 3); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + bcPrepared.setArgs(1, "aa"); + value = cn.queryValue(bcPrepared); + assert(!value.isNull); + assert(value.get.type != typeid(typeof(null))); + assert(value.get == 1); + } +} + +// mysql.connection +@("prepareFunction") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.test.common; + mixin(scopedCn); + + exec(cn, `DROP FUNCTION IF EXISTS hello`); + exec(cn, ` + CREATE FUNCTION hello (s CHAR(20)) + RETURNS CHAR(50) DETERMINISTIC + RETURN CONCAT('Hello ',s,'!') + `); + + auto preparedHello = prepareFunction(cn, "hello", 1); + preparedHello.setArgs("World"); + auto rs = cn.query(preparedHello).array; + assert(rs.length == 1); + assert(rs[0][0] == "Hello World!"); +} + +@("prepareProcedure") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.test.common; + import mysql.test.integration; + mixin(scopedCn); + initBaseTestTables(cn); + + exec(cn, `DROP PROCEDURE IF EXISTS insert2`); + exec(cn, ` + CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) + BEGIN + INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); + END + `); + + auto preparedInsert2 = prepareProcedure(cn, "insert2", 2); + preparedInsert2.setArgs(2001, "inserted string 1"); + cn.exec(preparedInsert2); + + auto rs = query(cn, "SELECT stringcol FROM basetest WHERE intcol=2001").array; + assert(rs.length == 1); + assert(rs[0][0] == "inserted string 1"); +} + +// This also serves as a regression test for #167: +// ResultRange doesn't get invalidated upon reconnect +@("reconnect") +debug(MYSQLN_TESTS) +unittest +{ + import std.variant; + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `reconnect`"); + cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)"); + + enum sql = "SELECT a FROM `reconnect`"; + + // Sanity check + auto rows = cn.query(sql).array; + assert(rows[0][0] == 1); + assert(rows[1][0] == 2); + assert(rows[2][0] == 3); + + // Ensure reconnect keeps the same connection when it's supposed to + auto range = cn.query(sql); + assert(range.front[0] == 1); + cn.reconnect(); + assert(!cn.closed); // Is open? + assert(range.isValid); // Still valid? + range.popFront(); + assert(range.front[0] == 2); + + // Ensure reconnect reconnects when it's supposed to + range = cn.query(sql); + assert(range.front[0] == 1); + cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities + cn.reconnect(~cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Try manually reconnecting + range = cn.query(sql); + assert(range.front[0] == 1); + cn.connect(cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Try manually closing and connecting + range = cn.query(sql); + assert(range.front[0] == 1); + cn.close(); + assert(cn.closed); // Is closed? + assert(!range.isValid); // Was invalidated? + cn.connect(cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Auto-reconnect upon a command + cn.close(); + assert(cn.closed); + range = cn.query(sql); + assert(!cn.closed); + assert(range.front[0] == 1); +} + +@("releaseAll") +debug(MYSQLN_TESTS) +unittest +{ + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `releaseAll`"); + cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`"); + auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)"); + assert(cn.isRegistered(preparedSelect)); + assert(cn.isRegistered(preparedInsert)); + + cn.releaseAll(); + assert(!cn.isRegistered(preparedSelect)); + assert(!cn.isRegistered(preparedInsert)); + cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)"); + assert(!cn.isRegistered(preparedSelect)); + assert(!cn.isRegistered(preparedInsert)); + + cn.exec(preparedInsert); + cn.query(preparedSelect).array; + assert(cn.isRegistered(preparedSelect)); + assert(cn.isRegistered(preparedInsert)); +} + +// Test register, release, isRegistered, and auto-register for prepared statements +@("autoRegistration") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.connection; + import mysql.test.common; + + Prepared preparedInsert; + Prepared preparedSelect; + immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; + immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; + int queryTupleResult; + + { + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); + cn.exec("CREATE TABLE `autoRegistration` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + // Initial register + preparedInsert = cn.prepare(insertSQL); + preparedSelect = cn.prepare(selectSQL); + + // Test basic register, release, isRegistered + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + + // Test manual re-register + cn.register(preparedInsert); + cn.register(preparedSelect); + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + + // Test double register + cn.register(preparedInsert); + cn.register(preparedSelect); + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + + // Test double release + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + } + + // Note that at this point, both prepared statements still exist, + // but are no longer registered on any connection. In fact, there + // are no open connections anymore. + + // Test auto-register: exec + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedInsert)); + cn.exec(preparedInsert); + assert(cn.isRegistered(preparedInsert)); + } + + // Test auto-register: query + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.query(preparedSelect).each(); + assert(cn.isRegistered(preparedSelect)); + } + + // Test auto-register: queryRow + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.queryRow(preparedSelect); + assert(cn.isRegistered(preparedSelect)); + } + + // Test auto-register: queryRowTuple + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.queryRowTuple(preparedSelect, queryTupleResult); + assert(cn.isRegistered(preparedSelect)); + } + + // Test auto-register: queryValue + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.queryValue(preparedSelect); + assert(cn.isRegistered(preparedSelect)); + } +} + +// An attempt to reproduce issue #81: Using mysql-native driver with no default database +// I'm unable to actually reproduce the error, though. +@("issue81") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.escape; + import std.conv; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `issue81`"); + cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); + + auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); + scope(exit) cn2.close(); + + cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); +} + +// Regression test for Issue #154: +// autoPurge can throw an exception if the socket was closed without purging +// +// This simulates a disconnect by closing the socket underneath the Connection +// object itself. +@("dropConnection") +debug(MYSQLN_TESTS) +unittest +{ + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `dropConnection`"); + cn.exec("CREATE TABLE `dropConnection` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)"); + import mysql.prepared; + { + auto prep = cn.prepare("SELECT * FROM `dropConnection`"); + cn.query(prep); + } + // close the socket forcibly + cn._socket.close(); + // this should still work (it should reconnect). + cn.exec("DROP TABLE `dropConnection`"); +} + +/+ +Test Prepared's ability to be safely refcount-released during a GC cycle +(ie, `Connection.release` must not allocate GC memory). + +Currently disabled because it's not guaranteed to always work +(and apparently, cannot be made to work?) +For relevant discussion, see issue #159: +https://github.com/mysql-d/mysql-native/issues/159 ++/ +version(none) +debug(MYSQLN_TESTS) +{ + /// Proof-of-concept ref-counted Prepared wrapper, just for testing, + /// not really intended for actual use. + private struct RCPreparedPayload + { + Prepared prepared; + Connection conn; // Connection to be released from + + alias prepared this; + + @disable this(this); // not copyable + ~this() + { + // There are a couple calls to this dtor where `conn` happens to be null. + if(conn is null) + return; + + assert(conn.isRegistered(prepared)); + conn.release(prepared); + } + } + ///ditto + alias RCPrepared = RefCounted!(RCPreparedPayload, RefCountedAutoInitialize.no); + ///ditto + private RCPrepared rcPrepare(Connection conn, const(char[]) sql) + { + import std.algorithm.mutation : move; + + auto prepared = conn.prepare(sql); + auto payload = RCPreparedPayload(prepared, conn); + return refCounted(move(payload)); + } + + @("rcPrepared") + unittest + { + import core.memory; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `rcPrepared`"); + cn.exec("CREATE TABLE `rcPrepared` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `rcPrepared` VALUES (1), (2), (3)"); + + // Define this in outer scope to guarantee data is left pending when + // RCPrepared's payload is collected. This will guarantee + // that Connection will need to queue the release. + ResultRange rows; + + void bar() + { + class Foo { RCPrepared p; } + auto foo = new Foo(); + + auto rcStmt = cn.rcPrepare("SELECT * FROM `rcPrepared`"); + foo.p = rcStmt; + rows = cn.query(rcStmt); + + /+ + At this point, there are two references to the prepared statement: + One in a `Foo` object (currently bound to `foo`), and one on the stack. + + Returning from this function will destroy the one on the stack, + and deterministically reduce the refcount to 1. + + So, right here we set `foo` to null to *keep* the Foo object's + reference to the prepared statement, but set adrift the Foo object + itself, ready to be destroyed (along with the only remaining + prepared statement reference it contains) by the next GC cycle. + + Thus, `RCPreparedPayload.~this` and `Connection.release(Prepared)` + will be executed during a GC cycle...and had better not perform + any allocations, or else...boom! + +/ + foo = null; + } + + bar(); + assert(cn.hasPending); // Ensure Connection is forced to queue the release. + GC.collect(); // `Connection.release(Prepared)` better not be allocating, or boom! + } +} + +// mysql.exceptions +@("wrongFunctionException") +debug(MYSQLN_TESTS) +unittest +{ + import std.exception; + import mysql.commands; + import mysql.connection; + import mysql.prepared; + import mysql.test.common : scopedCn, createCn; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `wrongFunctionException`"); + cn.exec("CREATE TABLE `wrongFunctionException` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable insertSQL = "INSERT INTO `wrongFunctionException` VALUES (1), (2)"; + immutable selectSQL = "SELECT * FROM `wrongFunctionException`"; + Prepared preparedInsert; + Prepared preparedSelect; + int queryTupleResult; + assertNotThrown!MYXWrongFunction(cn.exec(insertSQL)); + assertNotThrown!MYXWrongFunction(cn.query(selectSQL).each()); + assertNotThrown!MYXWrongFunction(cn.queryRowTuple(selectSQL, queryTupleResult)); + assertNotThrown!MYXWrongFunction(preparedInsert = cn.prepare(insertSQL)); + assertNotThrown!MYXWrongFunction(preparedSelect = cn.prepare(selectSQL)); + assertNotThrown!MYXWrongFunction(cn.exec(preparedInsert)); + assertNotThrown!MYXWrongFunction(cn.query(preparedSelect).each()); + assertNotThrown!MYXWrongFunction(cn.queryRowTuple(preparedSelect, queryTupleResult)); + + assertThrown!MYXResultRecieved(cn.exec(selectSQL)); + assertThrown!MYXNoResultRecieved(cn.query(insertSQL).each()); + assertThrown!MYXNoResultRecieved(cn.queryRowTuple(insertSQL, queryTupleResult)); + assertThrown!MYXResultRecieved(cn.exec(preparedSelect)); + assertThrown!MYXNoResultRecieved(cn.query(preparedInsert).each()); + assertThrown!MYXNoResultRecieved(cn.queryRowTuple(preparedInsert, queryTupleResult)); +} + +// mysql.pool +version(Have_vibe_core) +{ + @("onNewConnection") + debug(MYSQLN_TESTS) + unittest + { + auto count = 0; + void callback(Connection conn) + { + count++; + } + + // Test getting/setting + auto poolA = new MySQLPool(testConnectionStr, &callback); + auto poolB = new MySQLPool(testConnectionStr); + auto poolNoCallback = new MySQLPool(testConnectionStr); + + assert(poolA.onNewConnection == &callback); + assert(poolB.onNewConnection is null); + assert(poolNoCallback.onNewConnection is null); + + poolB.onNewConnection = &callback; + assert(poolB.onNewConnection == &callback); + assert(count == 0); + + // Ensure callback is called + { + auto connA = poolA.lockConnection(); + assert(!connA.closed); + assert(count == 1); + + auto connB = poolB.lockConnection(); + assert(!connB.closed); + assert(count == 2); + } + + // Ensure works with no callback + { + auto oldCount = count; + auto poolC = new MySQLPool(testConnectionStr); + auto connC = poolC.lockConnection(); + assert(!connC.closed); + assert(count == oldCount); + } + } + + @("registration") + debug(MYSQLN_TESTS) + unittest + { + import mysql.commands; + auto pool = new MySQLPool(testConnectionStr); + + // Setup + auto cn = pool.lockConnection(); + cn.exec("DROP TABLE IF EXISTS `poolRegistration`"); + cn.exec("CREATE TABLE `poolRegistration` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + immutable sql = "SELECT * from `poolRegistration`"; + auto cn2 = pool.lockConnection(); + pool.applyAuto(cn2); + assert(cn !is cn2); + + // Tests: + // Initial + assert(pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(!cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + // Register on connection #1 + auto prepared = cn.prepare(sql); + { + assert(pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + auto cn3 = pool.lockConnection(); + pool.applyAuto(cn3); + assert(!cn3.isRegistered(sql)); + } + + // autoRegister + pool.autoRegister(prepared); + { + assert(!pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(!pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + auto cn3 = pool.lockConnection(); + pool.applyAuto(cn3); + assert(cn3.isRegistered(sql)); + } + + // autoRelease + pool.autoRelease(prepared); + { + assert(!pool.isAutoCleared(sql)); + assert(!pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + auto cn3 = pool.lockConnection(); + pool.applyAuto(cn3); + assert(!cn3.isRegistered(sql)); + } + + // clearAuto + pool.clearAuto(prepared); + { + assert(pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + auto cn3 = pool.lockConnection(); + pool.applyAuto(cn3); + assert(!cn3.isRegistered(sql)); + } + } + + @("closedConnection") // "cct" + debug(MYSQLN_TESTS) + { + import mysql.pool; + MySQLPool cctPool; + int cctCount=0; + + void cctStart() + { + import std.array; + import mysql.commands; + + cctPool = new MySQLPool(testConnectionStr); + cctPool.onNewConnection = (Connection conn) { cctCount++; }; + assert(cctCount == 0); + + auto cn = cctPool.lockConnection(); + assert(!cn.closed); + cn.close(); + assert(cn.closed); + assert(cctCount == 1); + } + + unittest + { + cctStart(); + assert(cctCount == 1); + + auto cn = cctPool.lockConnection(); + assert(cctCount == 1); + assert(!cn.closed); + } + } +} + +// mysql.prepared +@("paramSpecial") +debug(MYSQLN_TESTS) +unittest +{ + import std.array; + import std.range; + import mysql.connection; + import mysql.test.common; + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `paramSpecial`"); + cn.exec("CREATE TABLE `paramSpecial` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below + auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + auto data = alph.cycle.take(totalSize).array; + + int chunkSize; + const(ubyte)[] dataToSend; + bool finished; + uint sender(ubyte[] chunk) + { + assert(!finished); + assert(chunk.length == chunkSize); + + if(dataToSend.length < chunkSize) + { + auto actualSize = cast(uint) dataToSend.length; + chunk[0..actualSize] = dataToSend[]; + finished = true; + dataToSend.length = 0; + return actualSize; + } + else + { + chunk[] = dataToSend[0..chunkSize]; + dataToSend = dataToSend[chunkSize..$]; + return chunkSize; + } + } + + immutable selectSQL = "SELECT `data` FROM `paramSpecial`"; + + // Sanity check + cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(string)data)~"\")"); + auto value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + + { + // Clear table + cn.exec("DELETE FROM `paramSpecial`"); + value = cn.queryValue(selectSQL); // Ensure deleted + assert(value.isNull); + + // Test: totalSize as a multiple of chunkSize + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); + + finished = false; + dataToSend = data; + auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); + prepared.setArg(0, cast(ubyte[])[], paramSpecial); + assert(cn.exec(prepared) == 1); + value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + } + + { + // Clear table + cn.exec("DELETE FROM `paramSpecial`"); + value = cn.queryValue(selectSQL); // Ensure deleted + assert(value.isNull); + + // Test: totalSize as a non-multiple of chunkSize + chunkSize = 64; + assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); + auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); + + finished = false; + dataToSend = data; + auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); + prepared.setArg(0, cast(ubyte[])[], paramSpecial); + assert(cn.exec(prepared) == 1); + value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + } +} + +@("setArg-typeMods") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.test.common; + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`"); + cn.exec("CREATE TABLE `setArg-typeMods` ( + `i` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)"; + + // Sanity check + { + int i = 111; + assert(cn.exec(insertSQL, i) == 1); + auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`"); + assert(!value.isNull); + assert(value.get == i); + } + + // Test const(int) + { + const(int) i = 112; + assert(cn.exec(insertSQL, i) == 1); + } + + // Test immutable(int) + { + immutable(int) i = 113; + assert(cn.exec(insertSQL, i) == 1); + } + + // Note: Variant doesn't seem to support + // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. + + // Test shared immutable(int) + { + shared immutable(int) i = 113; + assert(cn.exec(insertSQL, i) == 1); + } +} + +@("setNullArg") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.connection; + import mysql.test.common; + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `setNullArg`"); + cn.exec("CREATE TABLE `setNullArg` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; + immutable selectSQL = "SELECT * FROM `setNullArg`"; + auto preparedInsert = cn.prepare(insertSQL); + assert(preparedInsert.sql == insertSQL); + Row[] rs; + + { + Nullable!int nullableInt; + nullableInt.nullify(); + preparedInsert.setArg(0, nullableInt); + assert(preparedInsert.getArg(0).type == typeid(typeof(null))); + nullableInt = 7; + preparedInsert.setArg(0, nullableInt); + assert(preparedInsert.getArg(0) == 7); + + nullableInt.nullify(); + preparedInsert.setArgs(nullableInt); + assert(preparedInsert.getArg(0).type == typeid(typeof(null))); + nullableInt = 7; + preparedInsert.setArgs(nullableInt); + assert(preparedInsert.getArg(0) == 7); + } + + preparedInsert.setArg(0, 5); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 1); + assert(rs[0][0] == 5); + + preparedInsert.setArg(0, null); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 2); + assert(rs[0][0] == 5); + assert(rs[1].isNull(0)); + assert(rs[1][0].type == typeid(typeof(null))); + + preparedInsert.setArg(0, Variant(null)); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 3); + assert(rs[0][0] == 5); + assert(rs[1].isNull(0)); + assert(rs[2].isNull(0)); + assert(rs[1][0].type == typeid(typeof(null))); + assert(rs[2][0].type == typeid(typeof(null))); +} + +@("lastInsertID") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.connection; + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); + cn.exec("CREATE TABLE `testPreparedLastInsertID` ( + `a` INTEGER NOT NULL AUTO_INCREMENT, + PRIMARY KEY (a) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); + cn.exec(stmt); + assert(stmt.lastInsertID == 1); + cn.exec(stmt); + assert(stmt.lastInsertID == 2); + cn.exec(stmt); + assert(stmt.lastInsertID == 3); +} + +// Test PreparedRegistrations +debug(MYSQLN_TESTS) +{ + PreparedRegistrations!TestPreparedRegistrationsGood1 testPreparedRegistrationsGood1; + PreparedRegistrations!TestPreparedRegistrationsGood2 testPreparedRegistrationsGood2; + + @("PreparedRegistrations") + unittest + { + // Test init + PreparedRegistrations!TestPreparedRegistrationsGood2 pr; + assert(pr.directLookup.keys.length == 0); + + void resetData(bool isQueued1, bool isQueued2, bool isQueued3) + { + pr.directLookup["1"] = TestPreparedRegistrationsGood2(isQueued1, "1"); + pr.directLookup["2"] = TestPreparedRegistrationsGood2(isQueued2, "2"); + pr.directLookup["3"] = TestPreparedRegistrationsGood2(isQueued3, "3"); + assert(pr.directLookup.keys.length == 3); + } + + // Test resetData (sanity check) + resetData(false, true, false); + assert(pr.directLookup["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr.directLookup["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr.directLookup["3"] == TestPreparedRegistrationsGood2(false, "3")); + + // Test opIndex + resetData(false, true, false); + pr.directLookup["1"] = TestPreparedRegistrationsGood2(false, "1"); + pr.directLookup["2"] = TestPreparedRegistrationsGood2(true, "2"); + pr.directLookup["3"] = TestPreparedRegistrationsGood2(false, "3"); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + assert(pr["4"].isNull); + + // Test queueForRelease + resetData(false, true, false); + pr.queueForRelease("2"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + + pr.queueForRelease("3"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); + + pr.queueForRelease("4"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); + + // Test unqueueForRelease + resetData(false, true, false); + pr.unqueueForRelease("1"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + + pr.unqueueForRelease("2"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + + pr.unqueueForRelease("4"); + assert(pr.directLookup.keys.length == 3); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); + + // Test queueAllForRelease + resetData(false, true, false); + pr.queueAllForRelease(); + assert(pr["1"] == TestPreparedRegistrationsGood2(true, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); + assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); + assert(pr["4"].isNull); + + // Test clear + resetData(false, true, false); + pr.clear(); + assert(pr.directLookup.keys.length == 0); + + // Test registerIfNeeded + auto doRegister(const(char[]) sql) { return TestPreparedRegistrationsGood2(false, sql); } + pr.registerIfNeeded("1", &doRegister); + assert(pr.directLookup.keys.length == 1); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + + pr.registerIfNeeded("1", &doRegister); + assert(pr.directLookup.keys.length == 1); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + + pr.registerIfNeeded("2", &doRegister); + assert(pr.directLookup.keys.length == 2); + assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); + assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); + } +} + +// mysql.result +@("getName") +debug(MYSQLN_TESTS) +unittest +{ + import mysql.test.common; + import mysql.commands; + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `row_getName`"); + cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)"); + + enum sql = "SELECT another, someValue FROM `row_getName`"; + + auto rows = cn.query(sql).array; + assert(rows.length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == 1); + assert(rows[0].getName(0) == "another"); + assert(rows[0].getName(1) == "someValue"); + assert(rows[1][0] == 4); + assert(rows[1][1] == 3); + assert(rows[1].getName(0) == "another"); + assert(rows[1].getName(1) == "someValue"); +} diff --git a/source/mysql/test/common.d b/integration-tests/source/mysql/test/common.d similarity index 85% rename from source/mysql/test/common.d rename to integration-tests/source/mysql/test/common.d index b4f330ee..2b36b77f 100644 --- a/source/mysql/test/common.d +++ b/integration-tests/source/mysql/test/common.d @@ -1,4 +1,4 @@ -/++ +/++ Package mysql.test contains integration and regression tests, not unittests. Unittests (including regression unittests) are located together with the units they test. @@ -42,14 +42,22 @@ version(DoCoreTests) private @property string testConnectionStrFile() { import std.file, std.path; - - static string cached; + + return "testConnectionStr.txt"; + // This seems highly dependent on where the test is run from. At this + // point, I just am using the current directory to avoid going on a + // hunt for the file, and possibly running into permission issues. + /*static string cached; if(!cached) + { + import std.stdio; cached = buildPath(thisExePath.dirName.dirName, "testConnectionStr.txt"); + writeln("the file is ", cached); + } - return cached; + return cached;*/ } - + @property string testConnectionStr() { import std.file, std.string; @@ -64,7 +72,7 @@ version(DoCoreTests) testConnectionStrFile, "host=localhost;port=3306;user=mysqln_test;pwd=pass123;db=mysqln_testdb" ); - + import std.stdio; writeln( "Connection string file for tests wasn't found, so a default "~ @@ -72,13 +80,15 @@ version(DoCoreTests) "run the mysql-native tests again:" ); writeln(testConnectionStrFile); - assert(false, "Halting so the user can check connection string settings."); + writeln("Halting so the user can check connection string settings."); + import core.stdc.stdlib : exit; + exit(1); } - + cached = cast(string) std.file.read(testConnectionStrFile); cached = cached.strip(); } - + return cached; } @@ -93,7 +103,7 @@ version(DoCoreTests) { auto result = cn.queryValue(query); assert(!result.isNull); - + // Timestamp is a bit special as it's converted to a DateTime when // returning from MySQL to avoid having to use a mysql specific type. static if(is(T == DateTime) && is(U == Timestamp)) diff --git a/source/mysql/test/integration.d b/integration-tests/source/mysql/test/integration.d similarity index 99% rename from source/mysql/test/integration.d rename to integration-tests/source/mysql/test/integration.d index a4d60e5b..a51eec37 100644 --- a/source/mysql/test/integration.d +++ b/integration-tests/source/mysql/test/integration.d @@ -1,4 +1,4 @@ -module mysql.test.integration; +module mysql.test.integration; import std.algorithm; import std.conv; @@ -115,7 +115,7 @@ debug(MYSQLN_TESTS) unittest { import mysql.prepared; - + struct X { int a, b, c; @@ -457,9 +457,9 @@ unittest count++; } assert(count == 2); - + initBaseTestTables(cn); - + string[] tList = md.tables(); count = 0; foreach (string t; tList) @@ -477,7 +477,7 @@ unittest See "COLUMN_DEFAULT" at: https://mariadb.com/kb/en/library/information-schema-columns-table/ +/ - + ColumnInfo[] ca = md.columns("basetest"); assert( ca[0].schema == schemaName && ca[0].table == "basetest" && ca[0].name == "boolcol" && ca[0].index == 0 && ca[0].nullable && ca[0].type == "bit" && ca[0].charsMax == -1 && ca[0].octetsMax == -1 && @@ -638,7 +638,7 @@ unittest /+ // Commented out because leaving args unspecified is currently unsupported, // and I'm not convinced it should be allowed. - + // Insert null - params defaults to null { cn.truncate("manytypes"); @@ -998,7 +998,7 @@ unittest immutable selectNoRowsSQL = "SELECT * FROM `coupleTypes` WHERE s='no such match'"; auto prepared = cn.prepare(selectSQL); auto preparedSelectNoRows = cn.prepare(selectNoRowsSQL); - + { // Test query ResultRange rseq = cn.query(selectSQL); @@ -1186,7 +1186,7 @@ unittest `name` VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); } - + // Setup current working directory auto saveDir = getcwd(); scope(exit) diff --git a/source/mysql/test/regression.d b/integration-tests/source/mysql/test/regression.d similarity index 100% rename from source/mysql/test/regression.d rename to integration-tests/source/mysql/test/regression.d diff --git a/run_tests.d b/run_tests.d deleted file mode 100644 index 1d5099b7..00000000 --- a/run_tests.d +++ /dev/null @@ -1,312 +0,0 @@ -import std.file; -import std.getopt; -import std.process; -import std.file; -import std.process; -import std.stdio; -import std.string; - -// Commandline args -bool useUnitThreaded; -bool coreTestsOnly; -enum Mode { phobos, vibe, combined, travis }; -Mode mode; -string[] unitThreadedArgs; - -// Exit code to be returned -int exitCode=0; - -// Utils /////////////////////////////////////////////// - -pure string flagUseUnitThreaded(bool on) -{ - return on? " --ut" : ""; -} - -pure string flagCoreTestsOnly(bool on) -{ - return on? " --core" : ""; -} - -pure string flagMode(Mode mode) -{ - final switch(mode) - { - case Mode.phobos: return " --mode=phobos"; - case Mode.vibe: return " --mode=vibe"; - case Mode.combined: return " --mode=combined"; - case Mode.travis: return " --mode=travis"; - } -} - -string fixSlashes(string path) -{ - version(Windows) return path.replace(`/`, `\`); - else version(Posix) return path.replace(`\`, `/`); - else static assert(0); -} - -string envGet(string name) -{ - return environment.get(name, null); -} - -bool envBool(string name) -{ - return environment.get(name, null) == "true"; -} - -void tryMkdir(string dir) -{ - if(!exists(dir)) - mkdir(dir); -} - -int runFromCurrentDir(string command) -{ - version(Windows) return run(command); - else return run("./"~command); -} - -int run(string command) -{ - writeln(command); - return spawnShell(command).wait; -} - -int run(string[] command) -{ - writeln(command); - return spawnProcess(command).wait; -} - -bool canRun(string command) -{ - return executeShell(command).status == 0; -} - -string findRdmd() -{ - // LDC/GDC don't always include rdmd, so allow user to specify path to it in $RDMD. - // Otherwise, use "rdmd". - // - // For travis, if "rdmd" doesn't work (ie, LDC/GDC is being tested), then use - // the copy of rdmd that was downloaded by the 'ci_setup.d' script. - auto rdmd = environment.get("RDMD"); - if(rdmd is null) - { - rdmd = "rdmd"; - if(!canRun("rdmd --help")) - { - auto travisOsName = environment.get("TRAVIS_OS_NAME"); - if(travisOsName == "osx") - rdmd = "local-dmd/dmd2/"~travisOsName~"/bin/rdmd"; - else - rdmd = "local-dmd/dmd2/"~travisOsName~"/bin64/rdmd"; - } - } - - return rdmd; -} - -// Main (Process command line) /////////////////////////////////////////////// - -int main(string[] args) -{ - try - { - auto argsHelp = getopt(args, - "ut", "Use unit-threaded (Always includes ut's --trace)", &useUnitThreaded, - "core", "Only run basic core tests (Can only be used with --mode=phobos)", &coreTestsOnly, - std.getopt.config.required, - "m|mode", "'-m=phobos': Run Phobos tests | '-m=vibe': Run Vibe.d tests | '-m=combined': Run core-only Phobos tests and all Vibe.d tests | '-m=travis': Travis-CI mode (autodetect settings from envvars)", &mode, - ); - - if(argsHelp.helpWanted) - { - defaultGetoptPrinter( - "Runs the mysql-native test suite with Phobos sockets, Vibe.d sockets, or combined.\n"~ - "\n"~ - "Usage:\n"~ - " run_tests --mode=(phobos|vibe|combined|travis) [OPTIONS] [-- [UNIT-THREADED OPTIONS]]\n"~ - "\n"~ - "Examples:\n"~ - " run_tests --mode=combined\n"~ - " run_tests --ut --mode=vibe -- --help\n"~ - " run_tests --ut --mode=vibe -- --single --random\n"~ - "\n"~ - "Options:", - argsHelp.options - ); - - return 0; - } - } - catch(Exception e) - { - stderr.writeln(e.msg); - stderr.writeln("For help: run_tests --help"); - stderr.writeln("Recommended: run_tests -m=combined"); - return 1; - } - - if(coreTestsOnly && mode==Mode.combined) - { - stderr.writeln("Cannot use --core and --mode=combined together."); - //stderr.writeln("Instead, use:"); - //stderr.writeln(" run_tests --core --mode=phobos && run_tests --core --mode=vibe"); - stderr.writeln("For help: run_tests --help"); - return 1; - } - - if(coreTestsOnly && mode==Mode.vibe) - { - stderr.writeln("Cannot use --core and --mode=vibe together."); - stderr.writeln("For help: run_tests --help"); - return 1; - } - - if(mode==Mode.travis && (coreTestsOnly || useUnitThreaded)) - { - stderr.writeln("Cannot use --mode=travis together with any other option."); - stderr.writeln("For help: run_tests --help"); - return 1; - } - - unitThreadedArgs = args[1..$]; - runTests(); - return exitCode; -} - -// Run Tests /////////////////////////////////////////////// - -void runTests() -{ - //writeln("unitThreadedArgs: ", unitThreadedArgs); - - // GDC doesn't autocreate the dir (and git doesn't beleive in empty dirs) - tryMkdir("bin"); - - final switch(mode) - { - case Mode.phobos: runPhobosTests(); break; - case Mode.vibe: runVibeTests(); break; - case Mode.combined: runCombinedTests(); break; - case Mode.travis: runTravisTests(); break; - } -} - -void runPhobosTests() -{ - // Setup compilers - auto rdmd = findRdmd(); - auto dmd = environment.get("DMD", "dmd"); - - writeln("Using:"); - writeln(" RDMD=", rdmd); - writeln(" DMD=", dmd); - stdout.flush(); - - // Setup --core - auto debugTestsId = coreTestsOnly? "MYSQLN_CORE_TESTS" : "MYSQLN_TESTS"; - auto msgSuffix = coreTestsOnly? " (core tests only)" : ""; - - // Setup unit-threaded - string dFile; - string utSuffix; - string utArgs; - if(useUnitThreaded) - { - msgSuffix = msgSuffix~" (unit-threaded)"; - auto utVer="0.7.45"; - utSuffix="-ut"; - utArgs="-version=MYSQLN_TESTS_NO_MAIN -version=Have_unit_threaded -Iunit-threaded-"~utVer~"/unit-threaded/source/ --extra-file=unit-threaded-"~utVer~"/unit-threaded/libunit-threaded.a --exclude=unit_threaded"; - dFile = "bin/ut.d"; - - // Setup local unit-threaded - run("dub fetch unit-threaded --version="~utVer~" --cache=local"); - chdir(("unit-threaded-"~utVer~"/unit-threaded").fixSlashes); - run("dub build -c gen_ut_main"); - run("dub build -c library"); - chdir(("../..").fixSlashes); - run(("unit-threaded-"~utVer~"/unit-threaded/gen_ut_main -f bin/ut.d").fixSlashes); - } - else - { - dFile = "source/mysql/package.d"; - } - - // Compile tests - writeln("Compiling Phobos-socket tests", msgSuffix, "..."); - auto status = run(rdmd~" --compiler="~dmd~" --build-only -g -unittest "~utArgs~" -debug="~debugTestsId~" -ofbin/mysqln-tests-phobos"~utSuffix~" -Isource "~dFile); - //$RDMD --compiler=$DMD --build-only -g -unittest $UT_ARGS -debug=$DEBUG_TESTS_ID -ofbin/mysqln-tests-phobos${UT_SUFFIX} -Isource $D_FILE - if(status != 0) - { - exitCode = status; - return; - } - - writeln("Running Phobos-socket tests", msgSuffix, "..."); - status = run(["bin/mysqln-tests-phobos"~utSuffix, "-t"] ~ unitThreadedArgs); - // bin/mysqln-tests-phobos${UT_SUFFIX} -t "$@" - if(status != 0) - { - exitCode = status; - return; - } -} - -void runVibeTests() -{ - auto coreMsg = coreTestsOnly? " (core tests only)" : ""; - writeln("Doing Vibe-socket tests", coreMsg, "..."); - - if(useUnitThreaded) - exitCode = run(["dub", "run", "-c", "unittest-vibe-ut", "--", "-t"] ~ unitThreadedArgs); - else - exitCode = run("dub test -c unittest-vibe"); -} - -void runCombinedTests() -{ - auto phobosStatus = runFromCurrentDir( - "run_tests"~ - flagMode(Mode.phobos)~ - flagCoreTestsOnly(true)~ - flagUseUnitThreaded(useUnitThreaded) - ); - if(phobosStatus != 0) - { - exitCode = phobosStatus; - return; - } - - auto vibeStatus = runFromCurrentDir( - "run_tests"~ - flagMode(Mode.vibe)~ - flagCoreTestsOnly(false)~ - flagUseUnitThreaded(useUnitThreaded) - ); - exitCode = vibeStatus; -} - -void runTravisTests() -{ - useUnitThreaded = envBool("USE_UNIT_THREADED"); - auto noVibe = envBool("NO_VIBE"); - - if(noVibe) - { - auto phobosStatus = runFromCurrentDir( - "run_tests"~ - flagMode(Mode.phobos)~ - flagCoreTestsOnly(false)~ - flagUseUnitThreaded(useUnitThreaded) - ); - exitCode = phobosStatus; - } - else - { - runCombinedTests(); - } -} diff --git a/source/mysql/commands.d b/source/mysql/commands.d index b5a3aadd..ab64c4fd 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -51,103 +51,6 @@ struct ColumnSpecialization ///ditto alias CSN = ColumnSpecialization; -@("columnSpecial") -debug(MYSQLN_TESTS) -unittest -{ - import std.array; - import std.range; - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `columnSpecial`"); - cn.exec("CREATE TABLE `columnSpecial` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below - auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - auto data = alph.cycle.take(totalSize).array; - cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(string)data)~"\")"); - - // Common stuff - int chunkSize; - immutable selectSQL = "SELECT `data` FROM `columnSpecial`"; - ubyte[] received; - bool lastValueOfFinished; - void receiver(const(ubyte)[] chunk, bool finished) - { - assert(lastValueOfFinished == false); - - if(finished) - assert(chunk.length == chunkSize); - else - assert(chunk.length < chunkSize); // Not always true in general, but true in this unittest - - received ~= chunk; - lastValueOfFinished = finished; - } - - // Sanity check - auto value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); - - // Use ColumnSpecialization with sql string, - // and totalSize as a multiple of chunkSize - { - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); - - received = null; - lastValueOfFinished = false; - value = cn.queryValue(selectSQL, [columnSpecial]); - assert(!value.isNull); - assert(value.get == data); - //TODO: ColumnSpecialization is not yet implemented - //assert(lastValueOfFinished == true); - //assert(received == data); - } - - // Use ColumnSpecialization with sql string, - // and totalSize as a non-multiple of chunkSize - { - chunkSize = 64; - assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); - auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); - - received = null; - lastValueOfFinished = false; - value = cn.queryValue(selectSQL, [columnSpecial]); - assert(!value.isNull); - assert(value.get == data); - //TODO: ColumnSpecialization is not yet implemented - //assert(lastValueOfFinished == true); - //assert(received == data); - } - - // Use ColumnSpecialization with prepared statement, - // and totalSize as a multiple of chunkSize - { - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); - - received = null; - lastValueOfFinished = false; - auto prepared = cn.prepare(selectSQL); - prepared.columnSpecials = [columnSpecial]; - value = cn.queryValue(prepared); - assert(!value.isNull); - assert(value.get == data); - //TODO: ColumnSpecialization is not yet implemented - //assert(lastValueOfFinished == true); - //assert(received == data); - } -} - /++ Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. @@ -583,24 +486,6 @@ package void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, re conn.purgeResult(); } -// Test what happends when queryRowTuple receives no rows -@("queryRowTuple_noRows") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.test.common : scopedCn, createCn; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `queryRowTuple_noRows`"); - cn.exec("CREATE TABLE `queryRowTuple_noRows` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable selectSQL = "SELECT * FROM `queryRowTuple_noRows`"; - int queryTupleResult; - assertThrown!MYX(cn.queryRowTuple(selectSQL, queryTupleResult)); -} - /++ Execute an SQL SELECT command or prepared statement and return a single value: the first column of the first row received. @@ -742,273 +627,3 @@ package Nullable!Variant queryValueImpl(ColumnSpecialization[] csa, Connection c } } -@("execOverloads") -debug(MYSQLN_TESTS) -unittest -{ - import std.array; - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `execOverloads`"); - cn.exec("CREATE TABLE `execOverloads` ( - `i` INTEGER, - `s` VARCHAR(50) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable prepareSQL = "INSERT INTO `execOverloads` VALUES (?, ?)"; - - // Do the inserts, using exec - - // exec: const(char[]) sql - assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1); - assert(cn.exec(prepareSQL, 2, "bb") == 1); - assert(cn.exec(prepareSQL, [Variant(3), Variant("cc")]) == 1); - - // exec: prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(4, "dd"); - assert(cn.exec(prepared) == 1); - - assert(cn.exec(prepared, 5, "ee") == 1); - assert(prepared.getArg(0) == 5); - assert(prepared.getArg(1) == "ee"); - - assert(cn.exec(prepared, [Variant(6), Variant("ff")]) == 1); - assert(prepared.getArg(0) == 6); - assert(prepared.getArg(1) == "ff"); - - // exec: bcPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(7, "gg"); - assert(cn.exec(bcPrepared) == 1); - assert(bcPrepared.getArg(0) == 7); - assert(bcPrepared.getArg(1) == "gg"); - - // Check results - auto rows = cn.query("SELECT * FROM `execOverloads`").array(); - assert(rows.length == 7); - - assert(rows[0].length == 2); - assert(rows[1].length == 2); - assert(rows[2].length == 2); - assert(rows[3].length == 2); - assert(rows[4].length == 2); - assert(rows[5].length == 2); - assert(rows[6].length == 2); - - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); - assert(rows[1][0] == 2); - assert(rows[1][1] == "bb"); - assert(rows[2][0] == 3); - assert(rows[2][1] == "cc"); - assert(rows[3][0] == 4); - assert(rows[3][1] == "dd"); - assert(rows[4][0] == 5); - assert(rows[4][1] == "ee"); - assert(rows[5][0] == 6); - assert(rows[5][1] == "ff"); - assert(rows[6][0] == 7); - assert(rows[6][1] == "gg"); -} - -@("queryOverloads") -debug(MYSQLN_TESTS) -unittest -{ - import std.array; - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `queryOverloads`"); - cn.exec("CREATE TABLE `queryOverloads` ( - `i` INTEGER, - `s` VARCHAR(50) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `queryOverloads` VALUES (1, \"aa\"), (2, \"bb\"), (3, \"cc\")"); - - immutable prepareSQL = "SELECT * FROM `queryOverloads` WHERE `i`=? AND `s`=?"; - - // Test query - { - Row[] rows; - - // String sql - rows = cn.query("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); - - rows = cn.query(prepareSQL, 2, "bb").array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == "bb"); - - rows = cn.query(prepareSQL, [Variant(3), Variant("cc")]).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 3); - assert(rows[0][1] == "cc"); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(1, "aa"); - rows = cn.query(prepared).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); - - rows = cn.query(prepared, 2, "bb").array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == "bb"); - - rows = cn.query(prepared, [Variant(3), Variant("cc")]).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 3); - assert(rows[0][1] == "cc"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - rows = cn.query(bcPrepared).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); - } - - // Test queryRow - { - Nullable!Row row; - - // String sql - row = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); - assert(!row.isNull); - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); - - row = cn.queryRow(prepareSQL, 2, "bb"); - assert(!row.isNull); - assert(row.length == 2); - assert(row[0] == 2); - assert(row[1] == "bb"); - - row = cn.queryRow(prepareSQL, [Variant(3), Variant("cc")]); - assert(!row.isNull); - assert(row.length == 2); - assert(row[0] == 3); - assert(row[1] == "cc"); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(1, "aa"); - row = cn.queryRow(prepared); - assert(!row.isNull); - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); - - row = cn.queryRow(prepared, 2, "bb"); - assert(!row.isNull); - assert(row.length == 2); - assert(row[0] == 2); - assert(row[1] == "bb"); - - row = cn.queryRow(prepared, [Variant(3), Variant("cc")]); - assert(!row.isNull); - assert(row.length == 2); - assert(row[0] == 3); - assert(row[1] == "cc"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - row = cn.queryRow(bcPrepared); - assert(!row.isNull); - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); - } - - // Test queryRowTuple - { - int i; - string s; - - // String sql - cn.queryRowTuple("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"", i, s); - assert(i == 1); - assert(s == "aa"); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(2, "bb"); - cn.queryRowTuple(prepared, i, s); - assert(i == 2); - assert(s == "bb"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(3, "cc"); - cn.queryRowTuple(bcPrepared, i, s); - assert(i == 3); - assert(s == "cc"); - } - - // Test queryValue - { - Nullable!Variant value; - - // String sql - value = cn.queryValue("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\""); - assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); - assert(value.get == 1); - - value = cn.queryValue(prepareSQL, 2, "bb"); - assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); - assert(value.get == 2); - - value = cn.queryValue(prepareSQL, [Variant(3), Variant("cc")]); - assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); - assert(value.get == 3); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(1, "aa"); - value = cn.queryValue(prepared); - assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); - assert(value.get == 1); - - value = cn.queryValue(prepared, 2, "bb"); - assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); - assert(value.get == 2); - - value = cn.queryValue(prepared, [Variant(3), Variant("cc")]); - assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); - assert(value.get == 3); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - value = cn.queryValue(bcPrepared); - assert(!value.isNull); - assert(value.get.type != typeid(typeof(null))); - assert(value.get == 1); - } -} diff --git a/source/mysql/connection.d b/source/mysql/connection.d index e0042d4e..4cde1359 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -17,10 +17,6 @@ import mysql.protocol.constants; import mysql.protocol.packets; import mysql.protocol.sockets; import mysql.result; -debug(MYSQLN_TESTS) -{ - import mysql.test.common; -} version(Have_vibe_core) { @@ -97,28 +93,6 @@ Prepared prepareFunction(Connection conn, string name, int numArgs) return prepare(conn, sql); } -/// -@("prepareFunction") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.test.common; - mixin(scopedCn); - - exec(cn, `DROP FUNCTION IF EXISTS hello`); - exec(cn, ` - CREATE FUNCTION hello (s CHAR(20)) - RETURNS CHAR(50) DETERMINISTIC - RETURN CONCAT('Hello ',s,'!') - `); - - auto preparedHello = prepareFunction(cn, "hello", 1); - preparedHello.setArgs("World"); - auto rs = cn.query(preparedHello).array; - assert(rs.length == 1); - assert(rs[0][0] == "Hello World!"); -} - /++ Convenience function to create a prepared statement which calls a stored procedure. @@ -141,33 +115,6 @@ Prepared prepareProcedure(Connection conn, string name, int numArgs) return prepare(conn, sql); } -/// -@("prepareProcedure") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.test.common; - import mysql.test.integration; - mixin(scopedCn); - initBaseTestTables(cn); - - exec(cn, `DROP PROCEDURE IF EXISTS insert2`); - exec(cn, ` - CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) - BEGIN - INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); - END - `); - - auto preparedInsert2 = prepareProcedure(cn, "insert2", 2); - preparedInsert2.setArgs(2001, "inserted string 1"); - cn.exec(preparedInsert2); - - auto rs = query(cn, "SELECT stringcol FROM basetest WHERE intcol=2001").array; - assert(rs.length == 1); - assert(rs[0][0] == "inserted string 1"); -} - private string preparedPlaceholderArgs(int numArgs) { auto sql = "("; @@ -772,71 +719,6 @@ public: connect(clientCapabilities); } - // This also serves as a regression test for #167: - // ResultRange doesn't get invalidated upon reconnect - @("reconnect") - debug(MYSQLN_TESTS) - unittest - { - import std.variant; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `reconnect`"); - cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)"); - - enum sql = "SELECT a FROM `reconnect`"; - - // Sanity check - auto rows = cn.query(sql).array; - assert(rows[0][0] == 1); - assert(rows[1][0] == 2); - assert(rows[2][0] == 3); - - // Ensure reconnect keeps the same connection when it's supposed to - auto range = cn.query(sql); - assert(range.front[0] == 1); - cn.reconnect(); - assert(!cn.closed); // Is open? - assert(range.isValid); // Still valid? - range.popFront(); - assert(range.front[0] == 2); - - // Ensure reconnect reconnects when it's supposed to - range = cn.query(sql); - assert(range.front[0] == 1); - cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities - cn.reconnect(~cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Try manually reconnecting - range = cn.query(sql); - assert(range.front[0] == 1); - cn.connect(cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Try manually closing and connecting - range = cn.query(sql); - assert(range.front[0] == 1); - cn.close(); - assert(cn.closed); // Is closed? - assert(!range.isValid); // Was invalidated? - cn.connect(cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Auto-reconnect upon a command - cn.close(); - assert(cn.closed); - range = cn.query(sql); - assert(!cn.closed); - assert(range.front[0] == 1); - } - private void quit() in { @@ -1147,34 +1029,6 @@ public: preparedRegistrations.queueAllForRelease(); } - @("releaseAll") - debug(MYSQLN_TESTS) - unittest - { - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `releaseAll`"); - cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`"); - auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)"); - assert(cn.isRegistered(preparedSelect)); - assert(cn.isRegistered(preparedInsert)); - - cn.releaseAll(); - assert(!cn.isRegistered(preparedSelect)); - assert(!cn.isRegistered(preparedInsert)); - cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)"); - assert(!cn.isRegistered(preparedSelect)); - assert(!cn.isRegistered(preparedInsert)); - - cn.exec(preparedInsert); - cn.query(preparedSelect).array; - assert(cn.isRegistered(preparedSelect)); - assert(cn.isRegistered(preparedInsert)); - - } - /// Is the given statement registered on this connection as a prepared statement? bool isRegistered(Prepared prepared) { @@ -1193,252 +1047,3 @@ public: return !info.isNull && !info.get.queuedForRelease; } } - -// Test register, release, isRegistered, and auto-register for prepared statements -@("autoRegistration") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.connection; - import mysql.test.common; - - Prepared preparedInsert; - Prepared preparedSelect; - immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; - immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; - int queryTupleResult; - - { - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); - cn.exec("CREATE TABLE `autoRegistration` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - // Initial register - preparedInsert = cn.prepare(insertSQL); - preparedSelect = cn.prepare(selectSQL); - - // Test basic register, release, isRegistered - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - - // Test manual re-register - cn.register(preparedInsert); - cn.register(preparedSelect); - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - - // Test double register - cn.register(preparedInsert); - cn.register(preparedSelect); - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - - // Test double release - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - } - - // Note that at this point, both prepared statements still exist, - // but are no longer registered on any connection. In fact, there - // are no open connections anymore. - - // Test auto-register: exec - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedInsert)); - cn.exec(preparedInsert); - assert(cn.isRegistered(preparedInsert)); - } - - // Test auto-register: query - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedSelect)); - cn.query(preparedSelect).each(); - assert(cn.isRegistered(preparedSelect)); - } - - // Test auto-register: queryRow - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedSelect)); - cn.queryRow(preparedSelect); - assert(cn.isRegistered(preparedSelect)); - } - - // Test auto-register: queryRowTuple - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedSelect)); - cn.queryRowTuple(preparedSelect, queryTupleResult); - assert(cn.isRegistered(preparedSelect)); - } - - // Test auto-register: queryValue - { - mixin(scopedCn); - - assert(!cn.isRegistered(preparedSelect)); - cn.queryValue(preparedSelect); - assert(cn.isRegistered(preparedSelect)); - } -} - -// An attempt to reproduce issue #81: Using mysql-native driver with no default database -// I'm unable to actually reproduce the error, though. -@("issue81") -debug(MYSQLN_TESTS) -unittest -{ - import mysql.escape; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `issue81`"); - cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); - - auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); - scope(exit) cn2.close(); - - cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); -} - -// Regression test for Issue #154: -// autoPurge can throw an exception if the socket was closed without purging -// -// This simulates a disconnect by closing the socket underneath the Connection -// object itself. -@("dropConnection") -debug(MYSQLN_TESTS) -unittest -{ - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `dropConnection`"); - cn.exec("CREATE TABLE `dropConnection` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)"); - import mysql.prepared; - { - auto prep = cn.prepare("SELECT * FROM `dropConnection`"); - cn.query(prep); - } - // close the socket forcibly - cn._socket.close(); - // this should still work (it should reconnect). - cn.exec("DROP TABLE `dropConnection`"); -} - -/+ -Test Prepared's ability to be safely refcount-released during a GC cycle -(ie, `Connection.release` must not allocate GC memory). - -Currently disabled because it's not guaranteed to always work -(and apparently, cannot be made to work?) -For relevant discussion, see issue #159: -https://github.com/mysql-d/mysql-native/issues/159 -+/ -version(none) -debug(MYSQLN_TESTS) -{ - /// Proof-of-concept ref-counted Prepared wrapper, just for testing, - /// not really intended for actual use. - private struct RCPreparedPayload - { - Prepared prepared; - Connection conn; // Connection to be released from - - alias prepared this; - - @disable this(this); // not copyable - ~this() - { - // There are a couple calls to this dtor where `conn` happens to be null. - if(conn is null) - return; - - assert(conn.isRegistered(prepared)); - conn.release(prepared); - } - } - ///ditto - alias RCPrepared = RefCounted!(RCPreparedPayload, RefCountedAutoInitialize.no); - ///ditto - private RCPrepared rcPrepare(Connection conn, const(char[]) sql) - { - import std.algorithm.mutation : move; - - auto prepared = conn.prepare(sql); - auto payload = RCPreparedPayload(prepared, conn); - return refCounted(move(payload)); - } - - @("rcPrepared") - unittest - { - import core.memory; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `rcPrepared`"); - cn.exec("CREATE TABLE `rcPrepared` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `rcPrepared` VALUES (1), (2), (3)"); - - // Define this in outer scope to guarantee data is left pending when - // RCPrepared's payload is collected. This will guarantee - // that Connection will need to queue the release. - ResultRange rows; - - void bar() - { - class Foo { RCPrepared p; } - auto foo = new Foo(); - - auto rcStmt = cn.rcPrepare("SELECT * FROM `rcPrepared`"); - foo.p = rcStmt; - rows = cn.query(rcStmt); - - /+ - At this point, there are two references to the prepared statement: - One in a `Foo` object (currently bound to `foo`), and one on the stack. - - Returning from this function will destroy the one on the stack, - and deterministically reduce the refcount to 1. - - So, right here we set `foo` to null to *keep* the Foo object's - reference to the prepared statement, but set adrift the Foo object - itself, ready to be destroyed (along with the only remaining - prepared statement reference it contains) by the next GC cycle. - - Thus, `RCPreparedPayload.~this` and `Connection.release(Prepared)` - will be executed during a GC cycle...and had better not perform - any allocations, or else...boom! - +/ - foo = null; - } - - bar(); - assert(cn.hasPending); // Ensure Connection is forced to queue the release. - GC.collect(); // `Connection.release(Prepared)` better not be allocating, or boom! - } -} diff --git a/source/mysql/exceptions.d b/source/mysql/exceptions.d index 8717a7f2..115cb560 100644 --- a/source/mysql/exceptions.d +++ b/source/mysql/exceptions.d @@ -147,41 +147,3 @@ class MYXInvalidatedRange: MYX super(msg, file, line); } } - -@("wrongFunctionException") -debug(MYSQLN_TESTS) -unittest -{ - import std.exception; - import mysql.commands; - import mysql.connection; - import mysql.prepared; - import mysql.test.common : scopedCn, createCn; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `wrongFunctionException`"); - cn.exec("CREATE TABLE `wrongFunctionException` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable insertSQL = "INSERT INTO `wrongFunctionException` VALUES (1), (2)"; - immutable selectSQL = "SELECT * FROM `wrongFunctionException`"; - Prepared preparedInsert; - Prepared preparedSelect; - int queryTupleResult; - assertNotThrown!MYXWrongFunction(cn.exec(insertSQL)); - assertNotThrown!MYXWrongFunction(cn.query(selectSQL).each()); - assertNotThrown!MYXWrongFunction(cn.queryRowTuple(selectSQL, queryTupleResult)); - assertNotThrown!MYXWrongFunction(preparedInsert = cn.prepare(insertSQL)); - assertNotThrown!MYXWrongFunction(preparedSelect = cn.prepare(selectSQL)); - assertNotThrown!MYXWrongFunction(cn.exec(preparedInsert)); - assertNotThrown!MYXWrongFunction(cn.query(preparedSelect).each()); - assertNotThrown!MYXWrongFunction(cn.queryRowTuple(preparedSelect, queryTupleResult)); - - assertThrown!MYXResultRecieved(cn.exec(selectSQL)); - assertThrown!MYXNoResultRecieved(cn.query(insertSQL).each()); - assertThrown!MYXNoResultRecieved(cn.queryRowTuple(insertSQL, queryTupleResult)); - assertThrown!MYXResultRecieved(cn.exec(preparedSelect)); - assertThrown!MYXNoResultRecieved(cn.query(preparedInsert).each()); - assertThrown!MYXNoResultRecieved(cn.queryRowTuple(preparedInsert, queryTupleResult)); -} diff --git a/source/mysql/package.d b/source/mysql/package.d index d5c977e2..b0f919c3 100644 --- a/source/mysql/package.d +++ b/source/mysql/package.d @@ -75,25 +75,3 @@ public import mysql.prepared; public import mysql.protocol.constants : SvrCapFlags; public import mysql.result; public import mysql.types; - -debug(MYSQLN_TESTS) version = DoCoreTests; -debug(MYSQLN_CORE_TESTS) version = DoCoreTests; - -version(DoCoreTests) -{ - public import mysql.protocol.comms; - public import mysql.protocol.constants; - public import mysql.protocol.extra_types; - public import mysql.protocol.packet_helpers; - public import mysql.protocol.packets; - public import mysql.protocol.sockets; - - public import mysql.test.common; - public import mysql.test.integration; - public import mysql.test.regression; - - version(MYSQLN_TESTS_NO_MAIN) {} else - { - void main() {} - } -} diff --git a/source/mysql/pool.d b/source/mysql/pool.d index 8772a048..2d89c1af 100644 --- a/source/mysql/pool.d +++ b/source/mysql/pool.d @@ -17,10 +17,6 @@ import std.typecons; import mysql.connection; import mysql.prepared; import mysql.protocol.constants; -debug(MYSQLN_TESTS) -{ - import mysql.test.common; -} version(Have_vibe_core) { @@ -195,7 +191,7 @@ version(IncludeMySQLPool) /// Applies any `autoRegister`/`autoRelease` settings to a connection, /// if necessary. - private void applyAuto(T)(T conn) + package void applyAuto(T)(T conn) { foreach(sql, info; preparedRegistrations.directLookup) { @@ -232,50 +228,6 @@ version(IncludeMySQLPool) return m_onNewConnection; } - @("onNewConnection") - debug(MYSQLN_TESTS) - unittest - { - auto count = 0; - void callback(Connection conn) - { - count++; - } - - // Test getting/setting - auto poolA = new MySQLPool(testConnectionStr, &callback); - auto poolB = new MySQLPool(testConnectionStr); - auto poolNoCallback = new MySQLPool(testConnectionStr); - - assert(poolA.onNewConnection == &callback); - assert(poolB.onNewConnection is null); - assert(poolNoCallback.onNewConnection is null); - - poolB.onNewConnection = &callback; - assert(poolB.onNewConnection == &callback); - assert(count == 0); - - // Ensure callback is called - { - auto connA = poolA.lockConnection(); - assert(!connA.closed); - assert(count == 1); - - auto connB = poolB.lockConnection(); - assert(!connB.closed); - assert(count == 2); - } - - // Ensure works with no callback - { - auto oldCount = count; - auto poolC = new MySQLPool(testConnectionStr); - auto connC = poolC.lockConnection(); - assert(!connC.closed); - assert(count == oldCount); - } - } - /++ Forwards to vibe.d's $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency, ConnectionPool.maxConcurrency) @@ -470,125 +422,4 @@ version(IncludeMySQLPool) } } } - - @("registration") - debug(MYSQLN_TESTS) - unittest - { - import mysql.commands; - auto pool = new MySQLPool(testConnectionStr); - - // Setup - Connection cn = pool.lockConnection(); - cn.exec("DROP TABLE IF EXISTS `poolRegistration`"); - cn.exec("CREATE TABLE `poolRegistration` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - immutable sql = "SELECT * from `poolRegistration`"; - //auto cn2 = pool.lockConnection(); // Seems to return the same connection as `cn` - auto cn2 = pool.createConnection(); - pool.applyAuto(cn2); - assert(cn !is cn2); - - // Tests: - // Initial - assert(pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(!cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - // Register on connection #1 - auto prepared = cn.prepare(sql); - { - assert(pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` - auto cn3 = pool.createConnection(); - pool.applyAuto(cn3); - assert(!cn3.isRegistered(sql)); - } - - // autoRegister - pool.autoRegister(prepared); - { - assert(!pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(!pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - //auto cn3 = pool.lockConnection(); // Seems to return the *same* connection as `cn` - auto cn3 = pool.createConnection(); - pool.applyAuto(cn3); - assert(cn3.isRegistered(sql)); - } - - // autoRelease - pool.autoRelease(prepared); - { - assert(!pool.isAutoCleared(sql)); - assert(!pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` - auto cn3 = pool.createConnection(); - pool.applyAuto(cn3); - assert(!cn3.isRegistered(sql)); - } - - // clearAuto - pool.clearAuto(prepared); - { - assert(pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` - auto cn3 = pool.createConnection(); - pool.applyAuto(cn3); - assert(!cn3.isRegistered(sql)); - } - } - - @("closedConnection") // "cct" - debug(MYSQLN_TESTS) - { - MySQLPool cctPool; - int cctCount=0; - - void cctStart() - { - import std.array; - import mysql.commands; - - cctPool = new MySQLPool(testConnectionStr); - cctPool.onNewConnection = (Connection conn) { cctCount++; }; - assert(cctCount == 0); - - auto cn = cctPool.lockConnection(); - assert(!cn.closed); - cn.close(); - assert(cn.closed); - assert(cctCount == 1); - } - - unittest - { - cctStart(); - assert(cctCount == 1); - - auto cn = cctPool.lockConnection(); - assert(cctCount == 1); - assert(!cn.closed); - } - } } diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index 385eaf7e..a7b1d6df 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -13,8 +13,6 @@ import mysql.protocol.comms; import mysql.protocol.constants; import mysql.protocol.packets; import mysql.result; -debug(MYSQLN_TESTS) - import mysql.test.common; /++ A struct to represent specializations of prepared statement parameters. @@ -38,101 +36,6 @@ struct ParameterSpecialization ///ditto alias PSN = ParameterSpecialization; -@("paramSpecial") -debug(MYSQLN_TESTS) -unittest -{ - import std.array; - import std.range; - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `paramSpecial`"); - cn.exec("CREATE TABLE `paramSpecial` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below - auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - auto data = alph.cycle.take(totalSize).array; - - int chunkSize; - const(ubyte)[] dataToSend; - bool finished; - uint sender(ubyte[] chunk) - { - assert(!finished); - assert(chunk.length == chunkSize); - - if(dataToSend.length < chunkSize) - { - auto actualSize = cast(uint) dataToSend.length; - chunk[0..actualSize] = dataToSend[]; - finished = true; - dataToSend.length = 0; - return actualSize; - } - else - { - chunk[] = dataToSend[0..chunkSize]; - dataToSend = dataToSend[chunkSize..$]; - return chunkSize; - } - } - - immutable selectSQL = "SELECT `data` FROM `paramSpecial`"; - - // Sanity check - cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(string)data)~"\")"); - auto value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); - - { - // Clear table - cn.exec("DELETE FROM `paramSpecial`"); - value = cn.queryValue(selectSQL); // Ensure deleted - assert(value.isNull); - - // Test: totalSize as a multiple of chunkSize - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); - - finished = false; - dataToSend = data; - auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); - prepared.setArg(0, cast(ubyte[])[], paramSpecial); - assert(cn.exec(prepared) == 1); - value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); - } - - { - // Clear table - cn.exec("DELETE FROM `paramSpecial`"); - value = cn.queryValue(selectSQL); // Ensure deleted - assert(value.isNull); - - // Test: totalSize as a non-multiple of chunkSize - chunkSize = 64; - assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); - auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); - - finished = false; - dataToSend = data; - auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); - prepared.setArg(0, cast(ubyte[])[], paramSpecial); - assert(cn.exec(prepared) == 1); - value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); - } -} - /++ Encapsulation of a prepared statement. @@ -239,52 +142,6 @@ public: setArg(index, val.get(), psn); } - @("setArg-typeMods") - debug(MYSQLN_TESTS) - unittest - { - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`"); - cn.exec("CREATE TABLE `setArg-typeMods` ( - `i` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)"; - - // Sanity check - { - int i = 111; - assert(cn.exec(insertSQL, i) == 1); - auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`"); - assert(!value.isNull); - assert(value.get == i); - } - - // Test const(int) - { - const(int) i = 112; - assert(cn.exec(insertSQL, i) == 1); - } - - // Test immutable(int) - { - immutable(int) i = 113; - assert(cn.exec(insertSQL, i) == 1); - } - - // Note: Variant doesn't seem to support - // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. - - // Test shared immutable(int) - { - shared immutable(int) i = 113; - assert(cn.exec(insertSQL, i) == 1); - } - } - /++ Bind a tuple of D variables to the parameters of a prepared statement. @@ -380,67 +237,6 @@ public: return _sql; } - @("setNullArg") - debug(MYSQLN_TESTS) - unittest - { - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `setNullArg`"); - cn.exec("CREATE TABLE `setNullArg` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; - immutable selectSQL = "SELECT * FROM `setNullArg`"; - auto preparedInsert = cn.prepare(insertSQL); - assert(preparedInsert.sql == insertSQL); - Row[] rs; - - { - Nullable!int nullableInt; - nullableInt.nullify(); - preparedInsert.setArg(0, nullableInt); - assert(preparedInsert.getArg(0).type == typeid(typeof(null))); - nullableInt = 7; - preparedInsert.setArg(0, nullableInt); - assert(preparedInsert.getArg(0) == 7); - - nullableInt.nullify(); - preparedInsert.setArgs(nullableInt); - assert(preparedInsert.getArg(0).type == typeid(typeof(null))); - nullableInt = 7; - preparedInsert.setArgs(nullableInt); - assert(preparedInsert.getArg(0) == 7); - } - - preparedInsert.setArg(0, 5); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 1); - assert(rs[0][0] == 5); - - preparedInsert.setArg(0, null); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 2); - assert(rs[0][0] == 5); - assert(rs[1].isNull(0)); - assert(rs[1][0].type == typeid(typeof(null))); - - preparedInsert.setArg(0, Variant(null)); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 3); - assert(rs[0][0] == 5); - assert(rs[1].isNull(0)); - assert(rs[2].isNull(0)); - assert(rs[1][0].type == typeid(typeof(null))); - assert(rs[2][0].type == typeid(typeof(null))); - } - /// Gets the number of arguments this prepared statement expects to be passed in. @property ushort numArgs() pure const nothrow { @@ -452,27 +248,6 @@ public: /// from this prepared statement. @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } - @("lastInsertID") - debug(MYSQLN_TESTS) - unittest - { - import mysql.connection; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); - cn.exec("CREATE TABLE `testPreparedLastInsertID` ( - `a` INTEGER NOT NULL AUTO_INCREMENT, - PRIMARY KEY (a) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); - cn.exec(stmt); - assert(stmt.lastInsertID == 1); - cn.exec(stmt); - assert(stmt.lastInsertID == 2); - cn.exec(stmt); - assert(stmt.lastInsertID == 3); - } - /// Gets the prepared header's field descriptions. @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } @@ -494,6 +269,26 @@ private enum isPreparedRegistrationsPayload(Payload) = p.queuedForRelease = true; }); +debug(MYSQLN_TESTS) +{ + // Test template constraint + struct TestPreparedRegistrationsBad1 { } + struct TestPreparedRegistrationsBad2 { bool foo = false; } + struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; } + struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } + struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } + struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; const(char)[] id; } + + static assert(!isPreparedRegistrationsPayload!int); + static assert(!isPreparedRegistrationsPayload!bool); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4); + //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1); + //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2); +} + /++ Common functionality for recordkeeping of prepared statement registration and queueing for unregister. @@ -596,125 +391,3 @@ package struct PreparedRegistrations(Payload) } } -// Test PreparedRegistrations -debug(MYSQLN_TESTS) -{ - // Test template constraint - struct TestPreparedRegistrationsBad1 { } - struct TestPreparedRegistrationsBad2 { bool foo = false; } - struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; } - struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } - struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } - struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; const(char)[] id; } - - static assert(!isPreparedRegistrationsPayload!int); - static assert(!isPreparedRegistrationsPayload!bool); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4); - //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1); - //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2); - PreparedRegistrations!TestPreparedRegistrationsGood1 testPreparedRegistrationsGood1; - PreparedRegistrations!TestPreparedRegistrationsGood2 testPreparedRegistrationsGood2; - - @("PreparedRegistrations") - unittest - { - // Test init - PreparedRegistrations!TestPreparedRegistrationsGood2 pr; - assert(pr.directLookup.keys.length == 0); - - void resetData(bool isQueued1, bool isQueued2, bool isQueued3) - { - pr.directLookup["1"] = TestPreparedRegistrationsGood2(isQueued1, "1"); - pr.directLookup["2"] = TestPreparedRegistrationsGood2(isQueued2, "2"); - pr.directLookup["3"] = TestPreparedRegistrationsGood2(isQueued3, "3"); - assert(pr.directLookup.keys.length == 3); - } - - // Test resetData (sanity check) - resetData(false, true, false); - assert(pr.directLookup["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr.directLookup["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr.directLookup["3"] == TestPreparedRegistrationsGood2(false, "3")); - - // Test opIndex - resetData(false, true, false); - pr.directLookup["1"] = TestPreparedRegistrationsGood2(false, "1"); - pr.directLookup["2"] = TestPreparedRegistrationsGood2(true, "2"); - pr.directLookup["3"] = TestPreparedRegistrationsGood2(false, "3"); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - assert(pr["4"].isNull); - - // Test queueForRelease - resetData(false, true, false); - pr.queueForRelease("2"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - - pr.queueForRelease("3"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); - - pr.queueForRelease("4"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); - - // Test unqueueForRelease - resetData(false, true, false); - pr.unqueueForRelease("1"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - - pr.unqueueForRelease("2"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - - pr.unqueueForRelease("4"); - assert(pr.directLookup.keys.length == 3); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); - - // Test queueAllForRelease - resetData(false, true, false); - pr.queueAllForRelease(); - assert(pr["1"] == TestPreparedRegistrationsGood2(true, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); - assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); - assert(pr["4"].isNull); - - // Test clear - resetData(false, true, false); - pr.clear(); - assert(pr.directLookup.keys.length == 0); - - // Test registerIfNeeded - auto doRegister(const(char[]) sql) { return TestPreparedRegistrationsGood2(false, sql); } - pr.registerIfNeeded("1", &doRegister); - assert(pr.directLookup.keys.length == 1); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - - pr.registerIfNeeded("1", &doRegister); - assert(pr.directLookup.keys.length == 1); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - - pr.registerIfNeeded("2", &doRegister); - assert(pr.directLookup.keys.length == 2); - assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); - assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); - } -} diff --git a/source/mysql/result.d b/source/mysql/result.d index be9a5555..0928b53c 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -81,31 +81,6 @@ public: return _names[index]; } - @("getName") - debug(MYSQLN_TESTS) - unittest - { - import mysql.test.common; - import mysql.commands; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `row_getName`"); - cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)"); - - enum sql = "SELECT another, someValue FROM `row_getName`"; - - auto rows = cn.query(sql).array; - assert(rows.length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == 1); - assert(rows[0].getName(0) == "another"); - assert(rows[0].getName(1) == "someValue"); - assert(rows[1][0] == 4); - assert(rows[1][1] == 3); - assert(rows[1].getName(0) == "another"); - assert(rows[1].getName(1) == "someValue"); - } - /++ Check if a column in the result row was NULL diff --git a/testconn/dub.sdl b/testconn/dub.sdl new file mode 100644 index 00000000..32869a84 --- /dev/null +++ b/testconn/dub.sdl @@ -0,0 +1,7 @@ +name "testconn" +description "Test connection utility" +license "BSL-1.0" +copyright "Copyright (c) 2011-2021 Steve Teale, James W. Oliphant, Simen Endsjø, Sönke Ludwig, Sergey Shamov, Nick Sabalausky, and Steven Schveighoffer" +authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" "Steven Schveighoffer" + +dependency "mysql-native" path="../" diff --git a/source/app.d b/testconn/source/app.d similarity index 95% rename from source/app.d rename to testconn/source/app.d index fd4f8d91..2718741a 100644 --- a/source/app.d +++ b/testconn/source/app.d @@ -1,5 +1,5 @@ -/++ -Usage: app [connection string] +/++ +Usage: testconn [connection string] If connection string isn't provided, the following default connection string will be used: host=localhost;port=3306;user=testuser;pwd=testpassword;db=testdb @@ -21,11 +21,8 @@ void main(string[] args) connStr = args[1]; else writeln("No connection string provided on cmdline, using default:\n", connStr); - - try testMySql(connStr); - catch( Exception e ){ - writeln("Failed: ", e.toString()); - } + + testMySql(connStr); } void testMySql(string connStr) @@ -82,7 +79,7 @@ void testMySql(string connStr) if(caps && SvrCapFlags.MULTI_RESULTS) writeln("\tMultiple result set support"); writeln(); - + MetaData md = MetaData(c); auto dbList = md.databases(); writefln("Found %s databases", dbList.length);