-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-7944: Improve Suppress test coverage #6382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
63c4d7b
efb4407
a724897
5ad02ea
48dabcd
7420fd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,10 +47,7 @@ | |
| import org.junit.experimental.categories.Category; | ||
| import org.junit.runner.RunWith; | ||
| import org.junit.runners.Parameterized; | ||
| import org.junit.runners.Parameterized.Parameters; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Collection; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Properties; | ||
|
|
@@ -92,11 +89,6 @@ public SuppressionDurabilityIntegrationTest(final boolean eosEnabled) { | |
| this.eosEnabled = eosEnabled; | ||
| } | ||
|
|
||
| @Parameters(name = "{index}: eosEnabled={0}") | ||
| public static Collection<Object[]> parameters() { | ||
| return Arrays.asList(new Object[] {false}, new Object[] {true}); | ||
| } | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unused! |
||
| private KTable<String, Long> buildCountsTable(final String input, final StreamsBuilder builder) { | ||
| return builder | ||
| .table( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,16 +32,19 @@ | |
| import org.apache.kafka.streams.kstream.KTable; | ||
| import org.apache.kafka.streams.kstream.Materialized; | ||
| import org.apache.kafka.streams.kstream.Produced; | ||
| import org.apache.kafka.streams.kstream.Suppressed; | ||
| import org.apache.kafka.streams.kstream.Suppressed.BufferConfig; | ||
| import org.apache.kafka.streams.kstream.TimeWindows; | ||
| import org.apache.kafka.streams.kstream.Windowed; | ||
| import org.apache.kafka.streams.state.Stores; | ||
| import org.apache.kafka.streams.state.WindowStore; | ||
| import org.apache.kafka.test.TestUtils; | ||
|
|
||
| import java.time.Duration; | ||
| import java.time.Instant; | ||
| import java.util.Properties; | ||
|
|
||
| import static org.apache.kafka.streams.kstream.Suppressed.untilWindowCloses; | ||
|
|
||
| public class SmokeTestClient extends SmokeTestUtil { | ||
|
|
||
| private final String name; | ||
|
|
@@ -113,7 +116,7 @@ private KafkaStreams createKafkaStreams(final Properties props) { | |
| final Topology build = getTopology(); | ||
| final KafkaStreams streamsClient = new KafkaStreams(build, getStreamsConfig(props)); | ||
| streamsClient.setStateListener((newState, oldState) -> { | ||
| System.out.printf("%s: %s -> %s%n", name, oldState, newState); | ||
| System.out.printf("%s %s: %s -> %s%n", name, Instant.now(), oldState, newState); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added this while debugging the smoke test. Printing the time allows us to correlate events across test nodes and with the test logs.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice find! |
||
| if (oldState == KafkaStreams.State.REBALANCING && newState == KafkaStreams.State.RUNNING) { | ||
| started = true; | ||
| } | ||
|
|
@@ -149,24 +152,22 @@ public Topology getTopology() { | |
| .withRetention(Duration.ofHours(25)) | ||
| ); | ||
|
|
||
| minAggregation | ||
| .toStream() | ||
| .filterNot((k, v) -> k.key().equals("flush")) | ||
| .map((key, value) -> new KeyValue<>(key.toString(), value)) | ||
| .to("min-raw", Produced.with(stringSerde, intSerde)); | ||
| streamify(minAggregation, "min-raw"); | ||
|
|
||
| minAggregation | ||
| .suppress(Suppressed.untilWindowCloses(Suppressed.BufferConfig.unbounded())) | ||
| .toStream() | ||
| .filterNot((k, v) -> k.key().equals("flush")) | ||
| .map((key, value) -> new KeyValue<>(key.toString(), value)) | ||
| .to("min-suppressed", Produced.with(stringSerde, intSerde)); | ||
| streamify(minAggregation.suppress(untilWindowCloses(BufferConfig.unbounded())), "min-suppressed"); | ||
|
|
||
| minAggregation | ||
| .toStream(new Unwindow<>()) | ||
| .filterNot((k, v) -> k.equals("flush")) | ||
| .to("min", Produced.with(stringSerde, intSerde)); | ||
|
|
||
| final KTable<Windowed<String>, Integer> smallWindowSum = groupedData | ||
| .windowedBy(TimeWindows.of(Duration.ofSeconds(2)).advanceBy(Duration.ofSeconds(1)).grace(Duration.ofSeconds(30))) | ||
| .reduce((l, r) -> l + r); | ||
|
|
||
| streamify(smallWindowSum, "sws-raw"); | ||
| streamify(smallWindowSum.suppress(untilWindowCloses(BufferConfig.unbounded())), "sws-suppressed"); | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a "small window sum" stream to the topology, which does a "normal" windowed computation. The exact data production timing is non-deterministic, so we can't verify the results, but we can verify that there is exactly one result per windowed key in the suppressed stream |
||
| final KTable<String, Integer> minTable = builder.table( | ||
| "min", | ||
| Consumed.with(stringSerde, intSerde), | ||
|
|
@@ -250,4 +251,12 @@ public Topology getTopology() { | |
|
|
||
| return builder.build(); | ||
| } | ||
|
|
||
| private static void streamify(final KTable<Windowed<String>, Integer> windowedTable, final String topic) { | ||
| windowedTable | ||
| .toStream() | ||
| .filterNot((k, v) -> k.key().equals("flush")) | ||
| .map((key, value) -> new KeyValue<>(key.toString(), value)) | ||
| .to(topic, Produced.with(stringSerde, intSerde)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,13 +60,14 @@ | |
| import static org.apache.kafka.common.utils.Utils.mkEntry; | ||
|
|
||
| public class SmokeTestDriver extends SmokeTestUtil { | ||
| private static final String[] TOPICS = new String[] { | ||
| private static final String[] TOPICS = { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apparently this is unnecessary now? |
||
| "data", | ||
| "echo", | ||
| "max", | ||
| "min", "min-suppressed", "min-raw", | ||
| "dif", | ||
| "sum", | ||
| "sws-raw", "sws-suppressed", | ||
| "cnt", | ||
| "avg", | ||
| "tagg" | ||
|
|
@@ -80,18 +81,18 @@ private static class ValueList { | |
| private int index; | ||
|
|
||
| ValueList(final int min, final int max) { | ||
| this.key = min + "-" + max; | ||
| key = min + "-" + max; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. little bit of cleanup. |
||
|
|
||
| this.values = new int[max - min + 1]; | ||
| for (int i = 0; i < this.values.length; i++) { | ||
| this.values[i] = min + i; | ||
| values = new int[max - min + 1]; | ||
| for (int i = 0; i < values.length; i++) { | ||
| values[i] = min + i; | ||
| } | ||
| // We want to randomize the order of data to test not completely predictable processing order | ||
| // However, values are also use as a timestamp of the record. (TODO: separate data and timestamp) | ||
| // We keep some correlation of time and order. Thus, the shuffling is done with a sliding window | ||
| shuffle(this.values, 10); | ||
| shuffle(values, 10); | ||
|
|
||
| this.index = 0; | ||
| index = 0; | ||
| } | ||
|
|
||
| int next() { | ||
|
|
@@ -106,6 +107,7 @@ public static String[] topics() { | |
| public static Map<String, Set<Integer>> generate(final String kafka, | ||
| final int numKeys, | ||
| final int maxRecordsPerKey, | ||
| final Duration timeToSpend, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ability to slow down data production so that the system tests can have time to bounce nodes while processing instead of after processing. I happened to examine the smoke-test system test logs, and realized that we were fully processing the results before the first bounce. This totally circumvents the purpose of the test. To get the prior behavior, you can just pass
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good call!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hey @vvcephei I'm wondering if we can simplify the code further: with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of course we can then remove the whole
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, this is an interesting idea. I'll look into it. That's a very good point about the integration test, I'll look at that as well.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, After trying to simplify the code, I found that it was actually better to split the two use cases, so that's what I did. It lets us ditch a whole bunch of bookkeeping when we just want to generate until the process is killed, and it also lets us skip a bunch of mind-bending boolean logic when we do want to just generate a fixed amount of data. I also added a duration of 20 seconds to the integration test, and it didn't increase the run-time of the test, but now we're getting the same guarantee that we get with the system tests now. |
||
| final boolean autoTerminate) { | ||
| final Properties producerProps = new Properties(); | ||
| producerProps.put(ProducerConfig.CLIENT_ID_CONFIG, "SmokeTest"); | ||
|
|
@@ -131,6 +133,8 @@ public static Map<String, Set<Integer>> generate(final String kafka, | |
| remaining = data.length; | ||
| } | ||
|
|
||
| final long recordPauseTime = timeToSpend.toMillis() / numKeys / maxRecordsPerKey; | ||
|
|
||
| List<ProducerRecord<byte[], byte[]>> needRetry = new ArrayList<>(); | ||
|
|
||
| while (remaining > 0) { | ||
|
|
@@ -155,9 +159,9 @@ public static Map<String, Set<Integer>> generate(final String kafka, | |
| numRecordsProduced++; | ||
| allData.get(key).add(value); | ||
| if (numRecordsProduced % 100 == 0) { | ||
| System.out.println(numRecordsProduced + " records produced"); | ||
| System.out.println(Instant.now() + " " + numRecordsProduced + " records produced"); | ||
| } | ||
| Utils.sleep(2); | ||
| Utils.sleep(Math.max(recordPauseTime, 2)); | ||
| } | ||
| } | ||
| producer.flush(); | ||
|
|
@@ -232,12 +236,6 @@ private static void shuffle(final int[] data, @SuppressWarnings("SameParameterVa | |
| } | ||
|
|
||
| public static class NumberDeserializer implements Deserializer<Number> { | ||
|
|
||
| @Override | ||
| public void configure(final Map<String, ?> configs, final boolean isKey) { | ||
|
|
||
| } | ||
|
|
||
| @Override | ||
| public Number deserialize(final String topic, final byte[] data) { | ||
| final Number value; | ||
|
|
@@ -247,6 +245,8 @@ public Number deserialize(final String topic, final byte[] data) { | |
| case "min": | ||
| case "min-raw": | ||
| case "min-suppressed": | ||
| case "sws-raw": | ||
| case "sws-suppressed": | ||
| case "max": | ||
| case "dif": | ||
| value = intSerde.deserializer().deserialize(topic, data); | ||
|
|
@@ -264,11 +264,6 @@ public Number deserialize(final String topic, final byte[] data) { | |
| } | ||
| return value; | ||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
|
|
||
| } | ||
| } | ||
|
|
||
| public static VerificationResult verify(final String kafka, | ||
|
|
@@ -279,6 +274,7 @@ public static VerificationResult verify(final String kafka, | |
| props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka); | ||
| props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); | ||
| props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, NumberDeserializer.class); | ||
| props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); | ||
|
|
||
| final KafkaConsumer<String, Number> consumer = new KafkaConsumer<>(props); | ||
| final List<TopicPartition> partitions = getAllPartitions(consumer, TOPICS); | ||
|
|
@@ -406,7 +402,12 @@ private static VerificationResult verifyAll(final Map<String, Set<Integer>> inpu | |
| boolean pass; | ||
| try (final PrintStream resultStream = new PrintStream(byteArrayOutputStream)) { | ||
| pass = verifyTAgg(resultStream, inputs, events.get("tagg")); | ||
| pass &= verifySuppressed(resultStream, "min-suppressed", inputs, events, SmokeTestDriver::getMin); | ||
| pass &= verifySuppressed(resultStream, "min-suppressed", events); | ||
| pass &= verify(resultStream, "min-suppressed", inputs, events, windowedKey -> { | ||
| final String unwindowedKey = windowedKey.substring(1, windowedKey.length() - 1).replaceAll("@.*", ""); | ||
| return getMin(unwindowedKey); | ||
| }); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decomponsed |
||
| pass &= verifySuppressed(resultStream, "sws-suppressed", events); | ||
| pass &= verify(resultStream, "min", inputs, events, SmokeTestDriver::getMin); | ||
| pass &= verify(resultStream, "max", inputs, events, SmokeTestDriver::getMax); | ||
| pass &= verify(resultStream, "dif", inputs, events, key -> getMax(key).intValue() - getMin(key).intValue()); | ||
|
|
@@ -455,11 +456,10 @@ private static boolean verify(final PrintStream resultStream, | |
| } | ||
|
|
||
|
|
||
| @SuppressWarnings("DynamicRegexReplaceableByCompiledPattern") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, oops. I'll remove it.
vvcephei marked this conversation as resolved.
Outdated
|
||
| private static boolean verifySuppressed(final PrintStream resultStream, | ||
| @SuppressWarnings("SameParameterValue") final String topic, | ||
| final Map<String, Set<Integer>> inputs, | ||
| final Map<String, Map<String, LinkedList<ConsumerRecord<String, Number>>>> events, | ||
| final Function<String, Number> getMin) { | ||
| final Map<String, Map<String, LinkedList<ConsumerRecord<String, Number>>>> events) { | ||
| resultStream.println("verifying suppressed " + topic); | ||
| final Map<String, LinkedList<ConsumerRecord<String, Number>>> topicEvents = events.getOrDefault(topic, emptyMap()); | ||
| for (final Map.Entry<String, LinkedList<ConsumerRecord<String, Number>>> entry : topicEvents.entrySet()) { | ||
|
|
@@ -476,14 +476,11 @@ private static boolean verifySuppressed(final PrintStream resultStream, | |
| return false; | ||
| } | ||
| } | ||
| return verify(resultStream, topic, inputs, events, windowedKey -> { | ||
| final String unwindowedKey = windowedKey.substring(1, windowedKey.length() - 1).replaceAll("@.*", ""); | ||
| return getMin.apply(unwindowedKey); | ||
| }); | ||
| return true; | ||
| } | ||
|
|
||
| private static String indent(@SuppressWarnings("SameParameterValue") final String prefix, | ||
| final LinkedList<ConsumerRecord<String, Number>> list) { | ||
| final Iterable<ConsumerRecord<String, Number>> list) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. relaxing type constraints |
||
| final StringBuilder stringBuilder = new StringBuilder(); | ||
| for (final ConsumerRecord<String, Number> record : list) { | ||
| stringBuilder.append(prefix).append(record).append('\n'); | ||
|
|
@@ -494,13 +491,13 @@ private static String indent(@SuppressWarnings("SameParameterValue") final Strin | |
| private static Long getSum(final String key) { | ||
| final int min = getMin(key).intValue(); | ||
| final int max = getMax(key).intValue(); | ||
| return ((long) min + (long) max) * (max - min + 1L) / 2L; | ||
| return ((long) min + max) * (max - min + 1L) / 2L; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the second cast is unnecessary to prevent overflow |
||
| } | ||
|
|
||
| private static Double getAvg(final String key) { | ||
| final int min = getMin(key).intValue(); | ||
| final int max = getMax(key).intValue(); | ||
| return ((long) min + (long) max) / 2.0; | ||
| return ((long) min + max) / 2.0; | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -554,7 +551,7 @@ private static Number getMax(final String key) { | |
| } | ||
|
|
||
| private static List<TopicPartition> getAllPartitions(final KafkaConsumer<?, ?> consumer, final String... topics) { | ||
| final ArrayList<TopicPartition> partitions = new ArrayList<>(); | ||
| final List<TopicPartition> partitions = new ArrayList<>(); | ||
|
|
||
| for (final String topic : topics) { | ||
| for (final PartitionInfo info : consumer.partitionsFor(topic)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,11 +20,14 @@ | |
| import org.apache.kafka.streams.StreamsConfig; | ||
|
|
||
| import java.io.IOException; | ||
| import java.time.Duration; | ||
| import java.util.Map; | ||
| import java.util.Properties; | ||
| import java.util.Set; | ||
| import java.util.UUID; | ||
|
|
||
| import static org.apache.kafka.streams.tests.SmokeTestDriver.generate; | ||
|
|
||
| public class StreamsSmokeTest { | ||
|
|
||
| /** | ||
|
|
@@ -62,16 +65,23 @@ public static void main(final String[] args) throws InterruptedException, IOExce | |
| final int numKeys = 10; | ||
| final int maxRecordsPerKey = 500; | ||
| if (disableAutoTerminate) { | ||
| SmokeTestDriver.generate(kafka, numKeys, maxRecordsPerKey, false); | ||
| generate(kafka, numKeys, maxRecordsPerKey, Duration.ZERO, false); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since this is just going to run until stopped, there's no need (or any sensible value) for delay. |
||
| } else { | ||
| final Map<String, Set<Integer>> allData = SmokeTestDriver.generate(kafka, numKeys, maxRecordsPerKey, true); | ||
| // slow down data production to span 30 seconds so that system tests have time to | ||
| // do their bounces, etc. | ||
| final Map<String, Set<Integer>> allData = | ||
| generate(kafka, numKeys, maxRecordsPerKey, Duration.ofSeconds(30), true); | ||
| SmokeTestDriver.verify(kafka, allData, maxRecordsPerKey); | ||
| } | ||
| break; | ||
| case "process": | ||
| // this starts a KafkaStreams client | ||
| final SmokeTestClient client = new SmokeTestClient(UUID.randomUUID().toString()); | ||
| client.start(streamsProperties); | ||
| // this starts the stream processing app | ||
| new SmokeTestClient(UUID.randomUUID().toString()).start(streamsProperties); | ||
| break; | ||
| case "process-eos": | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added the ability to run the smoke test app with EOS
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use |
||
| // this starts the stream processing app with EOS | ||
| streamsProperties.setProperty(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE); | ||
| new SmokeTestClient(UUID.randomUUID().toString()).start(streamsProperties); | ||
| break; | ||
| case "close-deadlock-test": | ||
| final ShutdownDeadlockTest test = new ShutdownDeadlockTest(kafka); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -353,6 +353,10 @@ class StreamsSmokeTestJobRunnerService(StreamsSmokeTestBaseService): | |
| def __init__(self, test_context, kafka): | ||
| super(StreamsSmokeTestJobRunnerService, self).__init__(test_context, kafka, "process") | ||
|
|
||
| class StreamsSmokeTestEOSJobRunnerService(StreamsSmokeTestBaseService): | ||
| def __init__(self, test_context, kafka): | ||
| super(StreamsSmokeTestEOSJobRunnerService, self).__init__(test_context, kafka, "process-eos") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me know if there's a better way to do this...
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought there were already existing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To my other comment above: what's the difference of running
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shameful admission: I thought about this, but just didn't take the time to check. The bounce test was obviously duplicated, but the eos test is more complex. I knew I'd have to do a case-by-case check to make sure I didn't remove coverage. I'll do my homework now...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, so the smoke test and the eos test are similar, but not identical. The smoke test application has more features in it, though. So, we have more feature coverage under eos when we test with the smoke test. The eos test evaluates two topologies one with no repartition, and one with a repartition. The smoke test topology contains repartitions, so it only tests with repartition. I think that it should be sufficient to test only with repartition. The eos test verification specifically checks that "all transactions finished" ( WDYT?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think we should still retain this check if we want to merge in the EosTest to SmokeTest, but also if you do not want to drag on it we could keep it as a separate ticket and merge this as-is. Your call :)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, thanks for the explanation. Let's merge this, and I'll create a ticket to follow up by including this check and removing the eos test. That'll help review burden as well, I think. |
||
|
|
||
|
|
||
| class StreamsEosTestDriverService(StreamsEosTestBaseService): | ||
| def __init__(self, test_context, kafka): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More stable and future-proof test setup code. I needed this because I added a second test here, which I've since removed.
Leaving this change, though, so we won't have to waste time debugging again next time we try to add a test.