New Upstream Release - golang-k8s-utils

Ready changes

Summary

Merged new upstream version: 0.0~git20230505.9f67429 (was: 0.0~git20221128.99ec85e).

Diff

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3e3a48f..e2042f4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,7 +4,7 @@ jobs:
   build:
     strategy:
       matrix:
-        go-version: [1.18.x, 1.19.x]
+        go-version: [1.19.x, 1.20.x]
         platform: [windows-latest]
     runs-on: ${{ matrix.platform }}
     steps:
@@ -24,7 +24,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [1.18.x, 1.19.x]
+        go-version: [1.19.x, 1.20.x]
         platform: [ubuntu-latest, macos-latest]
     runs-on: ${{ matrix.platform }}
     steps:
@@ -47,7 +47,7 @@ jobs:
       - name: Lint
         run: |
           docker run --rm -v `pwd`:/go/src/k8s.io/klog -w /go/src/k8s.io/klog \
-            golangci/golangci-lint:v1.46.2 golangci-lint run --disable-all -v \
+            golangci/golangci-lint:v1.51.2 golangci-lint run --disable-all -v \
             -E govet -E misspell -E gofmt -E ineffassign -E golint
   apidiff:
     runs-on: ubuntu-latest
@@ -56,7 +56,7 @@ jobs:
       - name: Install Go
         uses: actions/setup-go@v3
         with:
-          go-version: 1.19.x
+          go-version: 1.20.x
       - name: Add GOBIN to PATH
         run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
       - name: Install dependencies
