Skip to content

Commit be1024d

Browse files
authored
Merge pull request #25 from braintrustdata/ark/vcr-wiremock
VCR util for unit tests
2 parents 1b58fef + 59d0a7d commit be1024d

25 files changed

Lines changed: 1090 additions & 935 deletions

scripts/erase-cassettes.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
3+
cd "$(dirname "$(readlink -f "${BASH_SOURCE}")")"/..
4+
5+
rm -rf src/test/resources/cassettes/
6+
mkdir -p src/test/resources/cassettes/

scripts/record-cassettes.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
3+
cd "$(dirname "$(readlink -f "${BASH_SOURCE}")")"/..
4+
5+
export VCR_MODE=record
6+
./gradlew test

src/test/java/dev/braintrust/TestHarness.java

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package dev.braintrust;
22

33
import static org.junit.jupiter.api.Assertions.assertTrue;
4-
import static org.junit.jupiter.api.Assertions.fail;
54

65
import dev.braintrust.api.BraintrustApiClient;
76
import dev.braintrust.config.BraintrustConfig;
@@ -14,7 +13,6 @@
1413
import io.opentelemetry.sdk.OpenTelemetrySdk;
1514
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
1615
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
17-
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
1816
import io.opentelemetry.sdk.trace.SdkTracerProvider;
1917
import io.opentelemetry.sdk.trace.data.SpanData;
2018
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
@@ -27,6 +25,18 @@
2725
import lombok.experimental.Accessors;
2826

