mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2026-05-25 01:11:11 +02:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4e2606ace | |||
| d78b523ae7 | |||
| 7b1fb13a21 | |||
| 414a43476b | |||
| dbac6c20ff | |||
| ef1a658028 | |||
| 76aea38ff2 | |||
| a69eb9d8a5 | |||
| a6875c3a12 | |||
| b3686e17d8 | |||
| 6ea989eedd | |||
| f104add37a | |||
| 39632906da | |||
| 1be0978e89 | |||
| 71bdf7d111 | |||
| 3123fa27f9 | |||
| 7cd711ab5a | |||
| df3206b80c | |||
| 878da22fdb | |||
| fcf6bf514b | |||
| 70a3306ca9 | |||
| 0ff3eab0b9 | |||
| 45851713c1 | |||
| cca06642f8 | |||
| a0a79c6439 | |||
| 6c45a8507f | |||
| d66d445bbb | |||
| aabb0e2a02 | |||
| 8349569e33 | |||
| b434a18802 | |||
| 4057cfb6c9 | |||
| 617ee8a0c3 | |||
| f37ca7e4e7 | |||
| 38c8f2a9cb | |||
| 03479c8772 | |||
| ee47432721 | |||
| 5a0fe68e68 | |||
| e535809e74 | |||
| af5bfe8000 | |||
| 9ac9901990 |
@@ -18,6 +18,6 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
|
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
|
||||||
fetch-depth: 0 # a full history is required for pull request analysis
|
fetch-depth: 0 # a full history is required for pull request analysis
|
||||||
- name: 'Qodana Scan'
|
- name: 'Qodana Scan'
|
||||||
uses: JetBrains/qodana-action@v2024.1
|
uses: JetBrains/qodana-action@v2026.1.0
|
||||||
env:
|
env:
|
||||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||||
@@ -29,19 +29,19 @@ jobs:
|
|||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
- name: Install Syft
|
- name: Install Syft
|
||||||
uses: anchore/sbom-action/download-syft@v0.20.11 # installs syft
|
uses: anchore/sbom-action/download-syft@v0.24.0 # installs syft
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@v3 # login to ghcr
|
uses: docker/login-action@v4 # login to ghcr
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6 # run goreleaser
|
uses: goreleaser/goreleaser-action@v7 # run goreleaser
|
||||||
with:
|
with:
|
||||||
version: nightly
|
version: nightly
|
||||||
args: release --clean --verbose
|
args: release --clean --verbose
|
||||||
@@ -49,11 +49,11 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# After GoReleaser runs, attest all the files in ./dist/checksums.txt:
|
# After GoReleaser runs, attest all the files in ./dist/checksums.txt:
|
||||||
- name: Attest Build Provenance for Archives
|
- name: Attest Build Provenance for Archives
|
||||||
uses: actions/attest-build-provenance@v3
|
uses: actions/attest-build-provenance@v4
|
||||||
with:
|
with:
|
||||||
subject-checksums: ./dist/checksums.txt
|
subject-checksums: ./dist/checksums.txt
|
||||||
# After GoReleaser runs, attest all the images in ./dist/digests.txt:
|
# After GoReleaser runs, attest all the images in ./dist/digests.txt:
|
||||||
- name: Attest Build Provenance for Docker Images
|
- name: Attest Build Provenance for Docker Images
|
||||||
uses: actions/attest-build-provenance@v3
|
uses: actions/attest-build-provenance@v4
|
||||||
with:
|
with:
|
||||||
subject-checksums: ./dist/digests.txt
|
subject-checksums: ./dist/digests.txt
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test result artifact
|
- name: Upload test result artifact
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
path: |
|
path: |
|
||||||
@@ -46,6 +46,6 @@ jobs:
|
|||||||
files: test-results/junit.xml
|
files: test-results/junit.xml
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Upload coverage reports to Codecov
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -345,3 +346,201 @@ func TestFormatFlagCaseInsensitive(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConvertCbzCommand_ManyFiles_NoDeadlock tests that processing many files in parallel
|
||||||
|
// does not cause a deadlock. This reproduces the scenario where processing
|
||||||
|
// recursive folders of CBZ files with parallelism > 1 could cause a "all goroutines are asleep - deadlock!" error.
|
||||||
|
func TestConvertCbzCommand_ManyFiles_NoDeadlock(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_cbz_many_files")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer errs.CaptureGeneric(&err, os.RemoveAll, tempDir, "failed to remove temporary directory")
|
||||||
|
|
||||||
|
// Locate the testdata directory
|
||||||
|
testdataDir := filepath.Join("../../../testdata")
|
||||||
|
if _, err := os.Stat(testdataDir); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("testdata directory not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subdirectories to simulate the recursive folder structure from the bug report
|
||||||
|
subdirs := []string{"author1/book1", "author2/book2", "author3/book3", "author4/book4"}
|
||||||
|
for _, subdir := range subdirs {
|
||||||
|
err := os.MkdirAll(filepath.Join(tempDir, subdir), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a sample CBZ file to copy
|
||||||
|
var sampleCBZ string
|
||||||
|
err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".cbz") && !strings.Contains(info.Name(), "converted") {
|
||||||
|
sampleCBZ = path
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil || sampleCBZ == "" {
|
||||||
|
t.Fatalf("Failed to find sample CBZ file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the sample file to multiple locations (simulating many files to process)
|
||||||
|
numFilesPerDir := 5
|
||||||
|
totalFiles := 0
|
||||||
|
for _, subdir := range subdirs {
|
||||||
|
for i := 0; i < numFilesPerDir; i++ {
|
||||||
|
destPath := filepath.Join(tempDir, subdir, fmt.Sprintf("Chapter_%d.cbz", i+1))
|
||||||
|
data, err := os.ReadFile(sampleCBZ)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read sample file: %v", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(destPath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
totalFiles++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf("Created %d test files across %d directories", totalFiles, len(subdirs))
|
||||||
|
|
||||||
|
// Mock the converter.Get function
|
||||||
|
originalGet := converter.Get
|
||||||
|
converter.Get = func(format constant.ConversionFormat) (converter.Converter, error) {
|
||||||
|
return &MockConverter{}, nil
|
||||||
|
}
|
||||||
|
defer func() { converter.Get = originalGet }()
|
||||||
|
|
||||||
|
// Set up the command with parallelism = 2 (same as the bug report)
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "optimize",
|
||||||
|
}
|
||||||
|
cmd.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
||||||
|
cmd.Flags().IntP("parallelism", "n", 2, "Number of chapters to convert in parallel")
|
||||||
|
cmd.Flags().BoolP("override", "o", false, "Override the original CBZ/CBR files")
|
||||||
|
cmd.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
||||||
|
cmd.Flags().DurationP("timeout", "t", 0, "Maximum time allowed for converting a single chapter")
|
||||||
|
|
||||||
|
converterType = constant.DefaultConversion
|
||||||
|
setupFormatFlag(cmd, &converterType, false)
|
||||||
|
|
||||||
|
// Run the command with a timeout to detect deadlocks
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- ConvertCbzCommand(cmd, []string{tempDir})
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Command completed successfully without deadlock")
|
||||||
|
case <-time.After(60 * time.Second):
|
||||||
|
t.Fatal("Deadlock detected: Command did not complete within 60 seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that converted files were created
|
||||||
|
var convertedCount int
|
||||||
|
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && strings.HasSuffix(info.Name(), "_converted.cbz") {
|
||||||
|
convertedCount++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error counting converted files: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if convertedCount != totalFiles {
|
||||||
|
t.Errorf("Expected %d converted files, found %d", totalFiles, convertedCount)
|
||||||
|
}
|
||||||
|
t.Logf("Found %d converted files as expected", convertedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConvertCbzCommand_HighParallelism_NoDeadlock tests processing with high parallelism setting.
|
||||||
|
func TestConvertCbzCommand_HighParallelism_NoDeadlock(t *testing.T) {
|
||||||
|
// Create a temporary directory
|
||||||
|
tempDir, err := os.MkdirTemp("", "test_cbz_high_parallel")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer errs.CaptureGeneric(&err, os.RemoveAll, tempDir, "failed to remove temporary directory")
|
||||||
|
|
||||||
|
// Locate the testdata directory
|
||||||
|
testdataDir := filepath.Join("../../../testdata")
|
||||||
|
if _, err := os.Stat(testdataDir); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("testdata directory not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and copy sample CBZ files
|
||||||
|
var sampleCBZ string
|
||||||
|
err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".cbz") && !strings.Contains(info.Name(), "converted") {
|
||||||
|
sampleCBZ = path
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil || sampleCBZ == "" {
|
||||||
|
t.Fatalf("Failed to find sample CBZ file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create many test files
|
||||||
|
numFiles := 15
|
||||||
|
for i := 0; i < numFiles; i++ {
|
||||||
|
destPath := filepath.Join(tempDir, fmt.Sprintf("test_file_%d.cbz", i+1))
|
||||||
|
data, err := os.ReadFile(sampleCBZ)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read sample file: %v", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(destPath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the converter
|
||||||
|
originalGet := converter.Get
|
||||||
|
converter.Get = func(format constant.ConversionFormat) (converter.Converter, error) {
|
||||||
|
return &MockConverter{}, nil
|
||||||
|
}
|
||||||
|
defer func() { converter.Get = originalGet }()
|
||||||
|
|
||||||
|
// Test with high parallelism (8)
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "optimize",
|
||||||
|
}
|
||||||
|
cmd.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
||||||
|
cmd.Flags().IntP("parallelism", "n", 8, "Number of chapters to convert in parallel")
|
||||||
|
cmd.Flags().BoolP("override", "o", false, "Override the original CBZ/CBR files")
|
||||||
|
cmd.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
||||||
|
cmd.Flags().DurationP("timeout", "t", 0, "Maximum time allowed for converting a single chapter")
|
||||||
|
|
||||||
|
converterType = constant.DefaultConversion
|
||||||
|
setupFormatFlag(cmd, &converterType, false)
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- ConvertCbzCommand(cmd, []string{tempDir})
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command execution failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(60 * time.Second):
|
||||||
|
t.Fatal("Deadlock detected with high parallelism")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/belphemur/CBZOptimizer/v2
|
module github.com/belphemur/CBZOptimizer/v2
|
||||||
|
|
||||||
go 1.25
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||||
@@ -8,14 +8,14 @@ require (
|
|||||||
github.com/mholt/archives v0.1.5
|
github.com/mholt/archives v0.1.5
|
||||||
github.com/oliamb/cutter v0.2.2
|
github.com/oliamb/cutter v0.2.2
|
||||||
github.com/pablodz/inotifywaitgo v0.0.9
|
github.com/pablodz/inotifywaitgo v0.0.9
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.35.1
|
||||||
github.com/samber/lo v1.52.0
|
github.com/samber/lo v1.53.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/thediveo/enumflag/v2 v2.0.7
|
github.com/thediveo/enumflag/v2 v2.2.0
|
||||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9
|
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
|
||||||
golang.org/x/image v0.34.0
|
golang.org/x/image v0.41.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -51,7 +51,7 @@ require (
|
|||||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -156,12 +156,18 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
|
|||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||||
|
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
|
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||||
|
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
|
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||||
|
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
@@ -196,6 +202,10 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/thediveo/enumflag/v2 v2.0.7 h1:uxXDU+rTel7Hg4X0xdqICpG9rzuI/mzLAEYXWLflOfs=
|
github.com/thediveo/enumflag/v2 v2.0.7 h1:uxXDU+rTel7Hg4X0xdqICpG9rzuI/mzLAEYXWLflOfs=
|
||||||
github.com/thediveo/enumflag/v2 v2.0.7/go.mod h1:bWlnNvTJuUK+huyzf3WECFLy557Ttlc+yk3o+BPs0EA=
|
github.com/thediveo/enumflag/v2 v2.0.7/go.mod h1:bWlnNvTJuUK+huyzf3WECFLy557Ttlc+yk3o+BPs0EA=
|
||||||
|
github.com/thediveo/enumflag/v2 v2.1.0 h1:F80w/h1U4B3/sBpFVUewzMVTfLk2m0D60+61UCuXSf8=
|
||||||
|
github.com/thediveo/enumflag/v2 v2.1.0/go.mod h1:wj2B0dHqqFOqIgnJ7mD8s97wK7/46oOZvDg93muD68g=
|
||||||
|
github.com/thediveo/enumflag/v2 v2.2.0 h1:T8ty4R+Pv+pYiK+lSVSk45MuyM39T/29ZZTH/6QH3Ls=
|
||||||
|
github.com/thediveo/enumflag/v2 v2.2.0/go.mod h1:3ax7ccNoUb+rnHhNepTx1vdhrlKyMUefD+Z8kvxiVg4=
|
||||||
github.com/thediveo/success v1.0.2 h1:w+r3RbSjLmd7oiNnlCblfGqItcsaShcuAorRVh/+0xk=
|
github.com/thediveo/success v1.0.2 h1:w+r3RbSjLmd7oiNnlCblfGqItcsaShcuAorRVh/+0xk=
|
||||||
github.com/thediveo/success v1.0.2/go.mod h1:hdPJB77k70w764lh8uLUZgNhgeTl3DYeZ4d4bwMO2CU=
|
github.com/thediveo/success v1.0.2/go.mod h1:hdPJB77k70w764lh8uLUZgNhgeTl3DYeZ4d4bwMO2CU=
|
||||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
@@ -225,12 +235,42 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
|
|||||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||||
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||||
|
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c=
|
||||||
|
golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8=
|
||||||
|
golang.org/x/exp v0.0.0-20260211191109-2735e65f0518 h1:2E1CW7v5QB+Wi3N+MXllOtVR6SFmI8iJM8EdzgxrgrU=
|
||||||
|
golang.org/x/exp v0.0.0-20260211191109-2735e65f0518/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||||
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||||
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||||
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
|
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
|
||||||
|
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||||
|
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||||
|
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||||
|
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||||
|
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||||
|
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
|
||||||
|
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||||
|
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||||
|
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||||
|
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||||
|
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||||
|
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||||
|
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||||
|
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||||
|
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -301,6 +341,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -313,6 +355,16 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
|
|||||||
select {
|
select {
|
||||||
case pagesChan <- manga.NewContainer(page, img, format, false):
|
case pagesChan <- manga.NewContainer(page, img, format, false):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
wgConvertedPages.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +196,7 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
|
|||||||
select {
|
select {
|
||||||
case pagesChan <- manga.NewContainer(page, img, format, true):
|
case pagesChan <- manga.NewContainer(page, img, format, true):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
wgConvertedPages.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -227,6 +229,7 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
|
|||||||
select {
|
select {
|
||||||
case pagesChan <- manga.NewContainer(newPage, img, "N/A", true):
|
case pagesChan <- manga.NewContainer(newPage, img, "N/A", true):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
wgConvertedPages.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package webp
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/gif"
|
"image/gif"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
"image/png"
|
"image/png"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "golang.org/x/image/webp"
|
_ "golang.org/x/image/webp"
|
||||||
|
|
||||||
@@ -422,3 +424,172 @@ func TestConverter_ConvertChapter_Timeout(t *testing.T) {
|
|||||||
assert.Nil(t, convertedChapter)
|
assert.Nil(t, convertedChapter)
|
||||||
assert.Equal(t, context.DeadlineExceeded, err)
|
assert.Equal(t, context.DeadlineExceeded, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConverter_ConvertChapter_ManyPages_NoDeadlock tests that converting chapters with many pages
|
||||||
|
// does not cause a deadlock. This test reproduces the scenario where processing
|
||||||
|
// many files with context cancellation could cause "all goroutines are asleep - deadlock!" error.
|
||||||
|
// The fix ensures that wgConvertedPages.Done() is called when context is cancelled after Add(1).
|
||||||
|
func TestConverter_ConvertChapter_ManyPages_NoDeadlock(t *testing.T) {
|
||||||
|
converter := New()
|
||||||
|
err := converter.PrepareConverter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a chapter with many pages to increase the chance of hitting the race condition
|
||||||
|
numPages := 50
|
||||||
|
pages := make([]*manga.Page, numPages)
|
||||||
|
for i := 0; i < numPages; i++ {
|
||||||
|
pages[i] = createTestPage(t, i+1, 100, 100, "jpeg")
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter := &manga.Chapter{
|
||||||
|
FilePath: "/test/chapter_many_pages.cbz",
|
||||||
|
Pages: pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := func(message string, current uint32, total uint32) {
|
||||||
|
// No-op progress callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run multiple iterations to increase the chance of hitting the race condition
|
||||||
|
for iteration := 0; iteration < 10; iteration++ {
|
||||||
|
t.Run(fmt.Sprintf("iteration_%d", iteration), func(t *testing.T) {
|
||||||
|
// Use a very short timeout to trigger context cancellation during processing
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// This should NOT deadlock - it should return quickly with context error
|
||||||
|
done := make(chan struct{})
|
||||||
|
var convertErr error
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
_, convertErr = converter.ConvertChapter(ctx, chapter, 80, false, progress)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait with a reasonable timeout - if it takes longer than 5 seconds, we have a deadlock
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// Expected - conversion should complete (with error) quickly
|
||||||
|
assert.Error(t, convertErr, "Expected context error")
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("Deadlock detected: ConvertChapter did not return within 5 seconds")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConverter_ConvertChapter_ManyPages_WithSplit_NoDeadlock tests that converting chapters
|
||||||
|
// with many pages and split enabled does not cause a deadlock.
|
||||||
|
func TestConverter_ConvertChapter_ManyPages_WithSplit_NoDeadlock(t *testing.T) {
|
||||||
|
converter := New()
|
||||||
|
err := converter.PrepareConverter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create pages with varying heights, some requiring splits
|
||||||
|
numPages := 30
|
||||||
|
pages := make([]*manga.Page, numPages)
|
||||||
|
for i := 0; i < numPages; i++ {
|
||||||
|
height := 1000 // Normal height
|
||||||
|
if i%5 == 0 {
|
||||||
|
height = 5000 // Tall image that will be split
|
||||||
|
}
|
||||||
|
pages[i] = createTestPage(t, i+1, 100, height, "png")
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter := &manga.Chapter{
|
||||||
|
FilePath: "/test/chapter_split_test.cbz",
|
||||||
|
Pages: pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := func(message string, current uint32, total uint32) {
|
||||||
|
// No-op progress callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run multiple iterations with short timeouts
|
||||||
|
for iteration := 0; iteration < 10; iteration++ {
|
||||||
|
t.Run(fmt.Sprintf("iteration_%d", iteration), func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
var convertErr error
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
_, convertErr = converter.ConvertChapter(ctx, chapter, 80, true, progress) // split=true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
assert.Error(t, convertErr, "Expected context error")
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("Deadlock detected: ConvertChapter with split did not return within 5 seconds")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConverter_ConvertChapter_ConcurrentChapters_NoDeadlock simulates the scenario from the
|
||||||
|
// original bug report where multiple chapters are processed in parallel with parallelism > 1.
|
||||||
|
// This test ensures no deadlock occurs when multiple goroutines are converting chapters concurrently.
|
||||||
|
func TestConverter_ConvertChapter_ConcurrentChapters_NoDeadlock(t *testing.T) {
|
||||||
|
converter := New()
|
||||||
|
err := converter.PrepareConverter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create multiple chapters, each with many pages
|
||||||
|
numChapters := 20
|
||||||
|
pagesPerChapter := 30
|
||||||
|
chapters := make([]*manga.Chapter, numChapters)
|
||||||
|
|
||||||
|
for c := 0; c < numChapters; c++ {
|
||||||
|
pages := make([]*manga.Page, pagesPerChapter)
|
||||||
|
for i := 0; i < pagesPerChapter; i++ {
|
||||||
|
pages[i] = createTestPage(t, i+1, 100, 100, "jpeg")
|
||||||
|
}
|
||||||
|
chapters[c] = &manga.Chapter{
|
||||||
|
FilePath: fmt.Sprintf("/test/chapter_%d.cbz", c+1),
|
||||||
|
Pages: pages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := func(message string, current uint32, total uint32) {}
|
||||||
|
|
||||||
|
// Process chapters concurrently with short timeouts (simulating parallelism flag)
|
||||||
|
parallelism := 4
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
semaphore := make(chan struct{}, parallelism)
|
||||||
|
|
||||||
|
// Overall test timeout
|
||||||
|
testCtx, testCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer testCancel()
|
||||||
|
|
||||||
|
for _, chapter := range chapters {
|
||||||
|
wg.Add(1)
|
||||||
|
semaphore <- struct{}{} // Acquire
|
||||||
|
|
||||||
|
go func(ch *manga.Chapter) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-semaphore }() // Release
|
||||||
|
|
||||||
|
// Use very short timeout to trigger cancellation
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// This should not deadlock
|
||||||
|
_, _ = converter.ConvertChapter(ctx, ch, 80, false, progress)
|
||||||
|
}(chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all conversions with a timeout
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// All goroutines completed successfully
|
||||||
|
case <-testCtx.Done():
|
||||||
|
t.Fatal("Deadlock detected: Concurrent chapter conversions did not complete within 30 seconds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user