Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
eyez
build/
coverage/

coverage.out
*.DS_Store
Expand Down
1 change: 0 additions & 1 deletion cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
37 changes: 37 additions & 0 deletions cmd/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"image/color"
"image/png"
"os"
"os/exec"
"path/filepath"
"testing"

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
106 changes: 106 additions & 0 deletions graphics/kitty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package graphics

import (
"image"
"math/rand"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -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")
}
})
}
10 changes: 8 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
97 changes: 97 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package main

import (
"bytes"
"image"
"image/png"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
Expand Down Expand Up @@ -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)
}
Loading