View Javadoc
1   /*
2    * Copyright 2025 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.util.test;
18  
19  import io.netty.util.LeakPresenceDetector;
20  import org.junit.jupiter.api.extension.AfterAllCallback;
21  import org.junit.jupiter.api.extension.AfterEachCallback;
22  import org.junit.jupiter.api.extension.BeforeAllCallback;
23  import org.junit.jupiter.api.extension.BeforeEachCallback;
24  import org.junit.jupiter.api.extension.ExtensionContext;
25  
26  import java.util.concurrent.TimeUnit;
27  
28  /**
29   * Junit 5 extension for leak detection using {@link LeakPresenceDetector}.
30   * <p>
31   * Leak presence is checked at the class level. Any resource must be closed at the end of the test class, by the time
32   * {@link org.junit.jupiter.api.AfterAll} has been called. Method-level detection is not possible because some tests
33   * retain resources between methods on the same class, notably parameterized tests that allocate different buffers
34   * before running the test methods with those buffers.
35   * <p>
36   * This extension supports parallel test execution, but has to make some assumptions about the thread lifecycle. The
37   * resource scope for the class is created in {@link org.junit.jupiter.api.BeforeAll}, and then saved in a thread local
38   * on each {@link org.junit.jupiter.api.BeforeEach}. This appears to work well with junit's default parallelism,
39   * despite the use of a fork-join pool that can transfer tasks between threads, but it may lead to problems if tests
40   * make use of fork-join machinery themselves.
41   * <p>
42   * The ThreadLocal holding the scope is {@link InheritableThreadLocal inheritable}, so that e.g. event loops created
43   * in a test are assigned to the test resource scope.
44   */
45  public final class LeakPresenceExtension
46          implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, AfterAllCallback {
47  
48      private static final Object SCOPE_KEY = new Object();
49      private static final Object PREVIOUS_SCOPE_KEY = new Object();
50  
51      static {
52          System.setProperty("io.netty.customResourceLeakDetector", WithTransferableScope.class.getName());
53      }
54  
55      @Override
56      public void beforeAll(ExtensionContext context) {
57          ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.GLOBAL);
58          if (store.get(SCOPE_KEY) != null) {
59              throw new IllegalStateException("Weird context lifecycle");
60          }
61          LeakPresenceDetector.ResourceScope scope = new LeakPresenceDetector.ResourceScope(context.getDisplayName());
62          store.put(SCOPE_KEY, scope);
63  
64          WithTransferableScope.SCOPE.set(scope);
65      }
66  
67      @Override
68      public void beforeEach(ExtensionContext context) {
69          LeakPresenceDetector.ResourceScope outerScope;
70          ExtensionContext outerContext = context;
71          while (true) {
72              outerScope = (LeakPresenceDetector.ResourceScope)
73                      outerContext.getStore(ExtensionContext.Namespace.GLOBAL).get(SCOPE_KEY);
74              if (outerScope != null) {
75                  break;
76              }
77              outerContext = outerContext.getParent()
78                      .orElseThrow(() -> new IllegalStateException("No resource scope found"));
79          }
80  
81          LeakPresenceDetector.ResourceScope previousScope = WithTransferableScope.SCOPE.get();
82          WithTransferableScope.SCOPE.set(outerScope);
83          if (previousScope != null) {
84              context.getStore(ExtensionContext.Namespace.GLOBAL).put(PREVIOUS_SCOPE_KEY, previousScope);
85          }
86      }
87  
88      @Override
89      public void afterEach(ExtensionContext context) {
90          LeakPresenceDetector.ResourceScope previousScope = (LeakPresenceDetector.ResourceScope)
91                  context.getStore(ExtensionContext.Namespace.GLOBAL).get(PREVIOUS_SCOPE_KEY);
92          if (previousScope != null) {
93              WithTransferableScope.SCOPE.set(previousScope);
94          }
95      }
96  
97      @Override
98      public void afterAll(ExtensionContext context) throws InterruptedException {
99          LeakPresenceDetector.ResourceScope scope =
100                 (LeakPresenceDetector.ResourceScope) context.getStore(ExtensionContext.Namespace.GLOBAL).get(SCOPE_KEY);
101 
102         // Wait some time for resources to close. Many tests do loop.shutdownGracefully without waiting, and that's ok.
103         long start = System.nanoTime();
104         while (scope.hasOpenResources() && System.nanoTime() - start < TimeUnit.SECONDS.toNanos(5)) {
105             TimeUnit.MILLISECONDS.sleep(100);
106         }
107 
108         scope.close();
109     }
110 
111     public static final class WithTransferableScope<T> extends LeakPresenceDetector<T> {
112         static final InheritableThreadLocal<ResourceScope> SCOPE = new InheritableThreadLocal<>();
113 
114         @SuppressWarnings("unused")
115         public WithTransferableScope(Class<?> resourceType, int samplingInterval) {
116             super(resourceType);
117         }
118 
119         @SuppressWarnings("unused")
120         public WithTransferableScope(Class<?> resourceType, int samplingInterval, long maxActive) {
121             super(resourceType);
122         }
123 
124         @Override
125         protected ResourceScope currentScope() throws AllocationProhibitedException {
126             ResourceScope scope = SCOPE.get();
127             if (scope == null) {
128                 throw new AllocationProhibitedException("Resource created outside test?");
129             }
130             return scope;
131         }
132     }
133 }