Warning, file /panda-server/pandaserver/workflow/tests/step_submission_test.py was not indexed
or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 """
0002 Offline check of workflow step task submission.
0003
0004 Covers the pieces that turn a parsed step into a queued task: resolving input dataset references
0005 against the datasets actually produced upstream, refusing a production label without the role,
0006 resolving the late-bound task ID into the step's output dataset names, and the task status mapping.
0007
0008 Run from the repository root: python3 pandaserver/workflow/tests/step_submission_test.py
0009 """
0010
0011 import copy
0012 import importlib.abc
0013 import importlib.machinery
0014 import json
0015 import os
0016 import re
0017 import sys
0018 import types
0019
0020 REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
0021 sys.path.insert(0, REPO_ROOT)
0022
0023 AUTO_STUB_ROOTS = ("idds", "pandaclient", "ruamel", "requests", "snakemake")
0024
0025
0026 class AutoStubFinder(importlib.abc.MetaPathFinder, importlib.abc.Loader):
0027 def find_spec(self, name, path=None, target=None):
0028 if name.split(".")[0] in AUTO_STUB_ROOTS:
0029 return importlib.machinery.ModuleSpec(name, self, is_package=True)
0030 return None
0031
0032 def create_module(self, spec):
0033 module = types.ModuleType(spec.name)
0034 module.__path__ = []
0035 return module
0036
0037 def exec_module(self, module):
0038 class Anything:
0039 def __init__(self, *a, **k):
0040 pass
0041
0042 def __call__(self, *a, **k):
0043 return self
0044
0045 module.__getattr__ = lambda name: Anything
0046
0047
0048 sys.meta_path.insert(0, AutoStubFinder())
0049
0050
0051 def stub(name, **attrs):
0052 module = types.ModuleType(name)
0053 for key, value in attrs.items():
0054 setattr(module, key, value)
0055 sys.modules[name] = module
0056 return module
0057
0058
0059 LOGGED: dict[str, list[str]] = {"warning": [], "error": []}
0060
0061
0062 class Log:
0063 def __init__(self, *a, **k):
0064 pass
0065
0066 def info(self, m):
0067 pass
0068
0069 def debug(self, m):
0070 pass
0071
0072 def warning(self, m):
0073 LOGGED["warning"].append(m)
0074
0075 def error(self, m):
0076 LOGGED["error"].append(m)
0077
0078
0079 stub("pandacommon")
0080 pandautils = stub("pandacommon.pandautils")
0081 pandautils.__path__ = []
0082 stub("pandacommon.pandautils.base", SpecBase=object)
0083 pandalogger = stub("pandacommon.pandalogger")
0084 pandalogger.__path__ = []
0085 stub("pandacommon.pandalogger.LogWrapper", LogWrapper=Log)
0086 stub("pandacommon.pandalogger.PandaLogger", PandaLogger=lambda: types.SimpleNamespace(getLogger=lambda n: None))
0087
0088 stub("pandacommon.pandautils.PandaUtils", naive_utcnow=lambda: None)
0089 stub("pandaserver.config", panda_config=types.SimpleNamespace(schemaJEDI="ATLAS_PANDA", schemaDEFT="ATLAS_DEFT"))
0090
0091 from pandaserver.workflow.step_handler_plugins.panda_task_step_handler import (
0092 PandaTaskStepHandler,
0093 )
0094 from pandaserver.workflow.workflow_base import (
0095 PARENT_TASKID_PLACEHOLDER,
0096 TASKID_PLACEHOLDER,
0097 WFStepSpec,
0098 WFStepStatus,
0099 )
0100
0101
0102 class FakeData:
0103 def __init__(self, name, target_id, source_step_id=None):
0104 self.name = name
0105 self.target_id = target_id
0106 self.workflow_id = 1
0107 self.data_id = abs(hash(name)) % 1000
0108
0109 self.source_step_id = source_step_id
0110
0111
0112 class FakeStep(WFStepSpec):
0113 def __init__(self, definition, parameters=None):
0114 self.workflow_id = 1
0115 self.step_id = 7
0116 self.name = "probe"
0117 self.flavor = "panda_task"
0118 self.target_id = None
0119 self.status = WFStepStatus.ready
0120
0121
0122 self.definition_json = json.dumps(definition)
0123 self.parameters = json.dumps(parameters or {})
0124
0125
0126 class FakeTaskBuffer:
0127 def __init__(self, data_by_name=None, task_id=49900001, error="", deft_status=None, steps_by_id=None, existing_task_names=None, name_lookup_fails=False):
0128 self.deft_status = deft_status
0129 self.data = data_by_name or {}
0130 self.steps = steps_by_id or {}
0131 self.task_id = task_id
0132 self.error = error
0133 self.inserted = []
0134 self.inserted_parent_tids = []
0135 self.updated_data = []
0136 self.existing_task_names = existing_task_names or {}
0137 self.name_lookup_fails = name_lookup_fails
0138 self.name_lookups = []
0139
0140 def get_existing_task_names(self, vo, prod_source_label, task_names):
0141 self.name_lookups.append((vo, prod_source_label, list(task_names)))
0142
0143 if self.name_lookup_fails:
0144 return None
0145 return {name: info for name, info in self.existing_task_names.items() if name in task_names}
0146
0147 def get_working_group(self, fqans):
0148 for fqan in fqans:
0149 match = re.search("/[^/]+/([^/]+)/Role=production", fqan)
0150 if match is not None:
0151 return match.group(1)
0152 return None
0153
0154 def get_workflow_step(self, step_id):
0155 return self.steps.get(step_id)
0156
0157 def get_steps_of_workflow(self, workflow_id, status_filter_list=None, status_exclusion_list=None):
0158 return list(self.steps.values())
0159
0160 def get_workflow_data_by_name(self, name, workflow_id):
0161 return self.data.get(name)
0162
0163 def update_workflow_data(self, data_spec):
0164 self.updated_data.append((data_spec.name, data_spec.target_id))
0165
0166 def update_workflow_step(self, step_spec):
0167 pass
0168
0169 def insert_step_task(self, task_params_map, user_dn, parent_tid=None):
0170 self.inserted.append(copy.deepcopy(task_params_map))
0171 self.inserted_parent_tids.append(parent_tid)
0172 if self.task_id is None:
0173 return None, self.error
0174
0175 self.existing_task_names[task_params_map["taskName"]] = {"jediTaskID": self.task_id, "status": None}
0176 return self.task_id, ""
0177
0178 def getTaskStatusSuperstatus(self, task_id):
0179 return self._status
0180
0181 def get_deft_task_status(self, task_id):
0182 return self.deft_status
0183
0184 def set_status(self, status, superstatus=None):
0185
0186 self._status = None if status is None else (status, superstatus or status)
0187
0188 def getTaskWithID_JEDI(self, task_id, *args, **kwargs):
0189
0190 return (False, None) if self._status is None else (True, None)
0191
0192
0193 def check(label, condition, detail=""):
0194 print(f" {'PASS' if condition else 'FAIL'} {label}{' ' + str(detail) if detail and not condition else ''}")
0195 return condition
0196
0197
0198 def main():
0199 failures = 0
0200 wfd = json.load(open(os.path.join(os.path.dirname(__file__), "..", "examples", "production_chain_wfd.json")))
0201
0202 simul_params = copy.deepcopy(wfd["steps"]["simul"]["task_params"])
0203
0204 simul_params = json.loads(json.dumps(simul_params).replace("${WFID}", "12345"))
0205
0206 def make_step(prod_role=True, all_inputs_complete=True, params=None, definition=None):
0207 step_definition = {
0208 "task_params": copy.deepcopy(params if params is not None else simul_params),
0209 "user_name": "Some User",
0210 "user_dn": "/DC=ch/CN=Some User",
0211 "prod_role": prod_role,
0212 "fqans": [],
0213 "output_data_list": ["simul/HITS"],
0214 }
0215 step_definition.update(definition or {})
0216 return FakeStep(step_definition, {"all_inputs_complete": all_inputs_complete})
0217
0218 produced_evnt = "mc23_13p6TeV.526140.x.merge.EVNT.e8590_e8586_wfid12345_tid48810699_00"
0219
0220 def make_tbif(**kw):
0221 data = {
0222 "merge_evnt/EVNT": FakeData("merge_evnt/EVNT", produced_evnt),
0223 "simul/HITS": FakeData("simul/HITS", f"mc23_13p6TeV.526140.x.simul.HITS.e8590_e8586_a934_wfid12345_tid{TASKID_PLACEHOLDER}_00"),
0224 }
0225 return FakeTaskBuffer(data_by_name=data, **kw)
0226
0227 print("\n=== submit_target: the happy path ===")
0228 tbif = make_tbif()
0229 handler = PandaTaskStepHandler(tbif)
0230 step = make_step()
0231 res = handler.submit_target(step)
0232 failures += not check("submitted", res.success is True, res.message)
0233 failures += not check("target_id is the task id", res.target_id == "49900001", res.target_id)
0234 submitted = tbif.inserted[0]
0235 inputs = [p["dataset"] for p in submitted["jobParameters"] if p.get("param_type") == "input"]
0236 failures += not check("input reference resolved to the produced dataset", inputs == [produced_evnt], inputs)
0237 failures += not check("no brace reference left in any input dataset", not any("{" in d for d in inputs), inputs)
0238
0239
0240 outputs = [p["dataset"] for p in submitted["jobParameters"] if p.get("param_type") == "output"]
0241 failures += not check("output datasets still carry the placeholder for the DB layer to resolve", all(TASKID_PLACEHOLDER in d for d in outputs), outputs)
0242 failures += not check("pseudo_input left untouched", any(p.get("dataset") == "seq_number" for p in submitted["jobParameters"]))
0243 failures += not check("workflowHoldup not set when inputs are complete", "workflowHoldup" not in submitted)
0244 failures += not check(
0245 "output dataset name resolved from the task id",
0246 tbif.updated_data == [("simul/HITS", "mc23_13p6TeV.526140.x.simul.HITS.e8590_e8586_a934_wfid12345_tid49900001_00")],
0247 tbif.updated_data,
0248 )
0249 failures += not check("submission attempt recorded", step.get_parameter("submit_attempt_task_name") == simul_params["taskName"])
0250
0251 print("\n=== workflowHoldup is set while inputs are incomplete ===")
0252 tbif = make_tbif()
0253 handler = PandaTaskStepHandler(tbif)
0254 handler.submit_target(make_step(all_inputs_complete=False))
0255 failures += not check("workflowHoldup set", tbif.inserted[0].get("workflowHoldup") is True)
0256
0257 print("\n=== a production label without the role is refused ===")
0258
0259 for label in ["managed"]:
0260 params = copy.deepcopy(simul_params)
0261 params["prodSourceLabel"] = label
0262 tbif = make_tbif()
0263 handler = PandaTaskStepHandler(tbif)
0264 res = handler.submit_target(make_step(prod_role=False, params=params))
0265 failures += not check(f"{label} refused without the role", res.success is not True)
0266 failures += not check(f"{label} reason mentions the production role", "production role" in res.message, res.message)
0267 failures += not check(f"{label} submitted nothing", tbif.inserted == [])
0268
0269 tbif = make_tbif()
0270 handler = PandaTaskStepHandler(tbif)
0271 res = handler.submit_target(make_step(prod_role=True, params=params))
0272 failures += not check(f"{label} accepted with the role", res.success is True, res.message)
0273
0274
0275
0276 for label in ["user", "test", "ptest"]:
0277 params = copy.deepcopy(simul_params)
0278 params["prodSourceLabel"] = label
0279 tbif = make_tbif()
0280 handler = PandaTaskStepHandler(tbif)
0281 res = handler.submit_target(make_step(prod_role=False, params=params))
0282 failures += not check(f"{label} needs no production role", res.success is True, res.message)
0283
0284 print("\n=== a step built from a command line gets the parameters the client never sends ===")
0285
0286
0287
0288 client_params = {
0289 "taskName": "user.someuser.0f9c6b31-4d7e-4a01-9a2f-7c3d5e8b1f40_001_step1",
0290 "uniqueTaskName": True,
0291 "vo": "atlas",
0292 "prodSourceLabel": "user",
0293 "architecture": "",
0294 "processingType": "panda-client-2.1.3-jedi-run",
0295 "log": {
0296 "dataset": "user.someuser.0f9c6b31-4d7e-4a01-9a2f-7c3d5e8b1f40_001_step1.log/",
0297 "type": "template",
0298 "param_type": "log",
0299 "value": "user.someuser.0f9c6b31-4d7e-4a01-9a2f-7c3d5e8b1f40_001_step1.log.$JEDITASKID.${SN}.log.tgz",
0300 },
0301 "jobParameters": [
0302 {
0303 "type": "template",
0304 "param_type": "output",
0305 "value": "user.someuser.$JEDITASKID._${SN/P}.merge.root",
0306 "dataset": "user.someuser.0f9c6b31-4d7e-4a01-9a2f-7c3d5e8b1f40_001_step1_merge.root/",
0307 },
0308 {"type": "template", "param_type": "input", "value": '-i "${IN/T}"', "dataset": "user.iliadis.data25_13p6TeV.NTUP_MCP.tid49752806_00/"},
0309 ],
0310 }
0311 no_outputs: dict[str, list[str]] = {"output_data_list": []}
0312 tbif = make_tbif()
0313 handler = PandaTaskStepHandler(tbif)
0314 res = handler.submit_target(make_step(prod_role=False, params=client_params, definition=no_outputs))
0315 failures += not check("submitted", res.success is True, res.message)
0316 submitted = tbif.inserted[0]
0317 failures += not check("userName written from the submitter", submitted.get("userName") == "Some User", submitted.get("userName"))
0318 failures += not check("taskType written as analysis", submitted.get("taskType") == "anal", submitted.get("taskType"))
0319 failures += not check("taskPriority written", submitted.get("taskPriority") == 1000, submitted.get("taskPriority"))
0320
0321 tbif = make_tbif()
0322 handler = PandaTaskStepHandler(tbif)
0323 res = handler.submit_target(make_step(prod_role=False, params=client_params, definition=dict(no_outputs, user_name=None)))
0324 failures += not check("userName derived from the DN", tbif.inserted[0].get("userName") == "Some User", tbif.inserted[0].get("userName"))
0325
0326 print("\n=== an authored identity is kept only for a submitter holding the role ===")
0327
0328 tbif = make_tbif()
0329 handler = PandaTaskStepHandler(tbif)
0330 handler.submit_target(make_step(prod_role=True))
0331 submitted = tbif.inserted[0]
0332 failures += not check("authored userName kept with the role", submitted["userName"] == simul_params["userName"], submitted["userName"])
0333 failures += not check("authored taskType kept with the role", submitted["taskType"] == simul_params["taskType"], submitted["taskType"])
0334 failures += not check("authored taskPriority kept with the role", submitted["taskPriority"] == simul_params["taskPriority"], submitted["taskPriority"])
0335
0336
0337
0338 params = copy.deepcopy(simul_params)
0339 params["prodSourceLabel"] = "user"
0340 tbif = make_tbif()
0341 handler = PandaTaskStepHandler(tbif)
0342 handler.submit_target(make_step(prod_role=False, params=params))
0343 submitted = tbif.inserted[0]
0344 failures += not check("authored userName overridden without the role", submitted["userName"] == "Some User", submitted["userName"])
0345 failures += not check("taskType forced to analysis without the role", submitted["taskType"] == "anal", submitted["taskType"])
0346
0347 print("\n=== an official dataset takes its working group from the submitter's FQANs ===")
0348 params = copy.deepcopy(client_params)
0349 params["official"] = True
0350 tbif = make_tbif()
0351 handler = PandaTaskStepHandler(tbif)
0352 handler.submit_target(make_step(prod_role=False, params=params, definition=dict(no_outputs, fqans=["/atlas/phys-higgs/Role=production"])))
0353 failures += not check("workingGroup resolved from the FQANs", tbif.inserted[0].get("workingGroup") == "phys-higgs", tbif.inserted[0].get("workingGroup"))
0354
0355 tbif = make_tbif()
0356 handler = PandaTaskStepHandler(tbif)
0357 handler.submit_target(make_step(prod_role=False, params=client_params, definition=dict(no_outputs, fqans=["/atlas/phys-higgs/Role=production"])))
0358 failures += not check("workingGroup left alone", "workingGroup" not in tbif.inserted[0])
0359
0360 print("\n=== a step naming no submitter is refused ===")
0361 tbif = make_tbif()
0362 handler = PandaTaskStepHandler(tbif)
0363 res = handler.submit_target(make_step(prod_role=False, params=client_params, definition=dict(no_outputs, user_name=None, user_dn=None)))
0364 failures += not check("refused", res.success is not True)
0365 failures += not check("reason mentions the submitter", "submitter" in res.message, res.message)
0366 failures += not check("nothing submitted", tbif.inserted == [])
0367
0368 print("\n=== a recorded attempt is checked against the tasks that exist ===")
0369 attempted_name = simul_params["taskName"]
0370
0371 def make_attempted_step():
0372 step = make_step()
0373 step.set_parameter("submit_attempt_task_name", attempted_name)
0374 return step
0375
0376
0377
0378 tbif = make_tbif()
0379 handler = PandaTaskStepHandler(tbif)
0380 res = handler.submit_target(make_attempted_step())
0381 failures += not check("submitted when the previous attempt queued nothing", res.success is True, res.message)
0382 failures += not check(
0383 "the lookup asked about this task name",
0384 tbif.name_lookups == [(simul_params["vo"], simul_params["prodSourceLabel"], [attempted_name])],
0385 tbif.name_lookups,
0386 )
0387
0388 tbif = make_tbif(existing_task_names={attempted_name: {"jediTaskID": 48810706, "status": "running"}})
0389 handler = PandaTaskStepHandler(tbif)
0390 res = handler.submit_target(make_attempted_step())
0391 failures += not check("refused when the task exists", res.success is not True)
0392 failures += not check("reason names the task that exists", "48810706" in res.message, res.message)
0393 failures += not check("nothing submitted", tbif.inserted == [])
0394
0395 tbif = make_tbif(name_lookup_fails=True)
0396 handler = PandaTaskStepHandler(tbif)
0397 res = handler.submit_target(make_attempted_step())
0398 failures += not check("refused when the lookup failed", res.success is not True)
0399 failures += not check("reason says it could not be checked", "could not be checked" in res.message, res.message)
0400 failures += not check("nothing submitted", tbif.inserted == [])
0401
0402 tbif = make_tbif()
0403 handler = PandaTaskStepHandler(tbif)
0404 step = make_step()
0405 step.set_parameter("submit_attempt_task_name", "some.other.taskName")
0406 res = handler.submit_target(step)
0407 failures += not check("submitted when the recorded name differs", res.success is True, res.message)
0408 failures += not check("no lookup needed", tbif.name_lookups == [], tbif.name_lookups)
0409
0410 print("\n=== an unresolved upstream output blocks submission ===")
0411 tbif = make_tbif()
0412 tbif.data["merge_evnt/EVNT"].target_id = f"mc23...merge.EVNT.e8590_wfid12345_tid{TASKID_PLACEHOLDER}_00"
0413 handler = PandaTaskStepHandler(tbif)
0414 res = handler.submit_target(make_step())
0415 failures += not check("refused", res.success is not True)
0416 failures += not check("reason mentions it is not resolved yet", "not resolved yet" in res.message, res.message)
0417 failures += not check("nothing submitted", tbif.inserted == [])
0418
0419 print("\n=== a missing upstream output blocks submission ===")
0420 tbif = make_tbif()
0421 del tbif.data["merge_evnt/EVNT"]
0422 handler = PandaTaskStepHandler(tbif)
0423 res = handler.submit_target(make_step())
0424 failures += not check("refused", res.success is not True)
0425 failures += not check("nothing submitted", tbif.inserted == [])
0426
0427 print("\n=== a second attempt for the same taskName is refused ===")
0428 tbif = make_tbif()
0429 handler = PandaTaskStepHandler(tbif)
0430 step = make_step()
0431 handler.submit_target(step)
0432 res2 = handler.submit_target(step)
0433 failures += not check("second attempt refused", res2.success is not True)
0434 failures += not check("reason mentions the previous attempt", "previous attempt" in res2.message, res2.message)
0435 failures += not check("submitted only once", len(tbif.inserted) == 1, len(tbif.inserted))
0436
0437 print("\n=== a literal (external) dataset is passed through ===")
0438 recon_params = json.loads(json.dumps(copy.deepcopy(wfd["steps"]["recon"]["task_params"])).replace("${WFID}", "12345"))
0439 tbif = make_tbif()
0440 tbif.data["merge_hits/HITS"] = FakeData("merge_hits/HITS", "mc23...merge.HITS..._tid48810713_00")
0441 tbif.data["rdo_bkg"] = FakeData("rdo_bkg", wfd["inputs"]["rdo_bkg"])
0442 handler = PandaTaskStepHandler(tbif)
0443 step = FakeStep({"task_params": recon_params, "user_dn": "/DC=ch/CN=t", "prod_role": True, "output_data_list": []}, {"all_inputs_complete": True})
0444 res = handler.submit_target(step)
0445 failures += not check("submitted", res.success is True, res.message)
0446 datasets = [p["dataset"] for p in tbif.inserted[0]["jobParameters"] if p.get("param_type") == "input"]
0447 failures += not check("both inputs resolved", set(datasets) == {"mc23...merge.HITS..._tid48810713_00", wfd["inputs"]["rdo_bkg"]}, datasets)
0448
0449 print("\n=== a task submitted but not yet refined into JEDI ===")
0450
0451
0452 del LOGGED["warning"][:], LOGGED["error"][:]
0453 tbif = make_tbif(deft_status="waiting")
0454 tbif.set_status(None)
0455 handler = PandaTaskStepHandler(tbif)
0456 step = make_step()
0457 step.status = WFStepStatus.running
0458 step.target_id = "49900001"
0459 result = handler.check_target(step)
0460 failures += not check("check_target does not advance the step", not result.success)
0461
0462 failures += not check("success is None, not False", result.success is None, result.success)
0463 failures += not check("logged as a warning, not an error", LOGGED["warning"] and not LOGGED["error"], (LOGGED["warning"], LOGGED["error"]))
0464 failures += not check("message explains it is queued in DEFT", "queued in DEFT" in result.message and "not yet refined" in result.message, result.message)
0465
0466 del LOGGED["warning"][:], LOGGED["error"][:]
0467 handler.on_all_inputs_done(step)
0468 failures += not check("on_all_inputs_done also warns rather than errors", LOGGED["warning"] and not LOGGED["error"], (LOGGED["warning"], LOGGED["error"]))
0469
0470 print("\n=== a task missing from both JEDI and DEFT is still an error ===")
0471 del LOGGED["warning"][:], LOGGED["error"][:]
0472 tbif = make_tbif(deft_status=None)
0473 tbif.set_status(None)
0474 handler = PandaTaskStepHandler(tbif)
0475 step = make_step()
0476 step.status = WFStepStatus.running
0477 step.target_id = "49900001"
0478 result = handler.check_target(step)
0479 failures += not check("logged as an error", LOGGED["error"] and not LOGGED["warning"], (LOGGED["warning"], LOGGED["error"]))
0480 failures += not check("message says neither JEDI nor DEFT", "not found in JEDI or DEFT" in result.message, result.message)
0481 failures += not check("success is False, so the caller reports a failure", result.success is False, result.success)
0482
0483 print("\n=== the tri-state is honoured by every check_target return path ===")
0484
0485 for label, setup, expected in [
0486 ("step not in a checkable status", lambda st, tb: setattr(st, "status", WFStepStatus.pending), None),
0487
0488 ("wrong flavor for this handler", lambda st, tb: setattr(st, "flavor", "something_else"), False),
0489 ("target not submitted yet", lambda st, tb: setattr(st, "target_id", None), None),
0490 ("unrecognised native status", lambda st, tb: tb.set_status("nonsense_status"), False),
0491 ]:
0492 tbif = make_tbif(deft_status="waiting")
0493 tbif.set_status("running")
0494 step = make_step()
0495 step.status = WFStepStatus.running
0496 step.target_id = "49900001"
0497 setup(step, tbif)
0498 got = PandaTaskStepHandler(tbif).check_target(step).success
0499 failures += not check(f"{label} -> success {expected}", got is expected, f"got {got}")
0500
0501 print("\n=== a flavor mismatch fails loudly on every entry point ===")
0502 for name in ("submit_target", "check_target", "cancel_target", "on_all_inputs_done"):
0503 del LOGGED["warning"][:], LOGGED["error"][:]
0504 tbif = make_tbif()
0505 tbif.set_status("running")
0506 step = make_step()
0507 step.status = WFStepStatus.running
0508 step.target_id = "49900001"
0509 step.flavor = "something_else"
0510 result = getattr(PandaTaskStepHandler(tbif), name)(step)
0511 failures += not check(f"{name} logs an error, not a warning", LOGGED["error"] and not LOGGED["warning"], (LOGGED["warning"], LOGGED["error"]))
0512 failures += not check(f"{name} names the wrong handler", any("wrong step handler" in m for m in LOGGED["error"]), LOGGED["error"])
0513 if result is not None:
0514 failures += not check(f"{name} reports success False", result.success is False, result.success)
0515 failures += not check(f"{name} did nothing", tbif.inserted == [] and tbif.updated_data == [])
0516
0517 print("\n=== ${PARENT_TASKID}: resolved from the step that produced the input ===")
0518
0519 def make_parent_step(parent_tid_value, noWaitParent=True, inputs=("merge_evnt/EVNT",)):
0520 params = copy.deepcopy(simul_params)
0521 params["parent_tid"] = parent_tid_value
0522 if noWaitParent:
0523 params["noWaitParent"] = True
0524 else:
0525 params.pop("noWaitParent", None)
0526 step = make_step(params=params)
0527 definition = step.definition_json_map
0528 definition["input_data_dict"] = {name: {} for name in inputs}
0529 step.definition_json = json.dumps(definition)
0530 return step
0531
0532
0533 def parent_tbif(**kw):
0534 data = {
0535 "merge_evnt/EVNT": FakeData("merge_evnt/EVNT", produced_evnt, source_step_id=3),
0536 "simul/HITS": FakeData("simul/HITS", f"...tid{TASKID_PLACEHOLDER}_00"),
0537 }
0538 data.update(kw.pop("extra_data", {}))
0539 feeding = FakeStep({}, None)
0540 feeding.step_id, feeding.name = 3, "merge_evnt"
0541 return FakeTaskBuffer(data_by_name=data, steps_by_id={3: feeding}, **kw)
0542
0543 tbif = parent_tbif()
0544 tbif.steps[3].target_id = "48810699"
0545 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step(PARENT_TASKID_PLACEHOLDER))
0546 failures += not check("the task is submitted", res.success is True, res.message)
0547 failures += not check("with the producing step's task as parent", tbif.inserted_parent_tids == [48810699], tbif.inserted_parent_tids)
0548 failures += not check("and parent_tid is taken out of the task params", "parent_tid" not in tbif.inserted[0], sorted(tbif.inserted[0])[:5])
0549
0550 print("\n=== a step with no parent inside the workflow ===")
0551 tbif = parent_tbif(extra_data={"rdo_bkg": FakeData("rdo_bkg", "external.dataset", source_step_id=None)})
0552 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step(PARENT_TASKID_PLACEHOLDER, inputs=("rdo_bkg",)))
0553 failures += not check("is submitted", res.success is True, res.message)
0554 failures += not check("with no parent, so JEDI makes the task its own", tbif.inserted_parent_tids == [None], tbif.inserted_parent_tids)
0555
0556 print("\n=== a step that does not ask for it is unaffected ===")
0557 tbif = parent_tbif()
0558 res = PandaTaskStepHandler(tbif).submit_target(make_step())
0559 failures += not check("submitted with no parent", res.success is True and tbif.inserted_parent_tids == [None], tbif.inserted_parent_tids)
0560
0561 print("\n=== a literal task ID is passed through ===")
0562 tbif = parent_tbif()
0563 tbif.steps[3].target_id = "48810699"
0564 PandaTaskStepHandler(tbif).submit_target(make_parent_step(52397622))
0565 failures += not check("as given, without consulting the graph", tbif.inserted_parent_tids == [52397622], tbif.inserted_parent_tids)
0566
0567 print("\n=== a joining step is refused rather than guessed at ===")
0568 tbif = parent_tbif(
0569 extra_data={
0570 "left/OUT": FakeData("left/OUT", "a", source_step_id=3),
0571 "right/OUT": FakeData("right/OUT", "b", source_step_id=4),
0572 }
0573 )
0574 tbif.steps[3].target_id = "52000003"
0575 tbif.steps[4] = FakeStep({}, None)
0576 tbif.steps[4].step_id, tbif.steps[4].name = 4, "right"
0577 tbif.steps[4].target_id = "52000004"
0578 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step(PARENT_TASKID_PLACEHOLDER, inputs=("left/OUT", "right/OUT")))
0579 failures += not check("not submitted", res.success is not True, res.success)
0580 failures += not check("and says it is ambiguous", "ambiguous" in res.message, res.message)
0581 failures += not check("and offers the named form", "PARENT_TASKID:<step name>" in res.message, res.message)
0582 failures += not check("nothing was inserted", tbif.inserted == [], tbif.inserted)
0583
0584 print("\n=== a value that is neither is refused ===")
0585 tbif = parent_tbif()
0586 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step("not-a-task"))
0587 failures += not check("not submitted", res.success is not True, res.success)
0588 failures += not check("and says why", "neither" in res.message, res.message)
0589
0590 print("\n=== a parent whose step has not submitted is an inconsistency ===")
0591 tbif = parent_tbif()
0592 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step(PARENT_TASKID_PLACEHOLDER))
0593 failures += not check("not submitted", res.success is not True, res.success)
0594 failures += not check("and says the parent has no task", "no task yet" in res.message, res.message)
0595
0596 print("\n=== setting a parent without noWaitParent is warned about ===")
0597 del LOGGED["warning"][:]
0598 tbif = parent_tbif()
0599 tbif.steps[3].target_id = "48810699"
0600 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step(PARENT_TASKID_PLACEHOLDER, noWaitParent=False))
0601 failures += not check("still submitted", res.success is True, res.message)
0602 failures += not check("with a warning naming noWaitParent", any("noWaitParent" in m for m in LOGGED["warning"]), LOGGED["warning"])
0603
0604 print("\n=== ${PARENT_TASKID:<step name>} on a joining step ===")
0605
0606 def joining_tbif():
0607 data = {
0608 "left/OUT": FakeData("left/OUT", "a", source_step_id=3),
0609 "right/OUT": FakeData("right/OUT", "b", source_step_id=4),
0610
0611
0612 "merge_evnt/EVNT": FakeData("merge_evnt/EVNT", produced_evnt, source_step_id=None),
0613 "simul/HITS": FakeData("simul/HITS", f"...tid{TASKID_PLACEHOLDER}_00"),
0614 }
0615 left, right = FakeStep({}, None), FakeStep({}, None)
0616 left.name, left.step_id, left.target_id = "left", 3, "52000003"
0617 right.name, right.step_id, right.target_id = "right", 4, "52000004"
0618 return FakeTaskBuffer(data_by_name=data, steps_by_id={3: left, 4: right})
0619
0620
0621 for named, expected_task in (("left", 52000003), ("right", 52000004)):
0622 tbif = joining_tbif()
0623 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step(f"${{PARENT_TASKID:{named}}}", inputs=("left/OUT", "right/OUT")))
0624 failures += not check(f"naming {named} submits", res.success is True, res.message)
0625 failures += not check(f"...with its task {expected_task}", tbif.inserted_parent_tids == [expected_task], tbif.inserted_parent_tids)
0626
0627 print("\n=== naming a step that does not feed this one ===")
0628 tbif = joining_tbif()
0629 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step("${PARENT_TASKID:elsewhere}", inputs=("left/OUT", "right/OUT")))
0630 failures += not check("is refused", res.success is not True, res.success)
0631 failures += not check("naming what does feed it", "left" in res.message and "right" in res.message, res.message)
0632 failures += not check("nothing inserted", tbif.inserted == [], tbif.inserted)
0633
0634 print("\n=== the bare form on a join now points at the named form ===")
0635 tbif = joining_tbif()
0636 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step(PARENT_TASKID_PLACEHOLDER, inputs=("left/OUT", "right/OUT")))
0637 failures += not check("still refused", res.success is not True, res.success)
0638 failures += not check("and says how to disambiguate", "PARENT_TASKID:<step name>" in res.message, res.message)
0639
0640 print("\n=== naming the only feeding step is the same as the bare form ===")
0641 tbif = parent_tbif()
0642 tbif.steps[3].target_id = "48810699"
0643 res = PandaTaskStepHandler(tbif).submit_target(make_parent_step("${PARENT_TASKID:merge_evnt}"))
0644 failures += not check("submits with that task", res.success is True and tbif.inserted_parent_tids == [48810699], tbif.inserted_parent_tids)
0645
0646 print("\n=== check_target status mapping ===")
0647 expectations = {
0648 WFStepStatus.running: ["running", "scouting", "scouted", "throttled", "prepared", "finishing", "passed", "merging", "toretry", "toincexec", "paused"],
0649 WFStepStatus.starting: [
0650 "registered",
0651 "defined",
0652 "assigned",
0653 "activated",
0654 "starting",
0655 "ready",
0656 "topreprocess",
0657 "preprocessing",
0658 "staging",
0659 "staged",
0660 "rerefine",
0661 ],
0662 WFStepStatus.done: ["done", "finished"],
0663 WFStepStatus.failed: ["failed", "exhausted", "aborted", "toabort", "aborting", "broken", "tobroken"],
0664 }
0665 for expected_status, statuses in expectations.items():
0666 for status in statuses:
0667 tbif = make_tbif()
0668 tbif.set_status(status)
0669 handler = PandaTaskStepHandler(tbif)
0670 step = make_step()
0671 step.status = WFStepStatus.running
0672 step.target_id = "49900001"
0673 result = handler.check_target(step)
0674 if not (result.success and result.step_status == expected_status):
0675 failures += not check(f"{status} -> {expected_status}", False, f"got success={result.success} status={result.step_status}")
0676 failures += not check(f"all {sum(len(v) for v in expectations.values())} task statuses map without error", True)
0677
0678 tbif = make_tbif()
0679 tbif.set_status("nonsense_status")
0680 handler = PandaTaskStepHandler(tbif)
0681 step = make_step()
0682 step.status = WFStepStatus.running
0683 step.target_id = "49900001"
0684 result = handler.check_target(step)
0685 failures += not check("an unknown status is still an error", result.success is False and "unknown" in result.message, result.message)
0686
0687 print(f"\n{'ALL CHECKS PASSED' if not failures else f'{failures} CHECK(S) FAILED'}")
0688 return 1 if failures else 0
0689
0690
0691 if __name__ == "__main__":
0692 sys.exit(main())