blob: 208e307924229ec7579bc3c6dbbb58b84eb21adb [file] [log] [blame]
// Copyright 2025 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package node
import (
"bytes"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestPortListener_Write(t *testing.T) {
tests := []struct {
name string
inputs [][]byte
expectedPort int
expectPort bool
expectedError string
expectedOut string
}{
{
name: "Port in single write",
inputs: [][]byte{
[]byte("Listening on [[12345]]"),
},
expectedPort: 12345,
expectPort: true,
expectedOut: "",
},
{
name: "Port split across writes",
inputs: [][]byte{
[]byte("Listening on [[12"),
[]byte("345]]"),
},
expectedPort: 12345,
expectPort: true,
expectedOut: "",
},
{
name: "Port with trailing data after port found",
inputs: [][]byte{
[]byte("Listening on [[12345]] Some other output"),
},
expectedPort: 12345,
expectPort: true,
expectedOut: " Some other output",
},
{
name: "No port in input",
inputs: [][]byte{
[]byte("Server starting..."),
[]byte("Still no port."),
},
expectPort: false,
expectedOut: "",
},
{
name: "Partial port start, no end",
inputs: [][]byte{
[]byte("Listening on [[12345"),
},
expectPort: false,
expectedOut: "",
},
{
name: "Invalid port number",
inputs: [][]byte{
[]byte("Listening on [[abc]]"),
},
expectPort: false,
expectedError: `strconv.Atoi: parsing "abc": invalid syntax`,
expectedOut: "",
},
{
name: "Empty port",
inputs: [][]byte{
[]byte("Listening on [[]]"),
},
expectPort: false,
expectedError: `strconv.Atoi: parsing "": invalid syntax`,
expectedOut: "",
},
{
name: "Port at beginning of input",
inputs: [][]byte{
[]byte("[[7777]] Server ready"),
},
expectedPort: 7777,
expectPort: true,
expectedOut: " Server ready",
},
{
name: "Writes after port found",
inputs: [][]byte{
[]byte("Listening on [[12345]]"),
[]byte("More data"),
[]byte(" and even more"),
},
expectedPort: 12345,
expectPort: true,
expectedOut: "More data and even more",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var outBuf bytes.Buffer
pl := newPortListener(&outBuf)
// Channel to receive the port or signal closure from the goroutine
portResultChan := make(chan struct {
port int
ok bool
}, 1) // Buffer 1 to allow goroutine to send and exit without blocking
// Start a goroutine to read from pl.port.
// This is necessary because pl.port is unbuffered, and pl.Write will block
// until the port is read.
go func() {
pVal, pOk := <-pl.port
portResultChan <- struct {
port int
ok bool
}{pVal, pOk}
}()
var writeErr error
for _, input := range tt.inputs {
// If pl.Write sends to pl.port, the goroutine above will receive it,
// unblocking pl.Write.
_, currentErr := pl.Write(input)
if currentErr != nil {
writeErr = currentErr // Capture the first error
break
}
}
if tt.expectedError != "" {
require.Error(t, writeErr, "Expected an error from Write")
require.Contains(t, writeErr.Error(), tt.expectedError, "Error message mismatch")
// If Write errored (e.g. strconv), pl.port might not be written to or closed by portListener.
// The reading goroutine would block. We check portResultChan with a timeout.
select {
case res := <-portResultChan:
t.Errorf("Unexpected port result (%+v) when Write error was expected", res)
case <-time.After(50 * time.Millisecond):
// Expected: goroutine is likely blocked as port was not sent/channel not closed due to error.
}
} else {
require.NoError(t, writeErr, "Expected no error from Write")
if tt.expectPort {
select {
case res := <-portResultChan:
require.True(t, res.ok, "Expected to read a port, but channel was closed prematurely by sender")
require.Equal(t, tt.expectedPort, res.port, "Port number mismatch")
case <-time.After(100 * time.Millisecond): // Timeout for expected port
t.Fatal("Timeout waiting for expected port")
}
} else { // Not expecting port
select {
case res := <-portResultChan:
if res.ok {
t.Errorf("Did not expect port, but got %d", res.port)
} else {
// This means pl.port was closed by sender. portListener.Write only closes *after* sending a port.
t.Error("Did not expect port, but port channel was closed by sender (implies port was sent and read)")
}
case <-time.After(50 * time.Millisecond):
// Expected: goroutine is likely blocked on <-pl.port as no port was sent and channel not closed.
}
}
}
require.Equal(t, tt.expectedOut, strings.TrimRight(outBuf.String(), " \n"))
if tt.expectPort && tt.expectedError == "" {
testStr := "subsequent write"
_, subsequentWriteErr := pl.Write([]byte(testStr))
require.NoError(t, subsequentWriteErr, "Error during subsequent write")
require.Contains(t, strings.TrimRight(outBuf.String(), " \n"), testStr)
}
})
}
}