diff --git a/OneBranchPipelines/build-release-package-pipeline.yml b/OneBranchPipelines/build-release-package-pipeline.yml index b5b719ce..bef2b49b 100644 --- a/OneBranchPipelines/build-release-package-pipeline.yml +++ b/OneBranchPipelines/build-release-package-pipeline.yml @@ -390,8 +390,8 @@ extends: # 1. Installs Python (UsePythonVersion or NuGet for 3.14) # 2. Downloads ARM64 python.lib if cross-compiling # 3. Builds .pyd native extension - # 4. Runs pytest (x64 only, ARM64 can't execute on x64 host) - # 5. Builds wheel + # 4. Builds wheel + # 5. Installs and tests the wheel outside the checkout (x64 only) # 6. Publishes artifacts (wheels + PYD + PDB) # 7. ESRP malware scanning - ${{ each config in parameters.windowsConfigs }}: @@ -423,8 +423,8 @@ extends: # 2. Installs CMake and pybind11 # 3. Builds universal2 .so (ARCHFLAGS="-arch x86_64 -arch arm64") # 4. Starts SQL Server Docker container (via Colima) - # 5. Runs pytest - # 6. Builds wheel + # 5. Builds and retags the universal2 wheel + # 6. Installs and tests the wheel outside the checkout # 7. Publishes artifacts (wheels + .so) # 8. ESRP malware scanning - ${{ each config in parameters.macosConfigs }}: diff --git a/OneBranchPipelines/stages/build-macos-single-stage.yml b/OneBranchPipelines/stages/build-macos-single-stage.yml index 20962ce6..2e25a762 100644 --- a/OneBranchPipelines/stages/build-macos-single-stage.yml +++ b/OneBranchPipelines/stages/build-macos-single-stage.yml @@ -221,43 +221,42 @@ stages: displayName: 'Install external ODBC wheel (mssql-python-odbc)' # ========================= - # TESTING + # WHEEL BUILD AND TESTING # ========================= - # Run pytest against SQL Server container - # Tests use localhost:1433 connection (SA user with password from variable) - # -v = verbose output (show test names and results) - - script: | - python -m pytest -v - displayName: 'Run pytests' - env: - # Connection string uses localhost (SQL Server container exposed on port 1433) - # TrustServerCertificate=yes bypasses SSL cert validation (test env only) - DB_CONNECTION_STRING: 'Server=tcp:127.0.0.1,1433;Database=master;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes' - - # ========================= - # WHEEL BUILD - # ========================= - - # Build wheel package from setup.py - # Wheel filename: mssql_python-X.Y.Z-cp3XX-cp3XX-macosx_XX_X_universal2.whl - # bdist_wheel = build binary wheel distribution (contains pre-compiled .so) + # Build and retag the final wheel, then install and test it from a + # directory that cannot resolve mssql_python from the source checkout. - script: | + set -euo pipefail + cd "$(Build.SourcesDirectory)" python -m pip install --upgrade pip wheel setuptools python setup.py bdist_wheel - displayName: 'Build wheel package' - - # Retag wheel to universal2 — the .so binary is already universal2 (CMake cross-compiles - # both arm64 + x86_64), but bdist_wheel may tag as x86_64 when the Python interpreter - # lacks universal2 support (e.g. Python 3.10 from UsePythonVersion@0 is x86_64-only). - # Same approach used in mssql-tds Rust pipeline (build-python-wheels-template.yml). - - script: | for whl in dist/*.whl; do - echo "Retagging: $(basename $whl)" + echo "Retagging: $(basename "$whl")" wheel tags --platform-tag macosx_15_0_universal2 --remove "$whl" done - echo "Wheels after retag:" - ls -lh dist/ - displayName: 'Ensure universal2 platform tag' + + TEST_DIR="$(Agent.TempDirectory)/mssql-python-wheel-test" + WHEEL=$(find "$(Build.SourcesDirectory)/dist" -maxdepth 1 -name '*.whl' -print -quit) + if [ -z "$WHEEL" ]; then + echo "No wheel found in dist/" >&2 + exit 1 + fi + + python -m pip install --force-reinstall --no-deps "$WHEEL" + rm -rf "$TEST_DIR" + mkdir -p "$TEST_DIR" + cp -R "$(Build.SourcesDirectory)/tests" "$TEST_DIR/tests" + cp -R "$(Build.SourcesDirectory)/.github" "$TEST_DIR/.github" + cp "$(Build.SourcesDirectory)/pytest.ini" "$TEST_DIR/pytest.ini" + + cd "$TEST_DIR" + python -c "import os,mssql_python as m; p=os.path.realpath(os.path.dirname(m.__file__)); s=os.path.realpath(os.environ['BUILD_SOURCESDIRECTORY']); assert not p.startswith(s), 'mssql_python resolved inside checkout: '+p; print('mssql_python resolved from installed wheel:', p)" + python -m pytest tests -v + displayName: 'Build and test wheel' + env: + # Connection string uses localhost (SQL Server container exposed on port 1433) + # TrustServerCertificate=yes bypasses SSL cert validation (test env only) + DB_CONNECTION_STRING: 'Server=tcp:127.0.0.1,1433;Database=master;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes' # ========================= # ARTIFACT PUBLISHING diff --git a/OneBranchPipelines/stages/build-windows-single-stage.yml b/OneBranchPipelines/stages/build-windows-single-stage.yml index 6830afb0..db6129c0 100644 --- a/OneBranchPipelines/stages/build-windows-single-stage.yml +++ b/OneBranchPipelines/stages/build-windows-single-stage.yml @@ -343,18 +343,51 @@ stages: condition: ne(variables['targetArch'], 'arm64') # ========================= - # TESTING + # WHEEL BUILD AND TESTING # ========================= - # Run pytest to validate bindings (x64 only) - # ARM64 binaries cannot execute on x64 host, so tests are skipped + # Build the signed wheel for every architecture. Install and test x64 + # wheels from a directory that cannot resolve mssql_python from the + # source checkout. ARM64 wheels cannot execute on the x64 build host. - powershell: | - Write-Host "Running pytests to validate bindings" + $ErrorActionPreference = "Stop" + Set-Location "$(Build.SourcesDirectory)" + python -m pip install --upgrade pip wheel setuptools + if ($LASTEXITCODE -ne 0) { throw "Failed to install wheel build dependencies" } + $env:ARCHITECTURE = "$(targetArch)" + python setup.py bdist_wheel + if ($LASTEXITCODE -ne 0) { throw "Failed to build wheel" } + if ("$(targetArch)" -eq "arm64") { - Write-Host "Skipping pytests on Windows ARM64" - } else { - python -m pytest -v + Write-Host "Skipping wheel tests on Windows ARM64" + exit 0 + } + + $wheels = @(Get-ChildItem "$(Build.SourcesDirectory)\dist" -Filter *.whl -File) + if ($wheels.Count -ne 1) { + Write-Error "Expected one wheel in dist\, found $($wheels.Count)" + exit 1 } - displayName: 'Run pytests' + + python -m pip install --force-reinstall --no-deps "$($wheels[0].FullName)" + if ($LASTEXITCODE -ne 0) { throw "Failed to install built wheel" } + + $testDir = Join-Path "$(Agent.TempDirectory)" "mssql-python-wheel-test" + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $testDir + New-Item -ItemType Directory -Force -Path $testDir | Out-Null + Copy-Item "$(Build.SourcesDirectory)\tests" "$testDir\tests" -Recurse + Copy-Item "$(Build.SourcesDirectory)\.github" "$testDir\.github" -Recurse + Copy-Item "$(Build.SourcesDirectory)\pytest.ini" "$testDir\pytest.ini" + + Push-Location $testDir + try { + python -c "import os,mssql_python as m; p=os.path.realpath(os.path.dirname(m.__file__)); s=os.path.realpath(os.environ['BUILD_SOURCESDIRECTORY']); assert not p.startswith(s), 'mssql_python resolved inside checkout: '+p; print('mssql_python resolved from installed wheel:', p)" + if ($LASTEXITCODE -ne 0) { throw "Built wheel did not import outside the checkout" } + python -m pytest tests -v + if ($LASTEXITCODE -ne 0) { throw "Wheel tests failed" } + } finally { + Pop-Location + } + displayName: 'Build and test wheel' env: DB_CONNECTION_STRING: 'Server=(localdb)\MSSQLLocalDB;Database=TestDB;Uid=testuser;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes' @@ -391,14 +424,6 @@ stages: TargetFolder: '$(Build.SourcesDirectory)\apiScan\pdbs\windows\py$(shortPyVer)\$(targetArch)' displayName: 'Copy PDB to ApiScan directory' - # Build Python wheel package from source distribution - # ARCHITECTURE environment variable controls target platform tagging - - script: | - python -m pip install --upgrade pip wheel setuptools - set ARCHITECTURE=$(targetArch) - python setup.py bdist_wheel - displayName: 'Build wheel package' - # ========================= # SIGNED-WHEEL EVIDENCE (verification only) # ========================= diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index a3ddaaa1..62bfb806 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -114,6 +114,8 @@ def test_package_sources_compile_with_warnings_as_errors(): """Every package source must compile when warnings are promoted to errors.""" package_dir = Path(__file__).parents[1] / "mssql_python" + if not package_dir.is_dir(): + pytest.skip("requires a source checkout") for source in sorted(package_dir.glob("*.py")): with warnings.catch_warnings(): warnings.simplefilter("error") diff --git a/tests/test_025_odbc_package_required.py b/tests/test_025_odbc_package_required.py index b56f7845..b3fe8151 100644 --- a/tests/test_025_odbc_package_required.py +++ b/tests/test_025_odbc_package_required.py @@ -17,22 +17,26 @@ the actionable guidance surfaced by ``connect()``. """ -import glob import os import subprocess import sys +from importlib.util import find_spec from pathlib import Path import pytest _REPO_ROOT = Path(__file__).resolve().parent.parent -_MSSQL_DIR = _REPO_ROOT / "mssql_python" +_PACKAGE_SPEC = find_spec("mssql_python") +_MSSQL_DIR = ( + Path(next(iter(_PACKAGE_SPEC.submodule_search_locations))).resolve() + if _PACKAGE_SPEC and _PACKAGE_SPEC.submodule_search_locations + else _REPO_ROOT / "mssql_python" +) # These tests drive the *native* resolver, so they require the compiled -# extension. Skip cleanly in a source-only checkout where it was never built. -_EXT_BUILT = bool( - glob.glob(str(_MSSQL_DIR / "ddbc_bindings.*.pyd")) - or glob.glob(str(_MSSQL_DIR / "ddbc_bindings.*.so")) +# extension from either the checkout or the installed wheel. +_EXT_BUILT = any(_MSSQL_DIR.glob("ddbc_bindings.*.pyd")) or any( + _MSSQL_DIR.glob("ddbc_bindings.*.so") ) pytestmark = pytest.mark.skipif(not _EXT_BUILT, reason="native ddbc_bindings extension not built")