webapp: add concept of "build architecture" to frontend
authorHelmut Grohne <helmut@subdivi.de>
Wed, 7 Oct 2020 14:41:14 +0000 (16:41 +0200)
committerHelmut Grohne <helmut@subdivi.de>
Wed, 7 Oct 2020 14:41:14 +0000 (16:41 +0200)
The frontend now pretends that we had a concept of a "build
architecture" in its visual appearance. Whenever dealing with, hard code
"amd64" though. That allows refactoring the underlying database without
changing how the frontend looks.

webapp.py

index 8acf92f..cc9de37 100644 (file)
--- a/webapp.py
+++ b/webapp.py
@@ -35,7 +35,8 @@ index_template = """<!DOCTYPE html>
      <tr>
       <th>source</th>
       <th>version</th>
-      <th>architecture</th>
+      <th>build architecture</th>
+      <th>host architecture</th>
       <th>started</th>
       <th>result log</th>
       <th>bugs</th>
@@ -50,7 +51,8 @@ index_template = """<!DOCTYPE html>
         </a>
        </td>
        <td>{{ build.version|e }}</td>
-       <td>{{ build.architecture|e }}</td>
+       <td>{{ build.buildarch|e }}</td>
+       <td>{{ build.hostarch|e }}</td>
        <td>
         {{- build.starttime|sqltimestamp|formatts -}}
        </td>
@@ -107,13 +109,13 @@ tr.dep.tempbad td:nth-child(1) {
 tr.dep.good td:nth-child(1) {
     background-color: #afa;
 }
-tr.build.bad td:nth-child(4) {
+tr.build.bad td:nth-child(5) {
     background-color: #faa;
 }
-tr.build.tempbad td:nth-child(4) {
+tr.build.tempbad td:nth-child(5) {
     background-color: #ffa;
 }
-tr.build.good td:nth-child(4) {
+tr.build.good td:nth-child(5) {
     background-color: #afa;
 }
 th {
@@ -177,13 +179,13 @@ footer {
      {%- for reason, archs in depresult.items()|sort -%}
       <tr class="dep {{ "tempbad" if reason.startswith("skew") else "bad" }}">
        <td>{{ reason|e }}</td>
-       <td>{{ archs|arch_format }}</td>
+       <td>{{ archs|archpairs_format }}</td>
       </tr>
      {%- endfor -%}
      {%- if okarchs -%}
       <tr class="dep good">
        <td>ok</td>
-       <td>{{ okarchs|arch_format }}</td>
+       <td>{{ okarchs|archpairs_format }}</td>
       </tr>
      {%- endif -%}
     </tbody>
@@ -220,7 +222,8 @@ footer {
       <tr>
        <th>started</th>
        <th>version</th>
-       <th>architecture</th>
+       <th>build architecture</th>
+       <th>host architecture</th>
        <th>result log</th>
       </tr>
      </thead>
@@ -231,7 +234,8 @@ footer {
          {{- build.starttime|sqltimestamp|formatts -}}
         </td>
         <td>{{ build.version|e }}</td>
-        <td>{{ build.architecture|e }}</td>
+        <td>{{ build.buildarch|e }}</td>
+        <td>{{ build.hostarch|e }}</td>
         <td>
          <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">
           {{- "ok" if build.success else "failed" -}}
@@ -249,10 +253,12 @@ footer {
     <input type="submit" name="schedule" value="cross build" />
     {{ sourcepackage|e }} for
     <input type="hidden" name="source" value="{{ sourcepackage|e }}" />
-    <select name="architecture">
-     <option value="any">any</option>
-     {%- for architecture in architectures|sort -%}
-      <option value="{{ architecture|e }}">{{ architecture|e }}</option>
+    <select name="archpair">
+     <option value="any_any">{{ ("any", "any")|archpair_format }}</option>
+     {%- for buildarch, hostarch in architectures|sort -%}
+      <option value="{{ buildarch|e }}_{{ hostarch|e }}">
+       {{- (buildarch, hostarch)|archpair_format -}}
+      </option>
      {%- endfor -%}
     </select>
    </form>
@@ -308,36 +314,64 @@ def formatts_filter(ts):
     return jinja2.Markup('<time title="%s" datetime="%s">%s</time>' %
                          (ts, ts, formatts(ts)))
 
-@app.template_filter('arch_format')
-@jinja2.contextfilter
-def arch_format_filter(context, some_archs):
-    if context["architectures"] == some_archs:
+@app.template_filter("archpair_format")
+def archpair_format_filter(archpair):
+    return jinja2.Markup("%s &rarr; %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 &rarr; %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, architecture, starttime, filename,
+            SELECT source, version, 'amd64' AS buildarch,
+                architecture AS hostarch, starttime, filename,
                 ifnull((SELECT max(patched + 1) FROM bugs
                                                 WHERE affects = source),
                        0) AS buglvl
@@ -351,12 +385,15 @@ def show_index():
 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("""
@@ -388,21 +425,23 @@ def show_log(filename):
 @app.route("/schedule", methods=["POST"])
 def request_schedule():
     source = flask.request.form["source"]
-    architecture = flask.request.form["architecture"]
+    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 architecture == "any":
-            architecture = None
+        if hostarch == "any":
+            hostarch = None
         else:
             query = sqlalchemy.text("""
-                SELECT 1 FROM depcheck WHERE architecture = :architecture;""")
-            if not conn.execute(query, architecture=architecture).first():
+                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, :architecture, datetime('now'));""")
-        conn.execute(query, source=source, architecture=architecture)
+                VALUES (:source, :hostarch, datetime('now'));""")
+        conn.execute(query, source=source, hostarch=hostarch)
     return flask.render_template_string(schedule_template)