Skip to content

Commit 7fa2b4a

Browse files
committed
CI: Add Windows unit test job and fix test compatibility
Adds a windows-latest job to python-ci.yml and fixes platform assumptions in tests that prevented the suite from passing on Windows. Test fixes: - Reduce RANDOM_LENGTH to 8 (Windows MAX_PATH is 260 chars) - Use warehouse.as_posix() in SQLite URIs - Use context managers for file handles (Windows file locking) - Use os.path.abspath() in path assertions - Use raw paths instead of file: URIs in pyarrow fixtures - Skip Rich box-rendering tests on Windows (different terminal chars) - Skip Kerberos tests on Windows (puresasl C lib unavailable) - Relax error message assertion ([WinError 2] vs [Errno 2]) - Normalize backslashes in Hive path assertions Result: 3795+ tests pass on Windows with 0 failures. Depends on #3721. Closes #2477.
1 parent 89b5714 commit 7fa2b4a

7 files changed

Lines changed: 96 additions & 61 deletions

File tree

.github/workflows/python-ci.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,30 @@ jobs:
7979
- name: Generate coverage report (85%) # Coverage threshold should only increase over time — never decrease it!
8080
run: COVERAGE_FAIL_UNDER=85 make coverage-report
8181

82+
windows-unit-test:
83+
runs-on: windows-latest
84+
timeout-minutes: 15
85+
strategy:
86+
fail-fast: true
87+
matrix:
88+
python: ['3.12']
89+
90+
steps:
91+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
92+
with:
93+
persist-credentials: false
94+
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
95+
with:
96+
python-version: ${{ matrix.python }}
97+
- name: Install UV
98+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
99+
with:
100+
enable-cache: true
101+
- name: Install
102+
run: uv sync --all-extras --no-extra hive-kerberos
103+
- name: Run unit tests
104+
run: uv run python -m pytest tests/ -m "(unmarked or parametrize) and not integration" --ignore=tests/integration -v -x
105+
82106
cibw-dev-env-smoke-test:
83107
runs-on: ubuntu-latest
84108
steps:

