tests.rsannotatedtests.rssource304 lines · 10.5 KB · raw

The policy on virtual time: a scripted transport and a clock that sleeps by advancing, so every case is exact and instant.

4use std::cell::{Cell, RefCell};
5use std::collections::VecDeque;
6use std::future::Future;
7use std::pin::pin;
8use std::task::{Context, Poll, Waker};
9use std::time::{Duration, Instant, SystemTime};
11use bytes::Bytes;
12use jev_protocol::{ApiErrorKind, Json, ModelId, Noul, Questions, REQUEST_ID_HEADER};
13
14use crate::{Client, ClientError, Event, HttpRequest, Observer, HttpResponse, Runtime, Transport, TransportError, TransportErrorKind};
15
16enum Step {
17    Reply { status: u16, headers: Vec<(&'static str, &'static str)>, body: String },
18    Fail(TransportErrorKind),

Never answers.

20    Stall,
21}
23fn ok(p: f64, id: &'static str) -> Step {
24    Step::Reply {
25        status: 200,
26        headers: vec![(REQUEST_ID_HEADER, id)],
27        body: format!(
28            r#"{{"model":"jev-1.13.0","answers":{{"q":{{"type":"noul","noul":{p}}}}},"usage":{{"input_tokens":1,"output_tokens":1}}}}"#
29        ),
30    }
31}
32
33fn status(status: u16, headers: Vec<(&'static str, &'static str)>) -> Step {
34    Step::Reply { status, headers, body: r#"{"detail":{"error_type":"e","message":"m"}}"#.into() }
35}
36
37struct Fake {
38    script: RefCell<VecDeque<Step>>,
39    events: RefCell<Vec<String>>,
40    sent: RefCell<Vec<HttpRequest>>,
41    distrusted: Cell<u32>,
42    clock: Cell<Duration>,
43    base: Instant,
44}
45
46struct FakeTransport<'a>(&'a Fake);
47struct FakeRuntime<'a>(&'a Fake);
48struct FakeObserver<'a>(&'a Fake);
49
50impl Observer for FakeObserver<'_> {
51    fn event(&self, event: Event<'_>) {
52        let line = match event {
53            Event::Sending { retry } => format!("send {retry}"),
54            Event::Retrying { attempt, delay, cause, request_id } => {
55                format!("retry after attempt {attempt} in {delay:?} ({cause}) id {request_id:?}")
56            }
57            Event::Redialing { cause } => format!("redial ({cause})"),
58            Event::Answered { request_id, attempts, usage } => {
59                format!("answered id {request_id:?} in {attempts} tokens {}+{}", usage.input_tokens, usage.output_tokens)
60            }
61        };
62        self.0.events.borrow_mut().push(line);
63    }
64}
65
66impl Transport for FakeTransport<'_> {
67    async fn send(&self, request: HttpRequest) -> Result<HttpResponse, TransportError> {
68        self.0.sent.borrow_mut().push(request);
69        let step = self.0.script.borrow_mut().pop_front().expect("the script ran out");
70        match step {
71            Step::Reply { status, headers, body } => {
72                let mut map = http::HeaderMap::new();
73                for (k, v) in headers {
74                    map.insert(k, http::HeaderValue::from_static(v));
75                }
76                Ok(HttpResponse { status, headers: map, body: Bytes::from(body) })
77            }
78            Step::Fail(kind) => Err(TransportError::new(kind, format!("{kind:?}"))),
79            Step::Stall => std::future::pending().await,
80        }
81    }
82
83    fn distrust_connection(&self) {
84        self.0.distrusted.set(self.0.distrusted.get() + 1);
85    }
86}
87
88impl Runtime for FakeRuntime<'_> {
89    fn now(&self) -> Instant {
90        self.0.base + self.0.clock.get()
91    }
92
93    fn wall_clock(&self) -> SystemTime {
94        SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000) + self.0.clock.get()
95    }
96
97    async fn sleep(&self, duration: Duration) {
98        self.0.clock.set(self.0.clock.get() + duration);
99    }
100
101    async fn timeout<F: Future>(&self, duration: Duration, future: F) -> Option<F::Output> {
102        // Fakes either answer at once or never: poll once, and a stall
103        // costs exactly the timeout.
104        let mut future = pin!(future);
105        match future.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
106            Poll::Ready(out) => Some(out),
107            Poll::Pending => {
108                self.0.clock.set(self.0.clock.get() + duration);
109                None
110            }
111        }
112    }
113
114    fn jitter(&self) -> f64 {
115        0.0
116    }
117}
118
119fn run(script: Vec<Step>) -> (Result<crate::Answered, ClientError>, Fake) {
120    let fake = Fake {
121        script: RefCell::new(script.into()),
122        events: RefCell::default(),
123        sent: RefCell::default(),
124        distrusted: Cell::default(),
125        clock: Cell::default(),
126        base: Instant::now(),
127    };
128    let result = {
129        let model = ModelId::pinned("jev-1.13.0").unwrap();
130        let client =
131            Client::new(FakeTransport(&fake), FakeRuntime(&fake), model, "k").unwrap().observed(FakeObserver(&fake));
132        let mut questions = Questions::new();
133        questions.noul("q", Noul::new(Json::text("?"))).unwrap();
134        let state = Json::text("s");
135        let future = pin!(client.ask(&state, &questions));
136        match future.poll(&mut Context::from_waker(Waker::noop())) {
137            Poll::Ready(r) => r,
138            Poll::Pending => panic!("fakes never leave the client pending"),
139        }
140    };
141    (result, fake)
142}
143
144fn retry_counts(fake: &Fake) -> Vec<Option<String>> {
145    fake.sent
146        .borrow()
147        .iter()
148        .map(|r| r.headers.get("x-typesafe-retry-count").map(|v| v.to_str().unwrap().to_owned()))
149        .collect()
150}
151
152#[test]
153fn answers_first_time() {
154    let (r, fake) = run(vec![ok(0.95, "a")]);
155    let a = r.unwrap();
156    assert_eq!((a.attempts, a.request_id.as_deref()), (1, Some("a")));
157    assert_eq!(fake.clock.get(), Duration::ZERO);
158    assert!(fake.sent.borrow()[0].headers[http::header::AUTHORIZATION].is_sensitive());
159}
160
161#[test]
162fn honours_the_servers_delay() {
163    let (r, fake) = run(vec![status(429, vec![("retry-after-ms", "300")]), ok(0.95, "b")]);
164    assert_eq!(r.unwrap().attempts, 2);
165    assert_eq!(fake.clock.get(), Duration::from_millis(300));
166    assert_eq!(retry_counts(&fake), vec![None, Some("1".into())]);
167}
168
169#[test]
170fn backs_off_then_gives_up_with_the_last_request_id() {
171    let (r, fake) = run(vec![
172        status(503, vec![(REQUEST_ID_HEADER, "r1")]),
173        status(503, vec![(REQUEST_ID_HEADER, "r2")]),
174        status(503, vec![(REQUEST_ID_HEADER, "r3")]),
175    ]);
176    match r.unwrap_err() {
177        ClientError::Api { error, attempts } => {
178            assert_eq!((error.status, attempts, error.request_id.as_deref()), (503, 3, Some("r3")));
179        }
180        other => panic!("{other:?}"),
181    }
182    assert_eq!(fake.clock.get(), Duration::from_millis(1500));
183    assert_eq!(retry_counts(&fake), vec![None, Some("1".into()), Some("2".into())]);
184}
185
186#[test]
187fn other_4xx_fail_at_once() {
188    let (r, fake) = run(vec![status(403, vec![])]);
189    assert!(matches!(r.unwrap_err(), ClientError::Api { attempts: 1, .. }));
190    assert_eq!(fake.sent.borrow().len(), 1);
191}
192
193#[test]
194fn context_overflow_is_not_resent() {
195    let (r, _) = run(vec![Step::Reply {
196        status: 400,
197        headers: vec![],
198        body: r#"{"detail":{"error_type":"max_tokens_exceeded","message":"too long"}}"#.into(),
199    }]);
200    match r.unwrap_err() {
201        ClientError::Api { error, .. } => assert_eq!(error.kind, ApiErrorKind::ContextOverflow),
202        other => panic!("{other:?}"),
203    }
204}
205
206#[test]
207fn never_sent_is_redialled_not_retried() {
208    let (r, fake) = run(vec![Step::Fail(TransportErrorKind::NotSent), ok(0.95, "c")]);
209    assert_eq!(r.unwrap().attempts, 1);
210    assert_eq!(fake.clock.get(), Duration::ZERO, "no backoff");
211    assert_eq!(retry_counts(&fake), vec![None, None], "not a retry");
212}
213
214#[test]
215fn repeated_never_sent_becomes_a_retry() {
216    use TransportErrorKind::NotSent;
217    let (r, fake) = run(vec![Step::Fail(NotSent), Step::Fail(NotSent), Step::Fail(NotSent), ok(0.95, "d")]);
218    assert_eq!(r.unwrap().attempts, 2);
219    assert_eq!(retry_counts(&fake), vec![None, None, None, Some("1".into())]);
220}
221
222#[test]
223fn unretryable_transport_failures_fail_at_once() {
224    for kind in [TransportErrorKind::Tls, TransportErrorKind::TooLarge, TransportErrorKind::Config] {
225        let (r, fake) = run(vec![Step::Fail(kind)]);
226        assert!(matches!(r.unwrap_err(), ClientError::Transport { attempts: 1, .. }), "{kind:?}");
227        assert_eq!(fake.sent.borrow().len(), 1, "{kind:?}");
228    }
229}
230
231#[test]
232fn a_stalled_attempt_times_out_distrusts_the_connection_and_retries() {
233    let (r, fake) = run(vec![Step::Stall, ok(0.95, "e")]);
234    assert_eq!(r.unwrap().attempts, 2);
235    assert_eq!(fake.distrusted.get(), 1);
236    assert_eq!(fake.clock.get(), Duration::from_millis(10_500));
237}
238
239#[test]
240fn the_budget_bounds_everything() {
241    let (r, fake) = run(vec![Step::Stall, Step::Stall, Step::Stall]);
242    // 10 s, 0.5 s backoff, 10 s, 1 s backoff, then the 8.5 s left.
243    assert!(matches!(r.unwrap_err(), ClientError::TimedOut { attempts: 3, .. }));
244    assert_eq!(fake.clock.get(), Duration::from_secs(30));
245}
246
247#[test]
248fn transport_errors_keep_an_earlier_request_id() {
249    use TransportErrorKind::Interrupted;
250    let (r, _) = run(vec![
251        status(503, vec![(REQUEST_ID_HEADER, "seen")]),
252        Step::Fail(Interrupted),
253        Step::Fail(Interrupted),
254    ]);
255    let e = r.unwrap_err();
256    assert_eq!(e.request_id(), Some("seen"));
257    assert!(e.to_string().ends_with("[request seen]"), "{e}");
258}
259
260#[test]
261fn a_wrong_answer_carries_its_request_id() {
262    let (r, _) = run(vec![Step::Reply {
263        status: 200,
264        headers: vec![(REQUEST_ID_HEADER, "m")],
265        body: r#"{"model":"jev-9.9.9","answers":{"q":{"type":"noul","noul":0.5}},"usage":{"input_tokens":1,"output_tokens":1}}"#.into(),
266    }]);
267    let e = r.unwrap_err();
268    assert!(matches!(e, ClientError::Response { .. }));
269    assert_eq!(e.request_id(), Some("m"));
270}
271
272fn events(fake: &Fake) -> Vec<String> {
273    fake.events.borrow().clone()
274}
275
276#[test]
277fn a_success_reports_its_request_id_and_usage() {
278    let (_, fake) = run(vec![ok(0.95, "a")]);
279    assert_eq!(events(&fake), ["send 0", r#"answered id Some("a") in 1 tokens 1+1"#]);
280}
281
282#[test]
283fn a_retry_reports_its_cause_delay_and_request_id() {
284    let (_, fake) = run(vec![
285        status(503, vec![(REQUEST_ID_HEADER, "r1")]),
286        Step::Stall,
287        ok(0.95, "b"),
288    ]);
289    let e = events(&fake);
290    assert_eq!(e.len(), 6, "{e:?}");
291    assert_eq!(e[0], "send 0");
292    assert!(e[1].starts_with("retry after attempt 1 in 500ms (Jev answered HTTP 503"), "{}", e[1]);
293    assert!(e[1].ends_with(r#"id Some("r1")"#), "{}", e[1]);
294    assert_eq!(e[2], "send 1");
295    assert!(e[3].starts_with("retry after attempt 2 in 1s (attempt timed out"), "{}", e[3]);
296    assert_eq!(e[4], "send 2");
297    assert_eq!(e[5], r#"answered id Some("b") in 3 tokens 1+1"#);
298}
299
300#[test]
301fn a_redial_is_reported_and_is_not_a_retry() {
302    let (_, fake) = run(vec![Step::Fail(TransportErrorKind::NotSent), ok(0.95, "c")]);
303    assert_eq!(events(&fake), ["send 0", "redial (NotSent)", "send 0", r#"answered id Some("c") in 1 tokens 1+1"#]);
304}