| // 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) | |
| } | |
| }) | |
| } | |
| } |