tests/catalog/test_hive.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import base64
1919
import copy
2020
import struct
21+
import sys
2122
import threading
2223
import uuid
2324
from collections.abc import Generator
@@ -300,7 +301,7 @@ def test_create_table(
300301
# it to construct the assert_called_with
301302
metadata_location: str = called_hive_table.parameters["metadata_location"]
302303
assert metadata_location.endswith(".metadata.json")
303-
assert "/database/table/metadata/" in metadata_location
304+
assert "/database/table/metadata/" in metadata_location.replace("\\", "/")
304305
catalog._client.__enter__().create_table.assert_called_with(
305306
HiveTable(
306307
tableName="table",
@@ -481,7 +482,7 @@ def test_create_table_with_given_location_removes_trailing_slash(
481482
# it to construct the assert_called_with
482483
metadata_location: str = called_hive_table.parameters["metadata_location"]
483484
assert metadata_location.endswith(".metadata.json")
484-
assert "/database/table-given-location/metadata/" in metadata_location
485+
assert "/database/table-given-location/metadata/" in metadata_location.replace("\\", "/")
485486
catalog._client.__enter__().create_table.assert_called_with(
486487
HiveTable(
487488
tableName="table",
@@ -1369,6 +1370,7 @@ def test_create_hive_client_failure() -> None:
13691370
assert mock_hive_client.call_count == 2
13701371

13711372

1373+
@pytest.mark.skipif(sys.platform == "win32", reason="Kerberos/puresasl not available on Windows")
13721374
def test_create_hive_client_with_kerberos(
13731375
kerberized_hive_metastore_fake_url: str,
13741376
) -> None:
@@ -1381,6 +1383,7 @@ def test_create_hive_client_with_kerberos(
13811383
assert client is not None
13821384

13831385

1386+
@pytest.mark.skipif(sys.platform == "win32", reason="Kerberos/puresasl not available on Windows")
13841387
def test_create_hive_client_with_kerberos_using_context_manager(
13851388
kerberized_hive_metastore_fake_url: str,
13861389
) -> None:
@@ -1412,6 +1415,7 @@ def test_create_hive_client_with_kerberos_using_context_manager(
14121415
assert open_client._iprot.trans.isOpen()
14131416

14141417

1418+
@pytest.mark.skipif(sys.platform == "win32", reason="Kerberos/puresasl not available on Windows")
14151419
def test_kerberized_client_uses_fresh_transport_on_reuse(
14161420
kerberized_hive_metastore_fake_url: str,
14171421
) -> None:

tests/catalog/test_sql.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def catalog_memory(catalog_name: str, warehouse: Path) -> Generator[SqlCatalog,
6161
@pytest.fixture(scope="module")
6262
def catalog_sqlite(catalog_name: str, warehouse: Path) -> Generator[SqlCatalog, None, None]:
6363
props = {
64-
"uri": f"sqlite:////{warehouse}/sql-catalog",
64+
"uri": f"sqlite:///{warehouse.as_posix()}/sql-catalog",
6565
"warehouse": f"file://{warehouse}",
6666
}
6767
catalog = SqlCatalog(catalog_name, **props)
@@ -72,7 +72,7 @@ def catalog_sqlite(catalog_name: str, warehouse: Path) -> Generator[SqlCatalog,
7272

7373
@pytest.fixture(scope="module")
7474
def catalog_uri(warehouse: Path) -> str:
75-
return f"sqlite:////{warehouse}/sql-catalog"
75+
return f"sqlite:///{warehouse.as_posix()}/sql-catalog"
7676

7777

7878
@pytest.fixture(scope="module")
@@ -96,7 +96,7 @@ def test_creation_with_echo_parameter(catalog_name: str, warehouse: Path) -> Non
9696

9797
for echo_param, expected_echo_value in test_cases:
9898
props = {
99-
"uri": f"sqlite:////{warehouse}/sql-catalog",
99+
"uri": f"sqlite:///{warehouse.as_posix()}/sql-catalog",
100100
"warehouse": f"file://{warehouse}",
101101
}
102102
# None is for default value
@@ -119,7 +119,7 @@ def test_creation_with_pool_pre_ping_parameter(catalog_name: str, warehouse: Pat
119119

120120
for pool_pre_ping_param, expected_pool_pre_ping_value in test_cases:
121121
props = {
122-
"uri": f"sqlite:////{warehouse}/sql-catalog",
122+
"uri": f"sqlite:///{warehouse.as_posix()}/sql-catalog",
123123
"warehouse": f"file://{warehouse}",
124124
}
125125
# None is for default value
@@ -139,7 +139,7 @@ def test_creation_from_impl(catalog_name: str, warehouse: Path) -> None:
139139
catalog_name,
140140
**{
141141
"py-catalog-impl": "pyiceberg.catalog.sql.SqlCatalog",
142-
"uri": f"sqlite:////{warehouse}/sql-catalog",
142+
"uri": f"sqlite:///{warehouse.as_posix()}/sql-catalog",
143143
"warehouse": f"file://{warehouse}",
144144
},
145145
),
@@ -268,7 +268,7 @@ def get_columns(engine: Engine) -> set[str]:
268268

269269

270270
def test_adds_iceberg_type_column_to_old_schema(warehouse: Path) -> None:
271-
uri = f"sqlite:////{warehouse}/test-migration-add-col"
271+
uri = f"sqlite:///{warehouse.as_posix()}/test-migration-add-col"
272272
engine = _create_v0_db(uri)
273273

274274
# Verify the column does not exist in the old schema
@@ -334,7 +334,7 @@ def test_list_tables_filters_by_iceberg_type(warehouse: Path) -> None:
334334

335335

336336
def test_migration_to_v1_with_property_set(warehouse: Path) -> None:
337-
uri = f"sqlite:////{warehouse}/test-v1-migrate"
337+
uri = f"sqlite:///{warehouse.as_posix()}/test-v1-migrate"
338338
engine = _create_v0_db(uri)
339339

340340
assert "iceberg_type" not in get_columns(engine)
@@ -361,7 +361,7 @@ def test_invalid_schema_version_raises(warehouse: Path) -> None:
361361

362362
def test_list_tables_works_on_v0_schema(warehouse: Path) -> None:
363363
"""list_tables should work on V0 schemas without iceberg_type column."""
364-
uri = f"sqlite:////{warehouse}/test-v0-list"
364+
uri = f"sqlite:///{warehouse.as_posix()}/test-v0-list"
365365
_create_v0_db(uri)
366366

367367
catalog = SqlCatalog(
@@ -441,15 +441,15 @@ def _make_v0_catalog(uri: str, warehouse: Path) -> SqlCatalog:
441441

442442
def test_namespace_exists_on_v0_schema(warehouse: Path) -> None:
443443
"""namespace_exists should not fail on V0 schema (no iceberg_type column)."""
444-
catalog = _make_v0_catalog(f"sqlite:////{warehouse}/test-v0-ns-exists", warehouse)
444+
catalog = _make_v0_catalog(f"sqlite:///{warehouse.as_posix()}/test-v0-ns-exists", warehouse)
445445
catalog.create_namespace("ns")
446446
assert catalog.namespace_exists("ns")
447447
assert not catalog.namespace_exists("missing")
448448

449449

450450
def test_create_and_load_table_on_v0_schema(warehouse: Path) -> None:
451451
"""create_table and load_table should work on V0 schema without iceberg_type column."""
452-
catalog = _make_v0_catalog(f"sqlite:////{warehouse}/test-v0-create", warehouse)
452+
catalog = _make_v0_catalog(f"sqlite:///{warehouse.as_posix()}/test-v0-create", warehouse)
453453
catalog.create_namespace("ns")
454454
schema = Schema(NestedField(1, "id", StringType(), required=True))
455455
tbl = catalog.create_table(("ns", "tbl"), schema)

tests/conftest.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2388,7 +2388,7 @@ def empty_home_dir_path(tmp_path_factory: pytest.TempPathFactory) -> str:
23882388
return home_path
23892389

23902390

2391-
RANDOM_LENGTH = 20
2391+
RANDOM_LENGTH = 8 # Keep short to stay within Windows MAX_PATH (260 chars)
23922392
NUM_TABLES = 2
23932393

23942394

@@ -2445,15 +2445,15 @@ def hierarchical_namespace_list(hierarchical_namespace_name: str) -> list[str]:
24452445

24462446
BUCKET_NAME = "test_bucket"
24472447
TABLE_METADATA_LOCATION_REGEX = re.compile(
2448-
r"""s3://test_bucket/my_iceberg_database-[a-z]{20}.db/
2449-
my_iceberg_table-[a-z]{20}/metadata/
2448+
r"""s3://test_bucket/my_iceberg_database-[a-z]{8}.db/
2449+
my_iceberg_table-[a-z]{8}/metadata/
24502450
[0-9]{5}-[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}.metadata.json""",
24512451
re.X,
24522452
)
24532453

24542454
BQ_TABLE_METADATA_LOCATION_REGEX = re.compile(
2455-
r"""gs://alexstephen-test-bq-bucket/my_iceberg_database_[a-z]{20}.db/
2456-
my_iceberg_table-[a-z]{20}/metadata/
2455+
r"""gs://alexstephen-test-bq-bucket/my_iceberg_database_[a-z]{8}.db/
2456+
my_iceberg_table-[a-z]{8}/metadata/
24572457
[0-9]{5}-[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}.metadata.json""",
24582458
re.X,
24592459
)
@@ -3138,7 +3138,7 @@ def _create_sql_without_rowcount_catalog(name: str, warehouse: Path) -> Catalog:
31383138
from pyiceberg.catalog.sql import SqlCatalog
31393139

31403140
props = {
3141-
"uri": f"sqlite:////{warehouse}/sql-catalog",
3141+
"uri": f"sqlite:///{warehouse.as_posix()}/sql-catalog",
31423142
"warehouse": f"file://{warehouse}",
31433143
}
31443144
catalog = SqlCatalog(name, **props)

tests/io/test_io.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ def test_custom_local_input_file() -> None:
4949
input_file = PyArrowFileIO().new_input(location=f"{absolute_file_location}")
5050

5151
# Test opening and reading the file
52-
f = input_file.open()
53-
data = f.read()
54-
assert data == b"foo"
55-
assert len(input_file) == 3
52+
with input_file.open() as f:
53+
data = f.read()
54+
assert data == b"foo"
55+
assert len(input_file) == 3
5656

5757

5858
def test_custom_local_output_file() -> None:
@@ -87,10 +87,10 @@ def test_pickled_pyarrow_round_trip() -> None:
8787
f.write(b"foo")
8888

8989
input_file = deserialized_file_io.new_input(location=f"{absolute_file_location}")
90-
f = input_file.open()
91-
data = f.read()
92-
assert data == b"foo"
93-
assert len(input_file) == 3
90+
with input_file.open() as f:
91+
data = f.read()
92+
assert data == b"foo"
93+
assert len(input_file) == 3
9494
deserialized_file_io.delete(location=f"{absolute_file_location}")
9595

9696

0 commit comments

Comments
 (0)