282 lines
6.5 KiB
Go
282 lines
6.5 KiB
Go
package freeport
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
|
)
|
|
|
|
// reset will reverse the setup from initialize() and then redo it (for tests)
|
|
func reset() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
logf("INFO", "resetting the freeport package state")
|
|
|
|
effectiveMaxBlocks = 0
|
|
firstPort = 0
|
|
if lockLn != nil {
|
|
lockLn.Close()
|
|
lockLn = nil
|
|
}
|
|
|
|
once = sync.Once{}
|
|
|
|
freePorts = nil
|
|
pendingPorts = nil
|
|
total = 0
|
|
}
|
|
|
|
// peekFree returns the next port that will be returned by Take to aid in testing.
|
|
func peekFree() int {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return freePorts.Front().Value.(int)
|
|
}
|
|
|
|
// peekAllFree returns all free ports that could be returned by Take to aid in testing.
|
|
func peekAllFree() []int {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
var out []int
|
|
for elem := freePorts.Front(); elem != nil; elem = elem.Next() {
|
|
port := elem.Value.(int)
|
|
out = append(out, port)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// stats returns diagnostic data to aid in testing
|
|
func stats() (numTotal, numPending, numFree int) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return total, pendingPorts.Len(), freePorts.Len()
|
|
}
|
|
|
|
func TestTakeReturn(t *testing.T) {
|
|
// NOTE: for global var reasons this cannot execute in parallel
|
|
// t.Parallel()
|
|
|
|
// Since this test is destructive (i.e. it leaks all ports) it means that
|
|
// any other test cases in this package will not function after it runs. To
|
|
// help out we reset the global state after we run this test.
|
|
defer reset()
|
|
|
|
// OK: do a simple take/return cycle to trigger the package initialization
|
|
func() {
|
|
ports, err := Take(1)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
defer Return(ports)
|
|
|
|
if len(ports) != 1 {
|
|
t.Fatalf("expected %d but got %d ports", 1, len(ports))
|
|
}
|
|
}()
|
|
|
|
waitForStatsReset := func() (numTotal int) {
|
|
t.Helper()
|
|
numTotal, numPending, numFree := stats()
|
|
if numTotal != numFree+numPending {
|
|
t.Fatalf("expected total (%d) and free+pending (%d) ports to match", numTotal, numFree+numPending)
|
|
}
|
|
retry.Run(t, func(r *retry.R) {
|
|
numTotal, numPending, numFree = stats()
|
|
if numPending != 0 {
|
|
r.Fatalf("pending is still non zero: %d", numPending)
|
|
}
|
|
if numTotal != numFree {
|
|
r.Fatalf("total (%d) does not equal free (%d)", numTotal, numFree)
|
|
}
|
|
})
|
|
return numTotal
|
|
}
|
|
|
|
// Reset
|
|
numTotal := waitForStatsReset()
|
|
|
|
// --------------------
|
|
// OK: take the max
|
|
func() {
|
|
ports, err := Take(numTotal)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
defer Return(ports)
|
|
|
|
if len(ports) != numTotal {
|
|
t.Fatalf("expected %d but got %d ports", numTotal, len(ports))
|
|
}
|
|
}()
|
|
|
|
// Reset
|
|
numTotal = waitForStatsReset()
|
|
|
|
expectError := func(expected string, got error) {
|
|
t.Helper()
|
|
if got == nil {
|
|
t.Fatalf("expected error but was nil")
|
|
}
|
|
if got.Error() != expected {
|
|
t.Fatalf("expected error %q but got %q", expected, got.Error())
|
|
}
|
|
}
|
|
|
|
// --------------------
|
|
// ERROR: take too many ports
|
|
func() {
|
|
ports, err := Take(numTotal + 1)
|
|
defer Return(ports)
|
|
expectError("freeport: block size too small", err)
|
|
}()
|
|
|
|
// --------------------
|
|
// ERROR: invalid ports request (negative)
|
|
func() {
|
|
_, err := Take(-1)
|
|
expectError("freeport: cannot take -1 ports", err)
|
|
}()
|
|
|
|
// --------------------
|
|
// ERROR: invalid ports request (zero)
|
|
func() {
|
|
_, err := Take(0)
|
|
expectError("freeport: cannot take 0 ports", err)
|
|
}()
|
|
|
|
// --------------------
|
|
// OK: Steal a port under the covers and let freeport detect the theft and compensate
|
|
leakedPort := peekFree()
|
|
func() {
|
|
leakyListener, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", leakedPort))
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
defer leakyListener.Close()
|
|
|
|
func() {
|
|
ports, err := Take(3)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
defer Return(ports)
|
|
|
|
if len(ports) != 3 {
|
|
t.Fatalf("expected %d but got %d ports", 3, len(ports))
|
|
}
|
|
|
|
for _, port := range ports {
|
|
if port == leakedPort {
|
|
t.Fatalf("did not expect for Take to return the leaked port")
|
|
}
|
|
}
|
|
}()
|
|
|
|
newNumTotal := waitForStatsReset()
|
|
if newNumTotal != numTotal-1 {
|
|
t.Fatalf("expected total to drop to %d but got %d", numTotal-1, newNumTotal)
|
|
}
|
|
numTotal = newNumTotal // update outer variable for later tests
|
|
}()
|
|
|
|
// --------------------
|
|
// OK: sequence it so that one Take must wait on another Take to Return.
|
|
func() {
|
|
mostPorts, err := Take(numTotal - 5)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
type reply struct {
|
|
ports []int
|
|
err error
|
|
}
|
|
ch := make(chan reply, 1)
|
|
go func() {
|
|
ports, err := Take(10)
|
|
ch <- reply{ports: ports, err: err}
|
|
}()
|
|
|
|
Return(mostPorts)
|
|
|
|
r := <-ch
|
|
if r.err != nil {
|
|
t.Fatalf("err: %v", r.err)
|
|
}
|
|
defer Return(r.ports)
|
|
|
|
if len(r.ports) != 10 {
|
|
t.Fatalf("expected %d ports but got %d", 10, len(r.ports))
|
|
}
|
|
}()
|
|
|
|
// Reset
|
|
numTotal = waitForStatsReset()
|
|
|
|
// --------------------
|
|
// ERROR: Now we end on the crazy "Ocean's 11" level port theft where we
|
|
// orchestrate a situation where all ports are stolen and we don't find out
|
|
// until Take.
|
|
func() {
|
|
// 1. Grab all of the ports.
|
|
allPorts := peekAllFree()
|
|
|
|
// 2. Leak all of the ports
|
|
leaked := make([]io.Closer, 0, len(allPorts))
|
|
defer func() {
|
|
for _, c := range leaked {
|
|
c.Close()
|
|
}
|
|
}()
|
|
for _, port := range allPorts {
|
|
ln, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", port))
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
leaked = append(leaked, ln)
|
|
}
|
|
|
|
// 3. Request 1 port which will detect the leaked ports and fail.
|
|
_, err := Take(1)
|
|
expectError("freeport: impossible to satisfy request; there are no actual free ports in the block anymore", err)
|
|
|
|
// 4. Wait for the block to zero out.
|
|
newNumTotal := waitForStatsReset()
|
|
if newNumTotal != 0 {
|
|
t.Fatalf("expected total to drop to %d but got %d", 0, newNumTotal)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func TestIntervalOverlap(t *testing.T) {
|
|
cases := []struct {
|
|
min1, max1, min2, max2 int
|
|
overlap bool
|
|
}{
|
|
{0, 0, 0, 0, true},
|
|
{1, 1, 1, 1, true},
|
|
{1, 3, 1, 3, true}, // same
|
|
{1, 3, 4, 6, false}, // serial
|
|
{1, 4, 3, 6, true}, // inner overlap
|
|
{1, 6, 3, 4, true}, // nest
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(fmt.Sprintf("%d:%d vs %d:%d", tc.min1, tc.max1, tc.min2, tc.max2), func(t *testing.T) {
|
|
if tc.overlap != intervalOverlap(tc.min1, tc.max1, tc.min2, tc.max2) { // 1 vs 2
|
|
t.Fatalf("expected %v but got %v", tc.overlap, !tc.overlap)
|
|
}
|
|
if tc.overlap != intervalOverlap(tc.min2, tc.max2, tc.min1, tc.max1) { // 2 vs 1
|
|
t.Fatalf("expected %v but got %v", tc.overlap, !tc.overlap)
|
|
}
|
|
})
|
|
}
|
|
}
|