Gate 0.1.0: eigen image, het command-blok is een script, de app-map is leeg
Plan Eigenimage, fase 1 tot en met 3. De vier templates verhuizen naar tools/electrum-gate/ zonder extensie; daarnaast Dockerfile (nginx:1.30-alpine plus python3), entrypoint.sh (het command-blok van de compose, zonder $$) en build.sh naar het voorbeeld van Evolu Relay. Een image voor beide containers, gebouwd op de Umbrel; open punt 2 en 3 daarmee beslist. Inhoudelijk anders dan alleen verplaatst: het log_format staat in stream.conf zelf, het backend-adres komt via twee plaatshouders zonder dollarteken uit de omgeving (ook in de server-service), en de pagina haalt versie en adres uit status.json via GATE_APP_VERSION. Tests mee verhuisd en uitgebreid: entrypoint.sh en Dockerfile in plaats van het command-blok, en de tag in de compose gelijk aan VERSION in build.sh voor elke eigen image. Mutatie-getest met drie ingrepen. Nog niet gebouwd: er is hier geen Docker. De tag staat ongepind tot de eerste push; dat is fase 4 en die is van de gebruiker. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
7b3804df89
commit
973b24a23f
@@ -9,18 +9,25 @@ niets. De symptomen wezen ergens anders heen: de app_proxy meldde alleen dat de
|
||||
server niet te bereiken was.
|
||||
|
||||
De reparatie is een verplaatsing, en die is niet aan de code af te lezen: het
|
||||
stream-blok staat nu in stream.conf.template en wordt door het command-blok van
|
||||
de compose pas in /var/lib/gate/tls/ gezet als cert.conf bestaat. nginx.conf
|
||||
haalt die map op met een jokerteken, en een jokerteken dat niets matcht is voor
|
||||
nginx geen fout. Eén iemand die dat blok "netjes" terugzet in nginx.conf en de
|
||||
klem is terug, zonder dat een van de andere toetsen iets merkt.
|
||||
stream-blok staat in stream.conf en wordt door entrypoint.sh pas in
|
||||
/var/lib/gate/tls/ gezet als cert.conf bestaat. nginx.conf haalt die map op met
|
||||
een jokerteken, en een jokerteken dat niets matcht is voor nginx geen fout. Eén
|
||||
iemand die dat blok "netjes" terugzet in nginx.conf en de klem is terug, zonder
|
||||
dat een van de andere toetsen iets merkt.
|
||||
|
||||
Sinds 0.1.0 zitten deze bestanden in een eigen image en staan ze in
|
||||
tools/electrum-gate/; tot dan waren het *.template-bestanden in de app-map en
|
||||
was het startscript een command-blok in de compose. De toetsen zijn mee
|
||||
verhuisd en toetsen hetzelfde gedrag, plus wat er bij die verhuizing nieuw
|
||||
kapot kan: de Dockerfile die een bestand niet meeneemt, en de plaatshouders in
|
||||
stream.conf die entrypoint.sh moet invullen.
|
||||
|
||||
Draaien:
|
||||
|
||||
python tests/test_server_start_zonder_certificaat.py
|
||||
|
||||
Er staan inmiddels een paar toetsen bij die niet over het starten gaan maar wel
|
||||
over dezelfde soort fout: een die niets meldt en pas opvalt als iemand het toevallig
|
||||
Er staan een paar toetsen bij die niet over het starten gaan maar wel over
|
||||
dezelfde soort fout: een die niets meldt en pas opvalt als iemand het toevallig
|
||||
ziet. De verbindingsteller die stil nul telt, een mount die weer alleen-lezen wordt,
|
||||
en twee taglines die uit elkaar lopen. Ze staan hier en niet in een derde bestand,
|
||||
want de suite heeft geen runner en een bestand dat niemand aanroept toetst niets.
|
||||
@@ -31,15 +38,20 @@ import sys
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
import os # noqa: E402
|
||||
import re # noqa: E402
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
APP = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate")
|
||||
RECEPT = os.path.join(HERE, os.pardir, "tools", "electrum-gate")
|
||||
|
||||
NGINX_CONF = os.path.join(APP, "nginx.conf.template")
|
||||
STREAM_CONF = os.path.join(APP, "stream.conf.template")
|
||||
NGINX_CONF = os.path.join(RECEPT, "nginx.conf")
|
||||
STREAM_CONF = os.path.join(RECEPT, "stream.conf")
|
||||
ENTRYPOINT = os.path.join(RECEPT, "entrypoint.sh")
|
||||
DOCKERFILE = os.path.join(RECEPT, "Dockerfile")
|
||||
BUILD = os.path.join(RECEPT, "build.sh")
|
||||
PAGINA = os.path.join(RECEPT, "index.html")
|
||||
COMPOSE = os.path.join(APP, "docker-compose.yml")
|
||||
MANIFEST = os.path.join(APP, "umbrel-app.yml")
|
||||
PAGINA = os.path.join(APP, "index.html.template")
|
||||
|
||||
|
||||
class Uitslag:
|
||||
@@ -67,7 +79,7 @@ def lees(pad):
|
||||
|
||||
|
||||
def zonder_commentaar(tekst):
|
||||
"""De regels die nginx daadwerkelijk uitvoert.
|
||||
"""De regels die nginx of de shell daadwerkelijk uitvoert.
|
||||
|
||||
Zonder dit zou elke toets hieronder afgaan op de uitleg erboven, en die
|
||||
noemt juist de dingen die er niet meer mogen staan.
|
||||
@@ -104,64 +116,71 @@ def test_pagina_hangt_niet_aan_een_certificaat(u):
|
||||
|
||||
|
||||
def test_stream_blok_staat_apart(u):
|
||||
"""Het TLS-deel is compleet, en het is een template."""
|
||||
u.check("stream.conf.template bestaat", os.path.isfile(STREAM_CONF),
|
||||
"anders komt het bij een update niet mee: alleen *.template, "
|
||||
"docker-compose.yml, exports.sh, torrc, hooks en umbrel-app.yml "
|
||||
"worden ververst")
|
||||
"""Het TLS-deel is compleet, en het backend-adres komt uit de omgeving."""
|
||||
u.check("stream.conf bestaat", os.path.isfile(STREAM_CONF))
|
||||
if not os.path.isfile(STREAM_CONF):
|
||||
return
|
||||
|
||||
tekst = lees(STREAM_CONF)
|
||||
regels = zonder_commentaar(tekst)
|
||||
regels = zonder_commentaar(lees(STREAM_CONF))
|
||||
|
||||
u.check("stream.conf luistert op 50022 met ssl",
|
||||
any("listen 50022 ssl" in r for r in regels))
|
||||
u.check("stream.conf includet de cert.conf van de agent",
|
||||
any("include /var/lib/gate/cert.conf" in r for r in regels))
|
||||
u.check("stream.conf includet het log_format",
|
||||
any("include /var/lib/gate/stream-log.conf" in r for r in regels))
|
||||
u.check("stream.conf verwijst naar de backend uit de omgeving",
|
||||
any("proxy_pass ${APP_ELECTRS_NODE_IP}" in r for r in regels))
|
||||
u.check("stream.conf heeft zijn log_format zelf",
|
||||
any(r.startswith("log_format gate") for r in regels),
|
||||
"tot 0.0.29 schreef het command-blok dat in een los bestand, omdat "
|
||||
"een dollarteken in een template leeggemaakt werd; dat hoeft niet meer")
|
||||
u.check("en schrijft de sessielog waar de agent hem leest",
|
||||
any(r.startswith("access_log /var/lib/gate/stream.log") for r in regels))
|
||||
|
||||
# Dezelfde architectuurregel als voor de andere templates: umbreld vult elke
|
||||
# accolade-variabele in, dus een nginx-variabele met een dollarteken wordt
|
||||
# hier stil leeggemaakt. Vandaar dat het log_format uit een ander bestand
|
||||
# komt.
|
||||
losse_dollars = []
|
||||
for regel in regels:
|
||||
pos = regel.find("$")
|
||||
while pos >= 0:
|
||||
if regel[pos + 1:pos + 2] != "{":
|
||||
losse_dollars.append(regel)
|
||||
break
|
||||
pos = regel.find("$", pos + 1)
|
||||
u.check("stream.conf heeft geen nginx-variabelen",
|
||||
not losse_dollars,
|
||||
"de template-invulling maakt die leeg: %r" % losse_dollars[:2])
|
||||
# Het adres van de backend staat niet in dit bestand maar komt uit de
|
||||
# omgeving, en de weg daarheen is een plaatshouder die entrypoint.sh invult.
|
||||
# Geen dollarteken in die plaatshouder: dan kan geen envsubst-stap, van
|
||||
# umbreld of van wie dan ook, hem stil leegmaken.
|
||||
proxy = [r for r in regels if r.startswith("proxy_pass")]
|
||||
u.check("stream.conf heeft precies één proxy_pass", len(proxy) == 1,
|
||||
"gevonden: %r" % proxy)
|
||||
u.check("en die verwijst naar de plaatshouders voor host en poort",
|
||||
proxy and "__GATE_ELECTRUM_HOST__:__GATE_ELECTRUM_PORT__" in proxy[0],
|
||||
"gevonden: %r" % proxy)
|
||||
|
||||
u.check("stream.conf bevat geen accolade-variabele meer",
|
||||
"${" not in lees(STREAM_CONF),
|
||||
"die wordt sinds de eigen image door niemand meer ingevuld en zou "
|
||||
"letterlijk in de configuratie belanden")
|
||||
|
||||
|
||||
def test_compose_wacht_niet_op_de_agent(u):
|
||||
"""Het command-blok start nginx altijd, en zet TLS erbij als het kan."""
|
||||
tekst = lees(COMPOSE)
|
||||
def test_entrypoint_wacht_niet_op_de_agent(u):
|
||||
"""entrypoint.sh start nginx altijd, en zet TLS erbij als het kan."""
|
||||
u.check("entrypoint.sh bestaat", os.path.isfile(ENTRYPOINT))
|
||||
if not os.path.isfile(ENTRYPOINT):
|
||||
return
|
||||
|
||||
tekst = lees(ENTRYPOINT)
|
||||
regels = zonder_commentaar(tekst)
|
||||
|
||||
u.check("geen wachtlus op cert.conf",
|
||||
not any("! -f /var/lib/gate/cert.conf" in r and "while" in r
|
||||
not any("! -f" in r and "cert.conf" in r and "while" in r
|
||||
for r in regels),
|
||||
"dat is precies de klem uit 0.0.3: geen certificaat, geen nginx, "
|
||||
"geen pagina, dus geen manier om een certificaat te kiezen")
|
||||
|
||||
u.check("stream.conf is in de server gemount",
|
||||
any("stream.conf:/etc/nginx/stream.conf" in r for r in regels),
|
||||
"anders is er niets om in /var/lib/gate/tls/ te zetten")
|
||||
u.check("het stream-blok wordt ingevuld uit de omgeving",
|
||||
any("__GATE_ELECTRUM_HOST__" in r and "GATE_ELECTRUM_HOST" in r
|
||||
for r in regels)
|
||||
and any("__GATE_ELECTRUM_PORT__" in r for r in regels),
|
||||
"anders staat de plaatshouder letterlijk in de proxy_pass")
|
||||
|
||||
u.check("en wordt uit stream.conf.in gelezen, zoals de Dockerfile hem neerzet",
|
||||
any("/etc/nginx/stream.conf.in" in r for r in regels))
|
||||
|
||||
u.check("het stream-blok wordt weggezet als cert.conf bestaat",
|
||||
any("cp /etc/nginx/stream.conf /var/lib/gate/tls/" in r
|
||||
any(r.startswith("if cp /etc/nginx/stream.conf") and "/tls/" in r
|
||||
for r in regels))
|
||||
|
||||
u.check("en weggehaald als cert.conf er niet is",
|
||||
any("rm -f /var/lib/gate/tls/stream.conf" in r for r in regels),
|
||||
any(r.startswith("rm -f") and "/tls/stream.conf" in r for r in regels),
|
||||
"een ingetrokken keuze moet de poort ook echt sluiten")
|
||||
|
||||
u.check("een mislukte herlading breekt de wachtlus niet af",
|
||||
@@ -169,6 +188,119 @@ def test_compose_wacht_niet_op_de_agent(u):
|
||||
"onder 'set -e' zou de lus verdwijnen en daarna pikt niets meer "
|
||||
"een wijziging op")
|
||||
|
||||
u.check("nginx wordt met exec het hoofdproces",
|
||||
regels and regels[-1].startswith("exec nginx"),
|
||||
"anders blijft de shell PID 1, geeft signalen niet door, en ziet "
|
||||
"Docker een geslaagde afsluiting als nginx omvalt; laatste regel: %r"
|
||||
% (regels[-1] if regels else None))
|
||||
|
||||
u.check("het script begint met een shebang",
|
||||
tekst.startswith("#!/bin/sh"))
|
||||
|
||||
u.check("geen verdubbelde dollartekens meer",
|
||||
"$$" not in "\n".join(regels),
|
||||
"dat was de ontsnapping voor het command-blok in de compose; in een "
|
||||
"gewoon script is $$ het proces-id")
|
||||
|
||||
|
||||
def test_dockerfile_neemt_alles_mee(u):
|
||||
"""Wat entrypoint.sh en nginx.conf verwachten, zet de Dockerfile ook neer.
|
||||
|
||||
Dit is de nieuwe klasse fouten van de eigen image: een bestand dat in de
|
||||
repo staat maar niet in de image, en dan faalt de container pas op het
|
||||
apparaat. Geen Docker nodig om dat te zien; de paden staan in de tekst.
|
||||
"""
|
||||
u.check("Dockerfile bestaat", os.path.isfile(DOCKERFILE))
|
||||
if not os.path.isfile(DOCKERFILE):
|
||||
return
|
||||
|
||||
regels = zonder_commentaar(lees(DOCKERFILE))
|
||||
copies = [r for r in regels if r.startswith("COPY ")]
|
||||
|
||||
def kopieert(bron, doel):
|
||||
return any(bron in r and r.endswith(" " + doel) for r in copies)
|
||||
|
||||
u.check("nginx.conf gaat naar /etc/nginx/nginx.conf",
|
||||
kopieert("tools/electrum-gate/nginx.conf", "/etc/nginx/nginx.conf"))
|
||||
u.check("stream.conf gaat naar /etc/nginx/stream.conf.in",
|
||||
kopieert("tools/electrum-gate/stream.conf", "/etc/nginx/stream.conf.in"),
|
||||
"entrypoint.sh leest dat pad en vult het in naar stream.conf")
|
||||
u.check("de pagina gaat naar de webroot",
|
||||
kopieert("tools/electrum-gate/index.html", "/usr/share/nginx/html/index.html"))
|
||||
u.check("het icoon komt uit de app-map",
|
||||
kopieert("whatsnext-electrum-gate/icon.png", "/usr/share/nginx/html/icon.png"),
|
||||
"de app-map houdt het origineel omdat het manifest ernaar wijst; "
|
||||
"de image kopieert het, zodat er niet twee exemplaren zijn")
|
||||
u.check("de agent gaat naar /app/agent.py",
|
||||
kopieert("tools/electrum-gate/agent.py", "/app/agent.py"),
|
||||
"de compose start hem op dat pad")
|
||||
u.check("entrypoint.sh gaat mee en wordt uitvoerbaar",
|
||||
kopieert("tools/electrum-gate/entrypoint.sh", "/entrypoint.sh")
|
||||
and any(r.startswith("RUN chmod") and "/entrypoint.sh" in r for r in regels),
|
||||
"git op Windows kent geen uitvoerbit, dus die moet de Dockerfile zetten")
|
||||
u.check("python3 wordt geïnstalleerd",
|
||||
any(r.startswith("RUN apk add") and "python3" in r for r in regels))
|
||||
u.check("de basisimage is nginx op een vaste tag",
|
||||
any(re.match(r"FROM nginx:\d+\.\d+-alpine$", r) for r in regels),
|
||||
"nginx:alpine beweegt mee en is ook mét digest niet toegestaan; "
|
||||
"zie Docs/Referenties/Images-pinnen.md")
|
||||
u.check("het entrypoint van de nginx-image is uitgeschakeld",
|
||||
"ENTRYPOINT []" in regels,
|
||||
"anders draait het envsubst-script van die image mee, en dat is "
|
||||
"precies het soort stille invulling dat dit project vreest")
|
||||
|
||||
# De compose noemt paden in de image; die moeten er zijn.
|
||||
compose = zonder_commentaar(lees(COMPOSE))
|
||||
u.check("de compose start de agent op het pad waar de Dockerfile hem neerzet",
|
||||
any(r == "- /app/agent.py" for r in compose))
|
||||
|
||||
|
||||
def test_compose_en_recept_lopen_gelijk(u):
|
||||
"""Dezelfde image in beide services, met de tag uit build.sh.
|
||||
|
||||
Twee services die elk hun eigen tag hebben lopen uit elkaar bij de eerste
|
||||
verhoging die iemand op één plek doet. En een tag die niet gelijk is aan
|
||||
VERSION in build.sh betekent een image die nooit gebouwd is.
|
||||
"""
|
||||
compose = zonder_commentaar(lees(COMPOSE))
|
||||
images = [r.split(":", 1)[1].strip() for r in compose if r.startswith("image:")]
|
||||
u.check("de compose heeft twee images", len(images) == 2, "gevonden: %r" % images)
|
||||
|
||||
tags = set()
|
||||
for image in images:
|
||||
zonder_digest = image.split("@", 1)[0]
|
||||
tags.add(zonder_digest)
|
||||
u.check("en het is twee keer dezelfde", len(tags) == 1, "gevonden: %r" % sorted(tags))
|
||||
|
||||
versie = None
|
||||
for regel in zonder_commentaar(lees(BUILD)):
|
||||
if regel.startswith("VERSION="):
|
||||
versie = regel.split("=", 1)[1].strip().strip('"')
|
||||
u.check("build.sh heeft een VERSION", bool(versie))
|
||||
if versie and tags:
|
||||
tag = sorted(tags)[0]
|
||||
u.check("de tag in de compose is VERSION uit build.sh",
|
||||
tag.endswith(":" + versie),
|
||||
"compose %r, build.sh %r" % (tag, versie))
|
||||
|
||||
# Wat er niet meer gemount mag worden: de programmabestanden. Zit er weer
|
||||
# een mount van een los bestand uit de app-map in, dan is dat een terugval
|
||||
# naar de template-tijd en bereikt een wijziging het apparaat via twee wegen.
|
||||
losse = [r for r in compose if r.startswith("- ${APP_DATA_DIR}/")
|
||||
and "/" not in r[len("- ${APP_DATA_DIR}/"):].split(":", 1)[0]]
|
||||
u.check("geen losse bestanden uit de app-map meer gemount",
|
||||
not losse, "gevonden: %r" % losse)
|
||||
|
||||
u.check("de server krijgt het backend-adres uit de omgeving",
|
||||
compose.count("GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP}") == 2
|
||||
and compose.count("GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT}") == 2,
|
||||
"agent én server hebben het nodig; entrypoint.sh vult er het "
|
||||
"stream-blok mee in")
|
||||
|
||||
u.check("de agent krijgt de versie van het manifest door",
|
||||
"GATE_APP_VERSION: ${APP_VERSION}" in compose,
|
||||
"anders toont de kop van de pagina geen versie")
|
||||
|
||||
|
||||
def test_uploadpad(u):
|
||||
"""De drie dingen buiten de agent die uploaden mogelijk maken.
|
||||
@@ -208,13 +340,12 @@ def test_uploadpad(u):
|
||||
def test_verbindingsteller(u):
|
||||
"""De teller in de nginx-container, en de poort in hex.
|
||||
|
||||
Hoort hier omdat het om hetzelfde `command`-blok gaat. Het gevaarlijke deel
|
||||
is de hex: de teller zoekt in /proc/net/tcp op de poort in hexadecimale vorm,
|
||||
en verandert de TLS-poort ooit, dan telt hij stil nul. Een teller die altijd
|
||||
nul zegt, ziet eruit als "geen wallet verbonden" en niet als een fout.
|
||||
Het gevaarlijke deel is de hex: de teller zoekt in /proc/net/tcp op de poort
|
||||
in hexadecimale vorm, en verandert de TLS-poort ooit, dan telt hij stil nul.
|
||||
Een teller die altijd nul zegt, ziet eruit als "geen wallet verbonden" en
|
||||
niet als een fout.
|
||||
"""
|
||||
tekst = lees(COMPOSE)
|
||||
regels = zonder_commentaar(tekst)
|
||||
regels = zonder_commentaar(lees(ENTRYPOINT))
|
||||
|
||||
teller = [r for r in regels if "/proc/net/tcp" in r]
|
||||
u.check("de teller leest /proc/net/tcp", bool(teller),
|
||||
@@ -222,11 +353,12 @@ def test_verbindingsteller(u):
|
||||
"staan; de agent zit in een andere netwerk-namespace")
|
||||
|
||||
u.check("en schrijft het aantal weg voor de agent",
|
||||
any("/var/lib/gate/sessions" in r for r in regels))
|
||||
any("/sessions" in r for r in regels))
|
||||
|
||||
# De poort uit de omgeving van de agent is de waarheid; de teller moet
|
||||
# dezelfde poort in hex zoeken.
|
||||
poorten = [r for r in regels if r.startswith("GATE_TLS_PORT:")]
|
||||
compose = zonder_commentaar(lees(COMPOSE))
|
||||
poorten = [r for r in compose if r.startswith("GATE_TLS_PORT:")]
|
||||
u.check("GATE_TLS_PORT staat in de compose", len(poorten) == 1,
|
||||
"gevonden: %r" % poorten)
|
||||
if not poorten or not teller:
|
||||
@@ -238,9 +370,13 @@ def test_verbindingsteller(u):
|
||||
# backslash-vervolg en dan valt de hex op een eigen regel.
|
||||
u.check("de teller zoekt de TLS-poort in hex (%d is %s)" % (poort, hexpoort),
|
||||
(":" + hexpoort) in " ".join(regels),
|
||||
"nergens in het command-blok staat :%s, dus telt de teller nul"
|
||||
"nergens in entrypoint.sh staat :%s, dus telt de teller nul"
|
||||
% hexpoort)
|
||||
|
||||
stream = zonder_commentaar(lees(STREAM_CONF))
|
||||
u.check("en stream.conf luistert op diezelfde poort",
|
||||
any(r == "listen %d ssl;" % poort for r in stream))
|
||||
|
||||
|
||||
def test_data_onder_data(u):
|
||||
"""Alles wat de app zelf schrijft staat onder `data/`.
|
||||
@@ -262,14 +398,9 @@ def test_data_onder_data(u):
|
||||
fout = []
|
||||
for mount in mounts:
|
||||
host = mount.strip('"').split(":")[0].replace("${APP_DATA_DIR}/", "")
|
||||
# Een bestand naast de compose mag: dat zijn de ingevulde templates en het
|
||||
# icoon, en die hóren daar omdat de whitelist ze daar verft. Een map die de
|
||||
# app zelf vult, hoort onder data/.
|
||||
if "." in os.path.basename(host):
|
||||
continue
|
||||
if not host.startswith("data/"):
|
||||
fout.append(host)
|
||||
u.check("elke map die de app zelf vult staat onder data/",
|
||||
u.check("elke mount uit de app-map staat onder data/",
|
||||
not fout, "deze niet: %r" % fout)
|
||||
|
||||
u.check("de gedeelde toestand staat onder data/",
|
||||
@@ -382,17 +513,35 @@ def test_tagline_is_overal_dezelfde(u):
|
||||
"de pagina noemt hem niet: %r" % tagline)
|
||||
|
||||
|
||||
def test_pagina_zonder_invulling(u):
|
||||
"""De pagina zit in de image en wordt door niemand meer ingevuld.
|
||||
|
||||
Een ${...} die blijft staan komt letterlijk in de browser terecht. En de twee
|
||||
waarden die umbreld vroeger invulde, de versie en het backend-adres, moeten
|
||||
nu uit status.json komen; anders is de kop leeg en het adres ook.
|
||||
"""
|
||||
tekst = lees(PAGINA)
|
||||
u.check("de pagina bevat geen accolade-variabele", "${" not in tekst)
|
||||
u.check("de kop haalt de versie uit status.json",
|
||||
"s.version" in tekst and 'id="head-version"' in tekst)
|
||||
u.check("het backend-adres komt uit status.json",
|
||||
"backend.address" in tekst)
|
||||
|
||||
|
||||
def main():
|
||||
u = Uitslag()
|
||||
test_pagina_hangt_niet_aan_een_certificaat(u)
|
||||
test_stream_blok_staat_apart(u)
|
||||
test_compose_wacht_niet_op_de_agent(u)
|
||||
test_entrypoint_wacht_niet_op_de_agent(u)
|
||||
test_dockerfile_neemt_alles_mee(u)
|
||||
test_compose_en_recept_lopen_gelijk(u)
|
||||
test_verbindingsteller(u)
|
||||
test_uploadpad(u)
|
||||
test_data_onder_data(u)
|
||||
test_manifest_volgorde(u)
|
||||
test_releasenotes_hebben_een_geschiedenis(u)
|
||||
test_tagline_is_overal_dezelfde(u)
|
||||
test_pagina_zonder_invulling(u)
|
||||
return u.rapport()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user