Skip to content

Commit 61af772

Browse files
committed
Added a workflow to parallelise the E2E tests. Updated E2E tests to create new table names for each run to avoid issue in parallelisation
1 parent bcab1df commit 61af772

File tree

4 files changed

+207
-18
lines changed

4 files changed

+207
-18
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
name: Code Coverage (Dynamic E2E Tests - SEA & Thrift)
2+
3+
permissions:
4+
contents: read
5+
6+
on: [pull_request, workflow_dispatch]
7+
8+
jobs:
9+
discover-tests:
10+
runs-on: ubuntu-latest
11+
outputs:
12+
test-files: ${{ steps.discover.outputs.test-files }}
13+
steps:
14+
- name: Check out repository
15+
uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
19+
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
20+
21+
- name: Discover test files
22+
id: discover
23+
run: |
24+
# Find all test files in e2e directory and create JSON array
25+
TEST_FILES=$(find tests/e2e -name "test_*.py" -type f | sort | jq -R -s -c 'split("\n")[:-1]')
26+
echo "test-files=$TEST_FILES" >> $GITHUB_OUTPUT
27+
echo "Discovered test files: $TEST_FILES"
28+
29+
e2e-tests:
30+
runs-on: ubuntu-latest
31+
environment: azure-prod
32+
needs: discover-tests
33+
strategy:
34+
matrix:
35+
test_file: ${{ fromJson(needs.discover-tests.outputs.test-files) }}
36+
mode: ["thrift", "sea"]
37+
env:
38+
DATABRICKS_SERVER_HOSTNAME: ${{ secrets.DATABRICKS_HOST }}
39+
DATABRICKS_HTTP_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }}
40+
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
41+
DATABRICKS_CATALOG: peco
42+
DATABRICKS_USER: ${{ secrets.TEST_PECO_SP_ID }}
43+
steps:
44+
- name: Check out repository
45+
uses: actions/checkout@v4
46+
with:
47+
fetch-depth: 0
48+
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
49+
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
50+
51+
- name: Set up python
52+
id: setup-python
53+
uses: actions/setup-python@v5
54+
with:
55+
python-version: "3.10"
56+
57+
- name: Install Poetry
58+
uses: snok/install-poetry@v1
59+
with:
60+
virtualenvs-create: true
61+
virtualenvs-in-project: true
62+
installer-parallel: true
63+
64+
- name: Load cached venv
65+
id: cached-poetry-dependencies
66+
uses: actions/cache@v4
67+
with:
68+
path: .venv
69+
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ github.event.repository.name }}-${{ hashFiles('**/poetry.lock') }}
70+
71+
- name: Install dependencies
72+
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
73+
run: poetry install --no-interaction --no-root
74+
75+
- name: Install library
76+
run: poetry install --no-interaction --all-extras
77+
78+
- name: Run ${{ matrix.mode }} tests for ${{ matrix.test_file }}
79+
run: |
80+
echo "Running ${{ matrix.mode }} tests for ${{ matrix.test_file }}"
81+
82+
# Set test filter based on mode
83+
if [ "${{ matrix.mode }}" = "sea" ]; then
84+
TEST_FILTER="-k 'extra_params1 or extra_params2'"
85+
else
86+
TEST_FILTER="-k 'extra_params0 or not extra_params'"
87+
fi
88+
89+
TEST_NAME=$(basename "${{ matrix.test_file }}" .py)
90+
COVERAGE_FILE="coverage-${TEST_NAME}-${{ matrix.mode }}.xml"
91+
92+
poetry run pytest "${{ matrix.test_file }}" $TEST_FILTER \
93+
--cov=src --cov-report=xml:$COVERAGE_FILE --cov-report=term \
94+
-v
95+
continue-on-error: true
96+
97+
- name: Upload coverage artifact
98+
uses: actions/upload-artifact@v4
99+
with:
100+
name: coverage-$(basename "${{ matrix.test_file }}" .py)-${{ matrix.mode }}
101+
path: |
102+
.coverage
103+
coverage-*-${{ matrix.mode }}.xml
104+
105+
merge-coverage:
106+
runs-on: ubuntu-latest
107+
needs: [e2e-tests]
108+
steps:
109+
- name: Check out repository
110+
uses: actions/checkout@v4
111+
112+
- name: Set up python
113+
id: setup-python
114+
uses: actions/setup-python@v5
115+
with:
116+
python-version: "3.10"
117+
118+
- name: Install Poetry
119+
uses: snok/install-poetry@v1
120+
with:
121+
virtualenvs-create: true
122+
virtualenvs-in-project: true
123+
installer-parallel: true
124+
125+
- name: Load cached venv
126+
id: cached-poetry-dependencies
127+
uses: actions/cache@v4
128+
with:
129+
path: .venv
130+
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ github.event.repository.name }}-${{ hashFiles('**/poetry.lock') }}
131+
132+
- name: Install dependencies
133+
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
134+
run: poetry install --no-interaction --no-root
135+
136+
- name: Install library
137+
run: poetry install --no-interaction --all-extras
138+
139+
- name: Download all coverage artifacts
140+
uses: actions/download-artifact@v4
141+
with:
142+
path: coverage_files
143+
144+
- name: Merge coverage
145+
run: |
146+
# Install xmllint if not available
147+
if ! command -v xmllint &> /dev/null; then
148+
sudo apt-get update && sudo apt-get install -y libxml2-utils
149+
fi
150+
151+
# Copy all coverage files with unique names
152+
for artifact_dir in coverage_files/*/; do
153+
if [ -f "$artifact_dir/.coverage" ]; then
154+
artifact_name=$(basename "$artifact_dir")
155+
cp "$artifact_dir/.coverage" ".coverage.$artifact_name"
156+
fi
157+
done
158+
159+
# Combine all coverage data
160+
poetry run coverage combine .coverage.*
161+
poetry run coverage xml
162+
poetry run coverage report
163+
164+
- name: Report coverage percentage
165+
run: |
166+
COVERAGE_FILE="coverage.xml"
167+
if [ ! -f "$COVERAGE_FILE" ]; then
168+
echo "ERROR: Coverage file not found at $COVERAGE_FILE"
169+
exit 1
170+
fi
171+
172+
COVERED=$(xmllint --xpath "string(//coverage/@lines-covered)" "$COVERAGE_FILE")
173+
TOTAL=$(xmllint --xpath "string(//coverage/@lines-valid)" "$COVERAGE_FILE")
174+
175+
# Calculate percentage using Python for precision
176+
PERCENTAGE=$(python3 -c "covered=${COVERED}; total=${TOTAL}; print(round((covered/total)*100, 2))")
177+
178+
echo "📊 Combined Coverage: ${PERCENTAGE}%"

tests/e2e/test_complex_types.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
from numpy import ndarray
33
from typing import Sequence
4+
from uuid import uuid4
45

56
from tests.e2e.test_driver import PySQLPytestTestCase
67

@@ -10,12 +11,15 @@ class TestComplexTypes(PySQLPytestTestCase):
1011
def table_fixture(self, connection_details):
1112
self.arguments = connection_details.copy()
1213
"""A pytest fixture that creates a table with a complex type, inserts a record, yields, and then drops the table"""
14+
15+
table_name = f"pysql_test_complex_types_table_{str(uuid4()).replace('-', '_')}"
16+
self.table_name = table_name
1317

1418
with self.cursor() as cursor:
1519
# Create the table
1620
cursor.execute(
17-
"""
18-
CREATE TABLE IF NOT EXISTS pysql_test_complex_types_table (
21+
f"""
22+
CREATE TABLE IF NOT EXISTS {table_name} (
1923
array_col ARRAY<STRING>,
2024
map_col MAP<STRING, INTEGER>,
2125
struct_col STRUCT<field1: STRING, field2: INTEGER>,
@@ -27,8 +31,8 @@ def table_fixture(self, connection_details):
2731
)
2832
# Insert a record
2933
cursor.execute(
30-
"""
31-
INSERT INTO pysql_test_complex_types_table
34+
f"""
35+
INSERT INTO {table_name}
3236
VALUES (
3337
ARRAY('a', 'b', 'c'),
3438
MAP('a', 1, 'b', 2, 'c', 3),
@@ -40,10 +44,10 @@ def table_fixture(self, connection_details):
4044
"""
4145
)
4246
try:
43-
yield
47+
yield table_name
4448
finally:
4549
# Clean up the table after the test
46-
cursor.execute("DELETE FROM pysql_test_complex_types_table")
50+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
4751

4852
@pytest.mark.parametrize(
4953
"field,expected_type",
@@ -61,7 +65,7 @@ def test_read_complex_types_as_arrow(self, field, expected_type, table_fixture):
6165

6266
with self.cursor() as cursor:
6367
result = cursor.execute(
64-
"SELECT * FROM pysql_test_complex_types_table LIMIT 1"
68+
f"SELECT * FROM {table_fixture} LIMIT 1"
6569
).fetchone()
6670

6771
assert isinstance(result[field], expected_type)
@@ -83,7 +87,7 @@ def test_read_complex_types_as_string(self, field, table_fixture):
8387
extra_params={"_use_arrow_native_complex_types": False}
8488
) as cursor:
8589
result = cursor.execute(
86-
"SELECT * FROM pysql_test_complex_types_table LIMIT 1"
90+
f"SELECT * FROM {table_fixture} LIMIT 1"
8791
).fetchone()
8892

8993
assert isinstance(result[field], str)

tests/e2e/test_parameterized_queries.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from enum import Enum
55
from typing import Dict, List, Type, Union
66
from unittest.mock import patch
7+
from uuid import uuid4
78

89
import time
910
import numpy as np
@@ -130,9 +131,13 @@ def inline_table(self, connection_details):
130131
Note that this fixture doesn't clean itself up. So the table will remain
131132
in the schema for use by subsequent test runs.
132133
"""
134+
135+
# Generate unique table name to avoid conflicts in parallel execution
136+
table_name = f"pysql_e2e_inline_param_test_table_{str(uuid4()).replace('-', '_')}"
137+
self.inline_table_name = table_name
133138

134-
query = """
135-
CREATE TABLE IF NOT EXISTS pysql_e2e_inline_param_test_table (
139+
query = f"""
140+
CREATE TABLE IF NOT EXISTS {table_name} (
136141
null_col INT,
137142
int_col INT,
138143
bigint_col BIGINT,
@@ -179,9 +184,10 @@ def _inline_roundtrip(self, params: dict, paramstyle: ParamStyle, target_column)
179184
:paramstyle:
180185
This is a no-op but is included to make the test-code easier to read.
181186
"""
182-
INSERT_QUERY = f"INSERT INTO pysql_e2e_inline_param_test_table (`{target_column}`) VALUES (%(p)s)"
183-
SELECT_QUERY = f"SELECT {target_column} `col` FROM pysql_e2e_inline_param_test_table LIMIT 1"
184-
DELETE_QUERY = "DELETE FROM pysql_e2e_inline_param_test_table"
187+
table_name = self.inline_table_name
188+
INSERT_QUERY = f"INSERT INTO {table_name} (`{target_column}`) VALUES (%(p)s)"
189+
SELECT_QUERY = f"SELECT {target_column} `col` FROM {table_name} LIMIT 1"
190+
DELETE_QUERY = f"DELETE FROM {table_name}"
185191

186192
with self.connection(extra_params={"use_inline_params": True}) as conn:
187193
with conn.cursor() as cursor:

tests/e2e/test_variant_types.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
from datetime import datetime
33
import json
4+
from uuid import uuid4
45

56
try:
67
import pyarrow
@@ -19,14 +20,14 @@ class TestVariantTypes(PySQLPytestTestCase):
1920
def variant_table(self, connection_details):
2021
"""A pytest fixture that creates a test table and cleans up after tests"""
2122
self.arguments = connection_details.copy()
22-
table_name = "pysql_test_variant_types_table"
23+
table_name = f"pysql_test_variant_types_table_{str(uuid4()).replace('-', '_')}"
2324

2425
with self.cursor() as cursor:
2526
try:
2627
# Create the table with variant columns
2728
cursor.execute(
28-
"""
29-
CREATE TABLE IF NOT EXISTS pysql_test_variant_types_table (
29+
f"""
30+
CREATE TABLE IF NOT EXISTS {table_name} (
3031
id INTEGER,
3132
variant_col VARIANT,
3233
regular_string_col STRING
@@ -36,8 +37,8 @@ def variant_table(self, connection_details):
3637

3738
# Insert test records with different variant values
3839
cursor.execute(
39-
"""
40-
INSERT INTO pysql_test_variant_types_table
40+
f"""
41+
INSERT INTO {table_name}
4142
VALUES
4243
(1, PARSE_JSON('{"name": "John", "age": 30}'), 'regular string'),
4344
(2, PARSE_JSON('[1, 2, 3, 4]'), 'another string')

0 commit comments

Comments
 (0)