Files
CBZOptimizer/internal/utils/optimize_large_file_integration_test.go
T

122 lines
4.0 KiB
Go

package utils
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/belphemur/CBZOptimizer/v2/internal/utils/errs"
"github.com/belphemur/CBZOptimizer/v2/pkg/converter"
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
"github.com/rs/zerolog/log"
)
// largeTestFile is a ~1GB synthetic CBZ fixture stored via Git LFS (see
// .gitattributes). It is used to exercise the optimize pipeline with a
// chapter large enough to make in-memory-only handling impractical, and to
// validate that converted pages are streamed to/from a staging temp folder
// (see manga.Page.TempFilePath / manga.Chapter.TempDir) instead of blowing
// up memory usage.
const largeTestFile = "../../testdata/large/large_chapter.cbz"
// TestOptimizeIntegration_LargeFile is opt-in (set CBZ_RUN_LARGE_FILE_TEST=1)
// since it processes a ~1GB fixture and can take a while to run. It is
// automatically skipped if the fixture is unavailable (e.g. Git LFS content
// wasn't fetched, leaving only a pointer file) or in short mode.
func TestOptimizeIntegration_LargeFile(t *testing.T) {
if testing.Short() {
t.Skip("Skipping large file integration test in short mode")
}
if os.Getenv("CBZ_RUN_LARGE_FILE_TEST") == "" {
t.Skip("Skipping large file integration test; set CBZ_RUN_LARGE_FILE_TEST=1 to run it")
}
info, err := os.Stat(largeTestFile)
if err != nil {
t.Skipf("large test fixture not found: %v", err)
}
// If Git LFS content wasn't fetched (e.g. `actions/checkout` without
// `lfs: true`), the file on disk is just a small pointer text file
// rather than the real ~1GB fixture. Detect and skip gracefully instead
// of failing the whole suite.
const minExpectedSize = 500 * 1024 * 1024 // 500MB
if info.Size() < minExpectedSize {
t.Skipf("large test fixture looks like a Git LFS pointer (size=%d), skipping; run `git lfs pull`", info.Size())
}
tempDir, err := os.MkdirTemp("", "test_optimize_large_file")
if err != nil {
t.Fatal(err)
}
defer errs.CaptureGeneric(&err, os.RemoveAll, tempDir, "failed to remove temporary directory")
converterInstance, err := converter.Get(constant.WebP)
if err != nil {
t.Skip("WebP converter not available, skipping large file integration test")
}
if err := converterInstance.PrepareConverter(); err != nil {
t.Skip("Failed to prepare WebP converter, skipping large file integration test")
}
cbzFile := filepath.Join(tempDir, "large_chapter.cbz")
if err := copyFile(largeTestFile, cbzFile); err != nil {
t.Fatal(err)
}
var memBefore, memAfter runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&memBefore)
options := &OptimizeOptions{
ChapterConverter: converterInstance,
Path: cbzFile,
Quality: 85,
Override: false,
Split: true,
}
err = Optimize(options)
if err != nil {
t.Fatalf("failed to optimize large chapter: %v", err)
}
runtime.GC()
runtime.ReadMemStats(&memAfter)
log.Info().
Uint64("heap_alloc_before", memBefore.HeapAlloc).
Uint64("heap_alloc_after", memAfter.HeapAlloc).
Int64("input_size", info.Size()).
Msg("Large file integration test memory usage")
outputFile := strings.TrimSuffix(cbzFile, ".cbz") + "_converted.cbz"
if _, err := os.Stat(outputFile); err != nil {
t.Fatalf("expected converted output file %s to exist: %v", outputFile, err)
}
}
// copyFile copies src to dst using streaming file I/O so that the whole
// file content is never held in memory at once, which matters for the
// large fixture used by this test.
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source file: %w", err)
}
defer errs.Capture(&err, in.Close, "failed to close source file")
out, err := os.Create(dst)
if err != nil {
return fmt.Errorf("failed to create destination file: %w", err)
}
defer errs.Capture(&err, out.Close, "failed to close destination file")
if _, err := io.Copy(out, in); err != nil {
return fmt.Errorf("failed to copy file contents: %w", err)
}
return nil
}