Unit tests & docs #34
@@ -14,6 +14,13 @@ steps:
|
||||
depends_on:
|
||||
- go build
|
||||
|
||||
- name: unit tests
|
||||
image: golang:1.26
|
||||
commands:
|
||||
- go test -timeout 60s -v ./...
|
||||
depends_on:
|
||||
- go build
|
||||
|
||||
trigger:
|
||||
action:
|
||||
exclude:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package iroh_ffi
|
||||
|
||||
// Application-Layer Protocol Negotiation
|
||||
type Alpn []byte
|
||||
|
||||
// List of ALPNs
|
||||
func Alpns(alpns ...Alpn) [][]byte {
|
||||
var a [][]byte
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// Accept implements the receiving end of a endpoint.
|
||||
func (e *Endpoint) Accept(ctx context.Context, a Accepter) error {
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
@@ -39,6 +40,7 @@ func (e *Endpoint) Accept(ctx context.Context, a Accepter) error {
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
// Open implements the remote connection of an endpoint.
|
||||
func (e *Endpoint) Open(ctx context.Context, addr *EndpointAddr, alpn Alpn, o Opener) error {
|
||||
conn, err := e.Connect(addr, alpn)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package endpoint_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
iroh "git.coopcloud.tech/decentral1se/iroh-go"
|
||||
"git.coopcloud.tech/decentral1se/iroh-go/endpoint"
|
||||
)
|
||||
|
||||
func TestEndpoint(t *testing.T) {
|
||||
alpn := iroh.Alpn("foo/0")
|
||||
|
||||
e, err := endpoint.New(
|
||||
endpoint.WithAlpns(alpn),
|
||||
endpoint.WithApplyMinimal(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("want endpoint creation, got: %s", iroh.As(err))
|
||||
}
|
||||
|
||||
if err := e.Close(); err != nil {
|
||||
t.Errorf("want endpoint close, got: %s", iroh.As(err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package iroh_ffi_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
iroh "git.coopcloud.tech/decentral1se/iroh-go"
|
||||
"git.coopcloud.tech/decentral1se/iroh-go/endpoint"
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
type TestAcceptOpener struct {
|
||||
ExpectedOutput string
|
||||
ReceivedOutput string
|
||||
}
|
||||
|
||||
func (t *TestAcceptOpener) Open(ctx context.Context, conn *iroh.Connection) error {
|
||||
stream, err := conn.OpenBi()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = stream.Ready(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recv := stream.Recv()
|
||||
|
||||
frame, err := recv.ReadToEnd(1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.ReceivedOutput += string(frame)
|
||||
|
||||
if err := conn.Close(0, []byte("eof")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestAcceptOpener) Accept(ctx context.Context, conn *iroh.Connection) error {
|
||||
stream, err := conn.AcceptBi()
|
||||
if err := iroh.As(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
send := stream.Send()
|
||||
|
||||
if err := send.WriteAll([]byte(t.ExpectedOutput)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := send.Finish(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Closed()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestEndpoint(t *testing.T) {
|
||||
alpn := iroh.Alpn("iroh-go-endpoint-test/0")
|
||||
|
||||
e1, err := endpoint.New(
|
||||
endpoint.WithAlpns(alpn),
|
||||
endpoint.WithApplyN0(),
|
||||
)
|
||||
if err := iroh.As(err); err != nil {
|
||||
t.Errorf("want endpoint create, got: %s", err)
|
||||
}
|
||||
|
||||
e1.Online()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
c := make(chan struct{})
|
||||
|
||||
tao := &TestAcceptOpener{ExpectedOutput: "ping"}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
wg.Go(func() {
|
||||
if err := e1.Accept(ctx, tao); err != nil {
|
||||
t.Errorf("want endpoint accept, got: %s", iroh.As(err))
|
||||
}
|
||||
if err := e1.Close(); err != nil {
|
||||
t.Errorf("want endpoint close, got: %s", iroh.As(err))
|
||||
}
|
||||
})
|
||||
|
||||
e2, err := endpoint.New(
|
||||
endpoint.WithAlpns(alpn),
|
||||
endpoint.WithApplyN0(),
|
||||
)
|
||||
if err := iroh.As(err); err != nil {
|
||||
t.Errorf("want endpoint create, got: %s", err)
|
||||
}
|
||||
|
||||
e2.Online()
|
||||
|
||||
wg.Go(func() {
|
||||
if err := e2.Open(ctx, e1.Addr(), alpn, tao); err != nil {
|
||||
t.Errorf("want endpoint open, got: %s", iroh.As(err))
|
||||
}
|
||||
if err := e2.Close(); err != nil {
|
||||
t.Errorf("want endpoint close, got: %s", iroh.As(err))
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
c <- struct{}{}
|
||||
}()
|
||||
|
||||
timeout := time.Duration(10) * time.Second
|
||||
|
||||
select {
|
||||
case <-c:
|
||||
assert.Equal(t, tao.ReceivedOutput, tao.ExpectedOutput)
|
||||
case <-time.After(timeout):
|
||||
t.Errorf("want graceful close, got: timeout of 10 seconds")
|
||||
}
|
||||
}
|
||||
@@ -77,9 +77,11 @@ func send(e *iroh.Endpoint, id string) {
|
||||
}
|
||||
|
||||
// mark send stream as done
|
||||
p.Finish()
|
||||
exit(conn)
|
||||
if err := p.Finish(); err != nil {
|
||||
panic(iroh.As(err))
|
||||
}
|
||||
|
||||
exit(conn)
|
||||
}
|
||||
|
||||
func exit(conn *iroh.Connection) {
|
||||
|
||||
@@ -2,4 +2,7 @@ module git.coopcloud.tech/decentral1se/iroh-go
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require golang.org/x/sync v0.24.0
|
||||
require (
|
||||
github.com/magiconair/properties v1.18.12
|
||||
golang.org/x/sync v0.24.0
|
||||
)
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
github.com/magiconair/properties v1.18.12 h1:sT9zQpvTB3B4gzrX0tmZNTEaGyg8Zw55MFYRE32Mr9I=
|
||||
github.com/magiconair/properties v1.18.12/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
|
||||
@@ -2,14 +2,19 @@ package iroh_ffi
|
||||
|
||||
import "context"
|
||||
|
||||
// Accepter implements the incoming side of a BiStream. This is still
|
||||
// peer-to-peer and the incoming side can send and receive.
|
||||
type Accepter interface {
|
||||
Accept(context.Context, *Connection) error
|
||||
}
|
||||
|
||||
// Opener implements the connecting side of a BiStream. This is still
|
||||
// peer-to-peer and the connecting side can send and receive.
|
||||
type Opener interface {
|
||||
Open(context.Context, *Connection) error
|
||||
}
|
||||
|
||||
// AcceptOpener combines both the "incoming" and "outgoing" interfaces.
|
||||
type AcceptOpener interface {
|
||||
Accepter
|
||||
Opener
|
||||
|
||||
@@ -5,4 +5,6 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Log is the library-wide internal logger. It can be configured using the
|
||||
// `endpoint.New(...)` options pattern. See `WithDebug` for more.
|
||||
var Log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{}))
|
||||
|
||||
@@ -5,14 +5,16 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Pipe is a BiStream which supports the Reader/Writer interface.
|
||||
type Pipe struct {
|
||||
stream *BiStream
|
||||
recv *RecvStream
|
||||
send *SendStream
|
||||
totalBytesRead int
|
||||
finished bool
|
||||
finished bool
|
||||
}
|
||||
|
||||
// NewPipe creates a new Pipe.
|
||||
func NewPipe(stream *BiStream) *Pipe {
|
||||
return &Pipe{
|
||||
stream: stream,
|
||||
@@ -21,6 +23,7 @@ func NewPipe(stream *BiStream) *Pipe {
|
||||
}
|
||||
}
|
||||
|
||||
// Ready ensures the other side of the Pipe knows that it is open.
|
||||
func (p *Pipe) Ready() error {
|
||||
if err := p.stream.Ready(); err != nil {
|
||||
return err
|
||||
@@ -28,6 +31,7 @@ func (p *Pipe) Ready() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read implements the Reader interface.
|
||||
func (p *Pipe) Read(buf []byte) (int, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
@@ -56,6 +60,7 @@ func (p *Pipe) Read(buf []byte) (int, error) {
|
||||
return len(retbuf), err
|
||||
}
|
||||
|
||||
// Write implements the Writer interface.
|
||||
func (p *Pipe) Write(buf []byte) (int, error) {
|
||||
if p.finished {
|
||||
return 0, errors.New("Pipe.Write: Write called after Finish()")
|
||||
@@ -71,6 +76,9 @@ func (p *Pipe) Write(buf []byte) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Finish called on the send stream signals that we will not send anything
|
||||
// further, which makes the receive side of the Pipe on the other end
|
||||
// terminate.
|
||||
func (p *Pipe) Finish() error {
|
||||
p.finished = true
|
||||
return p.send.Finish()
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package iroh_ffi_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
iroh "git.coopcloud.tech/decentral1se/iroh-go"
|
||||
"git.coopcloud.tech/decentral1se/iroh-go/endpoint"
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
type TestAcceptOpenerPipe struct {
|
||||
ExpectedOutput bytes.Buffer
|
||||
ReceivedOutput bytes.Buffer
|
||||
}
|
||||
|
||||
func (t *TestAcceptOpenerPipe) Open(ctx context.Context, conn *iroh.Connection) error {
|
||||
stream, err := conn.OpenBi()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := iroh.NewPipe(stream)
|
||||
|
||||
if err := p.Ready(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(p, &t.ExpectedOutput)
|
||||
if err = iroh.As(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.Finish(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Closed()
|
||||
|
||||
if err := conn.Close(0, []byte("eof")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestAcceptOpenerPipe) Accept(ctx context.Context, conn *iroh.Connection) error {
|
||||
stream, err := conn.AcceptBi()
|
||||
if err := iroh.As(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := iroh.NewPipe(stream)
|
||||
|
||||
_, err = io.Copy(p, &t.ReceivedOutput)
|
||||
if err = iroh.As(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPipe(t *testing.T) {
|
||||
alpn := iroh.Alpn("iroh-go-pipe-test/0")
|
||||
|
||||
e1, err := endpoint.New(
|
||||
endpoint.WithAlpns(alpn),
|
||||
endpoint.WithApplyN0(),
|
||||
)
|
||||
if err := iroh.As(err); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
e1.Online()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
c := make(chan struct{})
|
||||
|
||||
tao := &TestAcceptOpenerPipe{
|
||||
ExpectedOutput: *bytes.NewBufferString("ping"),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
wg.Go(func() {
|
||||
if err := e1.Accept(ctx, tao); err != nil {
|
||||
t.Errorf("want endpoint accept, got: %s", iroh.As(err))
|
||||
}
|
||||
if err := e1.Close(); err != nil {
|
||||
t.Errorf("want endpoint close, got: %s", iroh.As(err))
|
||||
}
|
||||
})
|
||||
|
||||
e2, err := endpoint.New(
|
||||
endpoint.WithAlpns(alpn),
|
||||
endpoint.WithApplyN0(),
|
||||
)
|
||||
if err := iroh.As(err); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
e2.Online()
|
||||
|
||||
wg.Go(func() {
|
||||
if err := e2.Open(ctx, e1.Addr(), alpn, tao); err != nil {
|
||||
t.Errorf("want endpoint open, got: %s", iroh.As(err))
|
||||
}
|
||||
if err := e2.Close(); err != nil {
|
||||
t.Errorf("want endpoint close, got: %s", iroh.As(err))
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
c <- struct{}{}
|
||||
}()
|
||||
|
||||
timeout := time.Duration(10) * time.Second
|
||||
|
||||
select {
|
||||
case <-c:
|
||||
assert.Equal(t, tao.ReceivedOutput.String(), tao.ExpectedOutput.String())
|
||||
case <-time.After(timeout):
|
||||
t.Errorf("want graceful close, got: timeout of 10 seconds")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package iroh_ffi
|
||||
|
||||
// Ready ensures the other side of the BiStream knows that the stream is open.
|
||||
func (s *BiStream) Ready() error {
|
||||
if err := s.Send().WriteAll([]byte{1}); err != nil {
|
||||
return err
|
||||
|
||||
+4
-3
@@ -4,8 +4,9 @@ import (
|
||||
"crypto/sha256"
|
||||
)
|
||||
|
||||
// SecretBytesFromString creates secret bytes from a string.
|
||||
func SecretBytesFromString(str string) []byte {
|
||||
// TODO (2026-11-09): securely clear all arrays to the extent possible
|
||||
digest := sha256.Sum256([]byte(str))
|
||||
return digest[:] // copy out bytes from strict [32]byte type to []byte used by everything else + iroh
|
||||
// TODO (2026-11-09): securely clear all arrays to the extent possible
|
||||
digest := sha256.Sum256([]byte(str))
|
||||
return digest[:] // copy out bytes from strict [32]byte type to []byte used by everything else + iroh
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
iroh "git.coopcloud.tech/decentral1se/iroh-go"
|
||||
"git.coopcloud.tech/decentral1se/iroh-go/endpoint"
|
||||
"git.coopcloud.tech/decentral1se/iroh-go/util"
|
||||
)
|
||||
|
||||
func TestSecretBytesFromString(t *testing.T) {
|
||||
alpn := iroh.Alpn("iroh-go-util-test/0")
|
||||
|
||||
e, err := endpoint.New(
|
||||
endpoint.WithAlpns(alpn),
|
||||
endpoint.WithApplyN0(),
|
||||
endpoint.WithSecretKey(util.SecretBytesFromString("hello-secret-bytes")),
|
||||
)
|
||||
if err := iroh.As(err); err != nil {
|
||||
t.Errorf("want endpoint create, got: %s", err)
|
||||
}
|
||||
|
||||
if err := e.Close(); err != nil {
|
||||
t.Errorf("want endpoint close, got: %s", iroh.As(err))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user