diff --git a/.rgignore b/.rgignore new file mode 100644 index 000000000..e7e1fb04f --- /dev/null +++ b/.rgignore @@ -0,0 +1 @@ +tests/* diff --git a/Cargo.lock b/Cargo.lock index bafc7938d..82dbff4ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1249,7 +1249,7 @@ dependencies = [ [[package]] name = "humility" -version = "0.11.9" +version = "0.11.11" dependencies = [ "anyhow", "bitfield", diff --git a/Cargo.toml b/Cargo.toml index 8ed0b19b4..fb74ef0a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ name = "humility" # # Be sure to check in and push all of the files that change. Happy versioning! # -version = "0.11.9" +version = "0.11.11" authors = ["Bryan Cantrill "] edition = "2018" license = "MPL-2.0" diff --git a/cmd/auxflash/src/lib.rs b/cmd/auxflash/src/lib.rs index 9c2ea689a..b299aad5b 100644 --- a/cmd/auxflash/src/lib.rs +++ b/cmd/auxflash/src/lib.rs @@ -254,6 +254,7 @@ impl<'a> AuxFlashHandler<'a> { bar.set_position(offset as u64); } bar.set_position(out.len() as u64); + bar.finish_and_clear(); Ok(out) } @@ -349,6 +350,7 @@ impl<'a> AuxFlashHandler<'a> { bar.set_position(offset as u64); } bar.set_position(data.len() as u64); + bar.finish_and_clear(); humility::msg!("done"); Ok(()) } diff --git a/cmd/dump/src/lib.rs b/cmd/dump/src/lib.rs index a86afaaf4..050a6464f 100644 --- a/cmd/dump/src/lib.rs +++ b/cmd/dump/src/lib.rs @@ -74,7 +74,7 @@ use humility_cli::{ExecutionContext, Subcommand}; use humility_cmd::{Archive, Attach, Command, CommandKind, Validate}; use humility_dump_agent::{ task_areas, DumpAgent, DumpAgentCore, DumpAgentExt, DumpArea, - HiffyDumpAgent, UdpDumpAgent, + DumpBreakdown, HiffyDumpAgent, UdpDumpAgent, }; use humpty::DumpTask; use indicatif::{HumanBytes, HumanDuration, ProgressBar, ProgressStyle}; @@ -97,7 +97,7 @@ struct DumpArgs { timeout: u32, /// show dump agent status - #[clap(long, conflicts_with_all = &["simulation", "task", "all"])] + #[clap(long, conflicts_with_all = &["simulation", "task", "extract-all"])] dump_agent_status: bool, /// force use of the dump agent when directly attached with a debug probe @@ -111,12 +111,12 @@ struct DumpArgs { /// force manual initiation, leaving target halted #[clap( long, requires = "force-dump-agent", - conflicts_with_all = &["all", "task"] + conflicts_with_all = &["extract-all", "task"] )] force_manual_initiation: bool, /// force existing in situ dump to be read - #[clap(long, conflicts_with_all = &["simulation", "task", "all"])] + #[clap(long, conflicts_with_all = &["simulation", "task", "extract-all"])] force_read: bool, /// initialize dump state, clearing any dump at the dump agent @@ -124,12 +124,12 @@ struct DumpArgs { initialize_dump_agent: bool, /// retain dump state after a system dump - #[clap(long, conflicts_with_all = &["task", "list", "area"])] + #[clap(long, conflicts_with_all = &["task", "list"])] retain_state: bool, /// overwrite any dump state as part of taking dump #[clap( - long, short = 'F', + long, conflicts_with_all = &["initialize-dump-agent", "simulate-dumper"], )] force_overwrite: bool, @@ -141,7 +141,7 @@ struct DumpArgs { /// in addition to simulating the dumper, generate a stock dump #[clap( long, requires = "simulation", - conflicts_with_all = &["task", "all"], + conflicts_with_all = &["task", "extract-all"], value_name = "filename", )] stock_dumpfile: Option, @@ -154,7 +154,7 @@ struct DumpArgs { /// simulates a single-task dump #[clap( long, - conflicts_with_all = &["area", "stock-dumpfile", "list"], + conflicts_with_all = &["extract", "stock-dumpfile", "list"], requires = "simulate-dumper", value_name = "task", )] @@ -163,28 +163,38 @@ struct DumpArgs { /// dumps a single task #[clap( long, - conflicts_with_all = &["simulation", "list", "area"], + conflicts_with_all = &["simulation", "list", "extract"], value_name = "task", )] task: Option, + /// extracts the dump in the specified area + #[clap( + short = 'a', alias = "area", long, value_name = "area", + conflicts_with_all = &["simulation", "list"] + )] + extract: Option, + /// extracts every available dump #[clap( long, - conflicts_with_all = &["simulation", "list", "area", "task"] + alias = "all", + conflicts_with_all = &["simulation", "list", "extract", "task"] )] - all: bool, - - #[clap(short, long, conflicts_with_all = &["simulation", "list"])] - area: Option, + extract_all: bool, /// leave the target halted #[clap(long, conflicts_with = "simulation")] leave_halted: bool, - #[clap(long, short, conflicts_with_all = &["simulation", "area"])] + /// list all dump areas + #[clap(long, short, conflicts_with_all = &["simulation", "extract"])] list: bool, + /// print dump breakdown + #[clap(long)] + print_dump_breakdown: bool, + dumpfile: Option, } @@ -208,7 +218,7 @@ fn emulate_dump( let mut rnum = 0; - let r = humpty::dump::( + let r = humpty::dump::( base, task, || { @@ -327,6 +337,74 @@ fn get_dump_agent<'a>( } } +fn read_dump<'a>( + agent: &mut Box, + area: Option, + out: &mut DumpAgentCore, + subargs: &DumpArgs, +) -> Result> { + let (task, breakdown) = agent.read_dump(area, out, true)?; + + if subargs.print_dump_breakdown { + print_dump_breakdown(&breakdown); + } + + Ok(task) +} + +fn print_dump_breakdown(breakdown: &DumpBreakdown) { + let w = 30; + let w2 = 10; + + let mut total = 0; + + humility::msg!("Dump breakdown:"); + + let print_val = |str, val| { + humility::msg!(" {str: {val}"); + }; + + let print_signed_val = |str, val| { + humility::msg!(" {str: {val}"); + }; + + let mut print_perc = |str, val, acct| { + let (label, perc) = if acct { + total += val; + ("consumed", (val as f32 / breakdown.used as f32) * 100.0) + } else { + ("total", (val as f32 / breakdown.total as f32) * 100.0) + }; + + humility::msg!(" {str: {val:6.2}% of {label}"); + }; + + print_val("total dump area", breakdown.total); + print_perc("dump area consumed", breakdown.used, false); + print_perc("dump headers", breakdown.headers, true); + print_perc("segment headers", breakdown.segment_headers, true); + print_perc("register headers + data", breakdown.registers, true); + print_perc("data headers", breakdown.data_headers, true); + print_perc("data", breakdown.compressed, true); + print_perc("data padding", breakdown.segment_padding, true); + print_perc("orphaned", breakdown.orphaned, true); + + humility::msg!( + " -------------------------------------\ + --------------------------------" + ); + + let unaccounted = breakdown.used as i32 - total as i32; + print_val("total accounted", total); + print_signed_val("unaccounted dump area", unaccounted); + + humility::msg!(""); + + print_val("uncompressed size", breakdown.uncompressed); + print_val("compressed size", breakdown.compressed); + print_val("data expansion", breakdown.inverted); +} + fn dump_via_agent( hubris: &HubrisArchive, core: &mut dyn Core, @@ -334,7 +412,7 @@ fn dump_via_agent( ) -> Result<()> { let mut out = DumpAgentCore::new(HubrisFlashMap::new(hubris)?); let started = Some(Instant::now()); - let mut area = subargs.area.map(DumpArea::ByIndex); + let mut area = subargs.extract.map(DumpArea::ByIndex); // // Our task can come from a couple of different spots: we can either @@ -473,7 +551,7 @@ fn dump_via_agent( let mut agent = get_dump_agent(hubris, core, subargs)?; let header = agent.read_dump_header()?; - if !subargs.force_read && subargs.area.is_none() { + if !subargs.force_read && subargs.extract.is_none() { if header.dumper != humpty::DUMPER_NONE && !subargs.initialize_dump_agent && !subargs.force_overwrite @@ -481,9 +559,7 @@ fn dump_via_agent( { bail!( "there appears to already be one or more dumps in situ; \ - list them with --list, clear them with \ - --initialize-dump-agent, or force them to be overwritten \ - with --force-overwrite" + list them with --list and extract them with --extract-all" ) } @@ -533,7 +609,7 @@ fn dump_via_agent( emulate_dump(agent.core(), task, address, total)?; agent.core().run()?; humility::msg!("core resumed"); - } else if !subargs.force_read && subargs.area.is_none() { + } else if !subargs.force_read && subargs.extract.is_none() { if subargs.force_manual_initiation { agent.core().halt()?; humility::msg!("leaving core halted"); @@ -559,15 +635,47 @@ fn dump_via_agent( agent.core().set_timeout(std::time::Duration::new(60, 0))?; // - // Tell the thing to take a dump + // Tell the thing to take a dump. // - agent.take_dump()?; + if let Err(err) = agent.take_dump() { + // + // If that fails, it may be because we ran out of space. Check + // our dump headers; if all of them are consumed, assume + // that we ran out of space -- and if any of them are consumed, + // process whatever we find (some dump is better than none!) and + // warn accordingly. + // + if let Ok(all) = agent.read_dump_headers(true) { + let c = all + .iter() + .filter(|&&(h, _)| h.dumper != humpty::DUMPER_NONE) + .count(); + + if c == all.len() { + humility::warn!( + "dump has indicated failure ({err:?}), but this is \ + likely due to space exhaustion; \ + dump will be extracted but may be incomplete!" + ); + } else if c != 0 { + humility::warn!( + "dump has indicated failure ({err:?}), but some dump \ + contents appear to have been written; \ + dump will be extracted but may be incomplete!" + ); + } else { + return Err(err); + } + } else { + return Err(err); + } + } } // // If we're here, we have a dump in situ -- time to pull it. // - task = agent.read_dump(area, &mut out, true)?; + task = read_dump(&mut agent, area, &mut out, subargs)?; // // If this was a whole-system dump, we will leave our state initialized @@ -610,10 +718,11 @@ fn dump_task_via_agent( bail!("cannot dump supervisor"); } let area = agent.dump_task(ndx)?; - let task = agent.read_dump( + let task = read_dump( + &mut agent, Some(DumpArea::ByIndex(area as usize)), &mut out, - true, + subargs, )?; assert!(task.is_some()); hubris.dump(&mut out, task, subargs.dumpfile.as_deref(), started)?; @@ -710,11 +819,13 @@ fn dump_all( let mut out = DumpAgentCore::new(HubrisFlashMap::new(hubris)?); let started = Some(Instant::now()); - let task = agent.read_dump( + let task = read_dump( + &mut agent, Some(DumpArea::ByIndex(*area)), &mut out, - true, + subargs, )?; + assert!(task.is_some()); hubris.dump(&mut out, task, Some(&dumpfile), started)?; } @@ -752,7 +863,7 @@ fn dumpcmd(context: &mut ExecutionContext) -> Result<()> { bail!("can only force the dump agent when attached via debug probe"); } - if subargs.all { + if subargs.extract_all { dump_all(hubris, core, &subargs) } else if subargs.list { dump_list(hubris, core, &subargs) @@ -766,7 +877,7 @@ fn dumpcmd(context: &mut ExecutionContext) -> Result<()> { } else if core.is_net() || subargs.force_dump_agent || subargs.force_read - || subargs.area.is_some() + || subargs.extract.is_some() { dump_via_agent(hubris, core, &subargs) } else { @@ -797,7 +908,7 @@ pub fn init() -> Command { run: dumpcmd, kind: CommandKind::Attached { archive: Archive::Required, - attach: Attach::LiveOnly, + attach: Attach::Any, validate: Validate::Match, }, } diff --git a/cmd/rebootleby/src/lib.rs b/cmd/rebootleby/src/lib.rs index 70626ab40..61c3ca0b6 100644 --- a/cmd/rebootleby/src/lib.rs +++ b/cmd/rebootleby/src/lib.rs @@ -42,13 +42,20 @@ fn rebootleby(context: &mut ExecutionContext) -> Result<()> { let mut zip = ZipArchive::new(bundle_reader).context("opening bundle file as ZIP")?; - let img_bootleby = { + let img_bootleby = if zip.file_names().any(|x| x == "bootleby.bin") { let mut entry = zip .by_name("bootleby.bin") .context("can't find bootleby.bin in bundle")?; let mut data = vec![]; entry.read_to_end(&mut data).context("reading bootleby.bin")?; data + } else { + let mut entry = zip + .by_name("img/final.bin") + .context("can't find bootleby.bin or img/final.bin in bundle")?; + let mut data = vec![]; + entry.read_to_end(&mut data).context("reading bootleby.bin")?; + data }; let img_cmpa = { let mut entry = diff --git a/cmd/rendmp/src/lib.rs b/cmd/rendmp/src/lib.rs index 7c87790d8..e032272c3 100644 --- a/cmd/rendmp/src/lib.rs +++ b/cmd/rendmp/src/lib.rs @@ -2693,6 +2693,7 @@ fn rendmp(context: &mut ExecutionContext) -> Result<()> { } } } + bar.finish_and_clear(); } Ok(()) diff --git a/cmd/rpc/src/lib.rs b/cmd/rpc/src/lib.rs index 651837789..c8d1d4e69 100644 --- a/cmd/rpc/src/lib.rs +++ b/cmd/rpc/src/lib.rs @@ -60,7 +60,7 @@ use std::collections::BTreeSet; use std::net::{IpAddr, Ipv6Addr, ToSocketAddrs, UdpSocket}; use std::time::{Duration, Instant}; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, bail, Context, Result}; use clap::{ArgGroup, IntoApp, Parser}; use colored::Colorize; use hubpack::SerializedSize; @@ -312,9 +312,16 @@ fn rpc_listen(rpc_args: &RpcArgs) -> Result> { .collect::>(); let mut seen = BTreeSet::new(); - for t in threads { - let out = t.join().unwrap()?; - seen.extend(out.into_iter()); + for (i, t) in threads.into_iter().enumerate() { + let port = ports[i]; + match t.join().unwrap() { + Ok(out) => { + seen.extend(out.into_iter()); + } + Err(e) => { + humility::warn!("thread for port {port} failed: {e:?}"); + } + } } if seen.is_empty() { humility::msg!("timed out, exiting"); @@ -410,8 +417,11 @@ impl<'a> RpcClient<'a> { let mut packet = header.as_bytes().to_vec(); packet.extend(payload.iter()); - self.socket.send(&packet)?; - let n = self.socket.recv(&mut self.buf)?; + self.socket.send(&packet).context("unable to send packet")?; + let n = self + .socket + .recv(&mut self.buf) + .context("unable to receive packet")?; let buf = &self.buf[..n]; if buf[0] != 0 { @@ -453,10 +463,12 @@ fn rpc_call( let timeout = Duration::from_millis(u64::from(timeout)); for &ip in &ips { - let mut client = RpcClient::new(hubris, ip, timeout)?; - let result = client.call(op, args)?; + let mut client = RpcClient::new(hubris, ip, timeout) + .context("unable to create RpcClient")?; + let result = client.call(op, args).context("unable to make call")?; print!("{:25} ", ip); - humility_hiffy::hiffy_print_result(hubris, op, result)?; + humility_hiffy::hiffy_print_result(hubris, op, result) + .context("unable to print result")?; } Ok(()) @@ -547,7 +559,8 @@ fn rpc_run(context: &mut ExecutionContext) -> Result<()> { }; let op = idol::IdolOperation::new(hubris, func[0], func[1], task)?; - rpc_call(hubris, &op, &args, ips, subargs.timeout)?; + rpc_call(hubris, &op, &args, ips, subargs.timeout) + .context("failed to make RPC call")?; return Ok(()); } diff --git a/cmd/tofino-eeprom/src/lib.rs b/cmd/tofino-eeprom/src/lib.rs index adbd8f50d..fdc22060d 100644 --- a/cmd/tofino-eeprom/src/lib.rs +++ b/cmd/tofino-eeprom/src/lib.rs @@ -99,7 +99,7 @@ impl<'a> EepromHandler<'a> { } bar.set_position(offset as u64); } - + bar.finish_and_clear(); Ok(out) } diff --git a/humility-core/src/core.rs b/humility-core/src/core.rs index d4f355633..84a628c7f 100644 --- a/humility-core/src/core.rs +++ b/humility-core/src/core.rs @@ -1514,6 +1514,32 @@ fn get_usb_probe(index: Option) -> Result { } } +fn open_probe_from_selector( + selector: probe_rs::DebugProbeSelector, +) -> Result { + let res = probe_rs::Probe::open(selector.clone()); + + if let Err(probe_rs::DebugProbeError::ProbeCouldNotBeCreated( + probe_rs::ProbeCreationError::NotFound, + )) = res + { + if selector.serial_number.is_some() { + bail!( + "Could not find probe {}.\n\ + \n\ + Because a serial number is present, this may be due to not \ + running humility with permission to read USB device serial \ + numbers; if not root already, run again as root?", + selector + ); + } else { + bail!("Could not find probe {}.", selector); + } + } + + res.map_err(|e| e.into()) +} + #[rustfmt::skip::macros(anyhow, bail)] pub fn attach_to_probe(probe: &str) -> Result> { let (probe, index) = parse_probe(probe); @@ -1556,7 +1582,7 @@ pub fn attach_to_probe(probe: &str) -> Result> { let vid = selector.vendor_id; let pid = selector.product_id; let serial = selector.serial_number.clone(); - let probe = probe_rs::Probe::open(selector)?; + let probe = open_probe_from_selector(selector)?; let name = probe.get_name(); crate::msg!("Opened {vidpid} via {name}"); @@ -1669,7 +1695,7 @@ pub fn attach_to_chip( let pid = selector.product_id; let serial = selector.serial_number.clone(); - let probe = probe_rs::Probe::open(selector)?; + let probe = open_probe_from_selector(selector)?; let name = probe.get_name(); // diff --git a/humility-dump-agent/src/lib.rs b/humility-dump-agent/src/lib.rs index 07c16c723..11e492261 100644 --- a/humility-dump-agent/src/lib.rs +++ b/humility-dump-agent/src/lib.rs @@ -88,6 +88,51 @@ pub enum DumpArea { //////////////////////////////////////////////////////////////////////////////// +#[derive(Default, Debug)] +pub struct DumpBreakdown { + pub total: u32, + pub used: u32, + pub headers: u32, + pub segment_headers: u32, + pub data_headers: u32, + pub registers: u32, + + pub compressed: u32, + pub uncompressed: u32, + pub segment_padding: u32, + pub inverted: u32, + pub orphaned: u32, +} + +impl DumpBreakdown { + fn new(headers: Vec, total: u32) -> Self { + Self { + total, + used: headers.iter().fold(0, |sum, header| sum + header.length), + headers: (headers.len() * size_of::()) as u32, + segment_headers: headers.iter().fold(0, |sum, header| { + sum + header.nsegments as u32 + * size_of::() as u32 + }), + orphaned: headers + .iter() + .fold(0, |sum, header| sum + (header.length - header.written)), + ..Default::default() + } + } + + fn add_data(&mut self, compressed: u16, uncompressed: u16, padding: usize) { + self.data_headers += size_of::() as u32; + self.compressed += compressed as u32; + self.uncompressed += uncompressed as u32; + self.segment_padding += padding as u32; + + if compressed > uncompressed { + self.inverted += (compressed - uncompressed) as u32; + } + } +} + // // When using the dump agent, we create our own ersatz Core // @@ -154,14 +199,17 @@ impl DumpAgentCore { } } - pub fn process_dump( + fn process_dump( &mut self, - header: &DumpAreaHeader, + headers: Vec, + total: u32, dump: &Vec, task: Option, - ) -> Result<()> { + ) -> Result { + let header = headers[0]; let nsegments = header.nsegments; let mut offset = nsegments as usize * size_of::(); + let mut breakdown = DumpBreakdown::new(headers, total); if offset > dump.len() { bail!("in situ dump is short; missing {nsegments} segments"); @@ -211,6 +259,7 @@ impl DumpAgentCore { ); } + breakdown.registers += size_of::() as u32; offset += size_of::(); continue; } @@ -239,6 +288,12 @@ impl DumpAgentCore { { offset += 1; } + + breakdown.add_data( + data.compressed_length, + data.uncompressed_length, + offset - limit, + ); } DumpSegment::Unknown(signature) => { @@ -250,7 +305,7 @@ impl DumpAgentCore { } } - Ok(()) + Ok(breakdown) } } @@ -472,8 +527,12 @@ pub trait DumpAgentExt { // Collect chunks into a single vec let mut out_data: Vec = out.into_iter().flatten().collect(); - // Because we read in chunks, we may have extra data at the end here - out_data.resize(offset, 0u8); + // + // Because we read in chunks, we may have extra data at the end here; + // truncate our returned data to be the length of the written data + // minus the size of the header (it isn't present in out_data). + // + out_data.truncate((header.written as usize).saturating_sub(size)); Ok((header, out_data)) } @@ -516,15 +575,16 @@ pub trait DumpAgentExt { area: Option, out: &mut DumpAgentCore, verbose: bool, - ) -> Result> { - let (base, headers, task) = { - // Read dump headers until the first empty header (DUMPER_NONE) - let all = self.read_dump_headers(false)?; + ) -> Result<(Option, DumpBreakdown)> { + let (total, base, headers, task) = { + let all = self.read_dump_headers(true)?; - if all.is_empty() { + if !all.iter().any(|&(h, _)| h.dumper != humpty::DUMPER_NONE) { bail!("no dumps are present"); } + let total = all.iter().fold(0, |sum, &(h, _)| sum + h.length); + let area = match area { None => None, Some(DumpArea::ByIndex(ndx)) => Some(ndx), @@ -538,8 +598,10 @@ pub trait DumpAgentExt { match area { None | Some(0) if all[0].1.is_none() => ( + total, 0usize, all.iter() + .filter(|&(h, _)| h.dumper != humpty::DUMPER_NONE) .map(|(h, _t)| *h) .collect::>(), None, @@ -552,7 +614,7 @@ pub trait DumpAgentExt { bail!("area {ndx} is invalid (--list to list)"); } Some((task, headers)) => { - (ndx, headers.clone(), Some(*task)) + (total, ndx, headers.clone(), Some(*task)) } } } @@ -563,11 +625,12 @@ pub trait DumpAgentExt { } }; - let total = headers.iter().fold(0, |sum, header| sum + header.written); + let written = + headers.iter().fold(0, |sum, header| sum + header.written); let started = Instant::now(); let bar = if verbose { - let bar = ProgressBar::new(total as u64); + let bar = ProgressBar::new(written as u64); bar.set_style(ProgressStyle::default_bar().template( "humility: pulling [{bar:30}] {bytes}/{total_bytes}", )); @@ -596,7 +659,7 @@ pub trait DumpAgentExt { if verbose { msg!( "pulled {} in {}", - HumanBytes(total as u64), + HumanBytes(written as u64), HumanDuration(started.elapsed()) ); } @@ -604,9 +667,9 @@ pub trait DumpAgentExt { // // We have a dump! On to processing... // - out.process_dump(&headers[0], &contents, task)?; + let breakdown = out.process_dump(headers, total, &contents, task)?; - Ok(task) + Ok((task, breakdown)) } } diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 49031ad5a..d8c2bb5b9 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -76,7 +76,7 @@ fn make_tests(tests: &[Test], kind: Kind) -> Result<()> { if let Some(f) = path.file_name() { if let Some(s) = f.to_str() { - if let Some(name) = s.strip_prefix(&kind.prefix()) { + if let Some(name) = s.strip_prefix(kind.prefix()) { input.push((name.to_string(), s.to_string())); } } @@ -112,7 +112,7 @@ fn make_tests(tests: &[Test], kind: Kind) -> Result<()> { let testcmd = if let Some(arg) = test.arg { format!("{} {arg}", test.cmd) } else { - format!("{}", test.cmd) + test.cmd.to_string() }; writeln!( diff --git a/tests/cmd/chip.trycmd b/tests/cmd/chip.trycmd index 3b6c69ccd..5704a7c8d 100644 --- a/tests/cmd/chip.trycmd +++ b/tests/cmd/chip.trycmd @@ -13,7 +13,7 @@ For more information try --help ``` $ humility --chip this-can-be-anything -V -humility 0.11.9 +humility 0.11.11 ``` @@ -28,7 +28,7 @@ For more information try --help ``` $ humility -c apx432 -V -humility 0.11.9 +humility 0.11.11 ``` diff --git a/tests/cmd/version.trycmd b/tests/cmd/version.trycmd index 73553d411..662cea2fa 100644 --- a/tests/cmd/version.trycmd +++ b/tests/cmd/version.trycmd @@ -2,7 +2,7 @@ Long version flag: ``` $ humility --version -humility 0.11.9 +humility 0.11.11 ``` @@ -10,6 +10,6 @@ Short version flag: ``` $ humility -V -humility 0.11.9 +humility 0.11.11 ```