Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a265a48
feat(ci): add GitHub workflow for proxy tests
eric-wang-1990 Jan 3, 2026
55dada6
fix(ci): use existing GitHub secrets for test configuration
eric-wang-1990 Jan 3, 2026
d4ed0f8
feat(ci): add manual trigger for proxy tests workflow
eric-wang-1990 Jan 3, 2026
1651d79
fix(ci): properly generate mitmproxy certificates in workflow
eric-wang-1990 Jan 3, 2026
9cc3ab5
fix(ci): certificates are auto-generated during pip install
eric-wang-1990 Jan 3, 2026
d4f7025
fix(ci): run mitmdump to generate certificates on first use
eric-wang-1990 Jan 3, 2026
2f2bdb5
fix(ci): use background process to generate certificates
eric-wang-1990 Jan 3, 2026
7bebf14
fix(ci): use correct test configuration format
eric-wang-1990 Jan 3, 2026
7991ba4
fix(ci): use correct env variable syntax and add config logging
eric-wang-1990 Jan 3, 2026
d757fed
fix(csharp): expand home directory path in mitmproxy confdir
eric-wang-1990 Jan 3, 2026
380f9e2
fix(csharp): add detailed logging for mitmdump startup errors
eric-wang-1990 Jan 3, 2026
0aa67bc
fix(csharp): add missing System.Linq using for LINQ methods
eric-wang-1990 Jan 3, 2026
db2e962
fix(ci): ensure mitmdump is fully terminated after cert generation
eric-wang-1990 Jan 3, 2026
08712c5
fix(test): support uri format in ProxyTestBase connection parameters
eric-wang-1990 Jan 3, 2026
4b7908d
fix(test): increase proxy startup timeout for macOS
eric-wang-1990 Jan 3, 2026
80958b5
fix(proxy): disable Flask reloader for faster startup
eric-wang-1990 Jan 3, 2026
3566ac3
fix(test-infra): use async sleep in proxy to avoid blocking event loop
eric-wang-1990 Jan 4, 2026
13bd678
chore(ci): run proxy tests on Ubuntu only
eric-wang-1990 Jan 4, 2026
805d511
chore(ci): align proxy tests triggers with e2e-tests
eric-wang-1990 Jan 4, 2026
cc930f9
feat(test-infra): add Thrift call verification to CloudFetch tests
eric-wang-1990 Jan 4, 2026
81ba3bf
feat: add dynamic baseline verification for CloudFetch tests
eric-wang-1990 Jan 5, 2026
93286c1
feat: refactor to use generic parameter helper and add CloudFetch tim…
eric-wang-1990 Jan 5, 2026
a836d6f
chore(test): add CloudFetch retry diagnostics and investigation
eric-wang-1990 Jan 5, 2026
c2e2344
chore: remove temporary debug logging from CloudFetchDownloader
eric-wang-1990 Jan 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions .github/workflows/proxy-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Copyright (c) 2025 ADBC Drivers Contributors
#
# 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.

name: Proxy Tests

on:
push:
branches:
- main
paths:
- '.github/workflows/proxy-tests.yml'
- 'test-infrastructure/proxy-server/**'
- 'test-infrastructure/tests/csharp/**'
- 'csharp/src/**'
pull_request:
# Only runs on PRs from the repo itself, not forks
paths:
- '.github/workflows/proxy-tests.yml'
- 'test-infrastructure/proxy-server/**'
- 'test-infrastructure/tests/csharp/**'
- 'csharp/src/**'
workflow_dispatch:
inputs:
run_all_tests:
description: 'Run all tests (ignores path filters)'
required: false
default: 'true'
type: boolean