diff --git a/cpuset/OWNERS b/cpuset/OWNERS
new file mode 100644
index 0000000..0ec2b08
--- /dev/null
+++ b/cpuset/OWNERS
@@ -0,0 +1,8 @@
+# See the OWNERS docs at https://go.k8s.io/owners
+
+approvers:
+  - dchen1107
+  - derekwaynecarr
+  - ffromani
+  - klueska
+  - SergeyKanzhelev
diff --git a/cpuset/cpuset.go b/cpuset/cpuset.go
new file mode 100644
index 0000000..52912d9
--- /dev/null
+++ b/cpuset/cpuset.go
@@ -0,0 +1,256 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package cpuset represents a collection of CPUs in a 'set' data structure.
+//
+// It can be used to represent core IDs, hyper thread siblings, CPU nodes, or processor IDs.
+//
+// The only special thing about this package is that
+// methods are provided to convert back and forth from Linux 'list' syntax.
+// See http://man7.org/linux/man-pages/man7/cpuset.7.html#FORMATS for details.
+//
+// Future work can migrate this to use a 'set' library, and relax the dubious 'immutable' property.
+//
+// This package was originally developed in the 'kubernetes' repository.
+package cpuset
+
+import (
+	"bytes"
+	"fmt"
+	"reflect"
+	"sort"
+	"strconv"
+	"strings"
+)
+
+// CPUSet is a thread-safe, immutable set-like data structure for CPU IDs.
+type CPUSet struct {
+	elems map[int]struct{}
+}
+
+// New returns a new CPUSet containing the supplied elements.
+func New(cpus ...int) CPUSet {
+	s := CPUSet{
+		elems: map[int]struct{}{},
+	}
+	for _, c := range cpus {
+		s.add(c)
+	}
+	return s
+}
+
+// add adds the supplied elements to the CPUSet.
+// It is intended for internal use only, since it mutates the CPUSet.
+func (s CPUSet) add(elems ...int) {
+	for _, elem := range elems {
+		s.elems[elem] = struct{}{}
+	}
+}
+
+// Size returns the number of elements in this set.
+func (s CPUSet) Size() int {
+	return len(s.elems)
+}
+
+// IsEmpty returns true if there are zero elements in this set.
+func (s CPUSet) IsEmpty() bool {
+	return s.Size() == 0
+}
+
+// Contains returns true if the supplied element is present in this set.
+func (s CPUSet) Contains(cpu int) bool {
+	_, found := s.elems[cpu]
+	return found
+}
+
+// Equals returns true if the supplied set contains exactly the same elements
+// as this set (s IsSubsetOf s2 and s2 IsSubsetOf s).
+func (s CPUSet) Equals(s2 CPUSet) bool {
+	return reflect.DeepEqual(s.elems, s2.elems)
+}
+
+// filter returns a new CPU set that contains all of the elements from this
+// set that match the supplied predicate, without mutating the source set.
+func (s CPUSet) filter(predicate func(int) bool) CPUSet {
+	r := New()
+	for cpu := range s.elems {
+		if predicate(cpu) {
+			r.add(cpu)
+		}
+	}
+	return r
+}
+
+// IsSubsetOf returns true if the supplied set contains all the elements
+func (s CPUSet) IsSubsetOf(s2 CPUSet) bool {
+	result := true
+	for cpu := range s.elems {
+		if !s2.Contains(cpu) {
+			result = false
+			break
+		}
+	}
+	return result
+}
+
+// Union returns a new CPU set that contains all of the elements from this
+// set and all of the elements from the supplied sets, without mutating
+// either source set.
+func (s CPUSet) Union(s2 ...CPUSet) CPUSet {
+	r := New()
+	for cpu := range s.elems {
+		r.add(cpu)
+	}
+	for _, cs := range s2 {
+		for cpu := range cs.elems {
+			r.add(cpu)
+		}
+	}
+	return r
+}
+
+// Intersection returns a new CPU set that contains all of the elements
+// that are present in both this set and the supplied set, without mutating
+// either source set.
+func (s CPUSet) Intersection(s2 CPUSet) CPUSet {
+	return s.filter(func(cpu int) bool { return s2.Contains(cpu) })
+}
+
+// Difference returns a new CPU set that contains all of the elements that
+// are present in this set and not the supplied set, without mutating either
+// source set.
+func (s CPUSet) Difference(s2 CPUSet) CPUSet {
+	return s.filter(func(cpu int) bool { return !s2.Contains(cpu) })
+}
+
+// List returns a slice of integers that contains all elements from
+// this set. The list is sorted.
+func (s CPUSet) List() []int {
+	result := s.UnsortedList()
+	sort.Ints(result)
+	return result
+}
+
+// UnsortedList returns a slice of integers that contains all elements from
+// this set.
+func (s CPUSet) UnsortedList() []int {
+	result := make([]int, 0, len(s.elems))
+	for cpu := range s.elems {
+		result = append(result, cpu)
+	}
+	return result
+}
+
+// String returns a new string representation of the elements in this CPU set
+// in canonical linux CPU list format.
+//
+// See: http://man7.org/linux/man-pages/man7/cpuset.7.html#FORMATS
+func (s CPUSet) String() string {
+	if s.IsEmpty() {
+		return ""
+	}
+
+	elems := s.List()
+
+	type rng struct {
+		start int
+		end   int
+	}
+
+	ranges := []rng{{elems[0], elems[0]}}
+
+	for i := 1; i < len(elems); i++ {
+		lastRange := &ranges[len(ranges)-1]
+		// if this element is adjacent to the high end of the last range
+		if elems[i] == lastRange.end+1 {
+			// then extend the last range to include this element
+			lastRange.end = elems[i]
+			continue
+		}
+		// otherwise, start a new range beginning with this element
+		ranges = append(ranges, rng{elems[i], elems[i]})
+	}
+
+	// construct string from ranges
+	var result bytes.Buffer
+	for _, r := range ranges {
+		if r.start == r.end {
+			result.WriteString(strconv.Itoa(r.start))
+		} else {
+			result.WriteString(fmt.Sprintf("%d-%d", r.start, r.end))
+		}
+		result.WriteString(",")
+	}
+	return strings.TrimRight(result.String(), ",")
+}
+
+// Parse CPUSet constructs a new CPU set from a Linux CPU list formatted string.
+//
+// See: http://man7.org/linux/man-pages/man7/cpuset.7.html#FORMATS
+func Parse(s string) (CPUSet, error) {
+	// Handle empty string.
+	if s == "" {
+		return New(), nil
+	}
+
+	result := New()
+
+	// Split CPU list string:
+	// "0-5,34,46-48" => ["0-5", "34", "46-48"]
+	ranges := strings.Split(s, ",")
+
+	for _, r := range ranges {
+		boundaries := strings.SplitN(r, "-", 2)
+		if len(boundaries) == 1 {
+			// Handle ranges that consist of only one element like "34".
+			elem, err := strconv.Atoi(boundaries[0])
+			if err != nil {
+				return New(), err
+			}
+			result.add(elem)
+		} else if len(boundaries) == 2 {
+			// Handle multi-element ranges like "0-5".
+			start, err := strconv.Atoi(boundaries[0])
+			if err != nil {
+				return New(), err
+			}
+			end, err := strconv.Atoi(boundaries[1])
+			if err != nil {
+				return New(), err
+			}
+			if start > end {
+				return New(), fmt.Errorf("invalid range %q (%d > %d)", r, start, end)
+			}
+			// start == end is acceptable (1-1 -> 1)
+
+			// Add all elements to the result.
+			// e.g. "0-5", "46-48" => [0, 1, 2, 3, 4, 5, 46, 47, 48].
+			for e := start; e <= end; e++ {
+				result.add(e)
+			}
+		}
+	}
+	return result, nil
+}
+
+// Clone returns a copy of this CPU set.
+func (s CPUSet) Clone() CPUSet {
+	r := New()
+	for elem := range s.elems {
+		r.add(elem)
+	}
+	return r
+}
diff --git a/cpuset/cpuset_test.go b/cpuset/cpuset_test.go
new file mode 100644
index 0000000..275cc9e
--- /dev/null
+++ b/cpuset/cpuset_test.go
@@ -0,0 +1,358 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cpuset
+
+import (
+	"reflect"
+	"sort"
+	"testing"
+)
+
+func TestCPUSetSize(t *testing.T) {
+	testCases := []struct {
+		cpuset   CPUSet
+		expected int
+	}{
+		{New(), 0},
+		{New(5), 1},
+		{New(1, 2, 3, 4, 5), 5},
+	}
+
+	for _, c := range testCases {
+		actual := c.cpuset.Size()
+		if actual != c.expected {
+			t.Errorf("expected: %d, actual: %d, cpuset: [%v]", c.expected, actual, c.cpuset)
+		}
+	}
+}
+
+func TestCPUSetIsEmpty(t *testing.T) {
+	testCases := []struct {
+		cpuset   CPUSet
+		expected bool
+	}{
+		{New(), true},
+		{New(5), false},
+		{New(1, 2, 3, 4, 5), false},
+	}
+
+	for _, c := range testCases {
+		actual := c.cpuset.IsEmpty()
+		if actual != c.expected {
+			t.Errorf("expected: %t, IsEmpty() returned: %t, cpuset: [%v]", c.expected, actual, c.cpuset)
+		}
+	}
+}
+
+func TestCPUSetContains(t *testing.T) {
+	testCases := []struct {
+		cpuset         CPUSet
+		mustContain    []int
+		mustNotContain []int
+	}{
+		{New(), []int{}, []int{1, 2, 3, 4, 5}},
+		{New(5), []int{5}, []int{1, 2, 3, 4}},
+		{New(1, 2, 4, 5), []int{1, 2, 4, 5}, []int{0, 3, 6}},
+	}
+
+	for _, c := range testCases {
+		for _, elem := range c.mustContain {
+			if !c.cpuset.Contains(elem) {
+				t.Errorf("expected cpuset to contain element %d: [%v]", elem, c.cpuset)
+			}
+		}
+		for _, elem := range c.mustNotContain {
+			if c.cpuset.Contains(elem) {
+				t.Errorf("expected cpuset not to contain element %d: [%v]", elem, c.cpuset)
+			}
+		}
+	}
+}
+
+func TestCPUSetEqual(t *testing.T) {
+	shouldEqual := []struct {
+		s1 CPUSet
+		s2 CPUSet
+	}{
+		{New(), New()},
+		{New(5), New(5)},
+		{New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5)},
+		{New(5, 4, 3, 2, 1), New(1, 2, 3, 4, 5)},
+	}
+
+	shouldNotEqual := []struct {
+		s1 CPUSet
+		s2 CPUSet
+	}{
+		{New(), New(5)},
+		{New(5), New()},
+		{New(), New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3, 4, 5), New()},
+		{New(5), New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3, 4, 5), New(5)},
+	}
+
+	for _, c := range shouldEqual {
+		if !c.s1.Equals(c.s2) {
+			t.Errorf("expected cpusets to be equal: s1: [%v], s2: [%v]", c.s1, c.s2)
+		}
+	}
+	for _, c := range shouldNotEqual {
+		if c.s1.Equals(c.s2) {
+			t.Errorf("expected cpusets to not be equal: s1: [%v], s2: [%v]", c.s1, c.s2)
+		}
+	}
+}
+
+func TestCPUSetIsSubsetOf(t *testing.T) {
+	shouldBeSubset := []struct {
+		s1 CPUSet
+		s2 CPUSet
+	}{
+		// A set is a subset of itself
+		{New(), New()},
+		{New(5), New(5)},
+		{New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5)},
+
+		// Empty set is a subset of every set
+		{New(), New(5)},
+		{New(), New(1, 2, 3, 4, 5)},
+
+		{New(5), New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3), New(1, 2, 3, 4, 5)},
+		{New(4, 5), New(1, 2, 3, 4, 5)},
+		{New(2, 3), New(1, 2, 3, 4, 5)},
+	}
+
+	shouldNotBeSubset := []struct {
+		s1 CPUSet
+		s2 CPUSet
+	}{
+		// A set with more elements is not a subset.
+		{New(5), New()},
+
+		// Disjoint set is not a subset.
+		{New(6), New(5)},
+	}
+
+	for _, c := range shouldBeSubset {
+		if !c.s1.IsSubsetOf(c.s2) {
+			t.Errorf("expected s1 to be a subset of s2: s1: [%v], s2: [%v]", c.s1, c.s2)
+		}
+	}
+	for _, c := range shouldNotBeSubset {
+		if c.s1.IsSubsetOf(c.s2) {
+			t.Errorf("expected s1 to not be a subset of s2: s1: [%v], s2: [%v]", c.s1, c.s2)
+		}
+	}
+}
+
+func TestCPUSetUnion(t *testing.T) {
+	testCases := []struct {
+		s1       CPUSet
+		others   []CPUSet
+		expected CPUSet
+	}{
+		{New(5), []CPUSet{}, New(5)},
+
+		{New(), []CPUSet{New()}, New()},
+
+		{New(), []CPUSet{New(5)}, New(5)},
+		{New(5), []CPUSet{New()}, New(5)},
+		{New(5), []CPUSet{New(5)}, New(5)},
+
+		{New(), []CPUSet{New(1, 2, 3, 4, 5)}, New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3, 4, 5), []CPUSet{New()}, New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3, 4, 5), []CPUSet{New(1, 2, 3, 4, 5)}, New(1, 2, 3, 4, 5)},
+
+		{New(5), []CPUSet{New(1, 2, 3, 4, 5)}, New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3, 4, 5), []CPUSet{New(5)}, New(1, 2, 3, 4, 5)},
+
+		{New(1, 2), []CPUSet{New(3, 4, 5)}, New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3), []CPUSet{New(3, 4, 5)}, New(1, 2, 3, 4, 5)},
+
+		{New(), []CPUSet{New(1, 2, 3, 4, 5), New(4, 5)}, New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3, 4, 5), []CPUSet{New(), New(4)}, New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3, 4, 5), []CPUSet{New(1, 2, 3, 4, 5), New(1, 5)}, New(1, 2, 3, 4, 5)},
+	}
+
+	for _, c := range testCases {
+		result := c.s1.Union(c.others...)
+		if !result.Equals(c.expected) {
+			t.Errorf("expected the union of s1 and s2 to be [%v] (got [%v]), others: [%v]", c.expected, result, c.others)
+		}
+	}
+}
+
+func TestCPUSetIntersection(t *testing.T) {
+	testCases := []struct {
+		s1       CPUSet
+		s2       CPUSet
+		expected CPUSet
+	}{
+		{New(), New(), New()},
+
+		{New(), New(5), New()},
+		{New(5), New(), New()},
+		{New(5), New(5), New(5)},
+
+		{New(), New(1, 2, 3, 4, 5), New()},
+		{New(1, 2, 3, 4, 5), New(), New()},
+		{New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5)},
+
+		{New(5), New(1, 2, 3, 4, 5), New(5)},
+		{New(1, 2, 3, 4, 5), New(5), New(5)},
+
+		{New(1, 2), New(3, 4, 5), New()},
+		{New(1, 2, 3), New(3, 4, 5), New(3)},
+	}
+
+	for _, c := range testCases {
+		result := c.s1.Intersection(c.s2)
+		if !result.Equals(c.expected) {
+			t.Errorf("expected the intersection of s1 and s2 to be [%v] (got [%v]), s1: [%v], s2: [%v]", c.expected, result, c.s1, c.s2)
+		}
+	}
+}
+
+func TestCPUSetDifference(t *testing.T) {
+	testCases := []struct {
+		s1       CPUSet
+		s2       CPUSet
+		expected CPUSet
+	}{
+		{New(), New(), New()},
+
+		{New(), New(5), New()},
+		{New(5), New(), New(5)},
+		{New(5), New(5), New()},
+
+		{New(), New(1, 2, 3, 4, 5), New()},
+		{New(1, 2, 3, 4, 5), New(), New(1, 2, 3, 4, 5)},
+		{New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5), New()},
+
+		{New(5), New(1, 2, 3, 4, 5), New()},
+		{New(1, 2, 3, 4, 5), New(5), New(1, 2, 3, 4)},
+
+		{New(1, 2), New(3, 4, 5), New(1, 2)},
+		{New(1, 2, 3), New(3, 4, 5), New(1, 2)},
+	}
+
+	for _, c := range testCases {
+		result := c.s1.Difference(c.s2)
+		if !result.Equals(c.expected) {
+			t.Errorf("expected the difference of s1 and s2 to be [%v] (got [%v]), s1: [%v], s2: [%v]", c.expected, result, c.s1, c.s2)
+		}
+	}
+}
+
+func TestCPUSetList(t *testing.T) {
+	testCases := []struct {
+		set      CPUSet
+		expected []int // must be sorted
+	}{
+		{New(), []int{}},
+		{New(5), []int{5}},
+		{New(1, 2, 3, 4, 5), []int{1, 2, 3, 4, 5}},
+		{New(5, 4, 3, 2, 1), []int{1, 2, 3, 4, 5}},
+	}
+
+	for _, c := range testCases {
+		result := c.set.List()
+		if !reflect.DeepEqual(result, c.expected) {
+			t.Errorf("unexpected List() contents. got [%v] want [%v] (set: [%v])", result, c.expected, c.set)
+		}
+
+		// We cannot rely on internal storage order details for a unit test.
+		// The best we can do is to sort the output of 'UnsortedList'.
+		result = c.set.UnsortedList()
+		sort.Ints(result)
+		if !reflect.DeepEqual(result, c.expected) {
+			t.Errorf("unexpected UnsortedList() contents. got [%v] want [%v] (set: [%v])", result, c.expected, c.set)
+		}
+	}
+}
+
+func TestCPUSetString(t *testing.T) {
+	testCases := []struct {
+		set      CPUSet
+		expected string
+	}{
+		{New(), ""},
+		{New(5), "5"},
+		{New(1, 2, 3, 4, 5), "1-5"},
+		{New(1, 2, 3, 5, 6, 8), "1-3,5-6,8"},
+	}
+
+	for _, c := range testCases {
+		result := c.set.String()
+		if result != c.expected {
+			t.Errorf("expected set as string to be %s (got \"%s\"), s: [%v]", c.expected, result, c.set)
+		}
+	}
+}
+
+func TestParse(t *testing.T) {
+	positiveTestCases := []struct {
+		cpusetString string
+		expected     CPUSet
+	}{
+		{"", New()},
+		{"5", New(5)},
+		{"1,2,3,4,5", New(1, 2, 3, 4, 5)},
+		{"1-5", New(1, 2, 3, 4, 5)},
+		{"1-2,3-5", New(1, 2, 3, 4, 5)},
+		{"5,4,3,2,1", New(1, 2, 3, 4, 5)},  // Range ordering
+		{"3-6,1-5", New(1, 2, 3, 4, 5, 6)}, // Overlapping ranges
+		{"3-3,5-5", New(3, 5)},             // Very short ranges
+	}
+
+	for _, c := range positiveTestCases {
+		result, err := Parse(c.cpusetString)
+		if err != nil {
+			t.Errorf("expected error not to have occurred: %v", err)
+		}
+		if !result.Equals(c.expected) {
+			t.Errorf("expected string \"%s\" to parse as [%v] (got [%v])", c.cpusetString, c.expected, result)
+		}
+	}
+
+	negativeTestCases := []string{
+		// Non-numeric entries
+		"nonnumeric", "non-numeric", "no,numbers", "0-a", "a-0", "0,a", "a,0", "1-2,a,3-5",
+		// Incomplete sequences
+		"0,", "0,,", ",3", ",,3", "0,,3",
+		// Incomplete ranges and/or negative numbers
+		"-1", "1-", "1,2-,3", "1,-2,3", "-1--2", "--1", "1--",
+		// Reversed ranges
+		"3-0", "0--3"}
+	for _, c := range negativeTestCases {
+		result, err := Parse(c)
+		if err == nil {
+			t.Errorf("expected parse failure of \"%s\", but it succeeded as \"%s\"", c, result.String())
+		}
+	}
+}
+
+func TestClone(t *testing.T) {
+	original := New(1, 2, 3, 4, 5)
+	clone := original.Clone()
+
+	if !original.Equals(clone) {
+		t.Errorf("expected clone [%v] to equal original [%v]", clone, original)
+	}
+}
diff --git a/debian/changelog b/debian/changelog
index f6d784b..859b1d7 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-k8s-utils (0.0~git20230505.9f67429-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 22 May 2023 22:35:43 -0000
+
 golang-k8s-utils (0.0~git20221128.99ec85e-1) unstable; urgency=medium
 
   * New upstream snapshot
diff --git a/exec/testing/fake_exec.go b/exec/testing/fake_exec.go
index 7380689..7c125a6 100644
--- a/exec/testing/fake_exec.go
+++ b/exec/testing/fake_exec.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"sync"
 
 	"k8s.io/utils/exec"
 )
