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.assertNotNull;
022import static org.junit.jupiter.api.Assertions.assertNull;
023
024import java.lang.reflect.Method;
025import java.nio.file.Files;
026import java.nio.file.Path;
027import org.apache.hadoop.hbase.testclassification.MiscTests;
028import org.apache.hadoop.hbase.testclassification.SmallTests;
029import org.junit.jupiter.api.Tag;
030import org.junit.jupiter.api.Test;
031import org.junit.jupiter.api.io.TempDir;
032
033/**
034 * Verifies {@link ProfilerBackend#detect} fallback behaviour when
035 * {@code one.profiler.AsyncProfiler} is absent from the classpath.
036 * <p>
037 * Each test loads {@code ProfilerBackend} through a custom {@link ClassLoader} that blocks
038 * {@code one.profiler.*}, simulating a deployment where the async-profiler JAR was never packaged.
039 * This is the exact scenario for users who have async-profiler installed as a native binary
040 * ({@code ASYNC_PROFILER_HOME}) but are not allowed to bundle the JAR.
041 * <p>
042 * The split of {@link LibraryBackend} into its own file is what makes this possible:
043 * {@code ProfilerBackend.class} carries no static reference to {@code AsyncProfiler}, so the
044 * isolated loader can load it without a {@code NoClassDefFoundError}.
045 */
046@Tag(MiscTests.TAG)
047@Tag(SmallTests.TAG)
048public class TestProfilerBackendIsolated {
049
050  @TempDir
051  Path tempDir;
052
053  /**
054   * When the library is absent AND no home is set, detect() must return null so that HttpServer
055   * registers DisabledServlet instead of crashing.
056   */
057  @Test
058  public void testDetectReturnsNullWhenLibraryAbsentAndNoHome() throws Exception {
059    ClassLoader isolated = isolatedLoader();
060    Method detect = detectMethod(isolated);
061
062    assertNull(detect.invoke(null, (String) null));
063    assertNull(detect.invoke(null, ""));
064    assertNull(detect.invoke(null, "   "));
065  }
066
067  /**
068   * User has async-profiler installed as a native binary (ASYNC_PROFILER_HOME set, bin/asprof
069   * present) but no JAR on the classpath. detect() must return BinaryBackend.
070   */
071  @Test
072  public void testDetectReturnsBinaryBackendWhenLibraryAbsentButHomeSet() throws Exception {
073    // Create a minimal fake profiler home with bin/asprof
074    Files.createDirectories(tempDir.resolve("bin"));
075    Files.createFile(tempDir.resolve("bin").resolve("asprof"));
076
077    ClassLoader isolated = isolatedLoader();
078    Method detect = detectMethod(isolated);
079
080    Object backend = detect.invoke(null, tempDir.toString());
081    assertNotNull(backend);
082    assertEquals("BinaryBackend", backend.getClass().getSimpleName());
083  }
084
085  /**
086   * When the library IS on the classpath (normal test classpath), detect() must return
087   * LibraryBackend regardless of whether a home is set — library takes priority.
088   */
089  @Test
090  public void testDetectReturnsLibraryBackendWhenLibraryPresent() {
091    // Use real classpath — async-profiler JAR is present as optional compile dep in tests
092    ProfilerBackend backend = ProfilerBackend.detect(null);
093    assertNotNull(backend);
094    assertEquals("LibraryBackend", backend.getClass().getSimpleName());
095  }
096
097  /**
098   * Library present AND home set — LibraryBackend must still win (priority check).
099   */
100  @Test
101  public void testDetectPrefersLibraryWhenBothPresent() throws Exception {
102    Files.createDirectories(tempDir.resolve("bin"));
103    Files.createFile(tempDir.resolve("bin").resolve("asprof"));
104
105    ProfilerBackend backend = ProfilerBackend.detect(tempDir.toString());
106    assertNotNull(backend);
107    assertEquals("LibraryBackend", backend.getClass().getSimpleName());
108  }
109
110  // ---- helpers ----
111
112  /**
113   * Returns a ClassLoader that: - blocks {@code one.profiler.*} entirely (simulates absent
114   * async-profiler JAR) - reloads {@code org.apache.hadoop.hbase.http.*} classes fresh (so
115   * LibraryBackend resolves its own imports through this loader and also sees one.profiler.* as
116   * absent) - delegates everything else to the parent
117   */
118  private ClassLoader isolatedLoader() {
119    ClassLoader parent = getClass().getClassLoader();
120    return new ClassLoader(parent) {
121      @Override
122      protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
123        if (name.startsWith("one.profiler.")) {
124          throw new ClassNotFoundException("Simulated absent library: " + name);
125        }
126        // Force fresh load of our http package so LibraryBackend uses this loader
127        // (and therefore also sees one.profiler.* as absent when it tries to resolve it)
128        if (name.startsWith("org.apache.hadoop.hbase.http.")) {
129          Class<?> c = findLoadedClass(name);
130          if (c != null) {
131            return c;
132          }
133          // Load bytes from parent, define in this loader
134          String path = name.replace('.', '/') + ".class";
135          try (java.io.InputStream in = parent.getResourceAsStream(path)) {
136            if (in != null) {
137              byte[] bytes = in.readAllBytes();
138              c = defineClass(name, bytes, 0, bytes.length);
139              if (resolve) {
140                resolveClass(c);
141              }
142              return c;
143            }
144          } catch (java.io.IOException e) {
145            throw new ClassNotFoundException(name, e);
146          }
147        }
148        return super.loadClass(name, resolve);
149      }
150    };
151  }
152
153  /**
154   * Loads {@code ProfilerBackend} through the given loader and returns its {@code detect(String)}
155   * method, made accessible across loader boundaries.
156   */
157  private Method detectMethod(ClassLoader loader) throws Exception {
158    Class<?> backendClass = loader.loadClass("org.apache.hadoop.hbase.http.ProfilerBackend");
159    Method m = backendClass.getMethod("detect", String.class);
160    m.setAccessible(true);
161    return m;
162  }
163}