use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use ytsaurus_client::{Client, ClientError, RetryPolicy};
const PATH: &str = "//tmp/f";
fn file_of(len: usize) -> Vec<u8> {
(0..len).map(|n| (n % 256) as u8).collect()
}
#[test]
fn a_file_comes_back_byte_for_byte_and_is_checked_against_the_recorded_size() {
let contents = file_of(3000);
let stub = FileStub::serving(contents.clone(), Size::Bytes(3000));
let read = stub.client().read_file(PATH).expect("reads");
assert_eq!(read, contents, "the bytes are not what the proxy sent");
assert_eq!(
stub.request_lines(),
["GET /api/v4/read_file HTTP/1.1", "GET /api/v4/get HTTP/1.1",]
);
let heads = stub.heads();
assert!(
parameters(&heads[1]).contains(r#"path="//tmp/f/@uncompressed_data_size""#),
"the check asks about a different attribute than the documented one:\n{}",
heads[1]
);
assert_eq!(
stub.connections(),
1,
"the size check opened a fresh connection, so the file's body was not consumed"
);
}
#[test]
fn the_read_is_a_get_with_the_path_and_nothing_else() {
let stub = FileStub::serving(b"x".to_vec(), Size::Bytes(1));
stub.client().read_file(PATH).expect("reads");
let heads = stub.heads();
assert!(
heads[0].starts_with("GET /api/v4/read_file HTTP/1.1"),
"{}",
heads[0]
);
assert_eq!(parameters(&heads[0]), r#"{path="//tmp/f"}"#);
}
#[test]
fn a_clean_short_body_is_an_error_rather_than_a_shorter_file() {
let stub = FileStub::serving(file_of(100), Size::Bytes(4096));
let error = stub
.client()
.read_file(PATH)
.expect_err("a 100-byte body is not a 4096-byte file");
assert!(matches!(error, ClientError::Decode { .. }), "{error:?}");
let rendered = error.to_string();
assert!(
rendered.contains("4096") && rendered.contains("100"),
"the error names neither size: {rendered}"
);
assert!(
rendered.contains("trailer"),
"the error does not say why the client cannot know more: {rendered}"
);
}
#[test]
fn a_size_the_cluster_cannot_answer_fails_the_read_rather_than_skipping_the_check() {
let stub = FileStub::serving(file_of(100), Size::NotAnInteger);
let error = stub
.client()
.read_file(PATH)
.expect_err("a size that is not an integer cannot check anything");
assert!(matches!(error, ClientError::Decode { .. }), "{error:?}");
assert!(
error.to_string().contains("uncompressed_data_size"),
"the error does not name the attribute that failed it: {error}"
);
}
#[test]
fn a_size_check_that_never_got_an_answer_is_reported_as_the_read_it_belongs_to() {
let stub = FileStub::serving(file_of(100), Size::Unanswerable);
let error = stub
.client()
.read_file(PATH)
.expect_err("the size the body was to be checked against never arrived");
let rendered = error.to_string();
assert!(
rendered.starts_with("read_file"),
"the caller is sent after a command they never sent: {rendered}"
);
assert!(
rendered.contains("500"),
"the underlying failure was swallowed rather than quoted: {rendered}"
);
}
#[test]
fn an_empty_file_reads_back_empty() {
let stub = FileStub::serving(Vec::new(), Size::Bytes(0));
let read = stub.client().read_file(PATH).expect("reads");
assert!(
read.is_empty(),
"{} bytes appeared from nowhere",
read.len()
);
}
#[test]
fn the_streaming_read_hands_back_the_bytes_and_asks_nothing_else() {
let contents = file_of(3000);
let stub = FileStub::serving(contents.clone(), Size::Bytes(3000));
let mut reader = stub
.client()
.read_file_streaming(PATH)
.expect("opens the stream");
let mut read = Vec::new();
reader.read_to_end(&mut read).expect("reads");
assert_eq!(read, contents, "the bytes are not what the proxy sent");
assert_eq!(reader.bytes_read(), contents.len() as u64);
assert_eq!(
stub.request_lines(),
["GET /api/v4/read_file HTTP/1.1"],
"the streaming path asked something the buffered path asks"
);
}
enum Size {
Bytes(i64),
NotAnInteger,
Unanswerable,
}
impl Size {
fn reply(&self) -> (&'static str, String) {
match self {
Size::Bytes(n) => ("200 OK", format!(r#"{{"value"={n}}}"#)),
Size::NotAnInteger => ("200 OK", r#"{"value"=%true}"#.to_owned()),
Size::Unanswerable => ("500 Internal Server Error", String::new()),
}
}
}
struct FileStub {
address: std::net::SocketAddr,
heads: Arc<Mutex<Vec<String>>>,
connections: Arc<Mutex<usize>>,
}
impl FileStub {
fn serving(file: Vec<u8>, size: Size) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("binds");
let address = listener.local_addr().expect("has an address");
let heads = Arc::new(Mutex::new(Vec::new()));
let connections = Arc::new(Mutex::new(0_usize));
let file = Arc::new(file);
let size = Arc::new(size.reply());
let seen = Arc::clone(&heads);
let counted = Arc::clone(&connections);
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(stream) = stream else { return };
*counted.lock().expect("not poisoned") += 1;
let file = Arc::clone(&file);
let size = Arc::clone(&size);
let seen = Arc::clone(&seen);
std::thread::spawn(move || serve(&stream, &file, &size, &seen));
}
});
Self {
address,
heads,
connections,
}
}
fn client(&self) -> Client {
Client::new(&format!("http://{}", self.address)).with_retries(RetryPolicy::none())
}
fn heads(&self) -> Vec<String> {
self.heads.lock().expect("not poisoned").clone()
}
fn request_lines(&self) -> Vec<String> {
self.heads()
.iter()
.map(|head| head.lines().next().unwrap_or_default().to_owned())
.collect()
}
fn connections(&self) -> usize {
*self.connections.lock().expect("not poisoned")
}
}
fn serve(
stream: &TcpStream,
file: &[u8],
size: &(&'static str, String),
seen: &Mutex<Vec<String>>,
) {
stream
.set_read_timeout(Some(Duration::from_secs(30)))
.expect("sets a timeout");
let mut writer = stream.try_clone().expect("clones");
let mut reader = BufReader::new(stream.try_clone().expect("clones"));
while let Some(head) = read_request(&mut reader) {
let (status, body): (&str, &[u8]) = if head.starts_with("GET /api/v4/read_file ") {
("200 OK", file)
} else {
(size.0, size.1.as_bytes())
};
seen.lock().expect("not poisoned").push(head);
let reply = format!(
"HTTP/1.1 {status}\r\nContent-Length: {}\r\nContent-Type: application/octet-stream\r\n\r\n",
body.len()
);
if writer.write_all(reply.as_bytes()).is_err() || writer.write_all(body).is_err() {
return;
}
writer.flush().ok();
}
}
fn read_request(reader: &mut BufReader<TcpStream>) -> Option<String> {
let mut head = String::new();
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => return None,
Ok(_) if line == "\r\n" => break,
Ok(_) => head.push_str(&line),
}
}
if let Some(length) = header(&head, "content-length").and_then(|v| v.parse().ok()) {
let mut body = vec![0_u8; length];
reader.read_exact(&mut body).ok()?;
}
Some(head)
}
fn parameters(head: &str) -> String {
header(head, "x-yt-parameters").unwrap_or_default()
}
fn header(head: &str, name: &str) -> Option<String> {
head.lines()
.find(|line| {
line.to_lowercase()
.starts_with(&format!("{}:", name.to_lowercase()))
})
.map(|line| line[line.find(':').unwrap_or(0) + 1..].trim().to_owned())
}