style: cargo fmt

This commit is contained in:
Ciphercat 2026-03-05 18:33:20 +00:00
commit dc1de5b344
9 changed files with 157 additions and 61 deletions

View file

@ -2,8 +2,8 @@ use std::time::Duration;
use serde::Deserialize; use serde::Deserialize;
use crate::api::types::{Interval, Ohlc, Period, Quote};
use crate::api::MarketDataProvider; use crate::api::MarketDataProvider;
use crate::api::types::{Interval, Ohlc, Period, Quote};
use crate::error::IdxError; use crate::error::IdxError;
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"; const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
@ -29,15 +29,16 @@ impl YahooProvider {
) )
} }
fn fetch_chart(&self, symbol: &str, period: &Period, interval: &Interval) -> Result<ChartResponse, IdxError> { fn fetch_chart(
&self,
symbol: &str,
period: &Period,
interval: &Interval,
) -> Result<ChartResponse, IdxError> {
let mut wait = Duration::from_millis(250); let mut wait = Duration::from_millis(250);
for attempt in 0..3 { for attempt in 0..3 {
let url = Self::chart_url(symbol, period, interval); let url = Self::chart_url(symbol, period, interval);
let response = self let response = self.agent.get(&url).header("User-Agent", USER_AGENT).call();
.agent
.get(&url)
.header("User-Agent", USER_AGENT)
.call();
match response { match response {
Ok(ok) => { Ok(ok) => {
return ok return ok
@ -72,7 +73,12 @@ impl MarketDataProvider for YahooProvider {
parse_quote(symbol, &chart) parse_quote(symbol, &chart)
} }
fn history(&self, symbol: &str, period: &Period, interval: &Interval) -> Result<Vec<Ohlc>, IdxError> { fn history(
&self,
symbol: &str,
period: &Period,
interval: &Interval,
) -> Result<Vec<Ohlc>, IdxError> {
let chart = self.fetch_chart(symbol, period, interval)?; let chart = self.fetch_chart(symbol, period, interval)?;
parse_history(&chart) parse_history(&chart)
} }
@ -92,12 +98,15 @@ fn parse_quote(symbol: &str, chart: &ChartResponse) -> Result<Quote, IdxError> {
.and_then(|r| r.first()) .and_then(|r| r.first())
.ok_or(IdxError::ProviderUnavailable)?; .ok_or(IdxError::ProviderUnavailable)?;
let meta = result.meta.as_ref().ok_or(IdxError::ProviderUnavailable)?; let meta = result.meta.as_ref().ok_or(IdxError::ProviderUnavailable)?;
let price = meta.regular_market_price.ok_or(IdxError::SymbolNotFound(symbol.to_string()))?; let price = meta
.regular_market_price
.ok_or(IdxError::SymbolNotFound(symbol.to_string()))?;
let prev_close = meta.previous_close.or(meta.chart_previous_close); let prev_close = meta.previous_close.or(meta.chart_previous_close);
let change = prev_close.map_or(0.0, |p| price - p); let change = prev_close.map_or(0.0, |p| price - p);
let change_pct = prev_close.map_or(0.0, |p| if p != 0.0 { (change / p) * 100.0 } else { 0.0 }); let change_pct = prev_close.map_or(0.0, |p| if p != 0.0 { (change / p) * 100.0 } else { 0.0 });
let (week52_position, range_signal) = match (meta.fifty_two_week_low, meta.fifty_two_week_high) { let (week52_position, range_signal) = match (meta.fifty_two_week_low, meta.fifty_two_week_high)
{
(Some(low), Some(high)) if high > low => { (Some(low), Some(high)) if high > low => {
let pos = (price - low) / (high - low); let pos = (price - low) / (high - low);
let signal = if pos > 0.66 { let signal = if pos > 0.66 {
@ -141,7 +150,10 @@ fn parse_history(chart: &ChartResponse) -> Result<Vec<Ohlc>, IdxError> {
.as_ref() .as_ref()
.and_then(|r| r.first()) .and_then(|r| r.first())
.ok_or(IdxError::ProviderUnavailable)?; .ok_or(IdxError::ProviderUnavailable)?;
let timestamps = result.timestamp.as_ref().ok_or(IdxError::ProviderUnavailable)?; let timestamps = result
.timestamp
.as_ref()
.ok_or(IdxError::ProviderUnavailable)?;
let quote = result let quote = result
.indicators .indicators
.as_ref() .as_ref()
@ -151,11 +163,23 @@ fn parse_history(chart: &ChartResponse) -> Result<Vec<Ohlc>, IdxError> {
let mut out = Vec::new(); let mut out = Vec::new();
for (i, ts) in timestamps.iter().enumerate() { for (i, ts) in timestamps.iter().enumerate() {
let open = quote.open.as_ref().and_then(|v| v.get(i).copied().flatten()); let open = quote
let high = quote.high.as_ref().and_then(|v| v.get(i).copied().flatten()); .open
.as_ref()
.and_then(|v| v.get(i).copied().flatten());
let high = quote
.high
.as_ref()
.and_then(|v| v.get(i).copied().flatten());
let low = quote.low.as_ref().and_then(|v| v.get(i).copied().flatten()); let low = quote.low.as_ref().and_then(|v| v.get(i).copied().flatten());
let close = quote.close.as_ref().and_then(|v| v.get(i).copied().flatten()); let close = quote
let volume = quote.volume.as_ref().and_then(|v| v.get(i).copied().flatten()); .close
.as_ref()
.and_then(|v| v.get(i).copied().flatten());
let volume = quote
.volume
.as_ref()
.and_then(|v| v.get(i).copied().flatten());
if let (Some(open), Some(high), Some(low), Some(close), Some(volume)) = if let (Some(open), Some(high), Some(low), Some(close), Some(volume)) =
(open, high, low, close, volume) (open, high, low, close, volume)
@ -224,7 +248,9 @@ struct IndicatorQuote {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{parse_history, parse_history_from_str, parse_quote, parse_quote_from_str, ChartResponse}; use super::{
ChartResponse, parse_history, parse_history_from_str, parse_quote, parse_quote_from_str,
};
const SAMPLE: &str = r#"{ const SAMPLE: &str = r#"{
"chart": { "chart": {
@ -264,8 +290,10 @@ mod tests {
#[test] #[test]
fn parses_realistic_fixture_json() { fn parses_realistic_fixture_json() {
let quote_raw = std::fs::read_to_string("tests/fixtures/chart_bbca_1d.json").expect("fixture exists"); let quote_raw =
let history_raw = std::fs::read_to_string("tests/fixtures/chart_bbca_3mo.json").expect("fixture exists"); std::fs::read_to_string("tests/fixtures/chart_bbca_1d.json").expect("fixture exists");
let history_raw =
std::fs::read_to_string("tests/fixtures/chart_bbca_3mo.json").expect("fixture exists");
let quote = parse_quote_from_str("BBCA.JK", &quote_raw).expect("fixture quote parsed"); let quote = parse_quote_from_str("BBCA.JK", &quote_raw).expect("fixture quote parsed");
assert_eq!(quote.symbol, "BBCA.JK"); assert_eq!(quote.symbol, "BBCA.JK");

View file

@ -43,23 +43,39 @@ impl Cache {
Self { root } Self { root }
} }
pub fn get<T: DeserializeOwned>(
pub fn get<T: DeserializeOwned>(&self, data_type: &str, symbol: &str) -> Result<Option<T>, IdxError> { &self,
data_type: &str,
symbol: &str,
) -> Result<Option<T>, IdxError> {
let Some(entry): Option<CacheEntry<T>> = self.read_entry(data_type, symbol)? else { let Some(entry): Option<CacheEntry<T>> = self.read_entry(data_type, symbol)? else {
return Ok(None); return Ok(None);
}; };
let age = Utc::now().signed_duration_since(entry.fetched_at); let age = Utc::now().signed_duration_since(entry.fetched_at);
if age > chrono::Duration::from_std(Duration::from_secs(entry.ttl_secs)).map_err(|e| IdxError::CacheMiss(e.to_string()))? { if age
> chrono::Duration::from_std(Duration::from_secs(entry.ttl_secs))
.map_err(|e| IdxError::CacheMiss(e.to_string()))?
{
return Ok(None); return Ok(None);
} }
Ok(Some(entry.data)) Ok(Some(entry.data))
} }
pub fn get_stale<T: DeserializeOwned>(&self, data_type: &str, symbol: &str) -> Result<Option<T>, IdxError> { pub fn get_stale<T: DeserializeOwned>(
&self,
data_type: &str,
symbol: &str,
) -> Result<Option<T>, IdxError> {
Ok(self.read_entry::<T>(data_type, symbol)?.map(|e| e.data)) Ok(self.read_entry::<T>(data_type, symbol)?.map(|e| e.data))
} }
pub fn put<T: Serialize>(&self, data_type: &str, symbol: &str, data: &T, ttl_secs: u64) -> Result<(), IdxError> { pub fn put<T: Serialize>(
&self,
data_type: &str,
symbol: &str,
data: &T,
ttl_secs: u64,
) -> Result<(), IdxError> {
let path = self.entry_path(data_type, symbol); let path = self.entry_path(data_type, symbol);
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| IdxError::Io(e.to_string()))?; fs::create_dir_all(parent).map_err(|e| IdxError::Io(e.to_string()))?;
@ -70,7 +86,8 @@ impl Cache {
schema_version: SCHEMA_VERSION, schema_version: SCHEMA_VERSION,
data, data,
}; };
let raw = serde_json::to_string_pretty(&entry).map_err(|e| IdxError::ParseError(e.to_string()))?; let raw = serde_json::to_string_pretty(&entry)
.map_err(|e| IdxError::ParseError(e.to_string()))?;
fs::write(path, raw).map_err(|e| IdxError::Io(e.to_string())) fs::write(path, raw).map_err(|e| IdxError::Io(e.to_string()))
} }
@ -88,7 +105,8 @@ impl Cache {
files += 1; files += 1;
total_size += meta.len(); total_size += meta.len();
if let Ok(raw) = fs::read_to_string(p) if let Ok(raw) = fs::read_to_string(p)
&& let Ok(entry) = serde_json::from_str::<CacheEntry<serde_json::Value>>(&raw) && let Ok(entry) =
serde_json::from_str::<CacheEntry<serde_json::Value>>(&raw)
{ {
oldest = Some(oldest.map_or(entry.fetched_at, |o| o.min(entry.fetched_at))); oldest = Some(oldest.map_or(entry.fetched_at, |o| o.min(entry.fetched_at)));
newest = Some(newest.map_or(entry.fetched_at, |n| n.max(entry.fetched_at))); newest = Some(newest.map_or(entry.fetched_at, |n| n.max(entry.fetched_at)));
@ -132,7 +150,11 @@ impl Cache {
Ok(()) Ok(())
} }
fn read_entry<T: DeserializeOwned>(&self, data_type: &str, symbol: &str) -> Result<Option<CacheEntry<T>>, IdxError> { fn read_entry<T: DeserializeOwned>(
&self,
data_type: &str,
symbol: &str,
) -> Result<Option<CacheEntry<T>>, IdxError> {
let path = self.entry_path(data_type, symbol); let path = self.entry_path(data_type, symbol);
if !path.exists() { if !path.exists() {
return Ok(None); return Ok(None);
@ -178,20 +200,29 @@ mod tests {
let root = tmp(); let root = tmp();
let cache = Cache::with_root(root.clone()); let cache = Cache::with_root(root.clone());
cache.put("quote", "BBCA.JK", &T { v: 7 }, 300).expect("cache write"); cache
.put("quote", "BBCA.JK", &T { v: 7 }, 300)
.expect("cache write");
let fresh: Option<T> = cache.get("quote", "BBCA.JK").expect("cache read fresh"); let fresh: Option<T> = cache.get("quote", "BBCA.JK").expect("cache read fresh");
assert_eq!(fresh, Some(T { v: 7 })); assert_eq!(fresh, Some(T { v: 7 }));
let path = root.join("quote/BBCA.JK.json"); let path = root.join("quote/BBCA.JK.json");
let mut entry: CacheEntry<T> = serde_json::from_str(&fs::read_to_string(&path).expect("read cache file")) let mut entry: CacheEntry<T> =
serde_json::from_str(&fs::read_to_string(&path).expect("read cache file"))
.expect("parse cache entry"); .expect("parse cache entry");
entry.fetched_at = chrono::Utc::now() - chrono::Duration::seconds(1000); entry.fetched_at = chrono::Utc::now() - chrono::Duration::seconds(1000);
fs::write(&path, serde_json::to_string(&entry).expect("serialize entry")).expect("write old entry"); fs::write(
&path,
serde_json::to_string(&entry).expect("serialize entry"),
)
.expect("write old entry");
let expired: Option<T> = cache.get("quote", "BBCA.JK").expect("cache read expired"); let expired: Option<T> = cache.get("quote", "BBCA.JK").expect("cache read expired");
assert_eq!(expired, None); assert_eq!(expired, None);
let stale: Option<T> = cache.get_stale("quote", "BBCA.JK").expect("cache read stale"); let stale: Option<T> = cache
.get_stale("quote", "BBCA.JK")
.expect("cache read stale");
assert_eq!(stale, Some(T { v: 7 })); assert_eq!(stale, Some(T { v: 7 }));
} }
} }

View file

@ -1,7 +1,7 @@
use clap::{Args, Subcommand}; use clap::{Args, Subcommand};
use crate::api::types::{Interval, Period};
use crate::api::MarketDataProvider; use crate::api::MarketDataProvider;
use crate::api::types::{Interval, Period};
use crate::cache::Cache; use crate::cache::Cache;
use crate::config::IdxConfig; use crate::config::IdxConfig;
use crate::error::IdxError; use crate::error::IdxError;
@ -15,7 +15,9 @@ pub struct StocksCmd {
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum StocksSubcommand { pub enum StocksSubcommand {
Quote { symbols: Vec<String> }, Quote {
symbols: Vec<String>,
},
History { History {
symbol: String, symbol: String,
#[arg(long, value_enum, default_value_t = Period::ThreeMonths)] #[arg(long, value_enum, default_value_t = Period::ThreeMonths)]
@ -39,9 +41,7 @@ pub fn handle(
let mut quotes = Vec::new(); let mut quotes = Vec::new();
for sym in symbols.iter().flat_map(|s| s.split(',')) { for sym in symbols.iter().flat_map(|s| s.split(',')) {
let resolved = crate::api::resolve_symbol(sym, &config.exchange); let resolved = crate::api::resolve_symbol(sym, &config.exchange);
if !no_cache if !no_cache && let Some(q) = cache.get("quote", &resolved)? {
&& let Some(q) = cache.get("quote", &resolved)?
{
quotes.push(q); quotes.push(q);
continue; continue;
} }
@ -61,10 +61,10 @@ pub fn handle(
quotes.push(q); quotes.push(q);
} }
Err(err) => { Err(err) => {
if !no_cache if !no_cache && let Some(stale) = cache.get_stale("quote", &resolved)? {
&& let Some(stale) = cache.get_stale("quote", &resolved)? eprintln!(
{ "warning: network failed, serving stale cache for {resolved}"
eprintln!("warning: network failed, serving stale cache for {resolved}"); );
quotes.push(stale); quotes.push(stale);
continue; continue;
} }
@ -82,13 +82,17 @@ pub fn handle(
let resolved = crate::api::resolve_symbol(symbol, &config.exchange); let resolved = crate::api::resolve_symbol(symbol, &config.exchange);
let key = format!("{}-{}", period.as_str(), interval.as_str()); let key = format!("{}-{}", period.as_str(), interval.as_str());
if !no_cache if !no_cache
&& let Some(history) = cache.get::<Vec<crate::api::types::Ohlc>>("history", &format!("{resolved}-{key}"))? && let Some(history) = cache
.get::<Vec<crate::api::types::Ohlc>>("history", &format!("{resolved}-{key}"))?
{ {
return render_history(&resolved, &history, &config.output); return render_history(&resolved, &history, &config.output);
} }
if offline { if offline {
let stale = cache let stale = cache
.get_stale::<Vec<crate::api::types::Ohlc>>("history", &format!("{resolved}-{key}"))? .get_stale::<Vec<crate::api::types::Ohlc>>(
"history",
&format!("{resolved}-{key}"),
)?
.ok_or_else(|| IdxError::CacheMiss(format!("history/{resolved}-{key}")))?; .ok_or_else(|| IdxError::CacheMiss(format!("history/{resolved}-{key}")))?;
return render_history(&resolved, &stale, &config.output); return render_history(&resolved, &stale, &config.output);
} }
@ -96,13 +100,21 @@ pub fn handle(
match provider.history(&resolved, period, interval) { match provider.history(&resolved, period, interval) {
Ok(history) => { Ok(history) => {
if !no_cache { if !no_cache {
cache.put("history", &format!("{resolved}-{key}"), &history, config.quote_ttl)?; cache.put(
"history",
&format!("{resolved}-{key}"),
&history,
config.quote_ttl,
)?;
} }
render_history(&resolved, &history, &config.output) render_history(&resolved, &history, &config.output)
} }
Err(err) => { Err(err) => {
if !no_cache if !no_cache
&& let Some(stale) = cache.get_stale::<Vec<crate::api::types::Ohlc>>("history", &format!("{resolved}-{key}"))? && let Some(stale) = cache.get_stale::<Vec<crate::api::types::Ohlc>>(
"history",
&format!("{resolved}-{key}"),
)?
{ {
eprintln!("warning: network failed, serving stale cache for {resolved}"); eprintln!("warning: network failed, serving stale cache for {resolved}");
return render_history(&resolved, &stale, &config.output); return render_history(&resolved, &stale, &config.output);

View file

@ -89,7 +89,8 @@ impl IdxConfig {
let path = config_path()?; let path = config_path()?;
if path.exists() { if path.exists() {
let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?; let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?;
let parsed: FileConfig = toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?; let parsed: FileConfig =
toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?;
if let Some(general) = parsed.general { if let Some(general) = parsed.general {
if let Some(exchange) = general.exchange { if let Some(exchange) = general.exchange {
cfg.exchange = exchange; cfg.exchange = exchange;
@ -142,7 +143,8 @@ pub fn get_config_value(key: &str) -> Result<Option<String>, IdxError> {
return Ok(None); return Ok(None);
} }
let raw = fs::read_to_string(path).map_err(|e| IdxError::Io(e.to_string()))?; let raw = fs::read_to_string(path).map_err(|e| IdxError::Io(e.to_string()))?;
let value: toml::Value = toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?; let value: toml::Value =
toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?;
let mut cur = &value; let mut cur = &value;
for part in key.split('.') { for part in key.split('.') {
@ -157,7 +159,8 @@ pub fn get_config_value(key: &str) -> Result<Option<String>, IdxError> {
pub fn set_config_value(key: &str, value: &str) -> Result<(), IdxError> { pub fn set_config_value(key: &str, value: &str) -> Result<(), IdxError> {
let path = ensure_default_config()?; let path = ensure_default_config()?;
let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?; let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?;
let mut root: toml::Value = toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?; let mut root: toml::Value =
toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?;
let mut parts = key.split('.').peekable(); let mut parts = key.split('.').peekable();
let mut current = root let mut current = root
@ -180,7 +183,10 @@ pub fn set_config_value(key: &str, value: &str) -> Result<(), IdxError> {
} }
} }
fs::write(&path, toml::to_string_pretty(&root).map_err(|e| IdxError::ConfigError(e.to_string()))?) fs::write(
&path,
toml::to_string_pretty(&root).map_err(|e| IdxError::ConfigError(e.to_string()))?,
)
.map_err(|e| IdxError::Io(e.to_string())) .map_err(|e| IdxError::Io(e.to_string()))
} }

View file

@ -40,7 +40,13 @@ fn run() -> Result<(), IdxError> {
} }
Commands::Stocks(stocks) => { Commands::Stocks(stocks) => {
let provider = default_provider(); let provider = default_provider();
if let Err(err) = cli::stocks::handle(stocks, &config, provider.as_ref(), cli.offline, cli.no_cache) { if let Err(err) = cli::stocks::handle(
stocks,
&config,
provider.as_ref(),
cli.offline,
cli.no_cache,
) {
emit_error(&err, &config.output); emit_error(&err, &config.output);
return Err(err); return Err(err);
} }

View file

@ -3,7 +3,8 @@ use serde::Serialize;
use crate::error::IdxError; use crate::error::IdxError;
pub fn print_json<T: Serialize + ?Sized>(value: &T) -> Result<(), IdxError> { pub fn print_json<T: Serialize + ?Sized>(value: &T) -> Result<(), IdxError> {
let out = serde_json::to_string_pretty(value).map_err(|e| IdxError::ParseError(e.to_string()))?; let out =
serde_json::to_string_pretty(value).map_err(|e| IdxError::ParseError(e.to_string()))?;
println!("{out}"); println!("{out}");
Ok(()) Ok(())
} }

View file

@ -15,14 +15,22 @@ pub enum OutputFormat {
Json, Json,
} }
pub fn render_quotes(quotes: &[Quote], format: &OutputFormat, no_color: bool) -> Result<(), IdxError> { pub fn render_quotes(
quotes: &[Quote],
format: &OutputFormat,
no_color: bool,
) -> Result<(), IdxError> {
match format { match format {
OutputFormat::Table => table::print_quotes(quotes, no_color), OutputFormat::Table => table::print_quotes(quotes, no_color),
OutputFormat::Json => json::print_json(quotes), OutputFormat::Json => json::print_json(quotes),
} }
} }
pub fn render_history(symbol: &str, history: &[Ohlc], format: &OutputFormat) -> Result<(), IdxError> { pub fn render_history(
symbol: &str,
history: &[Ohlc],
format: &OutputFormat,
) -> Result<(), IdxError> {
match format { match format {
OutputFormat::Table => table::print_history(symbol, history), OutputFormat::Table => table::print_history(symbol, history),
OutputFormat::Json => json::print_json(history), OutputFormat::Json => json::print_json(history),
@ -38,7 +46,10 @@ pub fn emit_error(err: &IdxError, format: &OutputFormat) {
"code": format!("{:?}", err.code()).to_uppercase(), "code": format!("{:?}", err.code()).to_uppercase(),
"message": err.to_string() "message": err.to_string()
}); });
eprintln!("{}", serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string())); eprintln!(
"{}",
serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string())
);
} }
} }
} }

View file

@ -1,4 +1,4 @@
use comfy_table::{presets::UTF8_FULL, Cell, Color, ContentArrangement, Table}; use comfy_table::{Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use crate::api::types::{Ohlc, Quote}; use crate::api::types::{Ohlc, Quote};
@ -39,7 +39,11 @@ pub fn print_quotes(quotes: &[Quote], no_color: bool) -> Result<(), IdxError> {
Cell::new(format!("{:+.2}", q.change)), Cell::new(format!("{:+.2}", q.change)),
pct_cell, pct_cell,
Cell::new(format_idr(q.volume as f64)), Cell::new(format_idr(q.volume as f64)),
Cell::new(q.market_cap.map(format_idr).unwrap_or_else(|| "-".to_string())), Cell::new(
q.market_cap
.map(format_idr)
.unwrap_or_else(|| "-".to_string()),
),
]); ]);
} }

View file

@ -1,6 +1,6 @@
use std::fs; use std::fs;
use assert_cmd::{cargo::cargo_bin, Command}; use assert_cmd::{Command, cargo::cargo_bin};
use predicates::prelude::*; use predicates::prelude::*;
fn bin() -> Command { fn bin() -> Command {
@ -16,10 +16,7 @@ fn test_env_dir(name: &str) -> std::path::PathBuf {
#[test] #[test]
fn help_works() { fn help_works() {
bin() bin().arg("--help").assert().success();
.arg("--help")
.assert()
.success();
} }
#[test] #[test]