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.Objects;
27  import java.util.concurrent.TimeUnit;
28  
29  /**
30   * Junit 5 extension for leak detection using {@link LeakPresenceDetector}.
31   * <p>
32   * Leak presence is checked at the class level. Any resource must be closed at the end of the test class, by the time
33   * {@link org.junit.jupiter.api.AfterAll} has been called. Method-level detection is not possible because some tests
34   * retain resources between methods on the same class, notably parameterized tests that allocate different buffers
35   * before running the test methods with those buffers.
36   * <p>
37   * This extension supports parallel test execution, but has to make some assumptions about the thread lifecycle. The
38   * resource scope for the class is created in {@link org.junit.jupiter.api.BeforeAll}, and then saved in a thread local
39   * on each {@link org.junit.jupiter.api.BeforeEach}. This appears to work well with junit's default parallelism,
40   * despite the use of a fork-join pool that can transfer tasks between threads, but it may lead to problems if tests
41   * make use of fork-join machinery themselves.
42   * <p>
43   * The ThreadLocal holding the scope is {@link InheritableThreadLocal inheritable}, so that e.g. event loops created
44   * in a test are assigned to the test resource scope.
45   */
46  public final class LeakPresenceExtension
47          implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, AfterAllCallback {
48  
49      private static final Object SCOPE_KEY = new Object();
50      private static final Object PREVIOUS_SCOPE_KEY = new Object();
51  
52      static {
53          System.setProperty("io.netty.customResourceLeakDetector", WithTransferableScope.class.getName());
54      }
55  
56      @Override
57      public void beforeAll(ExtensionContext context) {
58          ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.GLOBAL);
59          if (store.get(SCOPE_KEY) != null) {
60              throw new IllegalStateException("Weird context lifecycle");
61          }
62          LeakPresenceDetector.ResourceScope scope = new LeakPresenceDetector.ResourceScope(context.getDisplayName());
63          store.put(SCOPE_KEY, scope);
64  
65          WithTransferableScope.SCOPE.set(scope);
66      }
67  
68      @Override
69      public void beforeEach(ExtensionContext context) {
70          LeakPresenceDetector.ResourceScope outerScope;
71          ExtensionContext outerContext = context;
72          while (true) {
73              outerScope = (LeakPresenceDetector.ResourceScope)
74                      outerContext.getStore(ExtensionContext.Namespace.GLOBAL).get(SCOPE_KEY);
75              if (outerScope != null) {
76                  break;
77              }
78              outerContext = outerContext.getParent()
79                      .orElseThrow(() -> new IllegalStateException("No resource scope found"));
80          }
81  
82          LeakPresenceDetector.ResourceScope previousScope = WithTransferableScope.SCOPE.get();
83          WithTransferableScope.SCOPE.set(outerScope);
84          if (previousScope != null) {
85              context.getStore(ExtensionContext.Namespace.GLOBAL).put(PREVIOUS_SCOPE_KEY, previousScope);
86          }
87      }
88  
89      @Override
90      public void afterEach(ExtensionContext context) {
91          LeakPresenceDetector.ResourceScope previousScope = (LeakPresenceDetector.ResourceScope)
92                  context.getStore(ExtensionContext.Namespace.GLOBAL).get(PREVIOUS_SCOPE_KEY);
93          if (previousScope != null) {
94              WithTransferableScope.SCOPE.set(previousScope);
95          }
96      }
97  
98      @Override
99      public void afterAll(ExtensionContext context) throws InterruptedException {
100         LeakPresenceDetector.ResourceScope scope =
101                 (LeakPresenceDetector.ResourceScope) context.getStore(ExtensionContext.Namespace.GLOBAL).get(SCOPE_KEY);
102 
103         // Wait some time for resources to close. Many tests do loop.shutdownGracefully without waiting, and that's ok.
104         long start = System.nanoTime();
105         while (scope.hasOpenResources() && System.nanoTime() - start < TimeUnit.SECONDS.toNanos(5)) {
106             TimeUnit.MILLISECONDS.sleep(100);
107         }
108 
109         scope.close();
110     }
111 
112     public static final class WithTransferableScope<T> extends LeakPresenceDetector<T> {
113         static final InheritableThreadLocal<ResourceScope> SCOPE = new InheritableThreadLocal<>();
114 
115         @SuppressWarnings("unused")
116         public WithTransferableScope(Class<?> resourceType, int samplingInterval) {
117             super(resourceType);
118         }
119 
120         @SuppressWarnings("unused")
121         public WithTransferableScope(Class<?> resourceType, int samplingInterval, long maxActive) {
122             super(resourceType);
123         }
124 
125         @Override
126         protected ResourceScope currentScope() {
127             return Objects.requireNonNull(SCOPE.get(), "Resource created outside test?");
128         }
129     }
130 }