Skip to content

Commit 4b9b688

Browse files
authored
Chore: Get fast-test working on Windows (#4467)
1 parent 478892f commit 4b9b688

18 files changed

Lines changed: 118 additions & 54 deletions

File tree

.circleci/continue_config.yml

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,35 +99,35 @@ jobs:
9999
- store_test_results:
100100
path: test-results
101101

102-
style_and_cicd_tests_windows:
102+
cicd_tests_windows:
103103
executor:
104104
name: windows/default
105105
size: large
106-
environment:
107-
PYTEST_XDIST_AUTO_NUM_WORKERS: 4
108106
steps:
109107
- halt_unless_core
108+
- run:
109+
name: Enable symlinks in git config
110+
command: git config --global core.symlinks true
110111
- checkout
111112
- run:
112-
name: Install Python 3.9
113+
name: Install System Dependencies
113114
command: |
114-
choco install python --version=3.9 -y
115+
choco install make which -y
115116
refreshenv
116-
- run:
117-
name: Install make
118-
command: choco install make -y
119117
- run:
120118
name: Install SQLMesh dev dependencies
121-
command: make install-dev
122-
- run:
123-
name: Fix Git URL override
124-
command: git config --global --unset url."ssh://git@github.com".insteadOf
125-
# - run:
126-
# name: Run linters and code style checks
127-
# command: make py-style
119+
command: |
120+
python -m venv venv
121+
. ./venv/Scripts/activate
122+
python.exe -m pip install --upgrade pip
123+
make install-dev
128124
- run:
129-
name: Run cicd tests
130-
command: pytest -n auto -m "fast" --junitxml=test-results/junit-cicd.xml tests/lsp
125+
name: Run fast unit tests
126+
command: |
127+
. ./venv/Scripts/activate
128+
which python
129+
python --version
130+
make fast-test
131131
- store_test_results:
132132
path: test-results
133133

@@ -304,7 +304,7 @@ workflows:
304304
- "3.10"
305305
- "3.11"
306306
- "3.12"
307-
- style_and_cicd_tests_windows
307+
- cicd_tests_windows
308308
- engine_tests_docker:
309309
name: engine_<< matrix.engine >>
310310
matrix:

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ engine-up: engine-clickhouse-up engine-mssql-up engine-mysql-up engine-postgres-
6464
engine-down: engine-clickhouse-down engine-mssql-down engine-mysql-down engine-postgres-down engine-spark-down engine-trino-down
6565

6666
fast-test:
67-
pytest -n auto -m "fast and not cicdonly" && pytest -m "isolated"
67+
pytest -n auto -m "fast and not cicdonly" --junitxml=test-results/junit-fast-test.xml && pytest -m "isolated"
6868

6969
slow-test:
7070
pytest -n auto -m "(fast or slow) and not cicdonly" && pytest -m "isolated"

examples/sushi/linter/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# this makes "linter" a package so "linter.user" is a valid module for importlib.import_module() to load

sqlmesh/core/engine_adapter/athena.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from sqlmesh.core.engine_adapter.mixins import PandasNativeFetchDFSupportMixin, RowDiffMixin
99
from sqlmesh.core.engine_adapter.trino import TrinoEngineAdapter
1010
from sqlmesh.core.node import IntervalUnit
11-
import os
11+
import posixpath
1212
from sqlmesh.utils.errors import SQLMeshError
1313
from sqlmesh.core.engine_adapter.shared import (
1414
CatalogSupport,
@@ -403,11 +403,13 @@ def _table_location(
403403

404404
elif self.s3_warehouse_location:
405405
# If the user has set `s3_warehouse_location` in the connection config, the base URI is <s3_warehouse_location>/<catalog>/<schema>/
406-
base_uri = os.path.join(self.s3_warehouse_location, table.catalog or "", table.db or "")
406+
base_uri = posixpath.join(
407+
self.s3_warehouse_location, table.catalog or "", table.db or ""
408+
)
407409
else:
408410
return None
409411

410-
full_uri = validate_s3_uri(os.path.join(base_uri, table.text("this") or ""), base=True)
412+
full_uri = validate_s3_uri(posixpath.join(base_uri, table.text("this") or ""), base=True)
411413
return exp.LocationProperty(this=exp.Literal.string(full_uri))
412414

413415
def _find_matching_columns(

sqlmesh/core/model/cache.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from sqlmesh.core.model.definition import ExternalModel, Model, SqlModel, _Model
1616
from sqlmesh.utils.cache import FileCache
1717
from sqlmesh.utils.hashing import crc32
18+
from sqlmesh.utils.windows import IS_WINDOWS
1819

1920
from dataclasses import dataclass
2021

@@ -149,8 +150,10 @@ def _entry_name(model: SqlModel) -> str:
149150

150151

151152
def optimized_query_cache_pool(optimized_query_cache: OptimizedQueryCache) -> ProcessPoolExecutor:
153+
# fork doesnt work on Windows. ref: https://docs.python.org/3/library/multiprocessing.html#multiprocessing-start-methods
154+
context_type = "spawn" if IS_WINDOWS else "fork"
152155
return ProcessPoolExecutor(
153-
mp_context=mp.get_context("fork"),
156+
mp_context=mp.get_context(context_type),
154157
initializer=_init_optimized_query_cache,
155158
initargs=(optimized_query_cache,),
156159
max_workers=c.MAX_FORK_WORKERS,

sqlmesh/utils/cache.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from sqlmesh.utils import sanitize_name
1313
from sqlmesh.utils.date import to_datetime
1414
from sqlmesh.utils.errors import SQLMeshError
15+
from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path
1516

1617
logger = logging.getLogger(__name__)
1718

@@ -132,4 +133,8 @@ def clear(self) -> None:
132133

133134
def _cache_entry_path(self, name: str, entry_id: str = "") -> Path:
134135
entry_file_name = "__".join(p for p in (self._cache_version, name, entry_id) if p)
135-
return self._path / sanitize_name(entry_file_name)
136+
full_path = self._path / sanitize_name(entry_file_name)
137+
if IS_WINDOWS:
138+
# handle paths longer than 260 chars
139+
full_path = fix_windows_path(full_path)
140+
return full_path

sqlmesh/utils/windows.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import platform
2+
from pathlib import Path
3+
4+
IS_WINDOWS = platform.system() == "Windows"
5+
6+
7+
def fix_windows_path(path: Path) -> Path:
8+
"""
9+
Windows paths are limited to 260 characters: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
10+
Users can change this by updating a registry entry but we cant rely on that.
11+
We can quite commonly generate a cache file path that exceeds 260 characters which causes a FileNotFound error.
12+
If we prefix the path with "\\?\" then we can have paths up to 32,767 characters
13+
"""
14+
return Path("\\\\?\\" + str(path.absolute()))

tests/conftest.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from tempfile import TemporaryDirectory
1010
from unittest import mock
1111
from unittest.mock import PropertyMock
12+
import os
13+
import shutil
1214

1315
import duckdb
1416
import pandas as pd
@@ -39,6 +41,7 @@
3941
)
4042
from sqlmesh.utils import random_id
4143
from sqlmesh.utils.date import TimeLike, to_date
44+
from sqlmesh.utils.windows import IS_WINDOWS, fix_windows_path
4245
from sqlmesh.core.engine_adapter.shared import CatalogSupport
4346

4447
T = t.TypeVar("T", bound=EngineAdapter)
@@ -480,10 +483,27 @@ def _make_function(
480483
paths: t.Union[t.Union[str, Path], t.Collection[t.Union[str, Path]]],
481484
) -> t.List[Path]:
482485
paths = ensure_list(paths)
486+
all_paths = [Path(p) for p in paths]
483487
temp_dirs = []
484-
for path in paths:
488+
for path in all_paths:
485489
temp_dir = Path(tmp_path) / uuid.uuid4().hex
486-
copytree(path, temp_dir, symlinks=True, ignore=ignore)
490+
491+
if IS_WINDOWS:
492+
# shutil.copytree just doesnt work properly with the symlinks on Windows, regardless of the `symlinks` setting
493+
src = str(path.absolute())
494+
dst = str(temp_dir.absolute())
495+
os.system(f"robocopy {src} {dst} /E /COPYALL")
496+
497+
# after copying, delete the files that would have been ignored
498+
for root, dirs, _ in os.walk(temp_dir):
499+
for dir in dirs:
500+
full_dir = fix_windows_path(Path(root) / dir)
501+
for ignored in ignore(full_dir, [full_dir]):
502+
shutil.rmtree(ignored)
503+
504+
else:
505+
copytree(path, temp_dir, symlinks=True, ignore=ignore)
506+
487507
temp_dirs.append(temp_dir)
488508
return temp_dirs
489509

tests/core/test_format.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ def test_format_files(tmp_path: pathlib.Path, mocker: MockerFixture):
6161
assert all(
6262
c in context.console.log_status_update.mock_calls # type: ignore
6363
for c in [
64-
call(f"{tmp_path}/models/model_3.sql needs reformatting."),
65-
call(f"{tmp_path}/models/model_2.sql needs reformatting."),
66-
call(f"{tmp_path}/models/model_1.sql needs reformatting."),
67-
call(f"{tmp_path}/audits/audit_1.sql needs reformatting."),
64+
call(f"{tmp_path / 'models/model_3.sql'} needs reformatting."),
65+
call(f"{tmp_path / 'models/model_2.sql'} needs reformatting."),
66+
call(f"{tmp_path / 'models/model_1.sql'} needs reformatting."),
67+
call(f"{tmp_path / 'audits/audit_1.sql'} needs reformatting."),
6868
call("\n4 file(s) need reformatting."),
6969
]
7070
)

tests/core/test_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,7 +1238,7 @@ def test_seed_marker_substitution():
12381238
)
12391239

12401240
assert isinstance(model.kind, SeedKind)
1241-
assert model.kind.path == "examples/sushi/seeds/waiter_names.csv"
1241+
assert model.kind.path == str(Path("examples/sushi/seeds/waiter_names.csv"))
12421242
assert model.seed is not None
12431243
assert len(model.seed.content) > 0
12441244

0 commit comments

Comments
 (0)