Skip to content

Commit 47700ae

Browse files
committed
feat: add a logic test to make sure attributes and params are going to the right backends
1 parent 680e14a commit 47700ae

File tree

5 files changed

+170
-47
lines changed

5 files changed

+170
-47
lines changed

diracx-core/src/diracx/core/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ class SearchParams(BaseModel):
7575
# TODO: Add more validation
7676

7777

78-
class JobParameters(BaseModel):
79-
"""All the parameters that can be set for a job."""
78+
class JobParameters(BaseModel, populate_by_name=True, extra="allow"):
79+
"""Some of the most important parameters that can be set for a job."""
8080

8181
timestamp: datetime | None = None
8282
cpu_normalization_factor: int | None = Field(None, alias="CPUNormalizationFactor")
@@ -113,7 +113,7 @@ def convert_cpu_fields_to_int(cls, v):
113113
return v
114114

115115

116-
class JobAttributes(BaseModel):
116+
class JobAttributes(BaseModel, populate_by_name=True, extra="forbid"):
117117
"""All the attributes that can be set for a job."""
118118

119119
job_type: str | None = Field(None, alias="JobType")
@@ -138,7 +138,7 @@ class JobAttributes(BaseModel):
138138
accounted_flag: bool | str | None = Field(None, alias="AccountedFlag")
139139

140140

141-
class JobMetaData(JobAttributes, JobParameters):
141+
class JobMetaData(JobAttributes, JobParameters, extra="allow"):
142142
"""A model that combines both JobAttributes and JobParameters."""
143143

144144

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import AsyncGenerator
4+
from datetime import datetime, timezone
5+
6+
import pytest
7+
import sqlalchemy
8+
9+
from diracx.core.models import JobMetaData
10+
from diracx.db.os.job_parameters import JobParametersDB as RealJobParametersDB
11+
from diracx.db.sql.job.db import JobDB
12+
from diracx.logic.jobs.status import set_job_parameters_or_attributes
13+
from diracx.testing.mock_osdb import MockOSDBMixin
14+
from diracx.testing.time import mock_sqlite_time
15+
16+
17+
# Reuse the generic MockOSDBMixin to build a mock JobParameters DB implementation
18+
class _MockJobParametersDB(MockOSDBMixin, RealJobParametersDB):
19+
def __init__(self): # type: ignore[override]
20+
super().__init__({"sqlalchemy_dsn": "sqlite+aiosqlite:///:memory:"})
21+
22+
def upsert(self, vo, doc_id, document):
23+
"""Override to add JobID to the document."""
24+
# Add JobID to the document, which is required by the base class
25+
document["JobID"] = doc_id
26+
return super().upsert(vo, doc_id, document)
27+
28+
29+
# --------------------------------------------------------------------------------------
30+
# Test setup fixtures
31+
# --------------------------------------------------------------------------------------
32+
33+
34+
@pytest.fixture
35+
async def job_db() -> AsyncGenerator[JobDB, None]:
36+
"""Create a fake sandbox metadata database."""
37+
db = JobDB(db_url="sqlite+aiosqlite:///:memory:")
38+
async with db.engine_context():
39+
sqlalchemy.event.listen(db.engine.sync_engine, "connect", mock_sqlite_time)
40+
41+
async with db.engine.begin() as conn:
42+
await conn.run_sync(db.metadata.create_all)
43+
44+
yield db
45+
46+
47+
@pytest.fixture
48+
async def job_parameters_db() -> AsyncGenerator[_MockJobParametersDB, None]:
49+
db = _MockJobParametersDB()
50+
# Need engine_context entered before creating tables
51+
async with db.client_context():
52+
await db.create_index_template()
53+
yield db
54+
55+
56+
TEST_JDL = """
57+
Arguments = "jobDescription.xml -o LogLevel=INFO";
58+
Executable = "dirac-jobexec";
59+
JobGroup = jobGroup;
60+
JobName = jobName;
61+
JobType = User;
62+
LogLevel = INFO;
63+
OutputSandbox =
64+
{
65+
Script1_CodeOutput.log,
66+
std.err,
67+
std.out
68+
};
69+
Priority = 1;
70+
Site = ANY;
71+
StdError = std.err;
72+
StdOutput = std.out;
73+
"""
74+
75+
76+
@pytest.fixture
77+
async def valid_job_id(job_db: JobDB) -> int:
78+
"""Create a minimal job record and return its JobID."""
79+
async with job_db:
80+
job_id = await job_db.create_job("") # original JDL unused in these tests
81+
# Insert initial attributes (mimic job submission)
82+
await job_db.insert_job_attributes(
83+
{
84+
job_id: {
85+
"Status": "Received",
86+
"MinorStatus": "Job accepted",
87+
"ApplicationStatus": "Unknown",
88+
"VO": "lhcb",
89+
"Owner": "tester",
90+
"OwnerGroup": "lhcb_user",
91+
"VerifiedFlag": True,
92+
"JobType": "User",
93+
}
94+
}
95+
)
96+
return job_id
97+
98+
99+
# --------------------------------------------------------------------------------------
100+
# Tests
101+
# --------------------------------------------------------------------------------------
102+
103+
104+
@pytest.mark.asyncio
105+
async def test_patch_metadata_updates_attributes_and_parameters(
106+
job_db: JobDB, job_parameters_db: _MockJobParametersDB, valid_job_id: int
107+
):
108+
"""Patch metadata mixing:
109+
- Attribute only (UserPriority)
110+
- Attribute + parameter (JobType)
111+
- Parameter only (CPUNormalizationFactor)
112+
- Attribute (HeartBeatTime)
113+
- Non identified Metadata (does_not_exist)
114+
and verify correct persistence in the two backends.
115+
"""
116+
hbt = datetime.now(timezone.utc)
117+
118+
updates = {
119+
valid_job_id: JobMetaData(
120+
user_priority=2, # alias UserPriority
121+
job_type="VerySpecialIndeed", # alias JobType (attr + param)
122+
cpu_normalization_factor=10, # alias CPUNormalizationFactor (param only)
123+
heart_beat_time=hbt, # alias HeartBeatTime (attribute)
124+
does_not_exist="unknown", # Does not exist shoult be treated as a job parameter
125+
)
126+
}
127+
128+
# Act
129+
async with job_db: # ensure open connection for updates
130+
await set_job_parameters_or_attributes(updates, job_db, job_parameters_db)
131+
132+
# Assert job attributes (SQL)
133+
async with job_db:
134+
_, rows = await job_db.search(
135+
parameters=[
136+
"JobID",
137+
"UserPriority",
138+
"JobType",
139+
"HeartBeatTime",
140+
"Status",
141+
"MinorStatus",
142+
"ApplicationStatus",
143+
],
144+
search=[{"parameter": "JobID", "operator": "eq", "value": valid_job_id}],
145+
sorts=[],
146+
)
147+
assert len(rows) == 1
148+
row = rows[0]
149+
assert int(row["JobID"]) == valid_job_id
150+
assert row["UserPriority"] == 2
151+
assert row["JobType"] == "VerySpecialIndeed"
152+
# HeartBeatTime stored as ISO string (without tz) in DB helper; just ensure present
153+
assert row["HeartBeatTime"] is not None
154+
155+
# Assert job parameters (mocked OS / sqlite)
156+
params_rows = await job_parameters_db.search(
157+
parameters=["JobType", "CPUNormalizationFactor", "does_not_exist"],
158+
search=[{"parameter": "JobID", "operator": "eq", "value": valid_job_id}],
159+
sorts=[],
160+
)
161+
prow = params_rows[0]
162+
assert prow["JobType"] == "VerySpecialIndeed"
163+
assert prow["CPUNormalizationFactor"] == 10
164+
assert prow["does_not_exist"] == "unknown"

