11 __doc__ =
"""Checks the output of the jobs specified by their XML configuration file.
13 The jobs must have been submitted by `project.py` and must have completed already.
25 s:
"string to be processed",
26 *suffixes:
"strings to be removed from the end of `s`; better longer first",
27 ) ->
"a copy of `s` with all suffixes removed from its end":
29 for suffix
in suffixes:
30 if not s.endswith(suffix):
continue
39 """On the first call, it creates and returns an object.
40 On next calls, it returns the same object.
42 def __init__(self, fetchProc): self.fetchProc = fetchProc
43 def __call__(self, *args, **kwargs) -> "Returns the cached value.":
45 except AttributeError:
return self.
_fetchValue(*args, **kwargs)
47 def __nonzero__(self) -> "Returns whether the object is cached":
48 return hasattr(self,
"cachedValue")
58 """Class parsing a job ID."""
62 JobIDpattern = re.compile(
r'^([0-9]+)\.([0-9]+)@(.*)$')
67 res = JobIDclass.JobIDpattern.match(jobIDstring)
75 def jobNo(self):
return self._jobNo
77 def server(self):
return self._server
83 def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, self.
jobID())
89 """Iterator: returns one file name at each iteration.
91 The text file is expected to have one file name per line. Lines are stripped
92 (`str.strip()`) of spaces.
93 Empty lines and lines which are a comment are skipped.
94 A comment line is a line whose first non-blank character is a hash character
97 The value returned at each iteration is (lineNo, fileNo, fileName); if
98 `withLineNo` or `withFileNo` are `False`, the respective element is omitted.
99 If only the file name is requested (all other option `False`) the return value
100 is not a tuple but just the file name.
104 listFile:
"text file containing the list" =
None,
105 listName:
"path of the text file containing the list" =
None,
106 withLineNo:
"whether to return also the number of line in file" =
False,
107 withFileNo:
"whether to return also the number of file" =
False,
109 assert listFile
or listName
111 if not listFile: listFile =
open(listName,
'r')
123 if not fileName
or fileName[0] ==
'#':
continue
135 """Performs checks on job output and collects output files.
140 baseName:
"name used for the check (defaults are shaped after it)",
141 goodList:
"name of good job list (None: automatic; False: no list)" =
None,
142 badList:
"name of bad job list (None: automatic; False: no list)" =
None,
143 fileList:
"name of output file list (None: automatic; False: no list)" =
None,
144 skipKnownGoodJobs:
"do not check the jobs already in the good list" =
False,
145 skipKnownBadJobs:
"do not check the jobs already in the bad list" =
False,
151 os.path.splitext(os.path.basename(baseName))[0],
156 = self.
setupList(goodList,
'goodxml', mustExist=skipKnownGoodJobs)
158 = self.
setupList(badList,
'badxml', mustExist=skipKnownBadJobs)
168 def setupList(self, listName, listTag, mustExist = False):
169 """Returns the name of the list, an empty list to be filled and the existing
173 if listName
is False:
174 return None, [], set()
177 listPath = os.path.join \
179 elif not os.path.dirname(listName):
181 else: listPath = listName
183 if os.path.isfile(listPath):
185 (logging.info
if mustExist
else logging.debug) \
186 (
"File list %s contains already %d entries.", listPath, len(listContent))
188 raise RuntimeError(
"File list '{}' ({} list) is required to exist."
189 .
format(listPath, listTag))
190 else: listContent = set()
192 return listPath, [], listContent
197 """Resets all the counters and records."""
206 return self.outputFileListName
is not None
209 XMLfilePath:
"path to the file containing XML job configuration",
210 projectName:
"target the specified project name in the file" =
"",
211 stageName:
"target the specified stage name in the file" =
"",
212 maxJobs:
"if not None, process at most this many jobs" =
None
216 for lineNo, fileName
in FileListIterator(listName=XMLfilePath, withLineNo=
True):
218 if maxJobs
is not None and nJobs >= maxJobs:
219 logging.info(
"Maximum number of jobs checked (%d).", maxJobs)
225 self.
checkJob(fileName, projectName=projectName, stageName=stageName)
226 except KeyboardInterrupt:
raise
227 except Exception
as e:
228 logging.error(
"Error processing job '%s' (file list line %d): %s",
237 XMLfilePath:
"path to the file containing XML job configuration",
238 projectName:
"target the specified project name in the file" =
"",
239 stageName:
"target the specified stage name in the file" =
"",
242 if not os.path.isfile(XMLfilePath):
243 raise RuntimeError(
"Can't open file '{}'.".
format(XMLfilePath))
248 (XMLfilePath, projectName=projectName, stageName=stageName))
250 XMLfileDir, XMLfileName = os.path.split(XMLfilePath)
253 logging.info(
"%s: known as good, check skipped.", XMLfileName)
256 logging.info(
"%s: known as bad, check skipped.", XMLfileName)
263 self.goodList.append(XMLfilePath)
267 self.badList.append(XMLfilePath)
273 jobConfigFile:
"path to the file containing XML job configuration",
274 projectName:
"target the specified project name in the file" =
"",
275 stageName:
"target the specified stage name in the file" =
"",
281 projInfo = project.get_project \
282 (jobConfigFile, projectname=projectName, stagename=stageName)
285 raise RuntimeError(
"Job '{}' does not have project {} stage {}".
format(
286 jobConfigFile, repr(projectName), repr(stageName)
291 next(
filter(
lambda stage: stage.name == stageName, projInfo.stages),
None) \
292 if stageName
else projInfo.stages[0]
295 raise RuntimeError(
"Job '{}' project {} does not have a stage {}".
format(
296 jobConfigFile, repr(project.name), repr(stageName)
306 class JobCheckError(RuntimeError):
pass
309 outputDir = jobInfo.outdir
310 logging.debug(
"Job '%s' output directory: '%s'", jobName, outputDir)
312 if not os.path.isdir(outputDir):
313 raise JobCheckError(
"no output directory present ('{}')".
format(outputDir))
315 if not os.path.exists(os.path.join(outputDir,
'checked')):
316 raise JobCheckError(
"not checked (run `project.py --checkana` first)")
318 for jobID
in map(JobIDclass,
open(os.path.join(outputDir,
'jobids.list'),
'r')):
319 logging.debug("Checking subjob '%s'", jobID)
321 subjobDir = os.path.join(outputDir, jobID.subjobTag())
322 logging.debug(
"Subjob '%s' output directory: '%s'", jobID, subjobDir)
323 if not os.path.isdir(subjobDir):
324 raise JobCheckError(
"job %s missing output directory" % jobID)
326 statusFile = os.path.join(subjobDir,
'larStage0.stat')
327 if not os.path.isfile(statusFile):
328 raise JobCheckError(
"job %s missing status file" % jobID)
331 status = int(
open(statusFile,
'r').readline().strip())
332 except KeyboardInterrupt:
raise
333 except Exception
as e:
334 raise JobCheckError(
"job %s failed reading status file '%s': %s"
335 % (jobID, statusFile, e))
339 raise JobCheckError(
"job %s exited with error code %d" % (jobID, status))
343 expectedOutputFileList = os.path.join(outputDir,
'filesana.list')
344 if not os.path.exists(expectedOutputFileList):
345 raise JobCheckError(
"no output file list ('%s')" % expectedOutputFileList)
348 if len(expectedOutputFiles) == 0:
349 raise JobCheckError(
"job has no output file")
351 foundOutputFiles =
list(
filter(os.path.isfile, expectedOutputFiles))
352 if len(foundOutputFiles) != len(expectedOutputFiles):
353 raise JobCheckError(
"only %d/%d output files still present"
354 % (len(foundOutputFiles), len(expectedOutputFiles)))
357 except JobCheckError
as e:
358 logging.error(
"%s: %s", jobName, e)
361 logging.info(
"%s succeeded.", jobName)
369 outputFileList = os.path.join(jobInfo.outdir,
'filesana.list')
375 def writeList(self, content, fileName, tag) -> "Whether the file list was written":
377 if not fileName:
return False
380 if os.path.exists(fileName):
381 try: os.remove(fileName)
383 logging.warning(
"Could not delete the old %s file list '%s': %s.",
389 if not len(content):
return False
391 listDir = os.path.dirname(fileName)
392 os.makedirs(os.path.normpath(listDir), exist_ok=
True)
394 with
open(fileName,
'w')
as listFile:
396 print(
"# {} file list created on {}: {:d} entries".
format(
397 tag, time.ctime(), len(content)
400 listFile.write(
"\n".
join(content))
404 logging.info(
"File list for %s created as '%s' with %d entries.",
405 tag, fileName, len(content))
419 logging.info(
"None of the %d jobs was successful!!", nJobs)
421 logging.info(
"%d/%d jobs were not successful.", len(self.
badList), nJobs)
425 logging.info(
"All %d jobs were successful.", nJobs)
428 logging.error(
"No jobs checked.")
433 logging.critical(
"Could not write good job file list '%s': %s",
434 self.goodListName, e)
440 logging.critical(
"Could not write bad job file list '%s': %s",
447 logging.critical(
"Could not write output file list '%s': %s",
448 self.outputFileListName, e)
458 if __name__ ==
"__main__":
460 logging.basicConfig()
464 Parser = argparse.ArgumentParser(description=__doc__)
466 Parser.add_argument \
467 (
"XMLfileList", help=
"list of XML configuration of the jobs to check")
469 Parser.add_argument(
"--maxjobs",
"-n", dest=
"MaxJobs", default=
None, type=int,
470 help=
"if specified, process at most this number of jobs")
472 Parser.add_argument(
"--debug",
"-d", action=
"store_true",
473 help=
"enable debugging messages")
475 Parser.add_argument \
476 (
'--version',
'-V', action=
'version', version=
"%(prog)s v" + __version__)
478 jobListGroup = Parser.add_argument_group(
"Job lists")
480 jobListGroup.add_argument(
"--goodlist",
"-g", dest=
"GoodJobList",
481 default=
None, help=
"name of the list to be created with all good jobs")
482 jobListGroup.add_argument(
"--badlist",
"-b", dest=
"BadJobList",
483 default=
None, help=
"name of the list to be created with all bad jobs")
484 jobListGroup.add_argument(
"--outputlist",
"-o", dest=
"OutputFileList",
485 default=
None, help=
"name of the list to be created with ROOT output files")
487 jobListGroup.add_argument(
"--nogoodlist",
"-G", dest=
"NoGoodJobList",
488 action=
"store_true", help=
"do not create a list with all good jobs")
489 jobListGroup.add_argument(
"--nobadlist",
"-B", dest=
"NoBadJobList",
490 action=
"store_true", help=
"do not create a list with all bad jobs")
491 jobListGroup.add_argument(
"--nooutputlist",
"-O", dest=
"NoOutputFileList",
492 action=
"store_true", help=
"do not create a list with all output files")
494 jobListGroup.add_argument(
"--skipgood", dest=
"SkipGoodJobs",
496 help=
"do not check jobs that are already in the good list"
498 jobListGroup.add_argument(
"--skipbad", dest=
"SkipBadJobs",
500 help=
"do not check jobs that are already in the bad list"
503 args = Parser.parse_args()
505 logging.getLogger().setLevel(logging.DEBUG
if args.debug
else logging.INFO)
509 except ImportError
as e:
510 logging.error(
"Could not load `project.py` as Python module: %s", e)
514 goodList=(
False if args.NoGoodJobList
else args.GoodJobList),
515 badList=(
False if args.NoBadJobList
else args.BadJobList),
516 fileList=(
False if args.NoOutputFileList
else args.OutputFileList),
517 skipKnownGoodJobs=args.SkipGoodJobs,
518 skipKnownBadJobs=args.SkipBadJobs,
522 jobChecker.checkFromFile(args.XMLfileList, maxJobs=args.MaxJobs)
523 except KeyboardInterrupt:
524 logging.warning(
"\nCheck interrupted; file lists will not be changed.")
528 jobChecker.writeSummary()
static std::string format(PyObject *obj, unsigned int pos, unsigned int indent, unsigned int maxlen, unsigned int depth)
do one_file $F done echo for F in find $TOP name CMakeLists txt print
S join(S const &sep, Coll const &s)
Returns a concatenation of strings in s separated by sep.
def collectJobOutputFiles
open(RACETRACK) or die("Could not open file $RACETRACK for writing")