Back to home page

EIC code displayed by LXR

 
 

    


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 # the handler derives a submitter's compact name through CoreUtils, which needs only this
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 (  # noqa: E402
0092     PandaTaskStepHandler,
0093 )
0094 from pandaserver.workflow.workflow_base import (  # noqa: E402
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         # which step produced it, which is what ${PARENT_TASKID} resolves through
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         # definition_json and parameters are the real backing attributes, so the spec's own
0121         # definition_json_map and get/set_parameter do the work and are exercised as written
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         # a real lookup returns None when it failed, which is not the same as finding nothing
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         # a queued task is findable by name from then on, as the real lookup would find it
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         # a real getTaskStatusSuperstatus returns a falsy value when the task is not in JEDI
0186         self._status = None if status is None else (status, superstatus or status)
0187 
0188     def getTaskWithID_JEDI(self, task_id, *args, **kwargs):
0189         # mirrors the real signature: (found, task_spec); None when the task is not in JEDI yet
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     # the simul step consumes {merge_evnt/EVNT} and produces one HITS dataset
0202     simul_params = copy.deepcopy(wfd["steps"]["simul"]["task_params"])
0203     # resolve ${WFID} the way registration would, so this works whether or not the description uses it
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     # Output dataset names still carry ${TASKID} at this point on purpose: the ID does not exist
0239     # until the insert allocates it, so insert_step_task resolves it inside the same transaction.
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     # only the task-level production label is guarded; see PRODUCTION_SOURCE_LABELS
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         # ... and is accepted once the submitter holds it
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     # non-production task labels need no role. "test" is included deliberately: a JEDI instance may
0274     # route it to the production refiner, but which task labels it accepts is its own configuration,
0275     # so the server does not gate it. "prod_test" is a job-level label and never reaches here.
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     # panda-client sends no userName, taskType or taskPriority, because insertTaskParamsPanda has
0286     # always written them from the submitter's credentials. This is the shape of a pchain_native
0287     # analysis step, trimmed to what the submission path reads.
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     # a definition written before user_name was carried on the step still has the DN to derive from
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     # the production chain authors userName, taskType and taskPriority itself
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     # ... and without the role the submitter's own identity is written over it, as
0336     # insertTaskParamsPanda does, so a workflow cannot submit work under another name. The label is
0337     # relabelled first because "managed" is refused outright without the role.
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     # without an official dataset there is no group to claim
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     # The attempt was recorded before the insert was called, and the insert refused it before
0377     # reaching the database, so no task exists and the step must not be stranded.
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     # the task really was queued: refuse, and name the one that exists
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     # unknown is not the same as absent, so a failed lookup refuses too
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     # a step attempting a different name than the one recorded is not the duplicate case at all
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     # JEDI has no row yet, but DEFT does: expected for up to a refiner cycle, so this must be a
0451     # warning naming the situation, not an error suggesting the task was lost.
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     # the tri-state contract: None means "not checkable yet", which the caller treats as a wait
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     # None for the cases that are waits or skips, False only for genuine failures
0485     for label, setup, expected in [
0486         ("step not in a checkable status", lambda st, tb: setattr(st, "status", WFStepStatus.pending), None),
0487         # a flavor mismatch is a routing bug, not a wait: False so the caller reports it
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     # merge_evnt/EVNT was produced by step 3, whose task is 48810699
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()  # step 3 keeps target_id None
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             # the step's job parameters still reference this one, which input_data_dict does not
0611             # list, so it takes no part in working out the parent
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     # `expected` already names a bool | None earlier in this function
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     # an unknown status must still be reported rather than silently mapped
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())