Package gavo :: Package user :: Module validation
[frames] | no frames]

Source Code for Module gavo.user.validation

  1  """ 
  2  A cli-facing module providing functionality to "validate" one or more 
  3  resource descriptors. 
  4   
  5  Validation means giving some prognosis as to whether RD will properly work  
  6  within both the DC and the VO. 
  7   
  8  While validation is active there's base.VALIDATING=True.  If RDs 
  9  to anything expensive, they're advised to have something like:: 
 10   
 11          if getattr(base, "VALIDATING", False): 
 12                  (don't do the expensive thing) 
 13  """ 
 14   
 15  #c Copyright 2008-2019, the GAVO project 
 16  #c 
 17  #c This program is free software, covered by the GNU GPL.  See the 
 18  #c COPYING file in the source distribution. 
 19   
 20   
 21  from __future__ import print_function 
 22   
 23  import argparse 
 24  import re 
 25  import sys 
 26  import traceback 
 27   
 28  from gavo import api 
 29  from gavo import adql 
 30  from gavo import base 
 31  from gavo import stc 
 32  from gavo import registry 
 33  from gavo import utils 
 34  from gavo.helpers import testtricks 
 35  from gavo.registry import builders 
 36  from gavo.protocols import datalink 
 37  from gavo.user import errhandle 
 38  from gavo.web import htmltable 
 39   
 40  from gavo.web import examplesrender #noflake: for RST registration 
 41   
 42  builders.VALIDATING = True 
