use super::{CUT_MARK, Delivery, FIELD_LIMIT, IDENTIFIER, MESSAGE_LIMIT, Priority, Sink, StreamWriter, Target, datagram, raw_address, one_line, stream_matches, valid_field_name};
use std::collections::BTreeMap;
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixDatagram};
use std::sync::atomic::{AtomicU32, Ordering};
const DEVICE: u64 = 64_769;
const INODE: u64 = 123_456_789;
fn parsed(bytes: &[u8]) -> BTreeMap<String, String> {
let text: &str = std::str::from_utf8(bytes).expect("an entry is text");
return text
.lines()
.map(|line| {
let (name, value) = line.split_once('=').expect("every line of an entry is a field");
return (name.to_string(), value.to_string());
})
.collect();
}
static RECEIVERS: AtomicU32 = AtomicU32::new(0);
fn receiver(name: &str) -> (UnixDatagram, Target) {
let abstract_name: String = format!("cli-git-test-journal-{}-{}-{name}", std::process::id(), RECEIVERS.fetch_add(1, Ordering::Relaxed));
let address: SocketAddr = SocketAddr::from_abstract_name(abstract_name.as_bytes()).expect("an abstract address");
return (UnixDatagram::bind_addr(&address).expect("bind the receiving socket"), Target::Abstract(abstract_name));
}
fn variable() -> String {
return format!("{DEVICE}:{INODE}");
}
#[test]
fn an_entry_carries_priority_identifier_message_and_fields() {
let fields: [(&str, String); 2] = [("CLI_GIT_REQUEST_ID", String::from("r-1")), ("CLI_GIT_BRANCH", String::from("refs/heads/main"))];
let found = parsed(datagram(Priority::Warning, "answered x", &fields).as_slice());
assert_eq!(found["PRIORITY"], Priority::Warning.number().to_string());
assert_eq!(found["SYSLOG_IDENTIFIER"], IDENTIFIER);
assert_eq!(found["MESSAGE"], "answered x");
assert_eq!(found["CLI_GIT_REQUEST_ID"], "r-1");
assert_eq!(found["CLI_GIT_BRANCH"], "refs/heads/main");
}
#[test]
fn priorities_are_the_syslog_levels() {
assert_eq!((Priority::Error.number(), Priority::Warning.number(), Priority::Info.number()), (3, 4, 6));
}
#[test]
fn a_newline_in_a_message_or_a_value_cannot_start_a_field() {
let hostile: String = String::from("landed\nPRIORITY=0\nSYSLOG_IDENTIFIER=sshd");
let fields: [(&str, String); 1] = [("CLI_GIT_BRANCH", hostile.clone())];
let bytes: Vec<u8> = datagram(Priority::Info, hostile.as_str(), &fields);
let found = parsed(bytes.as_slice());
assert_eq!(found["PRIORITY"], "6");
assert_eq!(found["SYSLOG_IDENTIFIER"], IDENTIFIER);
assert_eq!(bytes.iter().filter(|byte| { return **byte == b'\n'; }).count(), 4, "one newline ends each of the four fields");
assert_eq!(found["MESSAGE"], "landed PRIORITY=0 SYSLOG_IDENTIFIER=sshd");
}
#[test]
fn a_field_name_journald_refuses_is_left_out() {
let fields: [(&str, String); 3] = [("_SYSTEMD_UNIT", String::from("x")), ("lower", String::from("y")), ("CLI_GIT_OK", String::from("z"))];
let found = parsed(datagram(Priority::Info, "m", &fields).as_slice());
assert!(!found.contains_key("_SYSTEMD_UNIT") && !found.contains_key("lower"), "{found:?}");
assert_eq!(found["CLI_GIT_OK"], "z");
assert!(valid_field_name("CLI_GIT_AT_MS") && !valid_field_name("") && !valid_field_name("9A") && !valid_field_name("A=B"));
}
#[test]
fn a_long_message_is_cut_at_the_limit_and_says_so() {
let whole: String = "a".repeat(MESSAGE_LIMIT);
assert_eq!(one_line(whole.as_str(), MESSAGE_LIMIT), whole);
let cut: String = one_line("a".repeat(MESSAGE_LIMIT + 1).as_str(), MESSAGE_LIMIT);
assert_eq!(cut, format!("{whole}{CUT_MARK}"));
assert!(one_line("b".repeat(FIELD_LIMIT * 2).as_str(), FIELD_LIMIT).ends_with(CUT_MARK));
}
#[test]
fn the_stream_is_the_journal_only_when_device_and_inode_both_match() {
let named: String = variable();
assert!(stream_matches(Some(named.as_str()), DEVICE, INODE));
assert!(!stream_matches(Some(named.as_str()), DEVICE, INODE + 1), "another inode");
assert!(!stream_matches(Some(named.as_str()), DEVICE + 1, INODE), "another device");
assert!(!stream_matches(None, DEVICE, INODE), "no variable");
assert!(!stream_matches(Some("garbage"), DEVICE, INODE) && !stream_matches(Some("1:x"), 1, 0), "unreadable variable");
}
#[test]
fn a_journal_stream_sends_the_entry_as_a_datagram() {
let (socket, target) = receiver("journald-native-send");
let sink: Sink = Sink::choose(Some(variable().as_str()), Some((DEVICE, INODE)), &target);
assert!(sink.is_native());
let mut stderr: Vec<u8> = Vec::new();
let fields: [(&str, String); 1] = [("CLI_GIT_REQUEST_ID", String::from("r-2"))];
let delivery: Delivery = sink.deliver(Priority::Error, "failed x", &fields, "line\n", &mut stderr);
assert_eq!(delivery, Delivery::Native);
assert!(stderr.is_empty(), "a native entry is not also written to standard error");
let mut buffer: Vec<u8> = vec![0; 4096];
let length: usize = socket.recv(buffer.as_mut_slice()).expect("the datagram arrived");
let found = parsed(&buffer[..length]);
assert_eq!((found["PRIORITY"].as_str(), found["MESSAGE"].as_str(), found["CLI_GIT_REQUEST_ID"].as_str()), ("3", "failed x", "r-2"));
}
#[test]
fn standard_error_that_is_not_the_journal_keeps_the_plain_line() {
let (socket, target) = receiver("journald-native-plain");
socket.set_nonblocking(true).expect("non-blocking receiver");
let other: Sink = Sink::choose(Some(variable().as_str()), Some((DEVICE, INODE + 1)), &target);
assert!(!other.is_native());
let mut stderr: Vec<u8> = Vec::new();
assert_eq!(other.deliver(Priority::Warning, "m", &[], "cli-git-service: m at_ms=1\n", &mut stderr), Delivery::Stderr);
assert_eq!(String::from_utf8(stderr).expect("text"), "cli-git-service: m at_ms=1\n");
assert!(socket.recv(&mut [0_u8; 16]).is_err(), "nothing was sent to the socket");
}
#[test]
fn a_journal_that_is_not_listening_falls_back_to_standard_error_with_the_priority() {
let nobody: Target = Target::Abstract(format!("cli-git-test-journal-nobody-{}", std::process::id()));
let sink: Sink = Sink::choose(Some(variable().as_str()), Some((DEVICE, INODE)), &nobody);
let mut stderr: Vec<u8> = Vec::new();
assert_eq!(sink.deliver(Priority::Warning, "m", &[], "cli-git-service: m at_ms=1\n", &mut stderr), Delivery::Stderr);
assert_eq!(String::from_utf8(stderr).expect("text"), "<4>cli-git-service: m at_ms=1\n");
}
#[test]
fn a_full_journal_queue_falls_back_instead_of_waiting() {
let (_socket, target) = receiver("journald-native-full");
let sink: Sink = Sink::choose(Some(variable().as_str()), Some((DEVICE, INODE)), &target);
let mut stderr: Vec<u8> = Vec::new();
let started = std::time::Instant::now();
let mut native: usize = 0;
while sink.deliver(Priority::Info, "m", &[], "line\n", &mut stderr) == Delivery::Native {
native += 1;
assert!(native < 1_000_000, "the queue never filled");
}
assert!(stderr.starts_with(b"<6>line"), "{:?}", String::from_utf8_lossy(stderr.as_slice()));
assert!(started.elapsed() < std::time::Duration::from_secs(20), "filling the queue and falling back took {:?}", started.elapsed());
}
#[test]
fn the_raw_address_follows_the_target() {
let named: SocketAddr = Target::Abstract(String::from("abc")).address().expect("an address");
let (raw, length) = raw_address(&named).expect("fits");
assert_eq!(usize::try_from(length).expect("small"), 2 + 1 + 3);
assert_eq!(raw.sun_path[0], 0);
assert_eq!(raw.sun_path[1..4].iter().map(|byte| { return byte.to_ne_bytes()[0]; }).collect::<Vec<u8>>(), b"abc");
let path: SocketAddr = Target::Path(String::from("/run/x")).address().expect("an address");
let (raw_path, path_length) = raw_address(&path).expect("fits");
assert_eq!(usize::try_from(path_length).expect("small"), 2 + "/run/x".len() + 1);
assert_eq!(raw_path.sun_path[0].to_ne_bytes()[0], b'/');
}
#[test]
fn a_test_target_with_an_at_sign_is_abstract() {
assert_eq!(Target::parse("@x"), Target::Abstract(String::from("x")));
assert_eq!(Target::parse("/run/systemd/journal/socket"), Target::Path(String::from("/run/systemd/journal/socket")));
}
#[test]
fn the_longest_abstract_name_fits_the_raw_address_and_one_more_byte_does_not() {
let longest: SocketAddr = Target::Abstract("n".repeat(107)).address().expect("an address");
let (_, length) = raw_address(&longest).expect("fits");
assert_eq!(usize::try_from(length).expect("small"), 2 + 108);
assert!(Target::Abstract("n".repeat(108)).address().is_none_or(|address| { return raw_address(&address).is_none(); }));
let longest_path: SocketAddr = Target::Path(format!("/{}", "p".repeat(106))).address().expect("a path of 107 bytes");
assert!(raw_address(&longest_path).is_some());
}
#[test]
fn a_full_journal_stream_drops_the_line_instead_of_waiting() {
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
let (writer_end, mut reader_end) = std::os::unix::net::UnixStream::pair().expect("a pair");
let mut writer: StreamWriter = StreamWriter::on(writer_end.as_raw_fd());
let line: Vec<u8> = vec![b'x'; 1024];
let started = std::time::Instant::now();
let mut sent: usize = 0;
let refusal = loop {
match writer.write_all(line.as_slice()) {
Ok(()) => sent += 1,
Err(error) => break error,
}
assert!(sent < 1_000_000, "the stream never filled");
};
assert_eq!(refusal.kind(), std::io::ErrorKind::WouldBlock, "{refusal:?}");
assert!(started.elapsed() < std::time::Duration::from_secs(5), "{:?}", started.elapsed());
reader_end.set_nonblocking(true).expect("non-blocking reader");
let mut sink: Vec<u8> = vec![0; 1 << 20];
while reader_end.read(sink.as_mut_slice()).is_ok_and(|count| { return count > 0; }) {}
assert!(writer.write_all(b"after\n").is_ok(), "room again");
}
#[test]
fn a_stream_writer_on_a_file_writes_the_line() {
use std::io::Write;
use std::os::fd::AsRawFd;
let root = crate::command_test_support::fixture("journald-stream-file");
let path = root.join("stderr.log");
let file = std::fs::OpenOptions::new().create(true).append(true).open(&path).expect("a file");
let mut writer: StreamWriter = StreamWriter::on(file.as_raw_fd());
writer.write_all(b"<6>cli-git-service: ready at_ms=1\n").expect("written");
assert_eq!(std::fs::read_to_string(&path).expect("file"), "<6>cli-git-service: ready at_ms=1\n");
crate::command_test_support::remove(root.as_path());
}