diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 8c6da727..c89f6a12 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -17,4 +17,13 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v3 - name: Run build with Gradle Wrapper - run: ./gradlew build + run: ./gradlew --info build -PgearHeads.testJvmArg='-XX:ErrorFile=${{ github.workspace }}/hs_err_pid%p.log' + - name: Archive test artifacts + if: success() || failure() + uses: actions/upload-artifact@v4 + with: + name: test-logs + path: | + */build/reports/tests/test + hs_err_pid*.log + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 93f34568..e4fe6314 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ bin ctre_sim +# JVM crash logs; see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* # Created by https://www.toptal.com/developers/gitignore/api/intellij # Edit at https://www.toptal.com/developers/gitignore?templates=intellij diff --git a/buildSrc/src/main/groovy/java-common-conventions.gradle b/buildSrc/src/main/groovy/java-common-conventions.gradle index 61f351ed..8da1a6be 100644 --- a/buildSrc/src/main/groovy/java-common-conventions.gradle +++ b/buildSrc/src/main/groovy/java-common-conventions.gradle @@ -1,3 +1,7 @@ +import groovy.time.TimeCategory +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + plugins { // Apply the java-library plugin for API and implementation separation. id 'java-library' @@ -9,11 +13,60 @@ java { targetCompatibility = JavaVersion.VERSION_17 } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { // Configure string concat to always inline compile options.compilerArgs.add '-XDstringConcat=inline' } +tasks.withType(Test).configureEach { testTask -> + + // Allow adding a JVM arg when running tests via a Gradle Property named 'gearHeads.testJvmArg'. + // In .github/workflows/gradle.yml this is used to specify where to put JVM crash logs. + if (project.hasProperty('gearHeads.testJvmArg')) { + jvmArgs project.properties['gearHeads.testJvmArg'] + } + + // Display test results as tests run, and a summary at the end. + // See https://stackoverflow.com/a/36130467/95725 + testLogging { + // Set options for log level LIFECYCLE + exceptionFormat TestExceptionFormat.FULL + showExceptions true + showCauses true + showStackTraces true + events = [TestLogEvent.FAILED, TestLogEvent.SKIPPED] + + // Set options for log level DEBUG and INFO + debug { + events = [TestLogEvent.STARTED, + TestLogEvent.FAILED, + TestLogEvent.PASSED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_ERROR, + TestLogEvent.STANDARD_OUT] + exceptionFormat TestExceptionFormat.FULL + } + info.events = debug.events + info.exceptionFormat = debug.exceptionFormat + } + + afterSuite { TestDescriptor desc, TestResult result -> + if (!desc.parent) { // will match the outermost suite + def output = "${testTask.project.name}:${testTask.name} results: ${result.resultType} " + + "(${result.testCount} tests, " + + "${result.successfulTestCount} passed, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped) " + + "in ${TimeCategory.minus(new Date(result.endTime), new Date(result.startTime))}" + def startItem = '│ ', endItem = ' │' + def repeatLength = startItem.length() + output.length() + endItem.length() - 2 + println('\n┌' + ('─' * repeatLength) + '┐') + println(startItem + output + endItem) + println('└' + ('─' * repeatLength) + '┘\n') + } + } +} + tasks.named('jar') { manifest { attributes( diff --git a/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java b/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java index e7544411..b33af67e 100644 --- a/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java +++ b/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java @@ -3,6 +3,9 @@ import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.NetworkTableInstance; import edu.wpi.first.wpilibj.Preferences; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import org.junit.rules.ExternalResource; /** @@ -10,6 +13,8 @@ * tests. */ public final class IsolatedPreferences extends ExternalResource { + private static final ScheduledExecutorService CLOSE_EXECUTOR = + Executors.newSingleThreadScheduledExecutor(); private NetworkTableInstance tempInstance; /** Gets the {@link NetworkTable} that contains the preference values. */ @@ -19,16 +24,14 @@ public NetworkTable getPreferencesTable() { @Override protected void before() { + NetworkTableInstance.getDefault(); tempInstance = NetworkTableInstance.create(); Preferences.setNetworkTableInstance(tempInstance); } @Override protected void after() { - try { - Preferences.setNetworkTableInstance(NetworkTableInstance.getDefault()); - } finally { - tempInstance.close(); - } + Preferences.setNetworkTableInstance(NetworkTableInstance.getDefault()); + CLOSE_EXECUTOR.schedule(() -> tempInstance.close(), 10, TimeUnit.MILLISECONDS); } } diff --git a/lib/src/test/java/com/team2813/lib2813/util/InputValidationTest.java b/lib/src/test/java/com/team2813/lib2813/util/InputValidationTest.java index d7b44eb2..7d5071f8 100644 --- a/lib/src/test/java/com/team2813/lib2813/util/InputValidationTest.java +++ b/lib/src/test/java/com/team2813/lib2813/util/InputValidationTest.java @@ -5,7 +5,10 @@ import static org.junit.Assert.assertThrows; import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +@RunWith(Enclosed.class) public class InputValidationTest { // Tests for the `InputValidation.checkCanId(...)` method. public static class CheckCanIdTest { diff --git a/limelight/build.gradle b/limelight/build.gradle index 5b39a260..607856cb 100644 --- a/limelight/build.gradle +++ b/limelight/build.gradle @@ -39,7 +39,7 @@ dependencies { wpi.java.configureTestTasks(test) -tasks.named('test') { +test { // Use JUnit 4 for tests useJUnit() }