concurrency:
group: ${{ github.repository }}-${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true

permissions:
contents: read

jobs:
proxy-tests:
name: "Proxy Tests"
runs-on: ubuntu-latest
if: ${{ !contains(github.event.pull_request.title, 'WIP') }}
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
submodules: recursive
persist-credentials: false

- name: Setup .NET 8.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r test-infrastructure/proxy-server/requirements.txt

- name: Generate mitmproxy certificates
run: |
# Start mitmdump in background to generate certificates
mitmdump &
MITM_PID=$!
# Wait for certificate generation (happens on first run)
sleep 5
# Stop mitmdump forcefully
kill -9 $MITM_PID || true
wait $MITM_PID 2>/dev/null || true
# Ensure no mitmdump processes are running
pkill -9 mitmdump || true
sleep 2
# Verify certificate was created
ls -la ~/.mitmproxy/
cat ~/.mitmproxy/mitmproxy-ca-cert.pem

- name: Trust mitmproxy certificate
run: |
sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy.crt
sudo update-ca-certificates

- name: Build test project
run: |
cd test-infrastructure/tests/csharp
dotnet build

- name: Create test configuration
env:
DATABRICKS_SERVER_HOSTNAME: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
DATABRICKS_HTTP_PATH: ${{ secrets.TEST_PECO_WAREHOUSE_HTTP_PATH }}
run: |
cat > databricks-test-config.json << EOF
{
"uri": "https://${{ env.DATABRICKS_SERVER_HOSTNAME }}${{ env.DATABRICKS_HTTP_PATH }}",
"auth_type": "token",
"token": "${{ env.DATABRICKS_TOKEN }}"
}
EOF
echo "Configuration file created:"
echo "URI: https://${{ env.DATABRICKS_SERVER_HOSTNAME }}${{ env.DATABRICKS_HTTP_PATH }}"
echo "Auth type: token"

- name: Run proxy infrastructure tests
env:
DATABRICKS_TEST_CONFIG_FILE: ${{ github.workspace }}/databricks-test-config.json
run: |
cd test-infrastructure/tests/csharp
dotnet test --filter "FullyQualifiedName~ProxyInfrastructureTests" \
--logger "console;verbosity=normal" \
--no-build

- name: Run CloudFetch proxy tests
env:
DATABRICKS_TEST_CONFIG_FILE: ${{ github.workspace }}/databricks-test-config.json
run: |
cd test-infrastructure/tests/csharp
dotnet test --filter "FullyQualifiedName~CloudFetchTests" \
--logger "console;verbosity=normal" \
--no-build
31 changes: 25 additions & 6 deletions test-infrastructure/proxy-server/mitmproxy_addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,20 +337,35 @@ def __init__(self):

# Start Flask control API in background thread
def run_api():
app.run(host='0.0.0.0', port=18081, threaded=True)
# Disable reloader and use production mode for faster startup
app.run(host='0.0.0.0', port=18081, threaded=True, use_reloader=False, debug=False)

api_thread = threading.Thread(target=run_api, daemon=True, name="ControlAPI")
api_thread.start()
ctx.log.info("Control API started on http://0.0.0.0:18081")

def request(self, flow: http.HTTPFlow) -> None:
async def request(self, flow: http.HTTPFlow) -> None:
"""
Intercept requests and inject failures based on enabled scenarios.
Called by mitmproxy for each HTTP request.
Made async to support non-blocking delays.
"""
# Detect request type
if self._is_cloudfetch_download(flow.request):
self._handle_cloudfetch_request(flow)
# Track cloud fetch download
with state_lock:
call_record = {
"timestamp": time.time(),
"type": "cloud_download",
"url": flow.request.pretty_url,
}
call_history.append(call_record)

# Enforce max history limit
if len(call_history) > MAX_CALL_HISTORY:
del call_history[:len(call_history) - MAX_CALL_HISTORY]

await self._handle_cloudfetch_request(flow)
elif self._is_thrift_request(flow.request):
self._handle_thrift_request(flow)

Expand Down Expand Up @@ -381,7 +396,7 @@ def _is_thrift_request(self, request: http.Request) -> bool:
# SEA requests use /api/2.0/sql/statements path
return "/sql/1.0/warehouses/" in request.path or "/sql/1.0/endpoints/" in request.path

def _handle_cloudfetch_request(self, flow: http.HTTPFlow) -> None:
async def _handle_cloudfetch_request(self, flow: http.HTTPFlow) -> None:
"""Handle CloudFetch requests and inject failures if scenario is enabled."""
with state_lock:
# Find first enabled CloudFetch scenario
Expand Down Expand Up @@ -425,10 +440,12 @@ def _handle_cloudfetch_request(self, flow: http.HTTPFlow) -> None:
self._disable_scenario(scenario_name)

elif action == "delay":
# Inject delay (simulates timeout) - now supports configurable duration
# Inject delay using asyncio.sleep() to avoid blocking the event loop
import asyncio
duration_seconds = scenario_config.get("duration_seconds", 5)
ctx.log.info(f"[INJECT] Delaying {duration_seconds}s for scenario: {scenario_name}")
time.sleep(duration_seconds)
await asyncio.sleep(duration_seconds)
ctx.log.info(f"[INJECT] Delay complete, auto-disabled scenario: {scenario_name}")
self._disable_scenario(scenario_name)
# Let request continue after delay

Expand All @@ -455,9 +472,11 @@ def _handle_thrift_request(self, flow: http.HTTPFlow) -> None:
with state_lock:
call_record = {
"timestamp": time.time(),
"type": "thrift",
"method": decoded.get("method", "unknown"),
"message_type": decoded.get("message_type", "unknown"),
"sequence_id": decoded.get("sequence_id", 0),
"fields": decoded.get("fields", {}),
}
call_history.append(call_record)

Expand Down
Loading
Loading