From ac065b7f0c5f5bc4bb21c853e8b498397d7e54c1 Mon Sep 17 00:00:00 2001 From: Esta Nagy Date: Sun, 12 Jun 2022 17:02:24 +0200 Subject: [PATCH] Fine grain test inclusion/exclusion (#104) - Adds new always aborting evaluator - Adds override keyword to all evaluators (to allow naming and referencing evaluators) - Adds additional properties to allow evaluator based abortion or suppression - Adds override keyword to the Flight Evaluation Report - Adds new properties to the Readme Resolves #102 {minor} Signed-off-by: Esta Nagy --- .../booster/cucumber/LaunchAbortHook.java | 9 + mission-control/README.md | 14 +- .../core/AbstractLaunchSequenceTemplate.java | 4 +- ...AbstractMissionLaunchSequenceTemplate.java | 8 +- .../abortmission/core/MissionControl.java | 40 +++- .../MissionHealthCheckEvaluator.java | 23 +++ .../AbstractMissionHealthCheckEvaluator.java | 59 +++++- ...ysAbortingMissionHealthCheckEvaluator.java | 64 +++++++ ...ntageBasedMissionHealthCheckEvaluator.java | 13 +- ...ReportOnlyMissionHealthCheckEvaluator.java | 14 +- .../converter/LaunchTelemetryConverter.java | 8 +- .../AbstractStageStatisticsCollectorTest.java | 102 +++++++++++ ...ortingMissionHealthCheckEvaluatorTest.java | 172 ++++++++++++++++++ .../DefaultStageStatisticsSnapshotTest.java | 117 ++++++++++++ ...eBasedMissionHealthCheckEvaluatorTest.java | 18 ++ ...rtOnlyMissionHealthCheckEvaluatorTest.java | 75 +++++++- .../impl/StageStatisticsCollectorTest.java | 89 +++++++++ .../telemetry/stats/TestRunTelemetryTest.java | 76 ++++++++ .../reporting/html/LaunchHtml.java | 4 +- .../reporting/html/LaunchHtmlTest.java | 6 +- .../test/resources/abort-mission-report.txt | 6 +- 21 files changed, 888 insertions(+), 33 deletions(-) create mode 100644 mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AlwaysAbortingMissionHealthCheckEvaluator.java create mode 100644 mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AbstractStageStatisticsCollectorTest.java create mode 100644 mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AlwaysAbortingMissionHealthCheckEvaluatorTest.java create mode 100644 mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/DefaultStageStatisticsSnapshotTest.java create mode 100644 mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/StageStatisticsCollectorTest.java create mode 100644 mission-control/src/test/java/com/github/nagyesta/abortmission/core/telemetry/stats/TestRunTelemetryTest.java diff --git a/boosters/booster-cucumber-jvm/src/main/java/com/github/nagyesta/abortmission/booster/cucumber/LaunchAbortHook.java b/boosters/booster-cucumber-jvm/src/main/java/com/github/nagyesta/abortmission/booster/cucumber/LaunchAbortHook.java index 1fbd3a16..dd00de1d 100644 --- a/boosters/booster-cucumber-jvm/src/main/java/com/github/nagyesta/abortmission/booster/cucumber/LaunchAbortHook.java +++ b/boosters/booster-cucumber-jvm/src/main/java/com/github/nagyesta/abortmission/booster/cucumber/LaunchAbortHook.java @@ -35,6 +35,15 @@ protected Map> defineOutline() { private final Map stopwatchMap = new HashMap<>(); + /** + * Returns a {@link MissionHealthCheckMatcher} matching any scenario URI. + * + * @return matcher + */ + public static MissionHealthCheckMatcher anyScenarioMatcher() { + return new ScenarioUriMatcher(".*"); + } + /** * Returns a {@link MissionHealthCheckMatcher} matching scenario URIs. * diff --git a/mission-control/README.md b/mission-control/README.md index 93be4cd7..99eb72d5 100644 --- a/mission-control/README.md +++ b/mission-control/README.md @@ -49,7 +49,7 @@ In order to configure Abort-Mission, the easiest option would be to follow these 1. Implement [MissionOutline](./src/main/java/com/github/nagyesta/abortmission/core/outline/MissionOutline.java) named as `MissionOutlineDefinition` preferably in your root package -2. Use tha annotations we provide [here](./src/main/java/com/github/nagyesta/abortmission/core/annotation/) +2. Use the annotations we provide [here](./src/main/java/com/github/nagyesta/abortmission/core/annotation/) 3. Make sure to hook our lifecycle methods by either 1. using a [LaunchSequenceTemplate](./src/main/java/com/github/nagyesta/abortmission/core/LaunchSequenceTemplate.java) 2. or one of the [Callable/runnable implementations here](./src/main/java/com/github/nagyesta/abortmission/core/selfpropelled/) @@ -57,8 +57,10 @@ preferably in your root package ### System properties -| Property | Type | Meaning | -| -------------------------------- | --------- | --------------------------------------------------------------------------- | -| `abort-mission.disarm.countdown` | `boolean` | Disables countdown aborts for all tests. Default: false | -| `abort-mission.disarm.mission` | `boolean` | Disables mission aborts for all tests. Default: false | -| `abort-mission.report.directory` | `String` | Output directory path where we want to save telemetry output. Default: null | +| Property | Type | Meaning | +|-------------------------------------------|-----------|------------------------------------------------------------------------------------------------------| +| `abort-mission.disarm.countdown` | `boolean` | Disables countdown aborts for all tests. Default: false | +| `abort-mission.disarm.mission` | `boolean` | Disables mission aborts for all tests. Default: false | +| `abort-mission.report.directory` | `String` | Output directory path where we want to save telemetry output. Default: null | +| `abort-mission.force.abort.evaluators` | `String` | Comma separated list of key words identifying evaluators which need to always abort. Default: null | +| `abort-mission.suppress.abort.evaluators` | `String` | Comma separated list of key words identifying evaluators which need to suppress abort. Default: null | diff --git a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/AbstractLaunchSequenceTemplate.java b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/AbstractLaunchSequenceTemplate.java index 90aaed39..b2837ba4 100644 --- a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/AbstractLaunchSequenceTemplate.java +++ b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/AbstractLaunchSequenceTemplate.java @@ -41,8 +41,10 @@ protected Optional performPreLaunchInit(final Class testI final StageTimeStopwatch watch = new StageTimeStopwatch(testInstanceClass); final Set evaluators = classBasedEvaluatorLookup.apply(testInstanceClass); + final boolean hasSuppression = evaluators.stream().anyMatch(MissionHealthCheckEvaluator::shouldSuppressAbort); final boolean reportingDone = evaluateAndAbortIfNeeded( - partitionBy(evaluators, MissionHealthCheckEvaluator::shouldAbortCountdown), + partitionBy(evaluators, missionHealthCheckEvaluator + -> !hasSuppression && missionHealthCheckEvaluator.shouldAbortCountdown()), annotationContextEvaluator().isAbortSuppressed(testInstanceClass), watch.stop(), MissionHealthCheckEvaluator::countdownLogger); diff --git a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/AbstractMissionLaunchSequenceTemplate.java b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/AbstractMissionLaunchSequenceTemplate.java index ec45a7cd..18472c15 100644 --- a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/AbstractMissionLaunchSequenceTemplate.java +++ b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/AbstractMissionLaunchSequenceTemplate.java @@ -36,10 +36,12 @@ public AbstractMissionLaunchSequenceTemplate(final Runnable abortSequence) { protected Optional evaluateLaunchAbort(final Set evaluators, final StageTimeStopwatch stopwatch, final Supplier abortSuppressionDecisionSupplier) { - final Boolean shouldSuppressAbortDecisions = Objects.requireNonNull(abortSuppressionDecisionSupplier).get(); + final boolean hasEvaluatorThatSuppressesAbort = evaluators.stream().anyMatch(MissionHealthCheckEvaluator::shouldSuppressAbort); + final boolean shouldSuppressAbortDecisions = Boolean.TRUE.equals(Objects.requireNonNull(abortSuppressionDecisionSupplier).get()); final boolean reportingDone = evaluateAndAbortIfNeeded( - partitionBy(evaluators, MissionHealthCheckEvaluator::shouldAbort), - Boolean.TRUE.equals(shouldSuppressAbortDecisions), + partitionBy(evaluators, missionHealthCheckEvaluator + -> !hasEvaluatorThatSuppressesAbort && missionHealthCheckEvaluator.shouldAbort()), + shouldSuppressAbortDecisions, stopwatch.stop(), MissionHealthCheckEvaluator::missionLogger); return emptyIfTrue(reportingDone, stopwatch); diff --git a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/MissionControl.java b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/MissionControl.java index e74e0a96..9dce688d 100644 --- a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/MissionControl.java +++ b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/MissionControl.java @@ -3,10 +3,7 @@ import com.github.nagyesta.abortmission.core.annotation.AnnotationContextEvaluator; import com.github.nagyesta.abortmission.core.healthcheck.MissionHealthCheckEvaluator; import com.github.nagyesta.abortmission.core.healthcheck.StageStatisticsCollectorFactory; -import com.github.nagyesta.abortmission.core.healthcheck.impl.DefaultStageStatisticsCollectorFactory; -import com.github.nagyesta.abortmission.core.healthcheck.impl.MissionStatisticsCollector; -import com.github.nagyesta.abortmission.core.healthcheck.impl.PercentageBasedMissionHealthCheckEvaluator; -import com.github.nagyesta.abortmission.core.healthcheck.impl.ReportOnlyMissionHealthCheckEvaluator; +import com.github.nagyesta.abortmission.core.healthcheck.impl.*; import com.github.nagyesta.abortmission.core.matcher.MissionHealthCheckMatcher; import com.github.nagyesta.abortmission.core.matcher.impl.MissionHealthCheckMatcherBuilder; import com.github.nagyesta.abortmission.core.matcher.impl.builder.InitialMissionHealthCheckMatcherBuilder; @@ -20,7 +17,14 @@ * Provides shorthands for the core functionality of the library. */ public final class MissionControl { - + /** + * System property name we need to use to force aborts for the selected evaluators. + */ + public static final String ABORT_MISSION_FORCE_ABORT_EVALUATORS = "abort-mission.force.abort.evaluators"; + /** + * System property name we need to use to suppress aborts for the selected evaluators. + */ + public static final String ABORT_MISSION_SUPPRESS_ABORT_EVALUATORS = "abort-mission.suppress.abort.evaluators"; /** * System property name we need to use to disarm mission aborts. */ @@ -74,6 +78,32 @@ public static PercentageBasedMissionHealthCheckEvaluator.Builder percentageBased statisticsFactory.newMissionStatistics(matcher))); } + /** + * Creates a builder instance for always aborting evaluators. + * + * @param matcher The matcher we want to use for this evaluator. + * @return builder + */ + public static AlwaysAbortingMissionHealthCheckEvaluator.Builder abortingEvaluator(final MissionHealthCheckMatcher matcher) { + return abortingEvaluator(matcher, new DefaultStageStatisticsCollectorFactory()); + } + + /** + * Creates a builder instance for always aborting evaluators. + * + * @param matcher The matcher we want to use for this evaluator. + * @param statisticsFactory The factory instance that can provide statistics collectors. + * @return builder + */ + public static AlwaysAbortingMissionHealthCheckEvaluator.Builder abortingEvaluator( + final MissionHealthCheckMatcher matcher, + final StageStatisticsCollectorFactory statisticsFactory) { + return AlwaysAbortingMissionHealthCheckEvaluator + .builder(matcher, new MissionStatisticsCollector( + statisticsFactory.newCountdownStatistics(matcher), + statisticsFactory.newMissionStatistics(matcher))); + } + /** * Creates a builder instance for report only evaluators. * diff --git a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/MissionHealthCheckEvaluator.java b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/MissionHealthCheckEvaluator.java index 10cb5648..47633baf 100644 --- a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/MissionHealthCheckEvaluator.java +++ b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/MissionHealthCheckEvaluator.java @@ -42,6 +42,22 @@ public interface MissionHealthCheckEvaluator { */ ReadOnlyStageStatistics getMissionStatistics(); + /** + * Tells us whether this evaluator should never abort and suppress other matching evaluators if they would. + * + * @return true if the evaluator should suppress abort, false otherwise. + */ + boolean shouldSuppressAbort(); + + /** + * Tells us whether this evaluator should always abort. + * Important: If two different evaluators are matching and one returns true for {@link #shouldSuppressAbort()} while + * the other returns true for {@code shouldForceAbort()}, then the "suppression" will take precedence and neither will abort. + * + * @return true if the evaluator should always abort. + */ + boolean shouldForceAbort(); + /** * Tells the launch controller whether we should abort the launch after the preparation was done. * @@ -70,4 +86,11 @@ public interface MissionHealthCheckEvaluator { * @return mission */ StatisticsLogger missionLogger(); + + /** + * Returns the keyword we can use for overriding abort/disarm decisions for the evaluator using command line parameters. + * + * @return override keyword + */ + String overrideKeyword(); } diff --git a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AbstractMissionHealthCheckEvaluator.java b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AbstractMissionHealthCheckEvaluator.java index f6d07006..1adc6529 100644 --- a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AbstractMissionHealthCheckEvaluator.java +++ b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AbstractMissionHealthCheckEvaluator.java @@ -6,8 +6,11 @@ import com.github.nagyesta.abortmission.core.healthcheck.StatisticsLogger; import com.github.nagyesta.abortmission.core.matcher.MissionHealthCheckMatcher; -import static com.github.nagyesta.abortmission.core.MissionControl.ABORT_MISSION_DISARM_COUNTDOWN; -import static com.github.nagyesta.abortmission.core.MissionControl.ABORT_MISSION_DISARM_MISSION; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.github.nagyesta.abortmission.core.MissionControl.*; /** * Implements the common functionality of {@link MissionHealthCheckEvaluator} instances. @@ -16,6 +19,7 @@ public abstract class AbstractMissionHealthCheckEvaluator implements MissionHeal private final MissionHealthCheckMatcher matcher; private final MissionStatisticsCollector stats; + private final String overrideKeyword; /** * Sets the matcher and the mission statistics collector. @@ -23,9 +27,29 @@ public abstract class AbstractMissionHealthCheckEvaluator implements MissionHeal * @param matcher The health check matcher mentioned by {@link MissionHealthCheckEvaluator#getMatcher()}. * @param stats The statistics collector mentioned by {@link MissionHealthCheckEvaluator#getStats()}. */ - protected AbstractMissionHealthCheckEvaluator(final MissionHealthCheckMatcher matcher, final MissionStatisticsCollector stats) { + protected AbstractMissionHealthCheckEvaluator(final MissionHealthCheckMatcher matcher, + final MissionStatisticsCollector stats) { + this(matcher, stats, null); + } + + /** + * Sets the matcher and the mission statistics collector. + * + * @param matcher The health check matcher mentioned by {@link MissionHealthCheckEvaluator#getMatcher()}. + * @param stats The statistics collector mentioned by {@link MissionHealthCheckEvaluator#getStats()}. + * @param overrideKeyword The keyword used for fine-grained abort/disarm overrides. + */ + protected AbstractMissionHealthCheckEvaluator(final MissionHealthCheckMatcher matcher, + final MissionStatisticsCollector stats, + final String overrideKeyword) { this.matcher = matcher; this.stats = stats; + this.overrideKeyword = overrideKeyword; + } + + @Override + public String overrideKeyword() { + return overrideKeyword; } @Override @@ -58,12 +82,24 @@ public StatisticsLogger missionLogger() { return stats.getMission(); } + @Override + public boolean shouldSuppressAbort() { + return evaluateOverrideList(ABORT_MISSION_SUPPRESS_ABORT_EVALUATORS) + .contains(overrideKeyword); + } + + @Override + public boolean shouldForceAbort() { + return evaluateOverrideList(ABORT_MISSION_FORCE_ABORT_EVALUATORS) + .contains(overrideKeyword); + } + @Override public boolean shouldAbort() { if (isDisarmed(ABORT_MISSION_DISARM_MISSION)) { return false; } - return shouldAbortInternal(); + return shouldForceAbort() || shouldAbortInternal(); } @Override @@ -71,7 +107,20 @@ public boolean shouldAbortCountdown() { if (isDisarmed(ABORT_MISSION_DISARM_COUNTDOWN)) { return false; } - return shouldAbortCountdownInternal(); + return shouldForceAbort() || shouldAbortCountdownInternal(); + } + + /** + * Returns the set of evaluator keywords which are defined in the given System property. + * + * @param propertyName The name of the System property. + * @return The tokenized set of keywords defined by the property value or empty set if not defined. + */ + protected Set evaluateOverrideList(final String propertyName) { + final String property = System.getProperty(propertyName, ""); + return Arrays.stream(property.split(",")) + .filter(s -> !s.isBlank()) + .collect(Collectors.toSet()); } /** diff --git a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AlwaysAbortingMissionHealthCheckEvaluator.java b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AlwaysAbortingMissionHealthCheckEvaluator.java new file mode 100644 index 00000000..6aaca10f --- /dev/null +++ b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AlwaysAbortingMissionHealthCheckEvaluator.java @@ -0,0 +1,64 @@ +package com.github.nagyesta.abortmission.core.healthcheck.impl; + +import com.github.nagyesta.abortmission.core.matcher.MissionHealthCheckMatcher; + +import java.util.Objects; + +/** + * {@link com.github.nagyesta.abortmission.core.healthcheck.MissionHealthCheckEvaluator} implementation intended to always abort. + */ +@SuppressWarnings("checkstyle:FinalClass") +public class AlwaysAbortingMissionHealthCheckEvaluator extends AbstractMissionHealthCheckEvaluator { + + private AlwaysAbortingMissionHealthCheckEvaluator(final Builder builder) { + super(Objects.requireNonNull(builder, "Builder cannot be null.").matcher, + builder.statisticsCollector, builder.overrideKeyword); + } + + public static Builder builder(final MissionHealthCheckMatcher matcher, + final MissionStatisticsCollector statisticsCollector) { + return new Builder(matcher, statisticsCollector); + } + + @Override + public int getBurnInTestCount() { + return Integer.MAX_VALUE; + } + + @Override + protected boolean shouldAbortInternal() { + return true; + } + + @Override + protected boolean shouldAbortCountdownInternal() { + return true; + } + + @SuppressWarnings({"checkstyle:HiddenField", "checkstyle:DesignForExtension"}) + public static final class Builder { + private final MissionHealthCheckMatcher matcher; + private final MissionStatisticsCollector statisticsCollector; + private String overrideKeyword; + + private Builder(final MissionHealthCheckMatcher matcher, + final MissionStatisticsCollector statisticsCollector) { + this.matcher = Objects.requireNonNull(matcher, "Matcher cannot be null."); + this.statisticsCollector = Objects.requireNonNull(statisticsCollector, "Statistic collector cannot be null."); + } + + public Builder overrideKeyword(final String overrideKeyword) { + if (overrideKeyword == null || overrideKeyword.isBlank()) { + throw new IllegalArgumentException("Override keyword must br non-blank."); + } else if (!overrideKeyword.matches("[\\da-zA-Z\\-]+")) { + throw new IllegalArgumentException("Override keyword must contain only alpha-numeric characters and dash (a-zA-Z0-9\\-)."); + } + this.overrideKeyword = overrideKeyword; + return this; + } + + public AlwaysAbortingMissionHealthCheckEvaluator build() { + return new AlwaysAbortingMissionHealthCheckEvaluator(this); + } + } +} diff --git a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/PercentageBasedMissionHealthCheckEvaluator.java b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/PercentageBasedMissionHealthCheckEvaluator.java index 1788717b..9984ccea 100644 --- a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/PercentageBasedMissionHealthCheckEvaluator.java +++ b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/PercentageBasedMissionHealthCheckEvaluator.java @@ -19,7 +19,7 @@ public class PercentageBasedMissionHealthCheckEvaluator extends AbstractMissionH private PercentageBasedMissionHealthCheckEvaluator(final Builder builder) { super(Objects.requireNonNull(builder, "Builder cannot be null.").matcher, - builder.statisticsCollector); + builder.statisticsCollector, builder.overrideKeyword); this.burnInTestCount = builder.burnInTestCount; this.abortThreshold = builder.abortThreshold; } @@ -65,6 +65,7 @@ public static final class Builder { private final MissionHealthCheckMatcher matcher; private int burnInTestCount = BURN_IN_LOWER_LIMIT; private int abortThreshold = PERCENTAGE_LOWER_LIMIT; + private String overrideKeyword; private Builder(final MissionHealthCheckMatcher matcher, final MissionStatisticsCollector statisticsCollector) { this.matcher = Objects.requireNonNull(matcher, "Matcher cannot be null."); @@ -87,6 +88,16 @@ public Builder abortThreshold(final int abortThreshold) { return this; } + public Builder overrideKeyword(final String overrideKeyword) { + if (overrideKeyword == null || overrideKeyword.isBlank()) { + throw new IllegalArgumentException("Override keyword must br non-blank."); + } else if (!overrideKeyword.matches("[\\da-zA-Z\\-]+")) { + throw new IllegalArgumentException("Override keyword must contain only alpha-numeric characters and dash (a-zA-Z0-9\\-)."); + } + this.overrideKeyword = overrideKeyword; + return this; + } + public PercentageBasedMissionHealthCheckEvaluator build() { return new PercentageBasedMissionHealthCheckEvaluator(this); } diff --git a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/ReportOnlyMissionHealthCheckEvaluator.java b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/ReportOnlyMissionHealthCheckEvaluator.java index f0b3593a..a4c71001 100644 --- a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/ReportOnlyMissionHealthCheckEvaluator.java +++ b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/healthcheck/impl/ReportOnlyMissionHealthCheckEvaluator.java @@ -12,7 +12,8 @@ public class ReportOnlyMissionHealthCheckEvaluator extends AbstractMissionHealthCheckEvaluator { private ReportOnlyMissionHealthCheckEvaluator(final Builder builder) { - super(Objects.requireNonNull(builder, "Builder cannot be null.").matcher, builder.statisticsCollector); + super(Objects.requireNonNull(builder, "Builder cannot be null.").matcher, + builder.statisticsCollector, builder.overrideKeyword); } public static Builder builder(final MissionHealthCheckMatcher matcher, @@ -39,6 +40,7 @@ protected boolean shouldAbortCountdownInternal() { public static final class Builder { private final MissionHealthCheckMatcher matcher; private final MissionStatisticsCollector statisticsCollector; + private String overrideKeyword; private Builder(final MissionHealthCheckMatcher matcher, final MissionStatisticsCollector statisticsCollector) { @@ -46,6 +48,16 @@ private Builder(final MissionHealthCheckMatcher matcher, this.statisticsCollector = Objects.requireNonNull(statisticsCollector, "Statistic collector cannot be null."); } + public Builder overrideKeyword(final String overrideKeyword) { + if (overrideKeyword == null || overrideKeyword.isBlank()) { + throw new IllegalArgumentException("Override keyword must br non-blank."); + } else if (!overrideKeyword.matches("[\\da-zA-Z\\-]+")) { + throw new IllegalArgumentException("Override keyword must contain only alpha-numeric characters and dash (a-zA-Z0-9\\-)."); + } + this.overrideKeyword = overrideKeyword; + return this; + } + public ReportOnlyMissionHealthCheckEvaluator build() { return new ReportOnlyMissionHealthCheckEvaluator(this); } diff --git a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/telemetry/converter/LaunchTelemetryConverter.java b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/telemetry/converter/LaunchTelemetryConverter.java index 912b884d..a5dfaa5a 100644 --- a/mission-control/src/main/java/com/github/nagyesta/abortmission/core/telemetry/converter/LaunchTelemetryConverter.java +++ b/mission-control/src/main/java/com/github/nagyesta/abortmission/core/telemetry/converter/LaunchTelemetryConverter.java @@ -73,7 +73,7 @@ protected void mergeInto(final Map>> matchersByC mergeInto(matchersByClassAndMethod, evaluator.getCountdownStatistics().timeSeriesStream(), evaluator.getMissionStatistics().timeSeriesStream(), - evaluator.getMatcher().getName()); + addoverrideKeywordIfPresent(evaluator.overrideKeyword(), evaluator.getMatcher().getName())); } /** @@ -92,4 +92,10 @@ protected void mergeInto(final Map> measureme mergeInto(measurementsByClassName, countdownStatistics.timeSeriesStream(), missionStatistics.timeSeriesStream()); } + private String addoverrideKeywordIfPresent(final String overrideKeyword, final String name) { + return Optional.ofNullable(overrideKeyword) + .map(kw -> "[" + kw + "] " + name) + .orElse(name); + } + } diff --git a/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AbstractStageStatisticsCollectorTest.java b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AbstractStageStatisticsCollectorTest.java new file mode 100644 index 00000000..39ec2b4a --- /dev/null +++ b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AbstractStageStatisticsCollectorTest.java @@ -0,0 +1,102 @@ +package com.github.nagyesta.abortmission.core.healthcheck.impl; + +import com.github.nagyesta.abortmission.core.MissionControl; +import com.github.nagyesta.abortmission.core.healthcheck.StageStatisticsSnapshot; +import com.github.nagyesta.abortmission.core.matcher.MissionHealthCheckMatcher; +import com.github.nagyesta.abortmission.core.telemetry.StageTimeMeasurement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +class AbstractStageStatisticsCollectorTest { + + private static final MissionHealthCheckMatcher MATCHER = MissionControl.matcher().anyClass().build(); + private static final AbstractStageStatisticsCollector AN_INSTANCE = new AbstractStageStatisticsCollector(MATCHER) { + @Override + public void logTimeMeasurement(final StageTimeMeasurement timeMeasurement) { + } + + @Override + public void logAndIncrement(final StageTimeMeasurement timeMeasurement) { + super.logAndIncrement(timeMeasurement); + } + + @Override + public StageStatisticsSnapshot getSnapshot() { + return null; + } + + @Override + public Stream timeSeriesStream() { + return null; + } + }; + + @SuppressWarnings("checkstyle:MagicNumber") + public static Stream equalsProvider() { + final StageStatisticsCollector collector = new StageStatisticsCollector(MATCHER, 1, 2, 3, 4); + return Stream.builder() + .add(Arguments.of(AN_INSTANCE, AN_INSTANCE, true)) + .add(Arguments.of(AN_INSTANCE, collector, true)) + .add(Arguments.of(AN_INSTANCE, null, false)) + .build(); + } + + @SuppressWarnings("checkstyle:MagicNumber") + public static Stream hashCodeProvider() { + return Stream.builder() + .add(Arguments.of(AN_INSTANCE, AN_INSTANCE, true)) + .add(Arguments.of(AN_INSTANCE, null, false)) + .build(); + } + + @ParameterizedTest + @MethodSource("equalsProvider") + void testEqualsShouldReturnTrueWhenTheObjectsAreEqual( + final AbstractStageStatisticsCollector a, + final AbstractStageStatisticsCollector b, + final boolean expected) { + //given + + //when + final boolean actual = a.equals(b); + + //then + Assertions.assertEquals(expected, actual); + } + + @ParameterizedTest + @MethodSource("hashCodeProvider") + void testHashCodeShouldReturnSameHashCodeWhenTheObjectsAreEqual( + final AbstractStageStatisticsCollector a, + final AbstractStageStatisticsCollector b, + final boolean expected) { + //given + + //when + final int aHashCode = Optional.ofNullable(a).map(Objects::hashCode).orElse(0); + final int bHashCode = Optional.ofNullable(b).map(Objects::hashCode).orElse(0); + + //then + Assertions.assertEquals(expected, aHashCode == bHashCode); + } + + @Test + void testGetMatcherShouldReturnProvidedMatcherWhenCalled() { + //given + @SuppressWarnings("UnnecessaryLocalVariable") + final AbstractStageStatisticsCollector underTest = AN_INSTANCE; + + //when + final MissionHealthCheckMatcher actual = underTest.getMatcher(); + + //then + Assertions.assertEquals(MATCHER, actual); + } +} diff --git a/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AlwaysAbortingMissionHealthCheckEvaluatorTest.java b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AlwaysAbortingMissionHealthCheckEvaluatorTest.java new file mode 100644 index 00000000..2ddab5c9 --- /dev/null +++ b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/AlwaysAbortingMissionHealthCheckEvaluatorTest.java @@ -0,0 +1,172 @@ +package com.github.nagyesta.abortmission.core.healthcheck.impl; + +import com.github.nagyesta.abortmission.core.MissionControl; +import com.github.nagyesta.abortmission.core.healthcheck.ReadOnlyMissionStatistics; +import com.github.nagyesta.abortmission.core.matcher.MissionHealthCheckMatcher; +import com.github.nagyesta.abortmission.core.telemetry.StageResult; +import com.github.nagyesta.abortmission.core.telemetry.StageTimeMeasurement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Set; +import java.util.UUID; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SuppressWarnings("checkstyle:MagicNumber") +class AlwaysAbortingMissionHealthCheckEvaluatorTest { + + private static final String DASH = "-"; + + private static Stream countdownEvaluatorProvider() { + return Stream.builder() + .add(Arguments.of(5, 5, 0)) + .add(Arguments.of(4, 0, 4)) + .add(Arguments.of(4, 2, 2)) + .add(Arguments.of(1, 0, 1)) + .add(Arguments.of(1, 0, 1)) + .build(); + } + + private static Stream launchEvaluatorProvider() { + return Stream.builder() + .add(Arguments.of(5, 0, 5)) + .add(Arguments.of(2, 1, 1)) + .add(Arguments.of(3, 0, 3)) + .add(Arguments.of(1, 1, 0)) + .add(Arguments.of(2, 1, 0)) + .build(); + } + + @ParameterizedTest + @ValueSource(strings = {"a b", "a_Z"}) + @NullAndEmptySource + void testBuilderShouldThrowExceptionWhenoverrideKeywordIsCalledWithInvalidData(final String input) { + //given + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final AlwaysAbortingMissionHealthCheckEvaluator.Builder underTest = MissionControl.abortingEvaluator(anyClass); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.overrideKeyword(input)); + + //then + exception + } + + @ParameterizedTest + @MethodSource("countdownEvaluatorProvider") + void testShouldAbortCountdownShouldAlwaysReturnFalseWhenCalled(final int countdownFailure, + final int countdownComplete, + final int failureCount) { + //given + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final AlwaysAbortingMissionHealthCheckEvaluator underTest = MissionControl.abortingEvaluator(anyClass) + .overrideKeyword("all") + .build(); + + //when + IntStream.range(0, countdownFailure).parallel() + .forEach(i -> underTest.countdownLogger().logAndIncrement(with(StageResult.FAILURE))); + IntStream.range(0, failureCount).parallel() + .forEach(i -> underTest.missionLogger().logAndIncrement(with(StageResult.FAILURE))); + IntStream.range(0, countdownComplete).parallel() + .forEach(i -> underTest.countdownLogger().logAndIncrement(with(StageResult.SUCCESS))); + final boolean actual = underTest.shouldAbortCountdown(); + + //then + assertTrue(actual); + } + + @ParameterizedTest + @MethodSource("launchEvaluatorProvider") + void testShouldAbortShouldAlwaysReturnFalseWhenCalled(final int countdownComplete, + final int failureCount, + final int successCount) { + //given + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final AlwaysAbortingMissionHealthCheckEvaluator underTest = MissionControl.abortingEvaluator(anyClass).build(); + + //when + IntStream.range(0, failureCount).parallel() + .forEach(i -> underTest.missionLogger().logAndIncrement(with(StageResult.FAILURE))); + IntStream.range(0, successCount).parallel() + .forEach(i -> underTest.missionLogger().logAndIncrement(with(StageResult.SUCCESS))); + IntStream.range(0, countdownComplete).parallel() + .forEach(i -> underTest.countdownLogger().logAndIncrement(with(StageResult.SUCCESS))); + final boolean actual = underTest.shouldAbort(); + + //then + assertTrue(actual); + } + + @Test + void testoverrideKeywordShouldReturnProvidedValueWhenCalled() { + //given + final String expected = "expected"; + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final AlwaysAbortingMissionHealthCheckEvaluator underTest = MissionControl.abortingEvaluator(anyClass) + .overrideKeyword(expected) + .build(); + + //when + final String actual = underTest.overrideKeyword(); + + //then + assertEquals(expected, actual); + } + + @Test + void testGetStatsShouldReturnStatisticsWhenCalled() { + //given + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final AlwaysAbortingMissionHealthCheckEvaluator underTest = MissionControl.abortingEvaluator(anyClass).build(); + + //when + final ReadOnlyMissionStatistics actual = underTest.getStats(); + + //then + assertNotNull(actual.getReadOnlyMission()); + assertNotNull(actual.getReadOnlyCountdown()); + } + + @Test + void testShouldSuppressAbortShouldReturnTrueWhenSuppressionIsActivated() { + //given + final String keyWord = "all"; + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final AlwaysAbortingMissionHealthCheckEvaluator underTest = spy(MissionControl.abortingEvaluator(anyClass) + .overrideKeyword(keyWord).build()); + doReturn(Set.of(keyWord)).when(underTest).evaluateOverrideList(eq(MissionControl.ABORT_MISSION_SUPPRESS_ABORT_EVALUATORS)); + + //when + final boolean actual = underTest.shouldSuppressAbort(); + + //then + assertTrue(actual); + verify(underTest).evaluateOverrideList(eq(MissionControl.ABORT_MISSION_SUPPRESS_ABORT_EVALUATORS)); + } + + @Test + void testGetBurnInTestCountShouldReturnConstantValue() { + //given + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final AlwaysAbortingMissionHealthCheckEvaluator underTest = MissionControl.abortingEvaluator(anyClass).build(); + + //when + final int actual = underTest.getBurnInTestCount(); + + //then + assertEquals(Integer.MAX_VALUE, actual); + } + + private StageTimeMeasurement with(final StageResult result) { + return new StageTimeMeasurement(UUID.randomUUID(), DASH, DASH, result, 0, 0); + } +} diff --git a/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/DefaultStageStatisticsSnapshotTest.java b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/DefaultStageStatisticsSnapshotTest.java new file mode 100644 index 00000000..d02b7b86 --- /dev/null +++ b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/DefaultStageStatisticsSnapshotTest.java @@ -0,0 +1,117 @@ +package com.github.nagyesta.abortmission.core.healthcheck.impl; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DefaultStageStatisticsSnapshotTest { + + @SuppressWarnings("checkstyle:MagicNumber") + public static Stream equalsProvider() { + final DefaultStageStatisticsSnapshot anInstance = new DefaultStageStatisticsSnapshot(1, 2, 3, 4); + return Stream.builder() + .add(Arguments.of( + new DefaultStageStatisticsSnapshot(1, 2, 3, 4), + new DefaultStageStatisticsSnapshot(1, 2, 3, 4), + true + )) + .add(Arguments.of( + new DefaultStageStatisticsSnapshot(4, 3, 2, 1), + new DefaultStageStatisticsSnapshot(1, 2, 3, 4), + false + )) + .add(Arguments.of( + new DefaultStageStatisticsSnapshot(1, 2, 3, 4), + new DefaultStageStatisticsSnapshot(0, 2, 3, 4), + false + )) + .add(Arguments.of( + new DefaultStageStatisticsSnapshot(1, 2, 3, 4), + new DefaultStageStatisticsSnapshot(1, 0, 3, 4), + false + )) + .add(Arguments.of( + new DefaultStageStatisticsSnapshot(1, 2, 3, 4), + new DefaultStageStatisticsSnapshot(1, 2, 0, 4), + false + )) + .add(Arguments.of( + new DefaultStageStatisticsSnapshot(1, 2, 3, 4), + new DefaultStageStatisticsSnapshot(1, 2, 3, 0), + false + )) + .add(Arguments.of( + new DefaultStageStatisticsSnapshot(1, 2, 3, 4), + null, + false + )) + .add(Arguments.of( + new DefaultStageStatisticsSnapshot(1, 2, 3, 4), + new DefaultStageStatisticsSnapshot(1, 2, 3, 4) { + }, + true + )) + .add(Arguments.of( + anInstance, + anInstance, + true + )) + .build(); + } + + @ParameterizedTest + @MethodSource("equalsProvider") + void testEqualsShouldReturnTrueWhenCalledWithEqualObject( + final DefaultStageStatisticsSnapshot a, + final DefaultStageStatisticsSnapshot b, + final boolean expected) { + //given + + //when + final boolean equals = a.equals(b); + + //then + assertEquals(expected, equals); + } + + @ParameterizedTest + @MethodSource("equalsProvider") + void testHashCodeShouldReturnSameHashCodeWhenCalledWithEqualObject( + final DefaultStageStatisticsSnapshot a, + final DefaultStageStatisticsSnapshot b, + final boolean expected) { + //given + + //when + final int aHashCode = Optional.ofNullable(a).map(Objects::hashCode).orElse(0); + final int bHashCode = Optional.ofNullable(b).map(Objects::hashCode).orElse(0); + + //then + assertEquals(expected, aHashCode == bHashCode); + } + + @SuppressWarnings("checkstyle:MagicNumber") + @Test + void testToStringShouldContainFieldValues() { + //given + final DefaultStageStatisticsSnapshot underTest = new DefaultStageStatisticsSnapshot(1, 2, 3, 4); + + //when + final String actual = underTest.toString(); + + //then + assertTrue(actual.contains("DefaultStageStatisticsSnapshot")); + assertTrue(actual.contains("failed=1")); + assertTrue(actual.contains("succeeded=2")); + assertTrue(actual.contains("aborted=3")); + assertTrue(actual.contains("suppressed=4")); + } +} diff --git a/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/PercentageBasedMissionHealthCheckEvaluatorTest.java b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/PercentageBasedMissionHealthCheckEvaluatorTest.java index 8ab64074..8a9720f2 100644 --- a/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/PercentageBasedMissionHealthCheckEvaluatorTest.java +++ b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/PercentageBasedMissionHealthCheckEvaluatorTest.java @@ -1,12 +1,15 @@ package com.github.nagyesta.abortmission.core.healthcheck.impl; +import com.github.nagyesta.abortmission.core.MissionControl; import com.github.nagyesta.abortmission.core.matcher.MissionHealthCheckMatcher; import com.github.nagyesta.abortmission.core.telemetry.StageResult; import com.github.nagyesta.abortmission.core.telemetry.StageTimeMeasurement; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import java.util.UUID; @@ -45,6 +48,20 @@ private static Stream launchEvaluatorProvider() { .build(); } + @ParameterizedTest + @ValueSource(strings = {"a b", "a_Z"}) + @NullAndEmptySource + void testBuilderShouldThrowExceptionWhenoverrideKeywordIsCalledWithInvalidData(final String input) { + //given + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final PercentageBasedMissionHealthCheckEvaluator.Builder underTest = MissionControl.percentageBasedEvaluator(anyClass); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.overrideKeyword(input)); + + //then + exception + } + @ParameterizedTest @MethodSource("countdownEvaluatorProvider") void testBurnInThresholdsAreWorkingWhenPreparationStepsAreUsed(final int burnInCount, @@ -58,6 +75,7 @@ void testBurnInThresholdsAreWorkingWhenPreparationStepsAreUsed(final int burnInC .builder(anyClass, new MissionStatisticsCollector(anyClass)) .abortThreshold(1) .burnInTestCount(burnInCount) + .overrideKeyword("any") .build(); //when diff --git a/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/ReportOnlyMissionHealthCheckEvaluatorTest.java b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/ReportOnlyMissionHealthCheckEvaluatorTest.java index 4b7ac56f..c50ca9cc 100644 --- a/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/ReportOnlyMissionHealthCheckEvaluatorTest.java +++ b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/ReportOnlyMissionHealthCheckEvaluatorTest.java @@ -4,16 +4,22 @@ import com.github.nagyesta.abortmission.core.matcher.MissionHealthCheckMatcher; import com.github.nagyesta.abortmission.core.telemetry.StageResult; import com.github.nagyesta.abortmission.core.telemetry.StageTimeMeasurement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import java.util.Set; import java.util.UUID; import java.util.stream.IntStream; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.Mockito.mock; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; @SuppressWarnings("checkstyle:MagicNumber") class ReportOnlyMissionHealthCheckEvaluatorTest { @@ -40,6 +46,20 @@ private static Stream launchEvaluatorProvider() { .build(); } + @ParameterizedTest + @ValueSource(strings = {"a b", "a_Z"}) + @NullAndEmptySource + void testBuilderShouldThrowExceptionWhenoverrideKeywordIsCalledWithInvalidData(final String input) { + //given + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final ReportOnlyMissionHealthCheckEvaluator.Builder underTest = MissionControl.reportOnlyEvaluator(anyClass); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> underTest.overrideKeyword(input)); + + //then + exception + } + @ParameterizedTest @MethodSource("countdownEvaluatorProvider") void testShouldAbortCountdownShouldAlwaysReturnFalseWhenCalled(final int countdownFailure, @@ -47,7 +67,9 @@ void testShouldAbortCountdownShouldAlwaysReturnFalseWhenCalled(final int countdo final int failureCount) { //given final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); - final ReportOnlyMissionHealthCheckEvaluator underTest = MissionControl.reportOnlyEvaluator(anyClass).build(); + final ReportOnlyMissionHealthCheckEvaluator underTest = MissionControl.reportOnlyEvaluator(anyClass) + .overrideKeyword("all") + .build(); //when IntStream.range(0, countdownFailure).parallel() @@ -84,6 +106,53 @@ void testShouldAbortShouldAlwaysReturnFalseWhenCalled(final int countdownComplet assertFalse(actual); } + @Test + void testShouldAbortShouldReturnTrueWhenForceAbortIsActivated() { + //given + final String keyWord = "all"; + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final ReportOnlyMissionHealthCheckEvaluator underTest = spy(MissionControl.reportOnlyEvaluator(anyClass) + .overrideKeyword(keyWord).build()); + doReturn(Set.of(keyWord)).when(underTest).evaluateOverrideList(eq(MissionControl.ABORT_MISSION_FORCE_ABORT_EVALUATORS)); + + //when + final boolean actual = underTest.shouldAbort(); + + //then + assertTrue(actual); + verify(underTest).evaluateOverrideList(eq(MissionControl.ABORT_MISSION_FORCE_ABORT_EVALUATORS)); + } + + @Test + void testShouldAbortCountdownShouldReturnTrueWhenForceAbortIsActivated() { + //given + final String keyWord = "all"; + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final ReportOnlyMissionHealthCheckEvaluator underTest = spy(MissionControl.reportOnlyEvaluator(anyClass) + .overrideKeyword(keyWord).build()); + doReturn(Set.of(keyWord)).when(underTest).evaluateOverrideList(eq(MissionControl.ABORT_MISSION_FORCE_ABORT_EVALUATORS)); + + //when + final boolean actual = underTest.shouldAbortCountdown(); + + //then + assertTrue(actual); + verify(underTest).evaluateOverrideList(eq(MissionControl.ABORT_MISSION_FORCE_ABORT_EVALUATORS)); + } + + @Test + void testGetBurnInTestCountShouldReturnConstantValue() { + //given + final MissionHealthCheckMatcher anyClass = mock(MissionHealthCheckMatcher.class); + final ReportOnlyMissionHealthCheckEvaluator underTest = MissionControl.reportOnlyEvaluator(anyClass).build(); + + //when + final int actual = underTest.getBurnInTestCount(); + + //then + assertEquals(Integer.MAX_VALUE, actual); + } + private StageTimeMeasurement with(final StageResult result) { return new StageTimeMeasurement(UUID.randomUUID(), DASH, DASH, result, 0, 0); } diff --git a/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/StageStatisticsCollectorTest.java b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/StageStatisticsCollectorTest.java new file mode 100644 index 00000000..700e8b24 --- /dev/null +++ b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/healthcheck/impl/StageStatisticsCollectorTest.java @@ -0,0 +1,89 @@ +package com.github.nagyesta.abortmission.core.healthcheck.impl; + +import com.github.nagyesta.abortmission.core.MissionControl; +import com.github.nagyesta.abortmission.core.matcher.MissionHealthCheckMatcher; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +class StageStatisticsCollectorTest { + + @SuppressWarnings("checkstyle:MagicNumber") + public static Stream equalsProvider() { + final MissionHealthCheckMatcher matcher = MissionControl.matcher().anyClass().build(); + final StageStatisticsCollector anInstance = new StageStatisticsCollector(matcher, 1, 2, 3, 4); + return Stream.builder() + .add(Arguments.of( + anInstance, + anInstance, + true + )) + .add(Arguments.of( + anInstance, + new StageStatisticsCollector(matcher, 1, 2, 3, 4), + true + )) + .add(Arguments.of( + anInstance, + new StageStatisticsCollector(matcher, 0, 2, 3, 4), + false + )) + .add(Arguments.of( + anInstance, + new StageStatisticsCollector(matcher, 1, 0, 3, 4), + false + )) + .add(Arguments.of( + anInstance, + new StageStatisticsCollector(matcher, 1, 2, 0, 4), + false + )) + .add(Arguments.of( + anInstance, + new StageStatisticsCollector(matcher, 1, 2, 3, 0), + false + )) + .add(Arguments.of( + anInstance, + null, + false + )) + .build(); + } + + @ParameterizedTest + @MethodSource("equalsProvider") + void testEqualsShouldReturnTrueWhenTheObjectsAreEqual( + final StageStatisticsCollector a, + final StageStatisticsCollector b, + final boolean expected) { + //given + + //when + final boolean actual = a.equals(b); + + //then + Assertions.assertEquals(expected, actual); + } + + @ParameterizedTest + @MethodSource("equalsProvider") + void testHashCodeShouldReturnSameHashCodeWhenTheObjectsAreEqual( + final StageStatisticsCollector a, + final StageStatisticsCollector b, + final boolean expected) { + //given + + //when + final int aHashCode = Optional.ofNullable(a).map(Objects::hashCode).orElse(0); + final int bHashCode = Optional.ofNullable(b).map(Objects::hashCode).orElse(0); + + //then + Assertions.assertEquals(expected, aHashCode == bHashCode); + } +} diff --git a/mission-control/src/test/java/com/github/nagyesta/abortmission/core/telemetry/stats/TestRunTelemetryTest.java b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/telemetry/stats/TestRunTelemetryTest.java new file mode 100644 index 00000000..c27ebe21 --- /dev/null +++ b/mission-control/src/test/java/com/github/nagyesta/abortmission/core/telemetry/stats/TestRunTelemetryTest.java @@ -0,0 +1,76 @@ +package com.github.nagyesta.abortmission.core.telemetry.stats; + +import com.github.nagyesta.abortmission.core.telemetry.StageResult; +import com.github.nagyesta.abortmission.core.telemetry.StageTimeMeasurement; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestRunTelemetryTest { + + private static final StageTimeMeasurement MEASUREMENT_1 = + new StageTimeMeasurement(UUID.randomUUID(), "class", "method", StageResult.ABORT, 0, 1); + private static final StageTimeMeasurement MEASUREMENT_2 = + new StageTimeMeasurement(UUID.randomUUID(), "class", "method", StageResult.ABORT, 2, 4); + + public static Stream equalsProvider() { + final TestRunTelemetry testRunTelemetry = new TestRunTelemetry(MEASUREMENT_1); + return Stream.builder() + .add(Arguments.of(testRunTelemetry, testRunTelemetry, true)) + .add(Arguments.of(testRunTelemetry, new TestRunTelemetry(MEASUREMENT_1), true)) + .add(Arguments.of(testRunTelemetry, new TestRunTelemetry(MEASUREMENT_2), false)) + .add(Arguments.of(testRunTelemetry, null, false)) + .build(); + } + + @Test + void getDurationMillisShouldReturnTheValueSet() { + //given + final TestRunTelemetry underTest = new TestRunTelemetry(MEASUREMENT_1); + + //when + final long actual = underTest.getDurationMillis(); + + //then + assertEquals(MEASUREMENT_1.getDurationMillis(), actual); + } + + @ParameterizedTest + @MethodSource("equalsProvider") + void testEqualsShouldReturnTrueWhenObjectsAreEqual( + final TestRunTelemetry a, + final TestRunTelemetry b, + final boolean expected) { + //given + + //when + final boolean actual = a.equals(b); + + //then + assertEquals(expected, actual); + } + + @ParameterizedTest + @MethodSource("equalsProvider") + void testHashCodeShouldReturnSameHashCodeWhenObjectsAreEqual( + final TestRunTelemetry a, + final TestRunTelemetry b, + final boolean expected) { + //given + + //when + final int aHashCode = Optional.ofNullable(a).map(Objects::hashCode).orElse(0); + final int bHashCode = Optional.ofNullable(b).map(Objects::hashCode).orElse(0); + + //then + assertEquals(expected, aHashCode == bHashCode); + } +} diff --git a/mission-report/flight-evaluation-report/src/main/java/com/github/nagyesta/abortmission/reporting/html/LaunchHtml.java b/mission-report/flight-evaluation-report/src/main/java/com/github/nagyesta/abortmission/reporting/html/LaunchHtml.java index 7165aa7b..7bf51532 100644 --- a/mission-report/flight-evaluation-report/src/main/java/com/github/nagyesta/abortmission/reporting/html/LaunchHtml.java +++ b/mission-report/flight-evaluation-report/src/main/java/com/github/nagyesta/abortmission/reporting/html/LaunchHtml.java @@ -43,7 +43,9 @@ private LaunchHtml(@NonNull final LaunchHtmlBuilder builder) { * @return The hashed and shortened value. */ public static String shortHash(final String displayName) { - return Integer.toString(Math.abs(displayName.hashCode()), HASH_RADIX); + final String hash = Integer.toString(Math.abs(displayName.hashCode()), HASH_RADIX); + final String mask = "00000000"; + return new StringBuilder(mask).replace(mask.length() - hash.length(), mask.length(), hash).toString(); } /** diff --git a/mission-report/flight-evaluation-report/src/test/java/com/github/nagyesta/abortmission/reporting/html/LaunchHtmlTest.java b/mission-report/flight-evaluation-report/src/test/java/com/github/nagyesta/abortmission/reporting/html/LaunchHtmlTest.java index ed6ac02b..0e181b9f 100644 --- a/mission-report/flight-evaluation-report/src/test/java/com/github/nagyesta/abortmission/reporting/html/LaunchHtmlTest.java +++ b/mission-report/flight-evaluation-report/src/test/java/com/github/nagyesta/abortmission/reporting/html/LaunchHtmlTest.java @@ -11,9 +11,9 @@ class LaunchHtmlTest { private static Stream hashProvider() { return Stream.builder() - .add(Arguments.of("input", "2vmlua")) - .add(Arguments.of(LaunchHtml.class.getName(), "5r0q71")) - .add(Arguments.of(LaunchHtmlTest.class.getName(), "1sneltf")) + .add(Arguments.of("input", "002vmlua")) + .add(Arguments.of(LaunchHtml.class.getName(), "005r0q71")) + .add(Arguments.of(LaunchHtmlTest.class.getName(), "01sneltf")) .build(); } diff --git a/mission-report/flight-evaluation-report/src/test/resources/abort-mission-report.txt b/mission-report/flight-evaluation-report/src/test/resources/abort-mission-report.txt index b0e88a46..02840494 100644 --- a/mission-report/flight-evaluation-report/src/test/resources/abort-mission-report.txt +++ b/mission-report/flight-evaluation-report/src/test/resources/abort-mission-report.txt @@ -8,14 +8,14 @@ function expandClass(className){let toShow=document.getElementsByClassName("abort-class-"+className+"-expand");for(let toShowElement of toShow)toShowElement.classList.remove("hidden-class");let toHide=document.getElementsByClassName("abort-class-"+className+"-collapse");for(let toHideElement of toHide)toHideElement.classList.add("hidden-class")} function toggleMethod(className,methodName){let cssClass="abort-class-"+className+"-mission-"+methodName+"-details";let toToggle=document.getElementsByClassName(cssClass);let isHidden=false;for(let toToggleElement of toToggle){toToggleElement.classList.toggle("hidden");isHidden=toToggleElement.classList.contains("hidden")}let indicatorClass="abort-class-"+className+"-mission-"+methodName+"-indicator";let toFlip=document.getElementsByClassName(indicatorClass);for(let toFlipElement of toFlip)if(isHidden)toFlipElement.innerHTML= "\x26#9658;";else toFlipElement.innerHTML="\x26#9660;"}function filterRow(resultName){let cssClass="row-"+resultName;let toToggle=document.getElementsByClassName(cssClass);for(let toToggleElement of toToggle)toToggleElement.classList.toggle("filtered")}; -

Flight Evaluation Report

Countdown started: 2020-11-30 22:54:43

Mission concluded: 2020-11-30 22:54:45

Filter:
► co.gi.na.ab.bo.te.FuelTankTestContext60s 14ms02.372121
▼ co.gi.na.ab.bo.te.FuelTankTestContextTimesResults
Countdown10s 7ms77.071000

Flight Evaluation Report

Countdown started: 2020-11-30 22:54:43

Mission concluded: 2020-11-30 22:54:45

Filter:
► co.gi.na.ab.bo.te.FuelTankTestContext60s 14ms02.372121
▼ co.gi.na.ab.bo.te.FuelTankTestContextTimesResults
Countdown10s 7ms77.071000
testFuelTankShouldFillWhenCalled50s 7ms01.461121
Mission matchers:
  • (1bcvmid) CLASS_MATCHING('.+\.FuelTankTestContext')
Class total (from 22:54:43.429 to 22:54:43.445)60s 14ms02.372121
► co.gi.na.ab.bo.te.ParachuteTestContext110s 4ms00.424160
▼ co.gi.na.ab.bo.te.ParachuteTestContextTimesResults
Countdown10s 2ms22.021000
testFuelTankShouldFillWhenCalled50s 7ms01.461121
Mission matchers:
  • (01bcvmid) CLASS_MATCHING('.+\.FuelTankTestContext')
Class total (from 22:54:43.429 to 22:54:43.445)60s 14ms02.372121
► co.gi.na.ab.bo.te.ParachuteTestContext110s 4ms00.424160
▼ co.gi.na.ab.bo.te.ParachuteTestContextTimesResults
Countdown10s 2ms22.021000
testParachuteShouldOpenWhenCalled100s 2ms00.213160
Mission matchers:
  • (1pvtkkg) CLASS_MATCHING('.+\.ParachuteTestContext')
Class total (from 22:54:43.693 to 22:54:43.701)110s 4ms00.424160
► co.gi.na.ab.bo.te.StaticFireTestWithSideBoosters10s 938ms938938.09380100
▼ co.gi.na.ab.bo.te.StaticFireTestWithSideBoostersTimesResults
Countdown10s 938ms938938.09380100
Countdown matchers:
  • (t12af7) SideBooster
Class total (from 22:54:43.808 to 22:54:44.746)10s 938ms938938.09380100
► co.gi.na.ab.bo.te.StaticFireTestCenterCoreOnly10s 2ms22.021000
▼ co.gi.na.ab.bo.te.StaticFireTestCenterCoreOnlyTimesResults
Countdown00s 0ms00.000000
testIsOnFire10s 2ms22.021000
Class total (from 22:54:45.003 to 22:54:45.005)10s 2ms22.021000
Countdown Subtotal30s 947ms2315.79382100
Missions Subtotal160s 11ms00.765281
Total190s 958ms050.49387381
+Called"onclick="toggleMethod("005u5amv", "019oau5t")">testParachuteShouldOpenWhenCalled
100s 2ms00.213160
Mission matchers:
  • (01pvtkkg) CLASS_MATCHING('.+\.ParachuteTestContext')
Class total (from 22:54:43.693 to 22:54:43.701)110s 4ms00.424160
► co.gi.na.ab.bo.te.StaticFireTestWithSideBoosters10s 938ms938938.09380100
▼ co.gi.na.ab.bo.te.StaticFireTestWithSideBoostersTimesResults
Countdown10s 938ms938938.09380100
Countdown matchers:
  • (00t12af7) SideBooster
Class total (from 22:54:43.808 to 22:54:44.746)10s 938ms938938.09380100
► co.gi.na.ab.bo.te.StaticFireTestCenterCoreOnly10s 2ms22.021000
▼ co.gi.na.ab.bo.te.StaticFireTestCenterCoreOnlyTimesResults
Countdown00s 0ms00.000000
testIsOnFire10s 2ms22.021000
Class total (from 22:54:45.003 to 22:54:45.005)10s 2ms22.021000
Countdown Subtotal30s 947ms2315.79382100
Missions Subtotal160s 11ms00.765281
Total190s 958ms050.49387381