diff --git a/.github/containers/Dockerfile b/.github/containers/Dockerfile index 3b4b0a7f87..260c01d89f 100644 --- a/.github/containers/Dockerfile +++ b/.github/containers/Dockerfile @@ -23,12 +23,15 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ build-essential \ curl \ expat \ + freetds-common \ + freetds-dev \ gcc \ git \ libbz2-dev \ libcurl4-openssl-dev \ libffi-dev \ libgmp-dev \ + libkrb5-dev \ liblzma-dev \ libmpfr-dev \ libncurses-dev \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f8e5182434..f4cb8b8223 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,6 +41,7 @@ jobs: #- kafka - memcached - mongodb + - mssql - mysql - postgres - rabbitmq @@ -118,7 +119,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -163,7 +164,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -208,7 +209,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -257,6 +258,69 @@ jobs: path: ./**/.coverage.* retention-days: 1 + mssql: + env: + TOTAL_GROUPS: 1 + + strategy: + fail-fast: false + matrix: + group-number: [1] + + runs-on: ubuntu-20.04 + container: + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest + options: >- + --add-host=host.docker.internal:host-gateway + timeout-minutes: 30 + + services: + mssql: + image: mcr.microsoft.com/azure-sql-edge:latest + env: + MSSQL_USER: python_agent + MSSQL_PASSWORD: python_agent + MSSQL_SA_PASSWORD: "python_agent#1234" + ACCEPT_EULA: "Y" + ports: + - 8080:1433 + - 8081:1433 + # Set health checks to wait until mysql has started + options: >- + --health-cmd "/opt/mssql-tools/bin/sqlcmd -U SA -P $MSSQL_SA_PASSWORD -Q 'SELECT 1'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v3 + + - name: Fetch git tags + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git fetch --tags origin + + - name: Get Environments + id: get-envs + run: | + echo "envs=$(tox -l | grep '^${{ github.job }}\-' | ./.github/workflows/get-envs.py)" >> $GITHUB_OUTPUT + env: + GROUP_NUMBER: ${{ matrix.group-number }} + + - name: Test + run: | + tox -vv -e ${{ steps.get-envs.outputs.envs }} -p auto + env: + TOX_PARALLEL_NO_SPINNER: 1 + PY_COLORS: 0 + + - name: Upload Coverage Artifacts + uses: actions/upload-artifact@v3 + with: + name: coverage-${{ github.job }}-${{ strategy.job-index }} + path: ./**/.coverage.* + retention-days: 1 + mysql: env: TOTAL_GROUPS: 2 @@ -268,7 +332,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -331,7 +395,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -389,7 +453,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -449,7 +513,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -507,7 +571,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -566,7 +630,7 @@ jobs: # runs-on: ubuntu-20.04 # container: - # image: ghcr.io/${{ github.repository }}-ci:latest + # image: ghcr.io/newrelic/newrelic-python-agent-ci:latest # options: >- # --add-host=host.docker.internal:host-gateway # timeout-minutes: 30 @@ -646,7 +710,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -704,7 +768,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -764,7 +828,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 @@ -825,7 +889,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/${{ github.repository }}-ci:latest + image: ghcr.io/newrelic/newrelic-python-agent-ci:latest options: >- --add-host=host.docker.internal:host-gateway timeout-minutes: 30 diff --git a/tests/datastore_pymssql/conftest.py b/tests/datastore_pymssql/conftest.py new file mode 100644 index 0000000000..a6584cdffe --- /dev/null +++ b/tests/datastore_pymssql/conftest.py @@ -0,0 +1,36 @@ +# Copyright 2010 New Relic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from testing_support.fixtures import ( + collector_agent_registration_fixture, + collector_available_fixture, +) # noqa: F401; pylint: disable=W0611 + + +_default_settings = { + "transaction_tracer.explain_threshold": 0.0, + "transaction_tracer.transaction_threshold": 0.0, + "transaction_tracer.stack_trace_threshold": 0.0, + "debug.log_data_collector_payloads": True, + "debug.record_transaction_failure": True, + "debug.log_explain_plan_queries": True, +} + +collector_agent_registration = collector_agent_registration_fixture( + app_name="Python Agent Test (datastore_pymssql)", + default_settings=_default_settings, + linked_applications=["Python Agent Test (datastore)"], +) diff --git a/tests/datastore_pymssql/test_database.py b/tests/datastore_pymssql/test_database.py new file mode 100644 index 0000000000..bdbf75c15f --- /dev/null +++ b/tests/datastore_pymssql/test_database.py @@ -0,0 +1,115 @@ +# Copyright 2010 New Relic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pymssql + +from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics +from testing_support.validators.validate_database_trace_inputs import validate_database_trace_inputs + +from testing_support.db_settings import mssql_settings + +from newrelic.api.background_task import background_task + +DB_SETTINGS = mssql_settings()[0] +TABLE_NAME = "datastore_pymssql_" + DB_SETTINGS["namespace"] +PROCEDURE_NAME = "hello_" + DB_SETTINGS["namespace"] + + +def execute_db_calls_with_cursor(cursor): + cursor.execute("""drop table if exists %s""" % TABLE_NAME) + + cursor.execute("""create table %s """ % TABLE_NAME + """(a integer, b real, c text)""") + + cursor.executemany( + """insert into %s """ % TABLE_NAME + """values (%s, %s, %s)""", + [(1, 1.0, "1.0"), (2, 2.2, "2.2"), (3, 3.3, "3.3")], + ) + + cursor.execute("""select * from %s""" % TABLE_NAME) + + for row in cursor: + pass + + cursor.execute("""update %s""" % TABLE_NAME + """ set a=%s, b=%s, """ """c=%s where a=%s""", (4, 4.0, "4.0", 1)) + + cursor.execute("""delete from %s where a=2""" % TABLE_NAME) + cursor.execute("""drop procedure if exists %s""" % PROCEDURE_NAME) + cursor.execute( + """CREATE PROCEDURE %s AS + BEGIN + SELECT 'Hello World!'; + END""" + % PROCEDURE_NAME + ) + + cursor.callproc(PROCEDURE_NAME) + + +_test_scoped_metrics = [ + ("Function/pymssql._pymssql:connect", 1), + ("Datastore/statement/MSSQL/%s/select" % TABLE_NAME, 1), + ("Datastore/statement/MSSQL/%s/insert" % TABLE_NAME, 1), + ("Datastore/statement/MSSQL/%s/update" % TABLE_NAME, 1), + ("Datastore/statement/MSSQL/%s/delete" % TABLE_NAME, 1), + ("Datastore/operation/MSSQL/drop", 2), + ("Datastore/operation/MSSQL/create", 2), + ("Datastore/statement/MSSQL/%s/call" % PROCEDURE_NAME, 1), + ("Datastore/operation/MSSQL/commit", 2), + ("Datastore/operation/MSSQL/rollback", 1), +] + +_test_rollup_metrics = [ + ("Datastore/all", 13), + ("Datastore/allOther", 13), + ("Datastore/MSSQL/all", 13), + ("Datastore/MSSQL/allOther", 13), + ("Datastore/statement/MSSQL/%s/select" % TABLE_NAME, 1), + ("Datastore/statement/MSSQL/%s/insert" % TABLE_NAME, 1), + ("Datastore/statement/MSSQL/%s/update" % TABLE_NAME, 1), + ("Datastore/statement/MSSQL/%s/delete" % TABLE_NAME, 1), + ("Datastore/operation/MSSQL/select", 1), + ("Datastore/operation/MSSQL/insert", 1), + ("Datastore/operation/MSSQL/update", 1), + ("Datastore/operation/MSSQL/delete", 1), + ("Datastore/statement/MSSQL/%s/call" % PROCEDURE_NAME, 1), + ("Datastore/operation/MSSQL/call", 1), + ("Datastore/operation/MSSQL/drop", 2), + ("Datastore/operation/MSSQL/create", 2), + ("Datastore/operation/MSSQL/commit", 2), + ("Datastore/operation/MSSQL/rollback", 1), +] + + +@validate_transaction_metrics( + "test_database:test_execute_via_cursor_context_manager", + scoped_metrics=_test_scoped_metrics, + rollup_metrics=_test_rollup_metrics, + background_task=True, +) +@validate_database_trace_inputs(sql_parameters_type=tuple) +@background_task() +def test_execute_via_cursor_context_manager(): + connection = pymssql.connect( + user=DB_SETTINGS["user"], password=DB_SETTINGS["password"], host=DB_SETTINGS["host"], port=DB_SETTINGS["port"] + ) + + with connection: + cursor = connection.cursor() + + with cursor: + execute_db_calls_with_cursor(cursor) + + connection.commit() + connection.rollback() + connection.commit() diff --git a/tests/testing_support/db_settings.py b/tests/testing_support/db_settings.py index bda3180622..ef9a3419c1 100644 --- a/tests/testing_support/db_settings.py +++ b/tests/testing_support/db_settings.py @@ -46,7 +46,7 @@ def postgresql_settings(): def mysql_settings(): - """Return a list of dict of settings for connecting to postgresql. + """Return a list of dict of settings for connecting to MySQL. Will return the correct settings, depending on which of the environments it is running in. It attempts to set variables in the following order, where @@ -72,6 +72,32 @@ def mysql_settings(): return settings +def mssql_settings(): + """Return a list of dict of settings for connecting to MS SQL. + + Will return the correct settings, depending on which of the environments it + is running in. It attempts to set variables in the following order, where + later environments override earlier ones. + + 1. Local + 2. Github Actions + """ + + host = "host.docker.internal" if "GITHUB_ACTIONS" in os.environ else "127.0.0.1" + instances = 1 + settings = [ + { + "user": "SA", + "password": "python_agent#1234", + "host": host, + "port": 8080 + instance_num, + "namespace": str(os.getpid()), + } + for instance_num in range(instances) + ] + return settings + + def redis_settings(): """Return a list of dict of settings for connecting to redis. diff --git a/tox.ini b/tox.ini index 0ac7331298..94722ce8bf 100644 --- a/tox.ini +++ b/tox.ini @@ -89,6 +89,7 @@ envlist = memcached-datastore_pymemcache-{py27,py37,py38,py39,py310,py311,pypy27,pypy37}, mongodb-datastore_pymongo-{py27,py37,py38,py39,py310,py311,pypy27}-pymongo{03}, mongodb-datastore_pymongo-{py37,py38,py39,py310,py311,pypy27,pypy37}-pymongo04, + mssql-datastore_pymssql-{py37,py38,py39,py310,py311}, mysql-datastore_pymysql-{py27,py37,py38,py39,py310,py311,pypy27,pypy37}, solr-datastore_pysolr-{py27,py37,py38,py39,py310,py311,pypy27,pypy37}, redis-datastore_redis-{py27,py37,py38,pypy27,pypy37}-redis03, @@ -246,6 +247,7 @@ deps = datastore_pymemcache: pymemcache datastore_pymongo-pymongo03: pymongo<4.0 datastore_pymongo-pymongo04: pymongo<5.0 + datastore_pymssql: pymssql datastore_pymysql: PyMySQL<0.11 datastore_pysolr: pysolr<4.0 datastore_redis-redislatest: redis @@ -448,6 +450,7 @@ changedir = datastore_pylibmc: tests/datastore_pylibmc datastore_pymemcache: tests/datastore_pymemcache datastore_pymongo: tests/datastore_pymongo + datastore_pymssql: tests/datastore_pymssql datastore_pymysql: tests/datastore_pymysql datastore_pysolr: tests/datastore_pysolr datastore_redis: tests/datastore_redis