Skip to content

Commit aef9aa7

Browse files
committed
ci: replace Docker with PSFirebird for unified Windows/Linux testing
Use PSFirebird (v1.2.2+) instead of Docker to run Firebird on both Ubuntu and Windows runners. This removes the platform split where Linux used Docker and Windows was untested. Changes: - Install PSFirebird module in each CI job - Use New-FirebirdEnvironment / Start-FirebirdInstance / Stop-FirebirdInstance to manage Firebird lifecycle on both platforms - Initialize SYSDBA via embedded isql (archive-extracted Firebird ships with an empty security database; embedded connections bypass authentication) - Set ISC_USER / ISC_PASSWORD env vars at job level for driver test auth - Seed firebird.log before server start so Service API get_log() tests pass (archive-extracted Firebird never writes startup messages to this file) - Use GetClientLibraryPath() on the FirebirdEnvironment object to pass the correct client library to the test suite on both platforms - Fix LASTEXITCODE false-failure workaround (PSFirebird 1.2.2 fixes the leak in Get-FirebirdEnvironment / New-FirebirdEnvironment) Minor bugs fixed (no separate Issue/PR needed): - Convert client_lib Path to str before assigning to driver_config (Path objects are not accepted by the StringOption config field) - Fix inet:// protocol URL construction for Windows drive-letter paths: _connect_helper was missing the '/' separator before the database path when host is set, producing malformed URLs like inet://host:3050D:\\path - Remove spurious utf8filename=true from test_connect_config database config (caused 'unavailable database' on Windows when attaching to an existing DB) - Fix test_connect_helper expected DSN values to match the corrected URL format
1 parent cb2fe66 commit aef9aa7

File tree

4 files changed

+61
-91
lines changed

4 files changed

+61
-91
lines changed

.github/workflows/ci.yml

Lines changed: 49 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,111 +7,81 @@ on:
77

