Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fac06cf
build(devcontainer): bring the devcontainer subsystem into this PR
marcinpsk Sep 9, 2026
7d7fed2
test: give every pytest worker private databases and one plugin
marcinpsk Sep 6, 2026
56a9ad1
test: build the repeated DCIM fixtures through shared helpers
marcinpsk Sep 6, 2026
4018aa1
test: enforce the family seam and the comment-block rule mechanically
marcinpsk Sep 6, 2026
0135ded
build(devcontainer): stop the container caching bytecode into the hos…
marcinpsk Sep 6, 2026
45e205d
test(devcontainer): render the compose files a recreate actually uses
marcinpsk Sep 6, 2026
674e096
fix: drop migration dependencies on a squashed-away dcim node
marcinpsk Sep 6, 2026
ba8aba4
test: report every dangling migration dependency, not just the first
marcinpsk Sep 6, 2026
9cb5640
build(devcontainer): install ripgrep for agent searches
marcinpsk Sep 7, 2026
07b6880
fix: close four review findings in the isolation and guard tests
marcinpsk Sep 7, 2026
aa80ef8
fix: match pytest options written as --option=value
marcinpsk Sep 7, 2026
8ffde69
fix: set the worker's Redis targets in the settings shim, not only in…
marcinpsk Sep 7, 2026
6443bf0
fix(tests): preserve Redis credentials without passwords
marcinpsk Sep 8, 2026
c7bbfab
test: write the base-row collision setup as an out-of-band rename
marcinpsk Sep 9, 2026
6313c80
fix(tests): preserve targets and strengthen repository guards
marcinpsk Sep 10, 2026
1582a59
fix(tests): distinguish command prefixes and file modes
marcinpsk Sep 10, 2026
ce415c9
fix(tests): detect pytest in chained shell commands
marcinpsk Sep 10, 2026
4bad0da
fix(tests): inspect workflow run commands for pytest
marcinpsk Sep 11, 2026
86f9d31
fix(tests): classify pytest commands with shell syntax
marcinpsk Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .devcontainer/config/isolated_test_settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
#
# Isolated test-database settings shim.
# Isolated test settings for the performance runner.
#
# The plugin's pytest suite uses ``netbox_interface_name_rules.tests.isolated_settings``, which
# gives every worker private databases. This shim serves ``manage.py test`` runs only: the
# performance runner measures the plugin under the devcontainer's full plugin list, so narrowing
# it here would change the environment the committed baselines were taken in.
#
# Django names the test database ``test_<DB_NAME>`` (here: ``test_netbox``), so two
# ``manage.py test`` runs in the same devcontainer collide on a single test DB and
Expand All @@ -22,3 +27,10 @@
_name = _os.environ.get("TEST_DB_NAME")
if _name:
DATABASES["default"].setdefault("TEST", {})["NAME"] = _name # noqa: F405

# NetBox writes the search cache inline only when no RQ worker serves the queue, so
# ``TEST_REDIS_DB`` moves the queues off the database the devcontainer's worker holds.
_redis_db = _os.environ.get("TEST_REDIS_DB")
if _redis_db:
for _queue in RQ_QUEUES.values(): # noqa: F405
_queue["DB"] = int(_redis_db)
21 changes: 17 additions & 4 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@ services:
SUPERUSER_EMAIL: ${SUPERUSER_EMAIL:-admin@example.com}
SUPERUSER_PASSWORD: ${SUPERUSER_PASSWORD:-admin}
SKIP_SUPERUSER: ${SKIP_SUPERUSER:-false}
# The container is root and mounts the host checkout, so cached bytecode lands there owned
# by root. `-p no:cacheprovider` does not prevent it.
PYTHONDONTWRITEBYTECODE: "1"
# Proxy settings (optional)
HTTP_PROXY: ${HTTP_PROXY:-}
HTTPS_PROXY: ${HTTPS_PROXY:-}
http_proxy: ${HTTP_PROXY:-}
https_proxy: ${HTTPS_PROXY:-}
NO_PROXY: ${NO_PROXY:-}
no_proxy: ${NO_PROXY:-}
REQUESTS_CA_BUNDLE: ${REQUESTS_CA_BUNDLE:-}
SSL_CERT_FILE: ${SSL_CERT_FILE:-}
CURL_CA_BUNDLE: ${CURL_CA_BUNDLE:-}
# setup.sh installs any ca-bundle.crt into this store, so a host path here
# would only name a file the container cannot open.
REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
CURL_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
depends_on:
postgres:
condition: service_healthy
Expand All @@ -45,7 +50,12 @@ services:

