1use clap::{Args, Parser, Subcommand};
6use indicatif::{ProgressBar, ProgressStyle};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(i32)]
15pub enum ExitCode {
16 Success = 0,
18 GeneralError = 1,
20 InvalidArgs = 2,
22 InputNotFound = 3,
24 OutputError = 4,
26 ProcessingError = 5,
28 GpuError = 6,
30 ExternalToolError = 7,
32}
33
34impl ExitCode {
35 #[must_use]
37 pub fn code(self) -> i32 {
38 self as i32
39 }
40
41 #[must_use]
43 pub fn description(self) -> &'static str {
44 match self {
45 ExitCode::Success => "Success",
46 ExitCode::GeneralError => "General error",
47 ExitCode::InvalidArgs => "Invalid arguments",
48 ExitCode::InputNotFound => "Input file or directory not found",
49 ExitCode::OutputError => "Output error (permission denied, disk full, etc.)",
50 ExitCode::ProcessingError => "Processing error",
51 ExitCode::GpuError => "GPU initialization or processing error",
52 ExitCode::ExternalToolError => "External tool error (Python, ImageMagick, etc.)",
53 }
54 }
55}
56
57impl From<ExitCode> for i32 {
58 fn from(code: ExitCode) -> Self {
59 code.code()
60 }
61}
62
63impl From<ExitCode> for std::process::ExitCode {
64 fn from(code: ExitCode) -> Self {
65 std::process::ExitCode::from(code.code() as u8)
66 }
67}
68
69#[derive(Parser, Debug)]
71#[command(name = "superbook-pdf")]
72#[command(author = "DN_SuperBook_PDF_Converter Contributors")]
73#[command(version)]
74#[command(about = "High-quality PDF converter for scanned books")]
75#[command(after_help = r#"
76Quick Start:
77 superbook-pdf convert input.pdf -o output/ # 基本変換
78 superbook-pdf convert input.pdf -o output/ --advanced --ocr # 高品質変換
79 superbook-pdf serve --port 8080 # Web UI起動
80
81詳細は `superbook-pdf <COMMAND> --help` を参照
82"#)]
83pub struct Cli {
84 #[command(subcommand)]
85 pub command: Commands,
86}
87
88#[derive(Subcommand, Debug)]
90pub enum Commands {
91 Convert(ConvertArgs),
93 Reprocess(ReprocessArgs),
95 Info,
97 CacheInfo(CacheInfoArgs),
99 #[cfg(feature = "web")]
101 Serve(ServeArgs),
102}
103
104#[derive(Args, Debug)]
106pub struct CacheInfoArgs {
107 #[arg(value_name = "OUTPUT_PDF")]
109 pub output_pdf: std::path::PathBuf,
110}
111
112#[derive(Args, Debug, Clone)]
114pub struct ReprocessArgs {
115 #[arg(value_name = "INPUT")]
117 pub input: PathBuf,
118
119 #[arg(short, long, value_delimiter = ',')]
121 pub pages: Option<Vec<usize>>,
122
123 #[arg(long, default_value = "3")]
125 pub max_retries: u32,
126
127 #[arg(short, long)]
129 pub force: bool,
130
131 #[arg(long)]
133 pub status: bool,
134
135 #[arg(short, long)]
137 pub output: Option<PathBuf>,
138
139 #[arg(long)]
141 pub keep_intermediates: bool,
142
143 #[arg(short, long, action = clap::ArgAction::Count)]
145 pub verbose: u8,
146
147 #[arg(short, long)]
149 pub quiet: bool,
150}
151
152impl ReprocessArgs {
153 pub fn is_state_file(&self) -> bool {
155 self.input
156 .to_string_lossy()
157 .ends_with(".superbook-state.json")
158 }
159
160 pub fn page_indices(&self) -> Vec<usize> {
162 self.pages.clone().unwrap_or_default()
163 }
164}
165
166#[cfg(feature = "web")]
168#[derive(Args, Debug)]
169pub struct ServeArgs {
170 #[arg(short, long, default_value = "8080")]
172 pub port: u16,
173
174 #[arg(short, long, default_value = "127.0.0.1")]
176 pub bind: String,
177
178 #[arg(long, default_value = "500")]
180 pub upload_limit: usize,
181
182 #[arg(long, default_value = "3600")]
184 pub job_timeout: u64,
185
186 #[arg(long, default_value = "true")]
188 pub cors: bool,
189
190 #[arg(long = "cors-origin")]
192 pub cors_origins: Vec<String>,
193
194 #[arg(long)]
196 pub no_cors: bool,
197}
198
199#[derive(clap::Args, Debug)]
201#[command(after_help = r#"
202Examples:
203 # 基本的な変換
204 superbook-pdf convert input.pdf -o output/
205
206 # AI超解像 + OCR付き高品質変換
207 superbook-pdf convert input.pdf -o output/ --advanced --ocr
208
209 # GPU無効化 (CPUのみ)
210 superbook-pdf convert input.pdf -o output/ --no-gpu
211
212 # 最初の10ページのみテスト
213 superbook-pdf convert input.pdf -o output/ --max-pages 10
214
215 # 詳細ログ出力
216 superbook-pdf convert input.pdf -o output/ -vvv
217"#)]
218pub struct ConvertArgs {
219 pub input: PathBuf,
221
222 #[arg(short = 'o', long = "output", default_value = "./output")]
224 pub output: PathBuf,
225
226 #[arg(short = 'c', long)]
228 pub config: Option<PathBuf>,
229
230 #[arg(long)]
232 pub ocr: bool,
233
234 #[arg(short, long, default_value_t = true)]
236 #[arg(action = clap::ArgAction::Set)]
237 pub upscale: bool,
238
239 #[arg(long = "no-upscale")]
241 #[arg(action = clap::ArgAction::SetTrue)]
242 no_upscale: bool,
243
244 #[arg(short, long, default_value_t = true)]
246 #[arg(action = clap::ArgAction::Set)]
247 pub deskew: bool,
248
249 #[arg(long = "no-deskew")]
251 #[arg(action = clap::ArgAction::SetTrue)]
252 no_deskew: bool,
253
254 #[arg(short, long, default_value_t = 0.5)]
256 pub margin_trim: f32,
257
258 #[arg(long, default_value_t = 300, value_parser = clap::value_parser!(u32).range(1..=4800))]
260 pub dpi: u32,
261
262 #[arg(long, default_value_t = 90, value_parser = clap::value_parser!(u8).range(1..=100))]
264 pub jpeg_quality: u8,
265
266 #[arg(short = 't', long)]
268 pub threads: Option<usize>,
269
270 #[arg(long, default_value_t = 0)]
272 pub chunk_size: usize,
273
274 #[arg(short, long, default_value_t = true)]
276 #[arg(action = clap::ArgAction::Set)]
277 pub gpu: bool,
278
279 #[arg(long = "no-gpu")]
281 #[arg(action = clap::ArgAction::SetTrue)]
282 no_gpu: bool,
283
284 #[arg(short, long, action = clap::ArgAction::Count)]
286 pub verbose: u8,
287
288 #[arg(short, long)]
290 pub quiet: bool,
291
292 #[arg(long)]
294 pub dry_run: bool,
295
296 #[arg(long)]
299 pub internal_resolution: bool,
300
301 #[arg(long)]
303 pub color_correction: bool,
304
305 #[arg(long)]
307 pub offset_alignment: bool,
308
309 #[arg(long, default_value_t = 3508)]
311 pub output_height: u32,
312
313 #[arg(long)]
316 pub advanced: bool,
317
318 #[arg(long)]
320 pub skip_existing: bool,
321
322 #[arg(long, short = 'f')]
324 pub force: bool,
325
326 #[arg(long)]
329 pub max_pages: Option<usize>,
330
331 #[arg(long)]
333 pub save_debug: bool,
334}
335
336impl ConvertArgs {
337 pub fn effective_upscale(&self) -> bool {
339 self.upscale && !self.no_upscale
340 }
341
342 pub fn effective_deskew(&self) -> bool {
344 self.deskew && !self.no_deskew
345 }
346
347 pub fn effective_gpu(&self) -> bool {
349 self.gpu && !self.no_gpu
350 }
351
352 pub fn thread_count(&self) -> usize {
354 self.threads.unwrap_or_else(num_cpus::get)
355 }
356
357 pub fn effective_internal_resolution(&self) -> bool {
359 self.internal_resolution || self.advanced
360 }
361
362 pub fn effective_color_correction(&self) -> bool {
364 self.color_correction || self.advanced
365 }
366
367 pub fn effective_offset_alignment(&self) -> bool {
369 self.offset_alignment || self.advanced
370 }
371}
372
373pub fn create_progress_bar(total: u64) -> ProgressBar {
375 let pb = ProgressBar::new(total);
376 pb.set_style(
377 ProgressStyle::default_bar()
378 .template(
379 "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
380 )
381 .expect("Invalid progress bar template")
382 .progress_chars("#>-"),
383 );
384 pb
385}
386
387pub fn create_spinner(message: &str) -> ProgressBar {
389 let pb = ProgressBar::new_spinner();
390 pb.set_style(
391 ProgressStyle::default_spinner()
392 .template("{spinner:.green} {msg}")
393 .expect("Invalid spinner template"),
394 );
395 pb.set_message(message.to_string());
396 pb
397}
398
399pub fn create_page_progress_bar(total: u64) -> ProgressBar {
401 let pb = ProgressBar::new(total);
402 pb.set_style(
403 ProgressStyle::default_bar()
404 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] Page {pos}/{len} ({percent}%) - {msg}")
405 .expect("Invalid progress bar template")
406 .progress_chars("█▓░"),
407 );
408 pb
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use clap::CommandFactory;
415
416 #[test]
417 fn test_cli_parse() {
418 Cli::command().debug_assert();
420 }
421
422 #[test]
424 fn test_help_display() {
425 let mut cmd = Cli::command();
426 let help = cmd.render_help().to_string();
427 assert!(help.contains("superbook-pdf"));
428 assert!(help.contains("convert"));
429 }
430
431 #[test]
433 fn test_version_display() {
434 let cmd = Cli::command();
435 let version = cmd.get_version().unwrap_or("unknown");
436 assert!(!version.is_empty());
437 }
438
439 #[test]
441 fn test_missing_input_error() {
442 let result = Cli::try_parse_from(["superbook-pdf", "convert"]);
443 assert!(result.is_err());
444 let err = result.unwrap_err();
445 assert!(err.to_string().contains("required"));
446 }
447
448 #[test]
450 fn test_nonexistent_file_parse() {
451 let result = Cli::try_parse_from(["superbook-pdf", "convert", "/nonexistent/file.pdf"]);
453 assert!(result.is_ok()); }
455
456 #[test]
458 fn test_option_parsing() {
459 let cli = Cli::try_parse_from([
460 "superbook-pdf",
461 "convert",
462 "input.pdf",
463 "--ocr",
464 "--no-upscale",
465 "--dpi",
466 "600",
467 "-vvv",
468 ])
469 .unwrap();
470
471 if let Commands::Convert(args) = cli.command {
472 assert!(args.ocr);
473 assert!(!args.effective_upscale());
474 assert_eq!(args.dpi, 600);
475 assert_eq!(args.verbose, 3);
476 } else {
477 panic!("Expected Convert command");
478 }
479 }
480
481 #[test]
483 fn test_default_values() {
484 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
485
486 if let Commands::Convert(args) = cli.command {
487 assert!(!args.ocr);
488 assert!(args.effective_upscale());
489 assert!(args.effective_deskew());
490 assert_eq!(args.margin_trim, 0.5);
491 assert_eq!(args.dpi, 300);
492 assert!(args.effective_gpu());
493 assert_eq!(args.verbose, 0);
494 assert!(!args.quiet);
495 assert!(!args.dry_run);
496 } else {
497 panic!("Expected Convert command");
498 }
499 }
500
501 #[test]
502 fn test_directory_input() {
503 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "/tmp/test_dir", "--dry-run"])
504 .unwrap();
505
506 if let Commands::Convert(args) = cli.command {
507 assert!(args.dry_run);
508 } else {
509 panic!("Expected Convert command");
510 }
511 }
512
513 #[test]
514 fn test_info_command() {
515 let cli = Cli::try_parse_from(["superbook-pdf", "info"]).unwrap();
516
517 assert!(matches!(cli.command, Commands::Info));
518 }
519
520 #[test]
522 fn test_progress_bar_display() {
523 let pb = create_progress_bar(100);
525 assert_eq!(pb.length(), Some(100));
526
527 pb.set_position(50);
529 assert_eq!(pb.position(), 50);
530
531 pb.finish_with_message("done");
532 }
533
534 #[test]
535 fn test_spinner_creation() {
536 let spinner = create_spinner("Processing...");
537 assert_eq!(spinner.message(), "Processing...");
538 spinner.finish_with_message("Complete");
539 }
540
541 #[test]
542 fn test_page_progress_bar() {
543 let pb = create_page_progress_bar(10);
544 assert_eq!(pb.length(), Some(10));
545
546 for i in 0..10 {
547 pb.set_position(i);
548 pb.set_message(format!("page_{}.png", i));
549 }
550 pb.finish_with_message("All pages processed");
551 }
552
553 #[test]
555 fn test_exit_code_values() {
556 assert_eq!(ExitCode::Success.code(), 0);
557 assert_eq!(ExitCode::GeneralError.code(), 1);
558 assert_eq!(ExitCode::InvalidArgs.code(), 2);
559 assert_eq!(ExitCode::InputNotFound.code(), 3);
560 assert_eq!(ExitCode::OutputError.code(), 4);
561 assert_eq!(ExitCode::ProcessingError.code(), 5);
562 assert_eq!(ExitCode::GpuError.code(), 6);
563 assert_eq!(ExitCode::ExternalToolError.code(), 7);
564 }
565
566 #[test]
567 fn test_exit_code_descriptions() {
568 assert_eq!(ExitCode::Success.description(), "Success");
569 assert!(!ExitCode::GeneralError.description().is_empty());
570 assert!(!ExitCode::InvalidArgs.description().is_empty());
571 assert!(!ExitCode::InputNotFound.description().is_empty());
572 assert!(!ExitCode::OutputError.description().is_empty());
573 assert!(!ExitCode::ProcessingError.description().is_empty());
574 assert!(!ExitCode::GpuError.description().is_empty());
575 assert!(!ExitCode::ExternalToolError.description().is_empty());
576 }
577
578 #[test]
579 fn test_exit_code_into_i32() {
580 let code: i32 = ExitCode::Success.into();
581 assert_eq!(code, 0);
582
583 let code: i32 = ExitCode::ExternalToolError.into();
584 assert_eq!(code, 7);
585 }
586
587 #[test]
588 fn test_exit_code_equality() {
589 assert_eq!(ExitCode::Success, ExitCode::Success);
590 assert_ne!(ExitCode::Success, ExitCode::GeneralError);
591 }
592
593 #[test]
594 fn test_exit_code_clone_copy() {
595 let code = ExitCode::ProcessingError;
596 let cloned = code;
597 let copied = code;
598 assert_eq!(code, cloned);
599 assert_eq!(code, copied);
600 }
601
602 #[test]
605 fn test_thread_count_default() {
606 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
607
608 if let Commands::Convert(args) = cli.command {
609 assert!(args.thread_count() > 0);
611 } else {
612 panic!("Expected Convert command");
613 }
614 }
615
616 #[test]
617 fn test_thread_count_explicit() {
618 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--threads", "4"])
619 .unwrap();
620
621 if let Commands::Convert(args) = cli.command {
622 assert_eq!(args.thread_count(), 4);
623 } else {
624 panic!("Expected Convert command");
625 }
626 }
627
628 #[test]
629 fn test_all_flags_combination() {
630 let cli = Cli::try_parse_from([
631 "superbook-pdf",
632 "convert",
633 "input.pdf",
634 "--ocr",
635 "--no-upscale",
636 "--no-deskew",
637 "--no-gpu",
638 "--quiet",
639 "--dry-run",
640 "-vvv",
641 ])
642 .unwrap();
643
644 if let Commands::Convert(args) = cli.command {
645 assert!(args.ocr);
646 assert!(!args.effective_upscale());
647 assert!(!args.effective_deskew());
648 assert!(!args.effective_gpu());
649 assert!(args.quiet);
650 assert!(args.dry_run);
651 assert_eq!(args.verbose, 3);
652 } else {
653 panic!("Expected Convert command");
654 }
655 }
656
657 #[test]
658 fn test_margin_trim_setting() {
659 let cli = Cli::try_parse_from([
660 "superbook-pdf",
661 "convert",
662 "input.pdf",
663 "--margin-trim",
664 "1.5",
665 ])
666 .unwrap();
667
668 if let Commands::Convert(args) = cli.command {
669 assert_eq!(args.margin_trim, 1.5);
670 } else {
671 panic!("Expected Convert command");
672 }
673 }
674
675 #[test]
676 fn test_dpi_setting() {
677 let cli =
678 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--dpi", "600"]).unwrap();
679
680 if let Commands::Convert(args) = cli.command {
681 assert_eq!(args.dpi, 600);
682 } else {
683 panic!("Expected Convert command");
684 }
685 }
686
687 #[test]
688 fn test_exit_code_to_process_exit_code() {
689 let code: std::process::ExitCode = ExitCode::Success.into();
690 let _ = code;
692
693 let code: std::process::ExitCode = ExitCode::GeneralError.into();
694 let _ = code;
695 }
696
697 #[test]
699 fn test_all_exit_codes() {
700 let codes = [
701 (ExitCode::Success, 0),
702 (ExitCode::GeneralError, 1),
703 (ExitCode::InvalidArgs, 2),
704 (ExitCode::InputNotFound, 3),
705 (ExitCode::OutputError, 4),
706 (ExitCode::ProcessingError, 5),
707 (ExitCode::GpuError, 6),
708 (ExitCode::ExternalToolError, 7),
709 ];
710
711 for (exit_code, expected) in codes {
712 assert_eq!(exit_code.code(), expected);
713 }
714 }
715
716 #[test]
718 fn test_version_flag() {
719 let result = Cli::try_parse_from(["superbook-pdf", "--version"]);
721 assert!(result.is_err());
723 }
724
725 #[test]
727 fn test_help_flag() {
728 let result = Cli::try_parse_from(["superbook-pdf", "--help"]);
729 assert!(result.is_err());
731 }
732
733 #[test]
735 fn test_invalid_command() {
736 let result = Cli::try_parse_from(["superbook-pdf", "invalid-command"]);
737 assert!(result.is_err());
738 }
739
740 #[test]
742 fn test_missing_input_file() {
743 let result = Cli::try_parse_from(["superbook-pdf", "convert"]);
744 assert!(result.is_err());
745 }
746
747 #[test]
749 fn test_invalid_dpi() {
750 let result =
751 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--dpi", "-100"]);
752 assert!(result.is_err());
754 }
755
756 #[test]
758 fn test_zero_threads() {
759 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--threads", "0"])
760 .unwrap();
761
762 if let Commands::Convert(args) = cli.command {
763 let count = args.thread_count();
765 assert_eq!(count, 0);
767 } else {
768 panic!("Expected Convert command");
769 }
770 }
771
772 #[test]
774 fn test_high_thread_count() {
775 let cli =
776 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--threads", "1024"])
777 .unwrap();
778
779 if let Commands::Convert(args) = cli.command {
780 assert_eq!(args.thread_count(), 1024);
781 } else {
782 panic!("Expected Convert command");
783 }
784 }
785
786 #[test]
788 fn test_output_path() {
789 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-o", "/custom/output"])
790 .unwrap();
791
792 if let Commands::Convert(args) = cli.command {
793 assert_eq!(args.output, PathBuf::from("/custom/output"));
794 } else {
795 panic!("Expected Convert command");
796 }
797 }
798
799 #[test]
801 fn test_default_output_path() {
802 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
803
804 if let Commands::Convert(args) = cli.command {
805 assert_eq!(args.output, PathBuf::from("./output"));
807 } else {
808 panic!("Expected Convert command");
809 }
810 }
811
812 #[test]
814 fn test_margin_trim_boundaries() {
815 let cli = Cli::try_parse_from([
817 "superbook-pdf",
818 "convert",
819 "input.pdf",
820 "--margin-trim",
821 "0.0",
822 ])
823 .unwrap();
824
825 if let Commands::Convert(args) = cli.command {
826 assert_eq!(args.margin_trim, 0.0);
827 } else {
828 panic!("Expected Convert command");
829 }
830
831 let cli = Cli::try_parse_from([
833 "superbook-pdf",
834 "convert",
835 "input.pdf",
836 "--margin-trim",
837 "10.0",
838 ])
839 .unwrap();
840
841 if let Commands::Convert(args) = cli.command {
842 assert_eq!(args.margin_trim, 10.0);
843 } else {
844 panic!("Expected Convert command");
845 }
846 }
847
848 #[test]
850 fn test_verbosity_levels() {
851 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
853 if let Commands::Convert(args) = cli.command {
854 assert_eq!(args.verbose, 0);
855 }
856
857 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-v"]).unwrap();
859 if let Commands::Convert(args) = cli.command {
860 assert_eq!(args.verbose, 1);
861 }
862
863 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-vv"]).unwrap();
865 if let Commands::Convert(args) = cli.command {
866 assert_eq!(args.verbose, 2);
867 }
868 }
869
870 #[test]
872 fn test_quiet_flag() {
873 let cli =
874 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--quiet"]).unwrap();
875
876 if let Commands::Convert(args) = cli.command {
877 assert!(args.quiet);
878 } else {
879 panic!("Expected Convert command");
880 }
881 }
882
883 #[test]
885 fn test_dry_run_flag() {
886 let cli =
887 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--dry-run"]).unwrap();
888
889 if let Commands::Convert(args) = cli.command {
890 assert!(args.dry_run);
891 } else {
892 panic!("Expected Convert command");
893 }
894 }
895
896 #[test]
899 fn test_exit_code_debug_impl() {
900 let code = ExitCode::Success;
901 let debug_str = format!("{:?}", code);
902 assert!(debug_str.contains("Success"));
903
904 let code2 = ExitCode::ProcessingError;
905 let debug_str2 = format!("{:?}", code2);
906 assert!(debug_str2.contains("ProcessingError"));
907 }
908
909 #[test]
910 fn test_cli_debug_impl() {
911 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
912 let debug_str = format!("{:?}", cli);
913 assert!(debug_str.contains("Cli"));
914 assert!(debug_str.contains("command"));
915 }
916
917 #[test]
918 fn test_commands_debug_impl() {
919 let cli = Cli::try_parse_from(["superbook-pdf", "info"]).unwrap();
920 let debug_str = format!("{:?}", cli.command);
921 assert!(debug_str.contains("Info"));
922
923 let cli2 = Cli::try_parse_from(["superbook-pdf", "convert", "test.pdf"]).unwrap();
924 let debug_str2 = format!("{:?}", cli2.command);
925 assert!(debug_str2.contains("Convert"));
926 }
927
928 #[test]
929 fn test_convert_args_debug_impl() {
930 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--ocr"]).unwrap();
931 if let Commands::Convert(args) = cli.command {
932 let debug_str = format!("{:?}", args);
933 assert!(debug_str.contains("ConvertArgs"));
934 assert!(debug_str.contains("input"));
935 assert!(debug_str.contains("ocr"));
936 }
937 }
938
939 #[test]
942 fn test_exit_code_copy() {
943 let original = ExitCode::GpuError;
944 let copied: ExitCode = original; let _still_valid = original; assert_eq!(copied, ExitCode::GpuError);
947 }
948
949 #[test]
950 fn test_exit_code_clone() {
951 let original = ExitCode::OutputError;
952 let cloned = original;
953 assert_eq!(original, cloned);
954 }
955
956 #[test]
959 fn test_all_exit_code_descriptions_non_empty() {
960 let codes = [
961 ExitCode::Success,
962 ExitCode::GeneralError,
963 ExitCode::InvalidArgs,
964 ExitCode::InputNotFound,
965 ExitCode::OutputError,
966 ExitCode::ProcessingError,
967 ExitCode::GpuError,
968 ExitCode::ExternalToolError,
969 ];
970
971 for code in codes {
972 let desc = code.description();
973 assert!(!desc.is_empty(), "Description for {:?} is empty", code);
974 }
975 }
976
977 #[test]
978 fn test_exit_code_unique_values() {
979 let codes = [
980 ExitCode::Success,
981 ExitCode::GeneralError,
982 ExitCode::InvalidArgs,
983 ExitCode::InputNotFound,
984 ExitCode::OutputError,
985 ExitCode::ProcessingError,
986 ExitCode::GpuError,
987 ExitCode::ExternalToolError,
988 ];
989
990 let values: Vec<i32> = codes.iter().map(|c| c.code()).collect();
992 for (i, v1) in values.iter().enumerate() {
993 for (j, v2) in values.iter().enumerate() {
994 if i != j {
995 assert_ne!(v1, v2, "Duplicate code value found");
996 }
997 }
998 }
999 }
1000
1001 #[test]
1002 fn test_exit_code_follows_unix_convention() {
1003 assert_eq!(ExitCode::Success.code(), 0);
1005 assert_ne!(ExitCode::GeneralError.code(), 0);
1007 assert_ne!(ExitCode::InvalidArgs.code(), 0);
1008 assert_ne!(ExitCode::InputNotFound.code(), 0);
1009 }
1010
1011 #[test]
1014 fn test_absolute_input_path() {
1015 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "/absolute/path/to/file.pdf"])
1016 .unwrap();
1017 if let Commands::Convert(args) = cli.command {
1018 assert!(args.input.is_absolute());
1019 }
1020 }
1021
1022 #[test]
1023 fn test_relative_input_path() {
1024 let cli =
1025 Cli::try_parse_from(["superbook-pdf", "convert", "relative/path/file.pdf"]).unwrap();
1026 if let Commands::Convert(args) = cli.command {
1027 assert!(args.input.is_relative());
1028 }
1029 }
1030
1031 #[test]
1032 fn test_path_with_spaces() {
1033 let cli =
1034 Cli::try_parse_from(["superbook-pdf", "convert", "/path/with spaces/document.pdf"])
1035 .unwrap();
1036 if let Commands::Convert(args) = cli.command {
1037 assert!(args.input.to_string_lossy().contains("with spaces"));
1038 }
1039 }
1040
1041 #[test]
1042 fn test_path_with_unicode() {
1043 let cli =
1044 Cli::try_parse_from(["superbook-pdf", "convert", "/パス/日本語/ドキュメント.pdf"])
1045 .unwrap();
1046 if let Commands::Convert(args) = cli.command {
1047 assert!(args.input.to_string_lossy().contains("日本語"));
1048 }
1049 }
1050
1051 #[test]
1052 fn test_directory_as_output() {
1053 let cli = Cli::try_parse_from([
1054 "superbook-pdf",
1055 "convert",
1056 "input.pdf",
1057 "-o",
1058 "/custom/output/directory",
1059 ])
1060 .unwrap();
1061 if let Commands::Convert(args) = cli.command {
1062 assert!(args.output.to_string_lossy().contains("directory"));
1063 }
1064 }
1065
1066 #[test]
1069 fn test_upscale_flag_explicit_true() {
1070 let cli =
1071 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--upscale", "true"])
1072 .unwrap();
1073 if let Commands::Convert(args) = cli.command {
1074 assert!(args.effective_upscale());
1075 }
1076 }
1077
1078 #[test]
1079 fn test_upscale_flag_explicit_false() {
1080 let cli = Cli::try_parse_from([
1081 "superbook-pdf",
1082 "convert",
1083 "input.pdf",
1084 "--upscale",
1085 "false",
1086 ])
1087 .unwrap();
1088 if let Commands::Convert(args) = cli.command {
1089 assert!(!args.effective_upscale());
1090 }
1091 }
1092
1093 #[test]
1094 fn test_deskew_flag_explicit_true() {
1095 let cli =
1096 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--deskew", "true"])
1097 .unwrap();
1098 if let Commands::Convert(args) = cli.command {
1099 assert!(args.effective_deskew());
1100 }
1101 }
1102
1103 #[test]
1104 fn test_deskew_flag_explicit_false() {
1105 let cli =
1106 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--deskew", "false"])
1107 .unwrap();
1108 if let Commands::Convert(args) = cli.command {
1109 assert!(!args.effective_deskew());
1110 }
1111 }
1112
1113 #[test]
1114 fn test_gpu_flag_explicit_true() {
1115 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--gpu", "true"])
1116 .unwrap();
1117 if let Commands::Convert(args) = cli.command {
1118 assert!(args.effective_gpu());
1119 }
1120 }
1121
1122 #[test]
1123 fn test_gpu_flag_explicit_false() {
1124 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--gpu", "false"])
1125 .unwrap();
1126 if let Commands::Convert(args) = cli.command {
1127 assert!(!args.effective_gpu());
1128 }
1129 }
1130
1131 #[test]
1132 fn test_no_flags_override_explicit() {
1133 let cli = Cli::try_parse_from([
1135 "superbook-pdf",
1136 "convert",
1137 "input.pdf",
1138 "--upscale",
1139 "true",
1140 "--no-upscale",
1141 ])
1142 .unwrap();
1143 if let Commands::Convert(args) = cli.command {
1144 assert!(!args.effective_upscale());
1145 }
1146 }
1147
1148 #[test]
1151 fn test_dpi_common_values() {
1152 let dpi_values = [72, 150, 300, 600, 1200];
1153 for dpi in dpi_values {
1154 let cli = Cli::try_parse_from([
1155 "superbook-pdf",
1156 "convert",
1157 "input.pdf",
1158 "--dpi",
1159 &dpi.to_string(),
1160 ])
1161 .unwrap();
1162 if let Commands::Convert(args) = cli.command {
1163 assert_eq!(args.dpi, dpi);
1164 }
1165 }
1166 }
1167
1168 #[test]
1169 fn test_dpi_minimum() {
1170 let cli =
1171 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--dpi", "1"]).unwrap();
1172 if let Commands::Convert(args) = cli.command {
1173 assert_eq!(args.dpi, 1);
1174 }
1175 }
1176
1177 #[test]
1178 fn test_dpi_very_high() {
1179 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--dpi", "2400"])
1180 .unwrap();
1181 if let Commands::Convert(args) = cli.command {
1182 assert_eq!(args.dpi, 2400);
1183 }
1184 }
1185
1186 #[test]
1189 fn test_progress_bar_zero_total() {
1190 let pb = create_progress_bar(0);
1191 assert_eq!(pb.length(), Some(0));
1192 }
1193
1194 #[test]
1195 fn test_progress_bar_large_total() {
1196 let pb = create_progress_bar(1_000_000);
1197 assert_eq!(pb.length(), Some(1_000_000));
1198 pb.set_position(500_000);
1199 assert_eq!(pb.position(), 500_000);
1200 }
1201
1202 #[test]
1203 fn test_spinner_empty_message() {
1204 let spinner = create_spinner("");
1205 assert_eq!(spinner.message(), "");
1206 }
1207
1208 #[test]
1209 fn test_spinner_unicode_message() {
1210 let spinner = create_spinner("処理中... 🔄");
1211 assert!(spinner.message().contains("処理中"));
1212 }
1213
1214 #[test]
1215 fn test_page_progress_bar_single_page() {
1216 let pb = create_page_progress_bar(1);
1217 assert_eq!(pb.length(), Some(1));
1218 pb.set_position(0);
1219 pb.set_message("page_0.png");
1220 pb.finish_with_message("Done");
1221 }
1222
1223 #[test]
1224 fn test_page_progress_bar_many_pages() {
1225 let pb = create_page_progress_bar(1000);
1226 assert_eq!(pb.length(), Some(1000));
1227 pb.set_position(999);
1228 assert_eq!(pb.position(), 999);
1229 }
1230
1231 #[test]
1234 fn test_short_flag_o_output() {
1235 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-o", "/custom/out"]).unwrap();
1236 if let Commands::Convert(args) = cli.command {
1237 assert_eq!(args.output, PathBuf::from("/custom/out"));
1238 }
1239 }
1240
1241 #[test]
1242 fn test_short_flag_m_margin() {
1243 let cli =
1244 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-m", "2.0"]).unwrap();
1245 if let Commands::Convert(args) = cli.command {
1246 assert_eq!(args.margin_trim, 2.0);
1247 }
1248 }
1249
1250 #[test]
1251 fn test_short_flag_t_threads() {
1252 let cli =
1253 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-t", "8"]).unwrap();
1254 if let Commands::Convert(args) = cli.command {
1255 assert_eq!(args.thread_count(), 8);
1256 }
1257 }
1258
1259 #[test]
1260 fn test_short_flag_g_gpu() {
1261 let cli =
1262 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-g", "false"]).unwrap();
1263 if let Commands::Convert(args) = cli.command {
1264 assert!(!args.effective_gpu());
1265 }
1266 }
1267
1268 #[test]
1269 fn test_short_flag_q_quiet() {
1270 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-q"]).unwrap();
1271 if let Commands::Convert(args) = cli.command {
1272 assert!(args.quiet);
1273 }
1274 }
1275
1276 #[test]
1279 fn test_various_input_extensions() {
1280 let extensions = ["pdf", "PDF", "Pdf"];
1281 for ext in extensions {
1282 let path = format!("document.{}", ext);
1283 let cli = Cli::try_parse_from(["superbook-pdf", "convert", &path]).unwrap();
1284 if let Commands::Convert(args) = cli.command {
1285 assert!(args.input.to_string_lossy().ends_with(ext));
1286 }
1287 }
1288 }
1289
1290 #[test]
1291 fn test_input_without_extension() {
1292 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "document"]).unwrap();
1293 if let Commands::Convert(args) = cli.command {
1294 assert_eq!(args.input, PathBuf::from("document"));
1295 }
1296 }
1297
1298 #[test]
1301 fn test_effective_methods_all_enabled() {
1302 let cli = Cli::try_parse_from([
1303 "superbook-pdf",
1304 "convert",
1305 "input.pdf",
1306 "--upscale",
1307 "true",
1308 "--deskew",
1309 "true",
1310 "--gpu",
1311 "true",
1312 ])
1313 .unwrap();
1314 if let Commands::Convert(args) = cli.command {
1315 assert!(args.effective_upscale());
1316 assert!(args.effective_deskew());
1317 assert!(args.effective_gpu());
1318 }
1319 }
1320
1321 #[test]
1322 fn test_effective_methods_all_disabled() {
1323 let cli = Cli::try_parse_from([
1324 "superbook-pdf",
1325 "convert",
1326 "input.pdf",
1327 "--no-upscale",
1328 "--no-deskew",
1329 "--no-gpu",
1330 ])
1331 .unwrap();
1332 if let Commands::Convert(args) = cli.command {
1333 assert!(!args.effective_upscale());
1334 assert!(!args.effective_deskew());
1335 assert!(!args.effective_gpu());
1336 }
1337 }
1338
1339 #[test]
1342 fn test_command_name() {
1343 let cmd = Cli::command();
1344 assert_eq!(cmd.get_name(), "superbook-pdf");
1345 }
1346
1347 #[test]
1348 fn test_command_about() {
1349 let cmd = Cli::command();
1350 let about = cmd.get_about().map(|s| s.to_string()).unwrap_or_default();
1351 assert!(!about.is_empty());
1352 }
1353
1354 #[test]
1355 fn test_subcommands_present() {
1356 let cmd = Cli::command();
1357 let subcommands: Vec<_> = cmd.get_subcommands().collect();
1358 assert!(subcommands.len() >= 2); }
1360
1361 #[test]
1364 fn test_invalid_dpi_format_error() {
1365 let result = Cli::try_parse_from([
1366 "superbook-pdf",
1367 "convert",
1368 "input.pdf",
1369 "--dpi",
1370 "not_a_number",
1371 ]);
1372 assert!(result.is_err());
1373 let err_msg = result.unwrap_err().to_string();
1374 assert!(
1376 err_msg.contains("invalid") || err_msg.contains("error") || err_msg.contains("parse")
1377 );
1378 }
1379
1380 #[test]
1381 fn test_invalid_thread_format_error() {
1382 let result =
1383 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--threads", "abc"]);
1384 assert!(result.is_err());
1385 }
1386
1387 #[test]
1388 fn test_invalid_margin_format_error() {
1389 let result = Cli::try_parse_from([
1390 "superbook-pdf",
1391 "convert",
1392 "input.pdf",
1393 "--margin-trim",
1394 "invalid",
1395 ]);
1396 assert!(result.is_err());
1397 }
1398
1399 #[test]
1402 fn test_cli_types_send_sync() {
1403 fn assert_send_sync<T: Send + Sync>() {}
1404 assert_send_sync::<Cli>();
1405 assert_send_sync::<Commands>();
1406 assert_send_sync::<ConvertArgs>();
1407 assert_send_sync::<ExitCode>();
1408 }
1409
1410 #[test]
1411 fn test_concurrent_cli_parsing() {
1412 use std::thread;
1413 let handles: Vec<_> = (0..4)
1414 .map(|i| {
1415 thread::spawn(move || {
1416 let cli = Cli::try_parse_from([
1417 "superbook-pdf",
1418 "convert",
1419 &format!("input_{}.pdf", i),
1420 "--dpi",
1421 &(300 + i * 100).to_string(),
1422 ])
1423 .unwrap();
1424 if let Commands::Convert(args) = cli.command {
1425 args.dpi
1426 } else {
1427 0
1428 }
1429 })
1430 })
1431 .collect();
1432
1433 let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1434 assert_eq!(results.len(), 4);
1435 for (i, dpi) in results.iter().enumerate() {
1436 assert_eq!(*dpi, 300 + (i as u32) * 100);
1437 }
1438 }
1439
1440 #[test]
1441 fn test_exit_code_thread_transfer() {
1442 use std::thread;
1443
1444 let codes = vec![
1445 ExitCode::Success,
1446 ExitCode::GeneralError,
1447 ExitCode::InvalidArgs,
1448 ExitCode::InputNotFound,
1449 ];
1450
1451 let handles: Vec<_> = codes
1452 .into_iter()
1453 .map(|code| {
1454 thread::spawn(move || {
1455 let c = code.code();
1456 let d = code.description().to_string();
1457 (c, d)
1458 })
1459 })
1460 .collect();
1461
1462 let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1463 assert_eq!(results.len(), 4);
1464 assert_eq!(results[0].0, 0);
1465 assert_eq!(results[1].0, 1);
1466 assert_eq!(results[2].0, 2);
1467 assert_eq!(results[3].0, 3);
1468 }
1469
1470 #[test]
1471 fn test_convert_args_clone_across_threads() {
1472 use std::sync::Arc;
1473 use std::thread;
1474
1475 let cli =
1476 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--dpi", "600"]).unwrap();
1477 if let Commands::Convert(args) = cli.command {
1478 let shared = Arc::new(args);
1479
1480 let handles: Vec<_> = (0..4)
1481 .map(|_| {
1482 let args_clone = Arc::clone(&shared);
1483 thread::spawn(move || {
1484 assert_eq!(args_clone.dpi, 600);
1485 args_clone.effective_deskew()
1486 })
1487 })
1488 .collect();
1489
1490 for handle in handles {
1491 let _ = handle.join().unwrap();
1492 }
1493 }
1494 }
1495
1496 #[test]
1499 fn test_dpi_boundary_minimum() {
1500 let cli =
1501 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--dpi", "1"]).unwrap();
1502 if let Commands::Convert(args) = cli.command {
1503 assert_eq!(args.dpi, 1);
1504 }
1505 }
1506
1507 #[test]
1508 fn test_dpi_boundary_maximum() {
1509 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--dpi", "2400"])
1510 .unwrap();
1511 if let Commands::Convert(args) = cli.command {
1512 assert_eq!(args.dpi, 2400);
1513 }
1514 }
1515
1516 #[test]
1519 fn test_jpeg_quality_default() {
1520 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
1521 if let Commands::Convert(args) = cli.command {
1522 assert_eq!(args.jpeg_quality, 90);
1523 }
1524 }
1525
1526 #[test]
1527 fn test_jpeg_quality_custom() {
1528 let cli = Cli::try_parse_from([
1529 "superbook-pdf",
1530 "convert",
1531 "input.pdf",
1532 "--jpeg-quality",
1533 "75",
1534 ])
1535 .unwrap();
1536 if let Commands::Convert(args) = cli.command {
1537 assert_eq!(args.jpeg_quality, 75);
1538 }
1539 }
1540
1541 #[test]
1542 fn test_jpeg_quality_boundary_minimum() {
1543 let cli = Cli::try_parse_from([
1544 "superbook-pdf",
1545 "convert",
1546 "input.pdf",
1547 "--jpeg-quality",
1548 "1",
1549 ])
1550 .unwrap();
1551 if let Commands::Convert(args) = cli.command {
1552 assert_eq!(args.jpeg_quality, 1);
1553 }
1554 }
1555
1556 #[test]
1557 fn test_jpeg_quality_boundary_maximum() {
1558 let cli = Cli::try_parse_from([
1559 "superbook-pdf",
1560 "convert",
1561 "input.pdf",
1562 "--jpeg-quality",
1563 "100",
1564 ])
1565 .unwrap();
1566 if let Commands::Convert(args) = cli.command {
1567 assert_eq!(args.jpeg_quality, 100);
1568 }
1569 }
1570
1571 #[test]
1572 fn test_jpeg_quality_invalid_zero() {
1573 let result = Cli::try_parse_from([
1574 "superbook-pdf",
1575 "convert",
1576 "input.pdf",
1577 "--jpeg-quality",
1578 "0",
1579 ]);
1580 assert!(result.is_err());
1581 }
1582
1583 #[test]
1584 fn test_jpeg_quality_invalid_over_100() {
1585 let result = Cli::try_parse_from([
1586 "superbook-pdf",
1587 "convert",
1588 "input.pdf",
1589 "--jpeg-quality",
1590 "101",
1591 ]);
1592 assert!(result.is_err());
1593 }
1594
1595 #[test]
1596 fn test_threads_boundary_one() {
1597 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--threads", "1"])
1598 .unwrap();
1599 if let Commands::Convert(args) = cli.command {
1600 assert_eq!(args.threads, Some(1));
1601 }
1602 }
1603
1604 #[test]
1605 fn test_threads_boundary_large() {
1606 let cli =
1607 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--threads", "128"])
1608 .unwrap();
1609 if let Commands::Convert(args) = cli.command {
1610 assert_eq!(args.threads, Some(128));
1611 }
1612 }
1613
1614 #[test]
1617 fn test_chunk_size_default() {
1618 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
1619 if let Commands::Convert(args) = cli.command {
1620 assert_eq!(args.chunk_size, 0);
1621 }
1622 }
1623
1624 #[test]
1625 fn test_chunk_size_explicit() {
1626 let cli =
1627 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--chunk-size", "10"])
1628 .unwrap();
1629 if let Commands::Convert(args) = cli.command {
1630 assert_eq!(args.chunk_size, 10);
1631 }
1632 }
1633
1634 #[test]
1635 fn test_chunk_size_large() {
1636 let cli =
1637 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--chunk-size", "100"])
1638 .unwrap();
1639 if let Commands::Convert(args) = cli.command {
1640 assert_eq!(args.chunk_size, 100);
1641 }
1642 }
1643
1644 #[test]
1645 fn test_chunk_size_with_threads() {
1646 let cli = Cli::try_parse_from([
1647 "superbook-pdf",
1648 "convert",
1649 "input.pdf",
1650 "--threads",
1651 "4",
1652 "--chunk-size",
1653 "20",
1654 ])
1655 .unwrap();
1656 if let Commands::Convert(args) = cli.command {
1657 assert_eq!(args.threads, Some(4));
1658 assert_eq!(args.chunk_size, 20);
1659 }
1660 }
1661
1662 #[test]
1663 fn test_margin_trim_boundary_zero() {
1664 let cli = Cli::try_parse_from([
1665 "superbook-pdf",
1666 "convert",
1667 "input.pdf",
1668 "--margin-trim",
1669 "0.0",
1670 ])
1671 .unwrap();
1672 if let Commands::Convert(args) = cli.command {
1673 assert_eq!(args.margin_trim, 0.0);
1674 }
1675 }
1676
1677 #[test]
1678 fn test_margin_trim_boundary_large() {
1679 let cli = Cli::try_parse_from([
1680 "superbook-pdf",
1681 "convert",
1682 "input.pdf",
1683 "--margin-trim",
1684 "50.0",
1685 ])
1686 .unwrap();
1687 if let Commands::Convert(args) = cli.command {
1688 assert_eq!(args.margin_trim, 50.0);
1689 }
1690 }
1691
1692 #[test]
1693 fn test_verbose_boundary_maximum() {
1694 let cli = Cli::try_parse_from([
1695 "superbook-pdf",
1696 "convert",
1697 "input.pdf",
1698 "-vvvvvvvv", ])
1700 .unwrap();
1701 if let Commands::Convert(args) = cli.command {
1702 assert_eq!(args.verbose, 8);
1703 }
1704 }
1705
1706 #[test]
1707 fn test_exit_code_all_variants() {
1708 let codes = [
1709 ExitCode::Success,
1710 ExitCode::GeneralError,
1711 ExitCode::InvalidArgs,
1712 ExitCode::InputNotFound,
1713 ExitCode::OutputError,
1714 ExitCode::ProcessingError,
1715 ExitCode::GpuError,
1716 ExitCode::ExternalToolError,
1717 ];
1718
1719 for code in codes {
1720 assert!(!code.description().is_empty());
1721 assert!(code.code() <= 7);
1722 }
1723 }
1724
1725 #[test]
1726 fn test_path_with_special_characters() {
1727 let paths = [
1728 "file with spaces.pdf",
1729 "日本語ファイル.pdf",
1730 "file-with-dashes.pdf",
1731 "file_with_underscores.pdf",
1732 ];
1733
1734 for path in paths {
1735 let cli = Cli::try_parse_from(["superbook-pdf", "convert", path]).unwrap();
1736 if let Commands::Convert(args) = cli.command {
1737 assert_eq!(args.input.to_string_lossy(), path);
1738 }
1739 }
1740 }
1741
1742 #[test]
1743 fn test_output_path_variants() {
1744 let outputs = ["./output", "/tmp/out", "../parent/out", "relative/path"];
1745
1746 for output in outputs {
1747 let cli =
1748 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-o", output]).unwrap();
1749 if let Commands::Convert(args) = cli.command {
1750 assert_eq!(args.output.to_string_lossy(), output);
1751 }
1752 }
1753 }
1754
1755 #[test]
1758 fn test_internal_resolution_flag() {
1759 let cli = Cli::try_parse_from([
1760 "superbook-pdf",
1761 "convert",
1762 "input.pdf",
1763 "--internal-resolution",
1764 ])
1765 .unwrap();
1766 if let Commands::Convert(args) = cli.command {
1767 assert!(args.internal_resolution);
1768 assert!(args.effective_internal_resolution());
1769 }
1770 }
1771
1772 #[test]
1773 fn test_color_correction_flag() {
1774 let cli = Cli::try_parse_from([
1775 "superbook-pdf",
1776 "convert",
1777 "input.pdf",
1778 "--color-correction",
1779 ])
1780 .unwrap();
1781 if let Commands::Convert(args) = cli.command {
1782 assert!(args.color_correction);
1783 assert!(args.effective_color_correction());
1784 }
1785 }
1786
1787 #[test]
1788 fn test_offset_alignment_flag() {
1789 let cli = Cli::try_parse_from([
1790 "superbook-pdf",
1791 "convert",
1792 "input.pdf",
1793 "--offset-alignment",
1794 ])
1795 .unwrap();
1796 if let Commands::Convert(args) = cli.command {
1797 assert!(args.offset_alignment);
1798 assert!(args.effective_offset_alignment());
1799 }
1800 }
1801
1802 #[test]
1803 fn test_output_height_default() {
1804 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
1805 if let Commands::Convert(args) = cli.command {
1806 assert_eq!(args.output_height, 3508);
1807 }
1808 }
1809
1810 #[test]
1811 fn test_output_height_custom() {
1812 let cli = Cli::try_parse_from([
1813 "superbook-pdf",
1814 "convert",
1815 "input.pdf",
1816 "--output-height",
1817 "7016",
1818 ])
1819 .unwrap();
1820 if let Commands::Convert(args) = cli.command {
1821 assert_eq!(args.output_height, 7016);
1822 }
1823 }
1824
1825 #[test]
1826 fn test_advanced_flag_enables_all() {
1827 let cli =
1828 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--advanced"]).unwrap();
1829 if let Commands::Convert(args) = cli.command {
1830 assert!(args.advanced);
1831 assert!(args.effective_internal_resolution());
1832 assert!(args.effective_color_correction());
1833 assert!(args.effective_offset_alignment());
1834 }
1835 }
1836
1837 #[test]
1838 fn test_advanced_flag_combined() {
1839 let cli = Cli::try_parse_from([
1840 "superbook-pdf",
1841 "convert",
1842 "input.pdf",
1843 "--advanced",
1844 "--output-height",
1845 "4000",
1846 ])
1847 .unwrap();
1848 if let Commands::Convert(args) = cli.command {
1849 assert!(args.advanced);
1850 assert_eq!(args.output_height, 4000);
1851 assert!(args.effective_internal_resolution());
1852 }
1853 }
1854
1855 #[test]
1856 fn test_individual_flags_without_advanced() {
1857 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
1858 if let Commands::Convert(args) = cli.command {
1859 assert!(!args.advanced);
1860 assert!(!args.internal_resolution);
1861 assert!(!args.color_correction);
1862 assert!(!args.offset_alignment);
1863 assert!(!args.effective_internal_resolution());
1864 assert!(!args.effective_color_correction());
1865 assert!(!args.effective_offset_alignment());
1866 }
1867 }
1868
1869 #[test]
1872 fn test_skip_existing_default() {
1873 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
1874 if let Commands::Convert(args) = cli.command {
1875 assert!(!args.skip_existing);
1876 }
1877 }
1878
1879 #[test]
1880 fn test_skip_existing_enabled() {
1881 let cli =
1882 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--skip-existing"])
1883 .unwrap();
1884 if let Commands::Convert(args) = cli.command {
1885 assert!(args.skip_existing);
1886 }
1887 }
1888
1889 #[test]
1890 fn test_skip_existing_with_other_options() {
1891 let cli = Cli::try_parse_from([
1892 "superbook-pdf",
1893 "convert",
1894 "input.pdf",
1895 "--skip-existing",
1896 "--advanced",
1897 "-v",
1898 ])
1899 .unwrap();
1900 if let Commands::Convert(args) = cli.command {
1901 assert!(args.skip_existing);
1902 assert!(args.advanced);
1903 assert_eq!(args.verbose, 1);
1904 }
1905 }
1906
1907 #[test]
1910 fn test_max_pages_default() {
1911 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
1912 if let Commands::Convert(args) = cli.command {
1913 assert!(args.max_pages.is_none());
1914 }
1915 }
1916
1917 #[test]
1918 fn test_max_pages_explicit() {
1919 let cli =
1920 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--max-pages", "10"])
1921 .unwrap();
1922 if let Commands::Convert(args) = cli.command {
1923 assert_eq!(args.max_pages, Some(10));
1924 }
1925 }
1926
1927 #[test]
1928 fn test_save_debug_default() {
1929 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
1930 if let Commands::Convert(args) = cli.command {
1931 assert!(!args.save_debug);
1932 }
1933 }
1934
1935 #[test]
1936 fn test_save_debug_enabled() {
1937 let cli =
1938 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--save-debug"]).unwrap();
1939 if let Commands::Convert(args) = cli.command {
1940 assert!(args.save_debug);
1941 }
1942 }
1943
1944 #[test]
1945 fn test_debug_options_combined() {
1946 let cli = Cli::try_parse_from([
1947 "superbook-pdf",
1948 "convert",
1949 "input.pdf",
1950 "--max-pages",
1951 "5",
1952 "--save-debug",
1953 ])
1954 .unwrap();
1955 if let Commands::Convert(args) = cli.command {
1956 assert_eq!(args.max_pages, Some(5));
1957 assert!(args.save_debug);
1958 }
1959 }
1960
1961 #[test]
1964 fn test_force_default() {
1965 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf"]).unwrap();
1966 if let Commands::Convert(args) = cli.command {
1967 assert!(!args.force);
1968 }
1969 }
1970
1971 #[test]
1972 fn test_force_long() {
1973 let cli =
1974 Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "--force"]).unwrap();
1975 if let Commands::Convert(args) = cli.command {
1976 assert!(args.force);
1977 }
1978 }
1979
1980 #[test]
1981 fn test_force_short() {
1982 let cli = Cli::try_parse_from(["superbook-pdf", "convert", "input.pdf", "-f"]).unwrap();
1983 if let Commands::Convert(args) = cli.command {
1984 assert!(args.force);
1985 }
1986 }
1987
1988 #[test]
1989 fn test_force_with_skip_existing() {
1990 let cli = Cli::try_parse_from([
1992 "superbook-pdf",
1993 "convert",
1994 "input.pdf",
1995 "--skip-existing",
1996 "--force",
1997 ])
1998 .unwrap();
1999 if let Commands::Convert(args) = cli.command {
2000 assert!(args.skip_existing);
2001 assert!(args.force);
2002 }
2003 }
2004
2005 #[test]
2008 fn test_cache_info_command() {
2009 let cli =
2010 Cli::try_parse_from(["superbook-pdf", "cache-info", "output.pdf"]).unwrap();
2011 if let Commands::CacheInfo(args) = cli.command {
2012 assert_eq!(args.output_pdf, PathBuf::from("output.pdf"));
2013 } else {
2014 panic!("Expected CacheInfo command");
2015 }
2016 }
2017
2018 #[test]
2019 fn test_cache_info_with_path() {
2020 let cli = Cli::try_parse_from([
2021 "superbook-pdf",
2022 "cache-info",
2023 "/path/to/output.pdf",
2024 ])
2025 .unwrap();
2026 if let Commands::CacheInfo(args) = cli.command {
2027 assert_eq!(args.output_pdf, PathBuf::from("/path/to/output.pdf"));
2028 } else {
2029 panic!("Expected CacheInfo command");
2030 }
2031 }
2032
2033 #[test]
2034 fn test_cache_info_missing_path() {
2035 let result = Cli::try_parse_from(["superbook-pdf", "cache-info"]);
2036 assert!(result.is_err());
2037 }
2038
2039 #[test]
2042 fn test_reprocess_command_basic() {
2043 let cli = Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf"]).unwrap();
2044 if let Commands::Reprocess(args) = cli.command {
2045 assert_eq!(args.input, PathBuf::from("input.pdf"));
2046 assert!(args.pages.is_none());
2047 assert_eq!(args.max_retries, 3);
2048 assert!(!args.force);
2049 assert!(!args.status);
2050 } else {
2051 panic!("Expected Reprocess command");
2052 }
2053 }
2054
2055 #[test]
2056 fn test_reprocess_with_status() {
2057 let cli =
2058 Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf", "--status"]).unwrap();
2059 if let Commands::Reprocess(args) = cli.command {
2060 assert!(args.status);
2061 } else {
2062 panic!("Expected Reprocess command");
2063 }
2064 }
2065
2066 #[test]
2067 fn test_reprocess_with_pages() {
2068 let cli = Cli::try_parse_from([
2069 "superbook-pdf",
2070 "reprocess",
2071 "input.pdf",
2072 "--pages",
2073 "1,3,5,10",
2074 ])
2075 .unwrap();
2076 if let Commands::Reprocess(args) = cli.command {
2077 assert_eq!(args.pages, Some(vec![1, 3, 5, 10]));
2078 assert_eq!(args.page_indices(), vec![1, 3, 5, 10]);
2079 } else {
2080 panic!("Expected Reprocess command");
2081 }
2082 }
2083
2084 #[test]
2085 fn test_reprocess_with_max_retries() {
2086 let cli = Cli::try_parse_from([
2087 "superbook-pdf",
2088 "reprocess",
2089 "input.pdf",
2090 "--max-retries",
2091 "5",
2092 ])
2093 .unwrap();
2094 if let Commands::Reprocess(args) = cli.command {
2095 assert_eq!(args.max_retries, 5);
2096 } else {
2097 panic!("Expected Reprocess command");
2098 }
2099 }
2100
2101 #[test]
2102 fn test_reprocess_with_force() {
2103 let cli =
2104 Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf", "--force"]).unwrap();
2105 if let Commands::Reprocess(args) = cli.command {
2106 assert!(args.force);
2107 } else {
2108 panic!("Expected Reprocess command");
2109 }
2110 }
2111
2112 #[test]
2113 fn test_reprocess_with_force_short() {
2114 let cli = Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf", "-f"]).unwrap();
2115 if let Commands::Reprocess(args) = cli.command {
2116 assert!(args.force);
2117 } else {
2118 panic!("Expected Reprocess command");
2119 }
2120 }
2121
2122 #[test]
2123 fn test_reprocess_with_output() {
2124 let cli = Cli::try_parse_from([
2125 "superbook-pdf",
2126 "reprocess",
2127 "input.pdf",
2128 "--output",
2129 "/custom/output",
2130 ])
2131 .unwrap();
2132 if let Commands::Reprocess(args) = cli.command {
2133 assert_eq!(args.output, Some(PathBuf::from("/custom/output")));
2134 } else {
2135 panic!("Expected Reprocess command");
2136 }
2137 }
2138
2139 #[test]
2140 fn test_reprocess_with_keep_intermediates() {
2141 let cli = Cli::try_parse_from([
2142 "superbook-pdf",
2143 "reprocess",
2144 "input.pdf",
2145 "--keep-intermediates",
2146 ])
2147 .unwrap();
2148 if let Commands::Reprocess(args) = cli.command {
2149 assert!(args.keep_intermediates);
2150 } else {
2151 panic!("Expected Reprocess command");
2152 }
2153 }
2154
2155 #[test]
2156 fn test_reprocess_with_verbose() {
2157 let cli =
2158 Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf", "-vvv"]).unwrap();
2159 if let Commands::Reprocess(args) = cli.command {
2160 assert_eq!(args.verbose, 3);
2161 } else {
2162 panic!("Expected Reprocess command");
2163 }
2164 }
2165
2166 #[test]
2167 fn test_reprocess_with_quiet() {
2168 let cli =
2169 Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf", "-q"]).unwrap();
2170 if let Commands::Reprocess(args) = cli.command {
2171 assert!(args.quiet);
2172 } else {
2173 panic!("Expected Reprocess command");
2174 }
2175 }
2176
2177 #[test]
2178 fn test_reprocess_state_file_detection() {
2179 let cli = Cli::try_parse_from([
2180 "superbook-pdf",
2181 "reprocess",
2182 "output/.superbook-state.json",
2183 ])
2184 .unwrap();
2185 if let Commands::Reprocess(args) = cli.command {
2186 assert!(args.is_state_file());
2187 } else {
2188 panic!("Expected Reprocess command");
2189 }
2190
2191 let cli2 = Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf"]).unwrap();
2192 if let Commands::Reprocess(args) = cli2.command {
2193 assert!(!args.is_state_file());
2194 } else {
2195 panic!("Expected Reprocess command");
2196 }
2197 }
2198
2199 #[test]
2200 fn test_reprocess_page_indices_empty() {
2201 let cli = Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf"]).unwrap();
2202 if let Commands::Reprocess(args) = cli.command {
2203 assert!(args.page_indices().is_empty());
2204 } else {
2205 panic!("Expected Reprocess command");
2206 }
2207 }
2208
2209 #[test]
2210 fn test_reprocess_full_options() {
2211 let cli = Cli::try_parse_from([
2212 "superbook-pdf",
2213 "reprocess",
2214 "input.pdf",
2215 "--pages",
2216 "1,2,3",
2217 "--max-retries",
2218 "5",
2219 "--force",
2220 "--output",
2221 "/custom/output",
2222 "--keep-intermediates",
2223 "-vv",
2224 ])
2225 .unwrap();
2226 if let Commands::Reprocess(args) = cli.command {
2227 assert_eq!(args.input, PathBuf::from("input.pdf"));
2228 assert_eq!(args.pages, Some(vec![1, 2, 3]));
2229 assert_eq!(args.max_retries, 5);
2230 assert!(args.force);
2231 assert_eq!(args.output, Some(PathBuf::from("/custom/output")));
2232 assert!(args.keep_intermediates);
2233 assert_eq!(args.verbose, 2);
2234 } else {
2235 panic!("Expected Reprocess command");
2236 }
2237 }
2238
2239 #[test]
2240 fn test_reprocess_missing_input() {
2241 let result = Cli::try_parse_from(["superbook-pdf", "reprocess"]);
2242 assert!(result.is_err());
2243 }
2244
2245 #[test]
2246 fn test_reprocess_debug_impl() {
2247 let cli = Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf"]).unwrap();
2248 let debug_str = format!("{:?}", cli.command);
2249 assert!(debug_str.contains("Reprocess"));
2250 }
2251
2252 #[test]
2253 fn test_reprocess_args_clone() {
2254 let cli = Cli::try_parse_from(["superbook-pdf", "reprocess", "input.pdf", "--force"])
2255 .unwrap();
2256 if let Commands::Reprocess(args) = cli.command {
2257 let cloned = args.clone();
2258 assert_eq!(args.input, cloned.input);
2259 assert_eq!(args.force, cloned.force);
2260 } else {
2261 panic!("Expected Reprocess command");
2262 }
2263 }
2264}