-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #91 from flamingchickens1540/test-bot
Create testing framework
- Loading branch information
Showing
7 changed files
with
446 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
lib/src/main/java/org/team1540/rooster/testers/AbstractTester.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
package org.team1540.rooster.testers; | ||
|
||
import com.google.common.collect.EvictingQueue; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.function.Function; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
/** | ||
* A basic implementation of the Tester interface using essentially EvictingQueues for result | ||
* storage. | ||
*/ | ||
@SuppressWarnings({"unused", "UnstableApiUsage"}) | ||
public abstract class AbstractTester<T, R> implements Tester<T, R> { | ||
|
||
/** | ||
* The maximum length of time for which we want to store the results. Note that this is just | ||
* used for estimating the queue depth based on the update delay, not actually checked against | ||
* while running. | ||
*/ | ||
private float logTime = 150; | ||
|
||
private int updateDelay = 2500; | ||
private boolean running = true; | ||
@SuppressWarnings("NullableProblems") | ||
@NotNull | ||
List<T> itemsToTest; | ||
@SuppressWarnings("NullableProblems") | ||
@NotNull | ||
private Function<T, R> test; | ||
@SuppressWarnings("NullableProblems") | ||
@NotNull | ||
private List<Function<T, Boolean>> runConditions; | ||
@SuppressWarnings("NullableProblems") | ||
@NotNull | ||
private Map<T, ResultStorage<R>> storedResults; | ||
|
||
/** | ||
* Construct a new instance with the default queue depth. | ||
* | ||
* @param test The test to execute. | ||
* @param itemsToTest The items to apply the test to. | ||
* @param runConditions The conditions that must be met before the test will be executed on an | ||
* item. | ||
*/ | ||
AbstractTester(@NotNull Function<T, R> test, @NotNull List<T> itemsToTest, | ||
@NotNull List<Function<T, Boolean>> runConditions) { | ||
realConstructor(test, itemsToTest, runConditions, (int) logTime / (updateDelay / 1000)); | ||
} | ||
|
||
/** | ||
* Construct a new instance, specifying the logTime and updateDelay to calculate the queue depth. | ||
* | ||
* @param test The test to execute. | ||
* @param itemsToTest The items to apply the test to. | ||
* @param runConditions The conditions that must be met before the test will be executed on an | ||
* item. | ||
* @param logTime The maximum length of time for which we want to store the results. | ||
* @param updateDelay The delay between the test being run on the items. | ||
*/ | ||
AbstractTester(@NotNull Function<T, R> test, @NotNull List<T> itemsToTest, | ||
@NotNull List<Function<T, Boolean>> runConditions, float logTime, int updateDelay) { | ||
this.logTime = logTime; | ||
this.updateDelay = updateDelay; | ||
realConstructor(test, itemsToTest, runConditions, | ||
(int) (logTime / ((float) updateDelay / 1000f))); | ||
} | ||
|
||
/** | ||
* | ||
* Construct a new instance with a given queueDepth. | ||
* @param test The test to execute. | ||
* @param itemsToTest The items to apply the test to. | ||
* @param runConditions The conditions that must be met before the test will be executed on an | ||
* item. | ||
* @param queueDepth The maximum number of items that the {@link EvictingQueue} can hold. | ||
*/ | ||
AbstractTester(@NotNull Function<T, R> test, @NotNull List<T> itemsToTest, | ||
@NotNull List<Function<T, Boolean>> runConditions, int queueDepth) { | ||
realConstructor(test, itemsToTest, runConditions, queueDepth); | ||
} | ||
|
||
private void realConstructor(@NotNull Function<T, R> test, @NotNull List<T> itemsToTest, | ||
@NotNull List<Function<T, Boolean>> runConditions, int queueDepth) { | ||
this.test = test; | ||
this.itemsToTest = itemsToTest; | ||
this.runConditions = runConditions; | ||
this.storedResults = new HashMap<>(itemsToTest.size()); | ||
// TODO I feel like there's a cleaner way of doing this | ||
for (T t : itemsToTest) { | ||
this.storedResults.put(t, new ResultStorage<>(queueDepth)); | ||
} | ||
} | ||
|
||
@Override | ||
@NotNull | ||
public Function<T, R> getTest() { | ||
return test; | ||
} | ||
|
||
@Override | ||
public void setTest(@NotNull Function<T, R> test) { | ||
this.test = test; | ||
} | ||
|
||
@Override | ||
@NotNull | ||
public List<Function<T, Boolean>> getRunConditions() { | ||
return Collections.unmodifiableList(runConditions); | ||
} | ||
|
||
@Override | ||
@NotNull | ||
public List<T> getItemsToTest() { | ||
return Collections.unmodifiableList(itemsToTest); | ||
} | ||
|
||
@Override | ||
@NotNull | ||
public EvictingQueue<ResultWithMetadata<R>> getStoredResults(T key) { | ||
return storedResults.get(key).queuedResults; | ||
} | ||
|
||
@Override | ||
@Nullable | ||
public ResultWithMetadata<R> peekMostRecentResult(T key) { | ||
return storedResults.get(key).lastResult; | ||
} | ||
|
||
@Override | ||
public int getUpdateDelay() { | ||
return updateDelay; | ||
} | ||
|
||
@Override | ||
public int setUpdateDelay(int delay) { | ||
int oldUpdateDelay = this.updateDelay; | ||
this.updateDelay = delay; | ||
return oldUpdateDelay; | ||
} | ||
|
||
@Override | ||
public boolean setRunning(boolean status) { | ||
boolean oldRunning = this.running; | ||
this.running = status; | ||
return status; | ||
} | ||
|
||
/** | ||
* The code that should be called every tick. This does the actual testing. Override me as | ||
* necessary (but don't forget to call super!) | ||
*/ | ||
void periodic() { | ||
for (T t : itemsToTest) { | ||
// Run through all the run conditions and make sure they all return true | ||
for (Function<T, Boolean> runCondition : runConditions) { | ||
if (!runCondition.apply(t)) { | ||
return; | ||
} | ||
} | ||
this.storedResults.get(t).addResult(this.test.apply(t), System.currentTimeMillis()); | ||
} | ||
} | ||
|
||
@Override | ||
public void run() { | ||
while (true) { | ||
|
||
if (running) { | ||
periodic(); | ||
} | ||
|
||
try { | ||
Thread.sleep(updateDelay); | ||
} catch (InterruptedException e) { | ||
// End the thread | ||
return; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* A class for handling the storage of results. Basically just so the tail can actually be | ||
* peeked at. | ||
* @param <A> The returned type to store. | ||
*/ | ||
private class ResultStorage<A> { | ||
|
||
@Nullable | ||
private ResultWithMetadata<A> lastResult; | ||
@NotNull | ||
private EvictingQueue<ResultWithMetadata<A>> queuedResults; | ||
|
||
private ResultStorage(int queueDepth) { | ||
this.queuedResults = EvictingQueue.create(queueDepth); | ||
} | ||
|
||
private void addResult(A result, long timeStampMillis) { | ||
ResultWithMetadata<A> resultWithMetadata = new ResultWithMetadata<>(result, timeStampMillis); | ||
this.lastResult = resultWithMetadata; | ||
queuedResults.add(resultWithMetadata); | ||
} | ||
|
||
} | ||
|
||
} |
115 changes: 115 additions & 0 deletions
115
lib/src/main/java/org/team1540/rooster/testers/BurnoutTester.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package org.team1540.rooster.testers; | ||
|
||
import edu.wpi.first.wpilibj.Sendable; | ||
import edu.wpi.first.wpilibj.smartdashboard.SendableBuilder; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; | ||
import org.apache.commons.math3.stat.descriptive.rank.Median; | ||
import org.team1540.rooster.wrappers.ChickenTalon; | ||
|
||
/** | ||
* Reports motor burnouts by comparing the current draw across a series of similarly-purposed | ||
* motors and reporting low outliers. | ||
*/ | ||
@SuppressWarnings("unused") | ||
public class BurnoutTester extends AbstractTester<ChickenTalon, Boolean> implements Sendable { | ||
|
||
|
||
private static final Median medianCalculator = new Median(); | ||
private static final StandardDeviation stdDevCalculator = new StandardDeviation(); | ||
private double medianCurrent = 0; | ||
private double stdDevCurrent = 0; | ||
|
||
private String name = "BurnoutTester"; | ||
|
||
/** | ||
* Construct a new instance. | ||
* | ||
* @param motorsToTest The motors to compare to each other. | ||
*/ | ||
public BurnoutTester(ChickenTalon... motorsToTest) { | ||
// Because passing in a reference to a non-static method in the constructor doesn't work. | ||
super((stupid) -> null, Arrays.asList(motorsToTest), | ||
Collections.singletonList((ignore) -> true), 150, 500); | ||
this.setTest(this::testBurnout); | ||
this.setUpdateDelay(500); | ||
} | ||
|
||
/** | ||
* Construct a new instance. | ||
* | ||
* @param motorsToTest The motors to compare to each other. | ||
*/ | ||
public BurnoutTester(List<ChickenTalon> motorsToTest) { | ||
// Because passing in a reference to a non-static method in the constructor doesn't work. | ||
super((stupid) -> null, motorsToTest, | ||
Collections.singletonList((ignore) -> true), 150, 500); | ||
this.setTest(this::testBurnout); | ||
} | ||
|
||
/** | ||
* Tests to see if a motor is burned out by checking to see if it is at least one standard | ||
* deviation below the median. | ||
* @param manageable The motor to test for burnout. | ||
* @return Boolean indicating burnout. | ||
*/ | ||
@SuppressWarnings("WeakerAccess") | ||
public Boolean testBurnout(ChickenTalon manageable) { | ||
return manageable.getOutputCurrent() < (this.medianCurrent - 1 * this.stdDevCurrent); | ||
} | ||
|
||
/** | ||
* Gets the currents, calculates the median and standard deviation, then calls super. | ||
*/ | ||
@Override | ||
void periodic() { | ||
double[] currents = itemsToTest.stream().mapToDouble(ChickenTalon::getOutputCurrent).toArray(); | ||
medianCurrent = medianCalculator.evaluate(currents); | ||
stdDevCurrent = stdDevCalculator.evaluate(currents); | ||
super.periodic(); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return this.name; | ||
} | ||
|
||
@Override | ||
public void setName(String name) { | ||
this.name = name; | ||
} | ||
|
||
@Override | ||
public String getSubsystem() { | ||
return this.name; | ||
} | ||
|
||
@Override | ||
public void setSubsystem(String subsystem) { | ||
this.name = subsystem; | ||
} | ||
|
||
/** | ||
* Displays the current status of each motor and the median current draw. | ||
* @param builder The {@link SendableBuilder} to use. | ||
*/ | ||
@Override | ||
public void initSendable(SendableBuilder builder) { | ||
for (ChickenTalon t : getItemsToTest()) { | ||
// Get the most recent value if present, else simply don't add it to the builder | ||
builder.addBooleanProperty(t.getDeviceID() + "", () -> { | ||
// TODO probably cleaner version of this, at the least ifPresentOrElse() in Java 9 | ||
Optional<ResultWithMetadata<Boolean>> result = Optional.ofNullable(peekMostRecentResult(t)); | ||
if (result.isPresent()) { | ||
return result.get().getResult(); | ||
} else { | ||
return false; | ||
} | ||
}, null); | ||
} | ||
builder.addDoubleProperty("Median current", () -> medianCurrent, null); | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
lib/src/main/java/org/team1540/rooster/testers/ResultWithMetadata.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package org.team1540.rooster.testers; | ||
|
||
import org.jetbrains.annotations.NotNull; | ||
|
||
/** | ||
* Class for encapsulating results. Includes the result and a timestamp. | ||
* | ||
* @param <R> The result type. | ||
*/ | ||
public class ResultWithMetadata<R> { | ||
|
||
@NotNull | ||
private final R result; | ||
private final long timestampMillis; | ||
|
||
ResultWithMetadata(@NotNull R result, long timestampMillis) { | ||
this.result = result; | ||
this.timestampMillis = timestampMillis; | ||
} | ||
|
||
@SuppressWarnings("WeakerAccess") | ||
@NotNull | ||
public R getResult() { | ||
return result; | ||
} | ||
|
||
@SuppressWarnings("unused") | ||
public long getTimestampMillis() { | ||
return timestampMillis; | ||
} | ||
|
||
} |
Oops, something went wrong.