From 9c76d65cef0f3cc7e57fc50da8419b3eeb78647d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Sj=C3=B6lund?= Date: Tue, 25 Aug 2026 06:24:55 +0200 Subject: [PATCH] Mark models a library no longer has as removed A library that still loads but has no model with an experiment annotation writes no rows into its branch table, so the newest date there stays at the last run that did have models and the reports go on listing them. Physiomodel lost its annotations in 2021 and master has reported its 12 models ever since; OpenHydraulics, IdealizedContact, Dynawo_1.6.1 and ModelicaTestOverdetermined_3.2.2 are in the same state, and Pharmacolibrary, ExtendedPetriNets, ExternalMemoryLib and Spot never had a model at all. A run now writes a finalphase -1 row for every model the previous run of that library had and it no longer finds. The previous run is the newest one with rows for the library rather than the previous run of the branch, which makes it self-limiting: once written, the removals are themselves that library's newest rows, so a library that stays empty is marked once instead of at every run. Nothing has to be backfilled - each stale library still gets a libversion row every run, so the next run of a branch marks it. The reports that counted rows without asking for a phase now exclude the marker, and the two regression queries no longer report a model going Verify -> Removed as a regression. A library that fails to load has no models either, and marking every model of it removed on the strength of a failed download would drop it out of the reports for a day. Collect the libraries whose loadModel failed and refuse to run, so the reason is fixed rather than recorded. Assisted-by: Claude Opus 5 --- all-plots.py | 4 ++-- all-reports.py | 3 ++- doc/README.md | 14 +++++++++++++- pr-report.py | 6 ++++-- report.py | 2 +- shared.py | 5 +++++ test.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 67 insertions(+), 9 deletions(-) diff --git a/all-plots.py b/all-plots.py index b990c9c..d9ce67b 100755 --- a/all-plots.py +++ b/all-plots.py @@ -114,11 +114,11 @@ def plotLibrary(branch, libname, xs, total, frontend,backend,simcode,template,co db.createDateIndex(branch) libs = {} - for (date,libname,total,frontend,backend,simcode,template,compile,simulate,verify) in cursor.execute("""SELECT date,libname,COUNT(finalphase),%s + for (date,libname,total,frontend,backend,simcode,template,compile,simulate,verify) in cursor.execute("""SELECT date,libname,%s FROM %s GROUP BY date,libname ORDER BY libname,date ASC -""" % (",".join(db.countIf("finalphase>=%d" % i) for i in range(1,8)), db.quote(branch))): +""" % (",".join(db.countIf("finalphase>=%d" % i) for i in range(0,8)), db.quote(branch))): if libname not in libs: libs[libname] = ([],[],[],[],[],[],[],[],[]) libs[libname][0].append(datetime.datetime.fromtimestamp(date)) diff --git a/all-reports.py b/all-reports.py index 66f1443..0394232 100755 --- a/all-reports.py +++ b/all-reports.py @@ -328,6 +328,7 @@ def historyOf(branch, nruns): query = ("""SELECT model,libname,%s FROM (SELECT model,libname,date,finalphase,frontend,backend,simcode,templates,compile,simulate FROM %%s WHERE date IN (?,?) AND libname IN (%%s) ORDER BY date) AS phases GROUP BY model,libname HAVING""" % concat + """ + MIN(finalphase) >= 0 AND ( (MIN(finalphase) <> MAX(finalphase)) OR ((MIN(finalphase) >= ?) AND (MAX(frontend) > ?*MIN(frontend) AND MAX(frontend) > ?) OR @@ -336,7 +337,7 @@ def historyOf(branch, nruns): (MAX(templates) > ?*MIN(templates) AND MAX(templates) > ?) OR (MAX(compile) > ?*MIN(compile) AND MAX(compile) > ?) OR (MAX(simulate) > ?*MIN(simulate) AND MAX(simulate) > ?) - ) + )) """) % (db.quote(branch),",".join(["'%s'" % libname for libname in startdates[d1lib]])) cursor.execute(query, (d1lib,d2,timeMinPhase,timeRel,timeAbs,timeRel,timeAbs,timeRel,timeAbs,timeRel,timeAbs,timeRel,2*timeAbs,timeRel,timeAbs)) regressions += cursor.fetchall() diff --git a/doc/README.md b/doc/README.md index 578692f..0678181 100644 --- a/doc/README.md +++ b/doc/README.md @@ -98,6 +98,7 @@ Notes on the values, which are produced by `testmodel.py` and written by | value | name | meaning | | --- | --- | --- | +| -1 | Removed | the library no longer has that model | | 0 | Failed | the front end did not finish | | 1 | FrontEnd | front end ok, back end failed | | 2 | BackEnd | back end ok, SimCode failed | @@ -108,7 +109,18 @@ Notes on the values, which are produced by `testmodel.py` and written by | 7 | Verify | the result matches the reference file | Reports count models per phase with `WHERE finalphase >= i`, so the columns of -the HTML tables are cumulative. +the HTML tables are cumulative, and phase -1 falls outside all of them. + +A run writes a phase -1 row for every model the previous run of that library had +and it no longer finds, so that a model dropped from a library stops being +reported once the run that lost it is the newest one. Without it a library whose +models all lose their `experiment` annotation - Physiomodel did in 2021 - keeps +a newest run in this table from years ago, and is reported forever with the +models of that run. The rows are written once, not at every run: the previous +run of an empty library is the one holding the removals, which have no models +left to remove. A library that fails to load has no models either, which is why +`test.py` refuses to run at all when a `loadModel` fails rather than treating it +as a library that lost every model. ### `omcversion` diff --git a/pr-report.py b/pr-report.py index e61feca..df09f8a 100755 --- a/pr-report.py +++ b/pr-report.py @@ -102,7 +102,8 @@ def omcVersion(table, date): def models(table, libname, date): return set(mod for (mod,) in cursor.execute( - "SELECT model FROM %s WHERE date=? AND libname=?" % db.quote(table), (date, libname))) + "SELECT model FROM %s WHERE date=? AND libname=? AND finalphase>=0" % db.quote(table), + (date, libname))) def changedModels(table1, date1, table2, date2, libnames): """The models whose phase or timings differ between the two runs. @@ -123,6 +124,7 @@ def changedModels(table1, date1, table2, date2, libnames): SELECT %s,1 AS ord FROM %s WHERE date=? AND libname IN (%s)) AS runs ORDER BY ord) AS phases GROUP BY model,libname HAVING + MIN(finalphase) >= 0 AND ( (MIN(finalphase) <> MAX(finalphase)) OR (MIN(finalphase) >= ? AND ( (MAX(frontend) > ?*MIN(frontend) AND MAX(frontend) > ?) OR @@ -130,7 +132,7 @@ def changedModels(table1, date1, table2, date2, libnames): (MAX(simcode) > ?*MIN(simcode) AND MAX(simcode) > ?) OR (MAX(templates) > ?*MIN(templates) AND MAX(templates) > ?) OR (MAX(compile) > ?*MIN(compile) AND MAX(compile) > ?) OR - (MAX(simulate) > ?*MIN(simulate) AND MAX(simulate) > ?))) + (MAX(simulate) > ?*MIN(simulate) AND MAX(simulate) > ?)))) """ % (concat, cols, db.quote(table1), inlibs, cols, db.quote(table2), inlibs) cursor.execute(query, (date1, date2, timeMinPhase, timeRel, timeAbs, timeRel, timeAbs, timeRel, timeAbs, diff --git a/report.py b/report.py index 97cbd80..03b6961 100755 --- a/report.py +++ b/report.py @@ -70,7 +70,7 @@ dates[branch][libname] = 0 continue dates[branch][libname] = v[0] - for x in cursor.execute("SELECT model FROM %s WHERE libname=? AND date=?" % db.quote(branch), (libname,v[0])): + for x in cursor.execute("SELECT model FROM %s WHERE libname=? AND date=? AND finalphase>=0" % db.quote(branch), (libname,v[0])): if libname not in libs: libs[libname] = set() libs[libname].add(x[0]) diff --git a/shared.py b/shared.py index a5afa80..36a0381 100644 --- a/shared.py +++ b/shared.py @@ -73,7 +73,12 @@ def libname(library, conf): return library+"_"+conf["libraryVersionNameForTests"] if conf["libraryVersionNameForTests"] else library return library+("_"+conf["libraryVersion"] if conf["libraryVersion"]!="default" else "")+(("_" + conf["configExtraName"]) if "configExtraName" in conf else "") +# A model the run no longer found in its library; the reports ask for >= 0. +DELETED_PHASE = -1 + def finalphaseName(finalphase): + if finalphase == DELETED_PHASE: + return "Removed" return ("Failed","FrontEnd","BackEnd","SimCode","Templates","Compile","Simulate","Verify")[finalphase] def getReferenceFileName(conf): diff --git a/test.py b/test.py index 6cb7797..ced5d64 100755 --- a/test.py +++ b/test.py @@ -594,8 +594,11 @@ def hashReferenceFiles(s): stats_by_libname = {} skipped_libs = {} +# A library that did not load cannot be told apart from one with no models left. +failedToLoad = [] tests=[] for (library,conf) in configs: + loadFailed = False # Only when asked, so a normal run's confighash is unchanged if args.nobuildmodel: conf["noBuildModel"] = True @@ -657,6 +660,7 @@ def hashReferenceFiles(s): print("Failed to run command %s: %s" % (command,omc.sendExpression('OpenModelica.Scripting.getErrorString()'))) except: print("Failed to run command %s OpenModelica.Scripting.getErrorString() failed..." % command) + loadFailed = True librariesToLoad = [] else: librariesToLoad = [[library,conf["libraryVersion"]]] + conf.get("extraLibraries", []) @@ -683,6 +687,11 @@ def hashReferenceFiles(s): print("Failed to load library %s %s: %s" % (library,versions,omc.sendExpression('OpenModelica.Scripting.getErrorString()'))) except: print("Failed to load library %s %s. OpenModelica.Scripting.getErrorString() failed..." % (library,conf["libraryVersion"])) + loadFailed = True + if loadFailed: + failedToLoad.append(shared.libname(library, conf)) + continue + # adrpo: do not sort the top level names as sometimes that loads a bad MSL version # conf["loadFiles"] = sorted(omc.sendExpression("{getSourceFile(cl) for cl in getClassNames()}")) conf["loadFiles"] = omc.sendExpression("{getSourceFile(cl) for cl in getClassNames()}") @@ -775,6 +784,10 @@ def hashReferenceFiles(s): except: pass +if failedToLoad: + db.release() + raise SystemExit("Failed to load: %s" % ", ".join(failedToLoad)) + print("Checked which libraries to run") sys.stdout.flush() @@ -879,7 +892,7 @@ def expectedExec(c): (model,lib,libName,name,data) = c if "expectedExec" in data: return data["expectedExec"] - cursor.execute("SELECT exectime FROM %s WHERE libname = ? AND model = ? ORDER BY date DESC LIMIT 1" % db.quote(primaryBranch), (libName,model)) + cursor.execute("SELECT exectime FROM %s WHERE libname = ? AND model = ? AND finalphase >= 0 ORDER BY date DESC LIMIT 1" % db.quote(primaryBranch), (libName,model)) v = cursor.fetchone() data["expectedExec"] = (v or (0.0,))[0] return data["expectedExec"] @@ -906,7 +919,7 @@ def expectedExec(c): numberOfTests = len(tests) -if numberOfTests==0: +if numberOfTests==0 and not stats_by_libname: print("Everything already up to date. Not executing any tests.") sys.exit(0) @@ -993,6 +1006,23 @@ def resultValues(model, libname, data, simulator=None): data.get("parsing") or 0.0 ) +def removedModels(resultBranch, libname, tested): + """The models the previous run of that library had and this one no longer finds. + + Once written, the removals are that library's newest rows, so a library that + stays empty is marked once rather than at every run. + """ + previous = cursor.execute("""SELECT model FROM %s WHERE libname=? AND finalphase>=0 + AND date=(SELECT MAX(date) FROM %s WHERE libname=? AND date