2927
public class TestHarness {
28+
private static final VCR vcr;
29+
30+
static {
31+
vcr =
32+
new VCR(
33+
java.util.Map.of(
34+
"https://api.openai.com/v1", "openai",
35+
"https://api.anthropic.com", "anthropic",
36+
"https://generativelanguage.googleapis.com", "google"));
37+
vcr.start();
38+
Runtime.getRuntime().addShutdownHook(new Thread(vcr::stop));
39+
}
3040

3141
public static TestHarness setup() {
3242
return setup(createTestConfig());
@@ -75,13 +85,12 @@ public static synchronized TestHarness setup(BraintrustConfig config) {
7585
@Accessors(fluent = true)
7686
private final Braintrust braintrust;
7787

78-
private final @Nonnull InMemorySpanExporter spanExporter;
88+
private final @Nonnull UnitTestSpanExporter spanExporter;
7989

8090
private TestHarness(@Nonnull Braintrust braintrust) {
8191
this.braintrust = braintrust;
82-
8392
var tracerBuilder = SdkTracerProvider.builder();
84-
this.spanExporter = InMemorySpanExporter.create();
93+
this.spanExporter = new UnitTestSpanExporter();
8594
var loggerBuilder = SdkLoggerProvider.builder();
8695
var meterBuilder = SdkMeterProvider.builder();
8796
braintrust.openTelemetryEnable(tracerBuilder, loggerBuilder, meterBuilder);
@@ -102,6 +111,30 @@ private TestHarness(@Nonnull Braintrust braintrust) {
102111
this.openTelemetry = openTelemetry;
103112
}
104113

114+
public String openAiBaseUrl() {
115+
return vcr.getUrlForTargetBase("https://api.openai.com/v1");
116+
}
117+
118+
public String openAiApiKey() {
119+
return getEnv("OPENAI_API_KEY", "test-key");
120+
}
121+
122+
public String anthropicBaseUrl() {
123+
return vcr.getUrlForTargetBase("https://api.anthropic.com");
124+
}
125+
126+
public String anthropicApiKey() {
127+
return getEnv("ANTHROPIC_API_KEY", "test-key");
128+
}
129+
130+
public String googleBaseUrl() {
131+
return vcr.getUrlForTargetBase("https://generativelanguage.googleapis.com");
132+
}
133+
134+
public String googleApiKey() {
135+
return getEnv("GOOGLE_API_KEY", getEnv("GEMINI_API_KEY", "test-key"));
136+
}
137+
105138
/** flush all pending spans and return all spans which have been exported so far */
106139
public List<SpanData> awaitExportedSpans() {
107140
assertTrue(
@@ -120,21 +153,7 @@ public List<SpanData> awaitExportedSpans() {
120153
*/
121154
@SneakyThrows
122155
public List<SpanData> awaitExportedSpans(int minSpanCount) {
123-
var spans = awaitExportedSpans();
124-
int attempts = 0;
125-
while (spans.size() < minSpanCount) {
126-
attempts++;
127-
if (attempts > 30) {
128-
fail(
129-
String.format(
130-
"Timeout waiting for spans: expected at least %d spans, but got %d"
131-
+ " after %d attempts",
132-
minSpanCount, spans.size(), attempts));
133-
}
134-
Thread.sleep(1000);
135-
spans = awaitExportedSpans();
136-
}
137-
return spans;
156+
return spanExporter.getFinishedSpanItems(minSpanCount);
138157
}
139158

140159
private static BraintrustApiClient.InMemoryImpl createApiClient() {
@@ -163,4 +182,9 @@ public static BraintrustConfig createTestConfig() {
163182
"BRAINTRUST_APP_URL", "https://testhost:3000",
164183
"BRAINTRUST_DEFAULT_PROJECT_NAME", defaultProjectName());
165184
}
185+
186+
private static String getEnv(String envarName, String defaultValue) {
187+
var envar = System.getenv(envarName);
188+
return envar == null ? defaultValue : envar;
189+
}
166190
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package dev.braintrust;
2+
3+
import static org.junit.jupiter.api.Assertions.fail;
4+
5+
import io.opentelemetry.sdk.common.CompletableResultCode;
6+
import io.opentelemetry.sdk.trace.data.SpanData;
7+
import io.opentelemetry.sdk.trace.export.SpanExporter;
8+
import java.util.*;
9+
import java.util.concurrent.ConcurrentLinkedQueue;
10+
import java.util.concurrent.TimeUnit;
11+
import java.util.concurrent.locks.Condition;
12+
import java.util.concurrent.locks.Lock;
13+
import java.util.concurrent.locks.ReentrantLock;
14+
import lombok.SneakyThrows;
15+
16+
/**
17+
* an in memory span exporter which allows for blocking until the exported span count reaches an
18+
* expected min
19+
*/
20+
public class UnitTestSpanExporter implements SpanExporter {
21+
private final Queue<SpanData> finishedSpanItems = new ConcurrentLinkedQueue<>();
22+
private final Lock lock = new ReentrantLock();
23+
private final Condition spansAdded = lock.newCondition();
24+
private boolean isStopped = false;
25+
26+
public UnitTestSpanExporter() {}
27+
28+
@SneakyThrows
29+
public List<SpanData> getFinishedSpanItems(int minSpanCount) {
30+
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
31+
lock.lock();
32+
try {
33+
while (finishedSpanItems.size() < minSpanCount) {
34+
long remainingNanos = deadline - System.nanoTime();
35+
if (remainingNanos <= 0) {
36+
fail(
37+
String.format(
38+
"Timeout waiting for spans: expected at least %d spans, but got"
39+
+ " %d after 30 seconds",
40+
minSpanCount, finishedSpanItems.size()));
41+
}
42+
spansAdded.awaitNanos(remainingNanos);
43+
}
44+
return Collections.unmodifiableList(new ArrayList<>(finishedSpanItems));
45+
} finally {
46+
lock.unlock();
47+
}
48+
}
49+
50+
public List<SpanData> getFinishedSpanItems() {
51+
return Collections.unmodifiableList(new ArrayList<>(finishedSpanItems));
52+
}
53+
54+
public void reset() {
55+
finishedSpanItems.clear();
56+
}
57+
58+
@Override
59+
public CompletableResultCode export(Collection<SpanData> spans) {
60+
if (isStopped) {
61+
return CompletableResultCode.ofFailure();
62+
}
63+
lock.lock();
64+
try {
65+
finishedSpanItems.addAll(spans);
66+
spansAdded.signalAll();
67+
} finally {
68+
lock.unlock();
69+
}
70+
return CompletableResultCode.ofSuccess();
71+
}
72+
73+
@Override
74+
public CompletableResultCode flush() {
75+
return CompletableResultCode.ofSuccess();
76+
}
77+
78+
@Override
79+
public CompletableResultCode shutdown() {
80+
finishedSpanItems.clear();
81+
isStopped = true;
82+
return CompletableResultCode.ofSuccess();
83+
}
84+
}

0 commit comments

Comments
 (0)