diracx-testing 0.0.1a44__py3-none-any.whl → 0.0.1a45__py3-none-any.whl
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/__init__.py +2 -4
- diracx/testing/__main__.py +67 -0
- diracx/testing/client_generation.py +92 -17
- diracx/testing/client_generation_pytest.py +37 -0
- diracx/testing/scripts/collect_demo_coverage.sh +147 -0
- diracx/testing/time.py +135 -0
- diracx/testing/utils.py +12 -28
- {diracx_testing-0.0.1a44.dist-info → diracx_testing-0.0.1a45.dist-info}/METADATA +3 -4
- diracx_testing-0.0.1a45.dist-info/RECORD +15 -0
- {diracx_testing-0.0.1a44.dist-info → diracx_testing-0.0.1a45.dist-info}/WHEEL +1 -2
- diracx_testing-0.0.1a44.dist-info/RECORD +0 -12
- diracx_testing-0.0.1a44.dist-info/top_level.txt +0 -1
diracx/testing/__init__.py
CHANGED
@@ -11,9 +11,8 @@ from .utils import (
|
|
11
11
|
demo_urls,
|
12
12
|
do_device_flow_with_dex,
|
13
13
|
fernet_key,
|
14
|
-
|
14
|
+
private_key,
|
15
15
|
pytest_addoption,
|
16
|
-
pytest_collection_modifyitems,
|
17
16
|
session_client_factory,
|
18
17
|
test_auth_settings,
|
19
18
|
test_dev_settings,
|
@@ -29,8 +28,7 @@ __all__ = (
|
|
29
28
|
"do_device_flow_with_dex",
|
30
29
|
"test_login",
|
31
30
|
"pytest_addoption",
|
32
|
-
"
|
33
|
-
"private_key_pem",
|
31
|
+
"private_key",
|
34
32
|
"fernet_key",
|
35
33
|
"test_dev_settings",
|
36
34
|
"test_auth_settings",
|
@@ -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()
|
@@ -2,17 +2,27 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
__all__ = [
|
4
4
|
"regenerate_client",
|
5
|
-
"AUTOREST_VERSION",
|
6
5
|
]
|
7
6
|
|
7
|
+
import argparse
|
8
8
|
import ast
|
9
9
|
import importlib.util
|
10
|
+
import json
|
11
|
+
import shlex
|
10
12
|
import subprocess
|
13
|
+
import sys
|
14
|
+
from importlib.metadata import Distribution
|
11
15
|
from pathlib import Path
|
16
|
+
from urllib.parse import urlparse
|
12
17
|
|
13
18
|
import git
|
14
19
|
|
15
|
-
AUTOREST_VERSION = "
|
20
|
+
AUTOREST_VERSION = "3.7.1"
|
21
|
+
AUTOREST_CORE_VERSION = "3.10.4"
|
22
|
+
AUTOREST_PUGINS = {
|
23
|
+
"@autorest/python": "6.34.2",
|
24
|
+
"@autorest/modelerfour": "4.23.7",
|
25
|
+
}
|
16
26
|
|
17
27
|
|
18
28
|
def extract_static_all(path):
|
@@ -70,6 +80,26 @@ def fixup_models_init(generated_dir, extension_name):
|
|
70
80
|
)
|
71
81
|
|
72
82
|
|
83
|
+
def _module_path(module_name: str) -> Path:
|
84
|
+
"""Get the path to a module.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
module_name: The name of the module.
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
The path to the module.
|
91
|
+
|
92
|
+
"""
|
93
|
+
spec = importlib.util.find_spec(module_name)
|
94
|
+
if spec is None:
|
95
|
+
raise ImportError("Cannot locate client_module package")
|
96
|
+
if spec.origin is None:
|
97
|
+
raise ImportError(
|
98
|
+
"Cannot locate client_module package, did you forget the __init__.py?"
|
99
|
+
)
|
100
|
+
return Path(spec.origin).parent
|
101
|
+
|
102
|
+
|
73
103
|
def regenerate_client(openapi_spec: Path, client_module: str):
|
74
104
|
"""Regenerate the AutoREST client and run pre-commit checks on it.
|
75
105
|
|
@@ -82,14 +112,29 @@ def regenerate_client(openapi_spec: Path, client_module: str):
|
|
82
112
|
|
83
113
|
WARNING: This test will modify the source code of the client!
|
84
114
|
"""
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
115
|
+
client_root = _module_path(client_module)
|
116
|
+
|
117
|
+
# If the client is not an editable install, we need to find the source code
|
118
|
+
direct_url = Distribution.from_name(client_module).read_text("direct_url.json")
|
119
|
+
pkg_url_info = json.loads(direct_url)
|
120
|
+
if not pkg_url_info.get("dir_info", {}).get("editable", False):
|
121
|
+
src_url = pkg_url_info.get("url")
|
122
|
+
if src_url is None:
|
123
|
+
raise ValueError("No URL found in direct_url.json")
|
124
|
+
url_info = urlparse(src_url)
|
125
|
+
if url_info.scheme != "file":
|
126
|
+
raise ValueError("URL is not a file URL")
|
127
|
+
pkg_root = Path(url_info.path) / "src" / client_module.replace(".", "/")
|
128
|
+
if not pkg_root.is_dir():
|
129
|
+
raise NotImplementedError(
|
130
|
+
"Failed to resolve client sources", client_module, pkg_root
|
131
|
+
)
|
132
|
+
client_root = pkg_root
|
133
|
+
|
134
|
+
if not pkg_url_info.get("dir_info", {}).get("editable", False):
|
135
|
+
raise NotImplementedError(
|
136
|
+
"Client generation from non-editable install is not yet supported",
|
91
137
|
)
|
92
|
-
client_root = Path(spec.origin).parent
|
93
138
|
|
94
139
|
assert client_root.is_dir()
|
95
140
|
assert client_root.name == "client"
|
@@ -97,7 +142,7 @@ def regenerate_client(openapi_spec: Path, client_module: str):
|
|
97
142
|
extension_name = client_root.parent.name
|
98
143
|
|
99
144
|
repo_root = client_root.parents[3]
|
100
|
-
if extension_name == "gubbins":
|
145
|
+
if extension_name == "gubbins" and not (repo_root / ".git").is_dir():
|
101
146
|
# Gubbins is special because it has a different structure due to being
|
102
147
|
# in a subdirectory of diracx
|
103
148
|
repo_root = repo_root.parents[1]
|
@@ -109,8 +154,10 @@ def regenerate_client(openapi_spec: Path, client_module: str):
|
|
109
154
|
"Client is currently in a modified state, skipping regeneration"
|
110
155
|
)
|
111
156
|
|
112
|
-
cmd = [
|
113
|
-
|
157
|
+
cmd = ["autorest", f"--version={AUTOREST_CORE_VERSION}"]
|
158
|
+
for plugin, version in AUTOREST_PUGINS.items():
|
159
|
+
cmd.append(f"--use={plugin}@{version}")
|
160
|
+
cmd += [
|
114
161
|
"--python",
|
115
162
|
f"--input-file={openapi_spec}",
|
116
163
|
"--models-mode=msrest",
|
@@ -118,11 +165,6 @@ def regenerate_client(openapi_spec: Path, client_module: str):
|
|
118
165
|
f"--output-folder={client_root}",
|
119
166
|
]
|
120
167
|
|
121
|
-
# This is required to be able to work offline
|
122
|
-
# TODO: if offline, find the version already installed
|
123
|
-
# and use it
|
124
|
-
# cmd += [f"--use=@autorest/python@{AUTOREST_VERSION}"]
|
125
|
-
|
126
168
|
# ruff: disable=S603
|
127
169
|
subprocess.run(cmd, check=True)
|
128
170
|
|
@@ -154,3 +196,36 @@ def regenerate_client(openapi_spec: Path, client_module: str):
|
|
154
196
|
if proc.returncode != 0:
|
155
197
|
raise AssertionError("Pre-commit failed")
|
156
198
|
raise AssertionError("Client was regenerated with changes")
|
199
|
+
|
200
|
+
|
201
|
+
def main():
|
202
|
+
from diracx.core.extensions import extensions_by_priority
|
203
|
+
|
204
|
+
parser = argparse.ArgumentParser(
|
205
|
+
description="Regenerate the AutoREST client and run pre-commit checks on it."
|
206
|
+
)
|
207
|
+
parser.parse_args()
|
208
|
+
|
209
|
+
client_extension_name = min(extensions_by_priority(), key=lambda x: x == "diracx")
|
210
|
+
|
211
|
+
cmd = ["npm", "install", "-g", f"autorest@{AUTOREST_VERSION}"]
|
212
|
+
print("Ensuring autorest is installed by running", shlex.join(cmd))
|
213
|
+
subprocess.run(cmd, check=True)
|
214
|
+
|
215
|
+
cmd = [
|
216
|
+
sys.executable,
|
217
|
+
"-m",
|
218
|
+
"pytest",
|
219
|
+
"-pdiracx.testing",
|
220
|
+
"--import-mode=importlib",
|
221
|
+
"--no-cov",
|
222
|
+
f"--regenerate-client={client_extension_name}",
|
223
|
+
str(Path(__file__).parent / "client_generation_pytest.py"),
|
224
|
+
]
|
225
|
+
print("Generating client for", client_extension_name)
|
226
|
+
print("Running:", shlex.join(cmd))
|
227
|
+
subprocess.run(cmd, check=True)
|
228
|
+
|
229
|
+
|
230
|
+
if __name__ == "__main__":
|
231
|
+
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,147 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
# Enable the unofficial bash strict mode
|
3
|
+
# See: http://redsymbol.net/articles/unofficial-bash-strict-mode/
|
4
|
+
set -euo pipefail
|
5
|
+
IFS=$'\n\t'
|
6
|
+
|
7
|
+
usage() {
|
8
|
+
echo "Usage: $(basename "$0") [OPTIONS]"
|
9
|
+
echo "Collect coverage data from a demo run."
|
10
|
+
echo
|
11
|
+
echo "Options:"
|
12
|
+
echo " --demo-dir <path> Path to the demo directory."
|
13
|
+
echo " --diracx-repo <path> Path to the DiracX repository."
|
14
|
+
echo " --extension-name <name> Name of the extension to collect coverage for."
|
15
|
+
echo " --extension-repo <path> Path to the extension repository."
|
16
|
+
echo " -h, --help Show this help message and exit."
|
17
|
+
echo
|
18
|
+
exit 1
|
19
|
+
}
|
20
|
+
|
21
|
+
# Parse command line arguments
|
22
|
+
demo_dir=""
|
23
|
+
diracx_repo=""
|
24
|
+
extension_name=""
|
25
|
+
extension_repo=""
|
26
|
+
|
27
|
+
while [[ "$#" -gt 0 ]]; do
|
28
|
+
case "$1" in
|
29
|
+
--demo-dir)
|
30
|
+
if [[ -n "${2:-}" ]]; then
|
31
|
+
demo_dir="$2"
|
32
|
+
shift 2
|
33
|
+
else
|
34
|
+
echo "Error: --demo-dir requires a value."
|
35
|
+
usage
|
36
|
+
fi
|
37
|
+
;;
|
38
|
+
--diracx-repo)
|
39
|
+
if [[ -n "${2:-}" ]]; then
|
40
|
+
diracx_repo=$(realpath "$2")
|
41
|
+
shift 2
|
42
|
+
else
|
43
|
+
echo "Error: --diracx-repo requires a value."
|
44
|
+
usage
|
45
|
+
fi
|
46
|
+
;;
|
47
|
+
--extension-name)
|
48
|
+
if [[ -n "${2:-}" ]]; then
|
49
|
+
extension_name="$2"
|
50
|
+
shift 2
|
51
|
+
else
|
52
|
+
echo "Error: --extension-name requires a value."
|
53
|
+
usage
|
54
|
+
fi
|
55
|
+
;;
|
56
|
+
--extension-repo)
|
57
|
+
if [[ -n "${2:-}" ]]; then
|
58
|
+
extension_repo=$(realpath "$2")
|
59
|
+
shift 2
|
60
|
+
else
|
61
|
+
echo "Error: --extension-repo requires a value."
|
62
|
+
usage
|
63
|
+
fi
|
64
|
+
;;
|
65
|
+
*)
|
66
|
+
echo "Unknown parameter passed: $1"
|
67
|
+
usage
|
68
|
+
;;
|
69
|
+
esac
|
70
|
+
done
|
71
|
+
|
72
|
+
# Argument validation
|
73
|
+
if [[ -z "$demo_dir" ]]; then
|
74
|
+
echo "Error: --demo-dir is required."
|
75
|
+
usage
|
76
|
+
fi
|
77
|
+
if [[ -z "${diracx_repo:-}" && -z "${extension_repo:-}" ]]; then
|
78
|
+
echo "Error: At least one of --diracx-repo or --extension-repo is required."
|
79
|
+
usage
|
80
|
+
fi
|
81
|
+
if [[ -n "${extension_name:-}" && -z "${extension_repo:-}" ]]; then
|
82
|
+
echo "Error: --extension-name is required when --extension-repo is provided."
|
83
|
+
usage
|
84
|
+
fi
|
85
|
+
if [[ ! -f ".coveragerc" ]]; then
|
86
|
+
echo "Error: Expected .coveragerc file not found in the current directory."
|
87
|
+
exit 1
|
88
|
+
fi
|
89
|
+
|
90
|
+
# Set up the environment
|
91
|
+
export KUBECONFIG=${demo_dir}/kube.conf
|
92
|
+
export PATH=${demo_dir}:$PATH
|
93
|
+
|
94
|
+
if ! kubectl cluster-info > /dev/null 2>&1; then
|
95
|
+
echo "Error: The demo does not appear to be running."
|
96
|
+
exit 1
|
97
|
+
fi
|
98
|
+
|
99
|
+
# Shutdown the pods so we collect coverage data
|
100
|
+
pods=$(kubectl get pods -o json | jq -r '.items[] | .metadata.name')
|
101
|
+
for pod_name in $(echo "${pods}" | grep -vE '(dex|minio|mysql|rabbitmq|opensearch)'); do
|
102
|
+
kubectl delete pod/"${pod_name}"
|
103
|
+
done
|
104
|
+
|
105
|
+
# Combine the coverage data from the demo and make an XML report
|
106
|
+
coverage_data=$(mktemp)
|
107
|
+
echo "Changing ownership of coverage data to $(id -u)"
|
108
|
+
sudo chown -R "$(id -u)" "${demo_dir}"/coverage-reports/
|
109
|
+
coverage combine --keep --data-file "${coverage_data}" "${demo_dir}"/coverage-reports/*
|
110
|
+
|
111
|
+
# coverage can't handle having multiple src directories, so we need to make a fake one with symlinks
|
112
|
+
fake_module=$(mktemp -d)
|
113
|
+
|
114
|
+
# Ensure we clean up the fake module and restore the original .coveragerc
|
115
|
+
cleanup() {
|
116
|
+
rm -rf "${fake_module}"
|
117
|
+
if [[ -f ".coveragerc.bak" ]]; then
|
118
|
+
mv ".coveragerc.bak" .coveragerc
|
119
|
+
fi
|
120
|
+
}
|
121
|
+
trap cleanup EXIT
|
122
|
+
|
123
|
+
# Symlink DiracX into the fake module (if provided)
|
124
|
+
if [[ -n "${diracx_repo:-}" ]]; then
|
125
|
+
mkdir -p "${fake_module}/src/diracx"
|
126
|
+
for fn in "${diracx_repo}"/*/src/diracx/*; do
|
127
|
+
ln -sf "${fn}" "${fake_module}/src/diracx/$(basename "${fn}")"
|
128
|
+
done
|
129
|
+
fi
|
130
|
+
|
131
|
+
# Symlink the extension into the fake module (if provided)
|
132
|
+
if [[ -n "${extension_repo:-}" ]]; then
|
133
|
+
mkdir -p "${fake_module}/src/${extension_name}/extensions"
|
134
|
+
for fn in "${extension_repo}"/*/src/"${extension_name}"/*; do
|
135
|
+
ln -sf "${fn}" "${fake_module}/src/${extension_name}/$(basename "${fn}")"
|
136
|
+
done
|
137
|
+
fi
|
138
|
+
|
139
|
+
# Edit the .coveragerc file to point to the fake module
|
140
|
+
sed -i.bak "s@source =@source =\n ${fake_module}/src@g" .coveragerc
|
141
|
+
if diff -q .coveragerc .coveragerc.bak > /dev/null; then
|
142
|
+
echo "Error: .coveragerc was not modified."
|
143
|
+
exit 1
|
144
|
+
fi
|
145
|
+
|
146
|
+
# Make the actual coverage report
|
147
|
+
coverage xml -o coverage-demo.xml --data-file "${coverage_data}"
|
diracx/testing/time.py
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
"""Helper functions for mocking time functions.
|
2
|
+
|
3
|
+
The main functionality in this module is to mock SQLite's date and time
|
4
|
+
functions. This is useful for testing purposes, especially when using
|
5
|
+
freezegun to freeze time.
|
6
|
+
|
7
|
+
Example usage to mock SQLite time functions in SQLAlchemy for connections in
|
8
|
+
a given engine:
|
9
|
+
|
10
|
+
import sqlalchemy
|
11
|
+
|
12
|
+
from diracx.testing.time import mock_sqlite_time
|
13
|
+
|
14
|
+
sqlalchemy.event.listen(engine.sync_engine, "connect", mock_sqlite_time)
|
15
|
+
|
16
|
+
This functionality is tested in the `diracx-db` tests, specifically in the
|
17
|
+
`test_freeze_time.py` file.
|
18
|
+
"""
|
19
|
+
|
20
|
+
from __future__ import annotations
|
21
|
+
|
22
|
+
__all__ = [
|
23
|
+
"mock_sqlite_time",
|
24
|
+
"julian_date",
|
25
|
+
]
|
26
|
+
|
27
|
+
import re
|
28
|
+
from datetime import UTC, datetime
|
29
|
+
|
30
|
+
RE_SQLITE_TIME = re.compile(r"(\d{4})-(\d{2})-(\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?")
|
31
|
+
|
32
|
+
|
33
|
+
def mock_sqlite_time(dbapi_connection, connection_record):
|
34
|
+
"""Override SQLite’s date and time functions.
|
35
|
+
|
36
|
+
See: https://sqlite.org/lang_datefunc.html#tmval
|
37
|
+
"""
|
38
|
+
|
39
|
+
# 1. date(time-value, modifier, modifier, ...)
|
40
|
+
def date_mock(*args):
|
41
|
+
"""Mock for DATE() function."""
|
42
|
+
raise NotImplementedError(args)
|
43
|
+
|
44
|
+
# 2. time(time-value, modifier, modifier, ...)
|
45
|
+
def time_mock(*args):
|
46
|
+
"""Mock for TIME() function."""
|
47
|
+
raise NotImplementedError(args)
|
48
|
+
|
49
|
+
# 3. datetime(time-value, modifier, modifier, ...)
|
50
|
+
def datetime_mock(*args):
|
51
|
+
"""Mock for DATETIME() function."""
|
52
|
+
if len(args) == 0:
|
53
|
+
args = ("now",)
|
54
|
+
if len(args) > 1:
|
55
|
+
raise NotImplementedError(args)
|
56
|
+
|
57
|
+
time_value = args[0]
|
58
|
+
if time_value == "now":
|
59
|
+
return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
|
60
|
+
|
61
|
+
raise NotImplementedError(args)
|
62
|
+
|
63
|
+
# 7. julianday(time-value, modifier, modifier, ...)
|
64
|
+
def julianday_mock(*args):
|
65
|
+
"""Mock for JULIANDAY() function."""
|
66
|
+
if len(args) == 0:
|
67
|
+
args = ("now",)
|
68
|
+
if len(args) > 1:
|
69
|
+
raise NotImplementedError(args)
|
70
|
+
|
71
|
+
if args[0] == "now":
|
72
|
+
return julian_date(datetime.now(UTC))
|
73
|
+
if match := RE_SQLITE_TIME.fullmatch(args[0]):
|
74
|
+
parts = [0 if x is None else int(x) for x in match.groups()]
|
75
|
+
return julian_date_from_parts(*parts)
|
76
|
+
if match := re.fullmatch(r"(\d{4})(?:\.(\d{2}))?", args[0]):
|
77
|
+
return float(args[0])
|
78
|
+
|
79
|
+
raise NotImplementedError(args[0])
|
80
|
+
|
81
|
+
# 5. unixepoch(time-value, modifier, modifier, ...)
|
82
|
+
def unixepoch_mock(*args):
|
83
|
+
"""Mock for UNIXEPOCH() function."""
|
84
|
+
raise NotImplementedError(args)
|
85
|
+
|
86
|
+
# 6. strftime(format, time-value, modifier, modifier, ...)
|
87
|
+
def strftime_mock(*args):
|
88
|
+
"""Mock for STRFTIME() function."""
|
89
|
+
raise NotImplementedError(args)
|
90
|
+
|
91
|
+
# 7. timediff(time-value, time-value)
|
92
|
+
def timediff_mock(*args):
|
93
|
+
"""Mock for TIMEDIFF() function."""
|
94
|
+
raise NotImplementedError(args)
|
95
|
+
|
96
|
+
conn = dbapi_connection
|
97
|
+
conn.create_function("DATE", -1, date_mock)
|
98
|
+
conn.create_function("TIME", -1, time_mock)
|
99
|
+
conn.create_function("DATETIME", -1, datetime_mock)
|
100
|
+
conn.create_function("JULIANDAY", -1, julianday_mock)
|
101
|
+
conn.create_function("UNIXEPOCH", -1, unixepoch_mock)
|
102
|
+
conn.create_function("STRFTIME", -1, strftime_mock)
|
103
|
+
conn.create_function("TIMEDIFF", -1, timediff_mock)
|
104
|
+
|
105
|
+
|
106
|
+
def julian_date(dt: datetime) -> float:
|
107
|
+
"""Convert a datetime to Julian Date."""
|
108
|
+
if dt.tzinfo is None:
|
109
|
+
raise ValueError("Datetime must be timezone-aware")
|
110
|
+
if dt.tzinfo.utcoffset(dt) is None:
|
111
|
+
raise ValueError("Datetime must be timezone-aware")
|
112
|
+
dt = dt.astimezone(UTC)
|
113
|
+
|
114
|
+
return julian_date_from_parts(
|
115
|
+
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
|
116
|
+
)
|
117
|
+
|
118
|
+
|
119
|
+
def julian_date_from_parts(
|
120
|
+
year: int, month: int, day: int, hour: int, minute: int, second: int
|
121
|
+
) -> float:
|
122
|
+
"""Convert year, month, day, hour, minute, second to Julian Date.
|
123
|
+
|
124
|
+
The time MUST be in UTC for the conversion to be correct.
|
125
|
+
"""
|
126
|
+
# Step 1: break out the "month shift"
|
127
|
+
a = (14 - month) // 12
|
128
|
+
y = year + 4800 - a
|
129
|
+
m = month + 12 * a - 3
|
130
|
+
|
131
|
+
# Step 2: compute the (integer) Julian Day Number
|
132
|
+
jdn = day + (153 * m + 2) // 5 + 365 * y + y // 4 - y // 100 + y // 400 - 32045
|
133
|
+
|
134
|
+
# Step 3: add fractional day
|
135
|
+
return jdn + (hour - 12) / 24 + minute / 1440 + second / 86400
|
diracx/testing/utils.py
CHANGED
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
6
6
|
# are the enabled_dependencies markers
|
7
7
|
import asyncio
|
8
8
|
import contextlib
|
9
|
+
import json
|
9
10
|
import os
|
10
11
|
import re
|
11
12
|
import ssl
|
@@ -19,6 +20,7 @@ from urllib.parse import parse_qs, urljoin, urlparse
|
|
19
20
|
|
20
21
|
import httpx
|
21
22
|
import pytest
|
23
|
+
from joserfc.jwk import KeySet, OKPKey
|
22
24
|
from uuid_utils import uuid7
|
23
25
|
|
24
26
|
from diracx.core.models import AccessTokenPayload, RefreshTokenPayload
|
@@ -43,8 +45,6 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
|
43
45
|
def pytest_addoption(parser):
|
44
46
|
parser.addoption(
|
45
47
|
"--regenerate-client",
|
46
|
-
action="store_true",
|
47
|
-
default=False,
|
48
48
|
help="Regenerate the AutoREST client",
|
49
49
|
)
|
50
50
|
parser.addoption(
|
@@ -55,28 +55,15 @@ def pytest_addoption(parser):
|
|
55
55
|
)
|
56
56
|
|
57
57
|
|
58
|
-
def pytest_collection_modifyitems(config, items):
|
59
|
-
"""Disable the test_regenerate_client if not explicitly asked for."""
|
60
|
-
if config.getoption("--regenerate-client"):
|
61
|
-
# --regenerate-client given in cli: allow client re-generation
|
62
|
-
return
|
63
|
-
skip_regen = pytest.mark.skip(reason="need --regenerate-client option to run")
|
64
|
-
for item in items:
|
65
|
-
if item.name == "test_regenerate_client":
|
66
|
-
item.add_marker(skip_regen)
|
67
|
-
|
68
|
-
|
69
58
|
@pytest.fixture(scope="session")
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
encryption_algorithm=serialization.NoEncryption(),
|
79
|
-
).decode()
|
59
|
+
def private_key() -> OKPKey:
|
60
|
+
return OKPKey.generate_key(
|
61
|
+
parameters={
|
62
|
+
"key_ops": ["sign", "verify"],
|
63
|
+
"alg": "EdDSA",
|
64
|
+
"kid": uuid7().hex,
|
65
|
+
}
|
66
|
+
)
|
80
67
|
|
81
68
|
|
82
69
|
@pytest.fixture(scope="session")
|
@@ -94,15 +81,12 @@ def test_dev_settings() -> Generator[DevelopmentSettings, None, None]:
|
|
94
81
|
|
95
82
|
|
96
83
|
@pytest.fixture(scope="session")
|
97
|
-
def test_auth_settings(
|
98
|
-
private_key_pem, fernet_key
|
99
|
-
) -> Generator[AuthSettings, None, None]:
|
84
|
+
def test_auth_settings(private_key, fernet_key) -> Generator[AuthSettings, None, None]:
|
100
85
|
from diracx.core.settings import AuthSettings
|
101
86
|
|
102
87
|
yield AuthSettings(
|
103
88
|
token_issuer=ISSUER,
|
104
|
-
|
105
|
-
token_key=private_key_pem,
|
89
|
+
token_keystore=json.dumps(KeySet([private_key]).as_dict(private_keys=True)),
|
106
90
|
state_key=fernet_key,
|
107
91
|
allowed_redirects=[
|
108
92
|
"http://diracx.test.invalid:8000/api/docs/oauth2-redirect",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: diracx-testing
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.1a45
|
4
4
|
Summary: TODO
|
5
5
|
License: GPL-3.0-only
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
@@ -9,12 +9,11 @@ Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Topic :: Scientific/Engineering
|
10
10
|
Classifier: Topic :: System :: Distributed Computing
|
11
11
|
Requires-Python: >=3.11
|
12
|
-
|
12
|
+
Requires-Dist: httpx
|
13
|
+
Requires-Dist: joserfc
|
13
14
|
Requires-Dist: pytest
|
14
15
|
Requires-Dist: pytest-asyncio
|
15
16
|
Requires-Dist: pytest-cov
|
16
17
|
Requires-Dist: pytest-xdist
|
17
|
-
Requires-Dist: httpx
|
18
18
|
Requires-Dist: uuid-utils
|
19
19
|
Provides-Extra: testing
|
20
|
-
Requires-Dist: diracx-testing; extra == "testing"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
diracx/testing/__init__.py,sha256=ocW2U2SaWC9zvTjWMVhOQtFQ4rqSJbsCJXCMUpBj0rs,904
|
2
|
+
diracx/testing/__main__.py,sha256=cd6HxeVKqVrxk32uAB_4O0PzzAo_rzwEK2GN7kND8XY,2419
|
3
|
+
diracx/testing/client_generation.py,sha256=B1_b1ku2GEJkDqfDrro3ZpwMbzE5Pxo7fkZ3ew0VYe4,7949
|
4
|
+
diracx/testing/client_generation_pytest.py,sha256=c4lvwi6U76yu_iNT6N3zgrdVsBhMPNZi50CXVXYNsi4,1173
|
5
|
+
diracx/testing/dummy_osdb.py,sha256=ptLrrreOHJBKMV7S1pR_k83jl-aLKvw7bFZufB6570Y,916
|
6
|
+
diracx/testing/entrypoints.py,sha256=ql7byyUOnUBdDoiN5xznlUTQSbD1YCNy4UF5t9MBhgc,2330
|
7
|
+
diracx/testing/mock_osdb.py,sha256=2pl9yKV1-e5kzf6meBZnhoc1i0ySAJ8IdnP3r1bOwcI,6033
|
8
|
+
diracx/testing/osdb.py,sha256=m6mUBLnGOoQLTCIBie9P2GhmLMybrgzIrlIYfhF1_Ss,3230
|
9
|
+
diracx/testing/routers.py,sha256=UW-TnikMQgcNxF5sUZD5DWoucGiCpP6s8mYmuahDiSc,979
|
10
|
+
diracx/testing/time.py,sha256=ec2b4yn-52ey8ea1oINdknxtQf-CpPRBIiI15Wa1W2Q,4320
|
11
|
+
diracx/testing/utils.py,sha256=c8NA1RMPemAUfrec8YfphcsKsauWlIJ1vyOL3-uWjuM,23804
|
12
|
+
diracx/testing/scripts/collect_demo_coverage.sh,sha256=EyIuP6rA-g542BHTPL2cD4qrBSm-FQ5y1e0c1qhVcEs,4555
|
13
|
+
diracx_testing-0.0.1a45.dist-info/METADATA,sha256=FHxCST0jbSX2EM6oNwELxd3Fh1YnP3u3LINJC9jM6f4,593
|
14
|
+
diracx_testing-0.0.1a45.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
+
diracx_testing-0.0.1a45.dist-info/RECORD,,
|
@@ -1,12 +0,0 @@
|
|
1
|
-
diracx/testing/__init__.py,sha256=nGbnGP8m53N9rqHR-hyqDa5vetcsmnQp806HadV6_dE,984
|
2
|
-
diracx/testing/client_generation.py,sha256=vegOUVQPcB7NTUFDr0HbasH5eByKQvLh_xYqKBYajzk,5539
|
3
|
-
diracx/testing/dummy_osdb.py,sha256=ptLrrreOHJBKMV7S1pR_k83jl-aLKvw7bFZufB6570Y,916
|
4
|
-
diracx/testing/entrypoints.py,sha256=ql7byyUOnUBdDoiN5xznlUTQSbD1YCNy4UF5t9MBhgc,2330
|
5
|
-
diracx/testing/mock_osdb.py,sha256=2pl9yKV1-e5kzf6meBZnhoc1i0ySAJ8IdnP3r1bOwcI,6033
|
6
|
-
diracx/testing/osdb.py,sha256=m6mUBLnGOoQLTCIBie9P2GhmLMybrgzIrlIYfhF1_Ss,3230
|
7
|
-
diracx/testing/routers.py,sha256=UW-TnikMQgcNxF5sUZD5DWoucGiCpP6s8mYmuahDiSc,979
|
8
|
-
diracx/testing/utils.py,sha256=MnFUgRWfpU9GFOvP5d-LQS0uo1grHy_-iNOGLS7XlNk,24483
|
9
|
-
diracx_testing-0.0.1a44.dist-info/METADATA,sha256=adRAUG765p5g6031W2i-wZzGiPMMUL_YADw-BUhXdKU,660
|
10
|
-
diracx_testing-0.0.1a44.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
|
11
|
-
diracx_testing-0.0.1a44.dist-info/top_level.txt,sha256=vJx10tdRlBX3rF2Psgk5jlwVGZNcL3m_7iQWwgPXt-U,7
|
12
|
-
diracx_testing-0.0.1a44.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
diracx
|