X-Git-Url: https://git.subdivi.de/?p=~helmut%2Fcrossqa.git;a=blobdiff_plain;f=webapp.py;h=cc9de3715279aa6c87c5968ac24eb82ef5c2de68;hp=dd20ad6dee90cbb5441c1b9fc19ccef5bbef0f18;hb=a52d12012b1befdf1f00d0fcd5101306c52271ff;hpb=b8d551c7e26f1aea1134a5324a863022537346f8 diff --git a/webapp.py b/webapp.py index dd20ad6..cc9de37 100644 --- a/webapp.py +++ b/webapp.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 +# SPDX-License-Identifier: GPL-2.0+ import datetime import lzma @@ -15,82 +16,286 @@ import werkzeug app = flask.Flask("crossqa") app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = flask_sqlalchemy.SQLAlchemy(app) -src_template = """ +index_template = """ + Debian cross build quality assurance +
+

Debian cross build quality assurance

+
+
+

Recently failed builds

+ + + + + + + + + + + + + + {%- for build in builds|sort(attribute='starttime', reverse=true) -%} + + + + + + + + + + {%- endfor -%} + +
sourceversionbuild architecturehost architecturestartedresult logbugs
+ + {{- build.source|e -}} + + {{ build.version|e }}{{ build.buildarch|e }}{{ build.hostarch|e }} + {{- build.starttime|sqltimestamp|formatts -}} + + log + xz + + {%- if build.buglvl == 2 -%} + patch reported + {%- elif build.buglvl == 1 -%} + bug reported + {%- endif -%} +
+
+ + + +""" + +src_template = """ +{%- macro render_bug(bugobj) -%} + #{{ bugobj.bugnum }} + {%- if bugobj.patched %} + [+] + {%- endif -%}: + {% if bugobj.package != "src:" + bugobj.affects and + not bugobj.title.startswith(bugobj.affects + ":") -%} + {{- bugobj.package|e }}: + {% endif -%} + {{- bugobj.title|e -}} +{%- endmacro -%} + + + {{ sourcepackage|e }} - Debian cross build -

{{ sourcepackage|e }}

-

Cross satisfiability

- - - - - - {%- set okarchs = depresult.pop(None, None) -%} - {%- for reason, archs in depresult.items()|sort -%} - - - - - {%- endfor -%} - {%- if okarchs -%} - - - - + + + +
+

+ + {{- sourcepackage|e -}} + +

+
+ {%- if bugs.ftbfs -%} +
+

Reported FTBFS bugs

+ +
+ {%- endif -%} +
+

Cross build dependency satisfiability

+ {%- if bugs.bdsat -%} +
Reported satisfiability problems
+ {%- endif -%} -
statearchitectures
{{ reason|e }}{{ archs|arch_format }}
ok{{ okarchs|arch_format }}
-
See also
- - {%- if builds -%} -

Cross builds

- - - - - - - {%- for build in builds|sort(attribute='starttime', reverse=true) -%} + - - - - + + - {%- endfor -%} + + + {%- set okarchs = depresult.pop(None, None) -%} + {%- for reason, archs in depresult.items()|sort -%} + + + + + {%- endfor -%} + {%- if okarchs -%} + + + + + {%- endif -%} +
startedversionarchitectureresult
- - {{- build.starttime|sqltimestamp|formatts -}} - - {{ build.version|e }}{{ build.architecture|e }} - - {{- "ok" if build.success else "failed" -}} - - xz - statearchitectures
{{ reason|e }}{{ archs|archpairs_format }}
ok{{ okarchs|archpairs_format }}
- {%- endif -%} + {%- if show_debcheck -%} +
See also
+ + {%- endif -%} + +
+

Cross builds

+ {%- if bugs.ftcbfs -%} +
Reported cross build failures
+ + {%- endif -%} + {%- if builds -%} + + + + + + + + + + + + {%- for build in builds|sort(attribute='starttime', reverse=true) -%} + + + + + + + + {%- endfor -%} + +
startedversionbuild architecturehost architectureresult log
+ {{- build.starttime|sqltimestamp|formatts -}} + {{ build.version|e }}{{ build.buildarch|e }}{{ build.hostarch|e }} + + {{- "ok" if build.success else "failed" -}} + + xz +
+ {%- else -%} +

No build performed yet.

+ {%- endif -%} +
+ + {{ sourcepackage|e }} for + + +
+
+ + + +""" + +schedule_template = """ + + +

Scheduled a build of {{ request.form["source"]|e }} + {%- if request.form["architecture"] != "any" %} + for {{ request.form["architecture"]|e -}} + {%- endif %}. +

""" @app.template_filter("sqltimestamp") def sqltimestamp_filter(s): - return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S.%f") + strptime = datetime.datetime.strptime + try: + return strptime(s, "%Y-%m-%d %H:%M:%S.%f").replace(microsecond=0) + except ValueError: + return strptime(s, "%Y-%m-%d %H:%M:%S") -@app.template_filter("formatts") -def formatts_filter(ts): + +def formatts(ts): assert isinstance(ts, datetime.datetime) dt = datetime.datetime.utcnow() - ts if dt < datetime.timedelta(seconds=1): @@ -103,43 +308,105 @@ def formatts_filter(ts): return "%d h" % (dt.seconds // (60 * 60)) return "%d d" % dt.days -@app.template_filter('arch_format') -@jinja2.contextfilter -def arch_format_filter(context, some_archs): - if context["architectures"] == some_archs: + +@app.template_filter("formatts") +def formatts_filter(ts): + return jinja2.Markup('' % + (ts, ts, formatts(ts))) + +@app.template_filter("archpair_format") +def archpair_format_filter(archpair): + return jinja2.Markup("%s → %s" % tuple(map(jinja2.escape, archpair))) + +def group_pairs(pairs): + result = {} + for v, w in pairs: + result.setdefault(v, set()).add(w) + return result + +def render_archset(subset, all_archs): + if len(subset) == 1: + return next(iter(subset)) + if subset == all_archs: return "any" - return ", ".join(sorted(some_archs)) + return "{%s}" % ", ".join(map(jinja2.escape, sorted(subset))) + +@app.template_filter('archpairs_format') +@jinja2.contextfilter +def archpairs_format_filter(context, some_archs): + architectures = group_pairs(context["architectures"]) + fwdmap = {} # build architecture -> host architecture set representation + for buildarch, hostarchs in group_pairs(some_archs).items(): + fwdmap[buildarch] = render_archset(hostarchs, architectures[buildarch]) + allbuildarchs = set(architectures.keys()) + # host architecture set representation -> build architecture set + flippedit = group_pairs((v, k) for (k, v) in fwdmap.items()).items() + maps = ("%s → %s" % (render_archset(buildarchs, allbuildarchs), + hostarchrep) + for hostarchrep, buildarchs in flippedit) + return jinja2.Markup("; ".join(sorted(maps))) def collect_depstate(conn, source): version = None depstate = None query = sqlalchemy.text(""" - SELECT version, architecture, satisfiable, reason + SELECT version, 'amd64' AS buildarch, architecture AS hostarch, + satisfiable, reason FROM depstate WHERE source = :source;""") for row in conn.execute(query, source=source): if version is None or version_compare(version, row.version) > 0: version = row.version depstate = {} - depstate[row.architecture] = None if row.satisfiable else row.reason + depstate[row.buildarch, row.hostarch] = \ + None if row.satisfiable else row.reason if version is None: raise werkzeug.exceptions.NotFound() depresult = {} - for arch, reason in depstate.items(): - depresult.setdefault(reason, set()).add(arch) + for archpair, reason in depstate.items(): + depresult.setdefault(reason, set()).add(archpair) return version, depresult +@app.route("/") +def show_index(): + with db.engine.connect() as conn: + builds = list(conn.execute(""" + SELECT source, version, 'amd64' AS buildarch, + architecture AS hostarch, starttime, filename, + ifnull((SELECT max(patched + 1) FROM bugs + WHERE affects = source), + 0) AS buglvl + FROM builds + WHERE success = 0 + ORDER BY starttime + DESC LIMIT 10;""")) + return flask.render_template_string(index_template, builds=builds) + @app.route("/src/") def show_source(source): context = dict(sourcepackage=source) with db.engine.connect() as conn: - query = sqlalchemy.text("SELECT architecture FROM depcheck;") - context["architectures"] = set(row[0] for row in conn.execute(query)) + query = sqlalchemy.text("""SELECT 'amd64' AS buildarch, + architecture AS hostarch + FROM depcheck;""") + context["architectures"] = set(map(tuple, conn.execute(query))) context["version"], context["depresult"] = collect_depstate(conn, source) query = sqlalchemy.text(""" - SELECT version, architecture, success, starttime, filename + SELECT version, 'amd64' AS buildarch, architecture AS hostarch, + success, starttime, filename FROM builds WHERE source = :source;""") context["builds"] = list(conn.execute(query, source=source)) + query = sqlalchemy.text(""" + SELECT bugnum, kind, title, package, patched, affects + FROM bugs WHERE affects = :affects;""") + context["bugs"] = {} + for bug in conn.execute(query, affects=source): + context["bugs"].setdefault(bug.kind, []).append(bug) + context["show_bootstrapdn"] = \ + any(reason and not reason.startswith("skew ") + for reason in context["depresult"].keys()) + context["show_debcheck"] = \ + any(context["depresult"].keys()) return flask.render_template_string(src_template, **context) @app.route("/build/") @@ -147,6 +414,34 @@ def show_log(filename): if filename.endswith(".xz"): return flask.send_from_directory("logs", filename, mimetype="application/octet-stream") - filename += ".xz" - return flask.send_file(lzma.open(flask.safe_join("logs", filename), "rb"), - mimetype="text/plain") + filename = flask.safe_join("logs", filename + ".xz") + try: + return flask.send_file(lzma.open(filename, "rb"), + mimetype="text/plain") + except FileNotFoundError: + raise werkzeug.exceptions.NotFound() + + +@app.route("/schedule", methods=["POST"]) +def request_schedule(): + source = flask.request.form["source"] + buildarch, hostarch = flask.request.form["archpair"].split("_") + if buildarch not in ("any", "amd64"): + raise werkzeug.exceptions.BadRequest() + with db.engine.connect() as conn: + query = sqlalchemy.text(""" + SELECT 1 FROM depstate WHERE source = :source;""") + if not conn.execute(query, source=source).first(): + raise werkzeug.exceptions.BadRequest() + if hostarch == "any": + hostarch = None + else: + query = sqlalchemy.text(""" + SELECT 1 FROM depcheck WHERE architecture = :hostarch;""") + if not conn.execute(query, hostarch=hostarch).first(): + raise werkzeug.exceptions.BadRequest() + query = sqlalchemy.text(""" + INSERT INTO buildrequests (source, architecture, requesttime) + VALUES (:source, :hostarch, datetime('now'));""") + conn.execute(query, source=source, hostarch=hostarch) + return flask.render_template_string(schedule_template)