Remove the Go implementation; build and release the Rust version for every OS #2
+2
-2
@@ -1,5 +1,5 @@
|
||||
//! In-process benchmarks mirroring the Go `go test -bench` suite
|
||||
//! (internal/tui/bench_test.go, internal/treemap/treemap_test.go):
|
||||
//! In-process benchmarks of the TUI model (render, navigation, filtering)
|
||||
//! and the treemap layout:
|
||||
//!
|
||||
//! cargo run --release --example bench
|
||||
//!
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ fn start_source(
|
||||
Ok((Some(s), t))
|
||||
}
|
||||
|
||||
/// Cancels on SIGINT for report mode, like Go's signal.NotifyContext.
|
||||
/// A token cancelled by SIGINT (Ctrl+C), for report mode.
|
||||
fn interrupt_cancel() -> Cancel {
|
||||
static CANCEL: std::sync::OnceLock<Cancel> = std::sync::OnceLock::new();
|
||||
extern "C" fn on_sigint(_: libc::c_int) {
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
//! A clonable cancellation token, the equivalent of a Go context's Done.
|
||||
//! A clonable cancellation token: a flag to poll plus a channel that is
|
||||
//! closed on cancellation, so it can also be waited on in a select.
|
||||
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
+5
-1
@@ -213,7 +213,11 @@ pub fn summary(w: &mut dyn Write, t: &Tree) -> io::Result<()> {
|
||||
textutil::size(r.tot_alloc),
|
||||
r.tot_alloc
|
||||
)?;
|
||||
writeln!(w, "Elapsed {}", textutil::go_duration(st.elapsed()))?;
|
||||
writeln!(
|
||||
w,
|
||||
"Elapsed {}",
|
||||
textutil::fmt_duration(st.elapsed())
|
||||
)?;
|
||||
for (k, &c) in st.err_counts.iter().enumerate() {
|
||||
if c > 0 {
|
||||
writeln!(
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ fn tok(kind: TokKind, val: &str, pos: usize) -> Token {
|
||||
}
|
||||
}
|
||||
|
||||
/// ASCII white space as Go's unicode.IsSpace sees it.
|
||||
/// ASCII white space: space, \t, \n, \v, \f, \r.
|
||||
fn is_space(c: u8) -> bool {
|
||||
matches!(c, b' ' | b'\t' | b'\n' | b'\x0b' | b'\x0c' | b'\r')
|
||||
}
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
assert!(names(&tr, "[").is_empty()); // malformed bare glob matches nothing
|
||||
assert!(names(&tr, "voilà\u{a0}x").is_empty()); // Go's lexer hangs on byte 0xA0
|
||||
assert!(names(&tr, "voilà\u{a0}x").is_empty()); // regression: a lexer once hung on byte 0xA0 (NBSP)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -309,7 +309,7 @@ fn has_glob(s: &str) -> bool {
|
||||
pub(super) fn name_match(v: &str) -> Expr {
|
||||
let lv = v.to_lowercase();
|
||||
if has_glob(v) {
|
||||
// Go's filepath.Match never matches a malformed pattern.
|
||||
// A malformed glob never matches.
|
||||
return if glob_ok(lv.as_bytes()) {
|
||||
Expr::Glob(Field::Name, lv)
|
||||
} else {
|
||||
@@ -319,8 +319,9 @@ pub(super) fn name_match(v: &str) -> Expr {
|
||||
Expr::Contains(Field::Name, lv)
|
||||
}
|
||||
|
||||
/// Reports whether pattern is well formed by Go's filepath.Match rules
|
||||
/// (fnmatch, which does the matching, accepts anything).
|
||||
/// Reports whether pattern is a well-formed glob (see
|
||||
/// `platform::glob_match`): every `[` class is closed and non-empty, ranges
|
||||
/// have both ends, and no escape is dangling.
|
||||
fn glob_ok(p: &[u8]) -> bool {
|
||||
// One possibly escaped character of a range; None if malformed.
|
||||
let esc = |mut i: usize| -> Option<usize> {
|
||||
@@ -461,13 +462,12 @@ pub fn parse_age(s: &str) -> Result<Duration, String> {
|
||||
))
|
||||
}
|
||||
};
|
||||
// Saturates where Go's time.Duration would overflow.
|
||||
// Saturates beyond u64 nanoseconds (about 584 years).
|
||||
Ok(Duration::from_nanos((v * secs * 1e9) as u64))
|
||||
}
|
||||
|
||||
/// Parses "2006-01-02", "2006-01-02T15:04", "2006-01-02 15:04" (local time)
|
||||
/// or RFC 3339, as Go's time.ParseInLocation with time.Local would, into unix
|
||||
/// nanoseconds.
|
||||
/// or RFC 3339 (with its own offset) into unix nanoseconds.
|
||||
fn parse_date(s: &str) -> Option<i64> {
|
||||
let b = s.as_bytes();
|
||||
let fixed = |i: usize, n: usize| -> Option<i64> {
|
||||
@@ -492,7 +492,7 @@ fn parse_date(s: &str) -> Option<i64> {
|
||||
if sep != b'T' && sep != b' ' {
|
||||
return None;
|
||||
}
|
||||
// Go's "15" hour accepts one or two digits.
|
||||
// The hour may have one or two digits.
|
||||
let mut i = 11;
|
||||
let hl = if fixed(i, 2).is_some() { 2 } else { 1 };
|
||||
let h = fixed(i, hl)?;
|
||||
|
||||
@@ -80,7 +80,7 @@ pub struct Node {
|
||||
pub files: u32, // recursive counts beneath (dirs excludes self)
|
||||
pub dirs: u32,
|
||||
pub errors: u32, // recursive error count beneath and including self
|
||||
pub mode: u32, // Go fs.FileMode bit layout (see platform::mode)
|
||||
pub mode: u32, // portable mode bits (see platform::mode)
|
||||
pub uid: u32,
|
||||
pub gid: u32,
|
||||
pub nlink: u32,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
//! mapsize: interactive terminal disk usage analyser (Rust port).
|
||||
//! mapsize: interactive terminal disk usage analyser.
|
||||
|
||||
pub mod app;
|
||||
pub mod brand;
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@ use mapsize::inventory::SizeMode;
|
||||
use mapsize::{brand, config, scan, snapshot, textutil};
|
||||
use std::path::Path;
|
||||
|
||||
/// Flags taking a value (Go's flag package: `-f v`, `-f=v`, `--f v`, `--f=v`).
|
||||
/// Flags taking a value, written `-f v`, `-f=v`, `--f v` or `--f=v`.
|
||||
const VALUE_FLAGS: &[&str] = &[
|
||||
"workers",
|
||||
"follow-symlinks",
|
||||
@@ -71,8 +71,8 @@ Flags:
|
||||
--theme NAME colour theme: default, dark, high-contrast, mono
|
||||
--color MODE colour support: auto, truecolor, 256, 16, none
|
||||
--no-mouse disable mouse support
|
||||
--log FILE write a debug log to FILE
|
||||
--log-level LEVEL log level: error, warn, info, debug, trace
|
||||
--log FILE accepted for compatibility; ignored
|
||||
--log-level LEVEL accepted for compatibility (error, warn, info, debug, trace); ignored
|
||||
--version print version and exit
|
||||
"
|
||||
);
|
||||
@@ -171,7 +171,7 @@ fn run(args: Vec<String>) -> i32 {
|
||||
return fail(format!("invalid --log-level {l:?}"));
|
||||
}
|
||||
}
|
||||
// ponytail: --log is accepted for CLI compatibility; the Rust port has no debug logging yet.
|
||||
// ponytail: --log and --log-level are accepted for compatibility and ignored; there is no debug logging.
|
||||
|
||||
let (depth, top, largest) = match (int("depth", -1), int("top", 0), int("largest-files", 0)) {
|
||||
(Ok(d), Ok(t), Ok(l)) => (d, t.max(0) as usize, l.max(0) as usize),
|
||||
|
||||
+2
-1
@@ -651,7 +651,8 @@ impl Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/// The OS error text without Rust's " (os error N)" suffix, matching Go.
|
||||
/// The OS error text, lower-cased and without the " (os error N)" suffix,
|
||||
/// e.g. "permission denied".
|
||||
fn err_msg(e: &io::Error) -> String {
|
||||
let s = e.to_string();
|
||||
match s.rfind(" (os error ") {
|
||||
|
||||
+70
-56
@@ -21,10 +21,22 @@
|
||||
//! sha256 32 bytes over the uncompressed body
|
||||
//! ```
|
||||
//!
|
||||
//! Varints are Go's encoding/binary (zig-zag for signed). Nodes are written
|
||||
//! in ID order; a parent always precedes its children, so a reader can
|
||||
//! rebuild the tree in one pass and malformed input cannot create cycles.
|
||||
//! Aggregates are recomputed on load, never trusted.
|
||||
//! A uvarint is unsigned LEB128: 7 bits per byte, least significant group
|
||||
//! first, the high bit set on every byte but the last; at most 10 bytes for
|
||||
//! 64 bits. A varint (signed) is zig-zag encoded first, (v << 1) ^ (v >> 63),
|
||||
//! so small magnitudes of either sign stay short: 0, -1, 1, -2 → 0, 1, 2, 3.
|
||||
//!
|
||||
//! In the meta JSON, timestamps are RFC 3339 strings with nanoseconds
|
||||
//! (trailing zeros trimmed) and the writer's UTC offset, or "Z" for UTC,
|
||||
//! e.g. "2026-10-08T10:40:00.123456789+01:00". An unset time is
|
||||
//! "0001-01-01T00:00:00Z"; null is read as unset.
|
||||
//!
|
||||
//! Compatibility: snapshots written by mapsize 1.x load unchanged, and the
|
||||
//! mode bits (see `platform::mode`) mean the same on every OS.
|
||||
//!
|
||||
//! Nodes are written in ID order; a parent always precedes its children, so
|
||||
//! a reader can rebuild the tree in one pass and malformed input cannot
|
||||
//! create cycles. Aggregates are recomputed on load, never trusted.
|
||||
|
||||
mod compare;
|
||||
|
||||
@@ -65,9 +77,9 @@ const FLAG_GZIPPED: u16 = 1;
|
||||
#[serde(default)]
|
||||
struct Meta {
|
||||
root: String,
|
||||
created: GoTime,
|
||||
start: GoTime,
|
||||
end: GoTime,
|
||||
created: JsonTime,
|
||||
start: JsonTime,
|
||||
end: JsonTime,
|
||||
complete: bool,
|
||||
cancelled: bool,
|
||||
files: i64,
|
||||
@@ -131,7 +143,7 @@ impl<W: Write> Writer<W> {
|
||||
self.bytes(&b[..=i])
|
||||
}
|
||||
fn varint(&mut self, v: i64) -> io::Result<()> {
|
||||
self.uvarint(((v << 1) ^ (v >> 63)) as u64) // zig-zag, as encoding/binary.PutVarint
|
||||
self.uvarint(((v << 1) ^ (v >> 63)) as u64) // zig-zag
|
||||
}
|
||||
fn str(&mut self, s: &[u8]) -> io::Result<()> {
|
||||
self.uvarint(s.len() as u64)?;
|
||||
@@ -154,11 +166,11 @@ pub fn save(out: &mut dyn Write, t: &Tree) -> io::Result<()> {
|
||||
let st = &t.stats;
|
||||
let m = Meta {
|
||||
root: String::from_utf8_lossy(&st.root).into_owned(),
|
||||
created: GoTime::from_system(SystemTime::now()),
|
||||
start: GoTime::from_system(st.start).local(),
|
||||
created: JsonTime::from_system(SystemTime::now()),
|
||||
start: JsonTime::from_system(st.start).local(),
|
||||
end: st
|
||||
.end
|
||||
.map(|e| GoTime::from_system(e).local())
|
||||
.map(|e| JsonTime::from_system(e).local())
|
||||
.unwrap_or_default(),
|
||||
complete: st.complete,
|
||||
cancelled: st.cancelled,
|
||||
@@ -233,7 +245,8 @@ pub fn save_file(path: &Path, t: &Tree) -> io::Result<()> {
|
||||
res
|
||||
}
|
||||
|
||||
/// Creates a new 0600 file named .mapsize-*.tmp in dir (like os.CreateTemp).
|
||||
/// Creates a new private (0600 on Unix) file named .mapsize-<random>.tmp in
|
||||
/// dir, retrying on name collisions.
|
||||
fn create_temp(dir: &Path) -> io::Result<(PathBuf, File)> {
|
||||
let seed = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -303,7 +316,8 @@ impl Body {
|
||||
Ok(self.buf[self.pos - 1])
|
||||
}
|
||||
|
||||
/// Fills out completely (errors like io.ReadFull).
|
||||
/// Fills out completely: "EOF" if the stream ends before the first byte,
|
||||
/// "unexpected EOF" if it ends part way.
|
||||
fn read_full(&mut self, out: &mut [u8]) -> Result<(), String> {
|
||||
let mut done = 0;
|
||||
while done < out.len() {
|
||||
@@ -332,7 +346,7 @@ impl Body {
|
||||
|
||||
fn varint(&mut self, what: &str) -> Result<i64, String> {
|
||||
let ux = read_uvarint(self).map_err(|e| format!("snapshot: bad {what}: {e}"))?;
|
||||
let v = (ux >> 1) as i64; // zig-zag, as encoding/binary.PutVarint
|
||||
let v = (ux >> 1) as i64; // undo zig-zag
|
||||
Ok(if ux & 1 != 0 { !v } else { v })
|
||||
}
|
||||
|
||||
@@ -349,7 +363,7 @@ impl Body {
|
||||
}
|
||||
}
|
||||
|
||||
/// encoding/binary.ReadUvarint over the body.
|
||||
/// Reads a uvarint (LEB128) from the body; more than 64 bits is an error.
|
||||
fn read_uvarint(b: &mut Body) -> Result<u64, String> {
|
||||
let (mut x, mut s) = (0u64, 0u32);
|
||||
for i in 0..10 {
|
||||
@@ -611,32 +625,32 @@ fn valid_name(s: &[u8]) -> bool {
|
||||
!s.is_empty() && s != b"." && s != b".." && !s.iter().any(|&c| c == b'/' || c == 0)
|
||||
}
|
||||
|
||||
/// A Go time.Time as JSON carries it: unix seconds plus nanoseconds, and
|
||||
/// the UTC offset it is written with. The default is Go's zero time
|
||||
/// (0001-01-01T00:00:00Z).
|
||||
/// A timestamp as the meta JSON carries it (see the module docs): unix
|
||||
/// seconds plus nanoseconds, and the UTC offset it is written with. The
|
||||
/// default is the unset time, 0001-01-01T00:00:00Z.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
struct GoTime {
|
||||
struct JsonTime {
|
||||
secs: i64,
|
||||
nanos: u32,
|
||||
off: i32, // seconds east of UTC; presentation only
|
||||
}
|
||||
|
||||
const GO_ZERO_SECS: i64 = -62_135_596_800;
|
||||
const ZERO_SECS: i64 = -62_135_596_800;
|
||||
|
||||
impl Default for GoTime {
|
||||
impl Default for JsonTime {
|
||||
fn default() -> Self {
|
||||
GoTime {
|
||||
secs: GO_ZERO_SECS,
|
||||
JsonTime {
|
||||
secs: ZERO_SECS,
|
||||
nanos: 0,
|
||||
off: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GoTime {
|
||||
fn from_system(t: SystemTime) -> GoTime {
|
||||
impl JsonTime {
|
||||
fn from_system(t: SystemTime) -> JsonTime {
|
||||
match t.duration_since(UNIX_EPOCH) {
|
||||
Ok(d) => GoTime {
|
||||
Ok(d) => JsonTime {
|
||||
secs: d.as_secs() as i64,
|
||||
nanos: d.subsec_nanos(),
|
||||
off: 0,
|
||||
@@ -649,7 +663,7 @@ impl GoTime {
|
||||
} else {
|
||||
(-s - 1, 1_000_000_000 - n)
|
||||
};
|
||||
GoTime {
|
||||
JsonTime {
|
||||
secs,
|
||||
nanos,
|
||||
off: 0,
|
||||
@@ -658,14 +672,14 @@ impl GoTime {
|
||||
}
|
||||
}
|
||||
|
||||
/// The same instant in the local time zone (as Go's time.Now()).
|
||||
fn local(self) -> GoTime {
|
||||
/// The same instant, presented in the local time zone.
|
||||
fn local(self) -> JsonTime {
|
||||
let off = crate::platform::local_time(self.secs).off;
|
||||
GoTime { off, ..self }
|
||||
JsonTime { off, ..self }
|
||||
}
|
||||
|
||||
fn is_zero(self) -> bool {
|
||||
(self.secs, self.nanos) == (GO_ZERO_SECS, 0)
|
||||
(self.secs, self.nanos) == (ZERO_SECS, 0)
|
||||
}
|
||||
|
||||
fn to_system(self) -> SystemTime {
|
||||
@@ -677,7 +691,7 @@ impl GoTime {
|
||||
}
|
||||
}
|
||||
|
||||
/// RFC 3339 with trimmed nanoseconds, like Go's MarshalJSON.
|
||||
/// RFC 3339 with nanoseconds (trailing zeros trimmed) and "Z" for UTC.
|
||||
fn format(self) -> String {
|
||||
let secs = self.secs + self.off as i64;
|
||||
let (y, mo, d) = civil_from_days(secs.div_euclid(86400));
|
||||
@@ -704,7 +718,7 @@ impl GoTime {
|
||||
}
|
||||
|
||||
/// Parses RFC 3339 ("2026-10-08T10:40:00.123456789+01:00").
|
||||
fn parse(s: &str) -> Option<GoTime> {
|
||||
fn parse(s: &str) -> Option<JsonTime> {
|
||||
let b = s.as_bytes();
|
||||
let num = |r: std::ops::Range<usize>| -> Option<i64> {
|
||||
let p = b.get(r)?;
|
||||
@@ -762,7 +776,7 @@ impl GoTime {
|
||||
_ => return None,
|
||||
};
|
||||
let secs = days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se - off;
|
||||
Some(GoTime {
|
||||
Some(JsonTime {
|
||||
secs,
|
||||
nanos,
|
||||
off: off as i32,
|
||||
@@ -779,18 +793,18 @@ impl GoTime {
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for GoTime {
|
||||
impl Serialize for JsonTime {
|
||||
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str(&self.format())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for GoTime {
|
||||
impl<'de> Deserialize<'de> for JsonTime {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
// null leaves the zero time, as in Go.
|
||||
// null leaves the time unset.
|
||||
match Option::<String>::deserialize(d)? {
|
||||
None => Ok(GoTime::default()),
|
||||
Some(s) => GoTime::parse(&s)
|
||||
None => Ok(JsonTime::default()),
|
||||
Some(s) => JsonTime::parse(&s)
|
||||
.ok_or_else(|| serde::de::Error::custom(format!("parsing time {s:?}"))),
|
||||
}
|
||||
}
|
||||
@@ -914,7 +928,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_uvarint_matches_go() {
|
||||
fn varint_encoding() {
|
||||
for v in [0, 1, 127, 128, 300, 1 << 35, u64::MAX] {
|
||||
assert_eq!(read_uvarint(&mut body(&put_uvarint(v))), Ok(v));
|
||||
}
|
||||
@@ -923,7 +937,7 @@ mod tests {
|
||||
read_uvarint(&mut body(&[0xff; 11])).is_err(),
|
||||
"overflow not detected"
|
||||
);
|
||||
// Zig-zag as Go's PutVarint.
|
||||
// Zig-zag.
|
||||
for (v, enc) in [(0i64, 0u64), (-1, 1), (1, 2), (-2, 3), (i64::MIN, u64::MAX)] {
|
||||
assert_eq!(((v << 1) ^ (v >> 63)) as u64, enc);
|
||||
assert_eq!(body(&put_uvarint(enc)).varint("v"), Ok(v));
|
||||
@@ -931,16 +945,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_time_json() {
|
||||
let t = GoTime::parse("2026-10-08T10:40:00.123456789+01:00").unwrap();
|
||||
fn json_time() {
|
||||
let t = JsonTime::parse("2026-10-08T10:40:00.123456789+01:00").unwrap();
|
||||
assert_eq!((t.secs, t.nanos, t.off), (1_791_452_400, 123_456_789, 3600));
|
||||
assert_eq!(t.format(), "2026-10-08T10:40:00.123456789+01:00");
|
||||
assert_eq!(
|
||||
GoTime { off: 0, ..t }.format(),
|
||||
JsonTime { off: 0, ..t }.format(),
|
||||
"2026-10-08T09:40:00.123456789Z"
|
||||
);
|
||||
assert_eq!(
|
||||
GoTime {
|
||||
JsonTime {
|
||||
off: -5 * 3600 - 1800,
|
||||
..t
|
||||
}
|
||||
@@ -948,17 +962,17 @@ mod tests {
|
||||
"2026-10-08T04:10:00.123456789-05:30"
|
||||
);
|
||||
assert_eq!(
|
||||
GoTime::parse("2026-10-08T09:40:00.1Z").unwrap().format(),
|
||||
JsonTime::parse("2026-10-08T09:40:00.1Z").unwrap().format(),
|
||||
"2026-10-08T09:40:00.1Z"
|
||||
);
|
||||
assert_eq!(
|
||||
GoTime::parse("0001-01-01T00:00:00Z"),
|
||||
Some(GoTime::default())
|
||||
JsonTime::parse("0001-01-01T00:00:00Z"),
|
||||
Some(JsonTime::default())
|
||||
);
|
||||
assert_eq!(GoTime::default().format(), "0001-01-01T00:00:00Z");
|
||||
assert_eq!(JsonTime::default().format(), "0001-01-01T00:00:00Z");
|
||||
assert_eq!(
|
||||
GoTime::from_system(GoTime::default().to_system()),
|
||||
GoTime::default()
|
||||
JsonTime::from_system(JsonTime::default().to_system()),
|
||||
JsonTime::default()
|
||||
);
|
||||
for bad in [
|
||||
"",
|
||||
@@ -967,17 +981,17 @@ mod tests {
|
||||
"2026-10-08T00:00:00",
|
||||
"2026-10-08T00:00:00.Z",
|
||||
] {
|
||||
assert_eq!(GoTime::parse(bad), None, "{bad}");
|
||||
assert_eq!(JsonTime::parse(bad), None, "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A tiny tree saved by the Go binary
|
||||
/// A snapshot written by mapsize 1.2.0 of a tiny tree
|
||||
/// (tree/{a/x.iso 10000 B, a/b/note.txt, c/y.log 3000 B, c/link -> ../a}).
|
||||
#[test]
|
||||
fn loads_go_snapshot() {
|
||||
fn loads_1_2_snapshot() {
|
||||
let t = load_file(Path::new(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/src/snapshot/testdata/go.msz"
|
||||
"/src/snapshot/testdata/v1.2.0.msz"
|
||||
)))
|
||||
.unwrap();
|
||||
let r = t.node(0);
|
||||
@@ -997,7 +1011,7 @@ mod tests {
|
||||
assert_eq!(t.node(iso).size, 10000);
|
||||
assert_eq!(t.ext_name(t.node(iso)), "iso");
|
||||
assert!(t.path_string(iso).ends_with("/tree/a/x.iso"));
|
||||
// And it survives a Rust round trip unchanged.
|
||||
// And it survives a save/load round trip unchanged.
|
||||
let again = load(&saved(&t)[..]).unwrap();
|
||||
assert_eq!(
|
||||
(again.node(0).tot_size, again.len(), again.stats.start),
|
||||
|
||||
+6
-6
@@ -357,9 +357,9 @@ pub fn rfc3339_utc(unix_nanos: i64) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// Formats a duration like Go's time.Duration.String after rounding to
|
||||
/// milliseconds, e.g. "1.234s", "250ms", "2m3.5s".
|
||||
pub fn go_duration(d: Duration) -> String {
|
||||
/// Formats a duration rounded to milliseconds: "0s", "250ms", "1.234s",
|
||||
/// "2m3.5s", "1h0m5s" (trailing zeros of the seconds trimmed).
|
||||
pub fn fmt_duration(d: Duration) -> String {
|
||||
let ms = (d.as_nanos() + 500_000) / 1_000_000;
|
||||
if ms == 0 {
|
||||
return "0s".into();
|
||||
@@ -393,8 +393,8 @@ mod time_tests {
|
||||
rfc3339_utc(1_791_456_000 * 1_000_000_000),
|
||||
"2026-10-08T10:40:00Z"
|
||||
);
|
||||
assert_eq!(go_duration(Duration::from_micros(1_234_400)), "1.234s");
|
||||
assert_eq!(go_duration(Duration::from_millis(250)), "250ms");
|
||||
assert_eq!(go_duration(Duration::from_millis(123_500)), "2m3.5s");
|
||||
assert_eq!(fmt_duration(Duration::from_micros(1_234_400)), "1.234s");
|
||||
assert_eq!(fmt_duration(Duration::from_millis(250)), "250ms");
|
||||
assert_eq!(fmt_duration(Duration::from_millis(123_500)), "2m3.5s");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -326,7 +326,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Go fuzz target's property, run over fixed seeds.
|
||||
/// Squarify invariants over random partitions, from fixed seeds.
|
||||
#[test]
|
||||
fn squarify_random_partitions() {
|
||||
let mut rng = Rng::new(1);
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
//! Benchmark hooks: the model driven with no terminal, rendering in
|
||||
//! true colour, as the Go benchmarks do.
|
||||
//! true colour.
|
||||
|
||||
use super::model::{Key, Model, Msg, Options, Profile};
|
||||
use crate::cancel::Cancel;
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ pub(crate) enum Profile {
|
||||
Ascii,
|
||||
}
|
||||
|
||||
/// A key press. name follows Bubble Tea's keystroke names ("a", "G",
|
||||
/// A key press. name is the keystroke name ("a", "G",
|
||||
/// "ctrl+c", "shift+tab", "space", "pgdown", …); text is the inserted
|
||||
/// text for printable keys.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -69,7 +69,7 @@ pub(crate) struct Key {
|
||||
}
|
||||
|
||||
impl Key {
|
||||
/// A key from its keystroke name, as the Go tests build them: single
|
||||
/// A key from its keystroke name, as tests build them: single
|
||||
/// characters insert themselves, "space" inserts " ".
|
||||
pub fn from_name(s: &str) -> Key {
|
||||
let text = match s {
|
||||
|
||||
+2
-2
@@ -162,7 +162,7 @@ fn enter_terminal(mouse: bool) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bubble Tea keystroke name for a key event.
|
||||
/// The keystroke name (see `Key`) for a key event.
|
||||
fn convert_key(ev: KeyEvent) -> Option<Key> {
|
||||
if ev.kind == KeyEventKind::Release {
|
||||
return None;
|
||||
@@ -251,7 +251,7 @@ fn convert(ev: Event) -> Option<Msg> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
// Pastes are ignored, as in the Go version.
|
||||
// Pastes are ignored.
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,4 +1,5 @@
|
||||
//! Headless model tests (ports of tui_test.go, nest_test.go, live_test.go).
|
||||
//! Headless model tests: rendering and navigation, nested directories,
|
||||
//! and a model driven by a live scan.
|
||||
|
||||
use super::bench::opts_for;
|
||||
use super::canvas::rgb;
|
||||
@@ -536,7 +537,7 @@ fn copy_refuses_control_characters() {
|
||||
);
|
||||
}
|
||||
|
||||
// ---- nest_test.go -----------------------------------------------------------
|
||||
// ---- Nested directories -----------------------------------------------------
|
||||
|
||||
fn root_tree() -> Tree {
|
||||
let mut t = Tree::new(b"/", Kind::Dir);
|
||||
@@ -611,7 +612,7 @@ fn reveal_headless_zooms_map() {
|
||||
);
|
||||
}
|
||||
|
||||
// ---- live_test.go -----------------------------------------------------------
|
||||
// ---- Live scan --------------------------------------------------------------
|
||||
|
||||
/// Renders, navigates, filters and resizes while the scanner mutates the
|
||||
/// inventory, verifying the locking discipline.
|
||||
|
||||
Reference in New Issue
Block a user