Просмотр исходного кода

IMG-33: adds simple integration tests for image.Load() (#1462)

* Load() integration test

* Added test-images as submodule
Victor Sokolov 2 месяцев назад
Родитель
Сommit
aa91342686

+ 1 - 1
.devcontainer/Dockerfile

@@ -31,7 +31,7 @@ ENV GOPATH=$HOME/go
 ENV PATH=$PATH:$GOROOT/bin:$GOPATH/bin
 
 # gnupg is requiered for clang-format
-RUN apt-get install -y gnupg lsb-release ssh unzip
+RUN apt-get install -y gnupg lsb-release ssh
 
 # Install air
 RUN go install github.com/air-verse/air@latest

+ 0 - 0
.devcontainer/images/.gitkeep


+ 0 - 1
.devcontainer/oss/devcontainer.json

@@ -28,6 +28,5 @@
             ]
         }
     },
-    "initializeCommand": "mkdir -p ${localWorkspaceFolder}/.devcontainer/images/ && curl -O -L https://github.com/imgproxy/test-images/archive/refs/heads/main.zip && unzip main.zip -d ${localWorkspaceFolder}/.devcontainer/images/ && rm main.zip && mv ${localWorkspaceFolder}/.devcontainer/images/test-images-main/* ${localWorkspaceFolder}/.devcontainer/images/ && rmdir ${localWorkspaceFolder}/.devcontainer/images/test-images-main",
     "postCreateCommand": "lefthook install"
 }

+ 0 - 66
.github/ci-docker/Dockerfile

@@ -1,66 +0,0 @@
-FROM public.ecr.aws/ubuntu/ubuntu:noble
-
-ARG VIPS_VERSIONS="8.14 8.15 8.16"
-
-RUN apt-get -qq update \
-  && apt-get install -y --no-install-recommends \
-    bash \
-    curl \
-    git \
-    ca-certificates \
-    build-essential \
-    gobject-introspection \
-    libgirepository1.0-dev \
-    python3-pip \
-    python3-venv \
-    libssl-dev \
-    libglib2.0-dev \
-    libxml2-dev \
-    libjpeg-dev \
-    libpng-dev \
-    libwebp-dev \
-    librsvg2-dev \
-    libexif-dev \
-    liblcms2-dev \
-  && python3 -m venv /root/.python \
-  && /root/.python/bin/pip install meson ninja \
-  && rm -rf /var/lib/apt/lists/*
-
-RUN curl https://sh.rustup.rs -sSf | sh -s -- -y \
-  && export PATH="/root/.cargo/bin:$PATH" \
-  && cargo install cargo-c \
-  && cd /root \
-  && git clone --depth 1 https://github.com/DarthSim/quantizr.git \
-  && cd quantizr \
-  && cargo cinstall --release --library-type=cdylib --prefix=/usr/local --libdir=/usr/local/lib \
-  && rm -rf /root/.rustup /root/.cargo
-
-ENV PATH="/root/.python/bin:$PATH"
-ENV LD_LIBRARY_PATH="/usr/local/lib"
-
-RUN \
-  mkdir /root/vips \
-    && cd /root/vips \
-    && curl -s -S -L -o vips_releases.json "https://api.github.com/repos/libvips/libvips/releases" \
-    && for VIPS_VERSION in $VIPS_VERSIONS; do \
-      mkdir $VIPS_VERSION \
-      && export VIPS_RELEASE=$(grep -m 1 "\"tag_name\": \"v$VIPS_VERSION." vips_releases.json | sed -E 's/.*"v([^"]+)".*/\1/') \
-      && echo "Building Vips $VIPS_RELEASE as $VIPS_VERSION" \
-      && curl -s -S -L -o libvips-$VIPS_RELEASE.tar.gz https://github.com/libvips/libvips/archive/refs/tags/v$VIPS_RELEASE.tar.gz \
-      && tar -xzf libvips-$VIPS_RELEASE.tar.gz \
-      && cd libvips-$VIPS_RELEASE \
-      && meson setup _build \
-        --buildtype=release \
-        --strip \
-        --prefix=/root/vips/$VIPS_VERSION \
-        --libdir=lib \
-        -Dgtk_doc=false \
-      && ninja -C _build \
-      && ninja -C _build install \
-      && cd .. \
-      && rm -rf libvips-$VIPS_RELEASE.tar.gz libvips-$VIPS_RELEASE; \
-    done
-
-WORKDIR /go/src
-
-ENTRYPOINT [ "/bin/bash" ]