postgres:
image: postgres:18
command: postgres -c max_connections=200
# PostgreSQL 18 allocates a dynamic shared memory segment at startup and fails
# to start on Docker's 64MB default /dev/shm.
shm_size: 1gb
# Parallel test runs open a connection per worker per test database, and this
# postgres serves several test databases at once.
command: postgres -c max_connections=800
environment:
POSTGRES_DB: ${DB_NAME:-netbox}
POSTGRES_USER: ${DB_USER:-netbox}
Expand Down Expand Up @@ -89,3 +99,6 @@ networks:
config:
# Pinned to avoid Docker auto-assigning 172.30.x.x
- subnet: "172.28.200.0/24"
# Dynamic assignment stays above .128 so no unpinned service (e.g. a local
# override) can take the static postgres/redis addresses setup.sh uses.
ip_range: "172.28.200.128/25"
19 changes: 14 additions & 5 deletions .devcontainer/scripts/load-aliases.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,23 @@ netbox-shell() {
cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py shell
}

# Run the plugin suite with pytest, the runner CI uses. The settings module gives each xdist
# worker private PostgreSQL and Redis databases, so concurrent suites in the shared devcontainer
# do not collide. Override the targets with TEST_DB_NAME=... / TEST_REDIS_HOST=... .
netbox-test() {
cd /opt/netbox/netbox && source /opt/netbox/venv/bin/activate && python manage.py test netbox_interface_name_rules "$@"
if [ "$#" -eq 0 ]; then
set -- netbox_interface_name_rules
fi
cd "$PLUGIN_DIR" && source /opt/netbox/venv/bin/activate && \
TEST_DB_NAME="${TEST_DB_NAME:-test_netbox_interface_name_rules}" \
TEST_REDIS_HOST="${TEST_REDIS_HOST:-redis}" \
pytest "$@"
}

# Run tests on a per-session ISOLATED test database, so concurrent suites in the
# shared devcontainer don't collide on test_netbox (which corrupts migrations and
# can leave lock-holding zombie connections). Pass the app(s) + any test flags,
# e.g. netbox-test-isolated netbox_nso_plugin --keepdb
# Run a Django-runner suite on an isolated test database. The plugin's own suite runs under
# pytest via netbox-test; this stays for the performance runner and for other apps.
# Sharing test_netbox corrupts migrations and can leave lock-holding zombie connections.
# Pass the app(s) + any test flags, e.g. netbox-test-isolated netbox_nso_plugin --keepdb
# Override the DB name with TEST_DB_NAME=...; otherwise it's derived from the first
# app argument (a stable name so --keepdb can reuse it).
netbox-test-isolated() {
Expand Down
3 changes: 2 additions & 1 deletion .devcontainer/scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ fi

echo "🔧 Installing development dependencies..."
apt-get update -qq
apt-get install -y -qq net-tools git
apt-get install -y -qq net-tools git ripgrep
# Dev tools used by the agent loop and pre-commit hooks. Keep in sync with
# the `dev` extras in pyproject.toml — at minimum, anything invoked by:
# - test + coverage runs: pytest, pytest-django, pytest-cov, pytest-xdist
Expand Down Expand Up @@ -172,6 +172,7 @@ if [ -z "$PLUGIN_WS_DIR" ]; then
fi
echo "📂 Plugin workspace: $PLUGIN_WS_DIR"
cd "$PLUGIN_WS_DIR"
$PIP_CMD install --group workflow-tests
$PIP_CMD install -e .
echo "✅ Installed $PLUGIN_NAME in editable mode"

Expand Down
18 changes: 18 additions & 0 deletions .devcontainer/scripts/tests/lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>

# Populate COMPOSE_FILES with the `-f` arguments a real recreate would use.
#
# The override is untracked and per-developer, but Docker applies it to every recreate. A check that
# renders only the base file cannot see a value the override replaces, so it would pass while the
# container it protects comes up wrong.
compose_file_args() {
local root="$1"
local override="$root/.devcontainer/docker-compose.override.yml"

COMPOSE_FILES=(-f "$root/.devcontainer/docker-compose.yml")
if [ -f "$override" ]; then
COMPOSE_FILES+=(-f "$override")
fi
}
46 changes: 46 additions & 0 deletions .devcontainer/scripts/tests/test-bytecode-writes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
# shellcheck source=.devcontainer/scripts/tests/lib.sh
source "$(dirname "$0")/lib.sh"
compose_file_args "$REPO_ROOT"

# The container runs as root and mounts the host checkout, so any bytecode Python caches lands in
# the developer's tree owned by root. Those files can block `git worktree remove` and host tooling.
docker compose "${COMPOSE_FILES[@]}" config --format json | python3 -c "
import json
import sys

configuration = json.load(sys.stdin)
services = configuration.get('services', {})

failures = []
for name, service in services.items():
# Only services that mount the host checkout can write into it.
mounts = service.get('volumes') or []
if not any(str(mount.get('target', '')).startswith('/workspaces') for mount in mounts):
continue
value = (service.get('environment') or {}).get('PYTHONDONTWRITEBYTECODE')
if str(value) != '1':
failures.append(f'{name} mounts the checkout but sets PYTHONDONTWRITEBYTECODE={value!r}')

if failures:
for failure in failures:
print(f'FAIL: {failure}', file=sys.stderr)
print(
'Root-owned .pyc files would accumulate in the host tree. '
'Set PYTHONDONTWRITEBYTECODE: \"1\" on that service.',
file=sys.stderr,
)
raise SystemExit(1)

if not services:
print('FAIL: the compose file declares no services', file=sys.stderr)
raise SystemExit(1)

print('Bytecode-write check passed (no checkout-mounting service caches bytecode)')
"
38 changes: 38 additions & 0 deletions .devcontainer/scripts/tests/test-ca-environment.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
# shellcheck source=.devcontainer/scripts/tests/lib.sh
source "$(dirname "$0")/lib.sh"
compose_file_args "$REPO_ROOT"
CONTAINER_CA_BUNDLE="/etc/ssl/certs/ca-certificates.crt"
HOST_CA_BUNDLE="/host-only/ca.pem"

config="$({
REQUESTS_CA_BUNDLE="$HOST_CA_BUNDLE" \
SSL_CERT_FILE="$HOST_CA_BUNDLE" \
CURL_CA_BUNDLE="$HOST_CA_BUNDLE" \
docker compose "${COMPOSE_FILES[@]}" config
})"

# Compare the rendered value as a string. A grep pattern would read every "." in a certificate
# path as a wildcard, so a wrong path such as ca-certificatesXcrt would satisfy the check.
for variable in REQUESTS_CA_BUNDLE SSL_CERT_FILE CURL_CA_BUNDLE; do
value="$(awk -v key="$variable:" '$1 == key { print $2; exit }' <<< "$config")"
value="${value%\"}"
value="${value#\"}"
if [ "$value" != "$CONTAINER_CA_BUNDLE" ]; then
echo "FAIL: $variable is '$value', not the container trust store $CONTAINER_CA_BUNDLE" >&2
exit 1
fi
done

