Compare commits

...

40 Commits

Author SHA1 Message Date
renovate[bot] f4e2606ace chore(deps): update jetbrains/qodana-action action to v2026 2026-05-22 19:11:50 +00:00
renovate[bot] d78b523ae7 fix(deps): update module golang.org/x/image to v0.41.0 (#190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-21 22:44:17 +00:00
renovate[bot] 7b1fb13a21 fix(deps): update module golang.org/x/image to v0.40.0 (#189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-10 17:20:37 +00:00
renovate[bot] 414a43476b fix(deps): update golang.org/x/exp digest to 74f9aab (#188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-10 15:59:04 +00:00
renovate[bot] dbac6c20ff chore(deps): update jetbrains/qodana-action action to v2024.3.4 (#185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 19:41:03 +00:00
renovate[bot] ef1a658028 fix(deps): update module github.com/rs/zerolog to v1.35.1 (#184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 00:41:29 +00:00
renovate[bot] 76aea38ff2 fix(deps): update golang.org/x/exp digest to 746e56f (#183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 12:47:57 +00:00
renovate[bot] a69eb9d8a5 fix(deps): update module golang.org/x/image to v0.39.0 (#182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 19:04:44 +00:00
renovate[bot] a6875c3a12 chore(deps): update codecov/codecov-action action to v6 (#180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 06:57:32 -04:00
renovate[bot] b3686e17d8 fix(deps): update module github.com/rs/zerolog to v1.35.0 (#181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 00:34:43 +00:00
renovate[bot] 6ea989eedd fix(deps): update module github.com/thediveo/enumflag/v2 to v2.2.0 (#179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 21:07:58 +00:00
renovate[bot] f104add37a fix(deps): update module golang.org/x/image to v0.38.0 (#178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 00:52:00 +00:00
renovate[bot] 39632906da chore(deps): update anchore/sbom-action action to v0.24.0 (#177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 21:47:22 +00:00
renovate[bot] 1be0978e89 fix(deps): update golang.org/x/exp digest to 7ab1446 (#176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 17:52:26 +00:00
renovate[bot] 71bdf7d111 fix(deps): update module golang.org/x/image to v0.37.0 (#175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 00:57:41 +00:00
renovate[bot] 3123fa27f9 chore(deps): update anchore/sbom-action action to v0.23.1 (#174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 22:45:16 +00:00
renovate[bot] 7cd711ab5a chore(deps): update docker/setup-qemu-action action to v4 (#171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 18:08:17 -05:00
renovate[bot] df3206b80c chore(deps): update docker/login-action action to v4 (#172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 18:08:06 -05:00
renovate[bot] 878da22fdb chore(deps): update docker/setup-buildx-action action to v4 (#173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 18:07:54 -05:00
renovate[bot] fcf6bf514b fix(deps): update module github.com/samber/lo to v1.53.0 (#170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 16:42:49 +00:00
renovate[bot] 70a3306ca9 chore(deps): update actions/upload-artifact action to v7 (#169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 19:51:10 -05:00
renovate[bot] 0ff3eab0b9 chore(deps): update actions/attest-build-provenance action to v4 (#168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 22:15:51 -05:00
renovate[bot] 45851713c1 chore(deps): update anchore/sbom-action action to v0.23.0 (#167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 20:47:24 +00:00
renovate[bot] cca06642f8 chore(deps): update goreleaser/goreleaser-action action to v7 (#166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-21 12:35:12 -05:00
renovate[bot] a0a79c6439 fix(deps): update golang.org/x/exp digest to 3dfff04 (#165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 22:53:19 +00:00
renovate[bot] 6c45a8507f fix(deps): update golang.org/x/exp digest to 81e46e3 (#164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 20:03:41 +00:00
renovate[bot] d66d445bbb fix(deps): update golang.org/x/exp digest to 2735e65 (#163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 21:33:26 +00:00
renovate[bot] aabb0e2a02 fix(deps): update golang.org/x/exp digest to 2842357 (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 21:32:38 +00:00
renovate[bot] 8349569e33 fix(deps): update module golang.org/x/image to v0.36.0 (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 18:18:25 +00:00
renovate[bot] b434a18802 chore(deps): update anchore/sbom-action action to v0.22.2 (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 18:43:13 +00:00
renovate[bot] 4057cfb6c9 chore(deps): update anchore/sbom-action action to v0.22.1 (#159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 19:38:33 +00:00
renovate[bot] 617ee8a0c3 chore(deps): update anchore/sbom-action action to v0.22.0 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 21:04:48 +00:00
renovate[bot] f37ca7e4e7 fix(deps): update golang.org/x/exp digest to 716be56 (#157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 21:33:22 +00:00
renovate[bot] 38c8f2a9cb fix(deps): update module golang.org/x/image to v0.35.0 (#156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 18:35:03 +00:00
renovate[bot] 03479c8772 chore(deps): update anchore/sbom-action action to v0.21.1 (#155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 17:14:58 +00:00
renovate[bot] ee47432721 fix(deps): update module github.com/thediveo/enumflag/v2 to v2.1.0 (#154)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 17:42:02 +00:00
renovate[bot] 5a0fe68e68 chore(deps): update anchore/sbom-action action to v0.21.0 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 17:47:47 +00:00
Copilot e535809e74 fix: Fix deadlock in ConvertChapter when context cancelled during page processing (#152)
* Initial plan

* Fix deadlock in ConvertChapter when context is cancelled after wgConvertedPages.Add

Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>

* Fix test comments to remove placeholder issue references

Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
2025-12-20 14:26:33 -05:00
renovate[bot] af5bfe8000 fix(deps): update golang.org/x/exp digest to 944ab1f (#151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 21:52:44 +00:00
renovate[bot] 9ac9901990 chore(deps): update actions/upload-artifact action to v6 (#149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-14 14:31:21 -05:00
8 changed files with 445 additions and 20 deletions
+1 -1
View File
@@ -18,6 +18,6 @@ jobs:
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
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2024.1
uses: JetBrains/qodana-action@v2026.1.0
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
+7 -7
View File
@@ -29,19 +29,19 @@ jobs:
go-version-file: go.mod
cache: true
- 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
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v3 # login to ghcr
uses: docker/login-action@v4 # login to ghcr
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 # run goreleaser
uses: goreleaser/goreleaser-action@v7 # run goreleaser
with:
version: nightly
args: release --clean --verbose
@@ -49,11 +49,11 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# After GoReleaser runs, attest all the files in ./dist/checksums.txt:
- name: Attest Build Provenance for Archives
uses: actions/attest-build-provenance@v3
uses: actions/attest-build-provenance@v4
with:
subject-checksums: ./dist/checksums.txt
# After GoReleaser runs, attest all the images in ./dist/digests.txt:
- name: Attest Build Provenance for Docker Images
uses: actions/attest-build-provenance@v3
uses: actions/attest-build-provenance@v4
with:
subject-checksums: ./dist/digests.txt
+2 -2
View File
@@ -31,7 +31,7 @@ jobs:
- name: Upload test result artifact
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: test-results
path: |
@@ -46,6 +46,6 @@ jobs:
files: test-results/junit.xml
- name: Upload coverage reports to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
@@ -2,6 +2,7 @@ package commands
import (
"context"
"fmt"
"log"
"os"
"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")
}
}
+8 -8
View File
@@ -1,6 +1,6 @@
module github.com/belphemur/CBZOptimizer/v2
go 1.25
go 1.25.0
require (
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
@@ -8,14 +8,14 @@ require (
github.com/mholt/archives v0.1.5
github.com/oliamb/cutter v0.2.2
github.com/pablodz/inotifywaitgo v0.0.9
github.com/rs/zerolog v1.34.0
github.com/samber/lo v1.52.0
github.com/rs/zerolog v1.35.1
github.com/samber/lo v1.53.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/thediveo/enumflag/v2 v2.0.7
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9
golang.org/x/image v0.34.0
github.com/thediveo/enumflag/v2 v2.2.0
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
golang.org/x/image v0.41.0
)
require (
@@ -51,7 +51,7 @@ require (
github.com/ulikunitz/xz v0.5.15 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+54 -2
View File
@@ -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/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
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/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/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
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.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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
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/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.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/go.mod h1:hdPJB77k70w764lh8uLUZgNhgeTl3DYeZ4d4bwMO2CU=
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-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-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
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-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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.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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+3
View File
@@ -184,6 +184,7 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
select {
case pagesChan <- manga.NewContainer(page, img, format, false):
case <-ctx.Done():
wgConvertedPages.Done()
return
}
@@ -195,6 +196,7 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
select {
case pagesChan <- manga.NewContainer(page, img, format, true):
case <-ctx.Done():
wgConvertedPages.Done()
return
}
return
@@ -227,6 +229,7 @@ func (converter *Converter) ConvertChapter(ctx context.Context, chapter *manga.C
select {
case pagesChan <- manga.NewContainer(newPage, img, "N/A", true):
case <-ctx.Done():
wgConvertedPages.Done()
return
}
}
+171
View File
@@ -3,6 +3,7 @@ package webp
import (
"bytes"
"context"
"fmt"
"image"
"image/color"
"image/gif"
@@ -10,6 +11,7 @@ import (
"image/png"
"sync"
"testing"
"time"
_ "golang.org/x/image/webp"
@@ -422,3 +424,172 @@ func TestConverter_ConvertChapter_Timeout(t *testing.T) {
assert.Nil(t, convertedChapter)
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")
}
}