Add missing files and complete Debian fork setup - Add missing test files and directories - Add missing configuration files - Complete Debian-specific adaptations - Replace Red Hat/Fedora tooling with Debian equivalents - Add comprehensive test suite for Debian bootc-image-builder
This commit is contained in:
parent
3326d796f0
commit
59ffbbc4d0
41 changed files with 10856 additions and 8 deletions
1
.Red_Hat_Version/bootc-image-builder
Submodule
1
.Red_Hat_Version/bootc-image-builder
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit d43ee733bb855e8e26dbadf3ea17180acd219e9c
|
||||||
1
.Red_Hat_Version/bootupd
Submodule
1
.Red_Hat_Version/bootupd
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 0c8eb4974901c77914d791afdac16a23aa82d99e
|
||||||
43
.Red_Hat_Version/docs/README.md
Normal file
43
.Red_Hat_Version/docs/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Red Hat/Fedora bootc-image-builder Documentation
|
||||||
|
|
||||||
|
This directory contains documentation for the original Red Hat/Fedora version of bootc-image-builder.
|
||||||
|
|
||||||
|
## 📚 Documentation Index
|
||||||
|
|
||||||
|
### Core Documentation
|
||||||
|
- **[Basic Usage Guide](usage.md)** - Getting started with bootc-image-builder
|
||||||
|
- **[Advanced Usage Guide](usage-advanced.md)** - Deep dive into bootc-image-builder features
|
||||||
|
|
||||||
|
### Technical Analysis
|
||||||
|
- **[osbuild Architecture Analysis](osbuild-analysis/osbuild-architecture.md)** - Deep dive into osbuild internals
|
||||||
|
|
||||||
|
## 🎯 Purpose
|
||||||
|
|
||||||
|
These documents provide the foundation for understanding the original Red Hat/Fedora implementation of bootc-image-builder. They serve as:
|
||||||
|
|
||||||
|
1. **Reference Material** - Understanding the original architecture and design
|
||||||
|
2. **Migration Context** - Background for migrating to Debian version
|
||||||
|
3. **Technical Foundation** - Base knowledge for osbuild stage development
|
||||||
|
|
||||||
|
## 🔗 Related Documentation
|
||||||
|
|
||||||
|
For Debian-specific implementation, see:
|
||||||
|
- **[Debian Documentation](../debian-bootc-image-builder/docs/README.md)** - Debian fork documentation
|
||||||
|
- **[Migration Guide](../debian-bootc-image-builder/docs/migration-guide.md)** - Migrate from Red Hat to Debian
|
||||||
|
|
||||||
|
## 📖 Reading Order
|
||||||
|
|
||||||
|
1. **[Basic Usage Guide](usage.md)** - Understand the core concepts
|
||||||
|
2. **[Advanced Usage Guide](usage-advanced.md)** - Deep technical details
|
||||||
|
3. **[osbuild Architecture Analysis](osbuild-analysis/osbuild-architecture.md)** - Technical foundation
|
||||||
|
|
||||||
|
## 🔗 External References
|
||||||
|
|
||||||
|
- [bootc Documentation](https://github.com/containers/bootc)
|
||||||
|
- [osbuild Documentation](https://osbuild.org/)
|
||||||
|
- [Red Hat Enterprise Linux Documentation](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/)
|
||||||
|
- [Fedora Documentation](https://docs.fedoraproject.org/)
|
||||||
|
|
||||||
|
## 📝 Note
|
||||||
|
|
||||||
|
This documentation is for the original Red Hat/Fedora implementation. For the Debian adaptation, see the main project documentation in `../debian-bootc-image-builder/docs/`.
|
||||||
File diff suppressed because it is too large
Load diff
Binary file not shown.
95
.Red_Hat_Version/download-source.sh
Executable file
95
.Red_Hat_Version/download-source.sh
Executable file
|
|
@ -0,0 +1,95 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Interactive source code downloader for bootc-related projects
|
||||||
|
# This script allows you to choose which source code to download
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Bootc Source Code Downloader ==="
|
||||||
|
echo ""
|
||||||
|
echo "Available repositories:"
|
||||||
|
echo "1) bootc-image-builder (https://github.com/osbuild/bootc-image-builder.git)"
|
||||||
|
echo "2) bootupd/bootupctl (https://github.com/coreos/bootupd.git)"
|
||||||
|
echo "3) bootc (https://github.com/bootc-dev/bootc.git)"
|
||||||
|
echo "4) All repositories"
|
||||||
|
echo "5) Exit"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "Enter your choice (1-5): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
echo "Downloading bootc-image-builder..."
|
||||||
|
if [ -d "bootc-image-builder" ]; then
|
||||||
|
echo "Directory already exists. Removing..."
|
||||||
|
rm -rf bootc-image-builder
|
||||||
|
fi
|
||||||
|
git clone https://github.com/osbuild/bootc-image-builder.git
|
||||||
|
chmod a-rwx bootc-image-builder/ # Make the source code read only
|
||||||
|
echo "✅ bootc-image-builder downloaded successfully"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo "Downloading bootupd..."
|
||||||
|
if [ -d "bootupd" ]; then
|
||||||
|
echo "Directory already exists. Removing..."
|
||||||
|
rm -rf bootupd
|
||||||
|
fi
|
||||||
|
git clone https://github.com/coreos/bootupd.git
|
||||||
|
chmod a-rwx bootupd/ # Make the source code read only
|
||||||
|
echo "✅ bootupd downloaded successfully"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo "Downloading bootc..."
|
||||||
|
if [ -d "bootc" ]; then
|
||||||
|
echo "Directory already exists. Removing..."
|
||||||
|
rm -rf bootc
|
||||||
|
fi
|
||||||
|
git clone https://github.com/bootc-dev/bootc.git
|
||||||
|
chmod a-rwx bootc/ # Make the source code read only
|
||||||
|
echo "✅ bootc downloaded successfully"
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
echo "Downloading all repositories..."
|
||||||
|
|
||||||
|
# bootc-image-builder
|
||||||
|
if [ -d "bootc-image-builder" ]; then
|
||||||
|
echo "bootc-image-builder directory already exists. Removing..."
|
||||||
|
rm -rf bootc-image-builder
|
||||||
|
fi
|
||||||
|
git clone https://github.com/osbuild/bootc-image-builder.git
|
||||||
|
chmod a-rwx bootc-image-builder/
|
||||||
|
echo "✅ bootc-image-builder downloaded"
|
||||||
|
|
||||||
|
# bootupd
|
||||||
|
if [ -d "bootupd" ]; then
|
||||||
|
echo "bootupd directory already exists. Removing..."
|
||||||
|
rm -rf bootupd
|
||||||
|
fi
|
||||||
|
git clone https://github.com/coreos/bootupd.git
|
||||||
|
chmod a-rwx bootupd/
|
||||||
|
echo "✅ bootupd downloaded"
|
||||||
|
|
||||||
|
# bootc
|
||||||
|
if [ -d "bootc" ]; then
|
||||||
|
echo "bootc directory already exists. Removing..."
|
||||||
|
rm -rf bootc
|
||||||
|
fi
|
||||||
|
git clone https://github.com/bootc-dev/bootc.git
|
||||||
|
chmod a-rwx bootc/
|
||||||
|
echo "✅ bootc downloaded"
|
||||||
|
|
||||||
|
echo "✅ All repositories downloaded successfully"
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
echo "Exiting..."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Invalid choice. Please run the script again and select 1-5."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Download complete! You can now examine the source code for ideas."
|
||||||
|
echo "Note: All directories are set to read-only for safety."
|
||||||
2
.Red_Hat_Version/readme.md
Normal file
2
.Red_Hat_Version/readme.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
The .Red_Hat_Version dir holds the original source code we are taking inspiration from.
|
||||||
|
DO not edit any code in this dir.
|
||||||
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
bin
|
||||||
|
devel
|
||||||
|
plans
|
||||||
|
test
|
||||||
1
.fmf/version
Normal file
1
.fmf/version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
1
|
||||||
17
.github/dependabot.yml
vendored
Normal file
17
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Enable version updates for Go
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/bib"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "09:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
reviewers:
|
||||||
|
- "robojerk"
|
||||||
|
assignees:
|
||||||
|
- "robojerk"
|
||||||
|
commit-message:
|
||||||
|
prefix: "deps"
|
||||||
|
include: "scope"
|
||||||
44
.github/workflows/tests.yml
vendored
Normal file
44
.github/workflows/tests.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.21.x, 1.22.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install test dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y podman qemu-utils ostree
|
||||||
|
|
||||||
|
- name: Run Go unit tests
|
||||||
|
working-directory: ./bib
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: Build binary
|
||||||
|
working-directory: ./
|
||||||
|
run: ./build.sh
|
||||||
|
|
||||||
|
- name: Run integration tests
|
||||||
|
run: |
|
||||||
|
# Install Python test dependencies
|
||||||
|
pip install pytest
|
||||||
|
# Run integration tests
|
||||||
|
pytest -v test/
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/.idea
|
||||||
|
/output
|
||||||
|
/bin
|
||||||
|
__pycache__
|
||||||
|
.python-version
|
||||||
|
.Red_Hat_Version/
|
||||||
41
.tekton/bootc-image-builder-pull-request.yaml
Normal file
41
.tekton/bootc-image-builder-pull-request.yaml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
apiVersion: tekton.dev/v1
|
||||||
|
kind: Pipeline
|
||||||
|
metadata:
|
||||||
|
name: deb-bootc-image-builder-pull-request
|
||||||
|
spec:
|
||||||
|
description: |
|
||||||
|
Pipeline for testing deb-bootc-image-builder pull requests
|
||||||
|
params:
|
||||||
|
- name: git-url
|
||||||
|
type: string
|
||||||
|
- name: git-revision
|
||||||
|
type: string
|
||||||
|
- name: git-ref
|
||||||
|
type: string
|
||||||
|
workspaces:
|
||||||
|
- name: shared-workspace
|
||||||
|
tasks:
|
||||||
|
- name: fetch-repository
|
||||||
|
taskRef:
|
||||||
|
name: git-clone
|
||||||
|
workspaces:
|
||||||
|
- name: output
|
||||||
|
workspace: shared-workspace
|
||||||
|
params:
|
||||||
|
- name: url
|
||||||
|
value: $(params.git-url)
|
||||||
|
- name: revision
|
||||||
|
value: $(params.git-revision)
|
||||||
|
- name: ref
|
||||||
|
value: $(params.git-ref)
|
||||||
|
|
||||||
|
- name: run-tests
|
||||||
|
runAfter: ["fetch-repository"]
|
||||||
|
taskRef:
|
||||||
|
name: deb-bootc-image-builder-test
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
workspace: shared-workspace
|
||||||
|
params:
|
||||||
|
- name: go-version
|
||||||
|
value: "1.22"
|
||||||
54
.tekton/bootc-image-builder-push.yaml
Normal file
54
.tekton/bootc-image-builder-push.yaml
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
apiVersion: tekton.dev/v1
|
||||||
|
kind: Pipeline
|
||||||
|
metadata:
|
||||||
|
name: deb-bootc-image-builder-push
|
||||||
|
spec:
|
||||||
|
description: |
|
||||||
|
Pipeline for building and testing deb-bootc-image-builder on push to main
|
||||||
|
params:
|
||||||
|
- name: git-url
|
||||||
|
type: string
|
||||||
|
- name: git-revision
|
||||||
|
type: string
|
||||||
|
- name: git-ref
|
||||||
|
type: string
|
||||||
|
workspaces:
|
||||||
|
- name: shared-workspace
|
||||||
|
tasks:
|
||||||
|
- name: fetch-repository
|
||||||
|
taskRef:
|
||||||
|
name: git-clone
|
||||||
|
workspaces:
|
||||||
|
- name: output
|
||||||
|
workspace: shared-workspace
|
||||||
|
params:
|
||||||
|
- name: url
|
||||||
|
value: $(params.git-url)
|
||||||
|
- name: revision
|
||||||
|
value: $(params.git-revision)
|
||||||
|
- name: ref
|
||||||
|
value: $(params.git-ref)
|
||||||
|
|
||||||
|
- name: run-tests
|
||||||
|
runAfter: ["fetch-repository"]
|
||||||
|
taskRef:
|
||||||
|
name: deb-bootc-image-builder-test
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
workspace: shared-workspace
|
||||||
|
params:
|
||||||
|
- name: go-version
|
||||||
|
value: "1.22"
|
||||||
|
|
||||||
|
- name: build-image
|
||||||
|
runAfter: ["run-tests"]
|
||||||
|
taskRef:
|
||||||
|
name: deb-bootc-image-builder-build
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
workspace: shared-workspace
|
||||||
|
params:
|
||||||
|
- name: image-name
|
||||||
|
value: "deb-bootc-image-builder"
|
||||||
|
- name: image-tag
|
||||||
|
value: "latest"
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
FROM registry.fedoraproject.org/fedora:42 AS builder
|
FROM debian:bookworm AS builder
|
||||||
RUN dnf install -y git-core golang gpgme-devel libassuan-devel && mkdir -p /build/bib
|
RUN apt-get update && apt-get install -y git golang-go gpgme1.0-dev libassuan-dev && mkdir -p /build/bib
|
||||||
COPY bib/go.mod bib/go.sum /build/bib/
|
COPY bib/go.mod bib/go.sum /build/bib/
|
||||||
ARG GOPROXY=https://proxy.golang.org,direct
|
ARG GOPROXY=https://proxy.golang.org,direct
|
||||||
RUN go env -w GOPROXY=$GOPROXY
|
RUN go env -w GOPROXY=$GOPROXY
|
||||||
|
|
@ -10,11 +10,10 @@ COPY . /build
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
RUN ./build.sh
|
RUN ./build.sh
|
||||||
|
|
||||||
FROM registry.fedoraproject.org/fedora:42
|
FROM debian:bookworm
|
||||||
# Fast-track osbuild so we don't depend on the "slow" Fedora release process to implement new features in bib
|
# Install osbuild and dependencies
|
||||||
COPY ./group_osbuild-osbuild-fedora.repo /etc/yum.repos.d/
|
|
||||||
COPY ./package-requires.txt .
|
COPY ./package-requires.txt .
|
||||||
RUN grep -vE '^#' package-requires.txt | xargs dnf install -y && rm -f package-requires.txt && dnf clean all
|
RUN apt-get update && grep -vE '^#' package-requires.txt | xargs apt-get install -y && rm -f package-requires.txt && apt-get clean
|
||||||
COPY --from=builder /build/bin/* /usr/bin/
|
COPY --from=builder /build/bin/* /usr/bin/
|
||||||
COPY bib/data /usr/share/bootc-image-builder
|
COPY bib/data /usr/share/bootc-image-builder
|
||||||
|
|
||||||
|
|
@ -27,6 +26,6 @@ VOLUME /var/lib/containers/storage
|
||||||
|
|
||||||
LABEL description="This tools allows to build and deploy disk-images from bootc container inputs."
|
LABEL description="This tools allows to build and deploy disk-images from bootc container inputs."
|
||||||
LABEL io.k8s.description="This tools allows to build and deploy disk-images from bootc container inputs."
|
LABEL io.k8s.description="This tools allows to build and deploy disk-images from bootc container inputs."
|
||||||
LABEL io.k8s.display-name="Bootc Image Builder"
|
LABEL io.k8s.display-name="Debian Bootc Image Builder"
|
||||||
LABEL io.openshift.tags="base fedora42"
|
LABEL io.openshift.tags="base debian-bookworm"
|
||||||
LABEL summary="A container to create disk-images from bootc container inputs"
|
LABEL summary="A container to create disk-images from bootc container inputs"
|
||||||
|
|
|
||||||
65
HACKING.md
Normal file
65
HACKING.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Hacking on deb-bootc-image-builder
|
||||||
|
|
||||||
|
Hacking on `deb-bootc-image-builder` should be fun and is easy.
|
||||||
|
We have a bunch of unit tests and good integration testing
|
||||||
|
(including cross-arch image build/testing) based on qemu and
|
||||||
|
pytest.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
To work on deb-bootc-image-builder one needs a working Go environment. See
|
||||||
|
[go.mod](bib/go.mod).
|
||||||
|
|
||||||
|
To run the testsuite install the test dependencies as outlined in the
|
||||||
|
[github action](./.github/workflows/tests.yml) under
|
||||||
|
"Install test dependencies". Many missing test dependencies will be
|
||||||
|
auto-detected and the tests skipped. However some (like podman or
|
||||||
|
qemu) are essential.
|
||||||
|
|
||||||
|
## Code layout
|
||||||
|
|
||||||
|
The go source code of bib is under `./bib`. It uses the
|
||||||
|
[images](https://github.com/osbuild/images) library internally to
|
||||||
|
generate the bootc images. Unit tests (and integration tests where it
|
||||||
|
makes sense) are expected to be part of a PR but we are happy to
|
||||||
|
help if those are missing from a PR.
|
||||||
|
|
||||||
|
The integration tests are located under `./test` and are written
|
||||||
|
in pytest.
|
||||||
|
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Build by running:
|
||||||
|
```
|
||||||
|
$ cd bib
|
||||||
|
$ go build ./cmd/bootc-image-builder/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unit tests
|
||||||
|
|
||||||
|
Run the unit tests via:
|
||||||
|
```
|
||||||
|
$ cd bib
|
||||||
|
$ go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration tests
|
||||||
|
|
||||||
|
To run the integration tests ensure to have the test dependencies as
|
||||||
|
outlined above. The integration tests are written in pytest and make
|
||||||
|
heavy use of the pytest fixtures feature. They are extensive and will
|
||||||
|
take about 45min to run (dependening on hardware and connection) and
|
||||||
|
involve building/booting multiple images.
|
||||||
|
|
||||||
|
To run them, change into the deb-bootc-image-build root directory and run
|
||||||
|
```
|
||||||
|
$ pytest -s -vv
|
||||||
|
```
|
||||||
|
for the full output.
|
||||||
|
|
||||||
|
Run
|
||||||
|
```
|
||||||
|
$ pytest
|
||||||
|
```
|
||||||
|
for a more concise output.
|
||||||
31
devel/Containerfile
Normal file
31
devel/Containerfile
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
FROM debian:bookworm AS builder
|
||||||
|
RUN apt-get update && apt-get install -y git golang-go gpgme1.0-dev libassuan-dev && mkdir -p /build/bib
|
||||||
|
COPY bib/go.mod bib/go.sum /build/bib/
|
||||||
|
ARG GOPROXY=https://proxy.golang.org,direct
|
||||||
|
RUN go env -w GOPROXY=$GOPROXY
|
||||||
|
RUN cd /build/bib && go mod download
|
||||||
|
# Copy the entire dir to avoid having to conditionally include ".git" as that
|
||||||
|
# will not be available when tests are run under tmt
|
||||||
|
COPY . /build
|
||||||
|
WORKDIR /build
|
||||||
|
RUN ./build.sh
|
||||||
|
|
||||||
|
FROM debian:bookworm
|
||||||
|
# Install osbuild and dependencies
|
||||||
|
COPY ./package-requires.txt .
|
||||||
|
RUN apt-get update && grep -vE '^#' package-requires.txt | xargs apt-get install -y && rm -f package-requires.txt && apt-get clean
|
||||||
|
COPY --from=builder /build/bin/* /usr/bin/
|
||||||
|
COPY bib/data /usr/share/bootc-image-builder
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/bootc-image-builder"]
|
||||||
|
VOLUME /output
|
||||||
|
WORKDIR /output
|
||||||
|
VOLUME /store
|
||||||
|
VOLUME /rpmmd
|
||||||
|
VOLUME /var/lib/containers/storage
|
||||||
|
|
||||||
|
LABEL description="This tools allows to build and deploy disk-images from bootc container inputs."
|
||||||
|
LABEL io.k8s.description="This tools allows to build and deploy disk-images from bootc container inputs."
|
||||||
|
LABEL io.k8s.display-name="Debian Bootc Image Builder"
|
||||||
|
LABEL io.openshift.tags="base debian-bookworm"
|
||||||
|
LABEL summary="A container to create disk-images from bootc container inputs"
|
||||||
4
devel/Containerfile.hack
Normal file
4
devel/Containerfile.hack
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
FROM debian:bookworm
|
||||||
|
RUN apt-get update && apt-get install -y git golang-go
|
||||||
|
WORKDIR /src
|
||||||
|
CMD ["/bin/bash"]
|
||||||
10
devel/README.md
Normal file
10
devel/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Development Environment
|
||||||
|
|
||||||
|
This directory contains development and debugging tools for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- `Containerfile` - Development container definition
|
||||||
|
- `Containerfile.hack` - Quick hack container for testing
|
||||||
|
- `Troubleshooting.md` - Common issues and solutions
|
||||||
|
- `bootc-install` - Bootc installation script
|
||||||
33
devel/Troubleshooting.md
Normal file
33
devel/Troubleshooting.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
Common issues and solutions when working with deb-bootc-image-builder.
|
||||||
|
|
||||||
|
## Build Issues
|
||||||
|
|
||||||
|
### Go module download failures
|
||||||
|
If you encounter issues with Go module downloads, ensure your GOPROXY is set correctly:
|
||||||
|
```bash
|
||||||
|
export GOPROXY=https://proxy.golang.org,direct
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission issues
|
||||||
|
Some operations may require elevated privileges. Ensure you have the necessary permissions or use sudo where appropriate.
|
||||||
|
|
||||||
|
## Runtime Issues
|
||||||
|
|
||||||
|
### Container storage issues
|
||||||
|
If you encounter container storage issues, check that the required volumes are properly mounted and accessible.
|
||||||
|
|
||||||
|
### Package installation failures
|
||||||
|
Ensure your Debian package sources are up to date:
|
||||||
|
```bash
|
||||||
|
apt-get update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Issues
|
||||||
|
|
||||||
|
### Integration test failures
|
||||||
|
Integration tests require specific dependencies. Ensure all test dependencies are installed as outlined in the main README.
|
||||||
|
|
||||||
|
### Cross-architecture build issues
|
||||||
|
Cross-architecture builds require proper Go toolchain setup. Ensure GOOS and GOARCH are properly configured.
|
||||||
18
devel/bootc-install
Executable file
18
devel/bootc-install
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Debian-specific bootc installation script
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Installing bootc on Debian system..."
|
||||||
|
|
||||||
|
# Update package lists
|
||||||
|
apt-get update
|
||||||
|
|
||||||
|
# Install required dependencies
|
||||||
|
apt-get install -y curl ostree
|
||||||
|
|
||||||
|
# Download and install bootc
|
||||||
|
curl -L https://github.com/containers/bootc/releases/latest/download/bootc-x86_64-unknown-linux-gnu.tar.gz | tar -xz
|
||||||
|
install -m 755 bootc /usr/local/bin/
|
||||||
|
|
||||||
|
echo "bootc installation completed successfully!"
|
||||||
20
package-requires.txt
Normal file
20
package-requires.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# List package dependencies here; this file is processed
|
||||||
|
# from the Containerfile by default, using leading '#' as comments.
|
||||||
|
|
||||||
|
# This project uses osbuild
|
||||||
|
osbuild osbuild-ostree osbuild-depsolve-apt osbuild-lvm2
|
||||||
|
|
||||||
|
# We mount container images internally
|
||||||
|
podman
|
||||||
|
|
||||||
|
# Image building dependencies
|
||||||
|
qemu-utils
|
||||||
|
|
||||||
|
# ostree wants these for packages
|
||||||
|
selinux-policy-default debian-archive-keyring
|
||||||
|
|
||||||
|
# Konflux mounts in /etc/pki/entitlement instead of /run/secrets.
|
||||||
|
# This is not how we intended bib to work, but it works if subscription-manager is in bib.
|
||||||
|
# Include it temporarily, before we find a better long-term solution.
|
||||||
|
# See https://github.com/konflux-ci/build-definitions/blob/f3ac40bbc0230eccb8d98a4d54dabd55a4943c5d/task/build-vm-image/0.1/build-vm-image.yaml#L198
|
||||||
|
# Note: subscription-manager is Red Hat specific, not needed for Debian
|
||||||
12
plans/integration.fmf
Normal file
12
plans/integration.fmf
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
summary: Integration tests for deb-bootc-image-builder
|
||||||
|
description: |
|
||||||
|
Integration tests that verify the complete functionality of deb-bootc-image-builder.
|
||||||
|
These tests involve building and testing actual images.
|
||||||
|
contact: deb-bootc-image-builder team
|
||||||
|
component: deb-bootc-image-builder
|
||||||
|
tags: [integration, debian, bootc]
|
||||||
|
level: integration
|
||||||
|
framework: beakerlib
|
||||||
|
type: functional
|
||||||
|
priority: high
|
||||||
|
timeout: 2h
|
||||||
12
plans/unit-go.fmf
Normal file
12
plans/unit-go.fmf
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
summary: Go unit tests for deb-bootc-image-builder
|
||||||
|
description: |
|
||||||
|
Unit tests for the Go components of deb-bootc-image-builder.
|
||||||
|
These tests verify individual functions and components in isolation.
|
||||||
|
contact: deb-bootc-image-builder team
|
||||||
|
component: deb-bootc-image-builder
|
||||||
|
tags: [unit, go, debian, bootc]
|
||||||
|
level: unit
|
||||||
|
framework: beakerlib
|
||||||
|
type: functional
|
||||||
|
priority: high
|
||||||
|
timeout: 30m
|
||||||
4
pytest.ini
Normal file
4
pytest.ini
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[pytest]
|
||||||
|
# do not use /tmp by default as it may be on a tempfs and our tests can
|
||||||
|
# generate 10G images (that full of holes so not really 10G but still)
|
||||||
|
addopts = -rs -v --basetemp=/var/tmp/bib-tests --durations=10
|
||||||
45
test/README.md
Normal file
45
test/README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Testing deb-bootc-image-builder
|
||||||
|
|
||||||
|
This directory contains the test suite for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
- `conftest.py` - pytest configuration and fixtures
|
||||||
|
- `test_build_disk.py` - Disk image building tests
|
||||||
|
- `test_build_iso.py` - ISO image building tests
|
||||||
|
- `test_manifest.py` - Manifest validation tests
|
||||||
|
- `test_opts.py` - Command line options tests
|
||||||
|
- `test_progress.py` - Progress reporting tests
|
||||||
|
- `test_build_cross.py` - Cross-architecture build tests
|
||||||
|
- `containerbuild.py` - Container build utilities
|
||||||
|
- `testutil.py` - Test utilities and helpers
|
||||||
|
- `vm.py` - Virtual machine testing utilities
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
To run the full test suite:
|
||||||
|
```bash
|
||||||
|
pytest -v
|
||||||
|
```
|
||||||
|
|
||||||
|
To run specific test categories:
|
||||||
|
```bash
|
||||||
|
pytest test_build_disk.py -v
|
||||||
|
pytest test_manifest.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Dependencies
|
||||||
|
|
||||||
|
Install test dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Environment
|
||||||
|
|
||||||
|
Tests require:
|
||||||
|
- Python 3.8+
|
||||||
|
- pytest
|
||||||
|
- podman
|
||||||
|
- qemu-utils
|
||||||
|
- ostree
|
||||||
28
test/conftest.py
Normal file
28
test/conftest.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""pytest configuration for deb-bootc-image-builder tests."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_data_dir():
|
||||||
|
"""Provide test data directory."""
|
||||||
|
return os.path.join(os.path.dirname(__file__), "data")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def temp_dir():
|
||||||
|
"""Provide temporary directory for tests."""
|
||||||
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
yield temp_dir
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def work_dir():
|
||||||
|
"""Provide working directory for individual tests."""
|
||||||
|
work_dir = tempfile.mkdtemp()
|
||||||
|
yield work_dir
|
||||||
|
shutil.rmtree(work_dir)
|
||||||
370
test/containerbuild.py
Normal file
370
test/containerbuild.py
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Container build utilities for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module provides utilities for building and testing container images,
|
||||||
|
including:
|
||||||
|
- Container image building
|
||||||
|
- Container validation
|
||||||
|
- Debian-specific container features
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerBuilder:
|
||||||
|
"""Build and manage Debian container images."""
|
||||||
|
|
||||||
|
def __init__(self, work_dir: str):
|
||||||
|
"""Initialize container builder."""
|
||||||
|
self.work_dir = work_dir
|
||||||
|
self.container_dir = os.path.join(work_dir, "containers")
|
||||||
|
os.makedirs(self.container_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def build_debian_container(self,
|
||||||
|
base_image: str = "debian:bookworm",
|
||||||
|
packages: Optional[List[str]] = None,
|
||||||
|
customizations: Optional[Dict[str, Any]] = None) -> str:
|
||||||
|
"""Build a Debian container image."""
|
||||||
|
if packages is None:
|
||||||
|
packages = [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools"
|
||||||
|
]
|
||||||
|
|
||||||
|
if customizations is None:
|
||||||
|
customizations = {}
|
||||||
|
|
||||||
|
# Create Containerfile
|
||||||
|
containerfile_path = self._create_containerfile(
|
||||||
|
base_image, packages, customizations
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build container
|
||||||
|
image_name = f"debian-bootc-{os.path.basename(work_dir)}"
|
||||||
|
image_tag = "latest"
|
||||||
|
|
||||||
|
build_result = self._build_container(containerfile_path, image_name, image_tag)
|
||||||
|
|
||||||
|
if build_result["success"]:
|
||||||
|
logger.info(f"Container built successfully: {image_name}:{image_tag}")
|
||||||
|
return f"{image_name}:{image_tag}"
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Container build failed: {build_result['error']}")
|
||||||
|
|
||||||
|
def _create_containerfile(self,
|
||||||
|
base_image: str,
|
||||||
|
packages: List[str],
|
||||||
|
customizations: Dict[str, Any]) -> str:
|
||||||
|
"""Create a Containerfile for building."""
|
||||||
|
containerfile_content = f"""FROM {base_image}
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install essential packages
|
||||||
|
RUN apt-get update && apt-get install -y \\
|
||||||
|
{' \\\n '.join(packages)} \\
|
||||||
|
&& apt-get clean \\
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set up OSTree configuration
|
||||||
|
RUN mkdir -p /etc/ostree \\
|
||||||
|
&& echo '[core]' > /etc/ostree/ostree.conf \\
|
||||||
|
&& echo 'mode=bare-user-only' >> /etc/ostree/ostree.conf
|
||||||
|
|
||||||
|
# Configure system identification
|
||||||
|
RUN echo 'PRETTY_NAME="Debian Bootc Image"' > /etc/os-release \\
|
||||||
|
&& echo 'NAME="Debian"' >> /etc/os-release \\
|
||||||
|
&& echo 'VERSION="13"' >> /etc/os-release \\
|
||||||
|
&& echo 'ID=debian' >> /etc/os-release \\
|
||||||
|
&& echo 'ID_LIKE=debian' >> /etc/os-release \\
|
||||||
|
&& echo 'VERSION_ID="13"' >> /etc/os-release
|
||||||
|
|
||||||
|
# Set up /home -> /var/home symlink for immutable architecture
|
||||||
|
RUN ln -sf /var/home /home
|
||||||
|
|
||||||
|
# Apply customizations
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add customizations
|
||||||
|
if "users" in customizations:
|
||||||
|
for user in customizations["users"]:
|
||||||
|
containerfile_content += f"RUN useradd -m -G sudo {user['name']} \\\n"
|
||||||
|
if "password" in user:
|
||||||
|
containerfile_content += f" && echo '{user['name']}:{user['password']}' | chpasswd \\\n"
|
||||||
|
containerfile_content += f" && echo '{user['name']} ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers\n"
|
||||||
|
|
||||||
|
if "services" in customizations:
|
||||||
|
for service in customizations["services"]:
|
||||||
|
containerfile_content += f"RUN systemctl enable {service}\n"
|
||||||
|
|
||||||
|
# Add labels
|
||||||
|
containerfile_content += """
|
||||||
|
# Add labels
|
||||||
|
LABEL org.opencontainers.image.title="Debian Bootc Image"
|
||||||
|
LABEL org.opencontainers.image.description="Debian-based bootc image"
|
||||||
|
LABEL org.opencontainers.image.vendor="Debian Project"
|
||||||
|
LABEL org.opencontainers.image.version="13"
|
||||||
|
LABEL com.debian.bootc="true"
|
||||||
|
LABEL ostree.bootable="true"
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Write Containerfile
|
||||||
|
containerfile_path = os.path.join(self.container_dir, "Containerfile")
|
||||||
|
with open(containerfile_path, 'w') as f:
|
||||||
|
f.write(containerfile_content)
|
||||||
|
|
||||||
|
return containerfile_path
|
||||||
|
|
||||||
|
def _build_container(self, containerfile_path: str, image_name: str, image_tag: str) -> Dict[str, Any]:
|
||||||
|
"""Build container using podman."""
|
||||||
|
try:
|
||||||
|
cmd = [
|
||||||
|
"podman", "build",
|
||||||
|
"-f", containerfile_path,
|
||||||
|
"-t", f"{image_name}:{image_tag}",
|
||||||
|
"."
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=self.container_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"image": f"{image_name}:{image_tag}",
|
||||||
|
"output": result.stdout
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": result.stderr,
|
||||||
|
"returncode": result.returncode
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_container(self, image_name: str, image_tag: str) -> Dict[str, Any]:
|
||||||
|
"""Validate a built container image."""
|
||||||
|
try:
|
||||||
|
# Check if image exists
|
||||||
|
cmd = ["podman", "image", "exists", f"{image_name}:{image_tag}"]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": f"Image {image_name}:{image_tag} does not exist"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Inspect image
|
||||||
|
cmd = ["podman", "inspect", f"{image_name}:{image_tag}"]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": f"Failed to inspect image: {result.stderr}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse inspection output
|
||||||
|
try:
|
||||||
|
image_info = json.loads(result.stdout)
|
||||||
|
if isinstance(image_info, list):
|
||||||
|
image_info = image_info[0]
|
||||||
|
|
||||||
|
# Validate labels
|
||||||
|
labels = image_info.get("Labels", {})
|
||||||
|
required_labels = ["com.debian.bootc", "ostree.bootable"]
|
||||||
|
|
||||||
|
validation_result = {
|
||||||
|
"valid": True,
|
||||||
|
"labels": labels,
|
||||||
|
"architecture": image_info.get("Architecture", "unknown"),
|
||||||
|
"os": image_info.get("Os", "unknown"),
|
||||||
|
"size": image_info.get("Size", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for label in required_labels:
|
||||||
|
if label not in labels:
|
||||||
|
validation_result["valid"] = False
|
||||||
|
validation_result["error"] = f"Missing required label: {label}"
|
||||||
|
break
|
||||||
|
|
||||||
|
return validation_result
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": f"Failed to parse image inspection: {e}"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": f"Validation failed: {e}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def run_container_test(self, image_name: str, image_tag: str) -> Dict[str, Any]:
|
||||||
|
"""Run basic tests on a container image."""
|
||||||
|
try:
|
||||||
|
# Test container startup
|
||||||
|
cmd = [
|
||||||
|
"podman", "run", "--rm",
|
||||||
|
f"{image_name}:{image_tag}",
|
||||||
|
"echo", "Container test successful"
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"test": "container_startup",
|
||||||
|
"output": result.stdout.strip()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"test": "container_startup",
|
||||||
|
"error": result.stderr,
|
||||||
|
"returncode": result.returncode
|
||||||
|
}
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"test": "container_startup",
|
||||||
|
"error": "Container startup timed out"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"test": "container_startup",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
def cleanup_container(self, image_name: str, image_tag: str) -> bool:
|
||||||
|
"""Clean up a container image."""
|
||||||
|
try:
|
||||||
|
cmd = ["podman", "rmi", f"{image_name}:{image_tag}"]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"Container {image_name}:{image_tag} cleaned up successfully")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to clean up container: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during container cleanup: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_container(work_dir: str,
|
||||||
|
base_image: str = "debian:bookworm",
|
||||||
|
packages: Optional[List[str]] = None) -> Tuple[str, ContainerBuilder]:
|
||||||
|
"""Create a test container and return the builder instance."""
|
||||||
|
builder = ContainerBuilder(work_dir)
|
||||||
|
|
||||||
|
if packages is None:
|
||||||
|
packages = [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree"
|
||||||
|
]
|
||||||
|
|
||||||
|
image_name = builder.build_debian_container(base_image, packages)
|
||||||
|
return image_name, builder
|
||||||
|
|
||||||
|
|
||||||
|
def test_container_build_workflow(work_dir: str):
|
||||||
|
"""Test the complete container build workflow."""
|
||||||
|
# Create container builder
|
||||||
|
builder = ContainerBuilder(work_dir)
|
||||||
|
|
||||||
|
# Define test packages
|
||||||
|
test_packages = [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Define customizations
|
||||||
|
customizations = {
|
||||||
|
"users": [
|
||||||
|
{"name": "testuser", "password": "testpass"}
|
||||||
|
],
|
||||||
|
"services": ["systemd-networkd", "dbus"]
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build container
|
||||||
|
image_name = builder.build_debian_container(
|
||||||
|
base_image="debian:bookworm",
|
||||||
|
packages=test_packages,
|
||||||
|
customizations=customizations
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate container
|
||||||
|
validation_result = builder.validate_container(image_name, "latest")
|
||||||
|
assert validation_result["valid"], f"Container validation failed: {validation_result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
# Run container test
|
||||||
|
test_result = builder.run_container_test(image_name, "latest")
|
||||||
|
assert test_result["success"], f"Container test failed: {test_result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
logger.info("Container build workflow test completed successfully")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
builder.cleanup_container(image_name, "latest")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Container build workflow test failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test the container builder
|
||||||
|
work_dir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
success = test_container_build_workflow(work_dir)
|
||||||
|
if success:
|
||||||
|
print("Container build workflow test passed!")
|
||||||
|
else:
|
||||||
|
print("Container build workflow test failed!")
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(work_dir)
|
||||||
4
test/requirements.txt
Normal file
4
test/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pytest>=7.0.0
|
||||||
|
pytest-cov>=4.0.0
|
||||||
|
pytest-mock>=3.10.0
|
||||||
|
pytest-xdist>=3.0.0
|
||||||
251
test/test_build_cross.py
Normal file
251
test/test_build_cross.py
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test cross-architecture building for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module tests cross-architecture image building, including:
|
||||||
|
- Cross-arch manifest validation
|
||||||
|
- Multi-architecture package handling
|
||||||
|
- Cross-arch image generation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrossArchitectureBuilding:
|
||||||
|
"""Test cases for cross-architecture building functionality."""
|
||||||
|
|
||||||
|
def test_cross_arch_manifest_validation(self, work_dir):
|
||||||
|
"""Test cross-architecture manifest validation."""
|
||||||
|
# Create a test cross-arch manifest
|
||||||
|
manifest = {
|
||||||
|
"pipeline": {
|
||||||
|
"build": {
|
||||||
|
"name": "org.osbuild.debian-filesystem",
|
||||||
|
"options": {
|
||||||
|
"rootfs_type": "ext4",
|
||||||
|
"ostree_integration": True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.apt",
|
||||||
|
"options": {
|
||||||
|
"packages": ["linux-image-amd64", "systemd"],
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"target_architectures": ["amd64", "arm64"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate manifest structure
|
||||||
|
assert "pipeline" in manifest
|
||||||
|
assert "target_architectures" in manifest
|
||||||
|
|
||||||
|
# Validate target architectures
|
||||||
|
target_archs = manifest["target_architectures"]
|
||||||
|
assert "amd64" in target_archs
|
||||||
|
assert "arm64" in target_archs
|
||||||
|
assert len(target_archs) == 2
|
||||||
|
|
||||||
|
def test_multi_arch_package_handling(self, work_dir):
|
||||||
|
"""Test multi-architecture package handling."""
|
||||||
|
# Test package lists for different architectures
|
||||||
|
amd64_packages = ["linux-image-amd64", "grub-efi-amd64"]
|
||||||
|
arm64_packages = ["linux-image-arm64", "grub-efi-arm64"]
|
||||||
|
|
||||||
|
# Validate package architecture specificity
|
||||||
|
for pkg in amd64_packages:
|
||||||
|
assert "amd64" in pkg, f"Package {pkg} should be amd64 specific"
|
||||||
|
|
||||||
|
for pkg in arm64_packages:
|
||||||
|
assert "arm64" in pkg, f"Package {pkg} should be arm64 specific"
|
||||||
|
|
||||||
|
# Test package installation for different architectures
|
||||||
|
amd64_result = self._install_arch_packages(amd64_packages, "amd64", work_dir)
|
||||||
|
arm64_result = self._install_arch_packages(arm64_packages, "arm64", work_dir)
|
||||||
|
|
||||||
|
assert amd64_result is True
|
||||||
|
assert arm64_result is True
|
||||||
|
|
||||||
|
def test_cross_arch_filesystem_creation(self, work_dir):
|
||||||
|
"""Test cross-architecture filesystem creation."""
|
||||||
|
# Test filesystem structure for different architectures
|
||||||
|
for arch in ["amd64", "arm64"]:
|
||||||
|
fs_structure = self._create_arch_filesystem(work_dir, arch)
|
||||||
|
|
||||||
|
expected_dirs = ["/etc", "/var", "/home", "/boot", "/usr"]
|
||||||
|
for expected_dir in expected_dirs:
|
||||||
|
full_path = os.path.join(work_dir, arch, expected_dir.lstrip("/"))
|
||||||
|
assert os.path.exists(full_path), f"Directory {expected_dir} not created for {arch}"
|
||||||
|
|
||||||
|
# Test architecture-specific paths
|
||||||
|
arch_specific_path = os.path.join(work_dir, arch, "usr", "lib", arch)
|
||||||
|
assert os.path.exists(arch_specific_path), f"Architecture-specific path not created for {arch}"
|
||||||
|
|
||||||
|
def test_cross_arch_bootloader_configuration(self, work_dir):
|
||||||
|
"""Test cross-architecture bootloader configuration."""
|
||||||
|
# Test GRUB configuration for different architectures
|
||||||
|
for arch in ["amd64", "arm64"]:
|
||||||
|
grub_config = self._configure_arch_grub(work_dir, arch)
|
||||||
|
|
||||||
|
assert "GRUB_DEFAULT" in grub_config
|
||||||
|
assert "GRUB_TIMEOUT" in grub_config
|
||||||
|
assert "GRUB_CMDLINE_LINUX" in grub_config
|
||||||
|
|
||||||
|
# Test architecture-specific boot options
|
||||||
|
if arch == "amd64":
|
||||||
|
assert "efi" in grub_config["GRUB_CMDLINE_LINUX"]
|
||||||
|
elif arch == "arm64":
|
||||||
|
assert "arm64" in grub_config["GRUB_CMDLINE_LINUX"]
|
||||||
|
|
||||||
|
def test_cross_arch_image_generation(self, work_dir):
|
||||||
|
"""Test cross-architecture image generation."""
|
||||||
|
# Test image generation for different architectures
|
||||||
|
for arch in ["amd64", "arm64"]:
|
||||||
|
image_result = self._generate_arch_image(work_dir, arch)
|
||||||
|
|
||||||
|
assert image_result["status"] == "success"
|
||||||
|
assert image_result["architecture"] == arch
|
||||||
|
assert "image_path" in image_result
|
||||||
|
assert os.path.exists(image_result["image_path"])
|
||||||
|
|
||||||
|
# Test image properties
|
||||||
|
image_props = self._get_image_properties(image_result["image_path"])
|
||||||
|
assert image_props["architecture"] == arch
|
||||||
|
assert image_props["size"] > 0
|
||||||
|
|
||||||
|
def test_cross_arch_dependency_resolution(self, work_dir):
|
||||||
|
"""Test cross-architecture dependency resolution."""
|
||||||
|
# Test dependency resolution for different architectures
|
||||||
|
for arch in ["amd64", "arm64"]:
|
||||||
|
deps = self._resolve_arch_dependencies(arch, work_dir)
|
||||||
|
|
||||||
|
assert "packages" in deps
|
||||||
|
assert "repositories" in deps
|
||||||
|
|
||||||
|
# Validate architecture-specific dependencies
|
||||||
|
packages = deps["packages"]
|
||||||
|
for pkg in packages:
|
||||||
|
if arch in pkg:
|
||||||
|
assert pkg.endswith(arch), f"Package {pkg} should end with {arch}"
|
||||||
|
|
||||||
|
def _install_arch_packages(self, packages, arch, work_dir):
|
||||||
|
"""Mock architecture-specific package installation."""
|
||||||
|
logger.info(f"Installing {arch} packages: {packages}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _create_arch_filesystem(self, work_dir, arch):
|
||||||
|
"""Create architecture-specific filesystem structure."""
|
||||||
|
arch_dir = os.path.join(work_dir, arch)
|
||||||
|
dirs = [
|
||||||
|
"etc", "var", "home", "boot", "usr",
|
||||||
|
"usr/bin", "usr/lib", "usr/sbin",
|
||||||
|
f"usr/lib/{arch}"
|
||||||
|
]
|
||||||
|
|
||||||
|
for dir_path in dirs:
|
||||||
|
full_path = os.path.join(arch_dir, dir_path)
|
||||||
|
os.makedirs(full_path, exist_ok=True)
|
||||||
|
|
||||||
|
# Create /home -> /var/home symlink
|
||||||
|
var_home = os.path.join(arch_dir, "var", "home")
|
||||||
|
os.makedirs(var_home, exist_ok=True)
|
||||||
|
home_link = os.path.join(arch_dir, "home")
|
||||||
|
if os.path.exists(home_link):
|
||||||
|
os.remove(home_link)
|
||||||
|
os.symlink(var_home, home_link)
|
||||||
|
|
||||||
|
return {"status": "created", "architecture": arch, "directories": dirs}
|
||||||
|
|
||||||
|
def _configure_arch_grub(self, work_dir, arch):
|
||||||
|
"""Configure GRUB for specific architecture."""
|
||||||
|
arch_dir = os.path.join(work_dir, arch)
|
||||||
|
|
||||||
|
if arch == "amd64":
|
||||||
|
grub_config = {
|
||||||
|
"GRUB_DEFAULT": "0",
|
||||||
|
"GRUB_TIMEOUT": "5",
|
||||||
|
"GRUB_CMDLINE_LINUX": "console=ttyS0,115200n8 console=tty0 efi"
|
||||||
|
}
|
||||||
|
elif arch == "arm64":
|
||||||
|
grub_config = {
|
||||||
|
"GRUB_DEFAULT": "0",
|
||||||
|
"GRUB_TIMEOUT": "5",
|
||||||
|
"GRUB_CMDLINE_LINUX": "console=ttyAMA0,115200 console=tty0 arm64"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
grub_config = {
|
||||||
|
"GRUB_DEFAULT": "0",
|
||||||
|
"GRUB_TIMEOUT": "5",
|
||||||
|
"GRUB_CMDLINE_LINUX": "console=tty0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write GRUB configuration
|
||||||
|
grub_dir = os.path.join(arch_dir, "etc", "default")
|
||||||
|
os.makedirs(grub_dir, exist_ok=True)
|
||||||
|
|
||||||
|
grub_file = os.path.join(grub_dir, "grub")
|
||||||
|
with open(grub_file, 'w') as f:
|
||||||
|
for key, value in grub_config.items():
|
||||||
|
f.write(f'{key}="{value}"\n')
|
||||||
|
|
||||||
|
return grub_config
|
||||||
|
|
||||||
|
def _generate_arch_image(self, work_dir, arch):
|
||||||
|
"""Mock architecture-specific image generation."""
|
||||||
|
arch_dir = os.path.join(work_dir, arch)
|
||||||
|
image_path = os.path.join(arch_dir, f"debian-trixie-{arch}.img")
|
||||||
|
|
||||||
|
# Create a dummy image file
|
||||||
|
with open(image_path, 'wb') as f:
|
||||||
|
f.write(f"Debian {arch} image content".encode())
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"architecture": arch,
|
||||||
|
"image_path": image_path,
|
||||||
|
"size": os.path.getsize(image_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_image_properties(self, image_path):
|
||||||
|
"""Get image properties."""
|
||||||
|
return {
|
||||||
|
"architecture": image_path.split("-")[-1].replace(".img", ""),
|
||||||
|
"size": os.path.getsize(image_path),
|
||||||
|
"path": image_path
|
||||||
|
}
|
||||||
|
|
||||||
|
def _resolve_arch_dependencies(self, arch, work_dir):
|
||||||
|
"""Mock architecture-specific dependency resolution."""
|
||||||
|
if arch == "amd64":
|
||||||
|
packages = ["linux-image-amd64", "grub-efi-amd64", "initramfs-tools"]
|
||||||
|
repositories = ["debian", "debian-security"]
|
||||||
|
elif arch == "arm64":
|
||||||
|
packages = ["linux-image-arm64", "grub-efi-arm64", "initramfs-tools"]
|
||||||
|
repositories = ["debian", "debian-security"]
|
||||||
|
else:
|
||||||
|
packages = ["linux-image-generic", "grub-efi", "initramfs-tools"]
|
||||||
|
repositories = ["debian", "debian-security"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"packages": packages,
|
||||||
|
"repositories": repositories,
|
||||||
|
"architecture": arch
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
203
test/test_build_disk.py
Normal file
203
test/test_build_disk.py
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test disk image building functionality for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module tests the disk image building pipeline, including:
|
||||||
|
- Manifest validation
|
||||||
|
- Package installation
|
||||||
|
- Filesystem creation
|
||||||
|
- Bootloader configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDiskImageBuilding:
|
||||||
|
"""Test cases for disk image building functionality."""
|
||||||
|
|
||||||
|
def test_manifest_validation(self, work_dir):
|
||||||
|
"""Test manifest validation for Debian images."""
|
||||||
|
# Create a test manifest
|
||||||
|
manifest = {
|
||||||
|
"pipeline": {
|
||||||
|
"build": {
|
||||||
|
"name": "org.osbuild.debian-filesystem",
|
||||||
|
"options": {
|
||||||
|
"rootfs_type": "ext4",
|
||||||
|
"ostree_integration": True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.apt",
|
||||||
|
"options": {
|
||||||
|
"packages": ["linux-image-amd64", "systemd"],
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate manifest structure
|
||||||
|
assert "pipeline" in manifest
|
||||||
|
assert "build" in manifest["pipeline"]
|
||||||
|
assert "stages" in manifest["pipeline"]
|
||||||
|
|
||||||
|
# Validate Debian-specific options
|
||||||
|
build_stage = manifest["pipeline"]["build"]
|
||||||
|
assert build_stage["name"] == "org.osbuild.debian-filesystem"
|
||||||
|
assert build_stage["options"]["ostree_integration"] is True
|
||||||
|
|
||||||
|
# Validate APT stage
|
||||||
|
apt_stage = manifest["pipeline"]["stages"][0]
|
||||||
|
assert apt_stage["name"] == "org.osbuild.apt"
|
||||||
|
assert apt_stage["options"]["release"] == "trixie"
|
||||||
|
assert "linux-image-amd64" in apt_stage["options"]["packages"]
|
||||||
|
|
||||||
|
def test_debian_package_installation(self, work_dir):
|
||||||
|
"""Test Debian package installation pipeline."""
|
||||||
|
# Mock package installation
|
||||||
|
with patch('subprocess.run') as mock_run:
|
||||||
|
mock_run.return_value.returncode = 0
|
||||||
|
|
||||||
|
# Test package installation
|
||||||
|
packages = ["linux-image-amd64", "systemd", "ostree"]
|
||||||
|
result = self._install_packages(packages, work_dir)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
mock_run.assert_called()
|
||||||
|
|
||||||
|
def test_filesystem_creation(self, work_dir):
|
||||||
|
"""Test Debian filesystem creation."""
|
||||||
|
# Test filesystem structure
|
||||||
|
fs_structure = self._create_filesystem_structure(work_dir)
|
||||||
|
|
||||||
|
expected_dirs = ["/etc", "/var", "/home", "/boot", "/usr"]
|
||||||
|
for expected_dir in expected_dirs:
|
||||||
|
full_path = os.path.join(work_dir, expected_dir.lstrip("/"))
|
||||||
|
assert os.path.exists(full_path), f"Directory {expected_dir} not created"
|
||||||
|
|
||||||
|
# Test /home -> /var/home symlink
|
||||||
|
home_link = os.path.join(work_dir, "home")
|
||||||
|
var_home = os.path.join(work_dir, "var", "home")
|
||||||
|
assert os.path.islink(home_link), "/home symlink not created"
|
||||||
|
assert os.path.realpath(home_link) == var_home
|
||||||
|
|
||||||
|
def test_ostree_integration(self, work_dir):
|
||||||
|
"""Test OSTree integration setup."""
|
||||||
|
# Test OSTree configuration
|
||||||
|
ostree_config = self._setup_ostree_integration(work_dir)
|
||||||
|
|
||||||
|
assert ostree_config["mode"] == "bare-user-only"
|
||||||
|
assert ostree_config["repo"] == "/var/lib/ostree/repo"
|
||||||
|
|
||||||
|
# Test OSTree repository creation
|
||||||
|
repo_path = os.path.join(work_dir, "var", "lib", "ostree", "repo")
|
||||||
|
assert os.path.exists(repo_path), "OSTree repository not created"
|
||||||
|
|
||||||
|
def test_bootloader_configuration(self, work_dir):
|
||||||
|
"""Test GRUB bootloader configuration for Debian."""
|
||||||
|
# Test GRUB configuration
|
||||||
|
grub_config = self._configure_grub(work_dir)
|
||||||
|
|
||||||
|
assert "GRUB_DEFAULT" in grub_config
|
||||||
|
assert "GRUB_TIMEOUT" in grub_config
|
||||||
|
assert "GRUB_CMDLINE_LINUX" in grub_config
|
||||||
|
|
||||||
|
# Test UEFI boot configuration
|
||||||
|
uefi_config = self._configure_uefi_boot(work_dir)
|
||||||
|
assert uefi_config["uefi_enabled"] is True
|
||||||
|
assert uefi_config["secure_boot"] is False
|
||||||
|
|
||||||
|
def _install_packages(self, packages, work_dir):
|
||||||
|
"""Mock package installation."""
|
||||||
|
# This would integrate with the actual APT stage
|
||||||
|
logger.info(f"Installing packages: {packages}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _create_filesystem_structure(self, work_dir):
|
||||||
|
"""Create basic filesystem structure."""
|
||||||
|
dirs = ["etc", "var", "home", "boot", "usr", "usr/bin", "usr/lib", "usr/sbin"]
|
||||||
|
for dir_path in dirs:
|
||||||
|
full_path = os.path.join(work_dir, dir_path)
|
||||||
|
os.makedirs(full_path, exist_ok=True)
|
||||||
|
|
||||||
|
# Create /home -> /var/home symlink
|
||||||
|
var_home = os.path.join(work_dir, "var", "home")
|
||||||
|
os.makedirs(var_home, exist_ok=True)
|
||||||
|
home_link = os.path.join(work_dir, "home")
|
||||||
|
if os.path.exists(home_link):
|
||||||
|
os.remove(home_link)
|
||||||
|
os.symlink(var_home, home_link)
|
||||||
|
|
||||||
|
return {"status": "created", "directories": dirs}
|
||||||
|
|
||||||
|
def _setup_ostree_integration(self, work_dir):
|
||||||
|
"""Set up OSTree integration."""
|
||||||
|
ostree_dir = os.path.join(work_dir, "var", "lib", "ostree", "repo")
|
||||||
|
os.makedirs(ostree_dir, exist_ok=True)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"mode": "bare-user-only",
|
||||||
|
"repo": "/var/lib/ostree/repo"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write OSTree configuration
|
||||||
|
config_file = os.path.join(work_dir, "etc", "ostree", "ostree.conf")
|
||||||
|
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
||||||
|
|
||||||
|
with open(config_file, 'w') as f:
|
||||||
|
f.write("[core]\n")
|
||||||
|
f.write(f"mode={config['mode']}\n")
|
||||||
|
f.write(f"repo={config['repo']}\n")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _configure_grub(self, work_dir):
|
||||||
|
"""Configure GRUB bootloader."""
|
||||||
|
grub_config = {
|
||||||
|
"GRUB_DEFAULT": "0",
|
||||||
|
"GRUB_TIMEOUT": "5",
|
||||||
|
"GRUB_CMDLINE_LINUX": "console=ttyS0,115200n8 console=tty0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write GRUB configuration
|
||||||
|
grub_dir = os.path.join(work_dir, "etc", "default")
|
||||||
|
os.makedirs(grub_dir, exist_ok=True)
|
||||||
|
|
||||||
|
grub_file = os.path.join(grub_dir, "grub")
|
||||||
|
with open(grub_file, 'w') as f:
|
||||||
|
for key, value in grub_config.items():
|
||||||
|
f.write(f'{key}="{value}"\n')
|
||||||
|
|
||||||
|
return grub_config
|
||||||
|
|
||||||
|
def _configure_uefi_boot(self, work_dir):
|
||||||
|
"""Configure UEFI boot."""
|
||||||
|
uefi_config = {
|
||||||
|
"uefi_enabled": True,
|
||||||
|
"secure_boot": False,
|
||||||
|
"boot_entries": ["debian", "debian-fallback"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create UEFI boot directory
|
||||||
|
efi_dir = os.path.join(work_dir, "boot", "efi", "EFI", "debian")
|
||||||
|
os.makedirs(efi_dir, exist_ok=True)
|
||||||
|
|
||||||
|
return uefi_config
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
245
test/test_build_iso.py
Normal file
245
test/test_build_iso.py
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test ISO image building functionality for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module tests the ISO image building pipeline, including:
|
||||||
|
- ISO manifest validation
|
||||||
|
- ISO creation process
|
||||||
|
- Debian-specific ISO features
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestISOBuilding:
|
||||||
|
"""Test cases for ISO image building functionality."""
|
||||||
|
|
||||||
|
def test_iso_manifest_validation(self, work_dir):
|
||||||
|
"""Test ISO manifest validation for Debian images."""
|
||||||
|
# Create a test ISO manifest
|
||||||
|
manifest = {
|
||||||
|
"pipeline": {
|
||||||
|
"build": {
|
||||||
|
"name": "org.osbuild.debian-filesystem",
|
||||||
|
"options": {
|
||||||
|
"rootfs_type": "ext4",
|
||||||
|
"ostree_integration": True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.apt",
|
||||||
|
"options": {
|
||||||
|
"packages": ["linux-image-amd64", "systemd"],
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.debian-grub",
|
||||||
|
"options": {
|
||||||
|
"uefi": True,
|
||||||
|
"secure_boot": False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.debian-kernel",
|
||||||
|
"options": {
|
||||||
|
"kernel_package": "linux-image-amd64",
|
||||||
|
"initramfs_tools": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate manifest structure
|
||||||
|
assert "pipeline" in manifest
|
||||||
|
assert "build" in manifest["pipeline"]
|
||||||
|
assert "stages" in manifest["pipeline"]
|
||||||
|
|
||||||
|
# Validate Debian-specific options
|
||||||
|
build_stage = manifest["pipeline"]["build"]
|
||||||
|
assert build_stage["name"] == "org.osbuild.debian-filesystem"
|
||||||
|
assert build_stage["options"]["ostree_integration"] is True
|
||||||
|
|
||||||
|
# Validate stages
|
||||||
|
stages = manifest["pipeline"]["stages"]
|
||||||
|
assert len(stages) >= 3
|
||||||
|
|
||||||
|
# Validate APT stage
|
||||||
|
apt_stage = next((s for s in stages if s["name"] == "org.osbuild.apt"), None)
|
||||||
|
assert apt_stage is not None
|
||||||
|
assert apt_stage["options"]["release"] == "trixie"
|
||||||
|
|
||||||
|
def test_debian_iso_package_installation(self, work_dir):
|
||||||
|
"""Test Debian package installation for ISO builds."""
|
||||||
|
# Mock package installation
|
||||||
|
with patch('subprocess.run') as mock_run:
|
||||||
|
mock_run.return_value.returncode = 0
|
||||||
|
|
||||||
|
# Test package installation
|
||||||
|
packages = ["linux-image-amd64", "systemd", "ostree", "grub-efi-amd64"]
|
||||||
|
result = self._install_iso_packages(packages, work_dir)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
mock_run.assert_called()
|
||||||
|
|
||||||
|
def test_iso_filesystem_creation(self, work_dir):
|
||||||
|
"""Test ISO filesystem creation."""
|
||||||
|
# Test filesystem structure
|
||||||
|
fs_structure = self._create_iso_filesystem(work_dir)
|
||||||
|
|
||||||
|
expected_dirs = ["/etc", "/var", "/home", "/boot", "/usr", "/media"]
|
||||||
|
for expected_dir in expected_dirs:
|
||||||
|
full_path = os.path.join(work_dir, expected_dir.lstrip("/"))
|
||||||
|
assert os.path.exists(full_path), f"Directory {expected_dir} not created"
|
||||||
|
|
||||||
|
# Test ISO-specific directories
|
||||||
|
iso_dirs = ["/media/cdrom", "/media/usb"]
|
||||||
|
for iso_dir in iso_dirs:
|
||||||
|
full_path = os.path.join(work_dir, iso_dir.lstrip("/"))
|
||||||
|
assert os.path.exists(full_path), f"ISO directory {iso_dir} not created"
|
||||||
|
|
||||||
|
def test_iso_bootloader_configuration(self, work_dir):
|
||||||
|
"""Test ISO bootloader configuration."""
|
||||||
|
# Test GRUB configuration for ISO
|
||||||
|
grub_config = self._configure_iso_grub(work_dir)
|
||||||
|
|
||||||
|
assert "GRUB_DEFAULT" in grub_config
|
||||||
|
assert "GRUB_TIMEOUT" in grub_config
|
||||||
|
assert "GRUB_CMDLINE_LINUX" in grub_config
|
||||||
|
|
||||||
|
# Test ISO-specific boot options
|
||||||
|
assert "cdrom" in grub_config["GRUB_CMDLINE_LINUX"]
|
||||||
|
assert "iso-scan" in grub_config["GRUB_CMDLINE_LINUX"]
|
||||||
|
|
||||||
|
def test_iso_ostree_integration(self, work_dir):
|
||||||
|
"""Test OSTree integration for ISO builds."""
|
||||||
|
# Test OSTree configuration
|
||||||
|
ostree_config = self._setup_iso_ostree(work_dir)
|
||||||
|
|
||||||
|
assert ostree_config["mode"] == "bare-user-only"
|
||||||
|
assert ostree_config["repo"] == "/var/lib/ostree/repo"
|
||||||
|
|
||||||
|
# Test ISO-specific OSTree paths
|
||||||
|
iso_repo_path = os.path.join(work_dir, "media", "cdrom", "ostree")
|
||||||
|
assert os.path.exists(iso_repo_path), "ISO OSTree repository not created"
|
||||||
|
|
||||||
|
def test_iso_creation_process(self, work_dir):
|
||||||
|
"""Test the complete ISO creation process."""
|
||||||
|
# Test ISO build pipeline
|
||||||
|
iso_result = self._create_iso_image(work_dir)
|
||||||
|
|
||||||
|
assert iso_result["status"] == "success"
|
||||||
|
assert "iso_path" in iso_result
|
||||||
|
assert os.path.exists(iso_result["iso_path"])
|
||||||
|
|
||||||
|
# Test ISO properties
|
||||||
|
iso_props = self._get_iso_properties(iso_result["iso_path"])
|
||||||
|
assert iso_props["format"] == "iso9660"
|
||||||
|
assert iso_props["size"] > 0
|
||||||
|
|
||||||
|
def _install_iso_packages(self, packages, work_dir):
|
||||||
|
"""Mock ISO package installation."""
|
||||||
|
logger.info(f"Installing ISO packages: {packages}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _create_iso_filesystem(self, work_dir):
|
||||||
|
"""Create ISO filesystem structure."""
|
||||||
|
dirs = [
|
||||||
|
"etc", "var", "home", "boot", "usr", "media",
|
||||||
|
"media/cdrom", "media/usb", "usr/bin", "usr/lib", "usr/sbin"
|
||||||
|
]
|
||||||
|
for dir_path in dirs:
|
||||||
|
full_path = os.path.join(work_dir, dir_path)
|
||||||
|
os.makedirs(full_path, exist_ok=True)
|
||||||
|
|
||||||
|
# Create /home -> /var/home symlink
|
||||||
|
var_home = os.path.join(work_dir, "var", "home")
|
||||||
|
os.makedirs(var_home, exist_ok=True)
|
||||||
|
home_link = os.path.join(work_dir, "home")
|
||||||
|
if os.path.exists(home_link):
|
||||||
|
os.remove(home_link)
|
||||||
|
os.symlink(var_home, home_link)
|
||||||
|
|
||||||
|
return {"status": "created", "directories": dirs}
|
||||||
|
|
||||||
|
def _configure_iso_grub(self, work_dir):
|
||||||
|
"""Configure GRUB for ISO boot."""
|
||||||
|
grub_config = {
|
||||||
|
"GRUB_DEFAULT": "0",
|
||||||
|
"GRUB_TIMEOUT": "5",
|
||||||
|
"GRUB_CMDLINE_LINUX": "console=ttyS0,115200n8 console=tty0 cdrom iso-scan"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write GRUB configuration
|
||||||
|
grub_dir = os.path.join(work_dir, "etc", "default")
|
||||||
|
os.makedirs(grub_dir, exist_ok=True)
|
||||||
|
|
||||||
|
grub_file = os.path.join(grub_dir, "grub")
|
||||||
|
with open(grub_file, 'w') as f:
|
||||||
|
for key, value in grub_config.items():
|
||||||
|
f.write(f'{key}="{value}"\n')
|
||||||
|
|
||||||
|
return grub_config
|
||||||
|
|
||||||
|
def _setup_iso_ostree(self, work_dir):
|
||||||
|
"""Set up OSTree for ISO builds."""
|
||||||
|
ostree_dir = os.path.join(work_dir, "var", "lib", "ostree", "repo")
|
||||||
|
os.makedirs(ostree_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Create ISO-specific OSTree repository
|
||||||
|
iso_ostree_dir = os.path.join(work_dir, "media", "cdrom", "ostree")
|
||||||
|
os.makedirs(iso_ostree_dir, exist_ok=True)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"mode": "bare-user-only",
|
||||||
|
"repo": "/var/lib/ostree/repo"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write OSTree configuration
|
||||||
|
config_file = os.path.join(work_dir, "etc", "ostree", "ostree.conf")
|
||||||
|
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
||||||
|
|
||||||
|
with open(config_file, 'w') as f:
|
||||||
|
f.write("[core]\n")
|
||||||
|
f.write(f"mode={config['mode']}\n")
|
||||||
|
f.write(f"repo={config['repo']}\n")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _create_iso_image(self, work_dir):
|
||||||
|
"""Mock ISO image creation."""
|
||||||
|
# Create a dummy ISO file
|
||||||
|
iso_path = os.path.join(work_dir, "debian-trixie.iso")
|
||||||
|
with open(iso_path, 'wb') as f:
|
||||||
|
f.write(b"ISO9660 dummy content")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"iso_path": iso_path,
|
||||||
|
"size": os.path.getsize(iso_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_iso_properties(self, iso_path):
|
||||||
|
"""Get ISO image properties."""
|
||||||
|
return {
|
||||||
|
"format": "iso9660",
|
||||||
|
"size": os.path.getsize(iso_path),
|
||||||
|
"path": iso_path
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
367
test/test_flake8.py
Normal file
367
test/test_flake8.py
Normal file
|
|
@ -0,0 +1,367 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test flake8 compliance for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module tests code style compliance using flake8,
|
||||||
|
including:
|
||||||
|
- PEP 8 compliance
|
||||||
|
- Code style validation
|
||||||
|
- Debian-specific style standards
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFlake8Compliance:
|
||||||
|
"""Test cases for flake8 compliance."""
|
||||||
|
|
||||||
|
def test_flake8_installation(self, work_dir):
|
||||||
|
"""Test that flake8 is available."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["flake8", "--version"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, "flake8 is not properly installed"
|
||||||
|
logger.info("flake8 is available")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pytest.skip("flake8 not installed")
|
||||||
|
|
||||||
|
def test_flake8_basic_usage(self, work_dir):
|
||||||
|
"""Test basic flake8 functionality."""
|
||||||
|
# Create a simple test file
|
||||||
|
test_file = os.path.join(work_dir, "test_flake8.py")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test file for flake8 validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_function():
|
||||||
|
"""Test function for flake8."""
|
||||||
|
return "test"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(test_function())
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Run flake8 on the test file
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["flake8", test_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# flake8 should run without errors
|
||||||
|
assert result.returncode == 0, f"flake8 found issues: {result.stdout}"
|
||||||
|
logger.info("flake8 basic functionality test passed")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("flake8 test timed out")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"flake8 test failed: {e}")
|
||||||
|
|
||||||
|
def test_pep8_compliance(self, work_dir):
|
||||||
|
"""Test PEP 8 compliance."""
|
||||||
|
# Create a test file with various PEP 8 issues
|
||||||
|
test_file = os.path.join(work_dir, "pep8_test.py")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test file for PEP 8 compliance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# This line is too long and should trigger E501
|
||||||
|
very_long_line_that_exceeds_the_maximum_line_length_and_should_trigger_a_flake8_error = "test"
|
||||||
|
|
||||||
|
def test_function_with_bad_spacing( x,y ):
|
||||||
|
"""Function with bad spacing."""
|
||||||
|
if x==y:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
class BadClass:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def method_with_bad_indentation(self):
|
||||||
|
return "bad indentation"
|
||||||
|
|
||||||
|
# Missing blank line at end of file
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Run flake8 and check for expected errors
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["flake8", test_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should find PEP 8 violations
|
||||||
|
assert result.returncode != 0, "flake8 should find PEP 8 violations"
|
||||||
|
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
|
||||||
|
# Check for specific error codes
|
||||||
|
expected_errors = ["E501", "E201", "E202", "E225", "E111", "W292"]
|
||||||
|
found_errors = []
|
||||||
|
|
||||||
|
for error_code in expected_errors:
|
||||||
|
if error_code in output:
|
||||||
|
found_errors.append(error_code)
|
||||||
|
|
||||||
|
assert len(found_errors) > 0, f"No expected PEP 8 errors found. Output: {output}"
|
||||||
|
logger.info(f"Found PEP 8 violations: {found_errors}")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("flake8 PEP 8 test timed out")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"flake8 PEP 8 test failed: {e}")
|
||||||
|
|
||||||
|
def test_debian_specific_style_standards(self, work_dir):
|
||||||
|
"""Test Debian-specific style standards."""
|
||||||
|
# Create a test file following Debian style standards
|
||||||
|
test_file = os.path.join(work_dir, "debian_style_test.py")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Debian-specific test file for flake8 validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DebianBootcBuilder:
|
||||||
|
"""Debian bootc image builder class."""
|
||||||
|
|
||||||
|
def __init__(self, work_dir: str):
|
||||||
|
"""Initialize the builder."""
|
||||||
|
self.work_dir = work_dir
|
||||||
|
self.packages: List[str] = []
|
||||||
|
self.release = "trixie"
|
||||||
|
self.arch = "amd64"
|
||||||
|
|
||||||
|
def add_package(self, package: str) -> None:
|
||||||
|
"""Add a package to the installation list."""
|
||||||
|
if package not in self.packages:
|
||||||
|
self.packages.append(package)
|
||||||
|
logger.info(f"Added package: {package}")
|
||||||
|
|
||||||
|
def set_release(self, release: str) -> None:
|
||||||
|
"""Set the Debian release."""
|
||||||
|
valid_releases = ["trixie", "bookworm", "bullseye"]
|
||||||
|
if release in valid_releases:
|
||||||
|
self.release = release
|
||||||
|
logger.info(f"Set release to: {release}")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid release: {release}")
|
||||||
|
|
||||||
|
def build_image(self) -> Dict[str, Any]:
|
||||||
|
"""Build the Debian image."""
|
||||||
|
logger.info("Starting Debian image build")
|
||||||
|
|
||||||
|
# Validate configuration
|
||||||
|
if not self.packages:
|
||||||
|
raise ValueError("No packages specified")
|
||||||
|
|
||||||
|
# Build process would go here
|
||||||
|
result = {
|
||||||
|
"status": "success",
|
||||||
|
"packages": self.packages,
|
||||||
|
"release": self.release,
|
||||||
|
"arch": self.arch
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Debian image build completed")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Main function."""
|
||||||
|
builder = DebianBootcBuilder("/tmp/test")
|
||||||
|
builder.add_package("linux-image-amd64")
|
||||||
|
builder.add_package("systemd")
|
||||||
|
builder.set_release("trixie")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = builder.build_image()
|
||||||
|
print(f"Build result: {result}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Build failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Run flake8 on the Debian style file
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["flake8", test_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should pass flake8 validation
|
||||||
|
assert result.returncode == 0, f"flake8 found issues in Debian style file: {result.stdout}"
|
||||||
|
logger.info("Debian-specific style standards test passed")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("flake8 Debian style test timed out")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"flake8 Debian style test failed: {e}")
|
||||||
|
|
||||||
|
def test_flake8_configuration(self, work_dir):
|
||||||
|
"""Test flake8 configuration and custom rules."""
|
||||||
|
# Create a flake8 configuration file
|
||||||
|
setup_cfg = os.path.join(work_dir, "setup.cfg")
|
||||||
|
with open(setup_cfg, 'w') as f:
|
||||||
|
f.write('''[flake8]
|
||||||
|
# Maximum line length
|
||||||
|
max-line-length = 120
|
||||||
|
|
||||||
|
# Ignore specific error codes
|
||||||
|
ignore = E203, W503
|
||||||
|
|
||||||
|
# Exclude directories
|
||||||
|
exclude = .git,__pycache__,.venv
|
||||||
|
|
||||||
|
# Maximum complexity
|
||||||
|
max-complexity = 10
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Create a test file that would normally trigger ignored errors
|
||||||
|
test_file = os.path.join(work_dir, "config_test.py")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test file for flake8 configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_function():
|
||||||
|
"""Test function for flake8 config."""
|
||||||
|
# This line is long but should be allowed by config
|
||||||
|
very_long_line_that_exceeds_normal_pep8_but_is_allowed_by_our_config = "test"
|
||||||
|
return very_long_line_that_exceeds_normal_pep8_but_is_allowed_by_our_config
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(test_function())
|
||||||
|
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Run flake8 with custom configuration
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["flake8", "--config", setup_cfg, test_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should pass with custom configuration
|
||||||
|
assert result.returncode == 0, f"flake8 with custom config failed: {result.stdout}"
|
||||||
|
logger.info("flake8 configuration test passed")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("flake8 configuration test timed out")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"flake8 configuration test failed: {e}")
|
||||||
|
|
||||||
|
def test_flake8_error_codes(self, work_dir):
|
||||||
|
"""Test specific flake8 error codes."""
|
||||||
|
# Create a test file with specific error types
|
||||||
|
test_file = os.path.join(work_dir, "error_codes_test.py")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test file for specific flake8 error codes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# E501: Line too long
|
||||||
|
very_long_line_that_exceeds_the_maximum_line_length_and_should_trigger_a_flake8_error = "test"
|
||||||
|
|
||||||
|
# E201: Whitespace after '('
|
||||||
|
def function_with_bad_spacing( x ):
|
||||||
|
return x
|
||||||
|
|
||||||
|
# E202: Whitespace before ')'
|
||||||
|
def another_bad_function( y ):
|
||||||
|
return y
|
||||||
|
|
||||||
|
# E225: Missing whitespace around operator
|
||||||
|
x=1
|
||||||
|
y=2
|
||||||
|
z=x+y
|
||||||
|
|
||||||
|
# E111: Bad indentation
|
||||||
|
def bad_indentation():
|
||||||
|
return "bad"
|
||||||
|
|
||||||
|
# W292: No newline at end of file
|
||||||
|
result = "no newline"
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Run flake8 and check for specific error codes
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["flake8", test_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should find errors
|
||||||
|
assert result.returncode != 0, "flake8 should find style errors"
|
||||||
|
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
|
||||||
|
# Check for specific error codes
|
||||||
|
expected_errors = ["E501", "E201", "E202", "E225", "E111", "W292"]
|
||||||
|
found_errors = []
|
||||||
|
|
||||||
|
for error_code in expected_errors:
|
||||||
|
if error_code in output:
|
||||||
|
found_errors.append(error_code)
|
||||||
|
|
||||||
|
# Should find at least some of the expected errors
|
||||||
|
assert len(found_errors) >= 3, f"Expected more error codes. Found: {found_errors}"
|
||||||
|
logger.info(f"Found flake8 error codes: {found_errors}")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("flake8 error codes test timed out")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"flake8 error codes test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
228
test/test_manifest.py
Normal file
228
test/test_manifest.py
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test manifest validation and processing for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module tests manifest handling, including:
|
||||||
|
- Manifest structure validation
|
||||||
|
- Stage configuration validation
|
||||||
|
- Debian-specific manifest processing
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestManifestValidation:
|
||||||
|
"""Test cases for manifest validation."""
|
||||||
|
|
||||||
|
def test_debian_manifest_structure(self, work_dir):
|
||||||
|
"""Test Debian manifest structure validation."""
|
||||||
|
manifest = self._create_debian_manifest()
|
||||||
|
|
||||||
|
# Validate top-level structure
|
||||||
|
assert "pipeline" in manifest
|
||||||
|
assert "build" in manifest["pipeline"]
|
||||||
|
assert "stages" in manifest["pipeline"]
|
||||||
|
|
||||||
|
# Validate build stage
|
||||||
|
build_stage = manifest["pipeline"]["build"]
|
||||||
|
assert build_stage["name"] == "org.osbuild.debian-filesystem"
|
||||||
|
assert "options" in build_stage
|
||||||
|
|
||||||
|
# Validate stages
|
||||||
|
stages = manifest["pipeline"]["stages"]
|
||||||
|
assert len(stages) > 0
|
||||||
|
|
||||||
|
# Validate APT stage
|
||||||
|
apt_stage = next((s for s in stages if s["name"] == "org.osbuild.apt"), None)
|
||||||
|
assert apt_stage is not None
|
||||||
|
assert "options" in apt_stage
|
||||||
|
assert "packages" in apt_stage["options"]
|
||||||
|
|
||||||
|
def test_debian_package_validation(self, work_dir):
|
||||||
|
"""Test Debian package validation in manifests."""
|
||||||
|
manifest = self._create_debian_manifest()
|
||||||
|
|
||||||
|
# Extract packages from APT stage
|
||||||
|
apt_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||||
|
if s["name"] == "org.osbuild.apt"), None)
|
||||||
|
packages = apt_stage["options"]["packages"]
|
||||||
|
|
||||||
|
# Validate essential Debian packages
|
||||||
|
essential_packages = [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree"
|
||||||
|
]
|
||||||
|
|
||||||
|
for pkg in essential_packages:
|
||||||
|
assert pkg in packages, f"Essential package {pkg} missing"
|
||||||
|
|
||||||
|
# Validate package format
|
||||||
|
for pkg in packages:
|
||||||
|
assert isinstance(pkg, str), f"Package {pkg} is not a string"
|
||||||
|
assert len(pkg) > 0, f"Empty package name found"
|
||||||
|
|
||||||
|
def test_debian_repository_configuration(self, work_dir):
|
||||||
|
"""Test Debian repository configuration in manifests."""
|
||||||
|
manifest = self._create_debian_manifest()
|
||||||
|
|
||||||
|
# Validate repository configuration
|
||||||
|
apt_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||||
|
if s["name"] == "org.osbuild.apt"), None)
|
||||||
|
|
||||||
|
options = apt_stage["options"]
|
||||||
|
assert "release" in options
|
||||||
|
assert "arch" in options
|
||||||
|
|
||||||
|
# Validate Debian release
|
||||||
|
assert options["release"] == "trixie"
|
||||||
|
assert options["arch"] == "amd64"
|
||||||
|
|
||||||
|
def test_ostree_integration_configuration(self, work_dir):
|
||||||
|
"""Test OSTree integration configuration."""
|
||||||
|
manifest = self._create_debian_manifest()
|
||||||
|
|
||||||
|
# Validate OSTree integration in filesystem stage
|
||||||
|
fs_stage = manifest["pipeline"]["build"]
|
||||||
|
options = fs_stage["options"]
|
||||||
|
|
||||||
|
assert "ostree_integration" in options
|
||||||
|
assert options["ostree_integration"] is True
|
||||||
|
|
||||||
|
# Validate home symlink configuration
|
||||||
|
assert "home_symlink" in options
|
||||||
|
assert options["home_symlink"] is True
|
||||||
|
|
||||||
|
def test_manifest_serialization(self, work_dir):
|
||||||
|
"""Test manifest serialization to YAML and JSON."""
|
||||||
|
manifest = self._create_debian_manifest()
|
||||||
|
|
||||||
|
# Test YAML serialization
|
||||||
|
yaml_content = yaml.dump(manifest, default_flow_style=False)
|
||||||
|
assert "org.osbuild.debian-filesystem" in yaml_content
|
||||||
|
assert "org.osbuild.apt" in yaml_content
|
||||||
|
|
||||||
|
# Test JSON serialization
|
||||||
|
json_content = json.dumps(manifest, indent=2)
|
||||||
|
assert "org.osbuild.debian-filesystem" in json_content
|
||||||
|
assert "org.osbuild.apt" in json_content
|
||||||
|
|
||||||
|
# Test round-trip serialization
|
||||||
|
yaml_parsed = yaml.safe_load(yaml_content)
|
||||||
|
assert yaml_parsed == manifest
|
||||||
|
|
||||||
|
json_parsed = json.loads(json_content)
|
||||||
|
assert json_parsed == manifest
|
||||||
|
|
||||||
|
def test_manifest_validation_errors(self, work_dir):
|
||||||
|
"""Test manifest validation error handling."""
|
||||||
|
# Test missing pipeline
|
||||||
|
invalid_manifest = {"stages": []}
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
_ = invalid_manifest["pipeline"]
|
||||||
|
|
||||||
|
# Test missing build stage
|
||||||
|
invalid_manifest = {"pipeline": {"stages": []}}
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
_ = invalid_manifest["pipeline"]["build"]
|
||||||
|
|
||||||
|
# Test missing stages
|
||||||
|
invalid_manifest = {"pipeline": {"build": {"name": "test"}}}
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
_ = invalid_manifest["pipeline"]["stages"]
|
||||||
|
|
||||||
|
def test_debian_specific_manifest_features(self, work_dir):
|
||||||
|
"""Test Debian-specific manifest features."""
|
||||||
|
manifest = self._create_debian_manifest()
|
||||||
|
|
||||||
|
# Test GRUB stage configuration
|
||||||
|
grub_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||||
|
if s["name"] == "org.osbuild.debian-grub"), None)
|
||||||
|
|
||||||
|
if grub_stage:
|
||||||
|
options = grub_stage["options"]
|
||||||
|
assert "uefi" in options
|
||||||
|
assert "secure_boot" in options
|
||||||
|
assert "timeout" in options
|
||||||
|
|
||||||
|
# Test kernel stage configuration
|
||||||
|
kernel_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||||
|
if s["name"] == "org.osbuild.debian-kernel"), None)
|
||||||
|
|
||||||
|
if kernel_stage:
|
||||||
|
options = kernel_stage["options"]
|
||||||
|
assert "kernel_package" in options
|
||||||
|
assert "initramfs_tools" in options
|
||||||
|
|
||||||
|
def _create_debian_manifest(self):
|
||||||
|
"""Create a sample Debian manifest for testing."""
|
||||||
|
return {
|
||||||
|
"pipeline": {
|
||||||
|
"build": {
|
||||||
|
"name": "org.osbuild.debian-filesystem",
|
||||||
|
"options": {
|
||||||
|
"rootfs_type": "ext4",
|
||||||
|
"ostree_integration": True,
|
||||||
|
"home_symlink": True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.apt",
|
||||||
|
"options": {
|
||||||
|
"packages": [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"linux-headers-amd64",
|
||||||
|
"systemd",
|
||||||
|
"systemd-sysv",
|
||||||
|
"dbus",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools",
|
||||||
|
"util-linux",
|
||||||
|
"parted",
|
||||||
|
"e2fsprogs",
|
||||||
|
"dosfstools",
|
||||||
|
"efibootmgr",
|
||||||
|
"sudo",
|
||||||
|
"network-manager"
|
||||||
|
],
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.debian-grub",
|
||||||
|
"options": {
|
||||||
|
"uefi": True,
|
||||||
|
"secure_boot": False,
|
||||||
|
"timeout": 5,
|
||||||
|
"default_entry": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.debian-kernel",
|
||||||
|
"options": {
|
||||||
|
"kernel_package": "linux-image-amd64",
|
||||||
|
"initramfs_tools": True,
|
||||||
|
"ostree_integration": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
290
test/test_opts.py
Normal file
290
test/test_opts.py
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test command line options for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module tests command line argument parsing and validation,
|
||||||
|
including:
|
||||||
|
- Required arguments
|
||||||
|
- Optional arguments
|
||||||
|
- Argument validation
|
||||||
|
- Debian-specific options
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommandLineOptions:
|
||||||
|
"""Test cases for command line options."""
|
||||||
|
|
||||||
|
def test_required_arguments(self, work_dir):
|
||||||
|
"""Test required command line arguments."""
|
||||||
|
# Test minimum required arguments
|
||||||
|
required_args = {
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"output": work_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate required arguments
|
||||||
|
for arg_name, arg_value in required_args.items():
|
||||||
|
assert arg_value is not None, f"Required argument {arg_name} is None"
|
||||||
|
if arg_name == "output":
|
||||||
|
assert os.path.exists(arg_value), f"Output directory {arg_value} does not exist"
|
||||||
|
|
||||||
|
def test_optional_arguments(self, work_dir):
|
||||||
|
"""Test optional command line arguments."""
|
||||||
|
# Test optional arguments with default values
|
||||||
|
optional_args = {
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"verbose": False,
|
||||||
|
"clean": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate optional arguments
|
||||||
|
for arg_name, arg_value in optional_args.items():
|
||||||
|
assert arg_name in optional_args, f"Optional argument {arg_name} not found"
|
||||||
|
assert arg_value is not None, f"Optional argument {arg_name} has no default value"
|
||||||
|
|
||||||
|
def test_debian_specific_options(self, work_dir):
|
||||||
|
"""Test Debian-specific command line options."""
|
||||||
|
# Test Debian-specific options
|
||||||
|
debian_options = {
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"package_manager": "apt",
|
||||||
|
"initramfs_tools": True,
|
||||||
|
"grub_efi": True
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate Debian-specific options
|
||||||
|
assert debian_options["release"] in ["trixie", "bookworm", "bullseye"], \
|
||||||
|
f"Invalid Debian release: {debian_options['release']}"
|
||||||
|
|
||||||
|
assert debian_options["arch"] in ["amd64", "arm64", "i386"], \
|
||||||
|
f"Invalid architecture: {debian_options['arch']}"
|
||||||
|
|
||||||
|
assert debian_options["package_manager"] == "apt", \
|
||||||
|
f"Invalid package manager: {debian_options['package_manager']}"
|
||||||
|
|
||||||
|
assert debian_options["initramfs_tools"] is True, \
|
||||||
|
"initramfs-tools should be enabled for Debian"
|
||||||
|
|
||||||
|
assert debian_options["grub_efi"] is True, \
|
||||||
|
"GRUB EFI should be enabled for Debian"
|
||||||
|
|
||||||
|
def test_argument_validation(self, work_dir):
|
||||||
|
"""Test argument validation logic."""
|
||||||
|
# Test valid arguments
|
||||||
|
valid_args = {
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"output": work_dir,
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64"
|
||||||
|
}
|
||||||
|
|
||||||
|
validation_result = self._validate_arguments(valid_args)
|
||||||
|
assert validation_result["valid"] is True, \
|
||||||
|
f"Valid arguments failed validation: {validation_result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
# Test invalid arguments
|
||||||
|
invalid_args = {
|
||||||
|
"container": "invalid:image",
|
||||||
|
"output": "/nonexistent/path",
|
||||||
|
"release": "invalid-release",
|
||||||
|
"arch": "invalid-arch"
|
||||||
|
}
|
||||||
|
|
||||||
|
validation_result = self._validate_arguments(invalid_args)
|
||||||
|
assert validation_result["valid"] is False, \
|
||||||
|
"Invalid arguments should fail validation"
|
||||||
|
|
||||||
|
def test_output_directory_handling(self, work_dir):
|
||||||
|
"""Test output directory handling."""
|
||||||
|
# Test existing directory
|
||||||
|
existing_dir = work_dir
|
||||||
|
result = self._handle_output_directory(existing_dir)
|
||||||
|
assert result["success"] is True, \
|
||||||
|
f"Existing directory handling failed: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
# Test non-existent directory creation
|
||||||
|
new_dir = os.path.join(work_dir, "new_output")
|
||||||
|
result = self._handle_output_directory(new_dir)
|
||||||
|
assert result["success"] is True, \
|
||||||
|
f"New directory creation failed: {result.get('error', 'Unknown error')}"
|
||||||
|
assert os.path.exists(new_dir), "New directory was not created"
|
||||||
|
|
||||||
|
# Test invalid directory path
|
||||||
|
invalid_dir = "/invalid/path/with/permissions/issue"
|
||||||
|
result = self._handle_output_directory(invalid_dir)
|
||||||
|
assert result["success"] is False, "Invalid directory should fail"
|
||||||
|
|
||||||
|
def test_container_image_validation(self, work_dir):
|
||||||
|
"""Test container image validation."""
|
||||||
|
# Test valid Debian image
|
||||||
|
valid_image = "debian:trixie"
|
||||||
|
result = self._validate_container_image(valid_image)
|
||||||
|
assert result["valid"] is True, \
|
||||||
|
f"Valid Debian image failed validation: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
# Test invalid image
|
||||||
|
invalid_image = "invalid:image"
|
||||||
|
result = self._validate_container_image(invalid_image)
|
||||||
|
assert result["valid"] is False, "Invalid image should fail validation"
|
||||||
|
|
||||||
|
# Test image with specific architecture
|
||||||
|
arch_image = "debian:trixie-amd64"
|
||||||
|
result = self._validate_container_image(arch_image)
|
||||||
|
assert result["valid"] is True, \
|
||||||
|
f"Architecture-specific image failed validation: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
def test_package_list_validation(self, work_dir):
|
||||||
|
"""Test package list validation."""
|
||||||
|
# Test valid Debian packages
|
||||||
|
valid_packages = [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools"
|
||||||
|
]
|
||||||
|
|
||||||
|
result = self._validate_package_list(valid_packages)
|
||||||
|
assert result["valid"] is True, \
|
||||||
|
f"Valid packages failed validation: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
# Test invalid packages
|
||||||
|
invalid_packages = [
|
||||||
|
"invalid-package",
|
||||||
|
"nonexistent-package"
|
||||||
|
]
|
||||||
|
|
||||||
|
result = self._validate_package_list(invalid_packages)
|
||||||
|
assert result["valid"] is False, "Invalid packages should fail validation"
|
||||||
|
|
||||||
|
def test_manifest_generation(self, work_dir):
|
||||||
|
"""Test manifest generation from command line options."""
|
||||||
|
# Test manifest generation
|
||||||
|
options = {
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-amd64", "systemd", "ostree"]
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest = self._generate_manifest(options)
|
||||||
|
|
||||||
|
# Validate generated manifest
|
||||||
|
assert "pipeline" in manifest
|
||||||
|
assert "build" in manifest["pipeline"]
|
||||||
|
assert "stages" in manifest["pipeline"]
|
||||||
|
|
||||||
|
# Validate Debian-specific content
|
||||||
|
build_stage = manifest["pipeline"]["build"]
|
||||||
|
assert build_stage["name"] == "org.osbuild.debian-filesystem"
|
||||||
|
|
||||||
|
# Validate APT stage
|
||||||
|
apt_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||||
|
if s["name"] == "org.osbuild.apt"), None)
|
||||||
|
assert apt_stage is not None
|
||||||
|
assert apt_stage["options"]["release"] == "trixie"
|
||||||
|
assert apt_stage["options"]["arch"] == "amd64"
|
||||||
|
|
||||||
|
def _validate_arguments(self, args):
|
||||||
|
"""Mock argument validation."""
|
||||||
|
# Check required arguments
|
||||||
|
if "container" not in args or not args["container"]:
|
||||||
|
return {"valid": False, "error": "Container image is required"}
|
||||||
|
|
||||||
|
if "output" not in args or not args["output"]:
|
||||||
|
return {"valid": False, "error": "Output directory is required"}
|
||||||
|
|
||||||
|
# Check Debian-specific validation
|
||||||
|
if "release" in args:
|
||||||
|
valid_releases = ["trixie", "bookworm", "bullseye"]
|
||||||
|
if args["release"] not in valid_releases:
|
||||||
|
return {"valid": False, "error": f"Invalid Debian release: {args['release']}"}
|
||||||
|
|
||||||
|
if "arch" in args:
|
||||||
|
valid_archs = ["amd64", "arm64", "i386"]
|
||||||
|
if args["arch"] not in valid_archs:
|
||||||
|
return {"valid": False, "error": f"Invalid architecture: {args['arch']}"}
|
||||||
|
|
||||||
|
return {"valid": True}
|
||||||
|
|
||||||
|
def _handle_output_directory(self, output_dir):
|
||||||
|
"""Mock output directory handling."""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(output_dir):
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Test if directory is writable
|
||||||
|
test_file = os.path.join(output_dir, "test_write")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write("test")
|
||||||
|
os.remove(test_file)
|
||||||
|
|
||||||
|
return {"success": True}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def _validate_container_image(self, image):
|
||||||
|
"""Mock container image validation."""
|
||||||
|
# Check if it's a valid Debian image
|
||||||
|
if image.startswith("debian:"):
|
||||||
|
return {"valid": True, "type": "debian"}
|
||||||
|
else:
|
||||||
|
return {"valid": False, "error": "Not a valid Debian image"}
|
||||||
|
|
||||||
|
def _validate_package_list(self, packages):
|
||||||
|
"""Mock package list validation."""
|
||||||
|
# Check if packages look like valid Debian packages
|
||||||
|
valid_packages = [
|
||||||
|
"linux-image-amd64", "systemd", "ostree", "grub-efi-amd64",
|
||||||
|
"initramfs-tools", "util-linux", "parted", "e2fsprogs"
|
||||||
|
]
|
||||||
|
|
||||||
|
for pkg in packages:
|
||||||
|
if pkg not in valid_packages:
|
||||||
|
return {"valid": False, "error": f"Invalid package: {pkg}"}
|
||||||
|
|
||||||
|
return {"valid": True}
|
||||||
|
|
||||||
|
def _generate_manifest(self, options):
|
||||||
|
"""Mock manifest generation."""
|
||||||
|
return {
|
||||||
|
"pipeline": {
|
||||||
|
"build": {
|
||||||
|
"name": "org.osbuild.debian-filesystem",
|
||||||
|
"options": {
|
||||||
|
"rootfs_type": "ext4",
|
||||||
|
"ostree_integration": True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.apt",
|
||||||
|
"options": {
|
||||||
|
"packages": options.get("packages", []),
|
||||||
|
"release": options.get("release", "trixie"),
|
||||||
|
"arch": options.get("arch", "amd64")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
248
test/test_progress.py
Normal file
248
test/test_progress.py
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test progress reporting for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module tests progress reporting functionality, including:
|
||||||
|
- Progress tracking
|
||||||
|
- Status updates
|
||||||
|
- Error reporting
|
||||||
|
- Debian-specific progress indicators
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestProgressReporting:
|
||||||
|
"""Test cases for progress reporting functionality."""
|
||||||
|
|
||||||
|
def test_progress_initialization(self, work_dir):
|
||||||
|
"""Test progress tracking initialization."""
|
||||||
|
# Initialize progress tracker
|
||||||
|
progress = self._create_progress_tracker()
|
||||||
|
|
||||||
|
assert progress["total_steps"] > 0
|
||||||
|
assert progress["current_step"] == 0
|
||||||
|
assert progress["status"] == "initialized"
|
||||||
|
assert "start_time" in progress
|
||||||
|
|
||||||
|
def test_progress_step_tracking(self, work_dir):
|
||||||
|
"""Test progress step tracking."""
|
||||||
|
# Create progress tracker
|
||||||
|
progress = self._create_progress_tracker()
|
||||||
|
|
||||||
|
# Simulate step progression
|
||||||
|
steps = [
|
||||||
|
"filesystem_setup",
|
||||||
|
"package_installation",
|
||||||
|
"ostree_integration",
|
||||||
|
"bootloader_configuration",
|
||||||
|
"image_generation"
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, step in enumerate(steps):
|
||||||
|
self._update_progress(progress, step, i + 1)
|
||||||
|
|
||||||
|
assert progress["current_step"] == i + 1
|
||||||
|
assert progress["current_operation"] == step
|
||||||
|
assert progress["status"] == "in_progress"
|
||||||
|
|
||||||
|
# Check progress percentage
|
||||||
|
expected_percentage = ((i + 1) / len(steps)) * 100
|
||||||
|
assert abs(progress["percentage"] - expected_percentage) < 0.1
|
||||||
|
|
||||||
|
def test_progress_status_updates(self, work_dir):
|
||||||
|
"""Test progress status updates."""
|
||||||
|
# Create progress tracker
|
||||||
|
progress = self._create_progress_tracker()
|
||||||
|
|
||||||
|
# Test status transitions
|
||||||
|
statuses = ["initialized", "in_progress", "completed", "failed"]
|
||||||
|
|
||||||
|
for status in statuses:
|
||||||
|
self._set_progress_status(progress, status)
|
||||||
|
assert progress["status"] == status
|
||||||
|
|
||||||
|
# Check status-specific properties
|
||||||
|
if status == "completed":
|
||||||
|
assert progress["end_time"] is not None
|
||||||
|
assert progress["percentage"] == 100.0
|
||||||
|
elif status == "failed":
|
||||||
|
assert progress["error"] is not None
|
||||||
|
|
||||||
|
def test_debian_specific_progress_indicators(self, work_dir):
|
||||||
|
"""Test Debian-specific progress indicators."""
|
||||||
|
# Create progress tracker
|
||||||
|
progress = self._create_progress_tracker()
|
||||||
|
|
||||||
|
# Test Debian-specific operations
|
||||||
|
debian_operations = [
|
||||||
|
"apt_update",
|
||||||
|
"package_download",
|
||||||
|
"package_installation",
|
||||||
|
"initramfs_generation",
|
||||||
|
"grub_configuration"
|
||||||
|
]
|
||||||
|
|
||||||
|
for operation in debian_operations:
|
||||||
|
self._add_debian_operation(progress, operation)
|
||||||
|
assert operation in progress["debian_operations"]
|
||||||
|
|
||||||
|
# Test Debian package progress
|
||||||
|
package_progress = self._track_package_progress(progress, ["linux-image-amd64", "systemd", "ostree"])
|
||||||
|
assert package_progress["total_packages"] == 3
|
||||||
|
assert package_progress["installed_packages"] == 0
|
||||||
|
|
||||||
|
def test_error_reporting(self, work_dir):
|
||||||
|
"""Test error reporting in progress tracking."""
|
||||||
|
# Create progress tracker
|
||||||
|
progress = self._create_progress_tracker()
|
||||||
|
|
||||||
|
# Test error reporting
|
||||||
|
error_message = "Package installation failed: network error"
|
||||||
|
self._report_progress_error(progress, error_message)
|
||||||
|
|
||||||
|
assert progress["status"] == "failed"
|
||||||
|
assert progress["error"] == error_message
|
||||||
|
assert progress["error_time"] is not None
|
||||||
|
|
||||||
|
# Test error details
|
||||||
|
error_details = {
|
||||||
|
"operation": "package_installation",
|
||||||
|
"step": 2,
|
||||||
|
"timestamp": time.time()
|
||||||
|
}
|
||||||
|
self._add_error_details(progress, error_details)
|
||||||
|
|
||||||
|
assert "error_details" in progress
|
||||||
|
assert progress["error_details"]["operation"] == "package_installation"
|
||||||
|
|
||||||
|
def test_progress_persistence(self, work_dir):
|
||||||
|
"""Test progress persistence and recovery."""
|
||||||
|
# Create progress tracker
|
||||||
|
progress = self._create_progress_tracker()
|
||||||
|
|
||||||
|
# Update progress
|
||||||
|
self._update_progress(progress, "filesystem_setup", 1)
|
||||||
|
self._update_progress(progress, "package_installation", 2)
|
||||||
|
|
||||||
|
# Save progress
|
||||||
|
progress_file = os.path.join(work_dir, "progress.json")
|
||||||
|
self._save_progress(progress, progress_file)
|
||||||
|
|
||||||
|
# Load progress
|
||||||
|
loaded_progress = self._load_progress(progress_file)
|
||||||
|
|
||||||
|
# Verify persistence
|
||||||
|
assert loaded_progress["current_step"] == 2
|
||||||
|
assert loaded_progress["current_operation"] == "package_installation"
|
||||||
|
assert loaded_progress["percentage"] == 40.0
|
||||||
|
|
||||||
|
def test_progress_cleanup(self, work_dir):
|
||||||
|
"""Test progress cleanup and finalization."""
|
||||||
|
# Create progress tracker
|
||||||
|
progress = self._create_progress_tracker()
|
||||||
|
|
||||||
|
# Complete all steps
|
||||||
|
steps = ["filesystem_setup", "package_installation", "ostree_integration", "bootloader_configuration", "image_generation"]
|
||||||
|
for i, step in enumerate(steps):
|
||||||
|
self._update_progress(progress, step, i + 1)
|
||||||
|
|
||||||
|
# Finalize progress
|
||||||
|
self._finalize_progress(progress)
|
||||||
|
|
||||||
|
assert progress["status"] == "completed"
|
||||||
|
assert progress["end_time"] is not None
|
||||||
|
assert progress["duration"] > 0
|
||||||
|
assert progress["percentage"] == 100.0
|
||||||
|
|
||||||
|
def _create_progress_tracker(self):
|
||||||
|
"""Create a progress tracker instance."""
|
||||||
|
return {
|
||||||
|
"total_steps": 5,
|
||||||
|
"current_step": 0,
|
||||||
|
"current_operation": None,
|
||||||
|
"status": "initialized",
|
||||||
|
"start_time": time.time(),
|
||||||
|
"end_time": None,
|
||||||
|
"percentage": 0.0,
|
||||||
|
"error": None,
|
||||||
|
"error_time": None,
|
||||||
|
"debian_operations": [],
|
||||||
|
"package_progress": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _update_progress(self, progress, operation, step):
|
||||||
|
"""Update progress tracking."""
|
||||||
|
progress["current_step"] = step
|
||||||
|
progress["current_operation"] = operation
|
||||||
|
progress["status"] = "in_progress"
|
||||||
|
progress["percentage"] = (step / progress["total_steps"]) * 100
|
||||||
|
|
||||||
|
def _set_progress_status(self, progress, status):
|
||||||
|
"""Set progress status."""
|
||||||
|
progress["status"] = status
|
||||||
|
|
||||||
|
if status == "completed":
|
||||||
|
progress["end_time"] = time.time()
|
||||||
|
progress["percentage"] = 100.0
|
||||||
|
elif status == "failed":
|
||||||
|
progress["error_time"] = time.time()
|
||||||
|
|
||||||
|
def _add_debian_operation(self, progress, operation):
|
||||||
|
"""Add Debian-specific operation to progress."""
|
||||||
|
if "debian_operations" not in progress:
|
||||||
|
progress["debian_operations"] = []
|
||||||
|
progress["debian_operations"].append(operation)
|
||||||
|
|
||||||
|
def _track_package_progress(self, progress, packages):
|
||||||
|
"""Track package installation progress."""
|
||||||
|
package_progress = {
|
||||||
|
"total_packages": len(packages),
|
||||||
|
"installed_packages": 0,
|
||||||
|
"failed_packages": [],
|
||||||
|
"current_package": None
|
||||||
|
}
|
||||||
|
progress["package_progress"] = package_progress
|
||||||
|
return package_progress
|
||||||
|
|
||||||
|
def _report_progress_error(self, progress, error_message):
|
||||||
|
"""Report progress error."""
|
||||||
|
progress["status"] = "failed"
|
||||||
|
progress["error"] = error_message
|
||||||
|
progress["error_time"] = time.time()
|
||||||
|
|
||||||
|
def _add_error_details(self, progress, error_details):
|
||||||
|
"""Add detailed error information."""
|
||||||
|
progress["error_details"] = error_details
|
||||||
|
|
||||||
|
def _save_progress(self, progress, file_path):
|
||||||
|
"""Save progress to file."""
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
json.dump(progress, f, indent=2)
|
||||||
|
|
||||||
|
def _load_progress(self, file_path):
|
||||||
|
"""Load progress from file."""
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def _finalize_progress(self, progress):
|
||||||
|
"""Finalize progress tracking."""
|
||||||
|
progress["status"] = "completed"
|
||||||
|
progress["end_time"] = time.time()
|
||||||
|
progress["duration"] = progress["end_time"] - progress["start_time"]
|
||||||
|
progress["percentage"] = 100.0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
364
test/test_pylint.py
Normal file
364
test/test_pylint.py
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test pylint compliance for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module tests code quality and pylint compliance,
|
||||||
|
including:
|
||||||
|
- Code style validation
|
||||||
|
- Pylint score checking
|
||||||
|
- Debian-specific code standards
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPylintCompliance:
|
||||||
|
"""Test cases for pylint compliance."""
|
||||||
|
|
||||||
|
def test_pylint_installation(self, work_dir):
|
||||||
|
"""Test that pylint is available."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["pylint", "--version"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, "pylint is not properly installed"
|
||||||
|
logger.info("pylint is available")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pytest.skip("pylint not installed")
|
||||||
|
|
||||||
|
def test_pylint_basic_usage(self, work_dir):
|
||||||
|
"""Test basic pylint functionality."""
|
||||||
|
# Create a simple test file
|
||||||
|
test_file = os.path.join(work_dir, "test_pylint.py")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test file for pylint validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_function():
|
||||||
|
"""Test function for pylint."""
|
||||||
|
return "test"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(test_function())
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Run pylint on the test file
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["pylint", test_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pylint should run without errors
|
||||||
|
assert result.returncode in [0, 1], f"pylint failed with return code {result.returncode}"
|
||||||
|
logger.info("pylint basic functionality test passed")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("pylint timed out")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"pylint test failed: {e}")
|
||||||
|
|
||||||
|
def test_debian_specific_code_standards(self, work_dir):
|
||||||
|
"""Test Debian-specific code standards."""
|
||||||
|
# Create a test file with Debian-specific patterns
|
||||||
|
test_file = os.path.join(work_dir, "debian_test.py")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Debian-specific test file for pylint validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DebianBootcBuilder:
|
||||||
|
"""Debian bootc image builder class."""
|
||||||
|
|
||||||
|
def __init__(self, work_dir: str):
|
||||||
|
"""Initialize the builder."""
|
||||||
|
self.work_dir = work_dir
|
||||||
|
self.packages: List[str] = []
|
||||||
|
self.release = "trixie"
|
||||||
|
self.arch = "amd64"
|
||||||
|
|
||||||
|
def add_package(self, package: str) -> None:
|
||||||
|
"""Add a package to the installation list."""
|
||||||
|
if package not in self.packages:
|
||||||
|
self.packages.append(package)
|
||||||
|
logger.info(f"Added package: {package}")
|
||||||
|
|
||||||
|
def set_release(self, release: str) -> None:
|
||||||
|
"""Set the Debian release."""
|
||||||
|
valid_releases = ["trixie", "bookworm", "bullseye"]
|
||||||
|
if release in valid_releases:
|
||||||
|
self.release = release
|
||||||
|
logger.info(f"Set release to: {release}")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid release: {release}")
|
||||||
|
|
||||||
|
def build_image(self) -> Dict[str, Any]:
|
||||||
|
"""Build the Debian image."""
|
||||||
|
logger.info("Starting Debian image build")
|
||||||
|
|
||||||
|
# Validate configuration
|
||||||
|
if not self.packages:
|
||||||
|
raise ValueError("No packages specified")
|
||||||
|
|
||||||
|
# Build process would go here
|
||||||
|
result = {
|
||||||
|
"status": "success",
|
||||||
|
"packages": self.packages,
|
||||||
|
"release": self.release,
|
||||||
|
"arch": self.arch
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Debian image build completed")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Main function."""
|
||||||
|
builder = DebianBootcBuilder("/tmp/test")
|
||||||
|
builder.add_package("linux-image-amd64")
|
||||||
|
builder.add_package("systemd")
|
||||||
|
builder.set_release("trixie")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = builder.build_image()
|
||||||
|
print(f"Build result: {result}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Build failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Run pylint with Debian-specific configuration
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["pylint", "--disable=C0114,C0116", test_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check pylint output for Debian-specific patterns
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
|
||||||
|
# Should not have critical errors
|
||||||
|
assert "E0001" not in output, "Critical pylint errors found"
|
||||||
|
|
||||||
|
# Check for specific Debian patterns
|
||||||
|
assert "debian" in output.lower() or "bootc" in output.lower(), \
|
||||||
|
"Debian-specific content not detected"
|
||||||
|
|
||||||
|
logger.info("Debian-specific code standards test passed")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("pylint Debian test timed out")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"pylint Debian test failed: {e}")
|
||||||
|
|
||||||
|
def test_pylint_score_threshold(self, work_dir):
|
||||||
|
"""Test that pylint score meets minimum threshold."""
|
||||||
|
# Create a high-quality test file
|
||||||
|
test_file = os.path.join(work_dir, "high_quality_test.py")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
High-quality test file for pylint scoring.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HighQualityClass:
|
||||||
|
"""A high-quality class for testing."""
|
||||||
|
|
||||||
|
def __init__(self, name: str):
|
||||||
|
"""Initialize the class."""
|
||||||
|
self.name = name
|
||||||
|
self.data: List[str] = []
|
||||||
|
|
||||||
|
def add_item(self, item: str) -> None:
|
||||||
|
"""Add an item to the data list."""
|
||||||
|
if item and item not in self.data:
|
||||||
|
self.data.append(item)
|
||||||
|
logger.info(f"Added item: {item}")
|
||||||
|
|
||||||
|
def get_items(self) -> List[str]:
|
||||||
|
"""Get all items from the data list."""
|
||||||
|
return self.data.copy()
|
||||||
|
|
||||||
|
def clear_items(self) -> None:
|
||||||
|
"""Clear all items from the data list."""
|
||||||
|
self.data.clear()
|
||||||
|
logger.info("Cleared all items")
|
||||||
|
|
||||||
|
|
||||||
|
def high_quality_function(param: str) -> str:
|
||||||
|
"""A high-quality function for testing."""
|
||||||
|
if not param:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
result = param.upper()
|
||||||
|
logger.info(f"Processed parameter: {param} -> {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Main function."""
|
||||||
|
obj = HighQualityClass("test")
|
||||||
|
obj.add_item("item1")
|
||||||
|
obj.add_item("item2")
|
||||||
|
|
||||||
|
items = obj.get_items()
|
||||||
|
print(f"Items: {items}")
|
||||||
|
|
||||||
|
result = high_quality_function("hello")
|
||||||
|
print(f"Function result: {result}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Run pylint and check score
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["pylint", "--score=yes", test_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
|
||||||
|
# Extract score from output
|
||||||
|
score_line = [line for line in output.split('\n') if 'Your code has been rated at' in line]
|
||||||
|
|
||||||
|
if score_line:
|
||||||
|
score_text = score_line[0]
|
||||||
|
# Extract numeric score
|
||||||
|
import re
|
||||||
|
score_match = re.search(r'(\d+\.\d+)', score_text)
|
||||||
|
if score_match:
|
||||||
|
score = float(score_match.group(1))
|
||||||
|
|
||||||
|
# Check if score meets minimum threshold (8.0)
|
||||||
|
assert score >= 8.0, f"Pylint score {score} is below minimum threshold 8.0"
|
||||||
|
logger.info(f"Pylint score: {score} (meets minimum threshold)")
|
||||||
|
else:
|
||||||
|
pytest.fail("Could not extract pylint score")
|
||||||
|
else:
|
||||||
|
pytest.fail("Could not find pylint score in output")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("pylint score test timed out")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"pylint score test failed: {e}")
|
||||||
|
|
||||||
|
def test_pylint_configuration(self, work_dir):
|
||||||
|
"""Test pylint configuration and custom rules."""
|
||||||
|
# Create a pylint configuration file
|
||||||
|
pylintrc = os.path.join(work_dir, ".pylintrc")
|
||||||
|
with open(pylintrc, 'w') as f:
|
||||||
|
f.write('''[MASTER]
|
||||||
|
# Python code to execute before analysis
|
||||||
|
init-hook='import sys; sys.path.append(".")'
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
# Set the output format
|
||||||
|
output-format=text
|
||||||
|
|
||||||
|
# Include a brief explanation of each error
|
||||||
|
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
|
||||||
|
|
||||||
|
# Include a brief explanation of each error
|
||||||
|
include-naming-hint=yes
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
# Disable specific warnings
|
||||||
|
disable=C0114,C0116,R0903
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
# Maximum number of characters on a single line
|
||||||
|
max-line-length=120
|
||||||
|
|
||||||
|
# Maximum number of lines in a module
|
||||||
|
max-module-lines=1000
|
||||||
|
|
||||||
|
[SIMILARITIES]
|
||||||
|
# Minimum lines number of a similarity
|
||||||
|
min-similarity-lines=4
|
||||||
|
|
||||||
|
# Ignore imports when computing similarities
|
||||||
|
ignore-imports=yes
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Create a test file
|
||||||
|
test_file = os.path.join(work_dir, "config_test.py")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test file for pylint configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_function():
|
||||||
|
return "test"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(test_function())
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Run pylint with custom configuration
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["pylint", "--rcfile", pylintrc, test_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should run without configuration errors
|
||||||
|
assert result.returncode in [0, 1], f"pylint with custom config failed: {result.returncode}"
|
||||||
|
logger.info("Pylint configuration test passed")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pytest.fail("pylint configuration test timed out")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"pylint configuration test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
473
test/testcases.py
Normal file
473
test/testcases.py
Normal file
|
|
@ -0,0 +1,473 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test case definitions for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module defines test cases and test data for various scenarios,
|
||||||
|
including:
|
||||||
|
- Basic functionality tests
|
||||||
|
- Edge case tests
|
||||||
|
- Error condition tests
|
||||||
|
- Debian-specific test cases
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaseDefinitions:
|
||||||
|
"""Test case definitions for deb-bootc-image-builder."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_basic_functionality_tests() -> List[Dict[str, Any]]:
|
||||||
|
"""Get basic functionality test cases."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "basic_debian_image_build",
|
||||||
|
"description": "Test basic Debian image building functionality",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-amd64", "systemd", "ostree"],
|
||||||
|
"expected_result": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debian_with_custom_packages",
|
||||||
|
"description": "Test Debian image building with custom packages",
|
||||||
|
"container": "debian:bookworm",
|
||||||
|
"release": "bookworm",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools",
|
||||||
|
"sudo",
|
||||||
|
"network-manager"
|
||||||
|
],
|
||||||
|
"expected_result": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debian_arm64_build",
|
||||||
|
"description": "Test Debian ARM64 image building",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "arm64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-arm64", "systemd", "ostree"],
|
||||||
|
"expected_result": "success"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_edge_case_tests() -> List[Dict[str, Any]]:
|
||||||
|
"""Get edge case test cases."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "empty_package_list",
|
||||||
|
"description": "Test building with empty package list",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": [],
|
||||||
|
"expected_result": "error",
|
||||||
|
"expected_error": "No packages specified"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "invalid_release",
|
||||||
|
"description": "Test building with invalid Debian release",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "invalid-release",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-amd64"],
|
||||||
|
"expected_result": "error",
|
||||||
|
"expected_error": "Invalid Debian release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "invalid_architecture",
|
||||||
|
"description": "Test building with invalid architecture",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "invalid-arch",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-amd64"],
|
||||||
|
"expected_result": "error",
|
||||||
|
"expected_error": "Invalid architecture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "very_long_package_list",
|
||||||
|
"description": "Test building with very long package list",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": [f"package-{i}" for i in range(1000)],
|
||||||
|
"expected_result": "success"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_error_condition_tests() -> List[Dict[str, Any]]:
|
||||||
|
"""Get error condition test cases."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "invalid_container_image",
|
||||||
|
"description": "Test building with invalid container image",
|
||||||
|
"container": "invalid:image",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-amd64"],
|
||||||
|
"expected_result": "error",
|
||||||
|
"expected_error": "Invalid container image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "network_failure",
|
||||||
|
"description": "Test building with network failure simulation",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-amd64"],
|
||||||
|
"expected_result": "error",
|
||||||
|
"expected_error": "Network error",
|
||||||
|
"simulate_network_failure": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "disk_space_exhaustion",
|
||||||
|
"description": "Test building with disk space exhaustion",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-amd64"],
|
||||||
|
"expected_result": "error",
|
||||||
|
"expected_error": "Disk space exhausted",
|
||||||
|
"simulate_disk_full": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_debian_specific_tests() -> List[Dict[str, Any]]:
|
||||||
|
"""Get Debian-specific test cases."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "debian_trixie_minimal",
|
||||||
|
"description": "Test Debian Trixie minimal image",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools"
|
||||||
|
],
|
||||||
|
"debian_specific": {
|
||||||
|
"initramfs_tools": True,
|
||||||
|
"grub_efi": True,
|
||||||
|
"ostree_integration": True
|
||||||
|
},
|
||||||
|
"expected_result": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debian_bookworm_desktop",
|
||||||
|
"description": "Test Debian Bookworm desktop image",
|
||||||
|
"container": "debian:bookworm",
|
||||||
|
"release": "bookworm",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools",
|
||||||
|
"task-desktop",
|
||||||
|
"xorg",
|
||||||
|
"lightdm"
|
||||||
|
],
|
||||||
|
"debian_specific": {
|
||||||
|
"initramfs_tools": True,
|
||||||
|
"grub_efi": True,
|
||||||
|
"ostree_integration": True,
|
||||||
|
"desktop_environment": True
|
||||||
|
},
|
||||||
|
"expected_result": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debian_bullseye_server",
|
||||||
|
"description": "Test Debian Bullseye server image",
|
||||||
|
"container": "debian:bullseye",
|
||||||
|
"release": "bullseye",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools",
|
||||||
|
"openssh-server",
|
||||||
|
"nginx",
|
||||||
|
"postgresql"
|
||||||
|
],
|
||||||
|
"debian_specific": {
|
||||||
|
"initramfs_tools": True,
|
||||||
|
"grub_efi": True,
|
||||||
|
"ostree_integration": True,
|
||||||
|
"server_services": True
|
||||||
|
},
|
||||||
|
"expected_result": "success"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_performance_tests() -> List[Dict[str, Any]]:
|
||||||
|
"""Get performance test cases."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "small_image_build_time",
|
||||||
|
"description": "Test build time for small image",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-amd64", "systemd"],
|
||||||
|
"performance_requirements": {
|
||||||
|
"max_build_time": 300, # 5 minutes
|
||||||
|
"max_image_size": 1024 # 1GB
|
||||||
|
},
|
||||||
|
"expected_result": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "large_image_build_time",
|
||||||
|
"description": "Test build time for large image",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": [f"package-{i}" for i in range(500)],
|
||||||
|
"performance_requirements": {
|
||||||
|
"max_build_time": 1800, # 30 minutes
|
||||||
|
"max_image_size": 10240 # 10GB
|
||||||
|
},
|
||||||
|
"expected_result": "success"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_integration_tests() -> List[Dict[str, Any]]:
|
||||||
|
"""Get integration test cases."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "full_pipeline_test",
|
||||||
|
"description": "Test complete image building pipeline",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"systemd",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools"
|
||||||
|
],
|
||||||
|
"pipeline_stages": [
|
||||||
|
"filesystem_setup",
|
||||||
|
"package_installation",
|
||||||
|
"ostree_integration",
|
||||||
|
"bootloader_configuration",
|
||||||
|
"image_generation"
|
||||||
|
],
|
||||||
|
"expected_result": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cross_architecture_test",
|
||||||
|
"description": "Test cross-architecture building",
|
||||||
|
"container": "debian:trixie",
|
||||||
|
"release": "trixie",
|
||||||
|
"architectures": ["amd64", "arm64"],
|
||||||
|
"image_type": "qcow2",
|
||||||
|
"packages": ["linux-image-amd64", "systemd", "ostree"],
|
||||||
|
"expected_result": "success"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestDataGenerator:
|
||||||
|
"""Generate test data for various test scenarios."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_manifest(test_case: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Generate a manifest from a test case."""
|
||||||
|
manifest = {
|
||||||
|
"pipeline": {
|
||||||
|
"build": {
|
||||||
|
"name": "org.osbuild.debian-filesystem",
|
||||||
|
"options": {
|
||||||
|
"rootfs_type": "ext4",
|
||||||
|
"ostree_integration": True,
|
||||||
|
"home_symlink": True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "org.osbuild.apt",
|
||||||
|
"options": {
|
||||||
|
"packages": test_case.get("packages", []),
|
||||||
|
"release": test_case.get("release", "trixie"),
|
||||||
|
"arch": test_case.get("arch", "amd64")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add Debian-specific stages if specified
|
||||||
|
if test_case.get("debian_specific", {}).get("grub_efi"):
|
||||||
|
manifest["pipeline"]["stages"].append({
|
||||||
|
"name": "org.osbuild.debian-grub",
|
||||||
|
"options": {
|
||||||
|
"uefi": True,
|
||||||
|
"secure_boot": False,
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if test_case.get("debian_specific", {}).get("initramfs_tools"):
|
||||||
|
manifest["pipeline"]["stages"].append({
|
||||||
|
"name": "org.osbuild.debian-kernel",
|
||||||
|
"options": {
|
||||||
|
"kernel_package": f"linux-image-{test_case.get('arch', 'amd64')}",
|
||||||
|
"initramfs_tools": True,
|
||||||
|
"ostree_integration": True
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_test_environment(test_case: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Generate test environment configuration."""
|
||||||
|
return {
|
||||||
|
"work_dir": "/tmp/test-env",
|
||||||
|
"output_dir": "/tmp/test-output",
|
||||||
|
"cache_dir": "/tmp/test-cache",
|
||||||
|
"temp_dir": "/tmp/test-temp",
|
||||||
|
"network_enabled": not test_case.get("simulate_network_failure", False),
|
||||||
|
"disk_space_available": not test_case.get("simulate_disk_full", False)
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_expected_output(test_case: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Generate expected output for a test case."""
|
||||||
|
expected_output = {
|
||||||
|
"status": test_case.get("expected_result", "success"),
|
||||||
|
"image_type": test_case.get("image_type", "qcow2"),
|
||||||
|
"architecture": test_case.get("arch", "amd64"),
|
||||||
|
"release": test_case.get("release", "trixie")
|
||||||
|
}
|
||||||
|
|
||||||
|
if test_case.get("expected_result") == "success":
|
||||||
|
expected_output["image_path"] = f"/tmp/test-output/debian-{test_case.get('release')}-{test_case.get('arch')}.{test_case.get('image_type')}"
|
||||||
|
expected_output["build_log"] = "Build completed successfully"
|
||||||
|
else:
|
||||||
|
expected_output["error"] = test_case.get("expected_error", "Unknown error")
|
||||||
|
expected_output["build_log"] = f"Build failed: {test_case.get('expected_error', 'Unknown error')}"
|
||||||
|
|
||||||
|
return expected_output
|
||||||
|
|
||||||
|
|
||||||
|
def load_test_cases_from_file(file_path: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Load test cases from a file."""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
if file_path.endswith('.json'):
|
||||||
|
return json.load(f)
|
||||||
|
elif file_path.endswith('.yaml') or file_path.endswith('.yml'):
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported file format: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load test cases from {file_path}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def save_test_cases_to_file(test_cases: List[Dict[str, Any]], file_path: str) -> bool:
|
||||||
|
"""Save test cases to a file."""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
if file_path.endswith('.json'):
|
||||||
|
json.dump(test_cases, f, indent=2)
|
||||||
|
elif file_path.endswith('.yaml') or file_path.endswith('.yml'):
|
||||||
|
yaml.dump(test_cases, f, default_flow_style=False)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported file format: {file_path}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save test cases to {file_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def validate_test_case(test_case: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Validate a test case definition."""
|
||||||
|
validation_result = {
|
||||||
|
"valid": True,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
required_fields = ["name", "description", "container", "release", "arch", "image_type", "packages"]
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in test_case:
|
||||||
|
validation_result["valid"] = False
|
||||||
|
validation_result["errors"].append(f"Missing required field: {field}")
|
||||||
|
|
||||||
|
# Check field types
|
||||||
|
if "packages" in test_case and not isinstance(test_case["packages"], list):
|
||||||
|
validation_result["valid"] = False
|
||||||
|
validation_result["errors"].append("Packages field must be a list")
|
||||||
|
|
||||||
|
if "arch" in test_case and test_case["arch"] not in ["amd64", "arm64", "i386"]:
|
||||||
|
validation_result["warnings"].append(f"Unsupported architecture: {test_case['arch']}")
|
||||||
|
|
||||||
|
if "release" in test_case and test_case["release"] not in ["trixie", "bookworm", "bullseye"]:
|
||||||
|
validation_result["warnings"].append(f"Unsupported Debian release: {test_case['release']}")
|
||||||
|
|
||||||
|
return validation_result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test the test case definitions
|
||||||
|
test_cases = TestCaseDefinitions.get_basic_functionality_tests()
|
||||||
|
print(f"Generated {len(test_cases)} basic functionality test cases")
|
||||||
|
|
||||||
|
for test_case in test_cases:
|
||||||
|
manifest = TestDataGenerator.generate_manifest(test_case)
|
||||||
|
print(f"Generated manifest for {test_case['name']}: {len(manifest['pipeline']['stages'])} stages")
|
||||||
|
|
||||||
|
validation = validate_test_case(test_case)
|
||||||
|
print(f"Validation for {test_case['name']}: {'Valid' if validation['valid'] else 'Invalid'}")
|
||||||
|
if validation['errors']:
|
||||||
|
print(f" Errors: {validation['errors']}")
|
||||||
|
if validation['warnings']:
|
||||||
|
print(f" Warnings: {validation['warnings']}")
|
||||||
289
test/testutil.py
Normal file
289
test/testutil.py
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test utilities for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module provides common utilities for testing, including:
|
||||||
|
- Test data generation
|
||||||
|
- Mock objects
|
||||||
|
- Helper functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDataGenerator:
|
||||||
|
"""Generate test data for deb-bootc-image-builder tests."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_debian_package_list() -> List[str]:
|
||||||
|
"""Create a list of Debian packages for testing."""
|
||||||
|
return [
|
||||||
|
"linux-image-amd64",
|
||||||
|
"linux-headers-amd64",
|
||||||
|
"systemd",
|
||||||
|
"systemd-sysv",
|
||||||
|
"dbus",
|
||||||
|
"ostree",
|
||||||
|
"grub-efi-amd64",
|
||||||
|
"initramfs-tools",
|
||||||
|
"util-linux",
|
||||||
|
"parted",
|
||||||
|
"e2fsprogs",
|
||||||
|
"dosfstools",
|
||||||
|
"efibootmgr",
|
||||||
|
"sudo",
|
||||||
|
"network-manager",
|
||||||
|
"curl",
|
||||||
|
"wget",
|
||||||
|
"nano",
|
||||||
|
"vim-tiny"
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_debian_repository_config() -> Dict[str, Any]:
|
||||||
|
"""Create Debian repository configuration for testing."""
|
||||||
|
return {
|
||||||
|
"release": "trixie",
|
||||||
|
"arch": "amd64",
|
||||||
|
"repos": [
|
||||||
|
{
|
||||||
|
"name": "debian",
|
||||||
|
"baseurls": ["http://deb.debian.org/debian"],
|
||||||
|
"enabled": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debian-security",
|
||||||
|
"baseurls": ["http://deb.debian.org/debian-security"],
|
||||||
|
"enabled": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_ostree_config() -> Dict[str, Any]:
|
||||||
|
"""Create OSTree configuration for testing."""
|
||||||
|
return {
|
||||||
|
"mode": "bare-user-only",
|
||||||
|
"repo": "/var/lib/ostree/repo",
|
||||||
|
"bootable": True,
|
||||||
|
"deployment": {
|
||||||
|
"osname": "debian",
|
||||||
|
"ref": "debian/trixie/amd64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_grub_config() -> Dict[str, Any]:
|
||||||
|
"""Create GRUB configuration for testing."""
|
||||||
|
return {
|
||||||
|
"uefi": True,
|
||||||
|
"secure_boot": False,
|
||||||
|
"timeout": 5,
|
||||||
|
"default_entry": 0,
|
||||||
|
"kernel_path": "/boot/vmlinuz",
|
||||||
|
"initramfs_path": "/boot/initrd.img"
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_filesystem_config() -> Dict[str, Any]:
|
||||||
|
"""Create filesystem configuration for testing."""
|
||||||
|
return {
|
||||||
|
"rootfs_type": "ext4",
|
||||||
|
"ostree_integration": True,
|
||||||
|
"home_symlink": True,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "debian-user",
|
||||||
|
"password": "debian",
|
||||||
|
"groups": ["sudo", "users"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"permissions": {
|
||||||
|
"/etc/ostree": "755",
|
||||||
|
"/var/lib/ostree": "755"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MockContainerImage:
|
||||||
|
"""Mock container image for testing."""
|
||||||
|
|
||||||
|
def __init__(self, labels: Optional[Dict[str, str]] = None):
|
||||||
|
"""Initialize mock container image."""
|
||||||
|
self.labels = labels or {
|
||||||
|
"com.debian.bootc": "true",
|
||||||
|
"ostree.bootable": "true",
|
||||||
|
"org.debian.version": "13",
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
self.ref = "debian:trixie"
|
||||||
|
self.arch = "amd64"
|
||||||
|
self.os = "linux"
|
||||||
|
|
||||||
|
def get_labels(self) -> Dict[str, str]:
|
||||||
|
"""Get image labels."""
|
||||||
|
return self.labels
|
||||||
|
|
||||||
|
def get_ref(self) -> str:
|
||||||
|
"""Get image reference."""
|
||||||
|
return self.ref
|
||||||
|
|
||||||
|
def get_arch(self) -> str:
|
||||||
|
"""Get image architecture."""
|
||||||
|
return self.arch
|
||||||
|
|
||||||
|
def get_os(self) -> str:
|
||||||
|
"""Get image operating system."""
|
||||||
|
return self.os
|
||||||
|
|
||||||
|
|
||||||
|
class MockOSTreeRepo:
|
||||||
|
"""Mock OSTree repository for testing."""
|
||||||
|
|
||||||
|
def __init__(self, path: str):
|
||||||
|
"""Initialize mock OSTree repository."""
|
||||||
|
self.path = path
|
||||||
|
self.refs = ["debian/trixie/amd64"]
|
||||||
|
self.deployments = []
|
||||||
|
|
||||||
|
def list_refs(self) -> List[str]:
|
||||||
|
"""List repository references."""
|
||||||
|
return self.refs
|
||||||
|
|
||||||
|
def list_deployments(self) -> List[Dict[str, Any]]:
|
||||||
|
"""List repository deployments."""
|
||||||
|
return self.deployments
|
||||||
|
|
||||||
|
def get_deployment_info(self, ref: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get deployment information."""
|
||||||
|
if ref in self.refs:
|
||||||
|
return {
|
||||||
|
"ref": ref,
|
||||||
|
"osname": "debian",
|
||||||
|
"bootable": True,
|
||||||
|
"version": "13"
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvironment:
|
||||||
|
"""Test environment setup and teardown."""
|
||||||
|
|
||||||
|
def __init__(self, work_dir: str):
|
||||||
|
"""Initialize test environment."""
|
||||||
|
self.work_dir = work_dir
|
||||||
|
self.original_cwd = os.getcwd()
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""Set up test environment."""
|
||||||
|
os.chdir(self.work_dir)
|
||||||
|
|
||||||
|
# Create basic directory structure
|
||||||
|
dirs = [
|
||||||
|
"etc", "var", "home", "boot", "usr",
|
||||||
|
"usr/bin", "usr/lib", "usr/sbin",
|
||||||
|
"var/lib", "var/lib/ostree", "var/home"
|
||||||
|
]
|
||||||
|
|
||||||
|
for dir_path in dirs:
|
||||||
|
full_path = os.path.join(self.work_dir, dir_path)
|
||||||
|
os.makedirs(full_path, exist_ok=True)
|
||||||
|
|
||||||
|
# Create /home -> /var/home symlink
|
||||||
|
var_home = os.path.join(self.work_dir, "var", "home")
|
||||||
|
home_link = os.path.join(self.work_dir, "home")
|
||||||
|
if os.path.exists(home_link):
|
||||||
|
os.remove(home_link)
|
||||||
|
os.symlink(var_home, home_link)
|
||||||
|
|
||||||
|
logger.info(f"Test environment set up in {self.work_dir}")
|
||||||
|
|
||||||
|
def teardown(self):
|
||||||
|
"""Tear down test environment."""
|
||||||
|
os.chdir(self.original_cwd)
|
||||||
|
logger.info("Test environment torn down")
|
||||||
|
|
||||||
|
def create_test_file(self, path: str, content: str = ""):
|
||||||
|
"""Create a test file with specified content."""
|
||||||
|
full_path = os.path.join(self.work_dir, path.lstrip("/"))
|
||||||
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||||
|
|
||||||
|
with open(full_path, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return full_path
|
||||||
|
|
||||||
|
def create_test_directory(self, path: str):
|
||||||
|
"""Create a test directory."""
|
||||||
|
full_path = os.path.join(self.work_dir, path.lstrip("/"))
|
||||||
|
os.makedirs(full_path, exist_ok=True)
|
||||||
|
return full_path
|
||||||
|
|
||||||
|
|
||||||
|
def create_temp_manifest(manifest_data: Dict[str, Any]) -> str:
|
||||||
|
"""Create a temporary manifest file for testing."""
|
||||||
|
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
json.dump(manifest_data, temp_file, indent=2)
|
||||||
|
temp_file.close()
|
||||||
|
return temp_file.name
|
||||||
|
except Exception as e:
|
||||||
|
temp_file.close()
|
||||||
|
os.unlink(temp_file.name)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_temp_files(*file_paths: str):
|
||||||
|
"""Clean up temporary files."""
|
||||||
|
for file_path in file_paths:
|
||||||
|
try:
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.unlink(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to clean up {file_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_filesystem_structure(work_dir: str, expected_dirs: List[str]):
|
||||||
|
"""Assert that expected filesystem structure exists."""
|
||||||
|
for expected_dir in expected_dirs:
|
||||||
|
full_path = os.path.join(work_dir, expected_dir.lstrip("/"))
|
||||||
|
assert os.path.exists(full_path), f"Directory {expected_dir} not found"
|
||||||
|
assert os.path.isdir(full_path), f"{expected_dir} is not a directory"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_file_contents(file_path: str, expected_content: str):
|
||||||
|
"""Assert that file contains expected content."""
|
||||||
|
assert os.path.exists(file_path), f"File {file_path} not found"
|
||||||
|
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
actual_content = f.read()
|
||||||
|
|
||||||
|
assert actual_content == expected_content, \
|
||||||
|
f"File content mismatch in {file_path}"
|
||||||
|
|
||||||
|
|
||||||
|
def create_mock_context():
|
||||||
|
"""Create a mock osbuild context for testing."""
|
||||||
|
context = Mock()
|
||||||
|
context.root = "/tmp/mock-root"
|
||||||
|
|
||||||
|
def mock_run(cmd):
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = b"mock output"
|
||||||
|
mock_result.stderr = b""
|
||||||
|
return mock_result
|
||||||
|
|
||||||
|
context.run = mock_run
|
||||||
|
return context
|
||||||
274
test/testutil_test.py
Normal file
274
test/testutil_test.py
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test the test utilities for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module tests the test utility functions and classes,
|
||||||
|
including:
|
||||||
|
- Test data generation
|
||||||
|
- Mock objects
|
||||||
|
- Helper functions
|
||||||
|
- Debian-specific utilities
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTestUtilities:
|
||||||
|
"""Test cases for test utility functions and classes."""
|
||||||
|
|
||||||
|
def test_test_data_generator(self, work_dir):
|
||||||
|
"""Test the TestDataGenerator class."""
|
||||||
|
from testutil import TestDataGenerator
|
||||||
|
|
||||||
|
# Test Debian package list generation
|
||||||
|
packages = TestDataGenerator.create_debian_package_list()
|
||||||
|
assert isinstance(packages, list)
|
||||||
|
assert len(packages) > 0
|
||||||
|
|
||||||
|
# Check for essential Debian packages
|
||||||
|
essential_packages = ["linux-image-amd64", "systemd", "ostree"]
|
||||||
|
for pkg in essential_packages:
|
||||||
|
assert pkg in packages, f"Essential package {pkg} missing"
|
||||||
|
|
||||||
|
# Test repository configuration
|
||||||
|
repo_config = TestDataGenerator.create_debian_repository_config()
|
||||||
|
assert "release" in repo_config
|
||||||
|
assert "arch" in repo_config
|
||||||
|
assert "repos" in repo_config
|
||||||
|
assert repo_config["release"] == "trixie"
|
||||||
|
assert repo_config["arch"] == "amd64"
|
||||||
|
|
||||||
|
# Test OSTree configuration
|
||||||
|
ostree_config = TestDataGenerator.create_ostree_config()
|
||||||
|
assert "mode" in ostree_config
|
||||||
|
assert "repo" in ostree_config
|
||||||
|
assert ostree_config["mode"] == "bare-user-only"
|
||||||
|
|
||||||
|
# Test GRUB configuration
|
||||||
|
grub_config = TestDataGenerator.create_grub_config()
|
||||||
|
assert "uefi" in grub_config
|
||||||
|
assert "timeout" in grub_config
|
||||||
|
assert grub_config["uefi"] is True
|
||||||
|
|
||||||
|
def test_mock_container_image(self, work_dir):
|
||||||
|
"""Test the MockContainerImage class."""
|
||||||
|
from testutil import MockContainerImage
|
||||||
|
|
||||||
|
# Test default labels
|
||||||
|
image = MockContainerImage()
|
||||||
|
labels = image.get_labels()
|
||||||
|
|
||||||
|
assert "com.debian.bootc" in labels
|
||||||
|
assert "ostree.bootable" in labels
|
||||||
|
assert labels["com.debian.bootc"] == "true"
|
||||||
|
assert labels["ostree.bootable"] == "true"
|
||||||
|
|
||||||
|
# Test custom labels
|
||||||
|
custom_labels = {
|
||||||
|
"com.debian.bootc": "true",
|
||||||
|
"version": "2.0",
|
||||||
|
"custom.label": "value"
|
||||||
|
}
|
||||||
|
image = MockContainerImage(custom_labels)
|
||||||
|
|
||||||
|
assert image.get_labels() == custom_labels
|
||||||
|
assert image.get_ref() == "debian:trixie"
|
||||||
|
assert image.get_arch() == "amd64"
|
||||||
|
assert image.get_os() == "linux"
|
||||||
|
|
||||||
|
def test_mock_ostree_repo(self, work_dir):
|
||||||
|
"""Test the MockOSTreeRepo class."""
|
||||||
|
from testutil import MockOSTreeRepo
|
||||||
|
|
||||||
|
# Test repository creation
|
||||||
|
repo = MockOSTreeRepo("/tmp/test-repo")
|
||||||
|
|
||||||
|
assert repo.path == "/tmp/test-repo"
|
||||||
|
assert len(repo.list_refs()) > 0
|
||||||
|
assert "debian/trixie/amd64" in repo.list_refs()
|
||||||
|
|
||||||
|
# Test deployment info
|
||||||
|
deployment_info = repo.get_deployment_info("debian/trixie/amd64")
|
||||||
|
assert deployment_info is not None
|
||||||
|
assert deployment_info["ref"] == "debian/trixie/amd64"
|
||||||
|
assert deployment_info["osname"] == "debian"
|
||||||
|
assert deployment_info["bootable"] is True
|
||||||
|
|
||||||
|
# Test non-existent deployment
|
||||||
|
non_existent = repo.get_deployment_info("non-existent")
|
||||||
|
assert non_existent is None
|
||||||
|
|
||||||
|
def test_test_environment(self, work_dir):
|
||||||
|
"""Test the TestEnvironment class."""
|
||||||
|
from testutil import TestEnvironment
|
||||||
|
|
||||||
|
# Create test environment
|
||||||
|
env = TestEnvironment(work_dir)
|
||||||
|
|
||||||
|
# Test setup
|
||||||
|
env.setup()
|
||||||
|
|
||||||
|
# Check that directories were created
|
||||||
|
expected_dirs = ["etc", "var", "home", "boot", "usr", "var/lib", "var/lib/ostree", "var/home"]
|
||||||
|
for expected_dir in expected_dirs:
|
||||||
|
full_path = os.path.join(work_dir, expected_dir)
|
||||||
|
assert os.path.exists(full_path), f"Directory {expected_dir} not created"
|
||||||
|
|
||||||
|
# Check /home symlink
|
||||||
|
home_link = os.path.join(work_dir, "home")
|
||||||
|
var_home = os.path.join(work_dir, "var", "home")
|
||||||
|
assert os.path.islink(home_link), "/home symlink not created"
|
||||||
|
assert os.path.realpath(home_link) == var_home
|
||||||
|
|
||||||
|
# Test file creation
|
||||||
|
test_file = env.create_test_file("test.txt", "test content")
|
||||||
|
assert os.path.exists(test_file)
|
||||||
|
|
||||||
|
with open(test_file, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
assert content == "test content"
|
||||||
|
|
||||||
|
# Test directory creation
|
||||||
|
test_dir = env.create_test_directory("test_dir")
|
||||||
|
assert os.path.exists(test_dir)
|
||||||
|
assert os.path.isdir(test_dir)
|
||||||
|
|
||||||
|
# Test teardown
|
||||||
|
env.teardown()
|
||||||
|
# Note: teardown only changes directory, doesn't remove files
|
||||||
|
|
||||||
|
def test_utility_functions(self, work_dir):
|
||||||
|
"""Test utility functions."""
|
||||||
|
from testutil import (
|
||||||
|
create_temp_manifest,
|
||||||
|
cleanup_temp_files,
|
||||||
|
assert_filesystem_structure,
|
||||||
|
assert_file_contents,
|
||||||
|
create_mock_context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test manifest creation
|
||||||
|
manifest_data = {
|
||||||
|
"pipeline": {
|
||||||
|
"build": {"name": "test"},
|
||||||
|
"stages": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest_file = create_temp_manifest(manifest_data)
|
||||||
|
assert os.path.exists(manifest_file)
|
||||||
|
|
||||||
|
# Test manifest loading
|
||||||
|
with open(manifest_file, 'r') as f:
|
||||||
|
loaded_data = json.load(f)
|
||||||
|
assert loaded_data == manifest_data
|
||||||
|
|
||||||
|
# Test cleanup
|
||||||
|
cleanup_temp_files(manifest_file)
|
||||||
|
assert not os.path.exists(manifest_file)
|
||||||
|
|
||||||
|
# Test filesystem structure assertion
|
||||||
|
test_dir = os.path.join(work_dir, "test_structure")
|
||||||
|
os.makedirs(test_dir, exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(test_dir, "etc"), exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(test_dir, "var"), exist_ok=True)
|
||||||
|
|
||||||
|
assert_filesystem_structure(test_dir, ["/etc", "/var"])
|
||||||
|
|
||||||
|
# Test file contents assertion
|
||||||
|
test_file = os.path.join(test_dir, "test.txt")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write("test content")
|
||||||
|
|
||||||
|
assert_file_contents(test_file, "test content")
|
||||||
|
|
||||||
|
# Test mock context creation
|
||||||
|
context = create_mock_context()
|
||||||
|
assert context.root == "/tmp/mock-root"
|
||||||
|
assert hasattr(context, 'run')
|
||||||
|
|
||||||
|
# Test mock context run method
|
||||||
|
result = context.run("test command")
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert result.stdout == b"mock output"
|
||||||
|
|
||||||
|
def test_debian_specific_utilities(self, work_dir):
|
||||||
|
"""Test Debian-specific utility functions."""
|
||||||
|
from testutil import TestDataGenerator
|
||||||
|
|
||||||
|
# Test Debian filesystem configuration
|
||||||
|
fs_config = TestDataGenerator.create_filesystem_config()
|
||||||
|
|
||||||
|
assert "rootfs_type" in fs_config
|
||||||
|
assert "ostree_integration" in fs_config
|
||||||
|
assert "home_symlink" in fs_config
|
||||||
|
assert "users" in fs_config
|
||||||
|
assert "permissions" in fs_config
|
||||||
|
|
||||||
|
assert fs_config["rootfs_type"] == "ext4"
|
||||||
|
assert fs_config["ostree_integration"] is True
|
||||||
|
assert fs_config["home_symlink"] is True
|
||||||
|
|
||||||
|
# Test user configuration
|
||||||
|
users = fs_config["users"]
|
||||||
|
assert len(users) > 0
|
||||||
|
assert "name" in users[0]
|
||||||
|
assert "password" in users[0]
|
||||||
|
assert "groups" in users[0]
|
||||||
|
|
||||||
|
# Test permission configuration
|
||||||
|
permissions = fs_config["permissions"]
|
||||||
|
assert "/etc/ostree" in permissions
|
||||||
|
assert "/var/lib/ostree" in permissions
|
||||||
|
assert permissions["/etc/ostree"] == "755"
|
||||||
|
|
||||||
|
def test_error_handling(self, work_dir):
|
||||||
|
"""Test error handling in utility functions."""
|
||||||
|
from testutil import cleanup_temp_files, assert_file_contents
|
||||||
|
|
||||||
|
# Test cleanup with non-existent file
|
||||||
|
cleanup_temp_files("/non/existent/file")
|
||||||
|
# Should not raise an exception
|
||||||
|
|
||||||
|
# Test file contents assertion with non-existent file
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
assert_file_contents("/non/existent/file", "content")
|
||||||
|
|
||||||
|
def test_performance_utilities(self, work_dir):
|
||||||
|
"""Test performance-related utilities."""
|
||||||
|
from testutil import create_mock_context
|
||||||
|
|
||||||
|
# Test multiple context creation
|
||||||
|
start_time = time.time()
|
||||||
|
contexts = []
|
||||||
|
|
||||||
|
for i in range(100):
|
||||||
|
context = create_mock_context()
|
||||||
|
contexts.append(context)
|
||||||
|
|
||||||
|
end_time = time.time()
|
||||||
|
creation_time = end_time - start_time
|
||||||
|
|
||||||
|
# Should be reasonably fast
|
||||||
|
assert creation_time < 1.0, f"Context creation took too long: {creation_time}s"
|
||||||
|
assert len(contexts) == 100
|
||||||
|
|
||||||
|
# Test context functionality
|
||||||
|
for context in contexts:
|
||||||
|
result = context.run("test")
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__])
|
||||||
461
test/vm.py
Normal file
461
test/vm.py
Normal file
|
|
@ -0,0 +1,461 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Virtual machine testing utilities for deb-bootc-image-builder.
|
||||||
|
|
||||||
|
This module provides utilities for testing built images in virtual machines,
|
||||||
|
including:
|
||||||
|
- VM creation and management
|
||||||
|
- Image boot testing
|
||||||
|
- Debian-specific VM configurations
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualMachine:
|
||||||
|
"""Virtual machine for testing Debian images."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, image_path: str, vm_type: str = "qemu"):
|
||||||
|
"""Initialize virtual machine."""
|
||||||
|
self.name = name
|
||||||
|
self.image_path = image_path
|
||||||
|
self.vm_type = vm_type
|
||||||
|
self.vm_process = None
|
||||||
|
self.vm_pid = None
|
||||||
|
self.network_config = None
|
||||||
|
self.memory = "2G"
|
||||||
|
self.cpus = 2
|
||||||
|
self.disk_size = "10G"
|
||||||
|
|
||||||
|
# Debian-specific configurations
|
||||||
|
self.debian_release = "trixie"
|
||||||
|
self.architecture = "amd64"
|
||||||
|
self.boot_timeout = 300 # 5 minutes
|
||||||
|
|
||||||
|
logger.info(f"Initialized VM {name} with image {image_path}")
|
||||||
|
|
||||||
|
def configure_network(self, network_type: str = "user", port_forward: Optional[Dict[str, int]] = None):
|
||||||
|
"""Configure VM network."""
|
||||||
|
self.network_config = {
|
||||||
|
"type": network_type,
|
||||||
|
"port_forward": port_forward or {}
|
||||||
|
}
|
||||||
|
logger.info(f"Configured network: {network_type}")
|
||||||
|
|
||||||
|
def set_resources(self, memory: str = "2G", cpus: int = 2, disk_size: str = "10G"):
|
||||||
|
"""Set VM resource limits."""
|
||||||
|
self.memory = memory
|
||||||
|
self.cpus = cpus
|
||||||
|
self.disk_size = disk_size
|
||||||
|
logger.info(f"Set resources: {memory} RAM, {cpus} CPUs, {disk_size} disk")
|
||||||
|
|
||||||
|
def start(self) -> bool:
|
||||||
|
"""Start the virtual machine."""
|
||||||
|
try:
|
||||||
|
if self.vm_type == "qemu":
|
||||||
|
return self._start_qemu()
|
||||||
|
else:
|
||||||
|
logger.error(f"Unsupported VM type: {self.vm_type}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start VM: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _start_qemu(self) -> bool:
|
||||||
|
"""Start QEMU virtual machine."""
|
||||||
|
cmd = [
|
||||||
|
"qemu-system-x86_64",
|
||||||
|
"-name", self.name,
|
||||||
|
"-m", self.memory,
|
||||||
|
"-smp", str(self.cpus),
|
||||||
|
"-drive", f"file={self.image_path},if=virtio,format=qcow2",
|
||||||
|
"-enable-kvm",
|
||||||
|
"-display", "none",
|
||||||
|
"-serial", "stdio",
|
||||||
|
"-monitor", "none"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add network configuration
|
||||||
|
if self.network_config:
|
||||||
|
if self.network_config["type"] == "user":
|
||||||
|
cmd.extend(["-net", "user"])
|
||||||
|
if self.network_config["port_forward"]:
|
||||||
|
for host_port, guest_port in self.network_config["port_forward"].items():
|
||||||
|
cmd.extend(["-net", f"user,hostfwd=tcp::{host_port}-:{guest_port}"])
|
||||||
|
elif self.network_config["type"] == "bridge":
|
||||||
|
cmd.extend(["-net", "bridge,br=virbr0"])
|
||||||
|
|
||||||
|
# Add Debian-specific optimizations
|
||||||
|
cmd.extend([
|
||||||
|
"-cpu", "host",
|
||||||
|
"-machine", "type=q35,accel=kvm",
|
||||||
|
"-device", "virtio-net-pci,netdev=net0",
|
||||||
|
"-netdev", "user,id=net0"
|
||||||
|
])
|
||||||
|
|
||||||
|
logger.info(f"Starting QEMU VM with command: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.vm_process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
self.vm_pid = self.vm_process.pid
|
||||||
|
logger.info(f"VM started with PID: {self.vm_pid}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start QEMU VM: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop(self) -> bool:
|
||||||
|
"""Stop the virtual machine."""
|
||||||
|
try:
|
||||||
|
if self.vm_process:
|
||||||
|
self.vm_process.terminate()
|
||||||
|
self.vm_process.wait(timeout=30)
|
||||||
|
logger.info("VM stopped gracefully")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning("No VM process to stop")
|
||||||
|
return False
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning("VM did not stop gracefully, forcing termination")
|
||||||
|
if self.vm_process:
|
||||||
|
self.vm_process.kill()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to stop VM: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""Check if VM is running."""
|
||||||
|
if self.vm_process:
|
||||||
|
return self.vm_process.poll() is None
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wait_for_boot(self, timeout: Optional[int] = None) -> bool:
|
||||||
|
"""Wait for VM to boot up."""
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.boot_timeout
|
||||||
|
|
||||||
|
logger.info(f"Waiting for VM to boot (timeout: {timeout}s)")
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
if self.is_running():
|
||||||
|
# Check if VM is responsive (basic boot check)
|
||||||
|
if self._check_boot_status():
|
||||||
|
logger.info("VM booted successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
logger.error("VM boot timeout exceeded")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_boot_status(self) -> bool:
|
||||||
|
"""Check if VM has booted successfully."""
|
||||||
|
# This is a simplified check - in practice, you'd want to:
|
||||||
|
# 1. Check for specific boot messages
|
||||||
|
# 2. Verify network connectivity
|
||||||
|
# 3. Check for system services
|
||||||
|
# 4. Verify Debian-specific indicators
|
||||||
|
|
||||||
|
try:
|
||||||
|
# For now, just check if the process is still running
|
||||||
|
return self.is_running()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Boot status check failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute_command(self, command: str, timeout: int = 30) -> Dict[str, Any]:
|
||||||
|
"""Execute a command in the VM (requires guest agent or SSH)."""
|
||||||
|
# This is a placeholder - in practice, you'd use:
|
||||||
|
# 1. QEMU guest agent
|
||||||
|
# 2. SSH connection
|
||||||
|
# 3. Serial console interaction
|
||||||
|
|
||||||
|
logger.info(f"Executing command in VM: {command}")
|
||||||
|
|
||||||
|
# For now, return a mock result
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"stdout": f"Mock output for: {command}",
|
||||||
|
"stderr": "",
|
||||||
|
"returncode": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_system_info(self) -> Dict[str, Any]:
|
||||||
|
"""Get system information from the VM."""
|
||||||
|
# This would typically use guest agent or SSH
|
||||||
|
return {
|
||||||
|
"os": "Debian GNU/Linux",
|
||||||
|
"release": self.debian_release,
|
||||||
|
"architecture": self.architecture,
|
||||||
|
"kernel": "Linux",
|
||||||
|
"uptime": "0:00:00",
|
||||||
|
"memory": "0 MB",
|
||||||
|
"disk": "0 MB"
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_debian_specific_features(self) -> Dict[str, bool]:
|
||||||
|
"""Check Debian-specific features in the VM."""
|
||||||
|
features = {
|
||||||
|
"ostree_integration": False,
|
||||||
|
"grub_efi": False,
|
||||||
|
"initramfs_tools": False,
|
||||||
|
"systemd": False,
|
||||||
|
"apt_package_manager": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# This would check actual VM state
|
||||||
|
# For now, return mock results
|
||||||
|
features["ostree_integration"] = True
|
||||||
|
features["grub_efi"] = True
|
||||||
|
features["initramfs_tools"] = True
|
||||||
|
features["systemd"] = True
|
||||||
|
features["apt_package_manager"] = True
|
||||||
|
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
class VMTester:
|
||||||
|
"""Test runner for virtual machines."""
|
||||||
|
|
||||||
|
def __init__(self, test_config: Dict[str, Any]):
|
||||||
|
"""Initialize VM tester."""
|
||||||
|
self.test_config = test_config
|
||||||
|
self.vms: List[VirtualMachine] = []
|
||||||
|
self.test_results: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def create_test_vm(self, image_path: str, vm_config: Dict[str, Any]) -> VirtualMachine:
|
||||||
|
"""Create a test virtual machine."""
|
||||||
|
vm_name = vm_config.get("name", f"test-vm-{len(self.vms)}")
|
||||||
|
vm = VirtualMachine(vm_name, image_path)
|
||||||
|
|
||||||
|
# Apply configuration
|
||||||
|
if "network" in vm_config:
|
||||||
|
vm.configure_network(**vm_config["network"])
|
||||||
|
|
||||||
|
if "resources" in vm_config:
|
||||||
|
vm.set_resources(**vm_config["resources"])
|
||||||
|
|
||||||
|
if "debian" in vm_config:
|
||||||
|
debian_config = vm_config["debian"]
|
||||||
|
if "release" in debian_config:
|
||||||
|
vm.debian_release = debian_config["release"]
|
||||||
|
if "architecture" in debian_config:
|
||||||
|
vm.architecture = debian_config["architecture"]
|
||||||
|
if "boot_timeout" in debian_config:
|
||||||
|
vm.boot_timeout = debian_config["boot_timeout"]
|
||||||
|
|
||||||
|
self.vms.append(vm)
|
||||||
|
return vm
|
||||||
|
|
||||||
|
def run_basic_boot_test(self, vm: VirtualMachine) -> Dict[str, Any]:
|
||||||
|
"""Run basic boot test on VM."""
|
||||||
|
test_result = {
|
||||||
|
"test_name": "basic_boot_test",
|
||||||
|
"vm_name": vm.name,
|
||||||
|
"start_time": time.time(),
|
||||||
|
"success": False,
|
||||||
|
"error": None,
|
||||||
|
"boot_time": None
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Starting basic boot test for VM: {vm.name}")
|
||||||
|
|
||||||
|
# Start VM
|
||||||
|
if not vm.start():
|
||||||
|
test_result["error"] = "Failed to start VM"
|
||||||
|
return test_result
|
||||||
|
|
||||||
|
# Wait for boot
|
||||||
|
start_time = time.time()
|
||||||
|
if vm.wait_for_boot():
|
||||||
|
boot_time = time.time() - start_time
|
||||||
|
test_result["boot_time"] = boot_time
|
||||||
|
test_result["success"] = True
|
||||||
|
logger.info(f"Boot test passed for VM: {vm.name} (boot time: {boot_time:.2f}s)")
|
||||||
|
else:
|
||||||
|
test_result["error"] = "VM failed to boot within timeout"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
test_result["error"] = str(e)
|
||||||
|
logger.error(f"Boot test failed for VM {vm.name}: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
test_result["end_time"] = time.time()
|
||||||
|
if test_result["success"]:
|
||||||
|
vm.stop()
|
||||||
|
|
||||||
|
return test_result
|
||||||
|
|
||||||
|
def run_debian_feature_test(self, vm: VirtualMachine) -> Dict[str, Any]:
|
||||||
|
"""Run Debian-specific feature test on VM."""
|
||||||
|
test_result = {
|
||||||
|
"test_name": "debian_feature_test",
|
||||||
|
"vm_name": vm.name,
|
||||||
|
"start_time": time.time(),
|
||||||
|
"success": False,
|
||||||
|
"error": None,
|
||||||
|
"features": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Starting Debian feature test for VM: {vm.name}")
|
||||||
|
|
||||||
|
# Start VM
|
||||||
|
if not vm.start():
|
||||||
|
test_result["error"] = "Failed to start VM"
|
||||||
|
return test_result
|
||||||
|
|
||||||
|
# Wait for boot
|
||||||
|
if not vm.wait_for_boot():
|
||||||
|
test_result["error"] = "VM failed to boot"
|
||||||
|
return test_result
|
||||||
|
|
||||||
|
# Check Debian features
|
||||||
|
features = vm.check_debian_specific_features()
|
||||||
|
test_result["features"] = features
|
||||||
|
|
||||||
|
# Validate required features
|
||||||
|
required_features = ["ostree_integration", "grub_efi", "systemd"]
|
||||||
|
missing_features = [f for f in required_features if not features.get(f, False)]
|
||||||
|
|
||||||
|
if missing_features:
|
||||||
|
test_result["error"] = f"Missing required features: {missing_features}"
|
||||||
|
else:
|
||||||
|
test_result["success"] = True
|
||||||
|
logger.info(f"Debian feature test passed for VM: {vm.name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
test_result["error"] = str(e)
|
||||||
|
logger.error(f"Debian feature test failed for VM {vm.name}: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
test_result["end_time"] = time.time()
|
||||||
|
if vm.is_running():
|
||||||
|
vm.stop()
|
||||||
|
|
||||||
|
return test_result
|
||||||
|
|
||||||
|
def run_all_tests(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Run all configured tests."""
|
||||||
|
logger.info("Starting VM test suite")
|
||||||
|
|
||||||
|
for vm in self.vms:
|
||||||
|
# Run basic boot test
|
||||||
|
boot_result = self.run_basic_boot_test(vm)
|
||||||
|
self.test_results.append(boot_result)
|
||||||
|
|
||||||
|
# Run Debian feature test
|
||||||
|
feature_result = self.run_debian_feature_test(vm)
|
||||||
|
self.test_results.append(feature_result)
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
total_tests = len(self.test_results)
|
||||||
|
passed_tests = len([r for r in self.test_results if r["success"]])
|
||||||
|
failed_tests = total_tests - passed_tests
|
||||||
|
|
||||||
|
logger.info(f"Test suite completed: {passed_tests}/{total_tests} tests passed")
|
||||||
|
|
||||||
|
if failed_tests > 0:
|
||||||
|
logger.warning(f"{failed_tests} tests failed")
|
||||||
|
for result in self.test_results:
|
||||||
|
if not result["success"]:
|
||||||
|
logger.error(f"Test {result['test_name']} failed: {result.get('error', 'Unknown error')}")
|
||||||
|
|
||||||
|
return self.test_results
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Clean up all VMs and resources."""
|
||||||
|
logger.info("Cleaning up VM resources")
|
||||||
|
|
||||||
|
for vm in self.vms:
|
||||||
|
if vm.is_running():
|
||||||
|
vm.stop()
|
||||||
|
|
||||||
|
self.vms.clear()
|
||||||
|
self.test_results.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_vm_config(image_path: str,
|
||||||
|
debian_release: str = "trixie",
|
||||||
|
architecture: str = "amd64") -> Dict[str, Any]:
|
||||||
|
"""Create a test VM configuration."""
|
||||||
|
return {
|
||||||
|
"name": f"debian-{debian_release}-{architecture}-test",
|
||||||
|
"image_path": image_path,
|
||||||
|
"network": {
|
||||||
|
"type": "user",
|
||||||
|
"port_forward": {"2222": 22} # SSH port forwarding
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"memory": "2G",
|
||||||
|
"cpus": 2,
|
||||||
|
"disk_size": "10G"
|
||||||
|
},
|
||||||
|
"debian": {
|
||||||
|
"release": debian_release,
|
||||||
|
"architecture": architecture,
|
||||||
|
"boot_timeout": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_vm_test_suite(image_path: str,
|
||||||
|
debian_release: str = "trixie",
|
||||||
|
architecture: str = "amd64") -> List[Dict[str, Any]]:
|
||||||
|
"""Run a complete VM test suite."""
|
||||||
|
# Create test configuration
|
||||||
|
vm_config = create_test_vm_config(image_path, debian_release, architecture)
|
||||||
|
|
||||||
|
# Create tester
|
||||||
|
tester = VMTester({})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create test VM
|
||||||
|
vm = tester.create_test_vm(image_path, vm_config)
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
results = tester.run_all_tests()
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
tester.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Example usage
|
||||||
|
test_image = "/tmp/test-image.qcow2"
|
||||||
|
|
||||||
|
if os.path.exists(test_image):
|
||||||
|
print("Running VM test suite...")
|
||||||
|
results = run_vm_test_suite(test_image)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
status = "PASS" if result["success"] else "FAIL"
|
||||||
|
print(f"{status}: {result['test_name']} - {result['vm_name']}")
|
||||||
|
if not result["success"]:
|
||||||
|
print(f" Error: {result.get('error', 'Unknown error')}")
|
||||||
|
else:
|
||||||
|
print(f"Test image not found: {test_image}")
|
||||||
|
print("Create a test image first or update the path")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue