Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,16 @@ dependencies {
test {
useJUnitPlatform()
systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true'
testLogging {
events "passed", "skipped", "failed"
exceptionFormat "full"
}
}

// Simulation configuration (e.g. environment variables).
wpi.sim.addGui().defaultEnabled = true
wpi.sim.addDriverstation()
def isCI = System.getenv("CI") != null || project.hasProperty("headless")
wpi.sim.addGui().defaultEnabled = !isCI
wpi.sim.addDriverstation().defaultEnabled = !isCI

// Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar')
// in order to make them all available at runtime. Also adding the manifest so WPILib
Expand Down
19 changes: 19 additions & 0 deletions docs/tools/simulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ That view is especially useful for autos. Select the auto in Elastic or Glass, e

AdvantageScope can also replay the same data from a `.wpilog` after the run. That makes the workflow: test the auto in sim, use 3D Field live while it runs, then open the saved log if you need to scrub frame-by-frame through the exact moment the path or pose estimate went sideways.

## Automated Simulation Testing in CI/CD

In addition to interactive debugging in Glass and AdvantageScope, the repository runs automated JUnit 5 simulation integration tests as part of `./gradlew build` and GitHub Actions CI.

The simulation test suite lives under [`src/test/java/frc/robot/sim/`](../../src/test/java/frc/robot/sim/) and is built around [`SimTestBase`](../../src/test/java/frc/robot/sim/SimTestBase.java), which handles:

* Initializing WPILib's Hardware Abstraction Layer (HAL) in simulation mode (`HAL.initialize(50, 0)`).
* Managing `DriverStationSim` state transitions (teleop and autonomous modes).
* Stepping simulation timing deterministically via `SimHooks.stepTiming(...)` and executing the `CommandScheduler`.
* Resetting scheduler, notifier, and DriverStation state between test runs so tests remain isolated.

In CI (`CI=true`) or when `-Pheadless=true` is passed to Gradle, `wpi.sim.addGui().defaultEnabled` and `wpi.sim.addDriverstation().defaultEnabled` are automatically set to `false`, allowing simulation tests to run in headless Ubuntu Linux containers without requiring X11 display servers.

You can run the full verification suite locally using:

```bash
scripts/verify.sh
```

## What Simulation Catches (and Doesn't)

It catches:
Expand Down
21 changes: 21 additions & 0 deletions scripts/verify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e

# Change to repo root
cd "$(dirname "$0")/.."

if [ "$1" = "--fix" ]; then
echo "=== Running Spotless Apply ==="
./gradlew spotlessApply
else
echo "=== Running Spotless Check ==="
./gradlew spotlessCheck
fi

echo "=== Running SpotBugs Static Analysis ==="
./gradlew spotbugsMain

echo "=== Running Compile and Tests (Unit & Sim Tests) ==="
./gradlew test -Pheadless=true

echo "=== Verification SUCCESSFUL ==="
3 changes: 2 additions & 1 deletion src/main/java/frc/robot/auton/Auton.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import frc.spectrumLib.framework.SpectrumState;
import frc.spectrumLib.telemetry.Telemetry;
import java.io.IOException;
import lombok.Getter;
import org.json.simple.parser.ParseException;

public class Auton {
Expand All @@ -35,7 +36,7 @@ public class Auton {
public static final EventTrigger autonUnjam = new EventTrigger("unjam");
public static final EventTrigger autonPoseUpdate = new EventTrigger("poseUpdate");

private final SendableChooser<Command> pathChooser = new SendableChooser<>();
@Getter private final SendableChooser<Command> pathChooser = new SendableChooser<>();
private boolean autoMessagePrinted = true;
private double autonStart = 0;

Expand Down
17 changes: 15 additions & 2 deletions src/main/java/frc/robot/subsystems/swerve/Swerve.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
* Class that extends the Phoenix SwerveDrivetrain class and implements subsystem so it can be used
* in command-based projects easily.
*/
public class Swerve extends SwerveDrivetrain<TalonFX, TalonFX, CANcoder> implements Subsystem {
public class Swerve extends SwerveDrivetrain<TalonFX, TalonFX, CANcoder>
implements Subsystem, AutoCloseable {

// ── State machine ──────────────────────────────────────────────────────────────────
public enum WantedState {
Expand Down Expand Up @@ -747,7 +748,10 @@ private void configurePathPlanner() {
@Getter private RobotBumpSim robotBumpSim = null;
@Getter private Pose3d simRobotPose3d = Pose3d.kZero;

/** Starts the sim thread. */
/**
* Starts the simulated drivetrain and bump simulation, then schedules periodic simulation
* updates.
*/
@SuppressWarnings("unchecked")
private void startSimThread() {
mapleSimSwerveDrivetrain =
Expand All @@ -772,4 +776,13 @@ private void startSimThread() {
simNotifier = new Notifier(mapleSimSwerveDrivetrain::update);
simNotifier.startPeriodic(config.getSimLoopPeriod());
}

/** Stops the simulation thread and cleans up resources. */
@Override
public void close() {
if (simNotifier != null) {
simNotifier.close();
simNotifier = null;
}
}
}
18 changes: 17 additions & 1 deletion src/main/java/frc/spectrumLib/sim/SimLoop.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ public static synchronized void register(DoubleConsumer step) {
notifier = started;
}
}
/** Tick. */
/**
* Advances the simulation loop and notifies each registered callback of the elapsed time.
*
* @param dt the elapsed time in seconds since the previous update
*/
Comment on lines +45 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C3 'javadoc|doclint|Xdoclint' build.gradle build.gradle.kts 2>/dev/null || true
sed -n '45,56p' src/main/java/frc/spectrumLib/sim/SimLoop.java

Repository: Spectrum3847/2026-Spectrum

Length of output: 1453


Remove the invalid @param dt tag. tick() has no dt parameter. Keep the description focused on notifying callbacks with the computed elapsed time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/frc/spectrumLib/sim/SimLoop.java` around lines 45 - 49, Update
the Javadoc for the tick() method by removing the invalid `@param` dt tag and
keeping the description focused on notifying registered callbacks with the
computed elapsed time.

private static void tick() {
double now = Utils.getCurrentTimeSeconds();
double dt = now - lastTime;
Expand All @@ -51,4 +55,16 @@ private static void tick() {
step.accept(dt);
}
}

/**
* Clears registered step callbacks and stops the notifier thread. Intended for unit/simulation
* tests.
*/
public static synchronized void reset() {
steps.clear();
if (notifier != null) {
notifier.close();
notifier = null;
}
}
}
46 changes: 46 additions & 0 deletions src/test/java/frc/robot/sim/AutonSimTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package frc.robot.sim;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import edu.wpi.first.wpilibj2.command.CommandScheduler;
import frc.robot.auton.Auton;
import frc.robot.subsystems.swerve.Swerve;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/** Automated simulation tests for the autonomous routine chooser and execution. */
public class AutonSimTest extends SimTestBase {

@Test
@DisplayName("Auton initializes the SendableChooser and default option without exceptions")
void testAutonChooserInitialization() {
assertDoesNotThrow(
() -> {
RobotStack stack = createRobotStack();
try (Swerve swerve = stack.swerve) {
Auton auton = new Auton(stack.superStructure);
assertNotNull(auton.getPathChooser());
assertNotNull(auton.getPathChooser().getSelected());
}
});
}

@Test
@DisplayName("Auton init schedules the selected auto and steps simulation time cleanly")
void testAutonInitAndStep() {
assertDoesNotThrow(
() -> {
RobotStack stack = createRobotStack();
try (Swerve swerve = stack.swerve) {
Auton auton = new Auton(stack.superStructure);
CommandScheduler.getInstance().registerSubsystem(stack.superStructure);

enableAutonomousSim();
auton.init();

stepSim(0.020, 0.50);
}
});
}
}
68 changes: 68 additions & 0 deletions src/test/java/frc/robot/sim/MechanismSimTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package frc.robot.sim;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import edu.wpi.first.wpilibj2.command.CommandScheduler;
import frc.robot.subsystems.hood.Hood;
import frc.robot.subsystems.intakeExtension.IntakeExtension;
import frc.robot.subsystems.turret.Turret;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/** Automated simulation tests for individual mechanism subsystems. */
public class MechanismSimTest extends SimTestBase {

@Test
@DisplayName("Hood subsystem initializes and runs simulation steps without exceptions")
void testHoodSimPeriodic() {
assertDoesNotThrow(
() -> {
Hood hood = new Hood(new Hood.HoodConfig());
CommandScheduler.getInstance().registerSubsystem(hood);

enableTeleopSim();
stepSim(0.020, 0.50);

assertNotNull(hood.getSim());
});
}

@Test
@DisplayName("Turret subsystem initializes and runs simulation steps without exceptions")
void testTurretSimPeriodic() {
assertDoesNotThrow(
() -> {
Turret turret = new Turret(new Turret.TurretConfig());
CommandScheduler.getInstance().registerSubsystem(turret);

enableTeleopSim();
stepSim(0.020, 0.50);

assertNotNull(turret.getSim());
});
}

@Test
@DisplayName(
"IntakeExtension subsystem initializes and runs simulation steps without exceptions")
void testIntakeExtensionSimPeriodic() {
assertDoesNotThrow(
() -> {
IntakeExtension.Left.LeftConfig leftConfig =
new IntakeExtension.Left.LeftConfig();
IntakeExtension.Right.RightConfig rightConfig =
new IntakeExtension.Right.RightConfig(leftConfig);
IntakeExtension intakeExtension =
new IntakeExtension(
new IntakeExtension.IntakeExtensionConfig(
leftConfig, rightConfig));
CommandScheduler.getInstance().registerSubsystem(intakeExtension);

enableTeleopSim();
stepSim(0.020, 0.50);

assertNotNull(intakeExtension.getSim());
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
37 changes: 37 additions & 0 deletions src/test/java/frc/robot/sim/RobotSimTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package frc.robot.sim;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import edu.wpi.first.wpilibj2.command.CommandScheduler;
import frc.robot.RobotSim;
import frc.robot.subsystems.swerve.Swerve;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/** Automated simulation tests for the RobotSim visualizer and fuel physics simulation. */
public class RobotSimTest extends SimTestBase {

@Test
@DisplayName("RobotSim initializes visualizer and fuel physics and updates cleanly")
void testRobotSimCreationAndPeriodic() {
assertDoesNotThrow(
() -> {
RobotStack stack = createRobotStack();
try (Swerve swerve = stack.swerve) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 'sourceCompatibility|targetCompatibility|JavaLanguageVersion' build.gradle build.gradle.kts 2>/dev/null || true
rg -n -C2 'try \(Swerve swerve = stack\.swerve\)' src/test/java/frc/robot/sim

Repository: Spectrum3847/2026-Spectrum

Length of output: 2536


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test files ---'
cat -n src/test/java/frc/robot/sim/RobotSimTest.java
cat -n src/test/java/frc/robot/sim/SuperStructureSimTest.java
cat -n src/test/java/frc/robot/sim/AutonSimTest.java

printf '%s\n' '--- RobotStack and Swerve declarations/usages ---'
rg -n -C3 'class RobotStack|record RobotStack|stack\.swerve|class Swerve|interface Swerve|implements .*AutoCloseable|close\(' src/main/java src/test/java build.gradle gradle.properties 2>/dev/null || true

printf '%s\n' '--- PMD configuration and Java toolchain ---'
rg -n -C3 'pmd|PMD|sourceCompatibility|targetCompatibility|toolchain|JavaLanguageVersion' . -g '!build' -g '!node_modules' -g '!vendor' 2>/dev/null || true

Repository: Spectrum3847/2026-Spectrum

Length of output: 13135


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

cat >"$tmpdir/TryResourceField.java" <<'JAVA'
public class TryResourceField {
    static class Resource implements AutoCloseable {
        public void close() {}
    }

    static class Holder {
        Resource resource = new Resource();

        void method() {
            try (resource) {
                System.out.println("body");
            }
        }
    }

    public static void main(String[] args) {
        new Holder().method();
    }
}
JAVA

if command -v javac >/dev/null 2>&1; then
    javac --release 17 "$tmpdir/TryResourceField.java"
    echo 'Java 17 accepts an instance-field try-with-resources reference.'
else
    echo 'javac is unavailable; source-level behavior requires manual verification.'
fi

Repository: Spectrum3847/2026-Spectrum

Length of output: 238


🌐 Web query:

Java 17 JLS try-with-resources VariableAccess field access effectively final resource

💡 Result:

As of Java 17 and later, the Java Language Specification (JLS) allows a try-with-resources statement to use an existing variable as a resource, provided that the variable is final or effectively final [1][2]. This capability was introduced in Java 9 to simplify resource management [1][2]. Under the JLS, the resource specification in a try-with-resources statement can consist of: 1. A formal local variable declaration (the original Java 7 approach) [2][3]. 2. A reference to an existing final or effectively final local variable [1][2]. It is important to note that field access or other arbitrary expressions (such as array access or method calls) are not permitted directly within the resource specification [4]. The resource must be a simple variable name that refers to an object implementing AutoCloseable [4]. If you need to manage a resource accessed via a field, you must first assign that field to a local variable and ensure that the local variable remains effectively final before using it in the try-with-resources statement [4]. A variable is considered effectively final if its value is not changed after it is initialized [2]. If you attempt to use a non-final or non-effectively final variable (e.g., a variable reassigned later in the scope) as a resource, the compiler will issue an error [2]. The requirement for a final or effectively final variable ensures that the resource being closed is the same one intended throughout the block's lifecycle, avoiding ambiguity [5].

Citations:


🌐 Web query:

site:docs.oracle.com/javase/specs/jls/se17/html/jls-14.html try-with-resources VariableAccess FieldAccess

💡 Result:

In a try-with-resources statement in Java, the resources declared within the resource specification act as local variables [1]. These variables are scoped strictly to the try-with-resources statement itself, meaning they are accessible within the associated try block and any corresponding catch or finally blocks [1]. Regarding VariableAccess and FieldAccess, there are no special restrictions imposed by the try-with-resources construct that prevent you from accessing class fields or other variables that are in scope [1]. You can access instance or static fields of an object, or any other accessible variables, from within the body of the try-with-resources statement just as you would in any other block of code [1]. The resource variables declared in the header are simply treated as local variable declarations, adhering to standard Java scoping and access rules [1].

Citations:


🌐 Web query:

Java 17 javac try (object.field) field access try-with-resources

💡 Result:

In Java 17, the try-with-resources statement does not support arbitrary field access expressions (such as object.field) directly within the resource specification [1][2]. Starting with Java 9, the try-with-resources statement was enhanced to allow the use of existing variables, provided they are declared as final or effectively final [2][3][4][5]. However, this support is strictly limited to variable identifiers; it does not extend to general expressions like field access expressions (object.field) or array access expressions [2][6][5]. If you attempt to use a field expression like try (this.field) { ... } or try (obj.field) { ... }, the compiler will generate an error because the syntax requires a simple variable identifier [4][5]. To use an instance field in a try-with-resources statement, you must first assign the field's value to a local final or effectively final variable [2][3]: final var myResource = this.field; try (myResource) { // Use myResource here } This design restriction exists for several reasons: 1. Stability: Fields can be modified by other threads or parts of the program during the execution of the try block, which could lead to unpredictable behavior regarding resource cleanup [2][3][4]. 2. Complexity: Allowing general expressions introduces significant specification and implementation complexities, particularly concerning how to evaluate the expression, when to capture its value, and how to handle potential side effects [6][5]. 3. Semantics: Requiring a simple variable identifier ensures clarity regarding the lifecycle and closing of the resource [6][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from urllib.request import urlopen
from re import search

urls = [
    "https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html",
    "https://docs.oracle.com/javase/specs/jls/se17/jls17.pdf",
]
for url in urls:
    try:
        data = urlopen(url, timeout=15).read()
        print(f"URL: {url}")
        print(f"HTTP bytes: {len(data)}")
        if url.endswith(".html"):
            text = data.decode("utf-8", "replace")
            for needle in ("Resource:", "VariableAccess:", "FieldAccess:"):
                pos = text.find(needle)
                print(f"--- {needle} ---")
                print(text[pos:pos + 1800] if pos >= 0 else "not found")
    except Exception as exc:
        print(f"URL: {url}\nERROR: {exc}")
PY

printf '%s\n' '--- available Java tools ---'
for tool in javac java ecj kotlinc groovy; do
    if command -v "$tool" >/dev/null 2>&1; then
        printf '%s: %s\n' "$tool" "$(command -v "$tool")"
    fi
done

Repository: Spectrum3847/2026-Spectrum

Length of output: 626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if command -v curl >/dev/null 2>&1; then
    curl --silent --show-error --fail --insecure \
        https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html |
        sed -n '/id="jls-14.20.3"/,/id="jls-14.20.4"/p' |
        grep -E -A8 -B3 'Resource:|VariableAccess:|FieldAccess:' || true
fi

printf '%s\n' '--- Java compiler module availability ---'
java --list-modules 2>/dev/null | grep -E '^(jdk.compiler|java.compiler)@' || true

printf '%s\n' '--- Java runtime version ---'
java -version 2>&1

Repository: Spectrum3847/2026-Spectrum

Length of output: 483


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

cat >"$tmpdir/TryResourceField.java" <<'JAVA'
public class TryResourceField {
    static final class Resource implements AutoCloseable {
        `@Override`
        public void close() {}
    }

    static final class RobotStack {
        public final Resource swerve = new Resource();
    }

    static void useStack() {
        RobotStack stack = new RobotStack();
        try (stack.swerve) {
            System.out.println("body");
        }
    }
}
JAVA

java --add-modules jdk.compiler com.sun.tools.javac.Main \
    --release 17 -d "$tmpdir/out" "$tmpdir/TryResourceField.java"

echo 'Java 17 accepted try (stack.swerve).'

Repository: Spectrum3847/2026-Spectrum

Length of output: 201


Remove the unused resource locals and imports.

At all four sites, use try (stack.swerve) and remove the now-unused Swerve imports.

🧰 Tools
🪛 PMD (7.26.0)

[Medium] 21-21: UnusedLocalVariable (Best Practices): Avoid unused local variables such as 'swerve'.

(UnusedLocalVariable (Best Practices))

📍 Affects 3 files
  • src/test/java/frc/robot/sim/RobotSimTest.java#L20-L21 (this comment)
  • src/test/java/frc/robot/sim/SuperStructureSimTest.java#L21-L22
  • src/test/java/frc/robot/sim/AutonSimTest.java#L20-L21
  • src/test/java/frc/robot/sim/AutonSimTest.java#L34-L35
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/frc/robot/sim/RobotSimTest.java` around lines 20 - 21, At the
four try-with-resources sites in src/test/java/frc/robot/sim/RobotSimTest.java
(20-21), src/test/java/frc/robot/sim/SuperStructureSimTest.java (21-22), and
src/test/java/frc/robot/sim/AutonSimTest.java (20-21, 34-35), remove the local
Swerve resource variable and use stack.swerve directly; remove the now-unused
Swerve imports from all affected files.

Source: Linters/SAST tools

RobotSim robotSim = new RobotSim(stack.superStructure);
CommandScheduler.getInstance().registerSubsystem(stack.superStructure);

assertNotNull(robotSim.getBallSim());
assertNotNull(RobotSim.leftView);
assertNotNull(RobotSim.topView);

enableTeleopSim();
stepSim(0.020, 0.50);

robotSim.updateArticulatedMechanisms();
robotSim.getBallSim().tick();
}
});
}
}
Loading
Loading