Skip to content

Commit bde422a

Browse files
author
Daan Hoogland
committed
tests
1 parent 5f8ca53 commit bde422a

1 file changed

Lines changed: 271 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.metrics;
18+
19+
import static org.junit.Assert.assertArrayEquals;
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
22+
import static org.junit.Assert.assertNull;
23+
import static org.junit.Assert.assertTrue;
24+
import static org.mockito.ArgumentMatchers.eq;
25+
import static org.mockito.ArgumentMatchers.isNull;
26+
import static org.mockito.Mockito.mock;
27+
import static org.mockito.Mockito.times;
28+
import static org.mockito.Mockito.verify;
29+
import static org.mockito.Mockito.when;
30+
31+
import java.io.ByteArrayOutputStream;
32+
import java.io.InputStream;
33+
import java.lang.reflect.Field;
34+
import java.net.ConnectException;
35+
import java.net.HttpURLConnection;
36+
import java.net.Socket;
37+
import java.net.URL;
38+
import java.nio.charset.StandardCharsets;
39+
import java.util.concurrent.ExecutorService;
40+
import java.util.concurrent.ThreadPoolExecutor;
41+
42+
import org.junit.After;
43+
import org.junit.Before;
44+
import org.junit.Test;
45+
46+
import com.sun.net.httpserver.HttpServer;
47+
48+
import org.apache.cloudstack.framework.config.ConfigKey;
49+
import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl;
50+
51+
public class PrometheusExporterServerImplTest {
52+
53+
private PrometheusExporterServerImpl server;
54+
private ConfigDepotImpl mockDepot;
55+
56+
@Before
57+
public void setUp() throws Exception {
58+
server = new PrometheusExporterServerImpl();
59+
mockDepot = mock(ConfigDepotImpl.class);
60+
setConfigDepot(mockDepot);
61+
// EnablePrometheusExporter is a non-dynamic ConfigKey, so its cached _value survives
62+
// across tests (and even across test classes sharing this JVM) unless cleared here:
63+
// isDynamic()==false means value() only re-reads the (mocked) depot while _value==null.
64+
resetConfigKeyValue(PrometheusExporterServer.EnablePrometheusExporter);
65+
setStaticField("httpServer", null);
66+
setInstanceField(server, "httpExecutor", null);
67+
}
68+
69+
@After
70+
public void tearDown() throws Exception {
71+
server.stop();
72+
setConfigDepot(null);
73+
resetConfigKeyValue(PrometheusExporterServer.EnablePrometheusExporter);
74+
setStaticField("httpServer", null);
75+
}
76+
77+
@Test
78+
public void testStartWhenDisabledDoesNotCreateServerOrExecutor() throws Exception {
79+
stubConfigValue(PrometheusExporterServer.EnablePrometheusExporter, "false");
80+
81+
boolean result = server.start();
82+
83+
assertTrue("start() should always return true", result);
84+
assertNull("httpServer should not be created when the exporter is disabled", getStaticField("httpServer"));
85+
assertNull("httpExecutor should not be created when the exporter is disabled", getInstanceField(server, "httpExecutor"));
86+
}
87+
88+
@Test
89+
public void testStopWhenNeverStartedDoesNotThrow() throws Exception {
90+
boolean result = server.stop();
91+
92+
assertTrue("stop() should return true even if the server was never started", result);
93+
}
94+
95+
@Test
96+
public void testStopShutsDownExecutorAndClosesServer() throws Exception {
97+
stubConfigValue(PrometheusExporterServer.EnablePrometheusExporter, "true");
98+
stubConfigValue(PrometheusExporterServer.PrometheusExporterServerPort, "0");
99+
stubConfigValue(PrometheusExporterServer.PrometheusExporterAllowedAddresses, "127.0.0.1");
100+
101+
assertTrue(server.start());
102+
103+
HttpServer startedHttpServer = (HttpServer) getStaticField("httpServer");
104+
ExecutorService startedExecutor = (ExecutorService) getInstanceField(server, "httpExecutor");
105+
int port = startedHttpServer.getAddress().getPort();
106+
107+
assertFalse("Executor should be alive right after start()", startedExecutor.isShutdown());
108+
109+
server.stop();
110+
111+
assertTrue("stop() should shut down the http executor", startedExecutor.isShutdown());
112+
assertNull("httpExecutor field should be cleared after stop()", getInstanceField(server, "httpExecutor"));
113+
114+
try {
115+
new Socket("127.0.0.1", port).close();
116+
org.junit.Assert.fail("Server socket should no longer accept connections after stop()");
117+
} catch (ConnectException expected) {
118+
// expected: the listening socket was closed by stop()
119+
}
120+
}
121+
122+
@Test
123+
public void testStartCreatesFixedThreadPoolOfTwoAndWiresItToTheServer() throws Exception {
124+
stubConfigValue(PrometheusExporterServer.EnablePrometheusExporter, "true");
125+
stubConfigValue(PrometheusExporterServer.PrometheusExporterServerPort, "0");
126+
stubConfigValue(PrometheusExporterServer.PrometheusExporterAllowedAddresses, "127.0.0.1");
127+
128+
assertTrue(server.start());
129+
130+
Object executor = getInstanceField(server, "httpExecutor");
131+
assertTrue("httpExecutor should be a ThreadPoolExecutor", executor instanceof ThreadPoolExecutor);
132+
assertEquals("httpExecutor should be a fixed pool of 2 threads", 2, ((ThreadPoolExecutor) executor).getMaximumPoolSize());
133+
}
134+
135+
@Test
136+
public void testAllowedRemoteAddressReceivesMetrics() throws Exception {
137+
PrometheusExporter mockExporter = mock(PrometheusExporter.class);
138+
when(mockExporter.getMetrics()).thenReturn("cloudstack_test_metric 1");
139+
setInstanceField(server, "prometheusExporter", mockExporter);
140+
141+
stubConfigValue(PrometheusExporterServer.EnablePrometheusExporter, "true");
142+
stubConfigValue(PrometheusExporterServer.PrometheusExporterServerPort, "0");
143+
stubConfigValue(PrometheusExporterServer.PrometheusExporterAllowedAddresses, "127.0.0.1");
144+
145+
assertTrue(server.start());
146+
int port = ((HttpServer) getStaticField("httpServer")).getAddress().getPort();
147+
148+
HttpURLConnection connection = (HttpURLConnection) new URL("http://127.0.0.1:" + port + "/metrics").openConnection();
149+
try {
150+
assertEquals(200, connection.getResponseCode());
151+
String body = readFully(connection.getInputStream());
152+
assertEquals("cloudstack_test_metric 1", body);
153+
} finally {
154+
connection.disconnect();
155+
}
156+
157+
verify(mockExporter, times(1)).updateMetrics();
158+
verify(mockExporter, times(1)).getMetrics();
159+
}
160+
161+
@Test
162+
public void testDisallowedRemoteAddressReceivesForbidden() throws Exception {
163+
PrometheusExporter mockExporter = mock(PrometheusExporter.class);
164+
setInstanceField(server, "prometheusExporter", mockExporter);
165+
166+
stubConfigValue(PrometheusExporterServer.EnablePrometheusExporter, "true");
167+
stubConfigValue(PrometheusExporterServer.PrometheusExporterServerPort, "0");
168+
stubConfigValue(PrometheusExporterServer.PrometheusExporterAllowedAddresses, "10.0.0.1");
169+
170+
assertTrue(server.start());
171+
int port = ((HttpServer) getStaticField("httpServer")).getAddress().getPort();
172+
173+
HttpURLConnection connection = (HttpURLConnection) new URL("http://127.0.0.1:" + port + "/metrics").openConnection();
174+
try {
175+
assertEquals(403, connection.getResponseCode());
176+
String body = readFully(connection.getErrorStream());
177+
assertEquals("Forbidden", body);
178+
} finally {
179+
connection.disconnect();
180+
}
181+
182+
verify(mockExporter, times(0)).updateMetrics();
183+
}
184+
185+
@Test
186+
public void testRootPathReturnsLandingPage() throws Exception {
187+
stubConfigValue(PrometheusExporterServer.EnablePrometheusExporter, "true");
188+
stubConfigValue(PrometheusExporterServer.PrometheusExporterServerPort, "0");
189+
stubConfigValue(PrometheusExporterServer.PrometheusExporterAllowedAddresses, "127.0.0.1");
190+
191+
assertTrue(server.start());
192+
int port = ((HttpServer) getStaticField("httpServer")).getAddress().getPort();
193+
194+
HttpURLConnection connection = (HttpURLConnection) new URL("http://127.0.0.1:" + port + "/").openConnection();
195+
try {
196+
assertEquals(200, connection.getResponseCode());
197+
String body = readFully(connection.getInputStream());
198+
assertTrue("Landing page should link to /metrics", body.contains("/metrics"));
199+
} finally {
200+
connection.disconnect();
201+
}
202+
}
203+
204+
@Test
205+
public void testGetConfigComponentName() {
206+
assertEquals("PrometheusExporter", server.getConfigComponentName());
207+
}
208+
209+
@Test
210+
public void testGetConfigKeysIncludesMinRefreshIntervalAddedByTheScrapeThrottlingFix() {
211+
ConfigKey<?>[] keys = server.getConfigKeys();
212+
213+
assertArrayEquals(new ConfigKey<?>[]{
214+
PrometheusExporterServer.EnablePrometheusExporter,
215+
PrometheusExporterServer.PrometheusExporterServerPort,
216+
PrometheusExporterServer.PrometheusExporterAllowedAddresses,
217+
PrometheusExporterServer.PrometheusExporterOfferingCountLimit,
218+
PrometheusExporterServer.PrometheusExporterMinRefreshInterval
219+
}, keys);
220+
}
221+
222+
private static String readFully(InputStream inputStream) throws Exception {
223+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
224+
byte[] chunk = new byte[1024];
225+
int bytesRead;
226+
while ((bytesRead = inputStream.read(chunk)) != -1) {
227+
buffer.write(chunk, 0, bytesRead);
228+
}
229+
return buffer.toString(StandardCharsets.UTF_8.name());
230+
}
231+
232+
private void stubConfigValue(ConfigKey<?> configKey, String value) {
233+
when(mockDepot.getConfigStringValue(eq(configKey.key()), eq(ConfigKey.Scope.Global), isNull())).thenReturn(value);
234+
}
235+
236+
private static void setConfigDepot(ConfigDepotImpl depot) throws Exception {
237+
Field field = ConfigKey.class.getDeclaredField("s_depot");
238+
field.setAccessible(true);
239+
field.set(null, depot);
240+
}
241+
242+
private static void resetConfigKeyValue(ConfigKey<?> configKey) throws Exception {
243+
Field field = ConfigKey.class.getDeclaredField("_value");
244+
field.setAccessible(true);
245+
field.set(configKey, null);
246+
}
247+
248+
private static void setStaticField(String fieldName, Object value) throws Exception {
249+
Field field = PrometheusExporterServerImpl.class.getDeclaredField(fieldName);
250+
field.setAccessible(true);
251+
field.set(null, value);
252+
}
253+
254+
private static Object getStaticField(String fieldName) throws Exception {
255+
Field field = PrometheusExporterServerImpl.class.getDeclaredField(fieldName);
256+
field.setAccessible(true);
257+
return field.get(null);
258+
}
259+
260+
private static void setInstanceField(Object target, String fieldName, Object value) throws Exception {
261+
Field field = PrometheusExporterServerImpl.class.getDeclaredField(fieldName);
262+
field.setAccessible(true);
263+
field.set(target, value);
264+
}
265+
266+
private static Object getInstanceField(Object target, String fieldName) throws Exception {
267+
Field field = PrometheusExporterServerImpl.class.getDeclaredField(fieldName);
268+
field.setAccessible(true);
269+
return field.get(target);
270+
}
271+
}

0 commit comments

Comments
 (0)