001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.http;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertNotNull;
023import static org.junit.jupiter.api.Assertions.assertNull;
024import static org.junit.jupiter.api.Assertions.assertTrue;
025
026import java.io.File;
027import java.io.PrintWriter;
028import java.io.StringWriter;
029import java.lang.reflect.Field;
030import java.nio.charset.StandardCharsets;
031import java.nio.file.Files;
032import java.time.Instant;
033import java.util.Collections;
034import java.util.HashMap;
035import java.util.Map;
036import java.util.concurrent.CountDownLatch;
037import javax.servlet.ServletConfig;
038import javax.servlet.ServletContext;
039import javax.servlet.http.HttpServletRequest;
040import javax.servlet.http.HttpServletResponse;
041import org.apache.hadoop.conf.Configuration;
042import org.apache.hadoop.hbase.testclassification.MiscTests;
043import org.apache.hadoop.hbase.testclassification.SmallTests;
044import org.junit.jupiter.api.AfterEach;
045import org.junit.jupiter.api.BeforeEach;
046import org.junit.jupiter.api.Tag;
047import org.junit.jupiter.api.Test;
048import org.mockito.ArgumentCaptor;
049import org.mockito.Mockito;
050
051@Tag(MiscTests.TAG)
052@Tag(SmallTests.TAG)
053public class TestProfileServlet {
054
055  @BeforeEach
056  public void resetStaticStateBeforeEach() throws Exception {
057    clearLastResult();
058    resetProfiling();
059  }
060
061  @AfterEach
062  public void joinStopperThreadAfterEach() throws Exception {
063    // T1: stopper threads left running by a test can write lastResult after the test body
064    // returns, overwriting the @BeforeEach clear of the next test. Join any live stopper so
065    // the next test starts with fully settled static state.
066    for (Thread t : Thread.getAllStackTraces().keySet()) {
067      if ("ProfileServlet-stopper".equals(t.getName()) && t.isAlive()) {
068        t.join(5000);
069      }
070    }
071  }
072
073  // ---- parseProfileRequest ----
074
075  @Test
076  public void testParseProfileRequestDefaults() {
077    ProfileServlet servlet = new ProfileServlet(null);
078    HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", null,
079      "output", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null,
080      "width", null, "height", null, "minwidth", null, "refreshDelay", null);
081
082    ProfileServlet.ProfileRequest parsed = servlet.parseProfileRequest(req);
083    assertNull(parsed.getPid());
084    assertEquals(10, parsed.getDuration());
085    assertEquals(ProfileServlet.Event.CPU, parsed.getEvent());
086    assertEquals(ProfileServlet.Output.HTML, parsed.getOutput());
087    assertFalse(parsed.isThread());
088    assertFalse(parsed.isSimple());
089    assertFalse(parsed.isReverse());
090  }
091
092  @Test
093  public void testParseProfileRequestAllOptions() {
094    Map<String, String[]> flags = new HashMap<>();
095    flags.put("thread", new String[] { "" });
096    flags.put("simple", new String[] { "" });
097    flags.put("reverse", new String[] { "" });
098
099    ProfileServlet servlet = new ProfileServlet(null);
100    HttpServletRequest req = mockRequest(flags, "pid", "42", "duration", "60", "output", "tree",
101      "event", "alloc", "interval", "1000", "jstackdepth", "256", "bufsize", "100000", "width",
102      "1200", "height", "16", "minwidth", "0.5", "refreshDelay", "3");
103
104    ProfileServlet.ProfileRequest parsed = servlet.parseProfileRequest(req);
105    assertEquals(42, parsed.getPid());
106    assertEquals(60, parsed.getDuration());
107    assertEquals(ProfileServlet.Output.TREE, parsed.getOutput());
108    assertEquals(ProfileServlet.Event.ALLOC, parsed.getEvent());
109    assertEquals(1000L, parsed.getInterval());
110    assertEquals(256, parsed.getJstackDepth());
111    assertEquals(100000L, parsed.getBufsize());
112    assertEquals(1200, parsed.getWidth());
113    assertEquals(16, parsed.getHeight());
114    assertEquals(0.5, parsed.getMinwidth(), 1e-9);
115    assertEquals(3, parsed.getRefreshDelay());
116    assertTrue(parsed.isThread());
117    assertTrue(parsed.isSimple());
118    assertTrue(parsed.isReverse());
119  }
120
121  // ---- doGet ----
122
123  @Test
124  public void testDoGetSetsRefreshHeaderAndCallsBackend() throws Exception {
125    ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class);
126    Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())).thenReturn("OK");
127
128    ProfileServlet servlet = new ProfileServlet(mockBackend);
129    servlet.init(mockServletConfig());
130
131    HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1",
132      "refreshDelay", "2", "output", null, "event", null, "interval", null, "jstackdepth", null,
133      "bufsize", null, "width", null, "height", null, "minwidth", null);
134
135    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
136    StringWriter body = new StringWriter();
137    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body));
138
139    servlet.doGet(req, resp);
140
141    Mockito.verify(mockBackend).executeStart(Mockito.any(), Mockito.any());
142    Mockito.verify(resp).setStatus(HttpServletResponse.SC_ACCEPTED);
143
144    ArgumentCaptor<String> refreshCaptor = ArgumentCaptor.forClass(String.class);
145    Mockito.verify(resp).setHeader(Mockito.eq("Refresh"), refreshCaptor.capture());
146    assertTrue(refreshCaptor.getValue().startsWith("3;"));
147    assertTrue(refreshCaptor.getValue().contains("/prof-output-hbase/"));
148  }
149
150  // ---- doGet error paths ----
151
152  @Test
153  public void testDoGetBackendThrowsRuntimeException() throws Exception {
154    ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class);
155    Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any()))
156      .thenThrow(new IllegalStateException("profiler already started"));
157
158    ProfileServlet servlet = new ProfileServlet(mockBackend);
159    servlet.init(mockServletConfig());
160
161    HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1",
162      "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null,
163      "bufsize", null, "width", null, "height", null, "minwidth", null);
164    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
165    StringWriter body = new StringWriter();
166    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body));
167
168    servlet.doGet(req, resp);
169
170    Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
171    assertTrue(body.toString().contains("Profiler error"));
172  }
173
174  @Test
175  public void testDoGetBackendThrowsError() throws Exception {
176    ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class);
177    Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any()))
178      .thenThrow(new UnsatisfiedLinkError("no libasyncProfiler in java.library.path"));
179
180    ProfileServlet servlet = new ProfileServlet(mockBackend);
181    servlet.init(mockServletConfig());
182
183    HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1",
184      "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null,
185      "bufsize", null, "width", null, "height", null, "minwidth", null);
186    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
187    StringWriter body = new StringWriter();
188    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body));
189
190    servlet.doGet(req, resp);
191
192    Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
193    assertTrue(body.toString().contains("Profiler error"));
194  }
195
196  @Test
197  public void testDoGetRejectsNonPositivePidWith400() throws Exception {
198    ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class);
199    ProfileServlet servlet = new ProfileServlet(mockBackend);
200    servlet.init(mockServletConfig());
201
202    for (String badPid : new String[] { "-1", "0", "-999" }) {
203      HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", badPid, "duration", "1",
204        "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null,
205        "bufsize", null, "width", null, "height", null, "minwidth", null);
206      HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
207      StringWriter body = new StringWriter();
208      Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body));
209
210      servlet.doGet(req, resp);
211
212      Mockito.verify(resp).setStatus(HttpServletResponse.SC_BAD_REQUEST);
213      assertTrue(body.toString().contains("Invalid pid"),
214        "Expected 'Invalid pid' in response for pid=" + badPid + ", got: " + body);
215      // backend must never be called for an invalid pid
216      Mockito.verify(mockBackend, Mockito.never()).executeStart(Mockito.any(), Mockito.any());
217
218      // reset mocks for next iteration
219      Mockito.reset(resp, mockBackend);
220    }
221  }
222
223  @Test
224  public void testDoGetSecondRequestRejectedWithConflictWhenProfilingActive() throws Exception {
225    // T2: without synchronization the stopper's 1s sleep could finish before the second doGet
226    // runs on a slow CI runner, producing 202 instead of 409. Block executeStop on a latch so
227    // profiling=true is guaranteed to be set when the second request arrives.
228    CountDownLatch secondRequestIssued = new CountDownLatch(1);
229    ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class);
230    Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())).thenReturn("OK");
231    Mockito.when(mockBackend.executeStop(Mockito.any(), Mockito.any())).thenAnswer(inv -> {
232      secondRequestIssued.await();
233      return "";
234    });
235
236    ProfileServlet servlet = new ProfileServlet(mockBackend);
237    servlet.init(mockServletConfig());
238
239    HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1",
240      "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null,
241      "bufsize", null, "width", null, "height", null, "minwidth", null);
242
243    // First request succeeds and sets profiling=true.
244    HttpServletResponse resp1 = Mockito.mock(HttpServletResponse.class);
245    Mockito.when(resp1.getWriter()).thenReturn(new PrintWriter(new StringWriter()));
246    servlet.doGet(req, resp1);
247    Mockito.verify(resp1).setStatus(HttpServletResponse.SC_ACCEPTED);
248
249    // Second request must see profiling=true under the lock and return 409 CONFLICT.
250    HttpServletResponse resp2 = Mockito.mock(HttpServletResponse.class);
251    StringWriter body2 = new StringWriter();
252    Mockito.when(resp2.getWriter()).thenReturn(new PrintWriter(body2));
253    servlet.doGet(req, resp2);
254    secondRequestIssued.countDown();
255
256    Mockito.verify(resp2).setStatus(HttpServletResponse.SC_CONFLICT);
257    assertTrue(body2.toString().contains("already running"));
258  }
259
260  // ---- doGet error paths — orphan file cleanup ----
261
262  @Test
263  public void testDoGetDeletesOrphanFileWhenExecuteStartThrows() throws Exception {
264    ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class);
265    ArgumentCaptor<File> fileCaptor = ArgumentCaptor.forClass(File.class);
266    Mockito.when(mockBackend.executeStart(Mockito.any(), fileCaptor.capture()))
267      .thenThrow(new IllegalStateException("profiler already started"));
268
269    ProfileServlet servlet = new ProfileServlet(mockBackend);
270    servlet.init(mockServletConfig());
271
272    HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1",
273      "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null,
274      "bufsize", null, "width", null, "height", null, "minwidth", null);
275    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
276    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter()));
277
278    servlet.doGet(req, resp);
279
280    Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
281    // The placeholder created for this specific request must have been deleted.
282    File created = fileCaptor.getValue();
283    assertFalse(created.exists(),
284      "Orphan placeholder must be deleted when executeStart fails: " + created.getName());
285  }
286
287  @Test
288  public void testStopperThreadWritesPaddedErrorOnExecuteStopFailure() throws Exception {
289    ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class);
290    Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())).thenReturn("OK");
291    Mockito.when(mockBackend.executeStop(Mockito.any(), Mockito.any()))
292      .thenThrow(new IllegalStateException("stop failed"));
293
294    ProfileServlet servlet = new ProfileServlet(mockBackend);
295    servlet.init(mockServletConfig());
296
297    HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "1",
298      "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null,
299      "bufsize", null, "width", null, "height", null, "minwidth", null);
300    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
301    ArgumentCaptor<String> refreshCaptor = ArgumentCaptor.forClass(String.class);
302    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter()));
303
304    servlet.doGet(req, resp);
305    Mockito.verify(resp).setHeader(Mockito.eq("Refresh"), refreshCaptor.capture());
306
307    // Extract the output file path from the Refresh header URL
308    String refreshValue = refreshCaptor.getValue();
309    // Refresh header: "1;/prof-output-hbase/<filename>"
310    String relUrl = refreshValue.substring(refreshValue.indexOf(';') + 1);
311    String fileName = relUrl.substring(relUrl.lastIndexOf('/') + 1);
312    File outputFile = new File(ProfileServlet.OUTPUT_DIR, fileName);
313
314    // Wait for the stopper thread to finish (duration=1s + some buffer)
315    long deadline = System.currentTimeMillis() + 5000;
316    while (
317      outputFile.length() < ProfileServlet.PROF_OUTPUT_MIN_BYTES
318        && System.currentTimeMillis() < deadline
319    ) {
320      Thread.sleep(50);
321    }
322
323    byte[] content = Files.readAllBytes(outputFile.toPath());
324    assertTrue(content.length > ProfileServlet.PROF_OUTPUT_MIN_BYTES,
325      "Stopper must pad error file to > PROF_OUTPUT_MIN_BYTES so ProfileOutputServlet stops polling");
326    assertTrue(new String(content, StandardCharsets.UTF_8).contains("stop failed"),
327      "Padded error file must contain the failure message");
328  }
329
330  @Test
331  public void testStopperThreadWritesPaddedErrorOnInterrupt() throws Exception {
332    ProfilerBackend mockBackend = Mockito.mock(ProfilerBackend.class);
333    Mockito.when(mockBackend.executeStart(Mockito.any(), Mockito.any())).thenReturn("OK");
334
335    // Use a long duration so the stopper is sleeping when we interrupt it
336    ProfileServlet servlet = new ProfileServlet(mockBackend);
337    servlet.init(mockServletConfig());
338
339    HttpServletRequest req = mockRequest(Collections.emptyMap(), "pid", null, "duration", "60",
340      "refreshDelay", null, "output", null, "event", null, "interval", null, "jstackdepth", null,
341      "bufsize", null, "width", null, "height", null, "minwidth", null);
342    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
343    ArgumentCaptor<String> refreshCaptor = ArgumentCaptor.forClass(String.class);
344    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter()));
345
346    servlet.doGet(req, resp);
347    Mockito.verify(resp).setHeader(Mockito.eq("Refresh"), refreshCaptor.capture());
348
349    String relUrl = refreshCaptor.getValue().substring(refreshCaptor.getValue().indexOf(';') + 1);
350    String fileName = relUrl.substring(relUrl.lastIndexOf('/') + 1);
351    File outputFile = new File(ProfileServlet.OUTPUT_DIR, fileName);
352
353    // Find the stopper thread by name and interrupt it while it is sleeping
354    Thread stopper = null;
355    long findDeadline = System.currentTimeMillis() + 2000;
356    while (stopper == null && System.currentTimeMillis() < findDeadline) {
357      for (Thread t : Thread.getAllStackTraces().keySet()) {
358        if ("ProfileServlet-stopper".equals(t.getName()) && t.isAlive()) {
359          stopper = t;
360          break;
361        }
362      }
363      if (stopper == null) {
364        Thread.sleep(10);
365      }
366    }
367    assertNotNull(stopper, "ProfileServlet-stopper thread must be alive during the sleep");
368    stopper.interrupt();
369
370    // Wait for the stopper to write the interrupted-session message
371    long deadline = System.currentTimeMillis() + 3000;
372    while (
373      outputFile.length() < ProfileServlet.PROF_OUTPUT_MIN_BYTES
374        && System.currentTimeMillis() < deadline
375    ) {
376      Thread.sleep(50);
377    }
378
379    byte[] content = Files.readAllBytes(outputFile.toPath());
380    assertTrue(content.length > ProfileServlet.PROF_OUTPUT_MIN_BYTES,
381      "Stopper must pad interrupted-session file to > PROF_OUTPUT_MIN_BYTES");
382    assertTrue(new String(content, StandardCharsets.UTF_8).contains("interrupted"),
383      "Padded error file must contain 'interrupted'");
384  }
385
386  // ---- ?last ----
387
388  @Test
389  public void testLastReturns404WhenNoResultCached() throws Exception {
390    clearLastResult();
391
392    ProfileServlet servlet = new ProfileServlet(null);
393    servlet.init(mockServletConfig());
394    Map<String, String[]> params = new HashMap<>();
395    params.put("last", new String[] { "" });
396    HttpServletRequest req = mockRequest(params, "pid", null, "duration", null, "output", null,
397      "event", null, "interval", null, "jstackdepth", null, "bufsize", null, "width", null,
398      "height", null, "minwidth", null, "refreshDelay", null);
399    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
400    StringWriter body = new StringWriter();
401    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body));
402
403    servlet.doGet(req, resp);
404
405    Mockito.verify(resp).setStatus(HttpServletResponse.SC_NOT_FOUND);
406    assertTrue(body.toString().contains("No profiling results available yet"));
407  }
408
409  @Test
410  public void testLastRedirectsToMostRecentResult() throws Exception {
411    // C8 fix: ?last checks Files.exists before redirecting, so the output file must exist on disk.
412    String fileName = "profile-cpu-20260612-120000.html";
413    File outputDir = new File(ProfileServlet.OUTPUT_DIR);
414    outputDir.mkdirs();
415    File outputFile = new File(outputDir, fileName);
416    outputFile.createNewFile();
417    String expectedUrl = "/prof-output-hbase/" + fileName;
418    setLastResult(new ProfileServlet.ProfileResult(expectedUrl, "cpu", 10, Instant.now()));
419
420    ProfileServlet servlet = new ProfileServlet(null);
421    servlet.init(mockServletConfig());
422    Map<String, String[]> params = new HashMap<>();
423    params.put("last", new String[] { "" });
424    HttpServletRequest req = mockRequest(params, "pid", null, "duration", null, "output", null,
425      "event", null, "interval", null, "jstackdepth", null, "bufsize", null, "width", null,
426      "height", null, "minwidth", null, "refreshDelay", null);
427    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
428    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter()));
429
430    servlet.doGet(req, resp);
431
432    Mockito.verify(resp).sendRedirect(expectedUrl);
433  }
434
435  @Test
436  public void testLastResultOverwrittenByNewSession() throws Exception {
437    // C8 fix: ?last checks Files.exists before redirecting, so output files must exist on disk.
438    File outputDir = new File(ProfileServlet.OUTPUT_DIR);
439    outputDir.mkdirs();
440    File firstFile = new File(outputDir, "profile-cpu-first.html");
441    firstFile.createNewFile();
442    File secondFile = new File(outputDir, "profile-cpu-second.html");
443    secondFile.createNewFile();
444    String firstUrl = "/prof-output-hbase/profile-cpu-first.html";
445    String secondUrl = "/prof-output-hbase/profile-cpu-second.html";
446    setLastResult(new ProfileServlet.ProfileResult(firstUrl, "cpu", 10, Instant.now()));
447
448    // Overwrite with a second result — only the latest is kept
449    setLastResult(new ProfileServlet.ProfileResult(secondUrl, "cpu", 30, Instant.now()));
450
451    ProfileServlet servlet = new ProfileServlet(null);
452    servlet.init(mockServletConfig());
453    Map<String, String[]> params = new HashMap<>();
454    params.put("last", new String[] { "" });
455    HttpServletRequest req = mockRequest(params, "pid", null, "duration", null, "output", null,
456      "event", null, "interval", null, "jstackdepth", null, "bufsize", null, "width", null,
457      "height", null, "minwidth", null, "refreshDelay", null);
458    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
459    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(new StringWriter()));
460
461    servlet.doGet(req, resp);
462
463    // Only the most recent result is cached — redirect must point to secondUrl, not firstUrl
464    Mockito.verify(resp).sendRedirect(secondUrl);
465    Mockito.verify(resp, Mockito.never()).sendRedirect(firstUrl);
466  }
467
468  // ---- parseProfileRequest — enum fallbacks ----
469
470  @Test
471  public void testGetOutputFallsBackToHtmlOnUnknownValue() {
472    ProfileServlet servlet = new ProfileServlet(null);
473    HttpServletRequest req = mockRequest(Collections.emptyMap(), "output", "bogusformat", "pid",
474      null, "duration", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null,
475      "width", null, "height", null, "minwidth", null, "refreshDelay", null);
476    assertEquals(ProfileServlet.Output.HTML, servlet.parseProfileRequest(req).getOutput());
477  }
478
479  @Test
480  public void testGetEventFallsBackToCpuOnUnknownValue() {
481    ProfileServlet servlet = new ProfileServlet(null);
482    HttpServletRequest req = mockRequest(Collections.emptyMap(), "event", "bogusevent", "pid", null,
483      "duration", null, "output", null, "interval", null, "jstackdepth", null, "bufsize", null,
484      "width", null, "height", null, "minwidth", null, "refreshDelay", null);
485    assertEquals(ProfileServlet.Event.CPU, servlet.parseProfileRequest(req).getEvent());
486  }
487
488  @Test
489  public void testDurationClampedToMinOne() {
490    ProfileServlet servlet = new ProfileServlet(null);
491    HttpServletRequest req = mockRequest(Collections.emptyMap(), "duration", "0", "pid", null,
492      "output", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null,
493      "width", null, "height", null, "minwidth", null, "refreshDelay", null);
494    assertEquals(1, servlet.parseProfileRequest(req).getDuration());
495
496    HttpServletRequest negReq = mockRequest(Collections.emptyMap(), "duration", "-5", "pid", null,
497      "output", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null,
498      "width", null, "height", null, "minwidth", null, "refreshDelay", null);
499    assertEquals(1, servlet.parseProfileRequest(negReq).getDuration());
500  }
501
502  @Test
503  public void testDurationCappedAtMax() {
504    ProfileServlet servlet = new ProfileServlet(null);
505    HttpServletRequest req = mockRequest(Collections.emptyMap(), "duration", "999999", "pid", null,
506      "output", null, "event", null, "interval", null, "jstackdepth", null, "bufsize", null,
507      "width", null, "height", null, "minwidth", null, "refreshDelay", null);
508    assertEquals(ProfileServlet.MAX_DURATION_SECONDS,
509      servlet.parseProfileRequest(req).getDuration());
510  }
511
512  // ---- isAvailable / getAsyncProfilerHome ----
513
514  @Test
515  public void testIsAvailableDetectReturnsBackendWhenLibraryPresent() {
516    // async-profiler is on the test classpath (compile-time optional dep present in tests),
517    // so detect() returns LibraryBackend even with null home.
518    assertNotNull(ProfilerBackend.detect(null));
519  }
520
521  @Test
522  public void testGetAsyncProfilerHomeSystemProperty() {
523    String key = "async.profiler.home";
524    String prev = System.getProperty(key);
525    try {
526      System.setProperty(key, "/tmp/fake-profiler");
527      assertEquals("/tmp/fake-profiler", ProfileServlet.getAsyncProfilerHome());
528    } finally {
529      if (prev == null) {
530        System.clearProperty(key);
531      } else {
532        System.setProperty(key, prev);
533      }
534    }
535  }
536
537  // ---- DisabledServlet ----
538
539  @Test
540  public void testDisabledServletReturns500WithDefaultReason() throws Exception {
541    ProfileServlet.DisabledServlet disabled = new ProfileServlet.DisabledServlet();
542    disabled.init(mockServletConfig());
543    HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
544    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
545    StringWriter body = new StringWriter();
546    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body));
547
548    disabled.doGet(req, resp);
549
550    Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
551    // No reason param set — falls back to the default message
552    assertTrue(body.toString().contains("disabled at startup"));
553  }
554
555  @Test
556  public void testDisabledServletReturns500WithCustomReason() throws Exception {
557    ProfileServlet.DisabledServlet disabled = new ProfileServlet.DisabledServlet();
558    ServletConfig config = Mockito.mock(ServletConfig.class);
559    ServletContext ctx = Mockito.mock(ServletContext.class);
560    Mockito.when(config.getServletContext()).thenReturn(ctx);
561    Mockito.when(config.getInitParameter(ProfileServlet.DisabledServlet.REASON_PARAM))
562      .thenReturn("disabled via hbase.profiler.enabled=false");
563    disabled.init(config);
564
565    HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
566    HttpServletResponse resp = Mockito.mock(HttpServletResponse.class);
567    StringWriter body = new StringWriter();
568    Mockito.when(resp.getWriter()).thenReturn(new PrintWriter(body));
569
570    disabled.doGet(req, resp);
571
572    Mockito.verify(resp).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
573    assertTrue(body.toString().contains("hbase.profiler.enabled=false"));
574  }
575
576  // ---- helpers ----
577
578  private HttpServletRequest mockRequest(Map<String, String[]> paramMap, String... kvPairs) {
579    HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
580    Mockito.when(req.getParameterMap()).thenReturn(paramMap);
581    for (int i = 0; i < kvPairs.length; i += 2) {
582      Mockito.when(req.getParameter(kvPairs[i])).thenReturn(kvPairs[i + 1]);
583    }
584    return req;
585  }
586
587  private ServletConfig mockServletConfig() throws Exception {
588    ServletContext ctx = Mockito.mock(ServletContext.class);
589    Mockito.when(ctx.getAttribute(HttpServer.CONF_CONTEXT_ATTRIBUTE))
590      .thenReturn(new Configuration(false));
591    ServletConfig config = Mockito.mock(ServletConfig.class);
592    Mockito.when(config.getServletContext()).thenReturn(ctx);
593    return config;
594  }
595
596  private static Field lastResultField() throws Exception {
597    Field f = ProfileServlet.class.getDeclaredField("lastResult");
598    f.setAccessible(true);
599    return f;
600  }
601
602  private static void setLastResult(ProfileServlet.ProfileResult result) throws Exception {
603    lastResultField().set(null, result);
604  }
605
606  private static void clearLastResult() throws Exception {
607    lastResultField().set(null, null);
608  }
609
610  private static void resetProfiling() throws Exception {
611    Field f = ProfileServlet.class.getDeclaredField("profiling");
612    f.setAccessible(true);
613    f.set(null, false);
614  }
615}