Skip to content

Commit 60f4747

Browse files
committed
Allow running tests with geltest
`geltest` [1] is `edb test` extracted out of Gel server codebase and generalized to run any `unittest`-based suite. Notable changes here: 1. `_testbase` moved to `_internal._testbase` and broken down along major case types (base, models, third-party ORM); 2. Test cases that involve model generation now reuse `std` reflection for better `mypy` cache utilization. Speaking of the latter, `mypy` now uses sqlite cache, which is notably faster on my machines; 3. Test models are now reflected into (.gitignored) `tests/models` and relative imports in tests now work consistently (so instead of `from models.orm import Foo` write `from .models.orm import Foo` and have your editor LSP pick that up. This obviates the need to run `tools/gen_models.py` (which I will remove in a subsequent commit).
1 parent 08631ce commit 60f4747

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2597
-1746
lines changed

gel/_internal/_dirhash.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# SPDX-PackageName: gel-python
2+
# SPDX-License-Identifier: Apache-2.0
3+
# SPDX-FileCopyrightText: Copyright Gel Data Inc. and the contributors.
4+
5+
"""Directory hashing utilities.
6+
7+
This module provides functionality to compute hashes of directory trees based
8+
on file contents. It's primarily used for generating content-based hashes that
9+
can detect changes in directory structures and file contents across multiple
10+
directories with specific file extensions.
11+
12+
The hashing is deterministic and uses SHA-1 for performance, making it
13+
suitable for use cases where speed is more important than cryptographic
14+
security (e.g., build system cache invalidation).
15+
"""
16+
17+
from __future__ import annotations
18+
from typing import TYPE_CHECKING
19+
20+
import hashlib
21+
import os
22+
import pathlib
23+
24+
if TYPE_CHECKING:
25+
from collections.abc import Iterable
26+
27+
28+
def dirhash(
29+
dirs: Iterable[tuple[str, str]],
30+
*,
31+
extra_files: Iterable[os.PathLike[str]] | None = None,
32+
extra_data: bytes | None = None,
33+
) -> bytes:
34+
"""Compute a hash digest of directory contents and additional data.
35+
36+
This function recursively scans directories for files matching specific
37+
extensions and computes a SHA-1 hash of their combined contents. The
38+
hash includes file contents from matching files in the specified
39+
directories, optional extra files, and optional extra data.
40+
41+
Args:
42+
dirs: An iterable of (directory_path, file_extension) tuples.
43+
Each tuple specifies a directory to scan and the file extension
44+
to filter for (e.g., ('.py', '.txt')). Only files ending with
45+
the specified extension will be included in the hash.
46+
extra_files: Optional iterable of additional file paths to include
47+
in the hash computation. These files are included regardless
48+
of their extension or location.
49+
extra_data: Optional bytes to include in the hash computation.
50+
This can be used to incorporate additional context or metadata
51+
into the hash.
52+
53+
Returns:
54+
The SHA-1 digest as bytes representing the combined hash of all
55+
included file contents and extra data.
56+
57+
Raises:
58+
OSError: If any of the specified directories or files cannot be
59+
accessed or read.
60+
FileNotFoundError: If any of the specified paths don't exist.
61+
62+
Note:
63+
- Files are processed in sorted order by their resolved absolute
64+
paths to ensure deterministic hash computation.
65+
- The function uses SHA-1 for performance reasons, not cryptographic
66+
security.
67+
- Symbolic links are resolved to their targets before hashing.
68+
"""
69+
70+
def hash_dir(dirname: str, ext: str, paths: list[pathlib.Path]) -> None:
71+
with os.scandir(dirname) as it:
72+
for entry in it:
73+
if entry.is_file() and entry.name.endswith(ext):
74+
paths.append(pathlib.Path(entry.path).resolve(strict=True))
75+
elif entry.is_dir():
76+
hash_dir(entry.path, ext, paths)
77+
78+
paths: list[pathlib.Path] = []
79+
for dirname, ext in dirs:
80+
hash_dir(dirname, ext, paths)
81+
82+
if extra_files:
83+
paths.extend(
84+
pathlib.Path(extra_file).resolve(strict=True)
85+
for extra_file in extra_files
86+
)
87+
88+
h = hashlib.sha1() # noqa: S324 # sha1 is the fastest one
89+
90+
for path in sorted(paths):
91+
h.update(path.read_bytes())
92+
93+
if extra_data is not None:
94+
h.update(extra_data)
95+
96+
return h.digest()

gel/_internal/_import_extras.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# SPDX-PackageName: gel-python
2+
# SPDX-License-Identifier: Apache-2.0
3+
# SPDX-FileCopyrightText: Copyright Gel Data Inc. and the contributors.
4+
5+
from __future__ import annotations
6+
from typing import TYPE_CHECKING
7+
8+
import contextlib
9+
import importlib
10+
import os
11+
import sys
12+
13+
if TYPE_CHECKING:
14+
from collections.abc import Iterator
15+
16+
17+
def _set_sys_path(entries: list[str]) -> None:
18+
if sys.path is None:
19+
# Might happen at shutdown
20+
return
21+
sys.path[:] = entries
22+
importlib.invalidate_caches()
23+
24+
25+
@contextlib.contextmanager
26+
def sys_path(*paths: os.PathLike[str] | str) -> Iterator[None]:
27+
"""Modify sys.path by temporarily placing the given entry in front"""
28+
orig_sys_path = sys.path[:]
29+
entries = [os.fspath(path) for path in paths]
30+
paths_set = {*entries}
31+
_set_sys_path(
32+
[*entries, *(p for p in orig_sys_path if p not in paths_set)]
33+
)
34+
try:
35+
yield
36+
finally:
37+
_set_sys_path(orig_sys_path)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# SPDX-PackageName: gel-python
2+
# SPDX-License-Identifier: Apache-2.0
3+
# SPDX-FileCopyrightText: Copyright Gel Data Inc. and the contributors.
4+
5+
6+
from ._base import (
7+
AsyncQueryTestCase,
8+
BranchTestCase,
9+
SyncQueryTestCase,
10+
TestAsyncIOClient,
11+
TestCase,
12+
TestClient,
13+
UI,
14+
gen_lock_key,
15+
silence_asyncio_long_exec_warning,
16+
xfail,
17+
)
18+
19+
20+
__all__ = (
21+
"UI",
22+
"AsyncQueryTestCase",
23+
"BranchTestCase",
24+
"SyncQueryTestCase",
25+
"TestAsyncIOClient",
26+
"TestCase",
27+
"TestClient",
28+
"gen_lock_key",
29+
"silence_asyncio_long_exec_warning",
30+
"xfail",
31+
)

0 commit comments

Comments
 (0)