@@ -32,11 +33,12 @@ type FakeExec struct {
 	// ExactOrder enforces that commands are called in the order they are scripted,
 	// and with the exact same arguments
 	ExactOrder bool
-	// DisableScripts removes the requirement that a slice of FakeCommandAction be
-	// populated before calling Command(). This makes the fakeexec (and subsequent
-	// calls to Run() or CombinedOutput() always return success and there is no
-	// ability to set their output.
+	// DisableScripts removes the requirement that CommandScripts be populated
+	// before calling Command(). This makes Command() and subsequent calls to
+	// Run() or CombinedOutput() always return success and empty output.
 	DisableScripts bool
+
+	mu sync.Mutex
 }
 
 var _ exec.Interface = &FakeExec{}
@@ -44,18 +46,15 @@ var _ exec.Interface = &FakeExec{}
 // FakeCommandAction is the function to be executed
 type FakeCommandAction func(cmd string, args ...string) exec.Cmd
 
-// Command is to track the commands that are executed
+// Command returns the next unexecuted command in CommandScripts.
+// This function is safe for concurrent access as long as the underlying
+// FakeExec struct is not modified during execution.
 func (fake *FakeExec) Command(cmd string, args ...string) exec.Cmd {
 	if fake.DisableScripts {
 		fakeCmd := &FakeCmd{DisableScripts: true}
 		return InitFakeCmd(fakeCmd, cmd, args...)
 	}
-	if fake.CommandCalls > len(fake.CommandScript)-1 {
-		panic(fmt.Sprintf("ran out of Command() actions. Could not handle command [%d]: %s args: %v", fake.CommandCalls, cmd, args))
-	}
-	i := fake.CommandCalls
-	fake.CommandCalls++
-	fakeCmd := fake.CommandScript[i](cmd, args...)
+	fakeCmd := fake.nextCommand(cmd, args)
 	if fake.ExactOrder {
 		argv := append([]string{cmd}, args...)
 		fc := fakeCmd.(*FakeCmd)
@@ -74,6 +73,18 @@ func (fake *FakeExec) Command(cmd string, args ...string) exec.Cmd {
 	return fakeCmd
 }
 
+func (fake *FakeExec) nextCommand(cmd string, args []string) exec.Cmd {
+	fake.mu.Lock()
+	defer fake.mu.Unlock()
+
+	if fake.CommandCalls > len(fake.CommandScript)-1 {
+		panic(fmt.Sprintf("ran out of Command() actions. Could not handle command [%d]: %s args: %v", fake.CommandCalls, cmd, args))
+	}
+	i := fake.CommandCalls
+	fake.CommandCalls++
+	return fake.CommandScript[i](cmd, args...)
+}
+
 // CommandContext wraps arguments into exec.Cmd
 func (fake *FakeExec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
 	return fake.Command(cmd, args...)
diff --git a/inotify/README.md b/inotify/README.md
index 0c723a8..fbaa166 100644
--- a/inotify/README.md
+++ b/inotify/README.md
@@ -1,5 +1,5 @@
 This is a fork of golang.org/x/exp/inotify before it was deleted.
 
-Please use gopkg.in/fsnotify.v0 instead.
+Please use gopkg.in/fsnotify.v1 instead.
 
-For updates, see: https://fsnotify.org/
+For updates, see: https://github.com/fsnotify/fsnotify
diff --git a/inotify/inotify_linux.go b/inotify/inotify_linux.go
index fbb50c2..2b9eabb 100644
--- a/inotify/inotify_linux.go
+++ b/inotify/inotify_linux.go
@@ -9,23 +9,23 @@
 Package inotify implements a wrapper for the Linux inotify system.
 
 Example:
-    watcher, err := inotify.NewWatcher()
-    if err != nil {
-        log.Fatal(err)
-    }
-    err = watcher.Watch("/tmp")
-    if err != nil {
-        log.Fatal(err)
-    }
-    for {
-        select {
-        case ev := <-watcher.Event:
-            log.Println("event:", ev)
-        case err := <-watcher.Error:
-            log.Println("error:", err)
-        }
-    }
 
+	watcher, err := inotify.NewWatcher()
+	if err != nil {
+	    log.Fatal(err)
+	}
+	err = watcher.Watch("/tmp")
+	if err != nil {
+	    log.Fatal(err)
+	}
+	for {
+	    select {
+	    case ev := <-watcher.Event:
+	        log.Println("event:", ev)
+	    case err := <-watcher.Error:
+	        log.Println("error:", err)
+	    }
+	}
 */
 package inotify // import "k8s.io/utils/inotify"
 
diff --git a/lru/lru.go b/lru/lru.go
index 5d0077a..47f1352 100644
--- a/lru/lru.go
+++ b/lru/lru.go
@@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-    http://www.apache.org/licenses/LICENSE-2.0
+	http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -22,6 +22,7 @@ import (
 )
 
 type Key = groupcache.Key
+type EvictionFunc = func(key Key, value interface{})
 
 // Cache is a thread-safe fixed size LRU cache.
 type Cache struct {
@@ -36,6 +37,13 @@ func New(size int) *Cache {
 	}
 }
 
+// NewWithEvictionFunc creates an LRU of the given size with the given eviction func.
+func NewWithEvictionFunc(size int, f EvictionFunc) *Cache {
+	c := New(size)
+	c.cache.OnEvicted = f
+	return c
+}
+
 // Add adds a value to the cache.
 func (c *Cache) Add(key Key, value interface{}) {
 	c.lock.Lock()
diff --git a/lru/lru_test.go b/lru/lru_test.go
index 25b824b..2fcaf79 100644
--- a/lru/lru_test.go
+++ b/lru/lru_test.go
@@ -113,3 +113,20 @@ func TestGetRace(t *testing.T) {
 	// let them run
 	time.Sleep(5 * time.Second)
 }
+
+func TestEviction(t *testing.T) {
+	var seenKey Key
+	var seenVal interface{}
+
+	lru := NewWithEvictionFunc(1, func(key Key, value interface{}) {
+		seenKey = key
+		seenVal = value
+	})
+
+	lru.Add(1, 2)
+	lru.Add(3, 4)
+
+	if seenKey != 1 || seenVal != 2 {
+		t.Errorf("unexpected eviction data: key=%v val=%v", seenKey, seenVal)
+	}
+}
diff --git a/net/ebtables/ebtables.go b/net/ebtables/ebtables.go
index 3e984a2..88b6e3b 100644
--- a/net/ebtables/ebtables.go
+++ b/net/ebtables/ebtables.go
@@ -121,10 +121,17 @@ func getEbtablesVersionString(exec utilexec.Interface) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	versionMatcher := regexp.MustCompile(`v([0-9]+\.[0-9]+\.[0-9]+)`)
-	match := versionMatcher.FindStringSubmatch(string(bytes))
+	return parseVersion(string(bytes))
+}
+
+func parseVersion(version string) (string, error) {
+	// the regular expression contains `v?` at the beginning because
+	// different OS distros have different version format output i.e
+	// either starts with `v` or it doesn't
+	versionMatcher := regexp.MustCompile(`v?([0-9]+\.[0-9]+\.[0-9]+)`)
+	match := versionMatcher.FindStringSubmatch(version)
 	if match == nil {
-		return "", fmt.Errorf("no ebtables version found in string: %s", bytes)
+		return "", fmt.Errorf("no ebtables version found in string: %s", version)
 	}
 	return match[1], nil
 }
diff --git a/net/ebtables/ebtables_test.go b/net/ebtables/ebtables_test.go
index 9933493..1920ac9 100644
--- a/net/ebtables/ebtables_test.go
+++ b/net/ebtables/ebtables_test.go
@@ -167,3 +167,61 @@ Bridge chain: TEST, entries: 0, policy: ACCEPT`), nil, nil
 		t.Errorf("expected err = nil")
 	}
 }
+
+func Test_parseVersion(t *testing.T) {
+	tests := []struct {
+		name    string
+		version string
+		want    string
+		wantErr bool
+	}{
+		{
+			name:    "version starting with `v`",
+			version: "v2.0.10",
+			want:    "2.0.10",
+			wantErr: false,
+		},
+		{
+			name:    "version without containing `v`",
+			version: "2.0.10",
+			want:    "2.0.10",
+			wantErr: false,
+		},
+		{
+			name:    "version containing `v` in between the regex expression match",
+			version: "2.0v.10",
+			want:    "",
+			wantErr: true,
+		},
+		{
+			name:    "version containing `v` after the regex expression match",
+			version: "2.0.10v",
+			want:    "2.0.10",
+			wantErr: false,
+		},
+		{
+			name:    "version starting with `v` and containing a symbol in between",
+			version: "v2.0.10-4",
+			want:    "2.0.10",
+			wantErr: false,
+		},
+		{
+			name:    "version starting with `v` and containing a symbol/alphabets in between",
+			version: "v2.0a.10-4",
+			want:    "",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseVersion(tt.version)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseVersion() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("parseVersion() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/nsenter/nsenter.go b/nsenter/nsenter.go
index 237b636..6f847db 100644
--- a/nsenter/nsenter.go
+++ b/nsenter/nsenter.go
@@ -49,26 +49,28 @@ type Nsenter = NSEnter
 //
 // NSEnter requires:
 //
-// 1.  Docker >= 1.6 due to the dependency on the slave propagation mode
+//  1. Docker >= 1.6 due to the dependency on the slave propagation mode
 //     of the bind-mount of the kubelet root directory in the container.
 //     Docker 1.5 used a private propagation mode for bind-mounts, so mounts
 //     performed in the host's mount namespace do not propagate out to the
 //     bind-mount in this docker version.
-// 2.  The host's root filesystem must be available at /rootfs
-// 3.  The nsenter binary must be on the Kubelet process' PATH in the container's
+//  2. The host's root filesystem must be available at /rootfs
+//  3. The nsenter binary must be on the Kubelet process' PATH in the container's
 //     filesystem.
-// 4.  The Kubelet process must have CAP_SYS_ADMIN (required by nsenter); at
+//  4. The Kubelet process must have CAP_SYS_ADMIN (required by nsenter); at
 //     the present, this effectively means that the kubelet is running in a
 //     privileged container.
-// 5.  The volume path used by the Kubelet must be the same inside and outside
+//  5. The volume path used by the Kubelet must be the same inside and outside
 //     the container and be writable by the container (to initialize volume)
 //     contents. TODO: remove this requirement.
-// 6.  The host image must have "mount", "findmnt", "umount", "stat", "touch",
+//  6. The host image must have "mount", "findmnt", "umount", "stat", "touch",
 //     "mkdir", "ls", "sh" and "chmod" binaries in /bin, /usr/sbin, or /usr/bin
-// 7.  The host image should have systemd-run in /bin, /usr/sbin, or /usr/bin if
+//  7. The host image should have systemd-run in /bin, /usr/sbin, or /usr/bin if
 //     systemd is installed/enabled in the operating system.
+//
 // For more information about mount propagation modes, see:
-//   https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
+//
+//	https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
 type NSEnter struct {
 	// a map of commands to their paths on the host filesystem
 	paths map[string]string
@@ -174,10 +176,13 @@ func (ne *NSEnter) SupportsSystemd() (string, bool) {
 // exist. When it's false, it evaluates symlinks of the existing part and
 // blindly adds the non-existing part:
 // pathname: /mnt/volume/non/existing/directory
-//     /mnt/volume exists
-//                non/existing/directory does not exist
+//
+//	/mnt/volume exists
+//	           non/existing/directory does not exist
+//
 // -> It resolves symlinks in /mnt/volume to say /mnt/foo and returns
-//    /mnt/foo/non/existing/directory.
+//
+//	/mnt/foo/non/existing/directory.
 //
 // BEWARE! EvalSymlinks is not able to detect symlink looks with mustExist=false!
 // If /tmp/link is symlink to /tmp/link, EvalSymlinks(/tmp/link/foo) returns /tmp/link/foo.
diff --git a/set/OWNERS b/set/OWNERS
new file mode 100644
index 0000000..9d2d33e
--- /dev/null
+++ b/set/OWNERS
@@ -0,0 +1,8 @@
+# See the OWNERS docs at https://go.k8s.io/owners
+
+reviewers:
+  - logicalhan
+  - thockin
+approvers:
+  - logicalhan
+  - thockin
diff --git a/set/ordered.go b/set/ordered.go
new file mode 100644
index 0000000..2b2c11f
--- /dev/null
+++ b/set/ordered.go
@@ -0,0 +1,53 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package set
+
+// ordered is a constraint that permits any ordered type: any type
+// that supports the operators < <= >= >.
+// If future releases of Go add new ordered types,
+// this constraint will be modified to include them.
+type ordered interface {
+	integer | float | ~string
+}
+
+// integer is a constraint that permits any integer type.
+// If future releases of Go add new predeclared integer types,
+// this constraint will be modified to include them.
+type integer interface {
+	signed | unsigned
+}
+
+// float is a constraint that permits any floating-point type.
+// If future releases of Go add new predeclared floating-point types,
+// this constraint will be modified to include them.
+type float interface {
+	~float32 | ~float64
+}
+
+// signed is a constraint that permits any signed integer type.
+// If future releases of Go add new predeclared signed integer types,
+// this constraint will be modified to include them.
+type signed interface {
+	~int | ~int8 | ~int16 | ~int32 | ~int64
+}
+
+// unsigned is a constraint that permits any unsigned integer type.
+// If future releases of Go add new predeclared unsigned integer types,
+// this constraint will be modified to include them.
+type unsigned interface {
+	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
+}
diff --git a/set/set.go b/set/set.go
new file mode 100644
index 0000000..172482c
--- /dev/null
+++ b/set/set.go
@@ -0,0 +1,229 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package set
+
+import (
+	"sort"
+)
+
+// Empty is public since it is used by some internal API objects for conversions between external
+// string arrays and internal sets, and conversion logic requires public types today.
+type Empty struct{}
+
+// Set is a set of the same type elements, implemented via map[ordered]struct{} for minimal memory consumption.
+type Set[E ordered] map[E]Empty
+
+// New creates a new set.
+func New[E ordered](items ...E) Set[E] {
+	ss := Set[E]{}
+	ss.Insert(items...)
+	return ss
+}
+
+// KeySet creates a Set[E] from a keys of a map[E](? extends interface{}).
+func KeySet[E ordered, A any](theMap map[E]A) Set[E] {
+	ret := Set[E]{}
+	for key := range theMap {
+		ret.Insert(key)
+	}
+	return ret
+}
+
+// Insert adds items to the set.
+func (s Set[E]) Insert(items ...E) Set[E] {
+	for _, item := range items {
+		s[item] = Empty{}
+	}
+	return s
+}
+
+// Delete removes all items from the set.
+func (s Set[E]) Delete(items ...E) Set[E] {
+	for _, item := range items {
+		delete(s, item)
+	}
+	return s
+}
+
+// Has returns true if and only if item is contained in the set.
+func (s Set[E]) Has(item E) bool {
+	_, contained := s[item]
+	return contained
+}
+
+// HasAll returns true if and only if all items are contained in the set.
+func (s Set[E]) HasAll(items ...E) bool {
+	for _, item := range items {
+		if !s.Has(item) {
+			return false
+		}
+	}
+	return true
+}
+
+// HasAny returns true if any items are contained in the set.
+func (s Set[E]) HasAny(items ...E) bool {
+	for _, item := range items {
+		if s.Has(item) {
+			return true
+		}
+	}
+	return false
+}
+
+// Union returns a new set which includes items in either s1 or s2.
+// For example:
+// s1 = {a1, a2}
+// s2 = {a3, a4}
+// s1.Union(s2) = {a1, a2, a3, a4}
+// s2.Union(s1) = {a1, a2, a3, a4}
+func (s Set[E]) Union(s2 Set[E]) Set[E] {
+	result := Set[E]{}
+	result.Insert(s.UnsortedList()...)
+	result.Insert(s2.UnsortedList()...)
+	return result
+}
+
+// Len returns the number of elements in the set.
+func (s Set[E]) Len() int {
+	return len(s)
+}
+
+// Intersection returns a new set which includes the item in BOTH s1 and s2
+// For example:
+// s1 = {a1, a2}
+// s2 = {a2, a3}
+// s1.Intersection(s2) = {a2}
+func (s Set[E]) Intersection(s2 Set[E]) Set[E] {
+	var walk, other Set[E]
+	result := Set[E]{}
+	if s.Len() < s2.Len() {
+		walk = s
+		other = s2
+	} else {
+		walk = s2
+		other = s
+	}
+	for key := range walk {
+		if other.Has(key) {
+			result.Insert(key)
+		}
+	}
+	return result
+}
+
+// IsSuperset returns true if and only if s1 is a superset of s2.
+func (s Set[E]) IsSuperset(s2 Set[E]) bool {
+	for item := range s2 {
+		if !s.Has(item) {
+			return false
+		}
+	}
+	return true
+}
+
+// Difference returns a set of objects that are not in s2
+// For example:
+// s1 = {a1, a2, a3}
+// s2 = {a1, a2, a4, a5}
+// s1.Difference(s2) = {a3}
+// s2.Difference(s1) = {a4, a5}
+func (s Set[E]) Difference(s2 Set[E]) Set[E] {
+	result := Set[E]{}
+	for key := range s {
+		if !s2.Has(key) {
+			result.Insert(key)
+		}
+	}
+	return result
+}
+
+// Equal returns true if and only if s1 is equal (as a set) to s2.
+// Two sets are equal if their membership is identical.
+func (s Set[E]) Equal(s2 Set[E]) bool {
+	return s.Len() == s.Len() && s.IsSuperset(s2)
+}
+
+type sortableSlice[E ordered] []E
+
+func (s sortableSlice[E]) Len() int {
+	return len(s)
+}
+func (s sortableSlice[E]) Less(i, j int) bool { return s[i] < s[j] }
+func (s sortableSlice[E]) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
+
+// SortedList returns the contents as a sorted slice.
+func (s Set[E]) SortedList() []E {
+	res := make(sortableSlice[E], 0, s.Len())
+	for key := range s {
+		res = append(res, key)
+	}
+	sort.Sort(res)
+	return res
+}
+
+// UnsortedList returns the slice with contents in random order.
+func (s Set[E]) UnsortedList() []E {
+	res := make([]E, 0, len(s))
+	for key := range s {
+		res = append(res, key)
+	}
+	return res
+}
+
+// PopAny returns a single element from the set.
+func (s Set[E]) PopAny() (E, bool) {
+	for key := range s {
+		s.Delete(key)
+		return key, true
+	}
+	var zeroValue E
+	return zeroValue, false
+}
+
+// Clone returns a new set which is a copy of the current set.
+func (s Set[T]) Clone() Set[T] {
+	result := make(Set[T], len(s))
+	for key := range s {
+		result.Insert(key)
+	}
+	return result
+}
+
+// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.
+// For example:
+// s1 = {a1, a2, a3}
+// s2 = {a1, a2, a4, a5}
+// s1.SymmetricDifference(s2) = {a3, a4, a5}
+// s2.SymmetricDifference(s1) = {a3, a4, a5}
+func (s Set[T]) SymmetricDifference(s2 Set[T]) Set[T] {
+	return s.Difference(s2).Union(s2.Difference(s))
+}
+
+// Clear empties the set.
+// It is preferable to replace the set with a newly constructed set,
+// but not all callers can do that (when there are other references to the map).
+// In some cases the set *won't* be fully cleared, e.g. a Set[float32] containing NaN
+// can't be cleared because NaN can't be removed.
+// For sets containing items of a type that is reflexive for ==,
+// this is optimized to a single call to runtime.mapclear().
+func (s Set[T]) Clear() Set[T] {
+	for key := range s {
+		delete(s, key)
+	}
+	return s
+}
diff --git a/set/set_test.go b/set/set_test.go
new file mode 100644
index 0000000..ea2d9d5
--- /dev/null
+++ b/set/set_test.go
@@ -0,0 +1,365 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package set
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestStringSetHasAll(t *testing.T) {
+	s := New[string]()
+	s2 := New[string]()
+	if len(s) != 0 {
+		t.Errorf("Expected len=0: %d", len(s))
+	}
+	s.Insert("a", "b")
+	if len(s) != 2 {
+		t.Errorf("Expected len=2: %d", len(s))
+	}
+	s.Insert("c")
+	if s.Has("d") {
+		t.Errorf("Unexpected contents: %#v", s)
+	}
+	if !s.Has("a") {
+		t.Errorf("Missing contents: %#v", s)
+	}
+	s.Delete("a")
+	if s.Has("a") {
+		t.Errorf("Unexpected contents: %#v", s)
+	}
+	s.Insert("a")
+	if s.HasAll("a", "b", "d") {
+		t.Errorf("Unexpected contents: %#v", s)
+	}
+	if !s.HasAll("a", "b") {
+		t.Errorf("Missing contents: %#v", s)
+	}
+	s2.Insert("a", "b", "d")
+	if s.IsSuperset(s2) {
+		t.Errorf("Unexpected contents: %#v", s)
+	}
+	s2.Delete("d")
+	if !s.IsSuperset(s2) {
+		t.Errorf("Missing contents: %#v", s)
+	}
+}
+
+func TestTypeInference(t *testing.T) {
+	s := New("a", "b", "c")
+	if len(s) != 3 {
+		t.Errorf("Expected len=3: %d", len(s))
+	}
+}
+
+func TestStringSetDeleteMultiples(t *testing.T) {
+	s := New[string]()
+	s.Insert("a", "b", "c")
+	if len(s) != 3 {
+		t.Errorf("Expected len=3: %d", len(s))
+	}
+
+	s.Delete("a", "c")
+	if len(s) != 1 {
+		t.Errorf("Expected len=1: %d", len(s))
+	}
+	if s.Has("a") {
+		t.Errorf("Unexpected contents: %#v", s)
+	}
+	if s.Has("c") {
+		t.Errorf("Unexpected contents: %#v", s)
+	}
+	if !s.Has("b") {
+		t.Errorf("Missing contents: %#v", s)
+	}
+}
+
+func TestNewStringSetWithMultipleStrings(t *testing.T) {
+	s := New[string]("a", "b", "c")
+	if len(s) != 3 {
+		t.Errorf("Expected len=3: %d", len(s))
+	}
+	if !s.Has("a") || !s.Has("b") || !s.Has("c") {
+		t.Errorf("Unexpected contents: %#v", s)
+	}
+}
+
+func TestStringSetSortedList(t *testing.T) {
+	s := New[string]("z", "y", "x", "a")
+	if !reflect.DeepEqual(s.SortedList(), []string{"a", "x", "y", "z"}) {
+		t.Errorf("SortedList gave unexpected result: %#v", s.SortedList())
+	}
+}
+
+func TestStringSetUnsortedList(t *testing.T) {
+	s := New[string]("z", "y", "x", "a")
+	ul := s.UnsortedList()
+	if len(ul) != 4 || !s.Has("z") || !s.Has("y") || !s.Has("x") || !s.Has("a") {
+		t.Errorf("UnsortedList gave unexpected result: %#v", s.UnsortedList())
+	}
+}
+
+func TestStringSetDifference(t *testing.T) {
+	a := New[string]("1", "2", "3")
+	b := New[string]("1", "2", "4", "5")
+	c := a.Difference(b)
+	d := b.Difference(a)
+	if len(c) != 1 {
+		t.Errorf("Expected len=1: %d", len(c))
+	}
+	if !c.Has("3") {
+		t.Errorf("Unexpected contents: %#v", c.SortedList())
+	}
+	if len(d) != 2 {
+		t.Errorf("Expected len=2: %d", len(d))
+	}
+	if !d.Has("4") || !d.Has("5") {
+		t.Errorf("Unexpected contents: %#v", d.SortedList())
+	}
+}
+
+func TestStringSetHasAny(t *testing.T) {
+	a := New[string]("1", "2", "3")
+
+	if !a.HasAny("1", "4") {
+		t.Errorf("expected true, got false")
+	}
+
+	if a.HasAny("0", "4") {
+		t.Errorf("expected false, got true")
+	}
+}
+
+func TestStringSetEquals(t *testing.T) {
+	// Simple case (order doesn't matter)
+	a := New[string]("1", "2")
+	b := New[string]("2", "1")
+	if !a.Equal(b) {
+		t.Errorf("Expected to be equal: %v vs %v", a, b)
+	}
+
+	// It is a set; duplicates are ignored
+	b = New[string]("2", "2", "1")
+	if !a.Equal(b) {
+		t.Errorf("Expected to be equal: %v vs %v", a, b)
+	}
+
+	// Edge cases around empty sets / empty strings
+	a = New[string]()
+	b = New[string]()
+	if !a.Equal(b) {
+		t.Errorf("Expected to be equal: %v vs %v", a, b)
+	}
+
+	b = New[string]("1", "2", "3")
+	if a.Equal(b) {
+		t.Errorf("Expected to be not-equal: %v vs %v", a, b)
+	}
+
+	b = New[string]("1", "2", "")
+	if a.Equal(b) {
+		t.Errorf("Expected to be not-equal: %v vs %v", a, b)
+	}
+
+	// Check for equality after mutation
+	a = New[string]()
+	a.Insert("1")
+	if a.Equal(b) {
+		t.Errorf("Expected to be not-equal: %v vs %v", a, b)
+	}
+
+	a.Insert("2")
+	if a.Equal(b) {
+		t.Errorf("Expected to be not-equal: %v vs %v", a, b)
+	}
+
+	a.Insert("")
+	if !a.Equal(b) {
+		t.Errorf("Expected to be equal: %v vs %v", a, b)
+	}
+
+	a.Delete("")
+	if a.Equal(b) {
+		t.Errorf("Expected to be not-equal: %v vs %v", a, b)
+	}
+}
+
+func TestStringUnion(t *testing.T) {
+	tests := []struct {
+		s1       Set[string]
+		s2       Set[string]
+		expected Set[string]
+	}{
+		{
+			New[string]("1", "2", "3", "4"),
+			New[string]("3", "4", "5", "6"),
+			New[string]("1", "2", "3", "4", "5", "6"),
+		},
+		{
+			New[string]("1", "2", "3", "4"),
+			New[string](),
+			New[string]("1", "2", "3", "4"),
+		},
+		{
+			New[string](),
+			New[string]("1", "2", "3", "4"),
+			New[string]("1", "2", "3", "4"),
+		},
+		{
+			New[string](),
+			New[string](),
+			New[string](),
+		},
+	}
+
+	for _, test := range tests {
+		union := test.s1.Union(test.s2)
+		if union.Len() != test.expected.Len() {
+			t.Errorf("Expected union.Len()=%d but got %d", test.expected.Len(), union.Len())
+		}
+
+		if !union.Equal(test.expected) {
+			t.Errorf("Expected union.Equal(expected) but not true.  union:%v expected:%v", union.SortedList(), test.expected.SortedList())
+		}
+	}
+}
+
+func TestStringIntersection(t *testing.T) {
+	tests := []struct {
+		s1       Set[string]
+		s2       Set[string]
+		expected Set[string]
+	}{
+		{
+			New[string]("1", "2", "3", "4"),
+			New[string]("3", "4", "5", "6"),
+			New[string]("3", "4"),
+		},
+		{
+			New[string]("1", "2", "3", "4"),
+			New[string]("1", "2", "3", "4"),
+			New[string]("1", "2", "3", "4"),
+		},
+		{
+			New[string]("1", "2", "3", "4"),
+			New[string](),
+			New[string](),
+		},
+		{
+			New[string](),
+			New[string]("1", "2", "3", "4"),
+			New[string](),
+		},
+		{
+			New[string](),
+			New[string](),
+			New[string](),
+		},
+	}
+
+	for _, test := range tests {
+		intersection := test.s1.Intersection(test.s2)
+		if intersection.Len() != test.expected.Len() {
+			t.Errorf("Expected intersection.Len()=%d but got %d", test.expected.Len(), intersection.Len())
+		}
+
+		if !intersection.Equal(test.expected) {
+			t.Errorf("Expected intersection.Equal(expected) but not true.  intersection:%v expected:%v", intersection.SortedList(), test.expected.SortedList())
+		}
+	}
+}
+
+func TestKeySet(t *testing.T) {
+	m := map[string]string{
+		"hallo":   "world",
+		"goodbye": "and goodnight",
+	}
+	expected := []string{"goodbye", "hallo"}
+	gotList := KeySet(m).SortedList() // List() returns a sorted list
+	if len(gotList) != len(m) {
+		t.Fatalf("got %v elements, wanted %v", len(gotList), len(m))
+	}
+	for i, entry := range KeySet(m).SortedList() {
+		if entry != expected[i] {
+			t.Errorf("got %v, expected %v", entry, expected[i])
+		}
+	}
+}
+
+func TestSetSymmetricDifference(t *testing.T) {
+	a := New("1", "2", "3")
+	b := New("1", "2", "4", "5")
+	c := a.SymmetricDifference(b)
+	d := b.SymmetricDifference(a)
+	if !c.Equal(New("3", "4", "5")) {
+		t.Errorf("Unexpected contents: %#v", c.SortedList())
+	}
+	if !d.Equal(New("3", "4", "5")) {
+		t.Errorf("Unexpected contents: %#v", d.SortedList())
+	}
+}
+
+func TestSetClear(t *testing.T) {
+	s := New[string]()
+	s.Insert("a", "b", "c")
+	if s.Len() != 3 {
+		t.Errorf("Expected len=3: %d", s.Len())
+	}
+
+	s.Clear()
+	if s.Len() != 0 {
+		t.Errorf("Expected len=0: %d", s.Len())
+	}
+}
+
+func TestSetClearWithSharedReference(t *testing.T) {
+	s := New[string]()
+	s.Insert("a", "b", "c")
+	if s.Len() != 3 {
+		t.Errorf("Expected len=3: %d", s.Len())
+	}
+
+	m := s
+	s.Clear()
+	if s.Len() != 0 {
+		t.Errorf("Expected len=0 on the cleared set: %d", s.Len())
+	}
+	if m.Len() != 0 {
+		t.Errorf("Expected len=0 on the shared reference: %d", m.Len())
+	}
+}
+
+func TestSetClearInSeparateFunction(t *testing.T) {
+	s := New[string]()
+	s.Insert("a", "b", "c")
+	if s.Len() != 3 {
+		t.Errorf("Expected len=3: %d", s.Len())
+	}
+
+	clearSetAndAdd(s, "d")
+	if s.Len() != 1 {
+		t.Errorf("Expected len=1: %d", s.Len())
+	}
+	if !s.Has("d") {
+		t.Errorf("Unexpected contents: %#v", s)
+	}
+}
+
+func clearSetAndAdd[T ordered](s Set[T], a T) {
+	s.Clear()
+	s.Insert(a)
+}
diff --git a/trace/trace.go b/trace/trace.go
index a0b07a6..187eb5d 100644
--- a/trace/trace.go
+++ b/trace/trace.go
@@ -65,6 +65,11 @@ func durationToMilliseconds(timeDuration time.Duration) int64 {
 }
 
 type traceItem interface {
+	// rLock must be called before invoking time or writeItem.
+	rLock()
+	// rUnlock must be called after processing the item is complete.
+	rUnlock()
+
 	// time returns when the trace was recorded as completed.
 	time() time.Time
 	// writeItem outputs the traceItem to the buffer. If stepThreshold is non-nil, only output the
@@ -79,6 +84,10 @@ type traceStep struct {
 	fields   []Field
 }
 
+// rLock doesn't need to do anything because traceStep instances are immutable.
+func (s traceStep) rLock()   {}
+func (s traceStep) rUnlock() {}
+
 func (s traceStep) time() time.Time {
 	return s.stepTime
 }
@@ -106,6 +115,14 @@ type Trace struct {
 	traceItems []traceItem
 }
 
+func (t *Trace) rLock() {
+	t.lock.RLock()
+}
+
+func (t *Trace) rUnlock() {
+	t.lock.RUnlock()
+}
+
 func (t *Trace) time() time.Time {
 	if t.endTime != nil {
 		return *t.endTime
@@ -231,8 +248,10 @@ func (t *Trace) logTrace() {
 func (t *Trace) writeTraceSteps(b *bytes.Buffer, formatter string, stepThreshold *time.Duration) {
 	lastStepTime := t.startTime
 	for _, stepOrTrace := range t.traceItems {
+		stepOrTrace.rLock()
 		stepOrTrace.writeItem(b, formatter, lastStepTime, stepThreshold)
 		lastStepTime = stepOrTrace.time()
+		stepOrTrace.rUnlock()
 	}
 }
 

More details

Full run details

Historical runs