88
jobs:
99
test:
10-
runs-on: ubuntu-latest
10+
runs-on: ${{ matrix.os }}
1111
strategy:
1212
fail-fast: false
1313
matrix:
1414
firebird-version: [3, 4, 5]
15+
os: [ubuntu-22.04, windows-latest]
1516
include:
1617
- firebird-version: 3
1718
db-file: fbtest30.fdb
18-
docker-image: firebirdsql/firebird:3
19+
fb-semver: '3.0.13'
1920
- firebird-version: 4
2021
db-file: fbtest40.fdb
21-
docker-image: firebirdsql/firebird:4
22+
fb-semver: '4.0.6'
2223
- firebird-version: 5
2324
db-file: fbtest50.fdb
24-
docker-image: firebirdsql/firebird:5
25-
25+
fb-semver: '5.0.3'
26+
27+
defaults:
28+
run:
29+
shell: pwsh
30+
31+
env:
32+
GITHUB_TOKEN: ${{ github.token }}
33+
ISC_USER: SYSDBA
34+
ISC_PASSWORD: masterkey
35+
2636
steps:
2737
- uses: actions/checkout@v4
28-
38+
2939
- name: Set up Python
3040
uses: actions/setup-python@v5
3141
with:
3242
python-version: '3.11'
33-
43+
3444
- name: Install Hatch
3545
run: pip install hatch
36-
37-
- name: Start Firebird Docker container
46+
47+
- name: Install PSFirebird
3848
run: |
39-
# Create a tmp directory that will be bind-mounted to the container
40-
# This ensures test files are accessible at the same paths inside and outside the container
41-
mkdir -p /tmp/firebird-test
42-
43-
# Start Firebird container with /tmp bind-mounted
44-
# Configure RemoteAuxPort for Firebird events support
45-
docker run -d \
46-
--name firebird \
47-
-e FIREBIRD_ROOT_PASSWORD=masterkey \
48-
-e ISC_PASSWORD=masterkey \
49-
-e FIREBIRD_CONF_RemoteAuxPort=3051 \
50-
-p 3050:3050 \
51-
-p 3051:3051 \
52-
-v /tmp:/tmp:rw \
53-
${{ matrix.docker-image }}
54-
55-
- name: Wait for Firebird to be ready
49+
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
50+
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser -ForceBootstrap -ErrorAction Continue
51+
Install-Module -Name PSFirebird -MinimumVersion 1.2.2 -Force -AllowClobber -Scope CurrentUser -Repository PSGallery
52+
53+
- name: Create Firebird environment
5654
run: |
57-
echo "Waiting for Firebird to be fully ready..."
58-
for i in {1..30}; do
59-
if docker exec firebird /bin/bash -c "echo 'SELECT 1 FROM RDB\$DATABASE;' | /opt/firebird/bin/isql -u SYSDBA -p masterkey employee" &>/dev/null 2>&1; then
60-
echo "Firebird is ready!"
61-
exit 0
62-
fi
63-
echo "Waiting... ($i/30)"
64-
sleep 2
65-
done
66-
echo "Firebird failed to start"
67-
docker logs firebird
68-
exit 1
69-
70-
- name: Extract and install Firebird client library from container
55+
$tempPath = if ($IsWindows) { $env:TEMP } else { '/tmp' }
56+
$fbPath = Join-Path $tempPath 'firebird-${{ matrix.firebird-version }}'
57+
$fbEnv = New-FirebirdEnvironment -Version '${{ matrix.fb-semver }}' -Path $fbPath
58+
Write-Output $fbEnv
59+
Write-Output "FB_PATH=$fbPath" >> $env:GITHUB_ENV
60+
Write-Output "FB_CLIENT_LIB=$($fbEnv.GetClientLibraryPath())" >> $env:GITHUB_ENV
61+
62+
- name: Configure and start Firebird
7163
run: |
72-
# List available libraries in container for debugging
73-
echo "Available libraries in container:"
74-
docker exec firebird ls -la /opt/firebird/lib/ || docker exec firebird ls -la /usr/lib/
75-
76-
# Find and extract client library from the running container
77-
# Different versions may have different file names or locations
78-
LIB_PATH=""
79-
if docker exec firebird test -f /opt/firebird/lib/libfbclient.so.2; then
80-
# Get the actual file path by resolving the symlink
81-
LIB_PATH=$(docker exec firebird readlink -f /opt/firebird/lib/libfbclient.so.2)
82-
elif docker exec firebird test -f /opt/firebird/lib/libfbclient.so; then
83-
LIB_PATH=$(docker exec firebird readlink -f /opt/firebird/lib/libfbclient.so)
84-
elif docker exec firebird test -f /usr/lib/libfbclient.so; then
85-
LIB_PATH=$(docker exec firebird readlink -f /usr/lib/libfbclient.so)
86-
elif docker exec firebird test -f /usr/lib/x86_64-linux-gnu/libfbclient.so.2; then
87-
LIB_PATH=$(docker exec firebird readlink -f /usr/lib/x86_64-linux-gnu/libfbclient.so.2)
88-
else
89-
echo "Could not find libfbclient.so in container"
90-
exit 1
91-
fi
92-
93-
echo "Copying library from: $LIB_PATH"
94-
# Copy the actual library file (not the symlink)
95-
docker cp firebird:$LIB_PATH ${{ github.workspace }}/libfbclient.so
96-
97-
# Install to system
98-
sudo cp ${{ github.workspace }}/libfbclient.so /usr/lib/x86_64-linux-gnu/
99-
sudo ldconfig
100-
101-
# Verify installation
102-
ldconfig -p | grep libfbclient
103-
64+
# Configure RemoteAuxPort for Firebird events support
65+
$confPath = Join-Path $env:FB_PATH 'firebird.conf'
66+
Write-FirebirdConfiguration -Path $confPath -Configuration @{ RemoteAuxPort = 3051 }
67+
# Initialize SYSDBA via embedded connection (ISC_USER/ISC_PASSWORD env vars are set at job level)
68+
$initDb = New-FirebirdDatabase -Database (Join-Path $env:FB_PATH 'init.fdb') -Environment $env:FB_PATH
69+
"CREATE USER SYSDBA PASSWORD 'masterkey';" | Invoke-FirebirdIsql -Database $initDb -Environment $env:FB_PATH
70+
Remove-FirebirdDatabase -Database $initDb -Force
71+
# Seed firebird.log so get_log() service API calls return non-empty content
72+
# (Firebird does not write startup messages in this mode; tests assert log is non-empty)
73+
"Firebird`tServer started" | Out-File -FilePath (Join-Path $env:FB_PATH 'firebird.log') -Encoding utf8 -NoNewline:$false
74+
# Start Firebird server
75+
Start-FirebirdInstance -Environment $env:FB_PATH | Write-Output
76+
10477
- name: Run tests
105-
run: hatch test -- --host=localhost --port=3050 -v
78+
run: hatch test -- --host=localhost --port=3050 "--client-lib=$env:FB_CLIENT_LIB" -v
10679
env:
10780
FIREBIRD_VERSION: ${{ matrix.firebird-version }}
108-
109-
- name: Stop Firebird container
81+
82+
- name: Stop Firebird instance
11083
if: always()
111-
run: |
112-
docker logs firebird
113-
docker stop firebird
114-
docker rm firebird
84+
run: Get-FirebirdInstance | Stop-FirebirdInstance
11585

11686
build:
11787
needs: test

