From 67673ebe7920f5df9b354cc7184b061e85a751ff Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:13:21 +0800
Subject: [PATCH 01/12] chore(cargo): add repository and homepage fields to
workspace configuration in CLI and core Cargo.toml files
---
crates/cli/Cargo.toml | 2 ++
crates/core/Cargo.toml | 2 ++
2 files changed, 4 insertions(+)
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index e0cd461..fdad842 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -4,6 +4,8 @@ version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
+repository.workspace = true
+homepage.workspace = true
[[bin]]
name = "forgekit"
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 93f9d36..f063ac5 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -4,6 +4,8 @@ version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
+repository.workspace = true
+homepage.workspace = true
[dependencies]
anyhow = { workspace = true }
From f9c8c548cf8850598728db086deeec13b0dbc4ad Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:13:47 +0800
Subject: [PATCH 02/12] feat(chocolatey): add Chocolatey package configuration
and installation scripts for ForgeKit
---
packaging/chocolatey/forgekit.nuspec | 29 +++++++++++++
.../chocolatey/tools/chocolateyinstall.ps1 | 41 +++++++++++++++++++
.../chocolatey/tools/chocolateyuninstall.ps1 | 13 ++++++
3 files changed, 83 insertions(+)
create mode 100644 packaging/chocolatey/forgekit.nuspec
create mode 100644 packaging/chocolatey/tools/chocolateyinstall.ps1
create mode 100644 packaging/chocolatey/tools/chocolateyuninstall.ps1
diff --git a/packaging/chocolatey/forgekit.nuspec b/packaging/chocolatey/forgekit.nuspec
new file mode 100644
index 0000000..2f05569
--- /dev/null
+++ b/packaging/chocolatey/forgekit.nuspec
@@ -0,0 +1,29 @@
+
+
+
+ forgekit
+ 0.0.9
+ ForgeKit
+ ForgeKit Contributors
+ ForgeKit Contributors
+ https://opensource.org/licenses/MIT
+ https://github.com/nedanwr/forgekit
+ false
+ Local-first media and PDF toolkit. Fast, privacy-focused CLI for PDF, image, audio, and video operations. All processing runs locally - no data leaves your device.
+ CLI toolkit for PDF, image, audio, and video operations
+ pdf media cli video audio image toolkit ffmpeg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packaging/chocolatey/tools/chocolateyinstall.ps1 b/packaging/chocolatey/tools/chocolateyinstall.ps1
new file mode 100644
index 0000000..4547f71
--- /dev/null
+++ b/packaging/chocolatey/tools/chocolateyinstall.ps1
@@ -0,0 +1,41 @@
+$ErrorActionPreference = 'Stop'
+
+$packageName = 'forgekit'
+$version = '0.0.9'
+$url64 = "https://github.com/nedanwr/forgekit/releases/download/v$version/forgekit-v$version-x86_64-pc-windows-msvc.zip"
+$checksum64 = 'PLACEHOLDER_SHA256'
+
+$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
+
+# Download and extract
+$packageArgs = @{
+ packageName = $packageName
+ unzipLocation = $toolsDir
+ url64bit = $url64
+ checksum64 = $checksum64
+ checksumType64= 'sha256'
+}
+Install-ChocolateyZipPackage @packageArgs
+
+# Install ocrmypdf via pip (dependencies already installed by Chocolatey)
+Write-Host "Installing ocrmypdf Python package..." -ForegroundColor Cyan
+try {
+ & python -m pip install --quiet --upgrade pip 2>$null
+ & python -m pip install --quiet ocrmypdf 2>$null
+ Write-Host "Successfully installed ocrmypdf" -ForegroundColor Green
+} catch {
+ Write-Warning "Could not install ocrmypdf. Run manually: pip install ocrmypdf"
+}
+
+# Install libvips via scoop if available (not in Chocolatey)
+if (Get-Command scoop -ErrorAction SilentlyContinue) {
+ Write-Host "Installing libvips via scoop..." -ForegroundColor Cyan
+ & scoop install libvips 2>$null
+} else {
+ Write-Warning "libvips not installed (requires Scoop). Image operations may be limited."
+ Write-Warning "To install: Install Scoop (scoop.sh), then run: scoop install libvips"
+}
+
+Write-Host ""
+Write-Host "ForgeKit installed successfully!" -ForegroundColor Green
+Write-Host "Run 'forgekit check-deps' to verify all dependencies." -ForegroundColor Gray
diff --git a/packaging/chocolatey/tools/chocolateyuninstall.ps1 b/packaging/chocolatey/tools/chocolateyuninstall.ps1
new file mode 100644
index 0000000..a2ee8d8
--- /dev/null
+++ b/packaging/chocolatey/tools/chocolateyuninstall.ps1
@@ -0,0 +1,13 @@
+$ErrorActionPreference = 'Stop'
+
+$packageName = 'forgekit'
+$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
+
+# Remove the binary
+$exePath = Join-Path $toolsDir 'forgekit.exe'
+if (Test-Path $exePath) {
+ Remove-Item $exePath -Force
+}
+
+Write-Host "ForgeKit uninstalled." -ForegroundColor Green
+Write-Host "Note: Dependencies (qpdf, ffmpeg, etc.) were not removed." -ForegroundColor Gray
From 15b4e03265597531d66c1c7e9902faaa80ff8595 Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:14:02 +0800
Subject: [PATCH 03/12] chore(debian): update package version of `forgekit`
from `0.0.3` to `0.0.9`
---
packaging/debian/control | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packaging/debian/control b/packaging/debian/control
index 313b5d4..39059a7 100644
--- a/packaging/debian/control
+++ b/packaging/debian/control
@@ -3,7 +3,7 @@
# When users install forgekit.deb, these dependencies will be automatically installed
Package: forgekit
-Version: 0.0.3
+Version: 0.0.9
Section: utils
Priority: optional
Architecture: amd64
From bebb08f3577660ef37d164b7f840db71944ac96d Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:14:13 +0800
Subject: [PATCH 04/12] chore(homebrew): update `forgekit` homepage and package
URL to version `0.0.9`
---
packaging/homebrew/forgekit.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packaging/homebrew/forgekit.rb b/packaging/homebrew/forgekit.rb
index 859141a..4d4ad49 100644
--- a/packaging/homebrew/forgekit.rb
+++ b/packaging/homebrew/forgekit.rb
@@ -4,8 +4,8 @@
class Forgekit < Formula
desc "Local-first media and PDF toolkit"
- homepage "https://github.com/nedanwar/forgekit"
- url "https://github.com/nedanwar/forgekit/releases/download/v0.0.3/forgekit-0.0.3.tar.gz"
+ homepage "https://github.com/nedanwr/forgekit"
+ url "https://github.com/nedanwr/forgekit/releases/download/v0.0.9/forgekit-0.0.9-x86_64-apple-darwin.tar.gz"
sha256 "PLACEHOLDER_SHA256"
license "MIT"
From 7f846a818afcc9cb1ad975cc071c766833265fa1 Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:14:37 +0800
Subject: [PATCH 05/12] chore(rpm): update `forgekit` version to `0.0.9`,
modify homepage URL
---
packaging/rpm/forgekit.spec | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/packaging/rpm/forgekit.spec b/packaging/rpm/forgekit.spec
index 89144af..0e36afb 100644
--- a/packaging/rpm/forgekit.spec
+++ b/packaging/rpm/forgekit.spec
@@ -3,11 +3,11 @@
# When users install forgekit.rpm, these dependencies will be automatically installed
Name: forgekit
-Version: 0.0.3
+Version: 0.0.9
Release: 1%{?dist}
Summary: Local-first media and PDF toolkit
License: MIT
-URL: https://github.com/nedanwar/forgekit
+URL: https://github.com/nedanwr/forgekit
Source0: %{name}-%{version}.tar.gz
BuildRequires: rust
@@ -49,6 +49,12 @@ fi
%{_bindir}/forgekit
%changelog
+* Fri Jan 2026 ForgeKit Contributors - 0.0.9-1
+- Added GitHub Actions release workflow
+- Added Dockerfile for containerized distribution
+- Added Makefile for common tasks
+- Updated Cargo.toml with crates.io publishing fields
+
* Wed Dec 2025 ForgeKit Contributors - 0.0.3-1
- Initial RPM package
From 3cdfdeddb082f1c59fe331f57708b48189298a5c Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:14:49 +0800
Subject: [PATCH 06/12] chore(winget): update `forgekit` version to `0.0.9`,
modify installer type to zip, and enhance post-installation script for
dependency management
---
packaging/winget/forgekit.yaml | 29 +++++++--------
packaging/winget/postinstall.ps1 | 60 ++++++++++++++++++++++++++------
2 files changed, 65 insertions(+), 24 deletions(-)
diff --git a/packaging/winget/forgekit.yaml b/packaging/winget/forgekit.yaml
index a52c716..b3d9de8 100644
--- a/packaging/winget/forgekit.yaml
+++ b/packaging/winget/forgekit.yaml
@@ -1,32 +1,33 @@
# winget manifest for ForgeKit
-# This file defines dependencies for Windows installation
-# When users install via `winget install forgekit`, these dependencies will be automatically installed
+# Note: winget does not auto-install dependencies. Users must install them manually.
+# See postinstall.ps1 for dependency installation commands.
Id: forgekit.forgekit
-Version: 0.0.3
+Version: 0.0.9
Name: ForgeKit
Publisher: ForgeKit Contributors
Description: Local-first media and PDF toolkit. Fast, privacy-focused CLI for PDF, image, audio, and video operations.
License: MIT
LicenseUrl: https://opensource.org/licenses/MIT
-Homepage: https://github.com/nedanwar/forgekit
+Homepage: https://github.com/nedanwr/forgekit
Tags:
- pdf
- media
- cli
- toolkit
-InstallerType: exe
+InstallerType: zip
Installers:
- Architecture: x64
- InstallerUrl: https://github.com/nedanwar/forgekit/releases/download/v0.0.3/forgekit-0.0.3-x64.exe
+ InstallerUrl: https://github.com/nedanwr/forgekit/releases/download/v0.0.9/forgekit-v0.0.9-x86_64-pc-windows-msvc.zip
InstallerSha256: PLACEHOLDER_SHA256
ManifestType: version
-Dependencies:
- - PackageIdentifier: qpdf.qpdf
- - PackageIdentifier: ArtifexSoftware.GhostScript
- - PackageIdentifier: tesseract-ocr
- - PackageIdentifier: Gyan.FFmpeg
- - PackageIdentifier: libvips.libvips
- - PackageIdentifier: exiftool.exiftool
- - PackageIdentifier: Python.Python.3
+
+# Dependencies (informational - must be installed manually):
+# winget install qpdf.qpdf
+# winget install ArtifexSoftware.GhostScript
+# winget install UB-Mannheim.TesseractOCR
+# winget install Gyan.FFmpeg
+# winget install Python.Python.3
+# scoop install libvips exiftool (not available in winget)
+# pip install ocrmypdf
diff --git a/packaging/winget/postinstall.ps1 b/packaging/winget/postinstall.ps1
index e26949d..54cfd9c 100644
--- a/packaging/winget/postinstall.ps1
+++ b/packaging/winget/postinstall.ps1
@@ -1,15 +1,55 @@
-# Post-installation script for ForgeKit winget package
-# Automatically installs ocrmypdf Python package
+# ForgeKit Windows Dependency Installer
+# Run this script after installing ForgeKit to install all required dependencies.
+# Usage: .\postinstall.ps1
-# Check if Python and pip are available
+Write-Host "ForgeKit Dependency Installer" -ForegroundColor Green
+Write-Host "==============================" -ForegroundColor Green
+Write-Host ""
+
+# Check for winget
+if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
+ Write-Host "Error: winget not found. Please install App Installer from Microsoft Store." -ForegroundColor Red
+ exit 1
+}
+
+# Install winget packages
+Write-Host "Installing winget packages..." -ForegroundColor Cyan
+$wingetPackages = @(
+ "qpdf.qpdf",
+ "ArtifexSoftware.GhostScript",
+ "UB-Mannheim.TesseractOCR",
+ "Gyan.FFmpeg",
+ "Python.Python.3"
+)
+
+foreach ($pkg in $wingetPackages) {
+ Write-Host " Installing $pkg..." -ForegroundColor Yellow
+ winget install --id $pkg --silent --accept-package-agreements --accept-source-agreements 2>$null
+}
+
+# Check for scoop (needed for libvips and exiftool)
+if (Get-Command scoop -ErrorAction SilentlyContinue) {
+ Write-Host "Installing scoop packages..." -ForegroundColor Cyan
+ scoop install libvips exiftool 2>$null
+} else {
+ Write-Host ""
+ Write-Host "Note: Scoop not found. For full functionality, install Scoop and run:" -ForegroundColor Yellow
+ Write-Host " scoop install libvips exiftool" -ForegroundColor White
+ Write-Host ""
+ Write-Host "To install Scoop: https://scoop.sh" -ForegroundColor Gray
+}
+
+# Install ocrmypdf via pip
+Write-Host "Installing Python packages..." -ForegroundColor Cyan
if (Get-Command python -ErrorAction SilentlyContinue) {
- Write-Host "Installing Python dependencies for ForgeKit..." -ForegroundColor Cyan
+ python -m pip install --quiet --upgrade pip 2>$null
python -m pip install --quiet ocrmypdf 2>$null
-} elseif (Get-Command python3 -ErrorAction SilentlyContinue) {
- Write-Host "Installing Python dependencies for ForgeKit..." -ForegroundColor Cyan
- python3 -m pip install --quiet ocrmypdf 2>$null
-} elseif (Get-Command pip -ErrorAction SilentlyContinue) {
- Write-Host "Installing Python dependencies for ForgeKit..." -ForegroundColor Cyan
- pip install --quiet ocrmypdf 2>$null
+ Write-Host " Installed ocrmypdf" -ForegroundColor Yellow
+} else {
+ Write-Host " Warning: Python not found. Restart terminal and run: pip install ocrmypdf" -ForegroundColor Yellow
}
+Write-Host ""
+Write-Host "Installation complete!" -ForegroundColor Green
+Write-Host "Run 'forgekit check-deps' to verify all dependencies." -ForegroundColor Gray
+
From d4e16eb2a9b2fb10d4fb9125bdd26337001a7803 Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:14:56 +0800
Subject: [PATCH 07/12] chore(ci): add GitHub Actions workflow for automated
release process, including build, packaging, and artifact upload
---
.github/workflows/release.yml | 101 ++++++++++++++++++++++++++++++++++
1 file changed, 101 insertions(+)
create mode 100644 .github/workflows/release.yml
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..0bae2a3
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,101 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ build:
+ name: Build (${{ matrix.target }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - target: x86_64-unknown-linux-gnu
+ os: ubuntu-latest
+ archive: tar.gz
+ - target: x86_64-apple-darwin
+ os: macos-latest
+ archive: tar.gz
+ - target: aarch64-apple-darwin
+ os: macos-latest
+ archive: tar.gz
+ - target: x86_64-pc-windows-msvc
+ os: windows-latest
+ archive: zip
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: ${{ matrix.target }}
+
+ - name: Cache Rust dependencies
+ uses: Swatinem/rust-cache@v2
+
+ - name: Build
+ run: cargo build --release --target ${{ matrix.target }}
+
+ - name: Package (Unix)
+ if: matrix.os != 'windows-latest'
+ run: |
+ cd target/${{ matrix.target }}/release
+ tar -czvf ../../../forgekit-${{ github.ref_name }}-${{ matrix.target }}.tar.gz forgekit
+ cd ../../..
+
+ - name: Package (Windows)
+ if: matrix.os == 'windows-latest'
+ run: |
+ cd target/${{ matrix.target }}/release
+ 7z a ../../../forgekit-${{ github.ref_name }}-${{ matrix.target }}.zip forgekit.exe
+ cd ../../..
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: forgekit-${{ matrix.target }}
+ path: forgekit-${{ github.ref_name }}-${{ matrix.target }}.*
+
+ release:
+ name: Create Release
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+
+ - name: Create checksums
+ run: |
+ cd artifacts
+ find . -type f \( -name "*.tar.gz" -o -name "*.zip" \) -exec mv {} . \;
+ sha256sum forgekit-* > checksums.txt
+ cat checksums.txt
+
+ - name: Create Release
+ uses: softprops/action-gh-release@v1
+ with:
+ draft: false
+ prerelease: ${{ contains(github.ref_name, '-') }}
+ generate_release_notes: true
+ files: |
+ artifacts/forgekit-*
+ artifacts/checksums.txt
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
From 0d7cbde9f9be17f4fa05f6d1ac1ef436d801e6ce Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:15:07 +0800
Subject: [PATCH 08/12] chore: update `forgekit` and `forgekit-core` versions
to `0.0.9`, modify repository and homepage URLs
---
Cargo.lock | 4 ++--
Cargo.toml | 5 +++--
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 0eee29e..a7ed27e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -124,7 +124,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "forgekit"
-version = "0.0.8"
+version = "0.0.9"
dependencies = [
"anyhow",
"clap",
@@ -135,7 +135,7 @@ dependencies = [
[[package]]
name = "forgekit-core"
-version = "0.0.8"
+version = "0.0.9"
dependencies = [
"anyhow",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index bd3d035..a4b5c43 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,11 +3,12 @@ members = ["crates/core", "crates/cli"]
resolver = "2"
[workspace.package]
-version = "0.0.8"
+version = "0.0.9"
edition = "2021"
authors = ["ForgeKit Contributors"]
license = "MIT"
-repository = "https://github.com/nedanwar/forgekit"
+repository = "https://github.com/nedanwr/forgekit"
+homepage = "https://github.com/nedanwr/forgekit"
[workspace.dependencies]
anyhow = "1.0"
From 7557aa21573783506645d95cde38ec46fc9b2dbc Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:28:01 +0800
Subject: [PATCH 09/12] test(cli): add comprehensive unit tests for audio and
video commands, including time parsing, gain parsing, duration formatting,
and validation checks
---
crates/cli/src/commands/audio.rs | 170 +++++++++++++++++++++++
crates/cli/src/commands/video.rs | 226 +++++++++++++++++++++++++++++++
2 files changed, 396 insertions(+)
diff --git a/crates/cli/src/commands/audio.rs b/crates/cli/src/commands/audio.rs
index 55b922b..10a8e94 100644
--- a/crates/cli/src/commands/audio.rs
+++ b/crates/cli/src/commands/audio.rs
@@ -741,3 +741,173 @@ fn format_duration(seconds: f64) -> String {
format!("{}:{:02}", minutes, secs)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // Time parsing tests
+ #[test]
+ fn test_parse_time_seconds() {
+ assert_eq!(parse_time("30").unwrap(), 30.0);
+ assert_eq!(parse_time("90.5").unwrap(), 90.5);
+ assert_eq!(parse_time("0").unwrap(), 0.0);
+ }
+
+ #[test]
+ fn test_parse_time_mm_ss() {
+ assert_eq!(parse_time("1:30").unwrap(), 90.0);
+ assert_eq!(parse_time("2:00").unwrap(), 120.0);
+ assert_eq!(parse_time("0:45").unwrap(), 45.0);
+ }
+
+ #[test]
+ fn test_parse_time_hh_mm_ss() {
+ assert_eq!(parse_time("1:30:00").unwrap(), 5400.0);
+ assert_eq!(parse_time("0:01:30").unwrap(), 90.0);
+ assert_eq!(parse_time("2:00:00").unwrap(), 7200.0);
+ }
+
+ #[test]
+ fn test_parse_time_invalid() {
+ assert!(parse_time("invalid").is_err());
+ assert!(parse_time("1:2:3:4").is_err());
+ assert!(parse_time("abc:def").is_err());
+ }
+
+ // Gain parsing tests
+ #[test]
+ fn test_parse_gain_positive() {
+ assert_eq!(parse_gain("+6").unwrap(), 6.0);
+ assert_eq!(parse_gain("6").unwrap(), 6.0);
+ assert_eq!(parse_gain("6dB").unwrap(), 6.0);
+ assert_eq!(parse_gain("+6dB").unwrap(), 6.0);
+ }
+
+ #[test]
+ fn test_parse_gain_negative() {
+ assert_eq!(parse_gain("-3").unwrap(), -3.0);
+ assert_eq!(parse_gain("-3dB").unwrap(), -3.0);
+ assert_eq!(parse_gain("-10db").unwrap(), -10.0);
+ }
+
+ #[test]
+ fn test_parse_gain_invalid() {
+ assert!(parse_gain("invalid").is_err());
+ assert!(parse_gain("abc").is_err());
+ }
+
+ // Duration formatting tests
+ #[test]
+ fn test_format_duration_minutes() {
+ assert_eq!(format_duration(90.0), "1:30");
+ assert_eq!(format_duration(0.0), "0:00");
+ assert_eq!(format_duration(59.0), "0:59");
+ }
+
+ #[test]
+ fn test_format_duration_hours() {
+ assert_eq!(format_duration(3600.0), "1:00:00");
+ assert_eq!(format_duration(5400.0), "1:30:00");
+ assert_eq!(format_duration(7265.0), "2:01:05");
+ }
+
+ // Validation tests
+ #[test]
+ fn test_convert_requires_output_or_to() {
+ let args = ConvertArgs {
+ input: PathBuf::from("input.wav"),
+ output: None,
+ to: None,
+ bitrate: None,
+ };
+ let result = handle_convert(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("Either --output or --to is required"));
+ }
+
+ #[test]
+ fn test_convert_invalid_format() {
+ let args = ConvertArgs {
+ input: PathBuf::from("input.wav"),
+ output: None,
+ to: Some("invalid".to_string()),
+ bitrate: None,
+ };
+ let result = handle_convert(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("Unknown format"));
+ }
+
+ #[test]
+ fn test_normalize_invalid_target() {
+ let args = NormalizeArgs {
+ input: PathBuf::from("input.wav"),
+ output: None,
+ target: "invalid_target".to_string(),
+ lufs: None,
+ };
+ let result = handle_normalize(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("Unknown target"));
+ }
+
+ #[test]
+ fn test_normalize_valid_targets() {
+ for target in ["ebu-r128", "ebu", "broadcast", "streaming", "stream"] {
+ let args = NormalizeArgs {
+ input: PathBuf::from("input.wav"),
+ output: Some(PathBuf::from("output.wav")),
+ target: target.to_string(),
+ lufs: None,
+ };
+ let result = handle_normalize(&args, true);
+ // Should not fail on target validation
+ assert!(result.is_ok() || !result.unwrap_err().to_string().contains("Unknown target"));
+ }
+ }
+
+ #[test]
+ fn test_normalize_custom_lufs() {
+ let args = NormalizeArgs {
+ input: PathBuf::from("input.wav"),
+ output: Some(PathBuf::from("output.wav")),
+ target: "ebu-r128".to_string(), // ignored when lufs is set
+ lufs: Some(-16.0),
+ };
+ let result = handle_normalize(&args, true);
+ // Custom LUFS should work
+ assert!(result.is_ok() || !result.unwrap_err().to_string().contains("Unknown target"));
+ }
+
+ #[test]
+ fn test_trim_requires_start_or_end() {
+ let args = TrimArgs {
+ input: PathBuf::from("input.mp3"),
+ output: PathBuf::from("output.mp3"),
+ start: None,
+ end: None,
+ };
+ let result = handle_trim(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("At least one of --start or --end"));
+ }
+
+ #[test]
+ fn test_extract_requires_output_or_to() {
+ let args = ExtractArgs {
+ input: PathBuf::from("video.mp4"),
+ output: None,
+ to: None,
+ bitrate: None,
+ };
+ let result = handle_extract(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("Either --output or --to is required"));
+ }
+}
diff --git a/crates/cli/src/commands/video.rs b/crates/cli/src/commands/video.rs
index 74653f5..a9c7906 100644
--- a/crates/cli/src/commands/video.rs
+++ b/crates/cli/src/commands/video.rs
@@ -892,3 +892,229 @@ fn handle_stitch(args: &StitchArgs, plan_only: bool) -> Result<()> {
println!("{}", result);
Ok(())
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // Time parsing tests
+ #[test]
+ fn test_parse_time_seconds() {
+ assert_eq!(parse_time("30").unwrap(), 30.0);
+ assert_eq!(parse_time("90.5").unwrap(), 90.5);
+ assert_eq!(parse_time("0").unwrap(), 0.0);
+ }
+
+ #[test]
+ fn test_parse_time_mm_ss() {
+ assert_eq!(parse_time("1:30").unwrap(), 90.0);
+ assert_eq!(parse_time("2:00").unwrap(), 120.0);
+ assert_eq!(parse_time("0:45").unwrap(), 45.0);
+ }
+
+ #[test]
+ fn test_parse_time_hh_mm_ss() {
+ assert_eq!(parse_time("1:30:00").unwrap(), 5400.0);
+ assert_eq!(parse_time("0:01:30").unwrap(), 90.0);
+ assert_eq!(parse_time("2:00:00").unwrap(), 7200.0);
+ }
+
+ #[test]
+ fn test_parse_time_invalid() {
+ assert!(parse_time("invalid").is_err());
+ assert!(parse_time("1:2:3:4").is_err());
+ assert!(parse_time("abc:def").is_err());
+ }
+
+ // Duration formatting tests
+ #[test]
+ fn test_format_duration_minutes() {
+ assert_eq!(format_duration(90.0), "1:30");
+ assert_eq!(format_duration(0.0), "0:00");
+ assert_eq!(format_duration(59.0), "0:59");
+ }
+
+ #[test]
+ fn test_format_duration_hours() {
+ assert_eq!(format_duration(3600.0), "1:00:00");
+ assert_eq!(format_duration(5400.0), "1:30:00");
+ assert_eq!(format_duration(7265.0), "2:01:05");
+ }
+
+ // Natural sorting tests
+ #[test]
+ fn test_natural_sort_key_numbers() {
+ let mut files = vec![
+ PathBuf::from("frame_10.png"),
+ PathBuf::from("frame_2.png"),
+ PathBuf::from("frame_1.png"),
+ ];
+ files.sort_by_key(|a| natural_sort_key(a));
+ assert_eq!(files[0], PathBuf::from("frame_1.png"));
+ assert_eq!(files[1], PathBuf::from("frame_2.png"));
+ assert_eq!(files[2], PathBuf::from("frame_10.png"));
+ }
+
+ #[test]
+ fn test_natural_sort_key_mixed() {
+ let mut files = vec![
+ PathBuf::from("part_100.mp4"),
+ PathBuf::from("part_20.mp4"),
+ PathBuf::from("part_3.mp4"),
+ ];
+ files.sort_by_key(|a| natural_sort_key(a));
+ assert_eq!(files[0], PathBuf::from("part_3.mp4"));
+ assert_eq!(files[1], PathBuf::from("part_20.mp4"));
+ assert_eq!(files[2], PathBuf::from("part_100.mp4"));
+ }
+
+ // Validation tests
+ #[test]
+ fn test_transcode_crf_validation() {
+ let args = TranscodeArgs {
+ input: PathBuf::from("input.mp4"),
+ output: PathBuf::from("output.mp4"),
+ crf: 52, // Invalid - max is 51
+ preset: "medium".to_string(),
+ scale: None,
+ reencode_audio: false,
+ };
+ let result = handle_transcode(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("CRF must be 0-51"));
+ }
+
+ #[test]
+ fn test_transcode_preset_validation() {
+ let args = TranscodeArgs {
+ input: PathBuf::from("input.mp4"),
+ output: PathBuf::from("output.mp4"),
+ crf: 23,
+ preset: "invalid_preset".to_string(),
+ scale: None,
+ reencode_audio: false,
+ };
+ let result = handle_transcode(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("Invalid preset"));
+ }
+
+ #[test]
+ fn test_trim_requires_start_or_end() {
+ let args = TrimArgs {
+ input: PathBuf::from("input.mp4"),
+ output: PathBuf::from("output.mp4"),
+ start: None,
+ end: None,
+ };
+ let result = handle_trim(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("At least one of --start or --end"));
+ }
+
+ #[test]
+ fn test_speed_must_be_positive() {
+ let args = SpeedArgs {
+ input: PathBuf::from("input.mp4"),
+ output: PathBuf::from("output.mp4"),
+ speed: 0.0,
+ };
+ let result = handle_speed(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("Speed must be greater than 0"));
+ }
+
+ #[test]
+ fn test_speed_negative() {
+ let args = SpeedArgs {
+ input: PathBuf::from("input.mp4"),
+ output: PathBuf::from("output.mp4"),
+ speed: -1.0,
+ };
+ let result = handle_speed(&args, true);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_rotate_valid_angles() {
+ // 90, 180, 270 are valid
+ for angle in [90, 180, 270] {
+ let args = RotateArgs {
+ input: PathBuf::from("input.mp4"),
+ output: PathBuf::from("output.mp4"),
+ degrees: angle,
+ };
+ // Will fail because input doesn't exist, but validation passes
+ let result = handle_rotate(&args, true);
+ // In plan mode, it should generate a plan (may fail due to missing file)
+ assert!(result.is_ok() || !result.unwrap_err().to_string().contains("Invalid rotation"));
+ }
+ }
+
+ #[test]
+ fn test_rotate_invalid_angle() {
+ let args = RotateArgs {
+ input: PathBuf::from("input.mp4"),
+ output: PathBuf::from("output.mp4"),
+ degrees: 45, // Invalid
+ };
+ let result = handle_rotate(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("Invalid rotation angle"));
+ }
+
+ #[test]
+ fn test_convert_valid_formats() {
+ for format in ["gif", "webm", "mp4", "mov", "avi", "mkv"] {
+ let args = ConvertArgs {
+ input: PathBuf::from("input.mp4"),
+ output: PathBuf::from("output.mp4"),
+ format: format.to_string(),
+ start: None,
+ duration: None,
+ width: None,
+ fps: None,
+ };
+ let result = handle_convert(&args, true);
+ // Should not fail on format validation
+ assert!(result.is_ok() || !result.unwrap_err().to_string().contains("Invalid format"));
+ }
+ }
+
+ #[test]
+ fn test_convert_invalid_format() {
+ let args = ConvertArgs {
+ input: PathBuf::from("input.mp4"),
+ output: PathBuf::from("output.mp4"),
+ format: "invalid".to_string(),
+ start: None,
+ duration: None,
+ width: None,
+ fps: None,
+ };
+ let result = handle_convert(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("Invalid format"));
+ }
+
+ #[test]
+ fn test_stitch_invalid_format() {
+ let args = StitchArgs {
+ inputs: vec!["frame.png".to_string()],
+ output: PathBuf::from("output.mp4"),
+ format: "invalid".to_string(),
+ fps: 24,
+ width: None,
+ };
+ let result = handle_stitch(&args, true);
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(err.contains("Invalid format"));
+ }
+}
From 818eb02826d39c7a6889a50ad14a60b01ecb87b0 Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:28:12 +0800
Subject: [PATCH 10/12] test(core): add unit tests for video processing
functions including trim, join, thumbnail, convert, speed, rotate, mute, and
stitch
---
crates/core/src/job/executor.rs | 193 ++++++++++++++++++++++++++++++++
crates/core/src/utils/error.rs | 105 +++++++++++++++++
2 files changed, 298 insertions(+)
diff --git a/crates/core/src/job/executor.rs b/crates/core/src/job/executor.rs
index 7e75558..1233191 100644
--- a/crates/core/src/job/executor.rs
+++ b/crates/core/src/job/executor.rs
@@ -2578,4 +2578,197 @@ mod video_operation_tests {
assert!(result.contains("-c:a aac"));
assert!(result.contains("-b:a 128k"));
}
+
+ #[test]
+ fn test_execute_video_trim_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("clip.mp4");
+
+ let result =
+ execute_video_trim(&input, &output, Some(30.0), Some(60.0), true).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("-ss 30"));
+ // When both start and end are specified, duration (-t) is used, not -to
+ assert!(result.contains("-t 30")); // duration = end - start = 60 - 30 = 30
+ assert!(result.contains("-c copy"));
+ }
+
+ #[test]
+ fn test_execute_video_trim_start_only_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("clip.mp4");
+
+ let result = execute_video_trim(&input, &output, Some(120.0), None, true).unwrap();
+
+ assert!(result.contains("-ss 120"));
+ assert!(!result.contains("-to"));
+ }
+
+ #[test]
+ fn test_execute_video_trim_end_only_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("clip.mp4");
+
+ let result = execute_video_trim(&input, &output, None, Some(60.0), true).unwrap();
+
+ assert!(result.contains("-to 60"));
+ assert!(!result.contains("-ss"));
+ }
+
+ #[test]
+ fn test_execute_video_join_plan() {
+ let inputs = vec![
+ PathBuf::from("part1.mp4"),
+ PathBuf::from("part2.mp4"),
+ ];
+ let output = PathBuf::from("full.mp4");
+
+ let result = execute_video_join(&inputs, &output, true).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("-f concat"));
+ assert!(result.contains("-c copy"));
+ }
+
+ #[test]
+ fn test_execute_video_thumbnail_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("thumb.jpg");
+
+ let result = execute_video_thumbnail(&input, &output, 5.0, true).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("-ss 5"));
+ assert!(result.contains("-frames:v 1"));
+ }
+
+ #[test]
+ fn test_execute_video_convert_gif_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("output.gif");
+
+ let result = execute_video_convert(
+ &input, &output, "gif", None, None, Some(480), Some(10), true
+ ).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("palettegen"));
+ assert!(result.contains("paletteuse"));
+ assert!(result.contains("scale=480"));
+ }
+
+ #[test]
+ fn test_execute_video_convert_webm_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("output.webm");
+
+ let result = execute_video_convert(
+ &input, &output, "webm", None, None, None, None, true
+ ).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("-c copy"));
+ }
+
+ #[test]
+ fn test_execute_video_speed_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("fast.mp4");
+
+ let result = execute_video_speed(&input, &output, 2.0, true).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("setpts=PTS/2"));
+ assert!(result.contains("atempo=2"));
+ }
+
+ #[test]
+ fn test_execute_video_speed_slow_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("slow.mp4");
+
+ let result = execute_video_speed(&input, &output, 0.5, true).unwrap();
+
+ assert!(result.contains("setpts=PTS/0.5"));
+ assert!(result.contains("atempo=0.5"));
+ }
+
+ #[test]
+ fn test_execute_video_rotate_90_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("rotated.mp4");
+
+ let result = execute_video_rotate(&input, &output, 90, true).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("transpose=1"));
+ }
+
+ #[test]
+ fn test_execute_video_rotate_180_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("rotated.mp4");
+
+ let result = execute_video_rotate(&input, &output, 180, true).unwrap();
+
+ assert!(result.contains("transpose=1,transpose=1"));
+ }
+
+ #[test]
+ fn test_execute_video_rotate_270_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("rotated.mp4");
+
+ let result = execute_video_rotate(&input, &output, 270, true).unwrap();
+
+ assert!(result.contains("transpose=2"));
+ }
+
+ // Note: Invalid rotation angle validation only happens during actual execution,
+ // not in plan mode. The plan function generates output for any angle.
+ // Validation is handled at the CLI layer for plan mode.
+
+ #[test]
+ fn test_execute_video_mute_plan() {
+ let input = PathBuf::from("video.mp4");
+ let output = PathBuf::from("silent.mp4");
+
+ let result = execute_video_mute(&input, &output, true).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("-an"));
+ assert!(result.contains("-c:v copy"));
+ }
+
+ #[test]
+ fn test_execute_video_stitch_mp4_plan() {
+ let inputs = vec![
+ PathBuf::from("frame1.png"),
+ PathBuf::from("frame2.png"),
+ ];
+ let output = PathBuf::from("video.mp4");
+
+ let result = execute_video_stitch(&inputs, &output, "mp4", 24, None, true).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("-f concat"));
+ assert!(result.contains("-r 24"));
+ }
+
+ #[test]
+ fn test_execute_video_stitch_gif_plan() {
+ let inputs = vec![
+ PathBuf::from("frame1.png"),
+ PathBuf::from("frame2.png"),
+ ];
+ let output = PathBuf::from("anim.gif");
+
+ let result = execute_video_stitch(&inputs, &output, "gif", 10, Some(480), true).unwrap();
+
+ assert!(result.contains("ffmpeg"));
+ assert!(result.contains("palettegen"));
+ assert!(result.contains("paletteuse"));
+ assert!(result.contains("scale=480"));
+ }
}
diff --git a/crates/core/src/utils/error.rs b/crates/core/src/utils/error.rs
index 04e7a5a..b7fe966 100644
--- a/crates/core/src/utils/error.rs
+++ b/crates/core/src/utils/error.rs
@@ -144,3 +144,108 @@ impl ForgeKitError {
/// Result type alias
pub type Result = std::result::Result;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_exit_code_tool_not_found() {
+ let err = ForgeKitError::ToolNotFound {
+ tool: "qpdf".to_string(),
+ hint: "Install with brew install qpdf".to_string(),
+ };
+ assert_eq!(err.exit_code(), ExitCode::MissingTool);
+ assert_eq!(i32::from(err.exit_code()), 2);
+ }
+
+ #[test]
+ fn test_exit_code_invalid_input() {
+ let err = ForgeKitError::InvalidInput {
+ path: PathBuf::from("test.pdf"),
+ reason: "File not found".to_string(),
+ };
+ assert_eq!(err.exit_code(), ExitCode::InvalidInput);
+ assert_eq!(i32::from(err.exit_code()), 3);
+ }
+
+ #[test]
+ fn test_exit_code_permission_denied() {
+ let err = ForgeKitError::PermissionDenied {
+ path: PathBuf::from("/root/file"),
+ };
+ assert_eq!(err.exit_code(), ExitCode::PermissionDenied);
+ assert_eq!(i32::from(err.exit_code()), 4);
+ }
+
+ #[test]
+ fn test_exit_code_disk_full() {
+ let err = ForgeKitError::DiskFull {
+ path: PathBuf::from("/output.pdf"),
+ };
+ assert_eq!(err.exit_code(), ExitCode::DiskFull);
+ assert_eq!(i32::from(err.exit_code()), 5);
+ }
+
+ #[test]
+ fn test_exit_code_cancelled() {
+ let err = ForgeKitError::Cancelled;
+ assert_eq!(err.exit_code(), ExitCode::Cancelled);
+ assert_eq!(i32::from(err.exit_code()), 130);
+ }
+
+ #[test]
+ fn test_exit_code_processing_failed() {
+ let err = ForgeKitError::ProcessingFailed {
+ tool: "ffmpeg".to_string(),
+ stderr: "Error encoding video".to_string(),
+ };
+ assert_eq!(err.exit_code(), ExitCode::GeneralError);
+ assert_eq!(i32::from(err.exit_code()), 1);
+ }
+
+ #[test]
+ fn test_exit_code_tool_version_mismatch() {
+ let err = ForgeKitError::ToolVersionMismatch {
+ tool: "qpdf".to_string(),
+ required: "11.0".to_string(),
+ found: "10.0".to_string(),
+ };
+ assert_eq!(err.exit_code(), ExitCode::GeneralError);
+ assert_eq!(i32::from(err.exit_code()), 1);
+ }
+
+ #[test]
+ fn test_error_display_tool_not_found() {
+ let err = ForgeKitError::ToolNotFound {
+ tool: "qpdf".to_string(),
+ hint: "Install with brew install qpdf".to_string(),
+ };
+ let msg = err.to_string();
+ assert!(msg.contains("qpdf"));
+ assert!(msg.contains("not found"));
+ assert!(msg.contains("brew install qpdf"));
+ }
+
+ #[test]
+ fn test_error_display_invalid_input() {
+ let err = ForgeKitError::InvalidInput {
+ path: PathBuf::from("missing.pdf"),
+ reason: "File does not exist".to_string(),
+ };
+ let msg = err.to_string();
+ assert!(msg.contains("missing.pdf"));
+ assert!(msg.contains("File does not exist"));
+ }
+
+ #[test]
+ fn test_error_display_processing_failed() {
+ let err = ForgeKitError::ProcessingFailed {
+ tool: "ffmpeg".to_string(),
+ stderr: "Invalid codec".to_string(),
+ };
+ let msg = err.to_string();
+ assert!(msg.contains("ffmpeg"));
+ assert!(msg.contains("Invalid codec"));
+ }
+}
From e10810d45c7874ea8d7631bb369fdc3ceb8b64cf Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:33:34 +0800
Subject: [PATCH 11/12] refactor(core, cli): improve formatting and readability
of video processing test cases
---
crates/cli/src/commands/video.rs | 4 +++-
crates/core/src/job/executor.rs | 35 +++++++++++++++-----------------
2 files changed, 19 insertions(+), 20 deletions(-)
diff --git a/crates/cli/src/commands/video.rs b/crates/cli/src/commands/video.rs
index a9c7906..8e8a816 100644
--- a/crates/cli/src/commands/video.rs
+++ b/crates/cli/src/commands/video.rs
@@ -1051,7 +1051,9 @@ mod tests {
// Will fail because input doesn't exist, but validation passes
let result = handle_rotate(&args, true);
// In plan mode, it should generate a plan (may fail due to missing file)
- assert!(result.is_ok() || !result.unwrap_err().to_string().contains("Invalid rotation"));
+ assert!(
+ result.is_ok() || !result.unwrap_err().to_string().contains("Invalid rotation")
+ );
}
}
diff --git a/crates/core/src/job/executor.rs b/crates/core/src/job/executor.rs
index 1233191..4bfcf61 100644
--- a/crates/core/src/job/executor.rs
+++ b/crates/core/src/job/executor.rs
@@ -2584,8 +2584,7 @@ mod video_operation_tests {
let input = PathBuf::from("video.mp4");
let output = PathBuf::from("clip.mp4");
- let result =
- execute_video_trim(&input, &output, Some(30.0), Some(60.0), true).unwrap();
+ let result = execute_video_trim(&input, &output, Some(30.0), Some(60.0), true).unwrap();
assert!(result.contains("ffmpeg"));
assert!(result.contains("-ss 30"));
@@ -2618,10 +2617,7 @@ mod video_operation_tests {
#[test]
fn test_execute_video_join_plan() {
- let inputs = vec![
- PathBuf::from("part1.mp4"),
- PathBuf::from("part2.mp4"),
- ];
+ let inputs = vec![PathBuf::from("part1.mp4"), PathBuf::from("part2.mp4")];
let output = PathBuf::from("full.mp4");
let result = execute_video_join(&inputs, &output, true).unwrap();
@@ -2649,8 +2645,16 @@ mod video_operation_tests {
let output = PathBuf::from("output.gif");
let result = execute_video_convert(
- &input, &output, "gif", None, None, Some(480), Some(10), true
- ).unwrap();
+ &input,
+ &output,
+ "gif",
+ None,
+ None,
+ Some(480),
+ Some(10),
+ true,
+ )
+ .unwrap();
assert!(result.contains("ffmpeg"));
assert!(result.contains("palettegen"));
@@ -2663,9 +2667,8 @@ mod video_operation_tests {
let input = PathBuf::from("video.mp4");
let output = PathBuf::from("output.webm");
- let result = execute_video_convert(
- &input, &output, "webm", None, None, None, None, true
- ).unwrap();
+ let result =
+ execute_video_convert(&input, &output, "webm", None, None, None, None, true).unwrap();
assert!(result.contains("ffmpeg"));
assert!(result.contains("-c copy"));
@@ -2743,10 +2746,7 @@ mod video_operation_tests {
#[test]
fn test_execute_video_stitch_mp4_plan() {
- let inputs = vec![
- PathBuf::from("frame1.png"),
- PathBuf::from("frame2.png"),
- ];
+ let inputs = vec![PathBuf::from("frame1.png"), PathBuf::from("frame2.png")];
let output = PathBuf::from("video.mp4");
let result = execute_video_stitch(&inputs, &output, "mp4", 24, None, true).unwrap();
@@ -2758,10 +2758,7 @@ mod video_operation_tests {
#[test]
fn test_execute_video_stitch_gif_plan() {
- let inputs = vec![
- PathBuf::from("frame1.png"),
- PathBuf::from("frame2.png"),
- ];
+ let inputs = vec![PathBuf::from("frame1.png"), PathBuf::from("frame2.png")];
let output = PathBuf::from("anim.gif");
let result = execute_video_stitch(&inputs, &output, "gif", 10, Some(480), true).unwrap();
From 753aec1fc2b5d2e2bbaa71204b543e0e96484621 Mon Sep 17 00:00:00 2001
From: Naveed <46922100+nedanwr@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:38:59 +0800
Subject: [PATCH 12/12] refactor(tests): replace `vec![]` with `[]` for
improved clarity in natural sorting test cases
---
crates/cli/src/commands/video.rs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/crates/cli/src/commands/video.rs b/crates/cli/src/commands/video.rs
index 8e8a816..23dd91e 100644
--- a/crates/cli/src/commands/video.rs
+++ b/crates/cli/src/commands/video.rs
@@ -944,7 +944,7 @@ mod tests {
// Natural sorting tests
#[test]
fn test_natural_sort_key_numbers() {
- let mut files = vec![
+ let mut files = [
PathBuf::from("frame_10.png"),
PathBuf::from("frame_2.png"),
PathBuf::from("frame_1.png"),
@@ -957,7 +957,7 @@ mod tests {
#[test]
fn test_natural_sort_key_mixed() {
- let mut files = vec![
+ let mut files = [
PathBuf::from("part_100.mp4"),
PathBuf::from("part_20.mp4"),
PathBuf::from("part_3.mp4"),