diff --git a/src/DIRAC/WorkloadManagementSystem/FutureClient/JobMonitoringClient.py b/src/DIRAC/WorkloadManagementSystem/FutureClient/JobMonitoringClient.py index e08b150954b..eb6deff55a3 100644 --- a/src/DIRAC/WorkloadManagementSystem/FutureClient/JobMonitoringClient.py +++ b/src/DIRAC/WorkloadManagementSystem/FutureClient/JobMonitoringClient.py @@ -1,25 +1,266 @@ -from DIRAC.Core.Utilities.ReturnValues import convertToReturnValue +from collections import defaultdict +from datetime import datetime +from DIRAC import gConfig from DIRAC.Core.Security.DiracX import DiracXClient +from DIRAC.Core.Utilities.ReturnValues import convertToReturnValue + + +DATETIME_PARAMETERS = [ + "EndExecTime", + "HeartBeatTime", + "LastUpdateTime", + "StartExecTime", + "SubmissionTime", + "RescheduleTime", +] + +SUMMARY_WAITING_STATUSES = {"Submitted", "Assigned", "Waiting", "Matched"} +SUMMARY_STATUSES = {"Waiting", "Running", "Stalled", "Done", "Failed"} class JobMonitoringClient: - def fetch(self, parameters, jobIDs): + def _fetch_summary(self, grouping, search=None): + diracxUrl = gConfig.getValue("/DiracX/URL") + if not diracxUrl: + raise ValueError("Missing mandatory /DiracX/URL configuration") + with DiracXClient() as api: + return api.jobs.summary(grouping=grouping, search=search) + + def _fetch_search(self, parameters, jobIDs): + if not isinstance(jobIDs, list): + jobIDs = [jobIDs] + + diracxUrl = gConfig.getValue("/DiracX/URL") + if not diracxUrl: + raise ValueError("Missing mandatory /DiracX/URL configuration") with DiracXClient() as api: jobs = api.jobs.search( - parameters=["JobID"] + parameters, + parameters=(["JobID"] + parameters) if parameters else None, search=[{"parameter": "JobID", "operator": "in", "values": jobIDs}], ) - return {j["JobID"]: {param: j[param] for param in parameters} for j in jobs} + for j in jobs: + for param in j: + if isinstance(j[param], bool): + j[param] = str(j[param]) + elif param in DATETIME_PARAMETERS and j[param] is not None: + j[param] = datetime.strptime(j[param], "%Y-%m-%dT%H:%M:%S") + if parameters is None: + return {j["JobID"]: j for j in jobs} + else: + return {j["JobID"]: {param: j[param] for param in parameters} for j in jobs} + + def _fetch_search_scalar(self, jobID, key): + result = self._fetch_search([key], jobID) + return result.get(jobID, {key: None})[key] + + def _dict_to_search(self, condDict, older, newer): + search = [ + {"parameter": k, "operator": "in", "values": v} + if isinstance(v, list) + else {"parameter": k, "operator": "eq", "value": v} + for k, v in (condDict or {}).items() + ] + if older: + search += [{"parameter": "LastUpdateTime", "operator": "lt", "value": older}] + if newer: + search += [ + # TODO: gte + {"parameter": "LastUpdateTime", "operator": "gt", "value": newer} + # {"parameter": "LastUpdateTime", "operator": "gte", "value": older} + ] + return search + + def _fetch_distinct_values(self, key, condDict, older, newer): + search = self._dict_to_search(condDict, older, newer) + # TODO: We should add the option to avoid the counting + result = self._fetch_summary([key], search=search) + # Apply the expected sort order + result = sorted([x[key] for x in result]) + if "Unknown" in result: + result.remove("Unknown") + result.append("Unknown") + return result @convertToReturnValue - def getJobsMinorStatus(self, jobIDs): - return self.fetch(["MinorStatus"], jobIDs) + def getApplicationStates(self, condDict=None, older=None, newer=None): + return self._fetch_distinct_values("ApplicationStatus", condDict, older, newer) + + @convertToReturnValue + def getJobTypes(self, condDict=None, older=None, newer=None): + return self._fetch_distinct_values("JobType", condDict, older, newer) + + @convertToReturnValue + def getOwners(self, condDict=None, older=None, newer=None): + return self._fetch_distinct_values("Owner", condDict, older, newer) + + # return self.getDistinctAttributeValues( + # "Jobs", "Owner", condDict=condDict, older=older, newer=newer, timeStamp="LastUpdateTime" + # ) + # def getDistinctAttributeValues( + # self, + # table, + # attribute, + # condDict=None, + # older=None, + # newer=None, + # timeStamp=None, + # connection=False, + # greater=None, + # smaller=None, + # ): + # """ + # Get distinct values of a table attribute under specified conditions + # """ + # try: + # cond = self.buildCondition( + # condDict=condDict, older=older, newer=newer, timeStamp=timeStamp, greater=greater, smaller=smaller + # ) + # except Exception as exc: + # return S_ERROR(DErrno.EMYSQL, exc) + + # cmd = f"SELECT DISTINCT( {attributeName} ) FROM {table} {cond} ORDER BY {attributeName}" + # res = self._query(cmd, conn=connection) + # if not res["OK"]: + # return res + # attr_list = [x[0] for x in res["Value"]] + # return S_OK(attr_list) + + @convertToReturnValue + def getOwnerGroup(self): + result = self._fetch_summary(["OwnerGroup"]) + return [x["OwnerGroup"] for x in result] + + @convertToReturnValue + def getJobGroups(self, condDict=None, older=None, cutDate=None): + return self._fetch_distinct_values("JobGroup", condDict, older, cutDate) + + @convertToReturnValue + def getSites(self, condDict=None, older=None, newer=None): + return self._fetch_distinct_values("Site", condDict, older, newer) + + @convertToReturnValue + def getStates(self, condDict=None, older=None, newer=None): + return self._fetch_distinct_values("Status", condDict, older, newer) + + @convertToReturnValue + def getMinorStates(self, condDict=None, older=None, newer=None): + return self._fetch_distinct_values("MinorStatus", condDict, older, newer) + + # @convertToReturnValue + # def getJobs(self, attrDict=None, cutDate=None): + + @convertToReturnValue + def getCounters(self, attrList, attrDict=None, cutDate=""): + if not attrList: + raise ValueError("Missing mandatory attrList") + if cutDate is None: + raise ValueError('cutDate must be "" or a valid date') + search = self._dict_to_search(attrDict, None, cutDate) + # TODO: We should add the option to avoid the counting + result = self._fetch_summary(attrList, search=search) + result = [[k, k.pop("count")] for k in result] + # Apply the expected sort order + return sorted(result, key=lambda x: tuple(x[0].values())) + + # _, _, attrDict = cls.parseSelectors(attrDict) + # return cls.jobDB.getCounters("Jobs", attrList, attrDict, newer=str(cutDate), timeStamp="LastUpdateTime")\ + # cmd = f"SELECT {attrNames}, COUNT(*) FROM {table} {cond} GROUP BY {attrNames} ORDER BY {attrNames}" + + @convertToReturnValue + def getJobOwner(self, jobID): + return self._fetch_search_scalar(jobID, "Owner") + + @convertToReturnValue + def getJobSite(self, jobID): + return self._fetch_search_scalar(jobID, "Site") + + @convertToReturnValue + def getJobJDL(self, jobID, original): + return self._fetch_search_scalar(jobID, "OriginalJDL" if original else "JDL") + + # @convertToReturnValue + # def getJobLoggingInfo(self, jobID): + + # @convertToReturnValue + # def getJobsParameters(self, jobIDs, parameters): @convertToReturnValue def getJobsStates(self, jobIDs): - return self.fetch(["Status", "MinorStatus", "ApplicationStatus"], jobIDs) + return self._fetch_search(["Status", "MinorStatus", "ApplicationStatus"], jobIDs) + + @convertToReturnValue + def getJobsStatus(self, jobIDs): + return self._fetch_search(["Status"], jobIDs) + + @convertToReturnValue + def getJobsMinorStatus(self, jobIDs): + return self._fetch_search(["MinorStatus"], jobIDs) + + @convertToReturnValue + def getJobsApplicationStatus(self, jobIDs): + return self._fetch_search(["ApplicationStatus"], jobIDs) @convertToReturnValue def getJobsSites(self, jobIDs): - return self.fetch(["Site"], jobIDs) + return self._fetch_search(["Site"], jobIDs) + + # @convertToReturnValue + # def getJobSummary(self, jobID): + + @convertToReturnValue + def getJobsSummary(self, jobIDs): + return {str(k): v for k, v in self._fetch_search(None, jobIDs).items()} + + # @convertToReturnValue + # def getJobPageSummaryWeb(self, selectDict, sortList, startItem, maxItems, selectJobs=True): + + # @convertToReturnValue + # def getJobStats(self, attribute, selectDict): + + # @convertToReturnValue + # def getJobParameter(self, jobID, parName): + + # @convertToReturnValue + # def getJobOptParameters(self, jobID): + + # @convertToReturnValue + # def getJobParameters(self, jobIDs, parName=None): + + # @convertToReturnValue + # def getAtticJobParameters(self, jobID, parameters=None, rescheduleCycle=-1): + + @convertToReturnValue + def getJobAttributes(self, jobID, attrList=None): + return self._fetch_search(attrList, [jobID]).get(jobID, {}) + + @convertToReturnValue + def getJobAttribute(self, jobID, attribute): + return self._fetch_search_scalar(jobID, attribute) + + @convertToReturnValue + def getSiteSummary(self): + summary = self._fetch_summary(["Status", "Site"]) + + result = defaultdict(lambda: {k: 0 for k in SUMMARY_STATUSES}) + for s in summary: + if s["Site"] == "ANY": + continue + if s["Status"] in SUMMARY_WAITING_STATUSES: + result[s["Site"]]["Waiting"] += s["count"] + elif s["Status"] in SUMMARY_STATUSES: + result[s["Site"]][s["Status"]] += s["count"] + else: + # Make sure that sites are included even if they have no jobs in a reported status + result[s["Site"]] + + for status in result["Total"]: + result["Total"][status] = sum(result[site][status] for site in result if site != "Total") + + return dict(result) + + # @convertToReturnValue + # def getJobHeartBeatData(self, jobID): + + # @convertToReturnValue + # def getInputData(self, jobID): diff --git a/tests/Integration/FutureClient/WorkloadManagement/Test_JobMonitoring.py b/tests/Integration/FutureClient/WorkloadManagement/Test_JobMonitoring.py index 3014534ad11..a841da1e5ad 100644 --- a/tests/Integration/FutureClient/WorkloadManagement/Test_JobMonitoring.py +++ b/tests/Integration/FutureClient/WorkloadManagement/Test_JobMonitoring.py @@ -8,208 +8,279 @@ from DIRAC.WorkloadManagementSystem.Client.JobMonitoringClient import JobMonitoringClient from ..utils import compare_results +MISSING_JOB_ID = 7 TEST_JOBS = [7470, 7471, 7469] -TEST_JOB_IDS = [TEST_JOBS] + TEST_JOBS + [str(x) for x in TEST_JOBS] +TEST_JOB_IDS = [TEST_JOBS] + TEST_JOBS + [str(TEST_JOBS[0]), MISSING_JOB_ID, TEST_JOBS + [MISSING_JOB_ID]] -def test_getApplicationStates(): +@pytest.mark.parametrize( + "condDict", [{}, None, {"Owner": "chaen"}, {"Owner": "chaen,cburr"}, {"Owner": ["chaen", "cburr"]}] +) +@pytest.mark.parametrize("older", [None, "2023-09-01", "2023-09-01 00:00", "2023-09-01 00:00:00"]) +@pytest.mark.parametrize("newer", [None, "2023-09-01", "2023-09-01 00:00:00"]) +def test_getApplicationStates(monkeypatch, condDict, older, newer): # JobMonitoringClient().getApplicationStates(condDict = None, older = None, newer = None) method = JobMonitoringClient().getApplicationStates - pytest.skip() + compare_results(monkeypatch, partial(method, condDict, older, newer)) -def test_getAtticJobParameters(): +def test_getAtticJobParameters(monkeypatch): # JobMonitoringClient().getAtticJobParameters(jobID: int, parameters = None, rescheduleCycle = -1) method = JobMonitoringClient().getAtticJobParameters pytest.skip() -def test_getCounters(): - # JobMonitoringClient().getCounters(attrList: list, attrDict = None, cutDate = ) +@pytest.mark.parametrize( + "attrList", + [ + # [], + # ["Owner"], + # ["Status"], + # ["Owner", "Status"], + ["Owner", "Status", "BadAttribute"], + ["BadAttribute"], + ], +) +@pytest.mark.parametrize( + "attrDict", [{}, None, {"Owner": "chaen"}, {"Owner": "chaen,cburr"}, {"Owner": ["chaen", "cburr"]}] +) +@pytest.mark.parametrize("cutDate", [None, "", "2023-09-01", "2023-09-01 00:00:00"]) +def test_getCounters(monkeypatch, attrList, attrDict, cutDate): + # JobMonitoringClient().getCounters(attrList: list, attrDict = None, cutDate = "") method = JobMonitoringClient().getCounters - pytest.skip() + compare_results(monkeypatch, partial(method, attrList, attrDict, cutDate)) -def test_getInputData(): +def test_getInputData(monkeypatch): # JobMonitoringClient().getInputData(jobID: int) method = JobMonitoringClient().getInputData pytest.skip() -def test_getJobAttribute(): +@pytest.mark.parametrize("jobID", [TEST_JOBS[0], MISSING_JOB_ID]) +@pytest.mark.parametrize("attribute", ["JobID", "Status", "Owner", "BadAttribute"]) +def test_getJobAttribute(monkeypatch, jobID, attribute): # JobMonitoringClient().getJobAttribute(jobID: int, attribute: str) method = JobMonitoringClient().getJobAttribute - pytest.skip() - - -def test_getJobAttributes(): + compare_results(monkeypatch, partial(method, jobID, attribute)) + + +@pytest.mark.parametrize("jobID", [TEST_JOBS[0], MISSING_JOB_ID]) +@pytest.mark.parametrize( + "attrList", + [ + ["JobID", "Status", "Owner", "BadAttribute"], + ["Status"], + ["BadAttribute"], + None, + ], +) +def test_getJobAttributes(monkeypatch, jobID, attrList): # JobMonitoringClient().getJobAttributes(jobID: int, attrList = None) method = JobMonitoringClient().getJobAttributes - pytest.skip() + compare_results(monkeypatch, partial(method, jobID, attrList)) -def test_getJobGroups(): +@pytest.mark.parametrize( + "condDict", [{}, None, {"Owner": "chaen"}, {"Owner": "chaen,cburr"}, {"Owner": ["chaen", "cburr"]}] +) +@pytest.mark.parametrize("older", [None, "2023-09-01", "2023-09-01 00:00:00"]) +@pytest.mark.parametrize("cutDate", [None, "2023-09-01", "2023-09-01 00:00:00"]) +def test_getJobGroups(monkeypatch, condDict, older, cutDate): # JobMonitoringClient().getJobGroups(condDict = None, older = None, cutDate = None) method = JobMonitoringClient().getJobGroups - pytest.skip() + compare_results(monkeypatch, partial(method, condDict, older, cutDate)) -def test_getJobHeartBeatData(): +@pytest.mark.parametrize("jobID", [TEST_JOBS[0], MISSING_JOB_ID]) +def test_getJobHeartBeatData(monkeypatch, jobID): # JobMonitoringClient().getJobHeartBeatData(jobID: int) - method = JobMonitoringClient().getJobHeartBeatData pytest.skip() + method = JobMonitoringClient().getJobHeartBeatData + compare_results(monkeypatch, partial(method, jobID)) -def test_getJobJDL(): +@pytest.mark.parametrize("jobID", [TEST_JOBS[0], MISSING_JOB_ID]) +@pytest.mark.parametrize("original", [True, False]) +def test_getJobJDL(monkeypatch, jobID, original): # JobMonitoringClient().getJobJDL(jobID: int, original: bool) - method = JobMonitoringClient().getJobJDL pytest.skip() + method = JobMonitoringClient().getJobJDL + compare_results(monkeypatch, partial(method, jobID, original)) -def test_getJobLoggingInfo(): +def test_getJobLoggingInfo(monkeypatch): # JobMonitoringClient().getJobLoggingInfo(jobID: int) method = JobMonitoringClient().getJobLoggingInfo pytest.skip() -def test_getJobOptParameters(): +def test_getJobOptParameters(monkeypatch): # JobMonitoringClient().getJobOptParameters(jobID: int) method = JobMonitoringClient().getJobOptParameters pytest.skip() -def test_getJobOwner(): +@pytest.mark.parametrize("jobID", [TEST_JOBS[0], MISSING_JOB_ID]) +def test_getJobOwner(monkeypatch, jobID): # JobMonitoringClient().getJobOwner(jobID: int) method = JobMonitoringClient().getJobOwner - pytest.skip() + compare_results(monkeypatch, partial(method, jobID)) -def test_getJobPageSummaryWeb(): +def test_getJobPageSummaryWeb(monkeypatch): # JobMonitoringClient().getJobPageSummaryWeb(self: dict, selectDict: list, sortList: int, startItem: int, maxItems, selectJobs = True) method = JobMonitoringClient().getJobPageSummaryWeb pytest.skip() -def test_getJobParameter(): +def test_getJobParameter(monkeypatch): # JobMonitoringClient().getJobParameter(jobID: str | int, parName: str) method = JobMonitoringClient().getJobParameter pytest.skip() -def test_getJobParameters(): +def test_getJobParameters(monkeypatch): # JobMonitoringClient().getJobParameters(jobIDs: str | int | list, parName = None) method = JobMonitoringClient().getJobParameters pytest.skip() -def test_getJobSite(): +@pytest.mark.parametrize("jobID", [TEST_JOBS[0], MISSING_JOB_ID]) +def test_getJobSite(monkeypatch, jobID): # JobMonitoringClient().getJobSite(jobID: int) method = JobMonitoringClient().getJobSite - pytest.skip() + compare_results(monkeypatch, partial(method, jobID)) -def test_getJobStats(): +def test_getJobStats(monkeypatch): # JobMonitoringClient().getJobStats(attribute: str, selectDict: dict) method = JobMonitoringClient().getJobStats pytest.skip() -def test_getJobSummary(): +def test_getJobSummary(monkeypatch): # JobMonitoringClient().getJobSummary(jobID: int) method = JobMonitoringClient().getJobSummary pytest.skip() -def test_getJobTypes(): +@pytest.mark.parametrize( + "condDict", [{}, None, {"Owner": "chaen"}, {"Owner": "chaen,cburr"}, {"Owner": ["chaen", "cburr"]}] +) +@pytest.mark.parametrize("older", [None, "2023-09-01", "2023-09-01 00:00:00"]) +@pytest.mark.parametrize("newer", [None, "2023-09-01", "2023-09-01 00:00:00"]) +def test_getJobTypes(monkeypatch, condDict, older, newer): # JobMonitoringClient().getJobTypes(condDict = None, older = None, newer = None) method = JobMonitoringClient().getJobTypes - pytest.skip() + compare_results(monkeypatch, partial(method, condDict, older, newer)) -def test_getJobs(): +def test_getJobs(monkeypatch): # JobMonitoringClient().getJobs(attrDict = None, cutDate = None) method = JobMonitoringClient().getJobs pytest.skip() @pytest.mark.parametrize("jobIDs", TEST_JOB_IDS) -def test_getJobsApplicationStatus(jobIDs): +def test_getJobsApplicationStatus(monkeypatch, jobIDs): # JobMonitoringClient().getJobsApplicationStatus(jobIDs: str | int | list) method = JobMonitoringClient().getJobsApplicationStatus - compare_results(partial(method, jobIDs)) + compare_results(monkeypatch, partial(method, jobIDs)) @pytest.mark.parametrize("jobIDs", TEST_JOB_IDS) -def test_getJobsMinorStatus(jobIDs): +def test_getJobsMinorStatus(monkeypatch, jobIDs): # JobMonitoringClient().getJobsMinorStatus(jobIDs: str | int | list) method = JobMonitoringClient().getJobsMinorStatus - compare_results(partial(method, jobIDs)) + compare_results(monkeypatch, partial(method, jobIDs)) -def test_getJobsParameters(): +def test_getJobsParameters(monkeypatch): # JobMonitoringClient().getJobsParameters(jobIDs: str | int | list, parameters: list) method = JobMonitoringClient().getJobsParameters pytest.skip() @pytest.mark.parametrize("jobIDs", TEST_JOB_IDS) -def test_getJobsSites(jobIDs): +def test_getJobsSites(monkeypatch, jobIDs): # JobMonitoringClient().getJobsSites(jobIDs: str | int | list) method = JobMonitoringClient().getJobsSites - compare_results(partial(method, jobIDs)) + compare_results(monkeypatch, partial(method, jobIDs)) @pytest.mark.parametrize("jobIDs", TEST_JOB_IDS) -def test_getJobsStates(jobIDs): +def test_getJobsStates(monkeypatch, jobIDs): # JobMonitoringClient().getJobsStates(jobIDs: str | int | list) method = JobMonitoringClient().getJobsStates - compare_results(partial(method, jobIDs)) + compare_results(monkeypatch, partial(method, jobIDs)) @pytest.mark.parametrize("jobIDs", TEST_JOB_IDS) -def test_getJobsStatus(jobIDs): +def test_getJobsStatus(monkeypatch, jobIDs): # JobMonitoringClient().getJobsStatus(jobIDs: str | int | list) method = JobMonitoringClient().getJobsStatus - compare_results(partial(method, jobIDs)) + compare_results(monkeypatch, partial(method, jobIDs)) -def test_getJobsSummary(): +def test_getJobsSummary(monkeypatch): # JobMonitoringClient().getJobsSummary(jobIDs: list) method = JobMonitoringClient().getJobsSummary - pytest.skip() + # TODO: Handle missing case + compare_results(monkeypatch, partial(method, TEST_JOBS)) -def test_getMinorStates(): +@pytest.mark.parametrize( + "condDict", [{}, None, {"Owner": "chaen"}, {"Owner": "chaen,cburr"}, {"Owner": ["chaen", "cburr"]}] +) +@pytest.mark.parametrize("older", [None, "2023-09-01", "2023-09-01 00:00:00"]) +@pytest.mark.parametrize("newer", [None, "2023-09-01", "2023-09-01 00:00:00"]) +def test_getMinorStates(monkeypatch, condDict, older, newer): # JobMonitoringClient().getMinorStates(condDict = None, older = None, newer = None) method = JobMonitoringClient().getMinorStates - pytest.skip() + compare_results(monkeypatch, partial(method, condDict, older, newer)) -def test_getOwnerGroup(): +def test_getOwnerGroup(monkeypatch): # JobMonitoringClient().getOwnerGroup() method = JobMonitoringClient().getOwnerGroup - pytest.skip() + compare_results(monkeypatch, method) -def test_getOwners(): +@pytest.mark.parametrize( + "condDict", [{}, None, {"Owner": "chaen"}, {"Owner": "chaen,cburr"}, {"Owner": ["chaen", "cburr"]}] +) +@pytest.mark.parametrize("older", [None, "2023-09-01", "2023-09-01 00:00:00"]) +@pytest.mark.parametrize("newer", [None, "2023-09-01", "2023-09-01 00:00:00"]) +def test_getOwners(monkeypatch, condDict, older, newer): # JobMonitoringClient().getOwners(condDict = None, older = None, newer = None) method = JobMonitoringClient().getOwners - pytest.skip() + compare_results(monkeypatch, partial(method, condDict, older, newer)) -def test_getSiteSummary(): +def test_getSiteSummary(monkeypatch): # JobMonitoringClient().getSiteSummary() method = JobMonitoringClient().getSiteSummary - pytest.skip() + compare_results(monkeypatch, method) -def test_getSites(): +@pytest.mark.parametrize( + "condDict", [{}, None, {"Owner": "chaen"}, {"Owner": "chaen,cburr"}, {"Owner": ["chaen", "cburr"]}] +) +@pytest.mark.parametrize("older", [None, "2023-09-01", "2023-09-01 00:00:00"]) +@pytest.mark.parametrize("newer", [None, "2023-09-01", "2023-09-01 00:00:00"]) +def test_getSites(monkeypatch, condDict, older, newer): # JobMonitoringClient().getSites(condDict = None, older = None, newer = None) method = JobMonitoringClient().getSites - pytest.skip() + compare_results(monkeypatch, partial(method, condDict, older, newer)) -def test_getStates(): +@pytest.mark.parametrize( + "condDict", [{}, None, {"Owner": "chaen"}, {"Owner": "chaen,cburr"}, {"Owner": ["chaen", "cburr"]}] +) +@pytest.mark.parametrize("older", [None, "2023-09-01", "2023-09-01 00:00:00"]) +@pytest.mark.parametrize("newer", [None, "2023-09-01", "2023-09-01 00:00:00"]) +def test_getStates(monkeypatch, condDict, older, newer): # JobMonitoringClient().getStates(condDict = None, older = None, newer = None) method = JobMonitoringClient().getStates - pytest.skip() + compare_results(monkeypatch, partial(method, condDict, older, newer))