diff --git a/.gitignore b/.gitignore index 4d7ecc7..c528a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ eyez build/ +coverage/ coverage.out *.DS_Store diff --git a/cmd/commands.go b/cmd/commands.go index 8617856..bf496a7 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -25,7 +25,6 @@ type commands struct { func (c *commands) ByArgs(filename string, width int64) error { err := validator.Validate(filename) if err != nil { - fmt.Println(err) return err } diff --git a/cmd/commands_test.go b/cmd/commands_test.go index 3602aa1..3c8d014 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -5,6 +5,7 @@ import ( "image/color" "image/png" "os" + "os/exec" "path/filepath" "testing" @@ -81,6 +82,14 @@ func TestByArgs(t *testing.T) { if err == nil { t.Errorf("ByArgs expected error for unsupported format, got nil") } + + // Test valid extension but invalid image data + invalidImgPath := filepath.Join(t.TempDir(), "invalid.png") + os.WriteFile(invalidImgPath, []byte("not an image"), 0644) + err = c.ByArgs(invalidImgPath, 10) + if err == nil { + t.Errorf("ByArgs expected error for invalid image data, got nil") + } } func TestByStdin(t *testing.T) { @@ -118,3 +127,31 @@ func TestByStdin(t *testing.T) { t.Errorf("ByStdin expected error for invalid image data, got nil") } } + +func TestNewCommandsInvalidGraphics(t *testing.T) { + if os.Getenv("BE_CRASHER") == "1" { + NewCommands("invalid_graphics", consts.ALGO_LANCZOS) + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestNewCommandsInvalidGraphics") + cmd.Env = append(os.Environ(), "BE_CRASHER=1") + err := cmd.Run() + if e, ok := err.(*exec.ExitError); ok && !e.Success() { + return + } + t.Fatalf("process ran with err %v, want exit status != 0", err) +} + +func TestNewCommandsInvalidAlgorithm(t *testing.T) { + if os.Getenv("BE_CRASHER") == "1" { + NewCommands(consts.GRAPHICS_ASCII, "invalid_algorithm") + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestNewCommandsInvalidAlgorithm") + cmd.Env = append(os.Environ(), "BE_CRASHER=1") + err := cmd.Run() + if e, ok := err.(*exec.ExitError); ok && !e.Success() { + return + } + t.Fatalf("process ran with err %v, want exit status != 0", err) +} diff --git a/graphics/kitty_test.go b/graphics/kitty_test.go index 5f587ee..133b1a8 100644 --- a/graphics/kitty_test.go +++ b/graphics/kitty_test.go @@ -2,6 +2,7 @@ package graphics import ( "image" + "math/rand" "os" "strings" "testing" @@ -71,4 +72,109 @@ func TestKittyDraw(t *testing.T) { t.Errorf("expected no error when TERM is xterm-kitty, got: %v", err) } }) + + t.Run("Passes When Large Image Requires Multiple Chunks", func(t *testing.T) { + os.Setenv("KITTY_WINDOW_ID", "123") + os.Setenv("TERM", "") + + // Create a large enough image to ensure it requires multiple chunks (base64 > 4096 bytes) + largeImg := image.NewRGBA(image.Rect(0, 0, 500, 500)) + for i := 0; i < len(largeImg.Pix); i++ { + // Add some pseudo-randomness to prevent high PNG compression + largeImg.Pix[i] = uint8((i * 137) % 256) + } + + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Read continuously to prevent deadlock if pipe buffer fills up + go func() { + var discard [1024]byte + for { + _, err := r.Read(discard[:]) + if err != nil { + break + } + } + }() + + err := k.Draw(largeImg) + + w.Close() + r.Close() + os.Stdout = oldStdout + + if err != nil { + t.Errorf("expected no error when writing large image, got: %v", err) + } + }) + + t.Run("Fails on Encode Error", func(t *testing.T) { + os.Setenv("KITTY_WINDOW_ID", "123") + os.Setenv("TERM", "") + + // 0x0 image will cause png.Encode to fail + err := k.Draw(image.NewRGBA(image.Rect(0, 0, 0, 0))) + if err == nil { + t.Errorf("expected error on zero sized image, got nil") + } + }) + + t.Run("Fails When First Write Errors", func(t *testing.T) { + os.Setenv("KITTY_WINDOW_ID", "123") + os.Setenv("TERM", "") + + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Close the read end immediately + r.Close() + + // Large image with noise forces Fprintf to overflow its bufio buffer and flush immediately on the first chunk + largeImg := image.NewRGBA(image.Rect(0, 0, 500, 500)) + for i := 0; i < len(largeImg.Pix); i++ { + largeImg.Pix[i] = uint8((i * 137) % 256) + } + err := k.Draw(largeImg) + + w.Close() + os.Stdout = oldStdout + + if err == nil { + t.Errorf("expected error when writing to closed pipe on first chunk, got nil") + } + }) + + t.Run("Fails When Second Write Errors", func(t *testing.T) { + os.Setenv("KITTY_WINDOW_ID", "123") + os.Setenv("TERM", "") + + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Close the read end asynchronously to allow the first chunk to succeed + go func() { + buf := make([]byte, 4096) + r.Read(buf) + r.Close() + }() + + // Large noisy image (> 64KB base64) to fill the pipe buffer and block until closed + largeImg := image.NewRGBA(image.Rect(0, 0, 500, 500)) + for i := 0; i < len(largeImg.Pix); i++ { + largeImg.Pix[i] = uint8(rand.Intn(256)) + } + + err := k.Draw(largeImg) + + w.Close() + os.Stdout = oldStdout + + if err == nil { + t.Errorf("expected error when writing to closed pipe on subsequent chunks, got nil") + } + }) } diff --git a/main.go b/main.go index 4f0ff57..2454023 100644 --- a/main.go +++ b/main.go @@ -58,10 +58,16 @@ func main() { isPiped := (stat.Mode() & os.ModeCharDevice) == 0 cmd := cmd.NewCommands(graphics, algo) if isPiped { - cmd.ByStdin(os.Stdin, width) + err := cmd.ByStdin(os.Stdin, width) + if err != nil { + return err + } } else { if c.Args().Len() > 0 { - cmd.ByArgs(c.Args().First(), width) + err := cmd.ByArgs(c.Args().First(), width) + if err != nil { + return err + } } else { fmt.Println("Error: missing required arguments") cli.ShowAppHelp(c) diff --git a/main_test.go b/main_test.go index aa9da2e..e2f1218 100644 --- a/main_test.go +++ b/main_test.go @@ -2,8 +2,12 @@ package main import ( "bytes" + "image" + "image/png" "io" "os" + "os/exec" + "path/filepath" "strings" "testing" ) @@ -76,3 +80,96 @@ func TestMainMissingArgs(t *testing.T) { t.Errorf("expected terminal output to instruct missing arguments, got\n%s", output) } } + +func createTempPNG(t *testing.T) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "test_image.png") + + f, err := os.Create(path) + if err != nil { + t.Fatalf("could not create temp file: %v", err) + } + defer f.Close() + + img := image.NewRGBA(image.Rect(0, 0, 10, 10)) + err = png.Encode(f, img) + if err != nil { + t.Fatalf("failed to encode png: %v", err) + } + return path +} + +func TestMainValidFile(t *testing.T) { + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + path := createTempPNG(t) + os.Args = []string{"eyez", path} + + oldStdout := os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + + main() + + w.Close() + os.Stdout = oldStdout +} + +func TestMainValidPipe(t *testing.T) { + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + os.Args = []string{"eyez"} + + var buf bytes.Buffer + img := image.NewRGBA(image.Rect(0, 0, 10, 10)) + png.Encode(&buf, img) + + oldStdin := os.Stdin + r, w, _ := os.Pipe() + os.Stdin = r + w.Write(buf.Bytes()) + w.Close() + defer func() { os.Stdin = oldStdin }() + + oldStdout := os.Stdout + _, wStd, _ := os.Pipe() + os.Stdout = wStd + + main() + + wStd.Close() + os.Stdout = oldStdout +} + +func TestMainInvalidFileExit(t *testing.T) { + if os.Getenv("BE_CRASHER") == "1" { + os.Args = []string{"eyez", "nonexistent_file.png"} + main() + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestMainInvalidFileExit") + cmd.Env = append(os.Environ(), "BE_CRASHER=1") + err := cmd.Run() + if e, ok := err.(*exec.ExitError); ok && !e.Success() { + return + } + t.Fatalf("process ran with err %v, want exit status 1", err) +} + +func TestMainInvalidStdinExit(t *testing.T) { + if os.Getenv("BE_CRASHER") == "1" { + os.Args = []string{"eyez"} + main() + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestMainInvalidStdinExit") + cmd.Env = append(os.Environ(), "BE_CRASHER=1") + cmd.Stdin = strings.NewReader("not a real image") + err := cmd.Run() + if e, ok := err.(*exec.ExitError); ok && !e.Success() { + return + } + t.Fatalf("process ran with err %v, want exit status 1", err) +}