+ 0 - 4
.github/ci-docker/hooks/push

@@ -1,4 +0,0 @@
-#!/bin/bash
-DATETAG=$(date +%Y%m%d%H%M)
-docker tag $IMAGE_NAME $DOCKER_REPO:$DATETAG
-docker push $DOCKER_REPO:$DATETAG

+ 0 - 44
.github/workflows/build-ci-docker.yml

@@ -1,44 +0,0 @@
-name: Build CI Docker
-
-on:
-  workflow_dispatch:
-    inputs:
-      vips_versions:
-        description: 'Whitespace separated list of libvips versions to build'
-        required: true
-        default: "8.14 8.15 8.16"
-
-jobs:
-  build:
-    runs-on: ubuntu-latest
-    permissions:
-      contents: read
-      packages: write
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v3
-        with:
-          registry: ghcr.io
-          username: ${{ github.actor }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3
-
-      - name: Generate Docker tag
-        id: tag
-        run: echo "tag=ghcr.io/imgproxy/imgproxy-ci:$(date +%Y%m%d%H%M)" >> "$GITHUB_OUTPUT"
-
-      - name: Build and push
-        uses: docker/build-push-action@v6
-        with:
-          context: .
-          file: ./.github/ci-docker/Dockerfile
-          tags: ${{ steps.tag.outputs.tag }}
-          platforms: linux/amd64
-          build-args: |
-            "VIPS_VERSIONS=${{ github.event.inputs.vips_versions }}"
-          push: true

+ 36 - 21
.github/workflows/ci.yml

@@ -5,44 +5,54 @@ on:
 
 env:
   CGO_LDFLAGS_ALLOW: "-s|-w"
+  PKG_CONFIG_LIBDIR: /opt/imgproxy/lib/pkgconfig
+  LD_LIBRARY_PATH: /opt/imgproxy/lib
+  GOFLAGS: -buildvcs=false
 
 jobs:
   test:
     runs-on: ubuntu-latest
     container:
-      image: ghcr.io/imgproxy/imgproxy-ci:202410292002
-    strategy:
-      matrix:
-        go-version: ["1.23.x", "1.22.x", "1.21.x"]
-        vips-version: ["8.16", "8.15", "8.14"]
+      image: ghcr.io/imgproxy/imgproxy-base:latest
     steps:
       - name: Checkout
         uses: actions/checkout@v4
-      - uses: actions/setup-go@v5
         with:
-          go-version: ${{ matrix.go-version }}
+          submodules: true
+      - name: Setup cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            ~/.cache/go-build
+            ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
       - name: Download mods
         run: go mod download
+      - name: Mark git workspace as safe
+        run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
       - name: Test
-        run: go test ./...
-        env:
-          LD_LIBRARY_PATH: "/usr/local/lib:/root/vips/${{ matrix.vips-version }}/lib"
-          PKG_CONFIG_PATH: "/usr/local/lib/pkgconfig:/root/vips/${{ matrix.vips-version }}/lib/pkgconfig"
+        run: go test -tags integration ./...
 
   lint:
     runs-on: ubuntu-latest
     container:
-      image: ghcr.io/imgproxy/imgproxy-ci:202410292002
-    strategy:
-      matrix:
-        go-version: ["1.23.x"]
-        vips-version: ["8.16"]
+      image: ghcr.io/imgproxy/imgproxy-base:latest
     steps:
       - name: Checkout
         uses: actions/checkout@v4
-      - uses: actions/setup-go@v5
         with:
-          go-version: ${{ matrix.go-version }}
+          submodules: true
+      - name: Setup cache
+        uses: actions/cache@v4
+        with:
+          path: |
+            ~/.cache/go-build
+            ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-go-
       - name: Download mods
         run: go mod download
       - name: Lint
@@ -51,20 +61,25 @@ jobs:
           version: v2.1.6
           args: --timeout 10m0s
         env:
-          LD_LIBRARY_PATH: "/usr/local/lib:/root/vips/${{ matrix.vips-version }}/lib"
-          PKG_CONFIG_PATH: "/usr/local/lib/pkgconfig:/root/vips/${{ matrix.vips-version }}/lib/pkgconfig"
+          PKG_CONFIG_LIBDIR: /opt/imgproxy/lib/pkgconfig
           GOFLAGS: -buildvcs=false
 
   c-lint:
     runs-on: ubuntu-24.04
+    permissions:
+      contents: read
     steps:
       - uses: actions/checkout@v4
+        with:
+          submodules: true
       - uses: cpp-linter/cpp-linter-action@v2
         id: linter
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         with:
           style: file
           version: 18 # Ubuntu 24.04 provides clang-format-18
-          tidy-checks: '-*' # disable clang-tidy
+          tidy-checks: "-*" # disable clang-tidy
 
       - name: Fail fast
         continue-on-error: true # TODO: remove this line in the future

+ 0 - 1
.gitignore

@@ -8,5 +8,4 @@ tmp/
 docker-base
 docs/sitemap.txt
 .env
-.devcontainer/images/*
 k6/*.json

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "testdata/test-images"]
+	path = testdata/test-images
+	url = git@github.com:imgproxy/test-images.git

+ 1 - 1
.lefthook/pre-commit/lint

@@ -14,4 +14,4 @@ fi
 export CGO_LDFLAGS_ALLOW="-s|-w"
 export CGO_CFLAGS_ALLOW="-I|-Xpreprocessor"
 
-golangci-lint run
+golangci-lint --build-tags integration run

+ 2 - 0
go.mod

@@ -25,6 +25,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/ssm v1.60.0
 	github.com/aws/aws-sdk-go-v2/service/sts v1.34.0
 	github.com/bugsnag/bugsnag-go/v2 v2.5.1
+	github.com/corona10/goimagehash v1.1.0
 	github.com/felixge/httpsnoop v1.0.4
 	github.com/fsouza/fake-gcs-server v1.42.2
 	github.com/getsentry/sentry-go v0.34.1
@@ -160,6 +161,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
 	github.com/outcaste-io/ristretto v0.2.3 // indirect
 	github.com/pborman/uuid v1.2.1 // indirect
 	github.com/philhofer/fwd v1.2.0 // indirect

+ 4 - 0
go.sum

@@ -170,6 +170,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
 github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
+github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -342,6 +344,8 @@ github.com/newrelic/go-agent/v3 v3.39.0 h1:VVhsJR422oOxU/sJ1HZrop/OC7G1GTClIviVJ
 github.com/newrelic/go-agent/v3 v3.39.0/go.mod h1:4QXvru0vVy/iu7mfkNHT7T2+9TC9zPGO8aUEdKqY138=
 github.com/newrelic/newrelic-telemetry-sdk-go v0.8.1 h1:6OX5VXMuj2salqNBc41eXKz6K+nV6OB/hhlGnAKCbwU=
 github.com/newrelic/newrelic-telemetry-sdk-go v0.8.1/go.mod h1:2kY6OeOxrJ+RIQlVjWDc/pZlT3MIf30prs6drzMfJ6E=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
 github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
 github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=

+ 141 - 0
integration/load_test.go

@@ -0,0 +1,141 @@
+//go:build integration
+// +build integration
+
+package integration
+
+import (
+	"bytes"
+	"fmt"
+	"image/png"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/corona10/goimagehash"
+	"github.com/imgproxy/imgproxy/v3/imagetype"
+	"github.com/imgproxy/imgproxy/v3/vips"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const (
+	similarityThreshold = 5 // Distance between images to be considered similar
+)
+
+// testLoadFolder fetches images iterates over images in the specified folder,
+// runs imgproxy on each image, and compares the result with the reference image
+// which is expected to be in the `integration` folder with the same name
+// but with `.png` extension.
+func testLoadFolder(t *testing.T, cs, sourcePath, folder string) {
+	t.Logf("Testing folder: %s", folder)
+
+	walkPath := path.Join(sourcePath, folder)
+
+	// Iterate over the files in the source folder
+	err := filepath.Walk(walkPath, func(path string, info os.FileInfo, err error) error {
+		require.NoError(t, err)
+
+		// Skip directories
+		if info.IsDir() {
+			return nil
+		}
+
+		// get the base name of the file (8-bpp.png)
+		basePath := filepath.Base(path)
+
+		// Replace the extension with .png
+		referencePath := strings.TrimSuffix(basePath, filepath.Ext(basePath)) + ".png"
+
+		// Construct the full path to the reference image (integration/ folder)
+		referencePath = filepath.Join(sourcePath, "integration", folder, referencePath)
+
+		// Construct the source URL for imgproxy (no processing)
+		sourceUrl := fmt.Sprintf("insecure/plain/local:///%s/%s@png", folder, basePath)
+
+		imgproxyImageBytes := fetchImage(t, cs, sourceUrl)
+		imgproxyImage, err := png.Decode(bytes.NewReader(imgproxyImageBytes))
+		require.NoError(t, err, "Failed to decode PNG image from imgproxy for %s", basePath)
+
+		referenceFile, err := os.Open(referencePath)
+		require.NoError(t, err)
+		defer referenceFile.Close()
+
+		referenceImage, err := png.Decode(referenceFile)
+		require.NoError(t, err, "Failed to decode PNG reference image for %s", referencePath)
+
+		hash1, err := goimagehash.DifferenceHash(imgproxyImage)
+		require.NoError(t, err)
+
+		hash2, err := goimagehash.DifferenceHash(referenceImage)
+		require.NoError(t, err)
+
+		distance, err := hash1.Distance(hash2)
+		require.NoError(t, err)
+
+		assert.LessOrEqual(t, distance, similarityThreshold,
+			"Image %s differs from reference image %s by %d, which is greater than the allowed threshold of %d",
+			basePath, referencePath, distance, similarityThreshold)
+
+		return nil
+	})
+
+	require.NoError(t, err)
+}
+
+// TestLoadSaveToPng ensures that our load pipeline works,
+// including standard and custom loaders. For each source image
+// in the folder, it does the passthrough request through imgproxy:
+// no processing, just convert format of the source file to png.
+// Then, it compares the result with the reference image.
+func TestLoadSaveToPng(t *testing.T) {
+	ctx := t.Context()
+
+	// TODO: Will be moved to test suite (like in processing_test.go)
+	// Since we use SupportsLoad, we need to initialize vips
+	defer vips.Shutdown() // either way it needs to be deinitialized
+	err := vips.Init()
+	require.NoError(t, err, "Failed to initialize vips")
+
+	path, err := testImagesPath(t)
+	require.NoError(t, err)
+
+	cs := startImgproxy(t, ctx, path)
+
+	if vips.SupportsLoad(imagetype.GIF) {
+		testLoadFolder(t, cs, path, "gif")
+	}
+
+	if vips.SupportsLoad(imagetype.JPEG) {
+		testLoadFolder(t, cs, path, "jpg")
+	}
+
+	if vips.SupportsLoad(imagetype.HEIC) {
+		testLoadFolder(t, cs, path, "heif")
+	}
+
+	if vips.SupportsLoad(imagetype.JXL) {
+		testLoadFolder(t, cs, path, "jxl")
+	}
+
+	if vips.SupportsLoad(imagetype.SVG) {
+		testLoadFolder(t, cs, path, "svg")
+	}
+
+	if vips.SupportsLoad(imagetype.TIFF) {
+		testLoadFolder(t, cs, path, "tiff")
+	}
+
+	if vips.SupportsLoad(imagetype.WEBP) {
+		testLoadFolder(t, cs, path, "webp")
+	}
+
+	if vips.SupportsLoad(imagetype.BMP) {
+		testLoadFolder(t, cs, path, "bmp")
+	}
+
+	if vips.SupportsLoad(imagetype.ICO) {
+		testLoadFolder(t, cs, path, "ico")
+	}
+}

+ 137 - 0
integration/test_utils.go

@@ -0,0 +1,137 @@
+//go:build integration
+// +build integration
+
+// Integration test helpers for imgproxy.
+// We use regular `go build` instead of Docker to make sure
+// tests run in the same environment as other tests,
+// including in CI, where everything runs in a custom Docker image
+// against the different libvips versions.
+
+package integration
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"os"
+	"os/exec"
+	"path"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+const (
+	buildContext = ".."                 // Source code folder
+	binPath      = "/tmp/imgproxy-test" // Path to the built imgproxy binary
+	bindPort     = 9090                 // Port to bind imgproxy to
+	bindHost     = "127.0.0.1"          // Host to bind imgproxy to
+)
+
+var (
+	buildCmd = []string{"build", "-v", "-ldflags=-s -w", "-o", binPath} // imgproxy build command
+)
+
+// waitForPort tries to connect to host:port until successful or timeout
+func waitForPort(host string, port int, timeout time.Duration) error {
+	var address string
+	if net.ParseIP(host) != nil && net.ParseIP(host).To4() == nil {
+		// IPv6 address, wrap in brackets
+		address = fmt.Sprintf("[%s]:%d", host, port)
+	} else {
+		address = fmt.Sprintf("%s:%d", host, port)
+	}
+	deadline := time.Now().Add(timeout)
+
+	for time.Now().Before(deadline) {
+		conn, err := net.DialTimeout("tcp", address, 500*time.Millisecond)
+		if err == nil {
+			conn.Close()
+			return nil // port is open
+		}
+		time.Sleep(200 * time.Millisecond)
+	}
+
+	return fmt.Errorf("timeout waiting for port %s", address)
+}
+
+func startImgproxy(t *testing.T, ctx context.Context, testImagesPath string) string {
+	// Build the imgproxy binary
+	buildCmd := exec.Command("go", buildCmd...)
+	buildCmd.Dir = buildContext
+	buildCmd.Env = os.Environ()
+	buildOut, err := buildCmd.CombinedOutput()
+	require.NoError(t, err, "failed to build imgproxy: %v\n%s", err, string(buildOut))
+
+	// Start imgproxy in the background
+	cmd := exec.CommandContext(ctx, binPath)
+
+	// Set environment variables for imgproxy
+	cmd.Env = append(os.Environ(), "IMGPROXY_BIND=:"+fmt.Sprintf("%d", bindPort))
+	cmd.Env = append(cmd.Env, "IMGPROXY_LOCAL_FILESYSTEM_ROOT="+testImagesPath)
+	cmd.Env = append(cmd.Env, "IMGPROXY_MAX_ANIMATION_FRAMES=999")
+	cmd.Env = append(cmd.Env, "IMGPROXY_VIPS_LEAK_CHECK=true")
+	cmd.Env = append(cmd.Env, "IMGPROXY_LOG_MEM_STATS=true")
+	cmd.Env = append(cmd.Env, "IMGPROXY_DEVELOPMENT_ERRORS_MODE=true")
+
+	// That one is for the build logs
+	stdout, _ := os.CreateTemp("", "imgproxy-stdout-*")
+	stderr, _ := os.CreateTemp("", "imgproxy-stderr-*")
+	cmd.Stdout = stdout
+	cmd.Stderr = stderr
+
+	err = cmd.Start()
+	require.NoError(t, err, "failed to start imgproxy: %v", err)
+
+	// Wait for port 8090 to be available
+	err = waitForPort(bindHost, bindPort, 5*time.Second)
+	if err != nil {
+		cmd.Process.Kill()
+		require.NoError(t, err, "imgproxy did not start in time")
+	}
+
+	// Return a dummy container (nil) and connection string
+	t.Cleanup(func() {
+		cmd.Process.Kill()
+		stdout.Close()
+		stderr.Close()
+		os.Remove(stdout.Name())
+		os.Remove(stderr.Name())
+		os.Remove(binPath)
+	})
+
+	return fmt.Sprintf("%s:%d", bindHost, bindPort)
+}
+
+// fetchImage fetches an image from the imgproxy server
+func fetchImage(t *testing.T, cs string, path string) []byte {
+	url := fmt.Sprintf("http://%s/%s", cs, path)
+
+	resp, err := http.Get(url)
+	require.NoError(t, err, "Failed to fetch image from %s", url)
+	defer resp.Body.Close()
+
+	require.Equal(t, http.StatusOK, resp.StatusCode, "Expected status code 200 OK, got %d, url: %s", resp.StatusCode, url)
+
+	bytes, err := io.ReadAll(resp.Body)
+	require.NoError(t, err, "Failed to read response body from %s", url)
+
+	return bytes
+}
+
+// testImagesPath returns the absolute path to the test images directory
+func testImagesPath(t *testing.T) (string, error) {
+	// Get current working directory
+	dir, err := os.Getwd()
+	require.NoError(t, err)
+
+	// Convert to absolute path (if it's not already)
+	absPath, err := filepath.Abs(dir)
+	require.NoError(t, err)
+
+	return path.Join(absPath, "../testdata/test-images"), nil
+}

+ 3 - 3
main.go

@@ -84,7 +84,7 @@ func shutdown() {
 	errorreport.Close()
 }
 
-func run() error {
+func run(ctx context.Context) error {
 	if err := initialize(); err != nil {
 		return err
 	}
@@ -103,7 +103,7 @@ func run() error {
 		}
 	}()
 
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(ctx)
 
 	if err := prometheus.StartServer(cancel); err != nil {
 		return err
@@ -137,7 +137,7 @@ func main() {
 		os.Exit(0)
 	}
 
-	if err := run(); err != nil {
+	if err := run(context.Background()); err != nil {
 		log.Fatal(err)
 	}
 }

+ 1 - 0
testdata/test-images

@@ -0,0 +1 @@
+Subproject commit 9bee50dcc141f3af3b7e0dd79713d9b973150ae1