diracx-testing 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diracx_testing-0.0.4/.gitignore +98 -0
- diracx_testing-0.0.4/PKG-INFO +20 -0
- diracx_testing-0.0.4/README.md +0 -0
- diracx_testing-0.0.4/pyproject.toml +43 -0
- diracx_testing-0.0.4/src/diracx/testing/__init__.py +45 -0
- diracx_testing-0.0.4/src/diracx/testing/__main__.py +67 -0
- diracx_testing-0.0.4/src/diracx/testing/client_generation.py +235 -0
- diracx_testing-0.0.4/src/diracx/testing/client_generation_pytest.py +37 -0
- diracx_testing-0.0.4/src/diracx/testing/dummy_osdb.py +30 -0
- diracx_testing-0.0.4/src/diracx/testing/entrypoints.py +69 -0
- diracx_testing-0.0.4/src/diracx/testing/mock_osdb.py +168 -0
- diracx_testing-0.0.4/src/diracx/testing/osdb.py +101 -0
- diracx_testing-0.0.4/src/diracx/testing/routers.py +36 -0
- diracx_testing-0.0.4/src/diracx/testing/scripts/collect_demo_coverage.sh +147 -0
- diracx_testing-0.0.4/src/diracx/testing/time.py +135 -0
- diracx_testing-0.0.4/src/diracx/testing/utils.py +711 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
*.py[cod]
|
|
3
|
+
|
|
4
|
+
# Conda
|
|
5
|
+
.conda
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
var
|
|
10
|
+
sdist
|
|
11
|
+
lib
|
|
12
|
+
lib64
|
|
13
|
+
|
|
14
|
+
# Packages
|
|
15
|
+
*.egg
|
|
16
|
+
*.egg-info
|
|
17
|
+
dist
|
|
18
|
+
build
|
|
19
|
+
eggs
|
|
20
|
+
parts
|
|
21
|
+
bin
|
|
22
|
+
develop-eggs
|
|
23
|
+
.installed.cfg
|
|
24
|
+
*.whl
|
|
25
|
+
|
|
26
|
+
# Translations
|
|
27
|
+
*.mo
|
|
28
|
+
|
|
29
|
+
# Mr Developer
|
|
30
|
+
.mr.developer.cfg
|
|
31
|
+
|
|
32
|
+
# Installer logs
|
|
33
|
+
pip-log.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
.coverage
|
|
37
|
+
.tox
|
|
38
|
+
.ruff_cache
|
|
39
|
+
.mypy_cache
|
|
40
|
+
|
|
41
|
+
# Eclipse
|
|
42
|
+
.project
|
|
43
|
+
.pydevproject
|
|
44
|
+
.pyproject
|
|
45
|
+
.settings
|
|
46
|
+
.metadata
|
|
47
|
+
|
|
48
|
+
#VSCode
|
|
49
|
+
.vscode
|
|
50
|
+
.env
|
|
51
|
+
|
|
52
|
+
# Vim
|
|
53
|
+
.*.sw[a-z]
|
|
54
|
+
*.un~
|
|
55
|
+
Session.vim
|
|
56
|
+
*~
|
|
57
|
+
|
|
58
|
+
# Intellij
|
|
59
|
+
.idea/
|
|
60
|
+
LHCbDIRAC.iml
|
|
61
|
+
|
|
62
|
+
# MaxOSX files
|
|
63
|
+
.DS_Store
|
|
64
|
+
|
|
65
|
+
# test stuff
|
|
66
|
+
.pytest_cache
|
|
67
|
+
.cache
|
|
68
|
+
__pycache__
|
|
69
|
+
pytests.xml
|
|
70
|
+
nosetests.xml
|
|
71
|
+
coverage.xml
|
|
72
|
+
Local_*
|
|
73
|
+
.hypothesis
|
|
74
|
+
*.gz
|
|
75
|
+
htmlcov/
|
|
76
|
+
*.xml.temp
|
|
77
|
+
|
|
78
|
+
#remove in case we want a specific LHCb one
|
|
79
|
+
.pylintrc
|
|
80
|
+
|
|
81
|
+
# docs
|
|
82
|
+
# this is auto generated
|
|
83
|
+
docs/source/CodeDocumentation/
|
|
84
|
+
docs/source/AdministratorGuide/Configuration/ExampleConfig.rst
|
|
85
|
+
docs/source/AdministratorGuide/CommandReference
|
|
86
|
+
docs/source/UserGuide/CommandReference
|
|
87
|
+
docs/_build
|
|
88
|
+
docs/source/_build
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# CMT junk
|
|
92
|
+
/*/*/x86_64-*-*-*/
|
|
93
|
+
*/*/cmt/Makefile
|
|
94
|
+
|
|
95
|
+
# pixi environments
|
|
96
|
+
.pixi
|
|
97
|
+
pixi.lock
|
|
98
|
+
*.egg-info
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diracx-testing
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: TODO
|
|
5
|
+
License: GPL-3.0-only
|
|
6
|
+
Classifier: Intended Audience :: Science/Research
|
|
7
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering
|
|
10
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: httpx
|
|
13
|
+
Requires-Dist: joserfc
|
|
14
|
+
Requires-Dist: pytest
|
|
15
|
+
Requires-Dist: pytest-asyncio==1.0.0
|
|
16
|
+
Requires-Dist: pytest-cov
|
|
17
|
+
Requires-Dist: pytest-github-actions-annotate-failures
|
|
18
|
+
Requires-Dist: pytest-xdist
|
|
19
|
+
Requires-Dist: uuid-utils
|
|
20
|
+
Provides-Extra: testing
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "diracx-testing"
|
|
3
|
+
description = "TODO"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
keywords = []
|
|
7
|
+
license = {text = "GPL-3.0-only"}
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Intended Audience :: Science/Research",
|
|
10
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Topic :: Scientific/Engineering",
|
|
13
|
+
"Topic :: System :: Distributed Computing",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pytest",
|
|
17
|
+
"pytest-asyncio==1.0.0",
|
|
18
|
+
"pytest-cov",
|
|
19
|
+
"pytest-xdist",
|
|
20
|
+
"httpx",
|
|
21
|
+
"joserfc",
|
|
22
|
+
"uuid-utils",
|
|
23
|
+
"pytest-github-actions-annotate-failures",
|
|
24
|
+
]
|
|
25
|
+
dynamic = ["version"]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
testing = [
|
|
29
|
+
"diracx-testing",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
34
|
+
build-backend = "hatchling.build"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.version]
|
|
37
|
+
source = "vcs"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.version.raw-options]
|
|
40
|
+
root = ".."
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/diracx"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .entrypoints import verify_entry_points
|
|
4
|
+
from .utils import (
|
|
5
|
+
ClientFactory,
|
|
6
|
+
aio_moto,
|
|
7
|
+
cli_env,
|
|
8
|
+
client_factory,
|
|
9
|
+
demo_dir,
|
|
10
|
+
demo_kubectl_env,
|
|
11
|
+
demo_urls,
|
|
12
|
+
do_device_flow_with_dex,
|
|
13
|
+
fernet_key,
|
|
14
|
+
private_key,
|
|
15
|
+
pytest_addoption,
|
|
16
|
+
session_client_factory,
|
|
17
|
+
test_auth_settings,
|
|
18
|
+
test_dev_settings,
|
|
19
|
+
test_login,
|
|
20
|
+
test_sandbox_settings,
|
|
21
|
+
with_cli_login,
|
|
22
|
+
with_config_repo,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = (
|
|
26
|
+
"verify_entry_points",
|
|
27
|
+
"ClientFactory",
|
|
28
|
+
"do_device_flow_with_dex",
|
|
29
|
+
"test_login",
|
|
30
|
+
"pytest_addoption",
|
|
31
|
+
"private_key",
|
|
32
|
+
"fernet_key",
|
|
33
|
+
"test_dev_settings",
|
|
34
|
+
"test_auth_settings",
|
|
35
|
+
"aio_moto",
|
|
36
|
+
"test_sandbox_settings",
|
|
37
|
+
"session_client_factory",
|
|
38
|
+
"client_factory",
|
|
39
|
+
"with_config_repo",
|
|
40
|
+
"demo_dir",
|
|
41
|
+
"demo_urls",
|
|
42
|
+
"demo_kubectl_env",
|
|
43
|
+
"cli_env",
|
|
44
|
+
"with_cli_login",
|
|
45
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
from importlib.resources import as_file, files
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import NoReturn
|
|
9
|
+
|
|
10
|
+
SCRIPTS_BASE = files("diracx.testing").joinpath("scripts")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_args() -> None:
|
|
14
|
+
"""Access to various utility scripts for testing DiracX and extensions."""
|
|
15
|
+
parser = argparse.ArgumentParser(description="Utility for testing DiracX.")
|
|
16
|
+
sp = parser.add_subparsers(dest="command", required=True)
|
|
17
|
+
|
|
18
|
+
# Create the 'coverage' argument group.
|
|
19
|
+
coverage_p = sp.add_parser("coverage", help="Coverage related commands")
|
|
20
|
+
coverage_sp = coverage_p.add_subparsers(dest="subcommand", required=True)
|
|
21
|
+
|
|
22
|
+
# Add the 'collect-demo' command under 'coverage'.
|
|
23
|
+
collect_demo_p = coverage_sp.add_parser(
|
|
24
|
+
"collect-demo", help="Collect demo coverage"
|
|
25
|
+
)
|
|
26
|
+
collect_demo_p.add_argument(
|
|
27
|
+
"--demo-dir",
|
|
28
|
+
required=True,
|
|
29
|
+
type=Path,
|
|
30
|
+
help="Path to the .demo dir of the diracx-charts repo.",
|
|
31
|
+
)
|
|
32
|
+
collect_demo_p.set_defaults(func=lambda a: coverage_collect_demo(a.demo_dir))
|
|
33
|
+
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
args.func(args)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def coverage_collect_demo(demo_dir: Path) -> NoReturn:
|
|
39
|
+
"""Collect coverage data from a running instance of the demo.
|
|
40
|
+
|
|
41
|
+
This script is primarily intended for use in CI/CD pipelines.
|
|
42
|
+
"""
|
|
43
|
+
from diracx.core.extensions import extensions_by_priority
|
|
44
|
+
|
|
45
|
+
client_extension_name = min(extensions_by_priority(), key=lambda x: x == "diracx")
|
|
46
|
+
|
|
47
|
+
with as_file(SCRIPTS_BASE / "collect_demo_coverage.sh") as script_file:
|
|
48
|
+
cmd = ["bash", str(script_file), "--demo-dir", str(demo_dir)]
|
|
49
|
+
if client_extension_name in {"diracx", "gubbins"}:
|
|
50
|
+
cmd += ["--diracx-repo", str(Path.cwd())]
|
|
51
|
+
if client_extension_name == "gubbins":
|
|
52
|
+
cmd += ["--extension-name", "gubbins"]
|
|
53
|
+
cmd += ["--extension-repo", str(Path.cwd() / "extensions" / "gubbins")]
|
|
54
|
+
else:
|
|
55
|
+
cmd += ["--extension-name", client_extension_name]
|
|
56
|
+
cmd += ["--extension-repo", str(Path.cwd())]
|
|
57
|
+
print("Running:", shlex.join(cmd))
|
|
58
|
+
proc = subprocess.run(cmd, check=False)
|
|
59
|
+
if proc.returncode == 0:
|
|
60
|
+
print("Process completed successfully.")
|
|
61
|
+
else:
|
|
62
|
+
print("Process failed with return code", proc.returncode)
|
|
63
|
+
raise SystemExit(proc.returncode)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
parse_args()
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"regenerate_client",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import ast
|
|
9
|
+
import importlib.util
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import shlex
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from importlib.metadata import Distribution
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from urllib.parse import urlparse
|
|
18
|
+
|
|
19
|
+
import git
|
|
20
|
+
|
|
21
|
+
AUTOREST_VERSION = "3.7.1"
|
|
22
|
+
AUTOREST_CORE_VERSION = "3.10.4"
|
|
23
|
+
AUTOREST_PUGINS = {
|
|
24
|
+
"@autorest/python": "6.34.2",
|
|
25
|
+
"@autorest/modelerfour": "4.23.7",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def extract_static_all(path):
|
|
30
|
+
tree = ast.parse(path.read_text(), filename=path)
|
|
31
|
+
|
|
32
|
+
name_to_module = {}
|
|
33
|
+
for node in tree.body:
|
|
34
|
+
if isinstance(node, ast.ImportFrom):
|
|
35
|
+
# Skip wildcard imports (like 'from ... import *')
|
|
36
|
+
for alias in node.names:
|
|
37
|
+
if alias.name == "*":
|
|
38
|
+
continue
|
|
39
|
+
# Use the alias if available, otherwise the original name.
|
|
40
|
+
local_name = alias.asname if alias.asname else alias.name
|
|
41
|
+
name_to_module[local_name] = node.module
|
|
42
|
+
|
|
43
|
+
# Look for the first top-level assignment to __all__
|
|
44
|
+
for node in tree.body:
|
|
45
|
+
if not isinstance(node, ast.Assign):
|
|
46
|
+
continue
|
|
47
|
+
for target in node.targets:
|
|
48
|
+
if not (isinstance(target, ast.Name) and target.id == "__all__"):
|
|
49
|
+
continue
|
|
50
|
+
return {
|
|
51
|
+
name: name_to_module.get(name) for name in ast.literal_eval(node.value)
|
|
52
|
+
}
|
|
53
|
+
raise NotImplementedError("__all__ not found")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def fixup_models_init(generated_dir, extension_name):
|
|
57
|
+
"""Workaround for https://github.com/python/mypy/issues/15300."""
|
|
58
|
+
models_init_path = generated_dir / "models" / "__init__.py"
|
|
59
|
+
# Enums cannot be extended, so we don't need to patch them
|
|
60
|
+
object_names = {
|
|
61
|
+
name
|
|
62
|
+
for name, module in extract_static_all(models_init_path).items()
|
|
63
|
+
if module != "_enums"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
patch_module = "diracx.client._generated.models._patch"
|
|
67
|
+
spec = importlib.util.find_spec(patch_module)
|
|
68
|
+
if spec is None or spec.origin is None:
|
|
69
|
+
raise ImportError(f"Cannot locate {patch_module} package")
|
|
70
|
+
missing = set(extract_static_all(Path(spec.origin))) - set(object_names)
|
|
71
|
+
missing_formatted = "\n".join(f' "{name}",' for name in missing)
|
|
72
|
+
|
|
73
|
+
with models_init_path.open("a") as fh:
|
|
74
|
+
fh.write(
|
|
75
|
+
"if TYPE_CHECKING:\n"
|
|
76
|
+
" __all__.extend(\n"
|
|
77
|
+
" [\n"
|
|
78
|
+
f"{missing_formatted}"
|
|
79
|
+
" ]\n"
|
|
80
|
+
" )\n"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _module_path(module_name: str) -> Path:
|
|
85
|
+
"""Get the path to a module.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
module_name: The name of the module.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The path to the module.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
spec = importlib.util.find_spec(module_name)
|
|
95
|
+
if spec is None:
|
|
96
|
+
raise ImportError("Cannot locate client_module package")
|
|
97
|
+
if spec.origin is None:
|
|
98
|
+
raise ImportError(
|
|
99
|
+
"Cannot locate client_module package, did you forget the __init__.py?"
|
|
100
|
+
)
|
|
101
|
+
return Path(spec.origin).parent
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def regenerate_client(openapi_spec: Path, client_module: str):
|
|
105
|
+
"""Regenerate the AutoREST client and run pre-commit checks on it.
|
|
106
|
+
|
|
107
|
+
This test is skipped by default, and can be enabled by passing
|
|
108
|
+
--regenerate-client to pytest. It is intended to be run manually
|
|
109
|
+
when the API changes.
|
|
110
|
+
|
|
111
|
+
The reason this is a test is that it is the only way to get access to the
|
|
112
|
+
test_client fixture, which is required to get the OpenAPI spec.
|
|
113
|
+
|
|
114
|
+
WARNING: This test will modify the source code of the client!
|
|
115
|
+
"""
|
|
116
|
+
client_root = _module_path(client_module)
|
|
117
|
+
|
|
118
|
+
# If the client is not an editable install, we need to find the source code
|
|
119
|
+
direct_url = Distribution.from_name(client_module).read_text("direct_url.json")
|
|
120
|
+
pkg_url_info = json.loads(direct_url)
|
|
121
|
+
if not pkg_url_info.get("dir_info", {}).get("editable", False):
|
|
122
|
+
src_url = pkg_url_info.get("url")
|
|
123
|
+
if src_url is None:
|
|
124
|
+
raise ValueError("No URL found in direct_url.json")
|
|
125
|
+
url_info = urlparse(src_url)
|
|
126
|
+
if url_info.scheme != "file":
|
|
127
|
+
raise ValueError("URL is not a file URL")
|
|
128
|
+
pkg_root = Path(url_info.path) / "src" / client_module.replace(".", "/")
|
|
129
|
+
if not pkg_root.is_dir():
|
|
130
|
+
raise NotImplementedError(
|
|
131
|
+
"Failed to resolve client sources", client_module, pkg_root
|
|
132
|
+
)
|
|
133
|
+
client_root = pkg_root
|
|
134
|
+
|
|
135
|
+
if not pkg_url_info.get("dir_info", {}).get("editable", False):
|
|
136
|
+
raise NotImplementedError(
|
|
137
|
+
"Client generation from non-editable install is not yet supported",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert client_root.is_dir()
|
|
141
|
+
assert client_root.name == "client"
|
|
142
|
+
assert (client_root / "_generated").is_dir()
|
|
143
|
+
extension_name = client_root.parent.name
|
|
144
|
+
|
|
145
|
+
repo_root = client_root.parents[3]
|
|
146
|
+
if extension_name == "gubbins" and not (repo_root / ".git").is_dir():
|
|
147
|
+
# Gubbins is special because it has a different structure due to being
|
|
148
|
+
# in a subdirectory of diracx
|
|
149
|
+
repo_root = repo_root.parents[1]
|
|
150
|
+
assert (repo_root / ".git").is_dir()
|
|
151
|
+
repo = git.Repo(repo_root)
|
|
152
|
+
generated_dir = client_root / "_generated"
|
|
153
|
+
if repo.is_dirty(path=generated_dir):
|
|
154
|
+
raise AssertionError(
|
|
155
|
+
"Client is currently in a modified state, skipping regeneration"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
cmd = ["autorest", f"--version={AUTOREST_CORE_VERSION}"]
|
|
159
|
+
for plugin, version in AUTOREST_PUGINS.items():
|
|
160
|
+
cmd.append(f"--use={plugin}@{version}")
|
|
161
|
+
cmd += [
|
|
162
|
+
"--python",
|
|
163
|
+
f"--input-file={openapi_spec}",
|
|
164
|
+
"--models-mode=msrest",
|
|
165
|
+
"--namespace=_generated",
|
|
166
|
+
f"--output-folder={client_root}",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
if "AUTOREST_HOME" in os.environ:
|
|
170
|
+
os.makedirs(os.environ["AUTOREST_HOME"], exist_ok=True)
|
|
171
|
+
|
|
172
|
+
# ruff: disable=S603
|
|
173
|
+
subprocess.run(cmd, check=True)
|
|
174
|
+
|
|
175
|
+
if extension_name != "diracx":
|
|
176
|
+
# For now we don't support extending the models in extensions. To make
|
|
177
|
+
# this clear manually remove the automatically generated _patch.py file
|
|
178
|
+
# and fixup the __init__.py file to use the diracx one.
|
|
179
|
+
(generated_dir / "models" / "_patch.py").unlink()
|
|
180
|
+
models_init_path = generated_dir / "models" / "__init__.py"
|
|
181
|
+
models_init = models_init_path.read_text()
|
|
182
|
+
assert models_init.count("from ._patch import") == 4
|
|
183
|
+
models_init = models_init.replace(
|
|
184
|
+
"from ._patch import",
|
|
185
|
+
"from diracx.client._generated.models._patch import",
|
|
186
|
+
)
|
|
187
|
+
models_init_path.write_text(models_init)
|
|
188
|
+
|
|
189
|
+
fixup_models_init(generated_dir, extension_name)
|
|
190
|
+
|
|
191
|
+
cmd = ["pre-commit", "run", "--all-files"]
|
|
192
|
+
print("Running pre-commit...")
|
|
193
|
+
subprocess.run(cmd, check=False, cwd=repo_root)
|
|
194
|
+
print("Re-running pre-commit...")
|
|
195
|
+
proc = subprocess.run(cmd, check=False, cwd=repo_root)
|
|
196
|
+
if proc.returncode == 0 and not repo.is_dirty(path=generated_dir):
|
|
197
|
+
return
|
|
198
|
+
# Show the diff to aid debugging
|
|
199
|
+
print(repo.git.diff(generated_dir))
|
|
200
|
+
if proc.returncode != 0:
|
|
201
|
+
raise AssertionError("Pre-commit failed")
|
|
202
|
+
raise AssertionError("Client was regenerated with changes")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main():
|
|
206
|
+
from diracx.core.extensions import extensions_by_priority
|
|
207
|
+
|
|
208
|
+
parser = argparse.ArgumentParser(
|
|
209
|
+
description="Regenerate the AutoREST client and run pre-commit checks on it."
|
|
210
|
+
)
|
|
211
|
+
parser.parse_args()
|
|
212
|
+
|
|
213
|
+
client_extension_name = min(extensions_by_priority(), key=lambda x: x == "diracx")
|
|
214
|
+
|
|
215
|
+
cmd = ["npm", "install", "-g", f"autorest@{AUTOREST_VERSION}"]
|
|
216
|
+
print("Ensuring autorest is installed by running", shlex.join(cmd))
|
|
217
|
+
subprocess.run(cmd, check=True)
|
|
218
|
+
|
|
219
|
+
cmd = [
|
|
220
|
+
sys.executable,
|
|
221
|
+
"-m",
|
|
222
|
+
"pytest",
|
|
223
|
+
"-pdiracx.testing",
|
|
224
|
+
"--import-mode=importlib",
|
|
225
|
+
"--no-cov",
|
|
226
|
+
f"--regenerate-client={client_extension_name}",
|
|
227
|
+
str(Path(__file__).parent / "client_generation_pytest.py"),
|
|
228
|
+
]
|
|
229
|
+
print("Generating client for", client_extension_name)
|
|
230
|
+
print("Running:", shlex.join(cmd))
|
|
231
|
+
subprocess.run(cmd, check=True)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from diracx.testing.client_generation import regenerate_client
|
|
6
|
+
|
|
7
|
+
pytestmark = pytest.mark.enabled_dependencies([])
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def test_client(client_factory):
|
|
12
|
+
with client_factory.unauthenticated() as client:
|
|
13
|
+
yield client
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_regenerate_client(test_client, tmp_path, request):
|
|
17
|
+
"""Regenerate the AutoREST client and run pre-commit checks on it.
|
|
18
|
+
|
|
19
|
+
This test is skipped by default, and can be enabled by passing
|
|
20
|
+
--regenerate-client to pytest. It is intended to be run manually
|
|
21
|
+
when the API changes.
|
|
22
|
+
|
|
23
|
+
The reason this is a test is that it is the only way to get access to the
|
|
24
|
+
test_client fixture, which is required to get the OpenAPI spec.
|
|
25
|
+
|
|
26
|
+
WARNING: This test will modify the source code of the client!
|
|
27
|
+
"""
|
|
28
|
+
client_name = request.config.getoption("--regenerate-client")
|
|
29
|
+
if client_name is None:
|
|
30
|
+
pytest.skip("--regenerate-client not specified")
|
|
31
|
+
|
|
32
|
+
r = test_client.get("/api/openapi.json")
|
|
33
|
+
r.raise_for_status()
|
|
34
|
+
openapi_spec = tmp_path / "openapi.json"
|
|
35
|
+
openapi_spec.write_text(r.text)
|
|
36
|
+
|
|
37
|
+
regenerate_client(openapi_spec, f"{client_name}.client")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
|
|
5
|
+
from diracx.db.os.utils import BaseOSDB
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DummyOSDB(BaseOSDB):
|
|
9
|
+
"""Example DiracX OpenSearch database class for testing.
|
|
10
|
+
|
|
11
|
+
A new random prefix is created each time the class is defined to ensure
|
|
12
|
+
test runs are independent of each other.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
fields = {
|
|
16
|
+
"DateField": {"type": "date"},
|
|
17
|
+
"IntField": {"type": "long"},
|
|
18
|
+
"KeywordField0": {"type": "keyword"},
|
|
19
|
+
"KeywordField1": {"type": "keyword"},
|
|
20
|
+
"KeywordField2": {"type": "keyword"},
|
|
21
|
+
"TextField": {"type": "text"},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def __init__(self, *args, **kwargs):
|
|
25
|
+
# Randomize the index prefix to ensure tests are independent
|
|
26
|
+
self.index_prefix = f"dummy_{secrets.token_hex(8)}"
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
|
|
29
|
+
def index_name(self, vo: str, doc_id: int) -> str:
|
|
30
|
+
return f"{self.index_prefix}-{doc_id // 1e6:.0f}m"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, distribution, entry_points
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_installed_entry_points():
|
|
11
|
+
"""Retrieve the installed entry points from the environment."""
|
|
12
|
+
entry_pts = entry_points()
|
|
13
|
+
diracx_eps = defaultdict(dict)
|
|
14
|
+
for group in entry_pts.groups:
|
|
15
|
+
if "diracx" in group:
|
|
16
|
+
for ep in entry_pts.select(group=group):
|
|
17
|
+
diracx_eps[group][ep.name] = ep.value
|
|
18
|
+
return dict(diracx_eps)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_entry_points_from_toml(toml_file):
|
|
22
|
+
"""Parse entry points from pyproject.toml."""
|
|
23
|
+
with open(toml_file, "rb") as f:
|
|
24
|
+
pyproject = tomllib.load(f)
|
|
25
|
+
package_name = pyproject["project"]["name"]
|
|
26
|
+
return package_name, pyproject.get("project", {}).get("entry-points", {})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_current_entry_points(repo_base) -> bool:
|
|
30
|
+
"""Create current entry points dict for comparison."""
|
|
31
|
+
current_eps = {}
|
|
32
|
+
for toml_file in repo_base.glob("diracx-*/pyproject.toml"):
|
|
33
|
+
package_name, entry_pts = get_entry_points_from_toml(f"{toml_file}")
|
|
34
|
+
# Ignore packages that are not installed
|
|
35
|
+
try:
|
|
36
|
+
distribution(package_name)
|
|
37
|
+
except PackageNotFoundError:
|
|
38
|
+
continue
|
|
39
|
+
# Merge the entry points
|
|
40
|
+
for key, value in entry_pts.items():
|
|
41
|
+
current_eps[key] = current_eps.get(key, {}) | value
|
|
42
|
+
return current_eps
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
46
|
+
def verify_entry_points(request, pytestconfig):
|
|
47
|
+
try:
|
|
48
|
+
ini_toml_name = tomllib.loads(pytestconfig.inipath.read_text())["project"][
|
|
49
|
+
"name"
|
|
50
|
+
]
|
|
51
|
+
except tomllib.TOMLDecodeError:
|
|
52
|
+
return
|
|
53
|
+
if ini_toml_name == "diracx":
|
|
54
|
+
repo_base = pytestconfig.inipath.parent
|
|
55
|
+
elif ini_toml_name.startswith("diracx-"):
|
|
56
|
+
repo_base = pytestconfig.inipath.parent.parent
|
|
57
|
+
else:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
installed_eps = set(get_installed_entry_points())
|
|
61
|
+
current_eps = set(get_current_entry_points(repo_base))
|
|
62
|
+
|
|
63
|
+
if installed_eps != current_eps:
|
|
64
|
+
pytest.fail(
|
|
65
|
+
"Project and installed entry-points are not consistent. "
|
|
66
|
+
"You should run `pip install -r requirements-dev.txt`"
|
|
67
|
+
f"{installed_eps-current_eps=}",
|
|
68
|
+
f"{current_eps-installed_eps=}",
|
|
69
|
+
)
|