toolkit: the tool program, read_file and write_file
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
//! `toolkit read_file` and `toolkit write_file`, run as `brokerd` runs them. Do not edit.
|
||||
|
||||
mod support;
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use proto::tools::{ReadFileArgs, WriteFileArgs};
|
||||
use support::{TempDir, json, toolkit};
|
||||
use toolkit::files::MAX_READ;
|
||||
|
||||
fn read(path: &str) -> support::Ran {
|
||||
toolkit(
|
||||
&["read_file"],
|
||||
&json(&ReadFileArgs {
|
||||
path: path.to_string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn write(path: &str, content: &str) -> support::Ran {
|
||||
let args = WriteFileArgs {
|
||||
path: path.to_string(),
|
||||
content: content.to_string(),
|
||||
};
|
||||
toolkit(&["write_file"], &json(&args))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_is_read_exactly() {
|
||||
let dir = TempDir::new("read");
|
||||
let text = "line one\nline two, no newline at the end: ✓";
|
||||
std::fs::write(dir.at("a.md"), text).unwrap();
|
||||
let ran = read(&dir.at("a.md"));
|
||||
assert_eq!((ran.code, ran.stdout.as_str()), (0, text));
|
||||
assert_eq!(ran.stderr, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_file_is_empty_output() {
|
||||
let dir = TempDir::new("read-empty");
|
||||
std::fs::write(dir.at("e"), "").unwrap();
|
||||
let ran = read(&dir.at("e"));
|
||||
assert_eq!((ran.code, ran.stdout.as_str()), (0, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_cannot_be_read_is_exit_1_with_one_line_for_the_model() {
|
||||
let dir = TempDir::new("read-bad");
|
||||
std::fs::write(dir.at("bin"), [0xff, 0xfe, 0x00]).unwrap();
|
||||
std::fs::write(dir.at("big"), vec![b'a'; MAX_READ + 1]).unwrap();
|
||||
std::fs::write(dir.at("exact"), vec![b'a'; MAX_READ]).unwrap();
|
||||
let cases = [
|
||||
(dir.at("missing"), "no such file"),
|
||||
(dir.at(""), "is a directory"),
|
||||
(dir.at("bin"), "not UTF-8 text"),
|
||||
(dir.at("big"), "larger than 1048576 bytes"),
|
||||
];
|
||||
for (path, why) in cases {
|
||||
let ran = read(&path);
|
||||
assert_eq!(ran.code, 1, "{path}");
|
||||
assert_eq!(ran.stdout, format!("read_file: {path}: {why}"));
|
||||
}
|
||||
let ran = read(&dir.at("exact"));
|
||||
assert_eq!(
|
||||
(ran.code, ran.stdout.len()),
|
||||
(0, MAX_READ),
|
||||
"exactly the limit is fine"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unreadable_file_names_the_error() {
|
||||
if is_root() {
|
||||
return; // root reads anything
|
||||
}
|
||||
let dir = TempDir::new("read-perm");
|
||||
std::fs::write(dir.at("secret"), "x").unwrap();
|
||||
std::fs::set_permissions(dir.at("secret"), std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||
let ran = read(&dir.at("secret"));
|
||||
assert_eq!(ran.code, 1);
|
||||
assert!(
|
||||
ran.stdout
|
||||
.starts_with(&format!("read_file: {}: ", dir.at("secret"))),
|
||||
"{}",
|
||||
ran.stdout
|
||||
);
|
||||
assert!(ran.stdout.contains("ermission denied"), "{}", ran.stdout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_is_written_created_or_replaced() {
|
||||
let dir = TempDir::new("write");
|
||||
let ran = write(&dir.at("new.md"), "hello ✓\n");
|
||||
assert_eq!(ran.code, 0);
|
||||
assert_eq!(
|
||||
ran.stdout,
|
||||
format!("wrote 10 bytes to {}", dir.at("new.md"))
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(dir.at("new.md")).unwrap(),
|
||||
"hello ✓\n"
|
||||
);
|
||||
let ran = write(&dir.at("new.md"), "");
|
||||
assert_eq!(ran.stdout, format!("wrote 0 bytes to {}", dir.at("new.md")));
|
||||
assert_eq!(std::fs::read_to_string(dir.at("new.md")).unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_cannot_be_written_is_exit_1() {
|
||||
let dir = TempDir::new("write-bad");
|
||||
std::fs::create_dir(dir.at("sub")).unwrap();
|
||||
let cases = [
|
||||
(dir.at("nope/a.md"), "the directory does not exist"),
|
||||
(dir.at("sub"), "is a directory"),
|
||||
];
|
||||
for (path, why) in cases {
|
||||
let ran = write(&path, "x");
|
||||
assert_eq!(ran.code, 1, "{path}");
|
||||
assert_eq!(ran.stdout, format!("write_file: {path}: {why}"));
|
||||
}
|
||||
assert!(!dir.path().join("nope").exists(), "no directory is created");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn misuse_is_exit_2_with_nothing_on_standard_output() {
|
||||
let long = vec![b' '; toolkit::input::MAX_INPUT + 1];
|
||||
let cases: [(&[&str], &[u8], &str); 7] = [
|
||||
(
|
||||
&["read_file"],
|
||||
br#"{"path":"/a","mode":1}"#,
|
||||
"the arguments do not parse",
|
||||
),
|
||||
(&["read_file"], b"", "the arguments do not parse"),
|
||||
(
|
||||
&["write_file"],
|
||||
br#"{"path":"/a"}"#,
|
||||
"the arguments do not parse",
|
||||
),
|
||||
(&["read_file"], &[0xff, 0xfe], "not UTF-8"),
|
||||
(&["read_file"], &long, "larger than 2097152 bytes"),
|
||||
(&["format_disk"], b"{}", "unknown tool"),
|
||||
(&[], b"{}", "unknown tool"),
|
||||
];
|
||||
for (args, input, why) in cases {
|
||||
let ran = toolkit(args, input);
|
||||
assert_eq!(ran.code, 2, "{args:?}");
|
||||
assert_eq!(
|
||||
ran.stdout, "",
|
||||
"{args:?}: the model sees nothing of a misuse"
|
||||
);
|
||||
assert!(ran.stderr.contains(why), "{args:?}: {}", ran.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_root() -> bool {
|
||||
std::fs::read_to_string("/proc/self/status")
|
||||
.map(|s| s.lines().any(|l| l.starts_with("Uid:\t0\t")))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Running the `toolkit` binary as `brokerd` does: arguments on standard input. Do not edit.
|
||||
|
||||
#![allow(dead_code)] // each test file uses its own part
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// A temporary directory, removed when dropped.
|
||||
pub struct TempDir(pub PathBuf);
|
||||
|
||||
impl TempDir {
|
||||
pub fn new(tag: &str) -> TempDir {
|
||||
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
||||
let path = std::env::temp_dir().join(format!("tk-{tag}-{}-{n}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
std::fs::create_dir_all(&path).unwrap();
|
||||
TempDir(path)
|
||||
}
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
/// The path of `name` inside, as a string (the tools take strings).
|
||||
pub fn at(&self, name: &str) -> String {
|
||||
self.0.join(name).to_str().unwrap().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ran {
|
||||
pub code: i32,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
/// Run `toolkit <args…>` with `input` on standard input.
|
||||
pub fn toolkit(args: &[&str], input: &[u8]) -> Ran {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_toolkit"))
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
// A broken pipe here only means toolkit stopped reading, which some tests expect.
|
||||
let _ = stdin.write_all(input);
|
||||
drop(stdin);
|
||||
let out = child.wait_with_output().unwrap();
|
||||
Ran {
|
||||
code: out.status.code().unwrap_or(-1),
|
||||
stdout: String::from_utf8(out.stdout).unwrap(),
|
||||
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The JSON for one argument struct.
|
||||
pub fn json<T: serde::Serialize>(value: &T) -> Vec<u8> {
|
||||
serde_json::to_vec(value).unwrap()
|
||||
}
|
||||
Reference in New Issue
Block a user