dcsctp: Added common utilities
These are quite generic utilities that are used by multiple modules
within dcSCTP. Some would be good to have in rtc_base and are simple
replicas of utilities available in abseil.
Bug: webrtc:12614
Change-Id: I9914286ced7317a34628a71697da9149d6d19d38
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/213190
Reviewed-by: Tommi <tommi@webrtc.org>
Commit-Queue: Victor Boivie <boivie@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#33609}
diff --git a/net/dcsctp/BUILD.gn b/net/dcsctp/BUILD.gn
index 0ccc897..ee15a45 100644
--- a/net/dcsctp/BUILD.gn
+++ b/net/dcsctp/BUILD.gn
@@ -13,6 +13,7 @@
testonly = true
deps = [
"../../test:test_main",
+ "common:dcsctp_common_unittests",
"packet:dcsctp_packet_unittests",
]
}
diff --git a/net/dcsctp/common/BUILD.gn b/net/dcsctp/common/BUILD.gn
new file mode 100644
index 0000000..55fa86b
--- /dev/null
+++ b/net/dcsctp/common/BUILD.gn
@@ -0,0 +1,55 @@
+# Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+#
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file in the root of the source
+# tree. An additional intellectual property rights grant can be found
+# in the file PATENTS. All contributing project authors may
+# be found in the AUTHORS file in the root of the source tree.
+
+import("//build/config/linux/pkg_config.gni")
+import("../../../webrtc.gni")
+
+rtc_source_set("math") {
+ deps = []
+ sources = [ "math.h" ]
+}
+
+rtc_source_set("pair_hash") {
+ deps = []
+ sources = [ "pair_hash.h" ]
+}
+
+rtc_source_set("sequence_numbers") {
+ deps = []
+ sources = [ "sequence_numbers.h" ]
+}
+
+rtc_source_set("str_join") {
+ deps = []
+ sources = [ "str_join.h" ]
+}
+
+if (rtc_include_tests) {
+ rtc_library("dcsctp_common_unittests") {
+ testonly = true
+
+ defines = []
+ deps = [
+ ":math",
+ ":pair_hash",
+ ":sequence_numbers",
+ ":str_join",
+ "../../../api:array_view",
+ "../../../rtc_base:checks",
+ "../../../rtc_base:gunit_helpers",
+ "../../../rtc_base:rtc_base_approved",
+ "../../../test:test_support",
+ ]
+ sources = [
+ "math_test.cc",
+ "pair_hash_test.cc",
+ "sequence_numbers_test.cc",
+ "str_join_test.cc",
+ ]
+ }
+}
diff --git a/net/dcsctp/common/math.h b/net/dcsctp/common/math.h
new file mode 100644
index 0000000..ee161d2
--- /dev/null
+++ b/net/dcsctp/common/math.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef NET_DCSCTP_COMMON_MATH_H_
+#define NET_DCSCTP_COMMON_MATH_H_
+
+namespace dcsctp {
+
+// Rounds up `val` to the nearest value that is divisible by four. Frequently
+// used to e.g. pad chunks or parameters to an even 32-bit offset.
+template <typename IntType>
+IntType RoundUpTo4(IntType val) {
+ return (val + 3) & -4;
+}
+
+} // namespace dcsctp
+
+#endif // NET_DCSCTP_COMMON_MATH_H_
diff --git a/net/dcsctp/common/math_test.cc b/net/dcsctp/common/math_test.cc
new file mode 100644
index 0000000..902aefa
--- /dev/null
+++ b/net/dcsctp/common/math_test.cc
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+#include "net/dcsctp/common/math.h"
+
+#include "test/gmock.h"
+
+namespace dcsctp {
+namespace {
+
+TEST(MathUtilTest, CanRoundUpTo4) {
+ EXPECT_EQ(RoundUpTo4(0), 0);
+ EXPECT_EQ(RoundUpTo4(1), 4);
+ EXPECT_EQ(RoundUpTo4(2), 4);
+ EXPECT_EQ(RoundUpTo4(3), 4);
+ EXPECT_EQ(RoundUpTo4(4), 4);
+ EXPECT_EQ(RoundUpTo4(5), 8);
+ EXPECT_EQ(RoundUpTo4(6), 8);
+ EXPECT_EQ(RoundUpTo4(7), 8);
+ EXPECT_EQ(RoundUpTo4(8), 8);
+ EXPECT_EQ(RoundUpTo4(10000000000), 10000000000);
+ EXPECT_EQ(RoundUpTo4(10000000001), 10000000004);
+}
+
+} // namespace
+} // namespace dcsctp
diff --git a/net/dcsctp/common/pair_hash.h b/net/dcsctp/common/pair_hash.h
new file mode 100644
index 0000000..62af8b4
--- /dev/null
+++ b/net/dcsctp/common/pair_hash.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef NET_DCSCTP_COMMON_PAIR_HASH_H_
+#define NET_DCSCTP_COMMON_PAIR_HASH_H_
+
+#include <stddef.h>
+
+#include <functional>
+#include <utility>
+
+namespace dcsctp {
+
+// A custom hash function for std::pair, to be able to be used as key in a
+// std::unordered_map. If absl::flat_hash_map would ever be used, this is
+// unnecessary as it already has a hash function for std::pair.
+struct PairHash {
+ template <class T1, class T2>
+ size_t operator()(const std::pair<T1, T2>& p) const {
+ return (3 * std::hash<T1>{}(p.first)) ^ std::hash<T2>{}(p.second);
+ }
+};
+} // namespace dcsctp
+
+#endif // NET_DCSCTP_COMMON_PAIR_HASH_H_
diff --git a/net/dcsctp/common/pair_hash_test.cc b/net/dcsctp/common/pair_hash_test.cc
new file mode 100644
index 0000000..bcc3ec8
--- /dev/null
+++ b/net/dcsctp/common/pair_hash_test.cc
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+#include "net/dcsctp/common/pair_hash.h"
+
+#include <unordered_map>
+#include <unordered_set>
+
+#include "test/gmock.h"
+
+namespace dcsctp {
+namespace {
+
+TEST(PairHashTest, CanInsertIntoSet) {
+ using MyPair = std::pair<int, int>;
+
+ std::unordered_set<MyPair, PairHash> pairs;
+
+ pairs.insert({1, 2});
+ pairs.insert({3, 4});
+
+ EXPECT_NE(pairs.find({1, 2}), pairs.end());
+ EXPECT_NE(pairs.find({3, 4}), pairs.end());
+ EXPECT_EQ(pairs.find({1, 3}), pairs.end());
+ EXPECT_EQ(pairs.find({3, 3}), pairs.end());
+}
+
+TEST(PairHashTest, CanInsertIntoMap) {
+ using MyPair = std::pair<int, int>;
+
+ std::unordered_map<MyPair, int, PairHash> pairs;
+
+ pairs[{1, 2}] = 99;
+ pairs[{3, 4}] = 100;
+
+ EXPECT_EQ((pairs[{1, 2}]), 99);
+ EXPECT_EQ((pairs[{3, 4}]), 100);
+ EXPECT_EQ(pairs.find({1, 3}), pairs.end());
+ EXPECT_EQ(pairs.find({3, 3}), pairs.end());
+}
+} // namespace
+} // namespace dcsctp
diff --git a/net/dcsctp/common/sequence_numbers.h b/net/dcsctp/common/sequence_numbers.h
new file mode 100644
index 0000000..f604337
--- /dev/null
+++ b/net/dcsctp/common/sequence_numbers.h
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef NET_DCSCTP_COMMON_SEQUENCE_NUMBERS_H_
+#define NET_DCSCTP_COMMON_SEQUENCE_NUMBERS_H_
+
+#include <cstdint>
+#include <limits>
+#include <utility>
+
+namespace dcsctp {
+
+// UnwrappedSequenceNumber handles wrapping sequence numbers and unwraps them to
+// an int64_t value space, to allow wrapped sequence numbers to be easily
+// compared for ordering.
+//
+// Sequence numbers are expected to be monotonically increasing, but they do not
+// need to be unwrapped in order, as long as the difference to the previous one
+// is not larger than half the range of the wrapped sequence number.
+template <typename WrappedType>
+class UnwrappedSequenceNumber {
+ public:
+ static_assert(!std::numeric_limits<WrappedType>::is_signed,
+ "The wrapped type must be unsigned");
+ static_assert(std::numeric_limits<WrappedType>::max() <
+ std::numeric_limits<int64_t>::max(),
+ "The wrapped type must be less than the int64_t value space");
+
+ // The unwrapper is a sort of factory and converts wrapped sequence numbers to
+ // unwrapped ones.
+ class Unwrapper {
+ public:
+ Unwrapper() : largest_(kValueLimit) {}
+ Unwrapper(const Unwrapper&) = default;
+ Unwrapper& operator=(const Unwrapper&) = default;
+
+ // Given a wrapped `value`, and with knowledge of its current last seen
+ // largest number, will return a value that can be compared using normal
+ // operators, such as less-than, greater-than etc.
+ //
+ // This will also update the Unwrapper's state, to track the last seen
+ // largest value.
+ UnwrappedSequenceNumber<WrappedType> Unwrap(WrappedType value) {
+ WrappedType wrapped_largest =
+ static_cast<WrappedType>(largest_ % kValueLimit);
+ int64_t result = largest_ + Delta(value, wrapped_largest);
+ if (largest_ < result) {
+ largest_ = result;
+ }
+ return UnwrappedSequenceNumber<WrappedType>(result);
+ }
+
+ // Similar to `Unwrap`, but will not update the Unwrappers's internal state.
+ UnwrappedSequenceNumber<WrappedType> PeekUnwrap(WrappedType value) const {
+ WrappedType uint32_largest =
+ static_cast<WrappedType>(largest_ % kValueLimit);
+ int64_t result = largest_ + Delta(value, uint32_largest);
+ return UnwrappedSequenceNumber<WrappedType>(result);
+ }
+
+ // Resets the Unwrapper to its pristine state. Used when a sequence number
+ // is to be reset to zero.
+ void Reset() { largest_ = kValueLimit; }
+
+ private:
+ static int64_t Delta(WrappedType value, WrappedType prev_value) {
+ static constexpr WrappedType kBreakpoint = kValueLimit / 2;
+ WrappedType diff = value - prev_value;
+ diff %= kValueLimit;
+ if (diff < kBreakpoint) {
+ return static_cast<int64_t>(diff);
+ }
+ return static_cast<int64_t>(diff) - kValueLimit;
+ }
+
+ int64_t largest_;
+ };
+
+ // Returns the wrapped value this type represents.
+ WrappedType Wrap() const {
+ return static_cast<WrappedType>(value_ % kValueLimit);
+ }
+
+ template <typename H>
+ friend H AbslHashValue(H state,
+ const UnwrappedSequenceNumber<WrappedType>& hash) {
+ return H::combine(std::move(state), hash.value_);
+ }
+
+ bool operator==(const UnwrappedSequenceNumber<WrappedType>& other) const {
+ return value_ == other.value_;
+ }
+ bool operator!=(const UnwrappedSequenceNumber<WrappedType>& other) const {
+ return value_ != other.value_;
+ }
+ bool operator<(const UnwrappedSequenceNumber<WrappedType>& other) const {
+ return value_ < other.value_;
+ }
+ bool operator>(const UnwrappedSequenceNumber<WrappedType>& other) const {
+ return value_ > other.value_;
+ }
+ bool operator>=(const UnwrappedSequenceNumber<WrappedType>& other) const {
+ return value_ >= other.value_;
+ }
+ bool operator<=(const UnwrappedSequenceNumber<WrappedType>& other) const {
+ return value_ <= other.value_;
+ }
+
+ // Increments the value.
+ void Increment() { ++value_; }
+ UnwrappedSequenceNumber<WrappedType> next_value() const {
+ return UnwrappedSequenceNumber<WrappedType>(value_ + 1);
+ }
+
+ // Adds a delta to the current value.
+ UnwrappedSequenceNumber<WrappedType> AddTo(int delta) const {
+ return UnwrappedSequenceNumber<WrappedType>(value_ + delta);
+ }
+
+ // Compares the difference between two sequence numbers.
+ WrappedType Difference(UnwrappedSequenceNumber<WrappedType> other) const {
+ return value_ - other.value_;
+ }
+
+ private:
+ explicit UnwrappedSequenceNumber(int64_t value) : value_(value) {}
+ static constexpr int64_t kValueLimit =
+ static_cast<int64_t>(1) << std::numeric_limits<WrappedType>::digits;
+
+ int64_t value_;
+};
+
+// Transmission Sequence Numbers (TSN)
+using TSN = UnwrappedSequenceNumber<uint32_t>;
+
+// Stream Sequence Numbers (SSN)
+using SSN = UnwrappedSequenceNumber<uint16_t>;
+
+// Message Identifier (MID)
+using MID = UnwrappedSequenceNumber<uint32_t>;
+
+} // namespace dcsctp
+
+#endif // NET_DCSCTP_COMMON_SEQUENCE_NUMBERS_H_
diff --git a/net/dcsctp/common/sequence_numbers_test.cc b/net/dcsctp/common/sequence_numbers_test.cc
new file mode 100644
index 0000000..f7ecd9b
--- /dev/null
+++ b/net/dcsctp/common/sequence_numbers_test.cc
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+#include "net/dcsctp/common/sequence_numbers.h"
+
+#include "test/gmock.h"
+
+namespace dcsctp {
+namespace {
+
+using TestSequence = UnwrappedSequenceNumber<uint16_t>;
+
+TEST(SequenceNumbersTest, SimpleUnwrapping) {
+ TestSequence::Unwrapper unwrapper;
+
+ TestSequence s0 = unwrapper.Unwrap(0);
+ TestSequence s1 = unwrapper.Unwrap(1);
+ TestSequence s2 = unwrapper.Unwrap(2);
+ TestSequence s3 = unwrapper.Unwrap(3);
+
+ EXPECT_LT(s0, s1);
+ EXPECT_LT(s0, s2);
+ EXPECT_LT(s0, s3);
+ EXPECT_LT(s1, s2);
+ EXPECT_LT(s1, s3);
+ EXPECT_LT(s2, s3);
+
+ EXPECT_EQ(s1.Difference(s0), 1);
+ EXPECT_EQ(s2.Difference(s0), 2);
+ EXPECT_EQ(s3.Difference(s0), 3);
+
+ EXPECT_GT(s1, s0);
+ EXPECT_GT(s2, s0);
+ EXPECT_GT(s3, s0);
+ EXPECT_GT(s2, s1);
+ EXPECT_GT(s3, s1);
+ EXPECT_GT(s3, s2);
+
+ s0.Increment();
+ EXPECT_EQ(s0, s1);
+ s1.Increment();
+ EXPECT_EQ(s1, s2);
+ s2.Increment();
+ EXPECT_EQ(s2, s3);
+
+ EXPECT_EQ(s0.AddTo(2), s3);
+}
+
+TEST(SequenceNumbersTest, MidValueUnwrapping) {
+ TestSequence::Unwrapper unwrapper;
+
+ TestSequence s0 = unwrapper.Unwrap(0x7FFE);
+ TestSequence s1 = unwrapper.Unwrap(0x7FFF);
+ TestSequence s2 = unwrapper.Unwrap(0x8000);
+ TestSequence s3 = unwrapper.Unwrap(0x8001);
+
+ EXPECT_LT(s0, s1);
+ EXPECT_LT(s0, s2);
+ EXPECT_LT(s0, s3);
+ EXPECT_LT(s1, s2);
+ EXPECT_LT(s1, s3);
+ EXPECT_LT(s2, s3);
+
+ EXPECT_EQ(s1.Difference(s0), 1);
+ EXPECT_EQ(s2.Difference(s0), 2);
+ EXPECT_EQ(s3.Difference(s0), 3);
+
+ EXPECT_GT(s1, s0);
+ EXPECT_GT(s2, s0);
+ EXPECT_GT(s3, s0);
+ EXPECT_GT(s2, s1);
+ EXPECT_GT(s3, s1);
+ EXPECT_GT(s3, s2);
+
+ s0.Increment();
+ EXPECT_EQ(s0, s1);
+ s1.Increment();
+ EXPECT_EQ(s1, s2);
+ s2.Increment();
+ EXPECT_EQ(s2, s3);
+
+ EXPECT_EQ(s0.AddTo(2), s3);
+}
+
+TEST(SequenceNumbersTest, WrappedUnwrapping) {
+ TestSequence::Unwrapper unwrapper;
+
+ TestSequence s0 = unwrapper.Unwrap(0xFFFE);
+ TestSequence s1 = unwrapper.Unwrap(0xFFFF);
+ TestSequence s2 = unwrapper.Unwrap(0x0000);
+ TestSequence s3 = unwrapper.Unwrap(0x0001);
+
+ EXPECT_LT(s0, s1);
+ EXPECT_LT(s0, s2);
+ EXPECT_LT(s0, s3);
+ EXPECT_LT(s1, s2);
+ EXPECT_LT(s1, s3);
+ EXPECT_LT(s2, s3);
+
+ EXPECT_EQ(s1.Difference(s0), 1);
+ EXPECT_EQ(s2.Difference(s0), 2);
+ EXPECT_EQ(s3.Difference(s0), 3);
+
+ EXPECT_GT(s1, s0);
+ EXPECT_GT(s2, s0);
+ EXPECT_GT(s3, s0);
+ EXPECT_GT(s2, s1);
+ EXPECT_GT(s3, s1);
+ EXPECT_GT(s3, s2);
+
+ s0.Increment();
+ EXPECT_EQ(s0, s1);
+ s1.Increment();
+ EXPECT_EQ(s1, s2);
+ s2.Increment();
+ EXPECT_EQ(s2, s3);
+
+ EXPECT_EQ(s0.AddTo(2), s3);
+}
+
+TEST(SequenceNumbersTest, WrapAroundAFewTimes) {
+ TestSequence::Unwrapper unwrapper;
+
+ TestSequence s0 = unwrapper.Unwrap(0);
+ TestSequence prev = s0;
+
+ for (uint32_t i = 1; i < 65536 * 3; i++) {
+ uint16_t wrapped = static_cast<uint16_t>(i);
+ TestSequence si = unwrapper.Unwrap(wrapped);
+
+ EXPECT_LT(s0, si);
+ EXPECT_LT(prev, si);
+ prev = si;
+ }
+}
+
+TEST(SequenceNumbersTest, IncrementIsSameAsWrapped) {
+ TestSequence::Unwrapper unwrapper;
+
+ TestSequence s0 = unwrapper.Unwrap(0);
+
+ for (uint32_t i = 1; i < 65536 * 2; i++) {
+ uint16_t wrapped = static_cast<uint16_t>(i);
+ TestSequence si = unwrapper.Unwrap(wrapped);
+
+ s0.Increment();
+ EXPECT_EQ(s0, si);
+ }
+}
+
+TEST(SequenceNumbersTest, UnwrappingLargerNumberIsAlwaysLarger) {
+ TestSequence::Unwrapper unwrapper;
+
+ for (uint32_t i = 1; i < 65536 * 2; i++) {
+ uint16_t wrapped = static_cast<uint16_t>(i);
+ TestSequence si = unwrapper.Unwrap(wrapped);
+
+ EXPECT_GT(unwrapper.Unwrap(wrapped + 1), si);
+ EXPECT_GT(unwrapper.Unwrap(wrapped + 5), si);
+ EXPECT_GT(unwrapper.Unwrap(wrapped + 10), si);
+ EXPECT_GT(unwrapper.Unwrap(wrapped + 100), si);
+ }
+}
+
+TEST(SequenceNumbersTest, UnwrappingSmallerNumberIsAlwaysSmaller) {
+ TestSequence::Unwrapper unwrapper;
+
+ for (uint32_t i = 1; i < 65536 * 2; i++) {
+ uint16_t wrapped = static_cast<uint16_t>(i);
+ TestSequence si = unwrapper.Unwrap(wrapped);
+
+ EXPECT_LT(unwrapper.Unwrap(wrapped - 1), si);
+ EXPECT_LT(unwrapper.Unwrap(wrapped - 5), si);
+ EXPECT_LT(unwrapper.Unwrap(wrapped - 10), si);
+ EXPECT_LT(unwrapper.Unwrap(wrapped - 100), si);
+ }
+}
+
+} // namespace
+} // namespace dcsctp
diff --git a/net/dcsctp/common/str_join.h b/net/dcsctp/common/str_join.h
new file mode 100644
index 0000000..0451782
--- /dev/null
+++ b/net/dcsctp/common/str_join.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef NET_DCSCTP_COMMON_STR_JOIN_H_
+#define NET_DCSCTP_COMMON_STR_JOIN_H_
+
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "rtc_base/strings/string_builder.h"
+
+namespace dcsctp {
+
+template <typename Range>
+std::string StrJoin(const Range& seq, absl::string_view delimiter) {
+ rtc::StringBuilder sb;
+ int idx = 0;
+
+ for (const typename Range::value_type& elem : seq) {
+ if (idx > 0) {
+ sb << delimiter;
+ }
+ sb << elem;
+
+ ++idx;
+ }
+ return sb.Release();
+}
+
+template <typename Range, typename Functor>
+std::string StrJoin(const Range& seq,
+ absl::string_view delimiter,
+ const Functor& fn) {
+ rtc::StringBuilder sb;
+ int idx = 0;
+
+ for (const typename Range::value_type& elem : seq) {
+ if (idx > 0) {
+ sb << delimiter;
+ }
+ fn(sb, elem);
+
+ ++idx;
+ }
+ return sb.Release();
+}
+
+} // namespace dcsctp
+
+#endif // NET_DCSCTP_COMMON_STR_JOIN_H_
diff --git a/net/dcsctp/common/str_join_test.cc b/net/dcsctp/common/str_join_test.cc
new file mode 100644
index 0000000..dbfd92c
--- /dev/null
+++ b/net/dcsctp/common/str_join_test.cc
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+#include "net/dcsctp/common/str_join.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "test/gmock.h"
+
+namespace dcsctp {
+namespace {
+
+TEST(StrJoinTest, CanJoinStringsFromVector) {
+ std::vector<std::string> strings = {"Hello", "World"};
+ std::string s = StrJoin(strings, " ");
+ EXPECT_EQ(s, "Hello World");
+}
+
+TEST(StrJoinTest, CanJoinNumbersFromArray) {
+ std::array<int, 3> numbers = {1, 2, 3};
+ std::string s = StrJoin(numbers, ",");
+ EXPECT_EQ(s, "1,2,3");
+}
+
+TEST(StrJoinTest, CanFormatElementsWhileJoining) {
+ std::vector<std::pair<std::string, std::string>> pairs = {
+ {"hello", "world"}, {"foo", "bar"}, {"fum", "gazonk"}};
+ std::string s = StrJoin(pairs, ",",
+ [&](rtc::StringBuilder& sb,
+ const std::pair<std::string, std::string>& p) {
+ sb << p.first << "=" << p.second;
+ });
+ EXPECT_EQ(s, "hello=world,foo=bar,fum=gazonk");
+}
+
+} // namespace
+} // namespace dcsctp