Skip to content

Commit fc36b7d

Browse files
committed
Add unit tests
Signed-off-by: Craig Perkins <cwperx@amazon.com>
1 parent f04c2f1 commit fc36b7d

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package org.opensearch.action.admin.indices.mapping.get;
2+
3+
import org.apache.lucene.util.RamUsageEstimator;
4+
import org.opensearch.action.support.ActionFilters;
5+
import org.opensearch.cluster.ClusterState;
6+
import org.opensearch.cluster.metadata.IndexMetadata;
7+
import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
8+
import org.opensearch.cluster.metadata.MappingMetadata;
9+
import org.opensearch.cluster.metadata.Metadata;
10+
import org.opensearch.cluster.service.ClusterService;
11+
import org.opensearch.common.compress.CompressedXContent;
12+
import org.opensearch.core.action.ActionListener;
13+
import org.opensearch.core.common.breaker.CircuitBreaker;
14+
import org.opensearch.core.indices.breaker.CircuitBreakerService;
15+
import org.opensearch.indices.IndicesService;
16+
import org.opensearch.test.OpenSearchTestCase;
17+
import org.opensearch.threadpool.ThreadPool;
18+
import org.opensearch.transport.TransportService;
19+
import org.junit.Before;
20+
21+
import java.io.IOException;
22+
import java.util.Map;
23+
24+
import org.mockito.ArgumentCaptor;
25+
import org.mockito.Captor;
26+
import org.mockito.MockitoAnnotations;
27+
28+
import static org.mockito.ArgumentMatchers.any;
29+
import static org.mockito.ArgumentMatchers.anyLong;
30+
import static org.mockito.ArgumentMatchers.eq;
31+
import static org.mockito.Mockito.mock;
32+
import static org.mockito.Mockito.never;
33+
import static org.mockito.Mockito.times;
34+
import static org.mockito.Mockito.verify;
35+
import static org.mockito.Mockito.when;
36+
37+
/**
38+
* Tests for TransportGetMappingsAction circuit breaker + estimation logic.
39+
*/
40+
public class TransportGetMappingsActionTests extends OpenSearchTestCase {
41+
42+
private TransportService transportService;
43+
private ClusterService clusterService;
44+
private ThreadPool threadPool;
45+
private ActionFilters actionFilters;
46+
private IndicesService indicesService;
47+
private CircuitBreakerService circuitBreakerService;
48+
private CircuitBreaker circuitBreaker;
49+
private IndexNameExpressionResolver resolver;
50+
private TransportGetMappingsAction action;
51+
52+
@Captor
53+
private ArgumentCaptor<GetMappingsResponse> responseCaptor;
54+
55+
@Before
56+
public void setup() {
57+
MockitoAnnotations.openMocks(this);
58+
transportService = mock(TransportService.class);
59+
clusterService = mock(ClusterService.class);
60+
threadPool = mock(ThreadPool.class);
61+
actionFilters = mock(ActionFilters.class);
62+
indicesService = mock(IndicesService.class);
63+
circuitBreakerService = mock(CircuitBreakerService.class);
64+
circuitBreaker = mock(CircuitBreaker.class);
65+
resolver = mock(IndexNameExpressionResolver.class);
66+
67+
when(circuitBreakerService.getBreaker(CircuitBreaker.REQUEST)).thenReturn(circuitBreaker);
68+
// Allow all fields for filtering during findMappings()
69+
when(indicesService.getFieldFilter()).thenReturn(index -> field -> true);
70+
71+
action = new TransportGetMappingsAction(
72+
transportService,
73+
clusterService,
74+
threadPool,
75+
actionFilters,
76+
resolver,
77+
indicesService,
78+
circuitBreakerService
79+
);
80+
}
81+
82+
public void testChargesAndReleasesForTwoIndices() throws IOException {
83+
// Given two indices with mappings whose compressed sizes we control
84+
final String[] indices = new String[] { "i1", "i2" };
85+
86+
ClusterState state = mock(ClusterState.class);
87+
Metadata metadata = mock(Metadata.class);
88+
IndexMetadata imd1 = mock(IndexMetadata.class);
89+
IndexMetadata imd2 = mock(IndexMetadata.class);
90+
MappingMetadata mm1 = mock(MappingMetadata.class);
91+
MappingMetadata mm2 = mock(MappingMetadata.class);
92+
final CompressedXContent src1 = new CompressedXContent("{\"properties\":{\"f1\":{\"type\":\"keyword\"}}}");
93+
final CompressedXContent src2 = new CompressedXContent("{\"properties\":{\"f2\":{\"type\":\"text\"}}}");
94+
95+
// expected estimate is based on the actual compressed bytes
96+
final long expectedEstimate = RamUsageEstimator.sizeOf(src1.compressed()) + RamUsageEstimator.sizeOf(src2.compressed());
97+
98+
when(imd1.mapping()).thenReturn(mm1);
99+
when(imd2.mapping()).thenReturn(mm2);
100+
when(mm1.source()).thenReturn(src1);
101+
when(mm2.source()).thenReturn(src2);
102+
103+
when(state.metadata()).thenReturn(metadata);
104+
when(metadata.index("i1")).thenReturn(imd1);
105+
when(metadata.index("i2")).thenReturn(imd2);
106+
when(imd1.mapping()).thenReturn(mm1);
107+
when(imd2.mapping()).thenReturn(mm2);
108+
109+
// The transport action asks metadata().findMappings(...) to build the response body.
110+
Map<String, MappingMetadata> responseMap = Map.of("i1", mm1, "i2", mm2);
111+
when(metadata.findMappings(eq(indices), any())).thenReturn(responseMap);
112+
113+
@SuppressWarnings("unchecked")
114+
ActionListener<GetMappingsResponse> listener = mock(ActionListener.class);
115+
116+
action.doClusterManagerOperation(new GetMappingsRequest(), indices, state, listener);
117+
118+
// Then: charged with the sum estimate and released afterward
119+
verify(circuitBreaker, times(1)).addEstimateBytesAndMaybeBreak(eq(expectedEstimate), eq("get_mappings"));
120+
verify(circuitBreaker, times(1)).addWithoutBreaking(eq(-expectedEstimate));
121+
122+
// And the listener received the response carrying the same map
123+
verify(listener, times(1)).onResponse(responseCaptor.capture());
124+
GetMappingsResponse resp = responseCaptor.getValue();
125+
126+
assertEquals(responseMap, resp.mappings());
127+
verify(listener, never()).onFailure(any());
128+
}
129+
130+
public void testNoReleaseWhenEstimateIsZero() throws IOException {
131+
// One index with null mapping → estimate should be 0; still "charged" call happens with 0
132+
final String[] indices = new String[] { "i0" };
133+
134+
ClusterState state = mock(ClusterState.class);
135+
Metadata metadata = mock(Metadata.class);
136+
IndexMetadata imd0 = mock(IndexMetadata.class);
137+
138+
when(state.metadata()).thenReturn(metadata);
139+
when(metadata.index("i0")).thenReturn(imd0);
140+
when(imd0.mapping()).thenReturn(null); // no mapping -> contributes 0 bytes
141+
142+
when(metadata.findMappings(eq(indices), any())).thenReturn(Map.of());
143+
144+
@SuppressWarnings("unchecked")
145+
ActionListener<GetMappingsResponse> listener = mock(ActionListener.class);
146+
147+
action.doClusterManagerOperation(new GetMappingsRequest(), indices, state, listener);
148+
149+
// Called with 0
150+
verify(circuitBreaker, times(1)).addEstimateBytesAndMaybeBreak(eq(0L), eq("get_mappings"));
151+
// No release when estimate == 0 (guard in finally)
152+
verify(circuitBreaker, never()).addWithoutBreaking(anyLong());
153+
154+
verify(listener, times(1)).onResponse(any());
155+
verify(listener, never()).onFailure(any());
156+
}
157+
158+
public void testReleaseHappensWhenFindMappingsThrows() throws IOException {
159+
// If an exception occurs after the breaker is charged, we still release in finally
160+
final String[] indices = new String[] { "i1" };
161+
162+
ClusterState state = mock(ClusterState.class);
163+
Metadata metadata = mock(Metadata.class);
164+
IndexMetadata imd1 = mock(IndexMetadata.class);
165+
MappingMetadata mm1 = mock(MappingMetadata.class);
166+
final CompressedXContent src = new CompressedXContent("{\"properties\":{\"f\":{\"type\":\"integer\"}}}");
167+
final long expectedEstimate = RamUsageEstimator.sizeOf(src.compressed());
168+
169+
when(imd1.mapping()).thenReturn(mm1);
170+
when(mm1.source()).thenReturn(src);
171+
172+
// Simulate failure after breaker charge
173+
when(metadata.findMappings(eq(indices), any())).thenThrow(new RuntimeException("boom"));
174+
175+
when(state.metadata()).thenReturn(metadata);
176+
when(metadata.index("i1")).thenReturn(imd1);
177+
when(imd1.mapping()).thenReturn(mm1);
178+
179+
when(metadata.findMappings(eq(indices), any())).thenThrow(new RuntimeException("boom"));
180+
181+
@SuppressWarnings("unchecked")
182+
ActionListener<GetMappingsResponse> listener = mock(ActionListener.class);
183+
184+
action.doClusterManagerOperation(new GetMappingsRequest(), indices, state, listener);
185+
186+
verify(circuitBreaker, times(1)).addEstimateBytesAndMaybeBreak(eq(expectedEstimate), eq("get_mappings"));
187+
verify(circuitBreaker, times(1)).addWithoutBreaking(eq(-expectedEstimate));
188+
verify(listener, times(1)).onFailure(any(RuntimeException.class));
189+
verify(listener, never()).onResponse(any());
190+
}
191+
}

0 commit comments

Comments
 (0)