mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2026-07-21 19:05:39 +02:00
Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Fixes #203
122 lines
4.0 KiB
Go
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
|
|
}
|