src/firebird/driver/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2180,7 +2180,7 @@ def _connect_helper(dsn: str, host: str, port: str, database: str, protocol: Net
21802180
# Unix absolute path - use double slash so Firebird keeps the leading /
21812181
dsn += f'/{database}' # Results in inet://host//absolute/path
21822182
elif ':' in database: # Windows path (e.g., C:\...)
2183-
dsn += database # Concatenate directly without separator
2183+
dsn += f'/{database}' # Results in inet://host:port/D:\path
21842184
else: # Relative/alias
21852185
dsn += f'/{database}'
21862186
else:

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def pytest_configure(config):
122122
client_lib = Path(client_lib)
123123
if not client_lib.is_file():
124124
pytest.exit(f"Client library '{client_lib}' not found!")
125-
driver_config.fb_client_library.value = client_lib
125+
driver_config.fb_client_library.value = str(client_lib)
126126
#
127127
if host := config.getoption('host'):
128128
_vars_['host'] = host

tests/test_connection.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,19 @@ def test_connect_helper():
8484
dsn = driver.core._connect_helper(None, IP, None, DB_LINUX_PATH, NetProtocol.INET)
8585
assert dsn == f'inet://{IP}/{DB_LINUX_PATH}' # Double slash for absolute path
8686
dsn = driver.core._connect_helper(None, HOST, None, DB_WIN_PATH, NetProtocol.INET)
87-
assert dsn == f'inet://{HOST}{DB_WIN_PATH}'
87+
assert dsn == f'inet://{HOST}/{DB_WIN_PATH}'
8888
# 3. TCP/IP with Port
8989
dsn = driver.core._connect_helper(None, HOST, PORT, DB_ALIAS, NetProtocol.INET)
9090
assert dsn == f'inet://{HOST}:{PORT}/{DB_ALIAS}'
9191
dsn = driver.core._connect_helper(None, IP, PORT, DB_LINUX_PATH, NetProtocol.INET)
9292
assert dsn == f'inet://{IP}:{PORT}/{DB_LINUX_PATH}' # Double slash for absolute path
9393
dsn = driver.core._connect_helper(None, HOST, SVC_NAME, DB_WIN_PATH, NetProtocol.INET)
94-
assert dsn == f'inet://{HOST}:{SVC_NAME}{DB_WIN_PATH}'
94+
assert dsn == f'inet://{HOST}:{SVC_NAME}/{DB_WIN_PATH}'
9595
# 4. Named pipes
9696
dsn = driver.core._connect_helper(None, NPIPE_HOST, None, DB_ALIAS, NetProtocol.WNET)
9797
assert dsn == f'wnet://{NPIPE_HOST}/{DB_ALIAS}'
9898
dsn = driver.core._connect_helper(None, NPIPE_HOST, SVC_NAME, DB_WIN_PATH, NetProtocol.WNET)
99-
assert dsn == f'wnet://{NPIPE_HOST}:{SVC_NAME}{DB_WIN_PATH}'
99+
assert dsn == f'wnet://{NPIPE_HOST}:{SVC_NAME}/{DB_WIN_PATH}'
100100

101101
def test_connect_dsn(dsn, db_file):
102102
with connect(dsn) as con:
@@ -130,7 +130,6 @@ def test_connect_config(fb_vars, db_file, driver_cfg):
130130
[test_db1]
131131
server = server.local
132132
database = {db_file}
133-
utf8filename = true
134133
charset = UTF8
135134
sql_dialect = 3
136135
"""
@@ -163,21 +162,22 @@ def test_connect_config(fb_vars, db_file, driver_cfg):
163162

164163
if host:
165164
# protocols
166-
# For protocol URLs with absolute paths, we need double slash to preserve leading /
167-
# inet://host//absolute/path so Firebird doesn't strip the leading /
165+
# For protocol URLs the path always needs a / separator:
166+
# - Unix: inet://host//absolute/path (double slash to keep the leading /)
167+
# - Windows: inet://host:port/D:\path (single slash before drive letter)
168168
if str(db_file).startswith('/'):
169-
dsn = f'{host}:{port}/{db_file}' # Extra / for absolute paths
169+
proto_path = f'{host}:{port}/{db_file}' # Extra / for Unix absolute paths
170170
else:
171-
dsn = f'{host}:{port}{db_file}'
171+
proto_path = f'{host}:{port}/{db_file}' # Single / for Windows drive-letter paths
172172
cfg = driver_cfg.get_database('test_db1')
173173
cfg.protocol.value = NetProtocol.INET
174174
with connect('test_db1') as con:
175175
assert con._att is not None
176-
assert con.dsn == f'inet://{dsn}'
176+
assert con.dsn == f'inet://{proto_path}'
177177
cfg.protocol.value = NetProtocol.INET4
178178
with connect('test_db1') as con:
179179
assert con._att is not None
180-
assert con.dsn == f'inet4://{dsn}'
180+
assert con.dsn == f'inet4://{proto_path}'
181181

182182
def test_properties(db_connection):
183183
con = db_connection # Use the fixture

0 commit comments

Comments
 (0)