Files

179 lines
4.1 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package i3ipc
import (
"context"
"net"
"path/filepath"
"testing"
"time"
)
func listener(t *testing.T) net.Listener {
t.Helper()
l, err := net.Listen("unix", filepath.Join(t.TempDir(), "ipc"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { l.Close() })
return l
}
func handshake(c net.Conn, initial string) error {
for _, response := range []string{`{"success":true}`, `[{"id":2,"name":"A","focused":true}]`, `{"id":1,"type":"root","nodes":[]}`} {
m, err := readMessage(c)
if err != nil {
return err
}
if initial != "" {
if err := writeMessage(c, modeEvent, []byte(initial)); err != nil {
return err
}
}
if err := writeMessage(c, m.kind, []byte(response)); err != nil {
return err
}
}
return nil
}
func TestSessionBaselineCancellationAndReconnect(t *testing.T) {
l := listener(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
events := make(chan Event, 8)
done := make(chan struct{})
server := make(chan error, 1)
go func() {
defer close(done)
Run(ctx, func() string { return l.Addr().String() }, func(e Event) { events <- e })
}()
go func() {
for _, name := range []string{"personal", "default"} {
c, err := l.Accept()
if err != nil {
server <- err
return
}
if err = handshake(c, `{"change":"snapshot"}`); err == nil {
err = writeMessage(c, modeEvent, []byte(`{"change":"`+name+`"}`))
}
if name == "default" && err == nil {
var b [1]byte
_, _ = c.Read(b[:])
}
c.Close()
if err != nil {
server <- err
return
}
}
server <- nil
}()
for _, name := range []string{"personal", "default"} {
select {
case e := <-events:
if e.Name != name {
t.Fatal(e)
}
case <-time.After(4 * time.Second):
t.Fatal("missing event/reconnect")
}
}
cancel()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("idle read did not cancel")
}
if err := <-server; err != nil {
t.Fatal(err)
}
select {
case e := <-events:
t.Fatalf("snapshot announced: %+v", e)
default:
}
}
func TestSubscriptionFailure(t *testing.T) {
l := listener(t)
go func() {
c, err := l.Accept()
if err != nil {
return
}
defer c.Close()
m, err := readMessage(c)
if err == nil {
_ = writeMessage(c, m.kind, []byte(`{"success":false}`))
}
}()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := session(ctx, l.Addr().String(), func(Event) { t.Error("unexpected event") }); err == nil {
t.Fatal("accepted rejection")
}
}
func TestUnavailableAndCanceledQuery(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := session(ctx, "/nonexistent/i3", func(Event) {}); err == nil {
t.Fatal("unavailable socket accepted")
}
l := listener(t)
accepted := make(chan struct{})
go func() {
c, err := l.Accept()
if err != nil {
return
}
defer c.Close()
close(accepted)
var b [4096]byte
for {
if _, err := c.Read(b[:]); err != nil {
return
}
}
}()
ctx, cancel = context.WithCancel(context.Background())
defer cancel()
done := make(chan error, 1)
go func() { done <- session(ctx, l.Addr().String(), func(Event) {}) }()
<-accepted
cancel()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("query did not cancel")
}
}
func TestInterleavedEventsAndBacklog(t *testing.T) {
a, b := net.Pipe()
defer a.Close()
defer b.Close()
c := connection{Conn: a}
go func() {
m, err := readMessage(b)
if err != nil {
return
}
_ = writeMessage(b, modeEvent, []byte(`{"change":"panel"}`))
_ = writeMessage(b, m.kind, []byte(`{"success":true}`))
}()
var result struct{ Success bool }
if err := c.request(subscribe, nil, &result); err != nil || !result.Success || len(c.pending) != 1 {
t.Fatalf("%+v %v", c.pending, err)
}
c.pending = make([]message, maxPending)
go func() {
_, err := readMessage(b)
if err == nil {
_ = writeMessage(b, modeEvent, []byte(`{}`))
}
}()
if err := c.request(tree, nil, &result); err == nil {
t.Fatal("unbounded pending events")
}
}