java-topology/defects/pulsar/unit/PulsarGetTopicsResultTest.java

171 lines
7.1 KiB
Java

package unit;
import java.util.*;
/**
* pulsar-0001: GetTopicsResult.getTopics() — ArrayList.contains() in dedup for loop.
*
* grouped is ArrayList<String>; .contains(partitionedTopic) is O(N) per call.
* For N topics, this is O(N^2). Fix: use LinkedHashSet for O(N) dedup.
*/
public class PulsarGetTopicsResultTest {
// Slow path: exact replica of GetTopicsResult.getTopics() logic
// Returns (dedupedList, opCount)
static Object[] slowGetTopics(List<String> nonPartitionedOrPartitionTopics) {
List<String> grouped = new ArrayList<>();
long ops = 0;
for (String topic : nonPartitionedOrPartitionTopics) {
// Simulate getPartitionedTopicName: strip "-partition-N" suffix
String partitionedTopic = stripPartitionSuffix(topic);
// grouped.contains: O(N) scan of ArrayList
boolean found = false;
for (String g : grouped) {
ops++;
if (g.equals(partitionedTopic)) { found = true; break; }
}
if (!found) {
grouped.add(partitionedTopic);
}
}
return new Object[]{grouped, ops};
}
// Fast path: LinkedHashSet for O(1) dedup, preserving insertion order
static Object[] fastGetTopics(List<String> nonPartitionedOrPartitionTopics) {
LinkedHashSet<String> grouped = new LinkedHashSet<>();
long ops = 0;
for (String topic : nonPartitionedOrPartitionTopics) {
String partitionedTopic = stripPartitionSuffix(topic);
ops++; // O(1) HashSet.add (dedup is implicit)
grouped.add(partitionedTopic);
}
return new Object[]{new ArrayList<>(grouped), ops};
}
static String stripPartitionSuffix(String topic) {
int idx = topic.lastIndexOf("-partition-");
if (idx >= 0) return topic.substring(0, idx);
return topic;
}
// Generate a list of partition topics for a given base topic
static List<String> makePartitions(String baseTopic, int numPartitions) {
List<String> result = new ArrayList<>();
for (int i = 0; i < numPartitions; i++) {
result.add(baseTopic + "-partition-" + i);
}
return result;
}
public static void main(String[] args) {
int passed = 0;
int total = 0;
// Test 1: correctness — single topic with partitions
{
total++;
List<String> inputs = makePartitions("persistent://tenant/ns/my-topic", 5);
@SuppressWarnings("unchecked")
List<String> slowResult = (List<String>) slowGetTopics(inputs)[0];
@SuppressWarnings("unchecked")
List<String> fastResult = (List<String>) fastGetTopics(inputs)[0];
assert slowResult.equals(fastResult)
: "Single-topic: slow and fast must agree. slow=" + slowResult + " fast=" + fastResult;
assert slowResult.size() == 1
: "Should deduplicate to 1 topic, got " + slowResult.size();
assert slowResult.get(0).equals("persistent://tenant/ns/my-topic")
: "Wrong base topic: " + slowResult.get(0);
System.out.println(" Test 1 PASS: single topic deduplicated to " + slowResult);
passed++;
}
// Test 2: correctness — multiple topics, mixed partitioned and non-partitioned
{
total++;
List<String> inputs = new ArrayList<>();
inputs.addAll(makePartitions("topic-A", 3));
inputs.add("topic-B"); // non-partitioned
inputs.addAll(makePartitions("topic-C", 2));
@SuppressWarnings("unchecked")
List<String> slowResult = (List<String>) slowGetTopics(inputs)[0];
@SuppressWarnings("unchecked")
List<String> fastResult = (List<String>) fastGetTopics(inputs)[0];
assert slowResult.equals(fastResult)
: "Multi-topic: slow and fast must agree. slow=" + slowResult + " fast=" + fastResult;
assert slowResult.size() == 3
: "Should have 3 unique topics, got " + slowResult.size() + ": " + slowResult;
assert slowResult.contains("topic-A") : "Must contain topic-A";
assert slowResult.contains("topic-B") : "Must contain topic-B";
assert slowResult.contains("topic-C") : "Must contain topic-C";
System.out.println(" Test 2 PASS: multi-topic case, result=" + slowResult);
passed++;
}
// Test 3: O(N^2) vs O(N) — op count scaling
{
total++;
int topicsCount = 50;
int partitionsPerTopic = 20; // 50 * 20 = 1000 inputs
List<String> inputs = new ArrayList<>();
for (int t = 0; t < topicsCount; t++) {
inputs.addAll(makePartitions("topic-" + t, partitionsPerTopic));
}
Object[] slowOut = slowGetTopics(inputs);
Object[] fastOut = fastGetTopics(inputs);
@SuppressWarnings("unchecked")
List<String> slowResult = (List<String>) slowOut[0];
@SuppressWarnings("unchecked")
List<String> fastResult = (List<String>) fastOut[0];
long slowOps = (Long) slowOut[1];
long fastOps = (Long) fastOut[1];
assert slowResult.equals(fastResult)
: "Scaling test: slow and fast must agree on results";
assert slowOps > fastOps
: "Slow must do more ops: slowOps=" + slowOps + " fastOps=" + fastOps;
assert slowResult.size() == topicsCount
: "Must deduplicate to " + topicsCount + " topics, got " + slowResult.size();
long speedup = slowOps / Math.max(fastOps, 1);
System.out.println(" Test 3 PASS: N=" + inputs.size() + " inputs → slowOps=" + slowOps
+ " fastOps=" + fastOps + " speedup=" + speedup + "x");
passed++;
}
// Test 4: worst-case O(N^2) — all unique topics (no dedup possible)
{
total++;
int N = 100;
// Each input is unique (already a base topic name, no partitions)
List<String> inputs = new ArrayList<>();
for (int i = 0; i < N; i++) inputs.add("unique-topic-" + i);
long expectedSlowOps = 0;
// After adding k elements, contains() scans k elements
// Total: 0 + 1 + 2 + ... + (N-1) = N*(N-1)/2
for (int k = 0; k < N; k++) expectedSlowOps += k;
Object[] slowOut = slowGetTopics(inputs);
long actualSlowOps = (Long) slowOut[1];
assert actualSlowOps == expectedSlowOps
: "Expected " + expectedSlowOps + " slow ops for unique N=" + N + " inputs, got " + actualSlowOps;
long fastOps = N;
long speedup = actualSlowOps / Math.max(fastOps, 1);
System.out.println(" Test 4 PASS: O(N*(N-1)/2)=" + actualSlowOps + " vs O(N)=" + fastOps
+ " speedup=" + speedup + "x (N=" + N + ")");
passed++;
}
System.out.println(passed + "/" + total + " PASS");
if (passed != total) System.exit(1);
}
}