diff --git a/components/context/src/main/java/datadog/context/Context.java b/components/context/src/main/java/datadog/context/Context.java index 7ca2309bece..169abe00b62 100644 --- a/components/context/src/main/java/datadog/context/Context.java +++ b/components/context/src/main/java/datadog/context/Context.java @@ -4,20 +4,44 @@ import static datadog.context.ContextProviders.manager; import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; /** * Immutable context scoped to an execution unit or carrier object. * - *

Each element of the context is accessible by its {@link ContextKey}. Keys represents product - * or functional areas and should be created sparingly. Elements in the context may themselves be - * mutable. + *

There are three ways to get a Context instance: + * + *

+ * + *

When there is no context attached to the current execution unit, {@link #current()} will + * return the root context. Similarly, {@link #from(Object carrier)} will return the root context + * when there is no context attached to the carrier. + * + *

From a {@link Context} instance, each value is stored and retrieved by its {@link ContextKey}, + * using {@link #with(ContextKey key, Object value)} to store a value (creating a new immutable + * {@link Context} instance), and {@link #get(ContextKey)} to retrieve it. {@link ContextKey}s + * represent product of functional areas, and should be created sparingly. + * + *

{@link Context} instances are thread safe as they are immutable (including their {@link + * ContextKey}) but the value they hold may themselves be mutable. + * + * @see ContextKey */ +@ParametersAreNonnullByDefault public interface Context { - /** * Returns the root context. * - *

This is the initial local context that all contexts extend. + * @return the initial local context that all contexts extend. */ static Context root() { return manager().root(); @@ -26,7 +50,7 @@ static Context root() { /** * Returns the context attached to the current execution unit. * - * @return Attached context; {@link #root()} if there is none + * @return the attached context; {@link #root()} if there is none. */ static Context current() { return manager().current(); @@ -35,7 +59,7 @@ static Context current() { /** * Attaches this context to the current execution unit. * - * @return Scope to be closed when the context is invalid. + * @return a scope to be closed when the context is invalid. */ default ContextScope attach() { return manager().attach(this); @@ -44,7 +68,7 @@ default ContextScope attach() { /** * Swaps this context with the one attached to current execution unit. * - * @return Previously attached context; {@link #root()} if there was none + * @return the previously attached context; {@link #root()} if there was none. */ default Context swap() { return manager().swap(this); @@ -53,13 +77,18 @@ default Context swap() { /** * Returns the context attached to the given carrier object. * - * @return Attached context; {@link #root()} if there is none + * @param carrier the carrier object to get the context from. + * @return the attached context; {@link #root()} if there is none. */ static Context from(Object carrier) { return binder().from(carrier); } - /** Attaches this context to the given carrier object. */ + /** + * Attaches this context to the given carrier object. + * + * @param carrier the object to carry the context. + */ default void attachTo(Object carrier) { binder().attachTo(carrier, this); } @@ -67,7 +96,8 @@ default void attachTo(Object carrier) { /** * Detaches the context attached to the given carrier object, leaving it context-less. * - * @return Previously attached context; {@link #root()} if there was none + * @param carrier the carrier object to detach its context from. + * @return the previously attached context; {@link #root()} if there was none. */ static Context detachFrom(Object carrier) { return binder().detachFrom(carrier); @@ -76,24 +106,37 @@ static Context detachFrom(Object carrier) { /** * Gets the value stored in this context under the given key. * - * @return Value stored under the key; {@code null} if there is no value. + * @param the type of the value. + * @param key the key used to store the value. + * @return the value stored under the key; {@code null} if there is none. */ @Nullable T get(ContextKey key); /** - * Creates a new context from the same elements, except the key is now mapped to the given value. + * Creates a copy of this context with the given key-value set. + * + *

Existing value with the given key will be replaced, and mapping to a {@code null} value will + * remove the key-value from the context copy. * - * @return New context with the key-value mapping. + * @param the type of the value. + * @param key the key to store the value. + * @param value the value to store. + * @return a new context with the key-value set. */ - Context with(ContextKey key, T value); + Context with(ContextKey key, @Nullable T value); /** - * Creates a new context from the same elements, except the implicit key is mapped to this value. + * Creates a copy of this context with the implicit key is mapped to the value. * - * @return New context with the implicitly keyed value. + * @param value the value to store. + * @return a new context with the implicitly keyed value set. + * @see #with(ContextKey, Object) */ - default Context with(ImplicitContextKeyed value) { + default Context with(@Nullable ImplicitContextKeyed value) { + if (value == null) { + return root(); + } return value.storeInto(this); } } diff --git a/components/context/src/main/java/datadog/context/ContextBinder.java b/components/context/src/main/java/datadog/context/ContextBinder.java index db461788942..f1e1155139b 100644 --- a/components/context/src/main/java/datadog/context/ContextBinder.java +++ b/components/context/src/main/java/datadog/context/ContextBinder.java @@ -1,26 +1,39 @@ package datadog.context; +import javax.annotation.ParametersAreNonnullByDefault; + /** Binds context to carrier objects. */ +@ParametersAreNonnullByDefault public interface ContextBinder { - /** * Returns the context attached to the given carrier object. * - * @return Attached context; {@link Context#root()} if there is none + * @param carrier the carrier object to get the context from. + * @return the attached context; {@link Context#root()} if there is none. */ Context from(Object carrier); - /** Attaches the given context to the given carrier object. */ + /** + * Attaches the given context to the given carrier object. + * + * @param carrier the object to carry the context. + * @param context the context to attach. + */ void attachTo(Object carrier, Context context); /** * Detaches the context attached to the given carrier object, leaving it context-less. * - * @return Previously attached context; {@link Context#root()} if there was none + * @param carrier the carrier object to detach its context from. + * @return the previously attached context; {@link Context#root()} if there was none. */ Context detachFrom(Object carrier); - /** Requests use of a custom {@link ContextBinder}. */ + /** + * Requests use of a custom {@link ContextBinder}. + * + * @param binder the binder to use (will replace any other binder in use). + */ static void register(ContextBinder binder) { ContextProviders.customBinder = binder; } diff --git a/components/context/src/main/java/datadog/context/ContextKey.java b/components/context/src/main/java/datadog/context/ContextKey.java index 962a1ce28df..c79c24ca592 100644 --- a/components/context/src/main/java/datadog/context/ContextKey.java +++ b/components/context/src/main/java/datadog/context/ContextKey.java @@ -10,8 +10,9 @@ */ public final class ContextKey { private static final AtomicInteger NEXT_INDEX = new AtomicInteger(0); - + /** The key name, for debugging purpose only . */ private final String name; + /** The key unique context, related to {@link IndexedContext} implementation. */ final int index; private ContextKey(String name) { @@ -19,20 +20,25 @@ private ContextKey(String name) { this.index = NEXT_INDEX.getAndIncrement(); } - /** Creates a new key with the given name. */ + /** + * Creates a new key with the given name. + * + * @param name the key name, for debugging purpose only. + * @return the newly created unique key. + */ public static ContextKey named(String name) { return new ContextKey<>(name); } @Override public int hashCode() { - return index; + return this.index; } // we want identity equality, so no need to override equals() @Override public String toString() { - return name; + return this.name; } } diff --git a/components/context/src/main/java/datadog/context/ContextManager.java b/components/context/src/main/java/datadog/context/ContextManager.java index af5811416fb..e259644239d 100644 --- a/components/context/src/main/java/datadog/context/ContextManager.java +++ b/components/context/src/main/java/datadog/context/ContextManager.java @@ -2,36 +2,41 @@ /** Manages context across execution units. */ public interface ContextManager { - /** * Returns the root context. * - *

This is the initial local context that all contexts extend. + * @return the initial local context that all contexts extend. */ Context root(); /** * Returns the context attached to the current execution unit. * - * @return Attached context; {@link #root()} if there is none + * @return the attached context; {@link #root()} if there is none. */ Context current(); /** * Attaches the given context to the current execution unit. * - * @return Scope to be closed when the context is invalid. + * @param context the context to attach. + * @return a scope to be closed when the context is invalid. */ ContextScope attach(Context context); /** * Swaps the given context with the one attached to current execution unit. * - * @return Previously attached context; {@link #root()} if there was none + * @param context the context to swap. + * @return the previously attached context; {@link #root()} if there was none. */ Context swap(Context context); - /** Requests use of a custom {@link ContextManager}. */ + /** + * Requests use of a custom {@link ContextManager}. + * + * @param manager the manager to use (will replace any other manager in use). + */ static void register(ContextManager manager) { ContextProviders.customManager = manager; } diff --git a/components/context/src/main/java/datadog/context/ContextScope.java b/components/context/src/main/java/datadog/context/ContextScope.java index 3048d00b37a..7788a077615 100644 --- a/components/context/src/main/java/datadog/context/ContextScope.java +++ b/components/context/src/main/java/datadog/context/ContextScope.java @@ -2,7 +2,6 @@ /** Controls the validity of context attached to an execution unit. */ public interface ContextScope extends AutoCloseable { - /** Returns the context controlled by this scope. */ Context context(); diff --git a/components/context/src/main/java/datadog/context/EmptyContext.java b/components/context/src/main/java/datadog/context/EmptyContext.java index ff1599fa4ee..fc810c757eb 100644 --- a/components/context/src/main/java/datadog/context/EmptyContext.java +++ b/components/context/src/main/java/datadog/context/EmptyContext.java @@ -1,16 +1,27 @@ package datadog.context; +import static java.util.Objects.requireNonNull; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + /** {@link Context} containing no values. */ +@ParametersAreNonnullByDefault final class EmptyContext implements Context { static final Context INSTANCE = new EmptyContext(); @Override + @Nullable public T get(ContextKey key) { return null; } @Override - public Context with(ContextKey key, T value) { + public Context with(ContextKey key, @Nullable T value) { + requireNonNull(key, "Context key cannot be null"); + if (value == null) { + return this; + } return new SingletonContext(key.index, value); } } diff --git a/components/context/src/main/java/datadog/context/ImplicitContextKeyed.java b/components/context/src/main/java/datadog/context/ImplicitContextKeyed.java index a822158984b..a852a19c01b 100644 --- a/components/context/src/main/java/datadog/context/ImplicitContextKeyed.java +++ b/components/context/src/main/java/datadog/context/ImplicitContextKeyed.java @@ -1,12 +1,16 @@ package datadog.context; +import javax.annotation.ParametersAreNonnullByDefault; + /** {@link Context} value that has its own implicit {@link ContextKey}. */ +@ParametersAreNonnullByDefault public interface ImplicitContextKeyed { - /** * Creates a new context with this value under its chosen key. * - * @return New context with the implicitly keyed value. + * @param context the context to copy the original values from. + * @return the new context with the implicitly keyed value. + * @see Context#with(ImplicitContextKeyed) */ Context storeInto(Context context); } diff --git a/components/context/src/main/java/datadog/context/IndexedContext.java b/components/context/src/main/java/datadog/context/IndexedContext.java index 89bd29b6200..cadc481d707 100644 --- a/components/context/src/main/java/datadog/context/IndexedContext.java +++ b/components/context/src/main/java/datadog/context/IndexedContext.java @@ -2,10 +2,14 @@ import static java.lang.Math.max; import static java.util.Arrays.copyOfRange; +import static java.util.Objects.requireNonNull; import java.util.Arrays; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; /** {@link Context} containing many values. */ +@ParametersAreNonnullByDefault final class IndexedContext implements Context { private final Object[] store; @@ -14,18 +18,20 @@ final class IndexedContext implements Context { } @Override + @Nullable @SuppressWarnings("unchecked") public T get(ContextKey key) { + requireNonNull(key, "Context key cannot be null"); int index = key.index; return index < store.length ? (T) store[index] : null; } @Override - public Context with(ContextKey key, T value) { + public Context with(ContextKey key, @Nullable T value) { + requireNonNull(key, "Context key cannot be null"); int index = key.index; Object[] newStore = copyOfRange(store, 0, max(store.length, index + 1)); newStore[index] = value; - return new IndexedContext(newStore); } diff --git a/components/context/src/main/java/datadog/context/SingletonContext.java b/components/context/src/main/java/datadog/context/SingletonContext.java index b65dffc63ae..7a8a4e98b6f 100644 --- a/components/context/src/main/java/datadog/context/SingletonContext.java +++ b/components/context/src/main/java/datadog/context/SingletonContext.java @@ -1,10 +1,14 @@ package datadog.context; import static java.lang.Math.max; +import static java.util.Objects.requireNonNull; import java.util.Objects; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; /** {@link Context} containing a single value. */ +@ParametersAreNonnullByDefault final class SingletonContext implements Context { private final int index; private final Object value; @@ -15,19 +19,24 @@ final class SingletonContext implements Context { } @Override + @Nullable @SuppressWarnings("unchecked") public V get(ContextKey key) { - return index == key.index ? (V) value : null; + requireNonNull(key, "Context key cannot be null"); + return this.index == key.index ? (V) this.value : null; } @Override - public Context with(ContextKey secondKey, V secondValue) { + public Context with(ContextKey secondKey, @Nullable V secondValue) { + requireNonNull(secondKey, "Context key cannot be null"); int secondIndex = secondKey.index; - if (index == secondIndex) { - return new SingletonContext(index, secondValue); + if (this.index == secondIndex) { + return secondValue == null + ? EmptyContext.INSTANCE + : new SingletonContext(this.index, secondValue); } else { - Object[] store = new Object[max(index, secondIndex) + 1]; - store[index] = value; + Object[] store = new Object[max(this.index, secondIndex) + 1]; + store[this.index] = this.value; store[secondIndex] = secondValue; return new IndexedContext(store); } @@ -38,14 +47,14 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SingletonContext that = (SingletonContext) o; - return index == that.index && Objects.equals(value, that.value); + return this.index == that.index && Objects.equals(this.value, that.value); } @Override public int hashCode() { int result = 31; - result = 31 * result + index; - result = 31 * result + Objects.hashCode(value); + result = 31 * result + this.index; + result = 31 * result + Objects.hashCode(this.value); return result; } } diff --git a/components/context/src/main/java/datadog/context/ThreadLocalContextManager.java b/components/context/src/main/java/datadog/context/ThreadLocalContextManager.java index 928098e2cc1..b492218cb6b 100644 --- a/components/context/src/main/java/datadog/context/ThreadLocalContextManager.java +++ b/components/context/src/main/java/datadog/context/ThreadLocalContextManager.java @@ -2,7 +2,6 @@ /** {@link ContextManager} that uses a {@link ThreadLocal} to track context per thread. */ final class ThreadLocalContextManager implements ContextManager { - private static final ThreadLocal CURRENT_HOLDER = ThreadLocal.withInitial(() -> new Context[] {EmptyContext.INSTANCE}); @@ -18,11 +17,9 @@ public Context current() { @Override public ContextScope attach(Context context) { - Context[] holder = CURRENT_HOLDER.get(); Context previous = holder[0]; holder[0] = context; - return new ContextScope() { private boolean closed; diff --git a/components/context/src/main/java/datadog/context/WeakMapContextBinder.java b/components/context/src/main/java/datadog/context/WeakMapContextBinder.java index 144a62c1c35..eea3728fc8a 100644 --- a/components/context/src/main/java/datadog/context/WeakMapContextBinder.java +++ b/components/context/src/main/java/datadog/context/WeakMapContextBinder.java @@ -1,29 +1,36 @@ package datadog.context; -import java.util.Collections; +import static datadog.context.Context.root; +import static java.util.Collections.synchronizedMap; +import static java.util.Objects.requireNonNull; + import java.util.Map; import java.util.WeakHashMap; +import javax.annotation.ParametersAreNonnullByDefault; /** {@link ContextBinder} that uses a global weak map of carriers to contexts. */ +@ParametersAreNonnullByDefault final class WeakMapContextBinder implements ContextBinder { - - private static final Map TRACKED = - Collections.synchronizedMap(new WeakHashMap<>()); + private static final Map TRACKED = synchronizedMap(new WeakHashMap<>()); @Override public Context from(Object carrier) { + requireNonNull(carrier, "Context carrier cannot be null"); Context bound = TRACKED.get(carrier); - return null != bound ? bound : Context.root(); + return null != bound ? bound : root(); } @Override public void attachTo(Object carrier, Context context) { + requireNonNull(carrier, "Context carrier cannot be null"); + requireNonNull(context, "Context cannot be null. Use detachFrom() instead."); TRACKED.put(carrier, context); } @Override public Context detachFrom(Object carrier) { + requireNonNull(carrier, "Context key cannot be null"); Context previous = TRACKED.remove(carrier); - return null != previous ? previous : Context.root(); + return null != previous ? previous : root(); } } diff --git a/components/context/src/test/java/datadog/context/ContextBinderTest.java b/components/context/src/test/java/datadog/context/ContextBinderTest.java index 6e1b77026c7..9af265fea50 100644 --- a/components/context/src/test/java/datadog/context/ContextBinderTest.java +++ b/components/context/src/test/java/datadog/context/ContextBinderTest.java @@ -1,23 +1,57 @@ package datadog.context; +import static datadog.context.Context.current; +import static datadog.context.Context.detachFrom; +import static datadog.context.Context.from; import static datadog.context.Context.root; import static datadog.context.ContextTest.STRING_KEY; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class ContextBinderTest { + @BeforeEach + void setUp() { + assertEquals(root(), current(), "No context is expected to be set"); + } @Test void testAttachAndDetach() { + // Setting up test Context context = root().with(STRING_KEY, "value"); Object carrier = new Object(); - assertEquals(root(), Context.from(carrier)); + assertEquals(root(), from(carrier), "Carrier expected to hold root context by default"); + // Attaching context context.attachTo(carrier); - assertEquals(context, Context.from(carrier)); + assertEquals(context, from(carrier), "Carrier expected to hold new context"); + assertEquals(root(), current(), "Current execution expected to stay root"); // Detaching removes all context - assertEquals(context, Context.detachFrom(carrier)); - assertEquals(root(), Context.detachFrom(carrier)); - assertEquals(root(), Context.from(carrier)); + assertEquals(context, detachFrom(carrier), "Detached context expected to new context"); + assertEquals(root(), detachFrom(carrier), "Carrier expected to hold no more context"); + assertEquals(root(), from(carrier), "Carrier expected to hold no more context"); + } + + @Test + void testNullCarrier() { + assertThrows( + NullPointerException.class, + () -> assertEquals(root(), from(null), "Binder expected to return non-null context"), + "Null carrier expected to hold root context"); + assertThrows( + NullPointerException.class, + () -> assertEquals(root(), detachFrom(null), "Binder expected to return non-null context"), + "Null carrier expected to hold root context"); + } + + @Test + void testNullContext() { + ContextBinder binder = ContextProviders.binder(); + Object carrier = new Object(); + assertThrows( + NullPointerException.class, + () -> binder.attachTo(carrier, null), + "Attaching null context not expected to throw"); } } diff --git a/components/context/src/test/java/datadog/context/ContextKeyTest.java b/components/context/src/test/java/datadog/context/ContextKeyTest.java index 3ef6236c51e..3ff674145da 100644 --- a/components/context/src/test/java/datadog/context/ContextKeyTest.java +++ b/components/context/src/test/java/datadog/context/ContextKeyTest.java @@ -9,29 +9,28 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; class ContextKeyTest { - @ParameterizedTest - @NullSource - @ValueSource(strings = {"", "key"}) + @NullAndEmptySource + @ValueSource(strings = {"key"}) void testConstructor(String name) { ContextKey key = ContextKey.named(name); - assertNotNull(key); - assertEquals(name, key.toString()); + assertNotNull(key, "created key should not be null"); + assertEquals(name, key.toString(), name + " label should be supported"); } @Test void testKeyNameCollision() { ContextKey key1 = ContextKey.named("same-name"); ContextKey key2 = ContextKey.named("same-name"); - assertNotEquals(key1, key2); + assertNotEquals(key1, key2, "distinct keys should not be equal"); String value = "value"; Context context = Context.root().with(key1, value); - assertEquals(value, context.get(key1)); - assertNull(context.get(key2)); + assertEquals(value, context.get(key1), "the original key should be able to retrieve the value"); + assertNull(context.get(key2), "the distinct keys should not be able to retrieve the value"); } @SuppressWarnings({ @@ -42,17 +41,20 @@ void testKeyNameCollision() { }) @Test void testEqualsAndHashCode() { - ContextKey key1 = ContextKey.named("same-name"); - ContextKey key2 = ContextKey.named("same-name"); + ContextKey key1 = ContextKey.named("some-name"); + ContextKey key2 = ContextKey.named("some-name"); // Test equals on self - assertTrue(key1.equals(key1)); - assertEquals(key1.hashCode(), key1.hashCode()); + assertTrue(key1.equals(key1), "Key should be equal with itself"); + assertEquals(key1.hashCode(), key1.hashCode(), "key hash should be constant"); // Test equals on null - assertFalse(key1.equals(null)); + assertFalse(key1.equals(null), "key should not be equal to null value"); // Test equals on different object type - assertFalse(key1.equals("value")); + assertFalse(key1.equals("value"), "key should not be equal to a different type"); // Test equals on different keys with the same name - assertFalse(key1.equals(key2)); - assertNotEquals(key1.hashCode(), key2.hashCode()); + assertFalse(key1.equals(key2), "different keys with the same name should not be equal"); + assertNotEquals( + key1.hashCode(), + key2.hashCode(), + "different keys with the same name should have the same hash"); } } diff --git a/components/context/src/test/java/datadog/context/ContextManagerTest.java b/components/context/src/test/java/datadog/context/ContextManagerTest.java index ed76419d06b..d8927abf76d 100644 --- a/components/context/src/test/java/datadog/context/ContextManagerTest.java +++ b/components/context/src/test/java/datadog/context/ContextManagerTest.java @@ -15,7 +15,6 @@ import org.junit.jupiter.api.Test; class ContextManagerTest { - @BeforeEach void init() { // Ensure no current context prior starting test diff --git a/components/context/src/test/java/datadog/context/ContextProviderForkedTest.java b/components/context/src/test/java/datadog/context/ContextProviderForkedTest.java index 9c15d37da45..a0f1afafdc6 100644 --- a/components/context/src/test/java/datadog/context/ContextProviderForkedTest.java +++ b/components/context/src/test/java/datadog/context/ContextProviderForkedTest.java @@ -4,27 +4,27 @@ import static datadog.context.ContextTest.STRING_KEY; import static org.junit.jupiter.api.Assertions.assertEquals; +import javax.annotation.Nonnull; import org.junit.jupiter.api.Test; class ContextProviderForkedTest { - @Test void testCustomBinder() { // register a NOOP context binder ContextBinder.register( new ContextBinder() { @Override - public Context from(Object carrier) { + public Context from(@Nonnull Object carrier) { return root(); } @Override - public void attachTo(Object carrier, Context context) { + public void attachTo(@Nonnull Object carrier, @Nonnull Context context) { // no-op } @Override - public Context detachFrom(Object carrier) { + public Context detachFrom(@Nonnull Object carrier) { return root(); } }); diff --git a/components/context/src/test/java/datadog/context/ContextTest.java b/components/context/src/test/java/datadog/context/ContextTest.java index a7ea8f42b52..1fbb58643c0 100644 --- a/components/context/src/test/java/datadog/context/ContextTest.java +++ b/components/context/src/test/java/datadog/context/ContextTest.java @@ -1,5 +1,7 @@ package datadog.context; +import static datadog.context.Context.root; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -7,8 +9,10 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import javax.annotation.Nullable; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; class ContextTest { static final ContextKey STRING_KEY = ContextKey.named("string-key"); @@ -16,40 +20,31 @@ class ContextTest { static final ContextKey FLOAT_KEY = ContextKey.named("float-key"); static final ContextKey LONG_KEY = ContextKey.named("long-key"); - // demonstrate how values can hide their context keys - static class ValueWithKey implements ImplicitContextKeyed { - static final ContextKey HIDDEN_KEY = ContextKey.named("hidden-key"); - - @Override - public Context storeInto(Context context) { - return context.with(HIDDEN_KEY, this); - } - - @Nullable - public static ValueWithKey from(Context context) { - return context.get(HIDDEN_KEY); - } - } - @Test - void testEmpty() { - // Test empty is always the same - Context empty = Context.root(); - assertEquals(empty, Context.root()); - // Test empty is not mutated + void testRoot() { + // Test root is always the same + Context root = root(); + assertEquals(root, root(), "Root context should be consistent"); + // Test root is not mutated String stringValue = "value"; - empty.with(STRING_KEY, stringValue); - assertEquals(empty, Context.root()); + root.with(STRING_KEY, stringValue); + assertEquals(root, root(), "Root context should be immutable"); } - @Test - void testWith() { - Context empty = Context.root(); + static Stream contextImplementations() { + SingletonContext singletonContext = new SingletonContext(ContextKey.named("test").index, true); + IndexedContext indexedContext = new IndexedContext(new Object[0]); + return Stream.of(Context.root(), singletonContext, indexedContext); + } + + @ParameterizedTest + @MethodSource("contextImplementations") + void testWith(Context context) { // Test accessing non-set value - assertNull(empty.get(STRING_KEY)); + assertNull(context.get(STRING_KEY)); // Test retrieving value String stringValue = "value"; - Context context1 = empty.with(STRING_KEY, stringValue); + Context context1 = context.with(STRING_KEY, stringValue); assertEquals(stringValue, context1.get(STRING_KEY)); // Test overriding value String stringValue2 = "value2"; @@ -59,21 +54,27 @@ void testWith() { Context context3 = context2.with(STRING_KEY, null); assertNull(context3.get(STRING_KEY)); // Test null key handling - assertThrows(NullPointerException.class, () -> empty.with(null, "test")); + assertThrows( + NullPointerException.class, () -> context.with(null, "test"), "Context forbids null keys"); + // Test null value handling + assertDoesNotThrow( + () -> context.with(BOOLEAN_KEY, null), "Null value should not throw exception"); + // Test null implicitly keyed value handling + assertDoesNotThrow(() -> context.with(null), "Null implicitly keyed value not throw exception"); } - @Test - void testGet() { + @ParameterizedTest + @MethodSource("contextImplementations") + void testGet(Context original) { // Setup context - Context empty = Context.root(); String value = "value"; - Context context = empty.with(STRING_KEY, value); + Context context = original.with(STRING_KEY, value); // Test null key handling - assertThrows(NullPointerException.class, () -> context.get(null)); + assertThrows(NullPointerException.class, () -> context.get(null), "Context forbids null keys"); // Test unset key - assertNull(context.get(BOOLEAN_KEY)); + assertNull(context.get(BOOLEAN_KEY), "Missing key expected to return null"); // Test set key - assertEquals(value, context.get(STRING_KEY)); + assertEquals(value, context.get(STRING_KEY), "Value expected to be retrieved"); } @SuppressWarnings({ @@ -85,13 +86,13 @@ void testGet() { @Test void testEqualsAndHashCode() { // Setup contexts - Context empty = Context.root(); - Context context1 = empty.with(STRING_KEY, "value"); - Context context2 = empty.with(STRING_KEY, "value "); - Context context3 = empty.with(STRING_KEY, "value ".trim()); - Context context4 = empty.with(STRING_KEY, "value").with(BOOLEAN_KEY, true); + Context root = root(); + Context context1 = root.with(STRING_KEY, "value"); + Context context2 = root.with(STRING_KEY, "value "); + Context context3 = root.with(STRING_KEY, "value ".trim()); + Context context4 = root.with(STRING_KEY, "value").with(BOOLEAN_KEY, true); // Test equals on self - assertTrue(empty.equals(empty)); + assertTrue(root.equals(root)); assertTrue(context1.equals(context1)); assertTrue(context4.equals(context4)); // Test equals on null @@ -104,34 +105,24 @@ void testEqualsAndHashCode() { assertTrue(context1.equals(context3)); assertEquals(context1.hashCode(), context3.hashCode()); // Test equals on different contexts - assertFalse(context1.equals(empty)); - assertNotEquals(context1.hashCode(), empty.hashCode()); + assertFalse(context1.equals(root)); + assertNotEquals(context1.hashCode(), root.hashCode()); assertFalse(context1.equals(context2)); assertNotEquals(context1.hashCode(), context2.hashCode()); assertFalse(context1.equals(context4)); assertNotEquals(context1.hashCode(), context4.hashCode()); - assertFalse(empty.equals(context1)); - assertNotEquals(empty.hashCode(), context1.hashCode()); + assertFalse(root.equals(context1)); + assertNotEquals(root.hashCode(), context1.hashCode()); assertFalse(context2.equals(context1)); assertNotEquals(context2.hashCode(), context1.hashCode()); assertFalse(context4.equals(context1)); assertNotEquals(context4.hashCode(), context1.hashCode()); } - @Test - void testImplicitKey() { - // Setup context - Context empty = Context.root(); - ValueWithKey valueWithKey = new ValueWithKey(); - Context context = empty.with(valueWithKey); - assertNull(ValueWithKey.from(empty)); - assertEquals(valueWithKey, ValueWithKey.from(context)); - } - @SuppressWarnings({"SimplifiableAssertion"}) @Test void testInflation() { - Context empty = Context.root(); + Context empty = root(); Context one = empty.with(STRING_KEY, "unset").with(STRING_KEY, "one"); Context two = one.with(BOOLEAN_KEY, false).with(BOOLEAN_KEY, true); diff --git a/components/context/src/test/java/datadog/context/ImplicitContextKeyedTest.java b/components/context/src/test/java/datadog/context/ImplicitContextKeyedTest.java new file mode 100644 index 00000000000..33e149bbb95 --- /dev/null +++ b/components/context/src/test/java/datadog/context/ImplicitContextKeyedTest.java @@ -0,0 +1,38 @@ +package datadog.context; + +import static datadog.context.Context.root; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +class ImplicitContextKeyedTest { + /** This class demonstrate how values can hide their context keys. */ + static class ValueWithKey implements ImplicitContextKeyed { + /** The private key used to store and retrieve context value. */ + private static final ContextKey HIDDEN_KEY = ContextKey.named("hidden-key"); + + @Override + public Context storeInto(@Nonnull Context context) { + return context.with(HIDDEN_KEY, this); + } + + @Nullable + public static ValueWithKey from(@Nonnull Context context) { + return context.get(HIDDEN_KEY); + } + } + + @Test + void testImplicitKey() { + // Setup context + Context root = root(); + ValueWithKey valueWithKey = new ValueWithKey(); + Context context = root.with(valueWithKey); + assertNull(ValueWithKey.from(root), "No value expected to be extracted from root context"); + assertEquals( + valueWithKey, ValueWithKey.from(context), "Expected to retrieve the implicit keyed value"); + } +}