mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
perf(tests): parallelize suites, hoist fixtures, gate ansible-lint
Run the python suites through pytest-xdist when pytest is importable (id-level collection parity with unittest discover verified per suite; serial unittest stays the fallback), with -rP so warnings printed by passing tests, like the update annotations, still reach the log. run.sh self-provisions the dev extras through scripts/install/dev-extras.sh, which stamps inside the venv and skips read-only venvs; make onboard pre-warms the same path in the running container. Hoist per-test cache resets and roles scans to setUpClass in the lookup and resolvable suites (a per-test fingerprint-memo reset closes an id()-reuse cache-poisoning flake the hoist exposed), inject generation names into the nfs backup script test instead of sleeping, and narrow ansible-lint scope to real ansible content. ansible-lint additionally sits behind a content fingerprint gate that skips the run when nothing it walks has changed; without a usable git (container bind mounts) the gate disables itself and the full run remains. Drop the import-linter layer and ignore for the deleted deps module; the contract now locks compose -> common -> env. Measured on the host: unit 101s to 20s, integration 90s to 24s, lint tests 43s to 19s, repeated ansible-lint 53s to 0.4s. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
9b4d002052
commit
1a908a6e5b
13 files changed
+257
-65
No files matched your search
+4
-2
@@ -14,9 +14,11 @@ exclude_paths:
|
||||
- .cache/
|
||||
- .claude/
|
||||
- .github/
|
||||
- /cli/
|
||||
- compose/
|
||||
- tests/integration/fixtures/
|
||||
- tests/lint/fixtures/
|
||||
- /docs/
|
||||
- /tests/
|
||||
- /utils/
|
||||
- venv/
|
||||
- .venv/
|
||||
- build/
|
||||
|
||||
+98
-18
@@ -22,16 +22,19 @@
|
||||
"WebFetch(domain:accounts.google.com)",
|
||||
"WebFetch(domain:accounts.hetzner.com)",
|
||||
"WebFetch(domain:addons.mozilla.org)",
|
||||
"WebFetch(domain:admin-docs.zammad.org)",
|
||||
"WebFetch(domain:agents.md)",
|
||||
"WebFetch(domain:agilemanifesto.org)",
|
||||
"WebFetch(domain:akaunting.com)",
|
||||
"WebFetch(domain:amble.blog)",
|
||||
"WebFetch(domain:ansible.readthedocs.io)",
|
||||
"WebFetch(domain:api.bluesky)",
|
||||
"WebFetch(domain:api.bsky.app)",
|
||||
"WebFetch(domain:api.cloudflare.com)",
|
||||
"WebFetch(domain:api.maptiler.com)",
|
||||
"WebFetch(domain:api.openai.com)",
|
||||
"WebFetch(domain:api.telegram.org)",
|
||||
"WebFetch(domain:apps.odoo.com)",
|
||||
"WebFetch(domain:archive.ubuntu.com)",
|
||||
"WebFetch(domain:archlinux.org)",
|
||||
"WebFetch(domain:askubuntu.com)",
|
||||
@@ -56,6 +59,7 @@
|
||||
"WebFetch(domain:bsky.app)",
|
||||
"WebFetch(domain:bsky.network)",
|
||||
"WebFetch(domain:bsky.social)",
|
||||
"WebFetch(domain:bugs.launchpad.net)",
|
||||
"WebFetch(domain:bugzilla.redhat.com)",
|
||||
"WebFetch(domain:business.adobe.com)",
|
||||
"WebFetch(domain:buymeacoffee.com)",
|
||||
@@ -68,6 +72,7 @@
|
||||
"WebFetch(domain:certbot.eff.org)",
|
||||
"WebFetch(domain:charts.bitnami.com)",
|
||||
"WebFetch(domain:check.spamhaus.org)",
|
||||
"WebFetch(domain:checkmk.com)",
|
||||
"WebFetch(domain:chromewebstore.google.com)",
|
||||
"WebFetch(domain:clients2.google.com)",
|
||||
"WebFetch(domain:cloud.veen.world)",
|
||||
@@ -99,64 +104,122 @@
|
||||
"WebFetch(domain:developer.wordpress.org)",
|
||||
"WebFetch(domain:developers.cloudflare.com)",
|
||||
"WebFetch(domain:discuss.frappe.io)",
|
||||
"WebFetch(domain:distribution.github.io)",
|
||||
"WebFetch(domain:dl-cdn.alpinelinux.org)",
|
||||
"WebFetch(domain:dl.fedoraproject.org)",
|
||||
"WebFetch(domain:dnf.readthedocs.io)",
|
||||
"WebFetch(domain:docker-registry.kixdesk.com)",
|
||||
"WebFetch(domain:docs.anthropic.com)",
|
||||
"WebFetch(domain:docs.bigbluebutton.org)",
|
||||
"WebFetch(domain:docs.bsky.app)",
|
||||
"WebFetch(domain:docs.checkmk.com)",
|
||||
"WebFetch(domain:docs.decidim.org)",
|
||||
"WebFetch(domain:docs.docker.com)",
|
||||
"WebFetch(domain:docs.ejabberd.im)",
|
||||
"WebFetch(domain:docs.espocrm.com)",
|
||||
"WebFetch(domain:docs.flowiseai.com)",
|
||||
"WebFetch(domain:docs.frappe.io)",
|
||||
"WebFetch(domain:docs.funkwhale.audio)",
|
||||
"WebFetch(domain:docs.gitlab.com)",
|
||||
"WebFetch(domain:docs.hetzner.com)",
|
||||
"WebFetch(domain:docs.joinbookwyrm.com)",
|
||||
"WebFetch(domain:docs.joinmastodon.org)",
|
||||
"WebFetch(domain:docs.joinpeertube.org)",
|
||||
"WebFetch(domain:docs.joomla.org)",
|
||||
"WebFetch(domain:docs.kernel.org)",
|
||||
"WebFetch(domain:docs.kixdesk.com)",
|
||||
"WebFetch(domain:docs.libretro.com)",
|
||||
"WebFetch(domain:docs.livekit.io)",
|
||||
"WebFetch(domain:docs.mattermost.com)",
|
||||
"WebFetch(domain:docs.mau.fi)",
|
||||
"WebFetch(domain:docs.moodle.org)",
|
||||
"WebFetch(domain:docs.n8n.io)",
|
||||
"WebFetch(domain:docs.nextcloud.com)",
|
||||
"WebFetch(domain:docs.opencloud.eu)",
|
||||
"WebFetch(domain:docs.opentalk.eu)",
|
||||
"WebFetch(domain:docs.openwebui.com)",
|
||||
"WebFetch(domain:docs.pixelfed.org)",
|
||||
"WebFetch(domain:docs.pretix.eu)",
|
||||
"WebFetch(domain:docs.redhat.com)",
|
||||
"WebFetch(domain:docs.semaphoreui.com)",
|
||||
"WebFetch(domain:docs.suitecrm.com)",
|
||||
"WebFetch(domain:docs.zammad.org)",
|
||||
"WebFetch(domain:download-center.epson.com)",
|
||||
"WebFetch(domain:download.ebz.epson.net)",
|
||||
"WebFetch(domain:download.ebz.epson.net)",
|
||||
"WebFetch(domain:download.rockylinux.org)",
|
||||
"WebFetch(domain:dyn.manpages.debian.org)",
|
||||
"WebFetch(domain:element.io)",
|
||||
"WebFetch(domain:elixir.bootlin.com)",
|
||||
"WebFetch(domain:en.wikipedia.org)",
|
||||
"WebFetch(domain:en.wordpress.org)",
|
||||
"WebFetch(domain:erpnext.com)",
|
||||
"WebFetch(domain:eslint.org)",
|
||||
"WebFetch(domain:evangelostsak.com)",
|
||||
"WebFetch(domain:events.bsky.app)",
|
||||
"WebFetch(domain:extensions.gnome.org)",
|
||||
"WebFetch(domain:extensions.joomla.org)",
|
||||
"WebFetch(domain:extensions.xwiki.org)",
|
||||
"WebFetch(domain:fed.brid.gy)",
|
||||
"WebFetch(domain:federationtester.matrix.org)",
|
||||
"WebFetch(domain:fediwall.social)",
|
||||
"WebFetch(domain:fedoraproject.org)",
|
||||
"WebFetch(domain:flowiseai.com)",
|
||||
"WebFetch(domain:fonts.bunny.net)",
|
||||
"WebFetch(domain:forum.checkmk.com)",
|
||||
"WebFetch(domain:forum.collaboraonline.com)",
|
||||
"WebFetch(domain:forum.keycloak.org)",
|
||||
"WebFetch(domain:forum.manjaro.org)",
|
||||
"WebFetch(domain:forum.openwrt.org)",
|
||||
"WebFetch(domain:forums.docker.com)",
|
||||
"WebFetch(domain:fossies.org)",
|
||||
"WebFetch(domain:frappe.io)",
|
||||
"WebFetch(domain:frappeframework.com)",
|
||||
"WebFetch(domain:ftp.debian.org)",
|
||||
"WebFetch(domain:fvonline-db.bplaced.net)",
|
||||
"WebFetch(domain:gcc.gnu.org)",
|
||||
"WebFetch(domain:geminicli.com)",
|
||||
"WebFetch(domain:gergap.de)",
|
||||
"WebFetch(domain:gergap.de)",
|
||||
"WebFetch(domain:getcomposer.org)",
|
||||
"WebFetch(domain:getfider.com)",
|
||||
"WebFetch(domain:gh.io)",
|
||||
"WebFetch(domain:git.coopcloud.tech)",
|
||||
"WebFetch(domain:git.friendi.ca)",
|
||||
"WebFetch(domain:git.kernel.org)",
|
||||
"WebFetch(domain:git.linux-nfs.org)",
|
||||
"WebFetch(domain:git.yoctoproject.org)",
|
||||
"WebFetch(domain:gitea.fudaoyuan.icu)",
|
||||
"WebFetch(domain:gitea.io)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:gitlab.com)",
|
||||
"WebFetch(domain:gitlab.opencode.de)",
|
||||
"WebFetch(domain:gohugo.io)",
|
||||
"WebFetch(domain:grep.app)",
|
||||
"WebFetch(domain:help.penpot.app)",
|
||||
"WebFetch(domain:help.ubuntu.com)",
|
||||
"WebFetch(domain:helpcenter.onlyoffice.com)",
|
||||
"WebFetch(domain:hexdocs.pm)",
|
||||
"WebFetch(domain:hluk.github.io)",
|
||||
"WebFetch(domain:hub.docker.com)",
|
||||
"WebFetch(domain:img.shields.io)",
|
||||
"WebFetch(domain:jellyfin.org)",
|
||||
"WebFetch(domain:jitsi.github.io)",
|
||||
"WebFetch(domain:jitsi.org)",
|
||||
"WebFetch(domain:joeeey.com)",
|
||||
"WebFetch(domain:joinmastodon.org)",
|
||||
"WebFetch(domain:jrnl.sh)",
|
||||
"WebFetch(domain:ka-f.fontawesome.com)",
|
||||
"WebFetch(domain:keepassxc.org)",
|
||||
"WebFetch(domain:kmpontech.wordpress.com)",
|
||||
"WebFetch(domain:learn.microsoft.com)",
|
||||
"WebFetch(domain:lima-vm.io)",
|
||||
"WebFetch(domain:links.sonatype.com)",
|
||||
"WebFetch(domain:linux-nfs.org)",
|
||||
"WebFetch(domain:listmonk.app)",
|
||||
"WebFetch(domain:lore.kernel.org)",
|
||||
"WebFetch(domain:magento.com)",
|
||||
"WebFetch(domain:mailu.io)",
|
||||
"WebFetch(domain:man.archlinux.org)",
|
||||
"WebFetch(domain:man.freebsd.org)",
|
||||
"WebFetch(domain:man7.org)",
|
||||
@@ -175,12 +238,15 @@
|
||||
"WebFetch(domain:micro-editor.github.io)",
|
||||
"WebFetch(domain:mobilizon.org)",
|
||||
"WebFetch(domain:moodle.org)",
|
||||
"WebFetch(domain:moodledev.io)",
|
||||
"WebFetch(domain:mxtoolbox.com)",
|
||||
"WebFetch(domain:my.telegram.org)",
|
||||
"WebFetch(domain:myportfolio.com)",
|
||||
"WebFetch(domain:myportfolio.com)",
|
||||
"WebFetch(domain:n8n.io)",
|
||||
"WebFetch(domain:nektos.com)",
|
||||
"WebFetch(domain:neovim.io)",
|
||||
"WebFetch(domain:next.erp)",
|
||||
"WebFetch(domain:nextcloud.com)",
|
||||
"WebFetch(domain:nexus.xwiki.org)",
|
||||
"WebFetch(domain:nfsv4.linux-nfs.narkive.com)",
|
||||
@@ -194,13 +260,18 @@
|
||||
"WebFetch(domain:ollama.com)",
|
||||
"WebFetch(domain:oneuptime.com)",
|
||||
"WebFetch(domain:open.spotify.com)",
|
||||
"WebFetch(domain:opencloud.eu)",
|
||||
"WebFetch(domain:opencollective.com)",
|
||||
"WebFetch(domain:openid.net)",
|
||||
"WebFetch(domain:openresty.org)",
|
||||
"WebFetch(domain:opensearch.org)",
|
||||
"WebFetch(domain:opentalk.eu)",
|
||||
"WebFetch(domain:openwebui.com)",
|
||||
"WebFetch(domain:owncloud.dev)",
|
||||
"WebFetch(domain:packages.debian.org)",
|
||||
"WebFetch(domain:patchwork.kernel.org)",
|
||||
"WebFetch(domain:penpot.app)",
|
||||
"WebFetch(domain:pi-hole.net)",
|
||||
"WebFetch(domain:piehost.com)",
|
||||
"WebFetch(domain:pixelfed.org)",
|
||||
"WebFetch(domain:pkg.go.dev)",
|
||||
@@ -225,8 +296,10 @@
|
||||
"WebFetch(domain:ralph.blog.imixs.com)",
|
||||
"WebFetch(domain:ramblings.narrabilis.com)",
|
||||
"WebFetch(domain:ramblings.narrabilis.com)",
|
||||
"WebFetch(domain:raw.git)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"WebFetch(domain:redis.io)",
|
||||
"WebFetch(domain:refspecs.linuxfoundation.org)",
|
||||
"WebFetch(domain:registry-1.docker.io)",
|
||||
"WebFetch(domain:registry.gitlab.com)",
|
||||
@@ -248,6 +321,8 @@
|
||||
"WebFetch(domain:security.debian.org)",
|
||||
"WebFetch(domain:security.ubuntu.com)",
|
||||
"WebFetch(domain:security.ubuntu.com)",
|
||||
"WebFetch(domain:semaphoreui.com)",
|
||||
"WebFetch(domain:semver.org)",
|
||||
"WebFetch(domain:senders.yahooinc.com)",
|
||||
"WebFetch(domain:sentry.com)",
|
||||
"WebFetch(domain:sentry.matrix.org)",
|
||||
@@ -262,6 +337,7 @@
|
||||
"WebFetch(domain:stackoverflow.com)",
|
||||
"WebFetch(domain:store.shopware.com)",
|
||||
"WebFetch(domain:suitecrm.com)",
|
||||
"WebFetch(domain:support.atlassian.com)",
|
||||
"WebFetch(domain:systemd.io)",
|
||||
"WebFetch(domain:t.me)",
|
||||
"WebFetch(domain:taiga.io)",
|
||||
@@ -287,6 +363,7 @@
|
||||
"WebFetch(domain:wiki.manjaro.org)",
|
||||
"WebFetch(domain:wireguard.com)",
|
||||
"WebFetch(domain:wolfgang.gassler.org)",
|
||||
"WebFetch(domain:wordpress.org)",
|
||||
"WebFetch(domain:www.2daygeek.com)",
|
||||
"WebFetch(domain:www.arduino.cc)",
|
||||
"WebFetch(domain:www.atlassian.com)",
|
||||
@@ -324,6 +401,7 @@
|
||||
"WebFetch(domain:www.man7.org)",
|
||||
"WebFetch(domain:www.mediawiki.org)",
|
||||
"WebFetch(domain:www.min.io)",
|
||||
"WebFetch(domain:www.npmjs.com)",
|
||||
"WebFetch(domain:www.odoo.com)",
|
||||
"WebFetch(domain:www.openldap.org)",
|
||||
"WebFetch(domain:www.openproject.org)",
|
||||
@@ -338,6 +416,7 @@
|
||||
"WebFetch(domain:www.postgresql.org)",
|
||||
"WebFetch(domain:www.primary.com)",
|
||||
"WebFetch(domain:www.python.org)",
|
||||
"WebFetch(domain:www.rabbitmq.com)",
|
||||
"WebFetch(domain:www.redhat.com)",
|
||||
"WebFetch(domain:www.retroarch.com)",
|
||||
"WebFetch(domain:www.rfc-editor.org)",
|
||||
@@ -361,6 +440,7 @@
|
||||
"WebFetch(domain:www.zsh.org)",
|
||||
"WebFetch(domain:yourls.org)",
|
||||
"WebFetch(domain:youtu.be)",
|
||||
"WebFetch(domain:zammad.org)",
|
||||
"WebFetch(domain:zoom.us)",
|
||||
"WebSearch",
|
||||
"Workflow",
|
||||
@@ -467,6 +547,24 @@
|
||||
"/tmp"
|
||||
]
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"caveman@caveman": true,
|
||||
"ponytail@ponytail": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"caveman": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "JuliusBrussee/caveman"
|
||||
}
|
||||
},
|
||||
"ponytail": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "DietrichGebert/ponytail"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sandbox": {
|
||||
"enabled": true,
|
||||
"failIfUnavailable": true,
|
||||
@@ -524,23 +622,5 @@
|
||||
"~/.kube"
|
||||
]
|
||||
}
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"caveman": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "JuliusBrussee/caveman"
|
||||
}
|
||||
},
|
||||
"ponytail": {
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "DietrichGebert/ponytail"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"caveman@caveman": true,
|
||||
"ponytail@ponytail": true
|
||||
}
|
||||
}
|
||||
@@ -473,8 +473,10 @@ network-trust-ca:
|
||||
@bash scripts/system/tls/trust/wsl2.sh
|
||||
|
||||
.PHONY: onboard
|
||||
# Set up a developer workstation end to end: dependencies, project setup, agent skills, and terminal aliases.
|
||||
# Set up a developer workstation end to end: dependencies, project setup, agent skills, terminal aliases, and the dev extras inside the running dev container.
|
||||
onboard: bootstrap install-skills install-alias
|
||||
@"$(MAKE)" compose-up
|
||||
@"$(MAKE)" compose-exec cmd="bash scripts/install/dev-extras.sh"
|
||||
|
||||
.PHONY: quality
|
||||
# Autoformat then run the full test suite in one shot (pre-commit gate).
|
||||
|
||||
+6
-6
@@ -29,6 +29,8 @@ dev = [
|
||||
"import-linter",
|
||||
"pre-commit",
|
||||
"git-maintainer-tools",
|
||||
"pytest>=9.1",
|
||||
"pytest-xdist",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -273,20 +275,18 @@ root_package = "cli"
|
||||
[[tool.importlinter.contracts]]
|
||||
# The dev/CI deploy package was repeatedly hit by CodeQL
|
||||
# `py/unsafe-cyclic-import`. The fix split the env/arg helpers into the
|
||||
# leaf module `env`, giving the load-time DAG compose -> common -> deps
|
||||
# -> env. This layers contract locks that order in: a lower layer
|
||||
# importing a higher one (i.e. a new cycle) breaks the build.
|
||||
name = "cli.administration.deploy.development stays acyclic (compose -> common -> deps -> env)"
|
||||
# leaf module `env`, giving the load-time DAG compose -> common -> env.
|
||||
# This layers contract locks that order in: a lower layer importing a
|
||||
# higher one (i.e. a new cycle) breaks the build.
|
||||
name = "cli.administration.deploy.development stays acyclic (compose -> common -> env)"
|
||||
type = "layers"
|
||||
layers = [
|
||||
"cli.administration.deploy.development.compose",
|
||||
"cli.administration.deploy.development.common",
|
||||
"cli.administration.deploy.development.deps",
|
||||
"cli.administration.deploy.development.env",
|
||||
]
|
||||
# Type-checking-only / lazily-deferred back-references to `compose` do
|
||||
# not run at import time, so they cannot form a load-time cycle.
|
||||
ignore_imports = [
|
||||
"cli.administration.deploy.development.common -> cli.administration.deploy.development.compose",
|
||||
"cli.administration.deploy.development.deps -> cli.administration.deploy.development.compose",
|
||||
]
|
||||
@@ -6,6 +6,10 @@
|
||||
# $1 SOURCE_DIR NFS export base to back up
|
||||
# $2 BACKUPS_DIR local backup root
|
||||
# $3 REPO_NAME repository directory name inside the machine hash dir
|
||||
#
|
||||
# Environment:
|
||||
# BKP_NFS_2_LOCAL_GENERATION optional generation name override
|
||||
# (defaults to the current timestamp)
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?usage: script.sh SOURCE_DIR BACKUPS_DIR REPO_NAME}"
|
||||
@@ -19,7 +23,7 @@ fi
|
||||
|
||||
MACHINE_HASH="$(sha256sum /etc/machine-id | cut -c1-64)"
|
||||
REPO_DIR="${BACKUPS_DIR%/}/${MACHINE_HASH}/${REPO_NAME}"
|
||||
GENERATION="$(date +%Y%m%d%H%M%S)"
|
||||
GENERATION="${BKP_NFS_2_LOCAL_GENERATION:-$(date +%Y%m%d%H%M%S)}"
|
||||
DEST_DIR="${REPO_DIR}/${GENERATION}/files"
|
||||
|
||||
PREVIOUS_FILES=""
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install the dev extras into the resolved venv once. A stamp inside the
|
||||
# venv skips reruns until pyproject.toml changes or the venv is recreated,
|
||||
# so the container-side install disappears together with its venv.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
# shellcheck source=scripts/meta/env/python.sh
|
||||
source scripts/meta/env/python.sh
|
||||
: "${VENV:?no venv resolved}"
|
||||
|
||||
STAMP="${VENV%/}/.dev-extras.stamp"
|
||||
|
||||
if [[ ! -w "${VENV}" ]]; then
|
||||
echo "[dev-extras] venv not writable (${VENV}); skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -f "${STAMP}" && "${STAMP}" -nt pyproject.toml ]]; then
|
||||
echo "[dev-extras] up to date (${STAMP})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
bash scripts/install/python.sh dev
|
||||
touch "${STAMP}"
|
||||
echo "[dev-extras] installed and stamped (${STAMP})"
|
||||
+37
-4
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
|
||||
# Syntax-check the playbook, then run ansible-lint behind a content
|
||||
# fingerprint gate: when no file ansible-lint walks (or its config) changed
|
||||
# since the last green run, the lint is skipped; any change triggers the
|
||||
# full run again, so coverage never shrinks.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -20,8 +23,38 @@ ansible_args+=(playbook.yml --syntax-check)
|
||||
|
||||
ansible-playbook "${ansible_args[@]}"
|
||||
|
||||
if command -v ansible-lint >/dev/null 2>&1; then
|
||||
ansible-lint
|
||||
else
|
||||
if ! command -v ansible-lint >/dev/null 2>&1; then
|
||||
echo "ansible-lint not installed; skipping. Run 'make install-lint' first." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LINT_SCOPE=(roles tasks group_vars host_vars library plugins playbook.yml ansible.cfg .ansible-lint .ansible-lint-ignore)
|
||||
STAMP="build/ansible-lint.stamp"
|
||||
|
||||
fingerprint() {
|
||||
{
|
||||
git rev-parse HEAD 2>/dev/null
|
||||
git ls-files -s -- "${LINT_SCOPE[@]}" 2>/dev/null
|
||||
git status --porcelain -- "${LINT_SCOPE[@]}" 2>/dev/null |
|
||||
awk '{print $NF}' | sort -u |
|
||||
while IFS= read -r dirty; do
|
||||
[ -f "${dirty}" ] && sha256sum "${dirty}"
|
||||
done
|
||||
} | sha256sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
current=""
|
||||
if git rev-parse HEAD >/dev/null 2>&1; then
|
||||
current="$(fingerprint)"
|
||||
fi
|
||||
if [[ -n "${current}" && -f "${STAMP}" && "$(cat "${STAMP}")" == "${current}" ]]; then
|
||||
echo "ansible-lint: no lintable file changed since the last green run; skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ansible-lint
|
||||
|
||||
if [[ -n "${current}" ]]; then
|
||||
mkdir -p "$(dirname "${STAMP}")"
|
||||
printf '%s\n' "${current}" >"${STAMP}"
|
||||
fi
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the actual unittest discover invocation for the test suite picked
|
||||
# by INFINITO_TEST_TYPE / INFINITO_TEST_PATTERN. Invoked either directly on the host or
|
||||
# Runs the test suite picked by INFINITO_TEST_TYPE / INFINITO_TEST_PATTERN:
|
||||
# parallel via pytest-xdist when pytest and xdist are importable, serial
|
||||
# unittest discover otherwise. Invoked either directly on the host or
|
||||
# inside the infinito compose container by scripts/tests/code/wrapper.sh
|
||||
# -- the body is identical for both runners.
|
||||
set -euo pipefail
|
||||
@@ -22,4 +23,22 @@ if [ -n "${PYTHON:-}" ]; then
|
||||
fi
|
||||
|
||||
make setup
|
||||
"${PYTHON}" -m unittest discover -s "tests/${INFINITO_TEST_TYPE}" -t . -p "${INFINITO_TEST_PATTERN}" # nocheck: makefile-supplied
|
||||
bash "$(dirname "${BASH_SOURCE[0]}")/../../install/dev-extras.sh" || echo "dev extras install failed; running serial" >&2
|
||||
_suite_dir="tests/${INFINITO_TEST_TYPE}" # nocheck: makefile-supplied
|
||||
if "${PYTHON}" -c 'import pytest, xdist' >/dev/null 2>&1; then
|
||||
# Exception: consider_namespace_packages + importlib import mode are
|
||||
# required for hyphenated tests/unit/roles/<role>/ dirs; pytest's default
|
||||
# identifier walk-up collapses every role's filter_plugins/files package
|
||||
# onto one top-level name and cross-imports the wrong module.
|
||||
# python_functions= pins collection to unittest-style TestCase methods,
|
||||
# the exact set `unittest discover` runs. PYTHONDONTWRITEBYTECODE keeps
|
||||
# namespace probing from dropping __pycache__ into roles/.
|
||||
PYTHONDONTWRITEBYTECODE=1 "${PYTHON}" -m pytest "${_suite_dir}" -q -n auto -rP -p no:cacheprovider \
|
||||
--import-mode=importlib \
|
||||
-o consider_namespace_packages=true \
|
||||
-o "norecursedirs=.* *.egg dist node_modules venv" \
|
||||
-o python_functions= \
|
||||
-o "python_files=${INFINITO_TEST_PATTERN}"
|
||||
else
|
||||
"${PYTHON}" -m unittest discover -s "${_suite_dir}" -t . -p "${INFINITO_TEST_PATTERN}"
|
||||
fi
|
||||
@@ -12,11 +12,14 @@ from . import PROJECT_ROOT
|
||||
class TestServicesResolvable(unittest.TestCase):
|
||||
"""Discovered service keys and provider roles must resolve via the service lookup."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.repo_root = PROJECT_ROOT
|
||||
cls.roles_dir = cls.repo_root / "roles"
|
||||
cls.applications = load_applications_from_roles_dir(cls.roles_dir)
|
||||
cls.service_registry = build_service_registry_from_roles_dir(cls.roles_dir)
|
||||
|
||||
def setUp(self):
|
||||
self.repo_root = PROJECT_ROOT
|
||||
self.roles_dir = self.repo_root / "roles"
|
||||
self.applications = load_applications_from_roles_dir(self.roles_dir)
|
||||
self.service_registry = build_service_registry_from_roles_dir(self.roles_dir)
|
||||
self.assertGreater(
|
||||
len(self.service_registry),
|
||||
0,
|
||||
|
||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
from ansible.errors import AnsibleError
|
||||
|
||||
from plugins.lookup.applications import LookupModule, _reset_cache_for_tests
|
||||
from utils.cache import base as cache_base
|
||||
from utils.cache.yaml import dump_yaml_str
|
||||
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
||||
|
||||
@@ -19,13 +20,21 @@ def _write_config(base_dir: Path, application_id: str, config: dict) -> None:
|
||||
|
||||
|
||||
class TestApplicationsLookup(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
_reset_cache_for_tests()
|
||||
cls.addClassCleanup(_reset_cache_for_tests)
|
||||
|
||||
def setUp(self) -> None:
|
||||
# Exception: the fingerprint memo keys by id(); a GC'd variables dict
|
||||
# can hand its id to the next test's dict and alias a stale signature,
|
||||
# so it must clear per test despite the class-shared caches.
|
||||
cache_base._reset()
|
||||
self.lookup = LookupModule()
|
||||
self._cwd = str(Path.cwd())
|
||||
self._tmpdir = tempfile.TemporaryDirectory()
|
||||
self._tmp = Path(self._tmpdir.name)
|
||||
os.chdir(self._tmp)
|
||||
_reset_cache_for_tests()
|
||||
|
||||
_write_config(
|
||||
self._tmp,
|
||||
@@ -35,13 +44,10 @@ class TestApplicationsLookup(unittest.TestCase):
|
||||
_write_config(self._tmp, "web-app-bar", {})
|
||||
|
||||
def tearDown(self) -> None:
|
||||
_reset_cache_for_tests()
|
||||
os.chdir(self._cwd)
|
||||
self._tmpdir.cleanup()
|
||||
|
||||
def test_returns_full_mapping(self) -> None:
|
||||
# Per the file root of meta/services.yml is the services map,
|
||||
# so the loader puts it under `services` in the materialised payload.
|
||||
result = self.lookup.run([], variables={}, roles_dir=str(self._tmp / "roles"))[
|
||||
0
|
||||
]
|
||||
|
||||
@@ -10,6 +10,7 @@ from ansible.errors import AnsibleError
|
||||
|
||||
from plugins.lookup.config import LookupModule
|
||||
from utils.cache import _reset_cache_for_tests
|
||||
from utils.cache import base as cache_base
|
||||
from utils.cache.yaml import dump_yaml_str
|
||||
from utils.roles.applications.config import AppConfigKeyError, ConfigEntryNotSetError
|
||||
from utils.roles.mapping import (
|
||||
@@ -62,18 +63,23 @@ class _DummyTemplar:
|
||||
|
||||
|
||||
class TestConfigLookup(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.lm = LookupModule()
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
_reset_cache_for_tests()
|
||||
cls.addClassCleanup(_reset_cache_for_tests)
|
||||
|
||||
# Create a temp working directory in /tmp and chdir into it
|
||||
def setUp(self) -> None:
|
||||
# Exception: the fingerprint memo keys by id(); a GC'd variables dict
|
||||
# can hand its id to the next test's dict and alias a stale signature,
|
||||
# so it must clear per test despite the class-shared caches.
|
||||
cache_base._reset()
|
||||
self.lm = LookupModule()
|
||||
self._cwd = str(Path.cwd())
|
||||
self._tmpdir = tempfile.TemporaryDirectory()
|
||||
self._tmp = Path(self._tmpdir.name)
|
||||
os.chdir(self._tmp)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
_reset_cache_for_tests()
|
||||
os.chdir(self._cwd)
|
||||
self._tmpdir.cleanup()
|
||||
|
||||
@@ -86,8 +92,6 @@ class TestConfigLookup(unittest.TestCase):
|
||||
self.lm.run(["a", "b", "c", "d"], variables={"applications": {}})
|
||||
|
||||
def test_resolves_from_roles_without_applications_var(self) -> None:
|
||||
# meta/services.yml file root IS the services map: the
|
||||
# consumer path therefore reads `services.<entity>.<…>`.
|
||||
_write_config(
|
||||
self._tmp,
|
||||
"web-app-foo",
|
||||
@@ -163,7 +167,7 @@ class TestConfigLookup(unittest.TestCase):
|
||||
_write_schema(
|
||||
self._tmp,
|
||||
"web-app-foo",
|
||||
{"smtp": {"host": {}, "port": {}}}, # port is defined in schema
|
||||
{"smtp": {"host": {}, "port": {}}},
|
||||
)
|
||||
variables = {
|
||||
"applications": {"web-app-foo": {"smtp": {"host": "mail.example.org"}}}
|
||||
|
||||
@@ -11,6 +11,7 @@ from ansible.errors import AnsibleError
|
||||
|
||||
from plugins.lookup.email import LookupModule
|
||||
from utils.cache import _reset_cache_for_tests
|
||||
from utils.cache import base as cache_base
|
||||
from utils.cache import users as cache_users
|
||||
from utils.cache.yaml import dump_yaml_str
|
||||
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
||||
@@ -23,7 +24,16 @@ def _write_role_config(base_dir: Path, role_name: str, payload: dict) -> None:
|
||||
|
||||
|
||||
class TestEmailLookup(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
_reset_cache_for_tests()
|
||||
cls.addClassCleanup(_reset_cache_for_tests)
|
||||
|
||||
def setUp(self) -> None:
|
||||
# Exception: the fingerprint memo keys by id(); a GC'd variables dict
|
||||
# can hand its id to the next test's dict and alias a stale signature,
|
||||
# so it must clear per test despite the class-shared caches.
|
||||
cache_base._reset()
|
||||
self.lookup = LookupModule()
|
||||
self.lookup._templar = None
|
||||
self._cwd = str(Path.cwd())
|
||||
@@ -31,7 +41,6 @@ class TestEmailLookup(unittest.TestCase):
|
||||
self._tmp = Path(self._tmpdir.name)
|
||||
(self._tmp / "roles").mkdir(parents=True, exist_ok=True)
|
||||
os.chdir(self._tmp)
|
||||
_reset_cache_for_tests()
|
||||
self._tokens_store_patcher = patch.object(
|
||||
cache_users, "_load_store_users", return_value={}
|
||||
)
|
||||
@@ -39,7 +48,6 @@ class TestEmailLookup(unittest.TestCase):
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._tokens_store_patcher.stop()
|
||||
_reset_cache_for_tests()
|
||||
os.chdir(self._cwd)
|
||||
self._tmpdir.cleanup()
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ into older generations."""
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -29,7 +28,10 @@ class NfsBackupScriptTests(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _run(self, source=None):
|
||||
def _run(self, source=None, generation=None):
|
||||
env = os.environ.copy()
|
||||
if generation is not None:
|
||||
env["BKP_NFS_2_LOCAL_GENERATION"] = generation
|
||||
return subprocess.run(
|
||||
[
|
||||
"bash",
|
||||
@@ -41,6 +43,7 @@ class NfsBackupScriptTests(unittest.TestCase):
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
env=env,
|
||||
)
|
||||
|
||||
def _generations(self):
|
||||
@@ -68,9 +71,8 @@ class NfsBackupScriptTests(unittest.TestCase):
|
||||
|
||||
def test_second_run_hardlinks_unchanged_files(self):
|
||||
(self.source / "a.txt").write_text("hello")
|
||||
self.assertEqual(self._run().returncode, 0)
|
||||
time.sleep(1.1)
|
||||
self.assertEqual(self._run().returncode, 0)
|
||||
self.assertEqual(self._run(generation="20240101000000").returncode, 0)
|
||||
self.assertEqual(self._run(generation="20240101000001").returncode, 0)
|
||||
|
||||
generations = self._generations()
|
||||
self.assertEqual(len(generations), 2)
|
||||
@@ -80,11 +82,12 @@ class NfsBackupScriptTests(unittest.TestCase):
|
||||
self.assertEqual(second.stat().st_nlink, 2)
|
||||
|
||||
def test_changed_file_gets_own_inode_and_history_survives(self):
|
||||
(self.source / "a.txt").write_text("v1")
|
||||
self.assertEqual(self._run().returncode, 0)
|
||||
time.sleep(1.1)
|
||||
(self.source / "a.txt").write_text("v2")
|
||||
self.assertEqual(self._run().returncode, 0)
|
||||
changed = self.source / "a.txt"
|
||||
changed.write_text("v1")
|
||||
os.utime(changed, (1000000000, 1000000000))
|
||||
self.assertEqual(self._run(generation="20240101000000").returncode, 0)
|
||||
changed.write_text("v2")
|
||||
self.assertEqual(self._run(generation="20240101000001").returncode, 0)
|
||||
|
||||
generations = self._generations()
|
||||
first = generations[0] / "files" / "a.txt"
|
||||
@@ -110,10 +113,9 @@ class NfsBackupScriptTests(unittest.TestCase):
|
||||
def test_deleted_file_vanishes_only_from_new_generation(self):
|
||||
(self.source / "keep.txt").write_text("keep")
|
||||
(self.source / "gone.txt").write_text("gone")
|
||||
self.assertEqual(self._run().returncode, 0)
|
||||
time.sleep(1.1)
|
||||
self.assertEqual(self._run(generation="20240101000000").returncode, 0)
|
||||
(self.source / "gone.txt").unlink()
|
||||
self.assertEqual(self._run().returncode, 0)
|
||||
self.assertEqual(self._run(generation="20240101000001").returncode, 0)
|
||||
|
||||
generations = self._generations()
|
||||
self.assertTrue((generations[0] / "files" / "gone.txt").is_file())
|
||||
|
||||
Reference in new issue
Block a user