Coverage for python/lsst/ctrl/mpexec/reports.py: 67%

70 statements  

« prev     ^ index     » next       coverage.py v7.3.0, created at 2023-08-25 09:44 +0000

1# This file is part of ctrl_mpexec. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ["ExceptionInfo", "ExecutionStatus", "Report", "QuantumReport"] 

25 

26import enum 

27import sys 

28from typing import Any 

29 

30import pydantic 

31from lsst.daf.butler import DataCoordinate, DataId, DataIdValue 

32from lsst.daf.butler._compat import PYDANTIC_V2, _BaseModelCompat 

33from lsst.utils.introspection import get_full_type_name 

34 

35 

36def _serializeDataId(dataId: DataId) -> dict[str, DataIdValue]: 

37 if isinstance(dataId, DataCoordinate): 

38 return dataId.byName() 

39 else: 

40 return dataId # type: ignore 

41 

42 

43class ExecutionStatus(enum.Enum): 

44 """Possible values for job execution status. 

45 

46 Status `FAILURE` is set if one or more tasks failed. Status `TIMEOUT` is 

47 set if there are no failures but one or more tasks timed out. Timeouts can 

48 only be detected in multi-process mode, child task is killed on timeout 

49 and usually should have non-zero exit code. 

50 """ 

51 

52 SUCCESS = "success" 

53 FAILURE = "failure" 

54 TIMEOUT = "timeout" 

55 SKIPPED = "skipped" 

56 

57 

58class ExceptionInfo(_BaseModelCompat): 

59 """Information about exception.""" 

60 

61 className: str 

62 """Name of the exception class if exception was raised.""" 

63 

64 message: str 

65 """Exception message for in-process quantum execution, None if 

66 quantum was executed in sub-process. 

67 """ 

68 

69 @classmethod 

70 def from_exception(cls, exception: Exception) -> ExceptionInfo: 

71 """Construct instance from an exception.""" 

72 return cls(className=get_full_type_name(exception), message=str(exception)) 

73 

74 

75class QuantumReport(_BaseModelCompat): 

76 """Task execution report for a single Quantum.""" 

77 

78 status: ExecutionStatus = ExecutionStatus.SUCCESS 

79 """Execution status, one of the values in `ExecutionStatus` enum.""" 

80 

81 dataId: dict[str, DataIdValue] 

82 """Quantum DataId.""" 

83 

84 taskLabel: str | None 

85 """Label for a task executing this Quantum.""" 

86 

87 exitCode: int | None = None 

88 """Exit code for a sub-process executing Quantum, None for in-process 

89 Quantum execution. Negative if process was killed by a signal. 

90 """ 

91 

92 exceptionInfo: ExceptionInfo | None = None 

93 """Exception information if exception was raised.""" 

94 

95 def __init__( 

96 self, 

97 dataId: DataId, 

98 taskLabel: str, 

99 status: ExecutionStatus = ExecutionStatus.SUCCESS, 

100 exitCode: int | None = None, 

101 exceptionInfo: ExceptionInfo | None = None, 

102 ): 

103 super().__init__( 

104 status=status, 

105 dataId=_serializeDataId(dataId), 

106 taskLabel=taskLabel, 

107 exitCode=exitCode, 

108 exceptionInfo=exceptionInfo, 

109 ) 

110 

111 @classmethod 

112 def from_exception( 

113 cls, 

114 exception: Exception, 

115 dataId: DataId, 

116 taskLabel: str, 

117 ) -> QuantumReport: 

118 """Construct report instance from an exception and other pieces of 

119 data. 

120 """ 

121 return cls( 

122 status=ExecutionStatus.FAILURE, 

123 dataId=dataId, 

124 taskLabel=taskLabel, 

125 exceptionInfo=ExceptionInfo.from_exception(exception), 

126 ) 

127 

128 @classmethod 

129 def from_exit_code( 

130 cls, 

131 exitCode: int, 

132 dataId: DataId, 

133 taskLabel: str, 

134 ) -> QuantumReport: 

135 """Construct report instance from an exit code and other pieces of 

136 data. 

137 """ 

138 return cls( 

139 status=ExecutionStatus.SUCCESS if exitCode == 0 else ExecutionStatus.FAILURE, 

140 dataId=dataId, 

141 taskLabel=taskLabel, 

142 exitCode=exitCode, 

143 ) 

144 

145 

146class Report(_BaseModelCompat): 

147 """Execution report for the whole job with one or few quanta.""" 

148 

149 status: ExecutionStatus = ExecutionStatus.SUCCESS 

150 """Job status.""" 

151 

152 cmdLine: list[str] | None = None 

153 """Command line for the whole job.""" 

154 

155 exitCode: int | None = None 

156 """Job exit code, this obviously cannot be set in pipetask.""" 

157 

158 exceptionInfo: ExceptionInfo | None = None 

159 """Exception information if exception was raised.""" 

160 

161 quantaReports: list[QuantumReport] = [] 

162 """List of per-quantum reports, ordering is not specified. Some or all 

163 quanta may not produce a report. 

164 """ 

165 

166 if PYDANTIC_V2: 166 ↛ 169line 166 didn't jump to line 169, because the condition on line 166 was never true

167 # Always want to validate the default value for cmdLine so 

168 # use a model_validator. 

169 @pydantic.model_validator(mode="before") # type: ignore[attr-defined] 

170 @classmethod 

171 def _set_cmdLine(cls, data: Any) -> Any: 

172 if data.get("cmdLine") is None: 

173 data["cmdLine"] = sys.argv 

174 return data 

175 

176 else: 

177 

178 @pydantic.validator("cmdLine", always=True) 

179 def _set_cmdLine(cls, v: list[str] | None) -> list[str]: # noqa: N805 

180 if v is None: 

181 v = sys.argv 

182 return v 

183 

184 def set_exception(self, exception: Exception) -> None: 

185 """Update exception information from an exception object.""" 

186 self.exceptionInfo = ExceptionInfo.from_exception(exception)