if grep -qF "$HOST_CA_BUNDLE" <<< "$config"; then
echo "FAIL: a host CA path leaked into the container configuration" >&2
exit 1
fi

echo "Container CA environment check passed"
33 changes: 33 additions & 0 deletions .devcontainer/scripts/tests/test-netbox-test-targets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>

set -eo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
TEST_DIR="$(mktemp -d)"
trap 'rm -rf "$TEST_DIR"' EXIT
export PYTEST_ARGV="$TEST_DIR/argv"

source() {
if [ "$1" = /opt/netbox/venv/bin/activate ]; then
return 0
fi
builtin source "$@"
}
source "$REPO_ROOT/.devcontainer/scripts/load-aliases.sh"
cat > "$TEST_DIR/pytest" <<'STUB'
#!/bin/bash
printf '%s\n' "$@" > "$PYTEST_ARGV"
STUB
chmod +x "$TEST_DIR/pytest"
export PATH="$TEST_DIR:$PATH"

netbox-test
printf '%s\n' netbox_interface_name_rules > "$TEST_DIR/expected"
diff -u "$TEST_DIR/expected" "$PYTEST_ARGV"

netbox-test netbox_interface_name_rules/tests/test_rules.py -k 'a test name'
printf '%s\n' netbox_interface_name_rules/tests/test_rules.py -k 'a test name' > "$TEST_DIR/expected"
diff -u "$TEST_DIR/expected" "$PYTEST_ARGV"
printf '%s\n' 'NetBox test target check passed'
48 changes: 48 additions & 0 deletions .devcontainer/scripts/tests/test-network-pins.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
# shellcheck source=.devcontainer/scripts/tests/lib.sh
source "$(dirname "$0")/lib.sh"
compose_file_args "$REPO_ROOT"

if [ -n "${NETWORK_PINS_CONFIG:-}" ]; then
config="$(cat -- "$NETWORK_PINS_CONFIG")"
else
config="$(docker compose "${COMPOSE_FILES[@]}" config)"
fi

mapfile -t ranges < <(sed -n 's/^[[:space:]]*ip_range:[[:space:]]*"\{0,1\}\([^"]*\)"\{0,1\}$/\1/p' <<< "$config")
if [ "${#ranges[@]}" -eq 0 ]; then
echo "FAIL: the default network declares no ip_range, so Docker may hand a pinned address to another service" >&2
exit 1
fi

if [ "${#ranges[@]}" -gt 1 ]; then
echo "FAIL: expected one ip_range, found ${#ranges[@]}" >&2
exit 1
fi
ip_range="${ranges[0]}"

mapfile -t pinned < <(sed -n 's/^[[:space:]]*ipv4_address:[[:space:]]*"\{0,1\}\([^"]*\)"\{0,1\}$/\1/p' <<< "$config")
if [ "${#pinned[@]}" -eq 0 ]; then
echo "FAIL: no service pins an ipv4_address, so this check has nothing to protect" >&2
exit 1
fi

# Every pinned address must sit outside the pool Docker assigns from.
for address in "${pinned[@]}"; do
verdict="$(python3 -c '
import ipaddress, sys
print("inside" if ipaddress.ip_address(sys.argv[1]) in ipaddress.ip_network(sys.argv[2]) else "outside")
' "$address" "$ip_range")"
if [ "$verdict" = "inside" ]; then
echo "FAIL: pinned address $address lies inside the dynamic range $ip_range" >&2
exit 1
fi
done