43 44 -class TestsCollector(object):
45 """a singleton that collects use cases to run. 46 47 Don't instantiate, this is a global singleton. 48 49 The testsToRun attribute contains the test suites to run. 50 """ 51 testsToRun = [] 52 53 @classmethod
54 - def addRD(cls, rd):
55 """adds tests from rd. 56 """ 57 for suite in rd.tests: 58 cls.testsToRun.append(suite)
59
60 61 -def outputDependentMessage(aString):
62 """an output function for errhandle.raiseAndCatch. 63 64 It is used here to indent dependent error messages. 65 """ 66 print(re.sub("(?m)^", " ", aString))
67
68 69 -def outputError(rdId, message, verbose=False):
70 print("[ERROR] %s: %s"%(rdId, message)) 71 if verbose: 72 errhandle.raiseAndCatch(output=outputDependentMessage)
73
74 75 -def outputWarning(rdId, message, verbose=False):
76 print("[WARNING] %s: %s"%(rdId, message)) 77 if verbose: 78 errhandle.raiseAndCatch(output=outputDependentMessage)
79
80 81 -def loadRD(rdId):
82 """returns the RD identified by rdId. 83 84 If that fails, diagnostics are printed and None is returned. 85 """ 86 try: 87 rd = api.getReferencedElement(rdId, doQueries=False) 88 89 # This is so we can validate userconfig.rd 90 if hasattr(rd, "getRealRD"): 91 rd = rd.getRealRD() 92 93 except api.RDNotFound: 94 outputError(rdId, "RD or dependency not found, message follows", True) 95 except api.LiteralParseError: 96 outputError(rdId, "Bad literal in RD, message follows", True) 97 except api.StructureError: 98 outputError(rdId, "Malformed RD input, message follows", True) 99 except api.Error: 100 outputError(rdId, "Syntax or internal error, message follows", True) 101 else: 102 return rd 103 # Fallthrough: RD could not be loaded 104 return None
105 106 107 _XSD_VALIDATOR = testtricks.XSDTestMixin()
108 109 110 -def isIVOPublished(svc):
111 """returns true if svc has a publication facing the VO. 112 """ 113 for pub in svc.publications: 114 if "ivo_managed" in pub.sets: 115 return True 116 else: 117 return False
118
119 120 -def validateServices(rd, args):
121 """outputs to stdout various diagnostics about the services on rd. 122 """ 123 validSoFar = True 124 for svc in rd.services: 125 # If it's not published, metadata are nobody's business. 126 if not (args.prePublication or svc.publications): 127 continue 128 try: 129 base.validateStructure(svc) 130 except api.MetaValidationError as ex: 131 validSoFar = False 132 outputError(rd.sourceId, "Missing metadata for publication of" 133 " service %s:\n%s"%(svc.id, str(ex))) 134 continue # further checks will just add verbosity 135 136 if not (args.prePublication or isIVOPublished(svc)): 137 # require sane metadata only if the VO will see the service 138 continue 139 140 # error out if the identifier cannot be generated 141 api.getMetaText(svc, "identifier") 142 registryRecord = None 143 try: 144 registryRecord = builders.getVORMetadataElement(svc) 145 except stc.STCSParseError as msg: 146 validSoFar = False 147 outputError(rd.sourceId, "Invalid STC-S (probably in coverage meta)" 148 ": %s"%str(msg)) 149 except: 150 validSoFar = False 151 outputError(rd.sourceId, "Error when producing registry record" 152 " of service %s:"%svc.id, True) 153 154 if registryRecord is not None: 155 try: 156 _XSD_VALIDATOR.assertValidates( 157 registryRecord.render(), leaveOffending=True) 158 except AssertionError as msg: 159 validSoFar = False 160 outputError(rd.sourceId, "Invalid registry record for service" 161 " %s:\n%s"%(svc.id, str(msg))) 162 163 return validSoFar
164
165 166 -def validateRST(rd, args):
167 """outputs diagnostics on RST formatting problems. 168 """ 169 def validateRSTOne(el): 170 validSoFar = True 171 172 for key, val in getattr(el, "getAllMetaPairs", lambda: [])(): 173 if val.format=='rst': 174 content = val.getExpandedContent(macroPackage=el) 175 _, msg = utils.rstxToHTMLWithWarning(content) 176 if msg: 177 outputWarning(rd.sourceId, 178 "%s metadata on %s (%s) has an RST problem: %s"%( 179 key, el, utils.makeEllipsis(content, 80), msg)) 180 181 for child in el.iterChildren(): 182 if child: 183 validSoFar = validSoFar and validateRSTOne(child) 184 185 return validSoFar
186 187 return validateRSTOne(rd) 188
189 190 -def validateRowmakers(rd, args):
191 """tries to build all rowmakers mentioned in the RD and bails out 192 if one is bad. 193 """ 194 for dd in rd: 195 for m in dd.makes: 196 m.table.onDisk = False 197 try: 198 api.TableForDef(m.table) 199 m.rowmaker.compileForTableDef(m.table) 200 finally: 201 m.table.onDisk = True 202 return True
203
204 205 -def validateOtherCode(rd, args):
206 """tries to compile other pieces of code in an RD and bails out 207 if one is bad. 208 """ 209 retval = True 210 211 for suite in rd.tests: 212 for test in suite.tests: 213 try: 214 test.compile() 215 except Exception as msg: 216 outputError(rd.sourceId, "Bad test '%s': %s"%(test.title, 217 msg)) 218 retval = False 219 220 for svc in rd.services: 221 for outputField in svc.getCurOutputFields(): 222 if outputField.formatter: 223 try: 224 htmltable._compileRenderer(outputField.formatter, None, rd) 225 except Exception as msg: 226 outputError(rd.sourceId, "Bad formatter on output field '%s': %s"%( 227 outputField.name, msg)) 228 retval = False 229 230 if isinstance(svc.core, datalink.DatalinkCore): 231 try: 232 if "dlmeta" in svc.allowed: 233 svc.core.descriptorGenerator.compile(svc.core) 234 if "dlget" in svc.allowed: 235 for df in svc.core.dataFunctions: 236 df.compile(svc.core) 237 svc.core.dataFormatter.compile(svc.core) 238 except Exception as msg: 239 outputError(rd.sourceId, "Bad datalink function in service '%s': %s"%( 240 svc.id, msg)) 241 if isinstance(msg, base.BadCode): 242 outputError(rd.sourceId, "Bad code:\n%s"%msg.code) 243 retval = False 244 245 for job in rd.jobs: 246 try: 247 job.job.compile(parent=rd) 248 except Exception as msg: 249 outputError(rd.sourceId, "Bad code in job '%s': %s"%( 250 job.title, msg)) 251 retval = False 252 253 # TODO: iterate over service/cores and standalone cores and 254 # fiddle out condDescs 255 # TODO: Iterate over scripts and data/make/scripts, see which 256 # are python and try to compile them 257 # TODO: Iterate over grammars and validate rowfilters 258 259 return retval
260
261 262 -def validateTables(rd, args):
263 """does some sanity checks on the (top-level) tables within rd. 264 """ 265 valid = True 266 267 identifierSymbol = adql.getSymbols()["identifier"] 268 269 for td in rd.tables: 270 for col in td: 271 try: 272 if col.unit: 273 parsedUnit = api.parseUnit(col.unit) 274 if parsedUnit.isUnknown and not args.acceptFreeUnits: 275 outputWarning(rd.sourceId, 276 "Column %s.%s: Unit %s is not interoperable"%( 277 td.getQName(), col.name, col.unit)) 278 279 except api.BadUnit: 280 valid = False 281 outputError(rd.sourceId, "Bad unit in table %s, column %s: %s"%( 282 td.getQName(), col.name, repr(col.unit))) 283 284 try: 285 identifierSymbol.parseString(str(col.name), parseAll=True) 286 except base.ParseException as msg: 287 outputWarning(rd.sourceId, "Column %s.%s: Name is not a regular" 288 " ADQL identifier."%(td.id, col.name)) 289 290 if td.onDisk and args.compareDB: 291 with base.getTableConn() as conn: 292 q = base.UnmanagedQuerier(conn) 293 if q.getTableType(td.getQName()) is not None: 294 t = api.TableForDef(td, connection=conn) 295 try: 296 t.ensureOnDiskMatches() 297 except api.DataError as msg: 298 valid = False 299 outputError(rd.sourceId, 300 utils.makeEllipsis(utils.safe_str(msg), 160)) 301 302 # associated datalink services and the columns must exist. 303 for dldef in td.iterMeta("_associatedDatalinkService"): 304 try: 305 _ = base.resolveId(td.rd, 306 base.getMetaText(dldef, "serviceId")) 307 _ = td.getColumnByName(base.getMetaText(dldef, "idColumn")) 308 _ 309 310 except (base.NotFoundError, base.MetaError) as msg: 311 valid = False 312 outputError(rd.sourceId, 313 utils.makeEllipsis(utils.safe_str(msg), 160)) 314 315 if td.registration: 316 registryRecord = None 317 try: 318 registryRecord = builders.getVORMetadataElement(td) 319 except Exception as msg: 320 valid = False 321 outputError(rd.sourceId, 322 "Table publication of %s could not be built: %s"%( 323 td.id, str(msg))) 324 325 if registryRecord is not None: 326 try: 327 _XSD_VALIDATOR.assertValidates( 328 registryRecord.render(), leaveOffending=True) 329 except AssertionError as msg: 330 valid = False 331 outputError(rd.sourceId, "Invalid registry record for table" 332 " %s:\n%s"%(td.id, str(msg))) 333 334 return valid
335
336 337 -def validateOne(rdId, args):
338 """outputs to stdout various information on the RD identified by rdId. 339 """ 340 with testtricks.collectedEvents("Info", "Warning") as warnings: 341 rd = loadRD(rdId) 342 if rd is None: 343 return 344 345 for warning in warnings: 346 outputWarning(rdId, warning[1]) 347 348 if args.runTests: 349 TestsCollector.addRD(rd) 350 351 validSoFar = validateServices(rd, args) 352 validSoFar = validSoFar and validateRowmakers(rd, args) 353 validSoFar = validSoFar and validateTables(rd, args) 354 validSoFar = validSoFar and validateOtherCode(rd, args) 355 validSoFar = validSoFar and validateRST(rd, args) 356 return validSoFar
357
358 359 -def validateAll(args):
360 """validates all accessible RDs. 361 """ 362 for rdId in registry.findAllRDs(): 363 if args.verbose: 364 sys.stdout.write(rdId+" ") 365 sys.stdout.flush() 366 try: 367 validateOne(rdId, args) 368 except Exception: 369 sys.stderr.write("Severe error while validating %s:\n"%rdId) 370 traceback.print_exc() 371 if args.verbose: 372 sys.stdout.write("\n")
373
374 375 -def parseCommandLine():
376 parser = argparse.ArgumentParser(description="Check RDs for well-formedness" 377 " and some aspects of VO-friendlyness") 378 parser.add_argument("rd", nargs="+", type=str, 379 help="RD identifier or file system path. Use magic value ALL to" 380 " check all reachable RDs.") 381 parser.add_argument("-p", "--pre-publication", help="Validate" 382 " as if all services were IVOA published even if they are not" 383 " (this may produce spurious errors if unpublished services are in" 384 " the RD).", 385 action="store_true", dest="prePublication") 386 parser.add_argument("-v", "--verbose", help="Talk while working", 387 action="store_true", dest="verbose") 388 parser.add_argument("-t", "--run-tests", help="Run regression tests" 389 " embedded in the checked RDs", action="store_true", dest="runTests") 390 parser.add_argument("-T", "--timeout", help="When running tests, abort" 391 " and fail requests after inactivity of SECONDS", 392 action="store", dest="timeout", type=int, default=15, metavar="SECONDS") 393 parser.add_argument("-c", "--compare-db", help="Also make sure that" 394 " tables that are on disk (somewhat) match the definition in the RD.", 395 action="store_true", dest="compareDB") 396 parser.add_argument("-u", "--accept-free-units", help="Do not warn" 397 " against units not listed in VOUnits.", 398 action="store_true", dest="acceptFreeUnits") 399 400 401 return parser.parse_args()
402
403 404 -def main():
405 base.VALIDATING = True 406 args = parseCommandLine() 407 if len(args.rd)==1 and args.rd[0]=="ALL": 408 validateAll(args) 409 else: 410 for rd in args.rd: 411 print(rd, "--", end=' ') 412 sys.stdout.flush() 413 if validateOne(rd, args): 414 print("OK") 415 else: 416 print("Fail") 417 418 if args.runTests: 419 print("\nRunning regression tests\n") 420 from gavo.rscdef import regtest 421 runner = regtest.TestRunner(TestsCollector.testsToRun, 422 verbose=False, timeout=args.timeout) 423 runner.runTests(showDots=True) 424 print(runner.stats.getReport()) 425 if runner.stats.fails: 426 print("\nThe following tests failed:\n") 427 print(runner.stats.getFailures())
428