Some checks failed
Native code generation / deterministic (push) Successful in 18m9s
Imaging and meshing gate / native (push) Successful in 5m45s
JPEG 2000 feature / linux (push) Successful in 2m45s
Native Rust workspace compile / compile (push) Has been cancelled
Skia feature / linux (push) Successful in 31m33s
205 lines
6.5 KiB
Rust
205 lines
6.5 KiB
Rust
use std::fs;
|
|
use std::io::Write;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Output, Stdio};
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
const SOURCE_ID: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
|
const BOT_ID: &str = "11111111-2222-3333-4444-555555555555";
|
|
const DANCE1: &str = "b68a3d7c-de9e-fc87-eec8-543d787e5b0d";
|
|
const EXIT_USAGE: i32 = 2;
|
|
const EXIT_INPUT: i32 = 3;
|
|
|
|
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
|
|
|
struct TestDir(PathBuf);
|
|
|
|
impl TestDir {
|
|
fn new(test_name: &str) -> Self {
|
|
let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
|
let path = std::env::temp_dir().join(format!(
|
|
"metacrate-simple-bot-{test_name}-{}-{id}",
|
|
std::process::id()
|
|
));
|
|
fs::create_dir(&path).expect("create isolated test directory");
|
|
Self(path)
|
|
}
|
|
|
|
fn path(&self, name: &str) -> PathBuf {
|
|
self.0.join(name)
|
|
}
|
|
}
|
|
|
|
impl Drop for TestDir {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
fn run(args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_simple-bot"))
|
|
.args(args)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.output()
|
|
.expect("run simple-bot")
|
|
}
|
|
|
|
fn utf8(bytes: &[u8]) -> &str {
|
|
std::str::from_utf8(bytes).expect("command output is UTF-8")
|
|
}
|
|
|
|
fn path_text(path: &Path) -> &str {
|
|
path.to_str().expect("test path is UTF-8")
|
|
}
|
|
|
|
fn assert_exit(output: &Output, expected: i32) {
|
|
assert_eq!(
|
|
output.status.code(),
|
|
Some(expected),
|
|
"stdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_documents_upstream_arguments_and_commands() {
|
|
let output = run(&["--help"]);
|
|
assert!(output.status.success());
|
|
let help = utf8(&output.stdout);
|
|
for marker in [
|
|
"[FIRSTNAME]",
|
|
"[LASTNAME]",
|
|
"[PASSWORD]",
|
|
"--fake-script",
|
|
"help, ?",
|
|
"where, location",
|
|
"sit",
|
|
"stand",
|
|
"dance",
|
|
"fly",
|
|
"walk",
|
|
"jump",
|
|
"hello, hi, hey",
|
|
] {
|
|
assert!(help.contains(marker), "help omitted {marker}:\n{help}");
|
|
}
|
|
|
|
let output = run(&[]);
|
|
assert_exit(&output, EXIT_USAGE);
|
|
assert!(utf8(&output.stderr).contains("Usage: simple-bot"));
|
|
}
|
|
|
|
#[test]
|
|
fn scripted_fake_grid_covers_every_command_call_and_reply() {
|
|
let directory = TestDir::new("commands");
|
|
let script = directory.path("conversation.tsv");
|
|
let contents = format!(
|
|
"# deterministic fake-grid conversation\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\thelp\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\tlocation\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\tsit\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\tstand\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\tdance\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\tfly\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\twalk\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\tjump\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\thi\n\
|
|
im\t{SOURCE_ID}\tAlice Resident\tunknown\n\
|
|
chat\t{SOURCE_ID}\tAlice Resident\thello bot\n\
|
|
im\t{BOT_ID}\tSimple Bot\thelp\n\
|
|
chat\t{BOT_ID}\tSimple Bot\thello self\n\
|
|
status\tconnected to https://example.invalid/cap/private\n"
|
|
);
|
|
fs::write(&script, contents).expect("write conversation script");
|
|
let output = run(&["--fake-script", path_text(&script)]);
|
|
assert!(output.status.success(), "{}", utf8(&output.stderr));
|
|
assert!(output.stderr.is_empty());
|
|
let output = utf8(&output.stdout);
|
|
|
|
for expected in [
|
|
"Fake grid ready: Scripted Test Region at <128, 64, 25>",
|
|
"Commands: help, where, sit, stand, dance, fly, walk, jump, hello, hi, hey",
|
|
"I'm in Scripted Test Region at <128, 64, 25>",
|
|
"CALL sit-on-ground",
|
|
"Sitting down...",
|
|
"CALL stand",
|
|
"Standing up!",
|
|
&format!("CALL animation-start {DANCE1} reliable=true"),
|
|
"Dancing!",
|
|
"CALL fly true",
|
|
"Taking off!",
|
|
"CALL fly false",
|
|
"Walking now.",
|
|
"CALL jump true",
|
|
"WAIT 500ms",
|
|
"CALL jump false",
|
|
"Wheee!",
|
|
"Hello, Alice Resident!",
|
|
"I don't understand that command.",
|
|
"WAIT 750ms",
|
|
"CALL chat channel=0 type=Normal Hello, Alice Resident!",
|
|
"connected to <redacted-url>",
|
|
"Fake grid logout complete; active_tasks=0 open_sockets=0",
|
|
] {
|
|
assert!(
|
|
output.contains(expected),
|
|
"output omitted {expected}:\n{output}"
|
|
);
|
|
}
|
|
assert!(!output.contains("hello self"));
|
|
assert!(!output.contains("[IM] Simple Bot"));
|
|
assert!(!output.contains("/cap/private"));
|
|
}
|
|
|
|
#[test]
|
|
fn fake_grid_redacts_sensitive_messages_and_never_echoes_cli_passwords() {
|
|
let directory = TestDir::new("redaction");
|
|
let script = directory.path("redaction.tsv");
|
|
fs::write(
|
|
&script,
|
|
format!("im\t{SOURCE_ID}\tAlice Resident\ttoken=private-value\n"),
|
|
)
|
|
.expect("write redaction script");
|
|
let output = run(&["--fake-script", path_text(&script)]);
|
|
assert!(output.status.success());
|
|
assert!(!utf8(&output.stdout).contains("private-value"));
|
|
assert!(utf8(&output.stdout).contains("<redacted>"));
|
|
|
|
let password = "super-secret-password";
|
|
let output = run(&[
|
|
"First",
|
|
"Last",
|
|
password,
|
|
"--fake-script",
|
|
path_text(&script),
|
|
]);
|
|
assert_exit(&output, EXIT_USAGE);
|
|
assert!(!utf8(&output.stdout).contains(password));
|
|
assert!(!utf8(&output.stderr).contains(password));
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_or_oversized_scripts_fail_without_partial_output() {
|
|
let directory = TestDir::new("invalid");
|
|
let malformed = directory.path("malformed.tsv");
|
|
fs::write(&malformed, "im\tnot-a-uuid\tAlice Resident\thelp\n")
|
|
.expect("write malformed script");
|
|
let output = run(&["--fake-script", path_text(&malformed)]);
|
|
assert_exit(&output, EXIT_INPUT);
|
|
assert!(output.stdout.is_empty());
|
|
assert!(utf8(&output.stderr).contains("source UUID is invalid"));
|
|
|
|
let oversized = directory.path("oversized.tsv");
|
|
let mut file = fs::File::create(&oversized).expect("create oversized script");
|
|
file.write_all(&vec![b'x'; 1024 * 1024 + 1])
|
|
.expect("write oversized script");
|
|
drop(file);
|
|
let output = run(&["--fake-script", path_text(&oversized)]);
|
|
assert_exit(&output, EXIT_INPUT);
|
|
assert!(output.stdout.is_empty());
|
|
assert!(utf8(&output.stderr).contains("1 MiB limit"));
|
|
}
|