echo "Network pin check passed (${#pinned[@]} pinned addresses outside $ip_range)"
3 changes: 3 additions & 0 deletions .github/workflows/lint-format.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,6 @@ jobs:
- name: Run devcontainer script tests
run: |
bash .devcontainer/scripts/tests/test-debug-toolbar-patches.sh
bash .devcontainer/scripts/tests/test-ca-environment.sh
bash .devcontainer/scripts/tests/test-network-pins.sh
bash .devcontainer/scripts/tests/test-bytecode-writes.sh
7 changes: 5 additions & 2 deletions .github/workflows/test-netbox-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ jobs:
working-directory: netbox-InterfaceNameRules-plugin
run: |
uv pip install --system -r ../netbox/requirements.txt
uv pip install --system --only-binary=:all: pytest==9.0.2 pytest-django==4.12.0 pytest-xdist==3.8.0 tblib==3.2.2
uv pip install --system --only-binary=:all: pytest==9.0.2 pytest-cov==7.0.0 pytest-django==4.12.0 pytest-xdist==3.8.0 tblib==3.2.2
uv pip install --system --only-binary=:all: --group workflow-tests
uv pip install --system -e .

- name: Set up NetBox configuration
Expand Down Expand Up @@ -104,5 +105,7 @@ jobs:
env:
NETBOX_CONFIGURATION: netbox.configuration
PYTHONPATH: ${{ github.workspace }}/netbox/netbox
TEST_DB_NAME: test_netbox_interface_name_rules_main_ci
TEST_REDIS_HOST: localhost
run: |
pytest -n auto netbox_interface_name_rules -o pythonpath=../netbox/netbox
pytest -n auto netbox_interface_name_rules --no-cov -o pythonpath=../netbox/netbox
Comment thread
coderabbitai[bot] marked this conversation as resolved.
6 changes: 4 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ jobs:
run: |
uv pip install --system -r ../netbox/requirements.txt
uv pip install --system --only-binary=:all: pytest==9.0.2 pytest-cov==7.0.0 pytest-django==4.12.0 pytest-xdist==3.8.0 tblib==3.2.2
uv pip install --system --only-binary=:all: --group workflow-tests
uv pip install --system -e .

- name: Set up NetBox configuration
Expand Down Expand Up @@ -126,6 +127,8 @@ jobs:
NETBOX_CONFIGURATION: netbox.configuration
PYTHONPATH: ${{ github.workspace }}/netbox/netbox
COVERAGE_RCFILE: pyproject.toml
TEST_DB_NAME: test_netbox_interface_name_rules_ci
TEST_REDIS_HOST: localhost
# Turns the channelization tests' skipUnless guard into an assertion on the leg that must
# have the feature, so a broken probe cannot silently skip the whole file.
EXPECT_NETBOX_CHANNELIZATION: ${{ (matrix.netbox-version == 'feature' || matrix.netbox-version == 'v4.7.0') && '1' || '' }}
Expand All @@ -136,8 +139,7 @@ jobs:
# condition, and tests/query_counts.json together.
UPDATE_QUERY_COUNTS: ${{ matrix.netbox-version != 'v4.7.0' && '1' || '' }}
run: |
pytest -n auto netbox_interface_name_rules --cov=netbox_interface_name_rules --cov-report=term-missing \
-o pythonpath=../netbox/netbox
pytest -n auto netbox_interface_name_rules -o pythonpath=../netbox/netbox

- name: Generate coverage report
if: matrix.netbox-version == 'v4.5.3'
Expand Down
6 changes: 6 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ path = "netbox_interface_name_rules/tests/query_counts.json"
SPDX-FileCopyrightText = "2025 Marcin Zieba <marcinpsk@gmail.com>"
SPDX-License-Identifier = "Apache-2.0"

# The comment-block permit list is generated from the tree, and JSON takes no header comment.
[[annotations]]
path = "netbox_interface_name_rules/tests/comment_blocks.json"
SPDX-FileCopyrightText = "2025 Marcin Zieba <marcinpsk@gmail.com>"
SPDX-License-Identifier = "Apache-2.0"

# CODEOWNERS (auto-request review); added via API so it had no license info — annotate it here.
[[annotations]]
path = ".github/CODEOWNERS"
Expand Down
Loading