diracx-routers/tests/jobs/test_query.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ def test_get_job_status_history_in_bulk(
880880
assert r.json()[str(valid_job_id)][0]["Source"] == "JobManager"
881881

882882

883-
def test_patch_summary(normal_user_client: TestClient, valid_job_id: int):
883+
def test_summary(normal_user_client: TestClient, valid_job_id: int):
884884
"""Test that the summary endpoint works as expected."""
885885
r = normal_user_client.post(
886886
"/api/jobs/summary",
@@ -906,7 +906,7 @@ def test_patch_summary(normal_user_client: TestClient, valid_job_id: int):
906906
assert r.json() == [{"Owner": "preferred_username", "count": 1}]
907907

908908

909-
def test_patch_summary_doc_example(normal_user_client: TestClient, valid_job_id: int):
909+
def test_summary_doc_example(normal_user_client: TestClient, valid_job_id: int):
910910
"""Test that the summary doc example is correct."""
911911
payload = EXAMPLE_SUMMARY["Group by JobGroup"]["value"]
912912
r = normal_user_client.post("/api/jobs/summary", json=payload)

diracx-routers/tests/jobs/test_status.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -898,47 +898,6 @@ def test_patch_metadata(normal_user_client: TestClient, valid_job_id: int):
898898
assert r.json()[0]["UserPriority"] == 2
899899

900900

901-
def test_bad_patch_metadata(normal_user_client: TestClient, valid_job_id: int):
902-
# Arrange
903-
r = normal_user_client.post(
904-
"/api/jobs/search",
905-
json={
906-
"search": [
907-
{
908-
"parameter": "JobID",
909-
"operator": "eq",
910-
"value": valid_job_id,
911-
}
912-
],
913-
"parameters": ["LoggingInfo"],
914-
},
915-
)
916-
917-
assert r.status_code == 200, r.json()
918-
for j in r.json():
919-
assert j["JobID"] == valid_job_id
920-
assert j["Status"] == JobStatus.RECEIVED.value
921-
assert j["MinorStatus"] == "Job accepted"
922-
assert j["ApplicationStatus"] == "Unknown"
923-
924-
# Act
925-
hbt = str(datetime.now(timezone.utc))
926-
r = normal_user_client.patch(
927-
"/api/jobs/metadata",
928-
json={
929-
valid_job_id: {
930-
"UserPriority": 2,
931-
"Heartbeattime": hbt,
932-
# set a parameter
933-
"JobType": "VerySpecialIndeed",
934-
}
935-
},
936-
)
937-
938-
# Assert
939-
assert r.status_code == 422, r.text
940-
941-
942901
def test_diracx_476(normal_user_client: TestClient, valid_job_id: int):
943902
"""Test fix for https://github.com/DIRACGrid/diracx/issues/476."""
944903
inner_payload = {"Status": JobStatus.FAILED.value, "MinorStatus": "Payload failed"}

0 commit comments

Comments
 (0)