diff --git a/buildSrc/src/main/groovy/java-common-conventions.gradle b/buildSrc/src/main/groovy/java-common-conventions.gradle index 56a308b8..09cf0629 100644 --- a/buildSrc/src/main/groovy/java-common-conventions.gradle +++ b/buildSrc/src/main/groovy/java-common-conventions.gradle @@ -91,7 +91,7 @@ spotless { // define the steps to apply to those files trimTrailingWhitespace() - indentWithSpaces() + leadingTabsToSpaces() endWithNewline() } groovyGradle { diff --git a/testing/src/test/java/com/team2813/lib2813/testing/truth/Component.java b/testing/src/test/java/com/team2813/lib2813/testing/truth/Component.java new file mode 100644 index 00000000..25b00971 --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/truth/Component.java @@ -0,0 +1,47 @@ +/* +Copyright 2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813.lib2813.testing.truth; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.support.ParameterDeclarations; + +interface Component { + + Type getType(); + + enum Type { + TRANSLATION, + ROTATION + } + + abstract class ComponentArgumentsProvider implements ArgumentsProvider { + private final List values; + + protected ComponentArgumentsProvider(Type componentType, Stream allValues) { + values = allValues.filter(c -> c.getType() == componentType).toList(); + } + + @Override + public final Stream provideArguments( + ParameterDeclarations parameters, ExtensionContext context) { + return values.stream().map(Arguments::of); + } + } +} diff --git a/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose2dComponent.java b/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose2dComponent.java new file mode 100644 index 00000000..af3c6bdc --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose2dComponent.java @@ -0,0 +1,76 @@ +/* +Copyright 2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813.lib2813.testing.truth; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Translation2d; +import java.util.stream.Stream; + +/** Represents component in a two-dimensional coordinate system. */ +enum Pose2dComponent implements Component { + X(Type.TRANSLATION) { + @Override + Pose2d add(Pose2d pose, double value) { + return new Pose2d(pose.getX() + value, pose.getY(), pose.getRotation()); + } + }, + Y(Type.TRANSLATION) { + @Override + Pose2d add(Pose2d pose, double value) { + return new Pose2d(pose.getX(), pose.getY() + value, pose.getRotation()); + } + }, + R(Type.ROTATION) { + @Override + Pose2d add(Pose2d pose, double value) { + return new Pose2d( + pose.getTranslation(), new Rotation2d(pose.getRotation().getRadians() + value)); + } + }; + + /** An arguments provider for Pose3dComponent values that represent translations. */ + static class TranslationsArgumentsProvider extends ComponentArgumentsProvider { + TranslationsArgumentsProvider() { + super(Type.TRANSLATION, Stream.of(Pose2dComponent.values())); + } + } + + private final Type componentType; + + Pose2dComponent(Type componentType) { + this.componentType = componentType; + } + + @Override + public final Type getType() { + return componentType; + } + + abstract Pose2d add(Pose2d pose, double value); + + final Translation2d add(Translation2d translation, double value) { + Pose2d pose = new Pose2d(translation, Rotation2d.kZero); + pose = add(pose, value); + return pose.getTranslation(); + } + + final Rotation2d add(Rotation2d rotation, double value) { + Pose2d pose = new Pose2d(Translation2d.kZero, rotation); + pose = add(pose, value); + return pose.getRotation(); + } +} diff --git a/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose2dSubjectTest.java b/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose2dSubjectTest.java new file mode 100644 index 00000000..211b72a7 --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose2dSubjectTest.java @@ -0,0 +1,45 @@ +/* +Copyright 2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813.lib2813.testing.truth; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Rotation2d; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +/** Tests for {@link Pose2dSubject}. */ +class Pose2dSubjectTest { + private static final Pose2d POSE = new Pose2d(7.353, 0.706, new Rotation2d(Math.PI / 6)); + + @ParameterizedTest + @EnumSource(Pose2dComponent.class) + public void isWithin_valueWithinTolerance_doesNotThrow(Pose2dComponent component) { + Pose2d closePose = component.add(POSE, 0.009); + + Pose2dSubject.assertThat(closePose).isWithin(0.01).of(POSE); + } + + @ParameterizedTest + @EnumSource(Pose2dComponent.class) + public void isWithin_valueNotWithinTolerance_throws(Pose2dComponent component) { + Pose2d closePose = component.add(POSE, 0.011); + + assertThrows( + AssertionError.class, () -> Pose2dSubject.assertThat(closePose).isWithin(0.01).of(POSE)); + } +} diff --git a/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose3dComponent.java b/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose3dComponent.java new file mode 100644 index 00000000..2c469f6c --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose3dComponent.java @@ -0,0 +1,109 @@ +/* +Copyright 2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813.lib2813.testing.truth; + +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Translation3d; +import java.util.stream.Stream; + +/** Represents component in a three-dimensional coordinate system. */ +enum Pose3dComponent implements Component { + X(Type.TRANSLATION) { + @Override + Pose3d add(Pose3d pose, double value) { + return new Pose3d(pose.getX() + value, pose.getY(), pose.getZ(), pose.getRotation()); + } + }, + Y(Type.TRANSLATION) { + @Override + Pose3d add(Pose3d pose, double value) { + return new Pose3d(pose.getX(), pose.getY() + value, pose.getZ(), pose.getRotation()); + } + }, + Z(Type.TRANSLATION) { + @Override + Pose3d add(Pose3d pose, double value) { + return new Pose3d(pose.getX(), pose.getY(), pose.getZ() + value, pose.getRotation()); + } + }, + ROLL(Type.ROTATION) { + @Override + Pose3d add(Pose3d pose, double value) { + Rotation3d rotation = pose.getRotation(); + return new Pose3d( + pose.getTranslation(), + new Rotation3d(rotation.getX() + value, rotation.getY(), rotation.getZ())); + } + }, + PITCH(Type.ROTATION) { + @Override + Pose3d add(Pose3d pose, double value) { + Rotation3d rotation = pose.getRotation(); + return new Pose3d( + pose.getTranslation(), + new Rotation3d(rotation.getX(), rotation.getY() + value, rotation.getZ())); + } + }, + YAW(Type.ROTATION) { + @Override + Pose3d add(Pose3d pose, double value) { + Rotation3d rotation = pose.getRotation(); + return new Pose3d( + pose.getTranslation(), + new Rotation3d(rotation.getX(), rotation.getY(), rotation.getZ() + value)); + } + }; + + /** An arguments provider for Pose3dComponent values that represent translations. */ + static class TranslationsArgumentsProvider extends ComponentArgumentsProvider { + TranslationsArgumentsProvider() { + super(Type.TRANSLATION, Stream.of(Pose3dComponent.values())); + } + } + + /** An arguments provider for Pose3dComponent values that represent rotations. */ + static class RotationsArgumentsProvider extends ComponentArgumentsProvider { + RotationsArgumentsProvider() { + super(Type.ROTATION, Stream.of(Pose3dComponent.values())); + } + } + + private final Type componentType; + + Pose3dComponent(Type componentType) { + this.componentType = componentType; + } + + @Override + public final Type getType() { + return componentType; + } + + abstract Pose3d add(Pose3d pose, double value); + + final Translation3d add(Translation3d translation, double value) { + Pose3d pose = new Pose3d(translation, Rotation3d.kZero); + pose = add(pose, value); + return pose.getTranslation(); + } + + final Rotation3d add(Rotation3d rotation, double value) { + Pose3d pose = new Pose3d(Translation3d.kZero, rotation); + pose = add(pose, value); + return pose.getRotation(); + } +} diff --git a/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose3dSubjectTest.java b/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose3dSubjectTest.java new file mode 100644 index 00000000..5e25fd76 --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/truth/Pose3dSubjectTest.java @@ -0,0 +1,46 @@ +/* +Copyright 2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813.lib2813.testing.truth; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation3d; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +/** Tests for {@link Pose3dSubject}. */ +class Pose3dSubjectTest { + private static final Pose3d POSE = + new Pose3d(7.353, 0.706, 42.00, new Rotation3d(6.81, -25.67, 3.16)); + + @ParameterizedTest + @EnumSource(Pose3dComponent.class) + public void isWithin_valueWithinTolerance_doesNotThrow(Pose3dComponent component) { + Pose3d closePose = component.add(POSE, 0.009); + + Pose3dSubject.assertThat(closePose).isWithin(0.01).of(POSE); + } + + @ParameterizedTest + @EnumSource(Pose3dComponent.class) + public void isWithin_valueNotWithinTolerance_throws(Pose3dComponent component) { + Pose3d closePose = component.add(POSE, 0.011); + + assertThrows( + AssertionError.class, () -> Pose3dSubject.assertThat(closePose).isWithin(0.01).of(POSE)); + } +} diff --git a/testing/src/test/java/com/team2813/lib2813/testing/truth/Rotation2dSubjectTest.java b/testing/src/test/java/com/team2813/lib2813/testing/truth/Rotation2dSubjectTest.java new file mode 100644 index 00000000..1c93357c --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/truth/Rotation2dSubjectTest.java @@ -0,0 +1,42 @@ +/* +Copyright 2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813.lib2813.testing.truth; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.first.math.geometry.Rotation2d; +import org.junit.jupiter.api.Test; + +/** Tests for {@link Rotation2dSubject}. */ +class Rotation2dSubjectTest { + private static final Rotation2d ROTATION = new Rotation2d(Math.PI / 6); + + @Test + public void isWithin_valueWithinTolerance_doesNotThrow() { + Rotation2d closeRotation = Pose2dComponent.R.add(ROTATION, 0.009); + + Rotation2dSubject.assertThat(closeRotation).isWithin(0.01).of(ROTATION); + } + + @Test + public void isWithin_valueNotWithinTolerance_throws() { + Rotation2d closeRotation = Pose2dComponent.R.add(ROTATION, 0.011); + + assertThrows( + AssertionError.class, + () -> Rotation2dSubject.assertThat(closeRotation).isWithin(0.01).of(ROTATION)); + } +} diff --git a/testing/src/test/java/com/team2813/lib2813/testing/truth/Rotation3dSubjectTest.java b/testing/src/test/java/com/team2813/lib2813/testing/truth/Rotation3dSubjectTest.java new file mode 100644 index 00000000..aa8eb055 --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/truth/Rotation3dSubjectTest.java @@ -0,0 +1,45 @@ +/* +Copyright 2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813.lib2813.testing.truth; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.first.math.geometry.Rotation3d; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +/** Tests for {@link Rotation3dSubject}. */ +class Rotation3dSubjectTest { + private static final Rotation3d ROTATION = new Rotation3d(6.81, -25.67, 3.16); + + @ParameterizedTest + @ArgumentsSource(Pose3dComponent.RotationsArgumentsProvider.class) + public void isWithin_valueWithinTolerance_doesNotThrow(Pose3dComponent component) { + Rotation3d closeRotation = component.add(ROTATION, 0.009); + + Rotation3dSubject.assertThat(closeRotation).isWithin(0.01).of(ROTATION); + } + + @ParameterizedTest + @ArgumentsSource(Pose3dComponent.RotationsArgumentsProvider.class) + public void isWithin_valueNotWithinTolerance_throws(Pose3dComponent component) { + Rotation3d closeRotation = component.add(ROTATION, 0.011); + + assertThrows( + AssertionError.class, + () -> Rotation3dSubject.assertThat(closeRotation).isWithin(0.01).of(ROTATION)); + } +} diff --git a/testing/src/test/java/com/team2813/lib2813/testing/truth/Translation2dSubjectTest.java b/testing/src/test/java/com/team2813/lib2813/testing/truth/Translation2dSubjectTest.java new file mode 100644 index 00000000..dc2b7ede --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/truth/Translation2dSubjectTest.java @@ -0,0 +1,45 @@ +/* +Copyright 2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813.lib2813.testing.truth; + +import static org.junit.jupiter.api.Assertions.*; + +import edu.wpi.first.math.geometry.Translation2d; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +/** Tests for {@link Translation2dSubject}. */ +class Translation2dSubjectTest { + private static final Translation2d TRANSLATION = new Translation2d(7.353, 0.706); + + @ParameterizedTest + @ArgumentsSource(Pose2dComponent.TranslationsArgumentsProvider.class) + public void isWithin_valueWithinTolerance_doesNotThrow(Pose2dComponent component) { + Translation2d closeTranslation = component.add(TRANSLATION, 0.009); + + Translation2dSubject.assertThat(closeTranslation).isWithin(0.01).of(TRANSLATION); + } + + @ParameterizedTest + @ArgumentsSource(Pose2dComponent.TranslationsArgumentsProvider.class) + public void isWithin_valueNotWithinTolerance_throws(Pose2dComponent component) { + Translation2d closeTranslation = component.add(TRANSLATION, 0.011); + + assertThrows( + AssertionError.class, + () -> Translation2dSubject.assertThat(closeTranslation).isWithin(0.01).of(TRANSLATION)); + } +} diff --git a/testing/src/test/java/com/team2813/lib2813/testing/truth/Translation3dSubjectTest.java b/testing/src/test/java/com/team2813/lib2813/testing/truth/Translation3dSubjectTest.java new file mode 100644 index 00000000..a11d2ad0 --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/truth/Translation3dSubjectTest.java @@ -0,0 +1,45 @@ +/* +Copyright 2026 Prospect Robotics SWENext Club + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.team2813.lib2813.testing.truth; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import edu.wpi.first.math.geometry.Translation3d; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +/** Tests for {@link Translation3dSubject}. */ +class Translation3dSubjectTest { + private static final Translation3d TRANSLATION = new Translation3d(7.353, 0.706, 42.00); + + @ParameterizedTest + @ArgumentsSource(Pose3dComponent.TranslationsArgumentsProvider.class) + public void isWithin_valueWithinTolerance_doesNotThrow(Pose3dComponent component) { + Translation3d closeTranslation = component.add(TRANSLATION, 0.009); + + Translation3dSubject.assertThat(closeTranslation).isWithin(0.01).of(TRANSLATION); + } + + @ParameterizedTest + @ArgumentsSource(Pose3dComponent.TranslationsArgumentsProvider.class) + public void isWithin_valueNotWithinTolerance_throws(Pose3dComponent component) { + Translation3d closeTranslation = component.add(TRANSLATION, 0.011); + + assertThrows( + AssertionError.class, + () -> Translation3dSubject.assertThat(closeTranslation).isWithin(0.01).of(TRANSLATION)); + } +}