Skip to content

Commit

Permalink
docs: add how-to debug tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kristian committed Oct 4, 2021
1 parent d97bdce commit 3646016
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 0 deletions.
37 changes: 37 additions & 0 deletions docs/internal/howto_debug_github_actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## **How To:** Debug / SSH into the GitHub actions workflow

This document describes how to hook into the GitHub actions workflow if required using a remote SSH session.

Add the following workflow step into any position of the `.github/workflows` file of your choice, e.g. [`voter.yml`](../../.github/workflows/voter.yml):

```yaml
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
```
It causes a [tmate](https://tmate.io/) session to be spun up (attention it is a public session, anybody could hook into it, when they monitor our build logs, protection would be possible with options of the `action-tmate`). The action *will wait* until you connect to the session via SSH and create a `continue` file (e.g. by using `touch continue`). Wait for the step to execute, checking the build logs you should see an SSH info like:

```bash
SSH: ssh A7BF8dKH8jHw7KnzBRAUGB42c@sfo2.tmate.io
```

This allows you to connect to connect to the container, before an action is executed, or afterwards, depending on the use case you are trying to debug. Connect to it via an SSH tool of your choice and hit `q` to close the welcome message. You are now on the machine, with step execution halted. in case you would like to continue the workflow create a file called `continue`:

```bash
touch continue
```

**Important**: Don't forget to **remove** the action again, once you merge your change. The `tmate` action should not stay in the workflow, when being merged to the main branch.

### Things you might be interested to do on the container

```bash
# grep all java processes running
pgrep -fl java
# or scan top in order to find out from a CPU point of view, which java process is most likely stuck
top
# use jstack to take a thread dump
jstack 3303 > /tmp/threaddump.txt
# use a service like https://transfer.sh/ to upload the dump
curl --upload-file /tmp/threaddump.txt https://transfer.sh/threaddump.txt
```
40 changes: 40 additions & 0 deletions docs/internal/howto_detect_stuck_tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## **How To:** Detect stuck tests

With asynchronous execution of tests a missed `@Timeout` annotation and/or not signaling completion could easily lead to tests getting stuck. With the amount of unit tests, it can be hard to determine which tests get stuck when. A easy *non-intrusive* way of determining which tests are currently running / stuck, without having to enable full `showStandardStreams` in `build.gradle`, is to add a `beforeTest` and `afterTest` closure in `build.gradle`. To do so add the following variable above the `test { ... }` block:

```groovy
def runningTests = java.util.Collections.synchronizedSet(new java.util.HashSet())
```

Into the `test { ... }` block then add:

```groovy
beforeTest { descriptor ->
runningTests.add(descriptor)
logger.lifecycle("Start execution of test: " + descriptor)
logger.lifecycle("Currently running: " + runningTests)
}
afterTest { descriptor, result ->
runningTests.remove(descriptor)
logger.lifecycle("Finished execution of test: " + descriptor)
logger.lifecycle("Still running: " + runningTests)
}
```

To report which tests are currently still running. As soon as as a single stuck test has been identified, it often helps to log its console outputs:

```groovy
onOutput { descriptor, event ->
if (descriptor.className == 'io.neonbee.test.AnyTestClassName') {
logger.lifecycle("debug: " + event.message)
}
}
```

Alternatively all standard output / error logs can be enabled using the following `testLogging` property:

```groovy
testLogging { showStandardStreams = true }
```

*Attention:* Enabling all standard outputs will generate a huge log file!
13 changes: 13 additions & 0 deletions docs/internal/howto_disable_tests_on_github.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## **How To:** Disable Tests on GitHub only

Use the following JUnit 5 annotation, to disable a test only for the voter on GitHub:

```java
@DisabledIfEnvironmentVariable(named = "GITHUB_ACTIONS", matches = "true", disabledReason = "provide any reason here")
```

Before you disable the test on the GitHub workflow, it maybe makes sense trying to `@Isolate` the test first. This sometimes fixes any issues with the concurrent execution:

```java
@Isolated("provide any reason here")
```
5 changes: 5 additions & 0 deletions docs/internal/howto_utilize_test_execution_listeners.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## How To: Utilize `TestExecutionListener`s in JUnit 5

Have a look at some examples in our `src/test/java/io/neonbee/test/listeners` folder.

Enable / disable the listeners by adding/removing them to/from the [`src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener`](../../src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener) file. JUnit 5 is loading all listeners, that are present in the file using the [Service Provider Interface (SPI)](https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html) mechanism. Any line starting with a `#` in the service file will be ignored.
98 changes: 98 additions & 0 deletions docs/internal/howto_write_an_intercepting_agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
## How To: Write an intercepting agent for debug purpose

Sometimes it might be helpful to hook into literally **any** Java class of your choice for debug purpose. This is where a [*Java Agent*](https://www.baeldung.com/java-instrumentation) will come in handy. Utilizing Gradle it is super easy to write one, here is what you need to do:

1. Create a subfolder `agent` in the root directory to put your Java Agent in
2. Use the following basic `build.gradle` file, to prepare yourself doing byte code manipulation:

```groovy
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
compileOnly group: 'org.slf4j', name: 'slf4j-api', version: '1.7.29'
compileOnly group: 'net.bytebuddy', name: 'byte-buddy', version: '1.11.15'
}
tasks.withType(Jar) {
manifest {
attributes['Manifest-Version'] = '1.0'
attributes['Premain-Class'] = 'io.neonbee.agent.AnyAgent'
attributes['Can-Redefine-Classes'] = 'true'
attributes['Can-Retransform-Classes'] = 'true'
}
}
```

*Note*: How we have to define a `Premain-Class` in the manifest. We chose `io.neonbee.agent.AnyAgent`.
3. Create the respective Java file in the `src/main/java/io/neonbee/agent/AnyAgent` directory of the `agent` subdirectory. For example:

```java
package io.neonbee.agent;

import static net.bytebuddy.matcher.ElementMatchers.isConstructor;

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Constructor;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.asm.Advice.Argument;
import net.bytebuddy.asm.Advice.Origin;
import net.bytebuddy.asm.Advice.This;
import net.bytebuddy.matcher.ElementMatchers;

public class AnyAgent {
public static void premain(String arguments, Instrumentation instrumentation) {
new AgentBuilder.Default().type(ElementMatchers.named("io.vertx.core.impl.VertxImpl"))
.transform((builder, typeDescription, classLoader, module) -> builder
.visit(Advice.to(VertxImplConstructorInterceptor.class).on(isConstructor())))
.installOn(instrumentation);

new AgentBuilder.Default().type(ElementMatchers.named("io.vertx.core.impl.VertxThread"))
.transform((builder, typeDescription, classLoader, module) -> builder
.visit(Advice.to(VertxThreadConstructorInterceptor.class).on(isConstructor())))
.installOn(instrumentation);
}

public static class VertxImplConstructorInterceptor {
@Advice.OnMethodExit
public static void intercept(@Origin Constructor<?> m, @This Object inst) throws Exception {
new IllegalStateException("VERTX IMPL INIT!!").printStackTrace();
}
}

public static class VertxThreadConstructorInterceptor {
@Advice.OnMethodExit
public static void intercept(@Origin Constructor<?> m, @This Object inst, @Argument(1) String name)
throws Exception {
new IllegalStateException("VERTX THREAD INIT!! " + name + " (" + ((Thread) inst).getId() + ")")
.printStackTrace();
}
}
}
```
4. Next we have to add the following plugin into our root projects `build.gradle`s `plugin { ... }` block:

```groovy
id 'com.zoltu.application-agent' version '1.0.14'
```

Add the following dependency to our `build.gradle`s `dependencies { ... }`:

```groovy
agent project(':agent')
```

And add the following `include` statement into our `settings.gradle`:

```groovy
include 'agent'
```

At the next start of the virtual machine, e.g. after running `./gradlew test` the instrumentation will be performed and the code associated to your `premain` method will get executed.

0 comments on commit 3646016

Please sign in to comment.