Skip to content

Commit

Permalink
Merge pull request #91 from flamingchickens1540/test-bot
Browse files Browse the repository at this point in the history
Create testing framework
  • Loading branch information
edelmanjm authored Nov 16, 2018
2 parents b8cdf21 + 7f0dd0d commit 86237c0
Show file tree
Hide file tree
Showing 7 changed files with 446 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip
2 changes: 2 additions & 0 deletions lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ dependencies {
compile "openrio.mirror.third.kauailabs:navx-java:3.0.346"
compile "jaci.pathfinder:Pathfinder-Java:1.8"
compile "openrio.powerup:MatchData:2018.01.07"
compile "com.google.guava:guava:27.0-jre"
compile "org.apache.commons:commons-math3:3.6.1"
}
209 changes: 209 additions & 0 deletions lib/src/main/java/org/team1540/rooster/testers/AbstractTester.java
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 lib/src/main/java/org/team1540/rooster/testers/BurnoutTester.java
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);
}
}
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;
}

}
Loading

0 comments on commit 86237c0

Please sign in to comment.