From bc6c3109d73039af1f2288a39c103d9234d70967 Mon Sep 17 00:00:00 2001 From: Daniel Heppner Date: Tue, 19 Nov 2024 16:17:56 -0800 Subject: [PATCH] adds more specific smoke tests to be used on local deployments --- .../smoketest/SeattleSmokeTest.java | 1 + .../opentripplanner/smoketest/SmokeTest.java | 2 +- .../smoketest/SoundTransitSmokeTest.java | 50 +++++ .../smoketest/util/SmokeTestItinerary.java | 177 ++++++++++++++++++ 4 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 application/src/test/java/org/opentripplanner/smoketest/SoundTransitSmokeTest.java create mode 100644 application/src/test/java/org/opentripplanner/smoketest/util/SmokeTestItinerary.java diff --git a/application/src/test/java/org/opentripplanner/smoketest/SeattleSmokeTest.java b/application/src/test/java/org/opentripplanner/smoketest/SeattleSmokeTest.java index de03e2a4121..60778bc8814 100644 --- a/application/src/test/java/org/opentripplanner/smoketest/SeattleSmokeTest.java +++ b/application/src/test/java/org/opentripplanner/smoketest/SeattleSmokeTest.java @@ -36,6 +36,7 @@ public class SeattleSmokeTest { private static final String CCSWW_ROUTE = "Volunteer Services: Northwest"; + static final Coordinate LYNNWOOD_STA = new Coordinate(47.8154272,-122.2940715); static final Coordinate SODO = new Coordinate(47.5811, -122.3290); static final Coordinate CLYDE_HILL = new Coordinate(47.6316, -122.2173); diff --git a/application/src/test/java/org/opentripplanner/smoketest/SmokeTest.java b/application/src/test/java/org/opentripplanner/smoketest/SmokeTest.java index 3fb8de1931b..0966e440179 100644 --- a/application/src/test/java/org/opentripplanner/smoketest/SmokeTest.java +++ b/application/src/test/java/org/opentripplanner/smoketest/SmokeTest.java @@ -36,7 +36,7 @@ public class SmokeTest { public static final OtpApiClient API_CLIENT = new OtpApiClient( ZoneId.of("America/New_York"), - "http://localhost:8080" + System.getenv().getOrDefault("OTP_API_URL", "https://sound-transit-qa-otp.ibi-transit.com/") ); /** diff --git a/application/src/test/java/org/opentripplanner/smoketest/SoundTransitSmokeTest.java b/application/src/test/java/org/opentripplanner/smoketest/SoundTransitSmokeTest.java new file mode 100644 index 00000000000..12abc982a60 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/smoketest/SoundTransitSmokeTest.java @@ -0,0 +1,50 @@ +package org.opentripplanner.smoketest; + +import static org.opentripplanner.client.model.RequestMode.TRANSIT; +import static org.opentripplanner.client.model.RequestMode.WALK; +import static org.opentripplanner.smoketest.SeattleSmokeTest.LYNNWOOD_STA; +import static org.opentripplanner.smoketest.SeattleSmokeTest.OLIVE_WAY; +import static org.opentripplanner.smoketest.SeattleSmokeTest.SHORELINE; +import static org.opentripplanner.smoketest.SeattleSmokeTest.SODO; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.opentripplanner.smoketest.util.SmokeTestItinerary; +import org.opentripplanner.smoketest.util.SmokeTestRequest; + +@Tag("smoke-test") +@Tag("soundtransit") +public class SoundTransitSmokeTest { + + @Test + public void testItinerary() { + var modes = Set.of(TRANSIT, WALK); + var plan = SmokeTest.basicRouteTest( + new SmokeTestRequest(OLIVE_WAY, SHORELINE, modes), + List.of("WALK", "BUS", "WALK") + ); + + SmokeTestItinerary + .from(plan) + .hasLeg() + .withMode("BUS") + .withRouteShortName("E Line") + .withFarePrice(2.75f) + .assertMatches(); + + plan = SmokeTest.basicRouteTest( + new SmokeTestRequest(LYNNWOOD_STA, SODO, modes), + List.of("WALK", "TRAM", "WALK") + ); + + SmokeTestItinerary + .from(plan) + .hasLeg() + .withMode("TRAM") + .withRouteShortName("1 Line") + .withFarePrice(3f) + .assertMatches(); + } +} diff --git a/application/src/test/java/org/opentripplanner/smoketest/util/SmokeTestItinerary.java b/application/src/test/java/org/opentripplanner/smoketest/util/SmokeTestItinerary.java new file mode 100644 index 00000000000..5aca82172a7 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/smoketest/util/SmokeTestItinerary.java @@ -0,0 +1,177 @@ +package org.opentripplanner.smoketest.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentripplanner.client.model.Itinerary; +import org.opentripplanner.client.model.Leg; +import org.opentripplanner.client.model.TripPlan; + +public class SmokeTestItinerary { + + private final List legMatchers = new ArrayList<>(); + private final TripPlan tripPlan; + + public SmokeTestItinerary(TripPlan tripPlan) { + this.tripPlan = tripPlan; + } + + public static SmokeTestItinerary from(TripPlan tripPlan) { + return new SmokeTestItinerary(tripPlan); + } + + public SmokeTestItinerary hasLeg() { + legMatchers.add(new LegMatcher()); + return this; + } + + public SmokeTestItinerary withRouteShortName(String shortName) { + getCurrentMatcher().routeShortName = shortName; + return this; + } + + public SmokeTestItinerary withFarePrice(float price) { + getCurrentMatcher().farePrice = price; + return this; + } + + public SmokeTestItinerary withMode(String mode) { + getCurrentMatcher().mode = mode; + return this; + } + + public void assertMatches() { + for (LegMatcher matcher : legMatchers) { + List partialMatches = new ArrayList<>(); + boolean foundMatch = false; + + for (Itinerary itinerary : tripPlan.itineraries()) { + for (Leg leg : itinerary.legs()) { + MatchResult result = matcher.matches(leg); + if (result.isFullMatch()) { + foundMatch = true; + break; + } else if (result.hasAnyMatch()) { + partialMatches.add(new PartialMatch(leg, result)); + } + } + if (foundMatch) break; + } + + if (!foundMatch) { + StringBuilder error = new StringBuilder("No leg found matching all criteria:\n"); + error.append(matcher.describeCriteria()).append("\n\n"); + + if (!partialMatches.isEmpty()) { + error.append("Partial matches found:\n"); + for (PartialMatch match : partialMatches) { + error + .append("- Leg with ") + .append(match.result.getMatchingCriteria()) + .append(" but missing ") + .append(match.result.getMissingCriteria()) + .append("\n"); + } + } + throw new AssertionError(error.toString()); + } + } + } + + private LegMatcher getCurrentMatcher() { + if (legMatchers.isEmpty()) { + throw new IllegalStateException("Call hasLeg() first before adding criteria"); + } + return legMatchers.get(legMatchers.size() - 1); + } + + private static class MatchResult { + + private final Map criteria = new HashMap<>(); + + void addCriterion(String name, boolean matched) { + criteria.put(name, matched); + } + + boolean isFullMatch() { + return criteria.values().stream().allMatch(v -> v); + } + + boolean hasAnyMatch() { + return criteria.values().stream().anyMatch(v -> v); + } + + String getMatchingCriteria() { + return criteria + .entrySet() + .stream() + .filter(Map.Entry::getValue) + .map(Map.Entry::getKey) + .collect(Collectors.joining(", ")); + } + + String getMissingCriteria() { + return criteria + .entrySet() + .stream() + .filter(e -> !e.getValue()) + .map(Map.Entry::getKey) + .collect(Collectors.joining(", ")); + } + } + + private static class PartialMatch { + + private final Leg leg; + private final MatchResult result; + + PartialMatch(Leg leg, MatchResult result) { + this.leg = leg; + this.result = result; + } + } + + private static class LegMatcher { + + String routeShortName; + float farePrice; + String mode; + + MatchResult matches(Leg leg) { + MatchResult result = new MatchResult(); + + if (routeShortName != null) { + boolean matches = + leg.isTransit() && + leg.route().shortName().isPresent() && + leg.route().shortName().get().equals(routeShortName); + result.addCriterion("route '" + routeShortName + "'", matches); + } + + if (farePrice > 0) { + boolean matches = leg + .fareProducts() + .stream() + .anyMatch(fp -> fp.product().price().amount().floatValue() == farePrice); + result.addCriterion("fare $" + farePrice, matches); + } + + if (mode != null) { + boolean matches = leg.mode().toString().equals(mode); + result.addCriterion("mode " + mode, matches); + } + + return result; + } + + String describeCriteria() { + List criteria = new ArrayList<>(); + if (routeShortName != null) criteria.add("route '" + routeShortName + "'"); + if (farePrice > 0) criteria.add("fare $" + farePrice); + if (mode != null) criteria.add("mode " + mode); + return String.join(", ", criteria); + } + } +}