mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
refactor(core): compile the mock provider only with a testing feature
Phase 3a of docs/DESIGN.md. MockProvider moves to api/mock.rs, and it, the embedded MSN fixture bodies, and the IDX_MOCK_* hooks compile only with idx-core's new `testing` feature. idx-cli enables it as a dev-dependency, so `cargo test` keeps the fixture-backed provider while release binaries no longer embed fixture data or honor IDX_USE_MOCK_PROVIDER. Parser helpers used only by the mock are gated the same way. The packaged-cli flake check no longer relies on the mock; it checks an offline cache miss (JSON CACHEMISS, exit 3) instead. Goldens unchanged; clippy is clean with and without `testing`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
15947de51a
commit
e78d9fcecd
13 changed files with 214 additions and 161 deletions
|
|
@ -31,7 +31,8 @@ Docs:
|
|||
## Rules
|
||||
1. Keep data access provider-driven; no ad hoc fetches in `cli/`.
|
||||
2. Keep parse/normalize functions pure.
|
||||
3. Tests use fixtures and `IDX_USE_MOCK_PROVIDER=1`; never hit the live network.
|
||||
3. Tests use fixtures and `IDX_USE_MOCK_PROVIDER=1` (needs idx-core's `testing`
|
||||
feature, on under `cargo test`); never hit the live network.
|
||||
4. Output contract: tables on stdout, one JSON document with `-o json`, errors
|
||||
on stderr (JSON-shaped in JSON mode), stdout empty on failure.
|
||||
5. Keep ownership queries (`releases`, `ticker`, `changes`, ...) working across
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ sha2 = { workspace = true, optional = true }
|
|||
ureq = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Tests drive the binary with the mock provider; release builds exclude it.
|
||||
idx-core = { workspace = true, features = ["testing"] }
|
||||
assert_cmd = "2"
|
||||
predicates = "3"
|
||||
rusqlite.workspace = true
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ default = ["ownership"]
|
|||
ownership = ["dep:rusqlite", "dep:quick-xml", "dep:sha2", "dep:zip"]
|
||||
# Derives clap::ValueEnum on enums frontends expose as CLI arguments.
|
||||
clap = ["dep:clap"]
|
||||
# Fixture-backed mock provider (IDX_USE_MOCK_PROVIDER) for tests only.
|
||||
testing = []
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
|
|
|
|||
119
crates/idx-core/src/api/mock.rs
Normal file
119
crates/idx-core/src/api/mock.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
//! Fixture-backed provider for tests, enabled by the `testing` feature and
|
||||
//! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it.
|
||||
|
||||
use super::types::{Bar, Fundamentals, Interval, Period, Quote};
|
||||
use super::{FundamentalsProvider, HistoryProvider, QuoteProvider, msn, yahoo};
|
||||
use crate::config::ProviderKind;
|
||||
use crate::error::IdxError;
|
||||
|
||||
pub(crate) fn enabled() -> bool {
|
||||
std::env::var("IDX_USE_MOCK_PROVIDER").is_ok()
|
||||
}
|
||||
|
||||
pub struct MockProvider {
|
||||
quote: Result<Quote, IdxError>,
|
||||
fundamentals: Result<Fundamentals, IdxError>,
|
||||
history: Result<Vec<Bar>, IdxError>,
|
||||
}
|
||||
|
||||
impl MockProvider {
|
||||
pub fn from_fixtures(provider: ProviderKind) -> Self {
|
||||
Self::from_fixtures_with_history_verbose(provider, false)
|
||||
}
|
||||
|
||||
pub fn from_fixtures_with_history_verbose(
|
||||
provider: ProviderKind,
|
||||
history_verbose: bool,
|
||||
) -> Self {
|
||||
if std::env::var("IDX_MOCK_ERROR").is_ok() {
|
||||
return Self::with_error(IdxError::ProviderUnavailable);
|
||||
}
|
||||
|
||||
match provider {
|
||||
ProviderKind::Yahoo => Self::from_yahoo_fixtures(history_verbose),
|
||||
ProviderKind::Msn => Self::from_msn_fixtures(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_yahoo_fixtures(history_verbose: bool) -> Self {
|
||||
let quote_raw = std::fs::read_to_string("tests/fixtures/chart_bbca_1d.json")
|
||||
.unwrap_or_else(|_| "{}".to_string());
|
||||
let history_path = std::env::var("IDX_MOCK_YAHOO_HISTORY_FIXTURE")
|
||||
.unwrap_or_else(|_| "tests/fixtures/chart_bbca_3mo.json".to_string());
|
||||
let history_raw =
|
||||
std::fs::read_to_string(&history_path).unwrap_or_else(|_| "{}".to_string());
|
||||
let fundamentals_raw = std::fs::read_to_string("tests/fixtures/quotesummary_bbca.json")
|
||||
.unwrap_or_else(|_| "{}".to_string());
|
||||
|
||||
let quote = yahoo::parse_quote_from_str("BBCA.JK", "e_raw)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
let fundamentals = yahoo::parse_fundamentals_from_str("BBCA.JK", &fundamentals_raw)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
let history =
|
||||
yahoo::parse_history_from_str_with_verbose("BBCA.JK", &history_raw, history_verbose)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
|
||||
Self {
|
||||
quote,
|
||||
fundamentals,
|
||||
history,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_msn_fixtures() -> Self {
|
||||
let quote_raw = std::fs::read_to_string("tests/fixtures/msn_quote_bbca.json")
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let fundamentals_path = std::env::var("IDX_MOCK_MSN_KEYRATIOS_FIXTURE")
|
||||
.unwrap_or_else(|_| "tests/fixtures/msn_keyratios_bbca.json".to_string());
|
||||
let fundamentals_raw =
|
||||
std::fs::read_to_string(&fundamentals_path).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
let quote = msn::parse_quote_from_str("BBCA.JK", "e_raw)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
let fundamentals = msn::parse_fundamentals_from_str(&fundamentals_raw, Some("e_raw))
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
let history_raw = std::fs::read_to_string("tests/fixtures/msn_chart_bbca_3m.json")
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let history = msn::parse_history_from_str("BBCA.JK", &history_raw)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
|
||||
Self {
|
||||
quote,
|
||||
fundamentals,
|
||||
history,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_error(err: IdxError) -> Self {
|
||||
Self {
|
||||
quote: Err(err.clone()),
|
||||
fundamentals: Err(err.clone()),
|
||||
history: Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QuoteProvider for MockProvider {
|
||||
fn quote(&self, symbol: &str) -> Result<Quote, IdxError> {
|
||||
let mut q = self.quote.clone()?;
|
||||
q.symbol = symbol.to_string();
|
||||
Ok(q)
|
||||
}
|
||||
}
|
||||
|
||||
impl FundamentalsProvider for MockProvider {
|
||||
fn fundamentals(&self, _symbol: &str) -> Result<Fundamentals, IdxError> {
|
||||
self.fundamentals.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryProvider for MockProvider {
|
||||
fn history(
|
||||
&self,
|
||||
_symbol: &str,
|
||||
_period: &Period,
|
||||
_interval: &Interval,
|
||||
) -> Result<Vec<Bar>, IdxError> {
|
||||
self.history.clone()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
#[cfg(feature = "testing")]
|
||||
pub mod mock;
|
||||
pub mod msn;
|
||||
pub mod types;
|
||||
pub mod yahoo;
|
||||
|
|
@ -171,11 +173,36 @@ pub fn resolve_symbol(symbol: &str, exchange: &str) -> Result<String, IdxError>
|
|||
}
|
||||
|
||||
pub fn default_provider(provider: ProviderKind, verbose: bool) -> SelectedProvider {
|
||||
build_selected_provider(
|
||||
provider,
|
||||
verbose,
|
||||
std::env::var("IDX_USE_MOCK_PROVIDER").is_ok(),
|
||||
)
|
||||
build_selected_provider(provider, verbose, mock_enabled())
|
||||
}
|
||||
|
||||
fn mock_enabled() -> bool {
|
||||
#[cfg(feature = "testing")]
|
||||
return mock::enabled();
|
||||
#[cfg(not(feature = "testing"))]
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
fn mock_market(provider: ProviderKind) -> Option<Box<dyn MarketDataProvider>> {
|
||||
Some(Box::new(mock::MockProvider::from_fixtures(provider)))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "testing"))]
|
||||
fn mock_market(_provider: ProviderKind) -> Option<Box<dyn MarketDataProvider>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
fn mock_history(provider: ProviderKind, verbose: bool) -> Option<Box<dyn HistoryProvider>> {
|
||||
Some(Box::new(
|
||||
mock::MockProvider::from_fixtures_with_history_verbose(provider, verbose),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "testing"))]
|
||||
fn mock_history(_provider: ProviderKind, _verbose: bool) -> Option<Box<dyn HistoryProvider>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn build_selected_provider(
|
||||
|
|
@ -183,10 +210,15 @@ fn build_selected_provider(
|
|||
verbose: bool,
|
||||
use_mock: bool,
|
||||
) -> SelectedProvider {
|
||||
match (provider, use_mock) {
|
||||
(ProviderKind::Yahoo, true) => SelectedProvider {
|
||||
let mock = if use_mock {
|
||||
mock_market(provider)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match provider {
|
||||
ProviderKind::Yahoo => SelectedProvider {
|
||||
kind: ProviderKind::Yahoo,
|
||||
market: Box::new(MockProvider::from_fixtures(ProviderKind::Yahoo)),
|
||||
market: mock.unwrap_or_else(|| Box::new(yahoo::YahooProvider::new(verbose))),
|
||||
profile: None,
|
||||
financials: None,
|
||||
earnings: None,
|
||||
|
|
@ -195,31 +227,9 @@ fn build_selected_provider(
|
|||
news: None,
|
||||
screener: None,
|
||||
},
|
||||
(ProviderKind::Yahoo, false) => SelectedProvider {
|
||||
kind: ProviderKind::Yahoo,
|
||||
market: Box::new(yahoo::YahooProvider::new(verbose)),
|
||||
profile: None,
|
||||
financials: None,
|
||||
earnings: None,
|
||||
sentiment: None,
|
||||
insights: None,
|
||||
news: None,
|
||||
screener: None,
|
||||
},
|
||||
(ProviderKind::Msn, true) => SelectedProvider {
|
||||
ProviderKind::Msn => SelectedProvider {
|
||||
kind: ProviderKind::Msn,
|
||||
market: Box::new(MockProvider::from_fixtures(ProviderKind::Msn)),
|
||||
profile: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
financials: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
earnings: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
sentiment: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
insights: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
news: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
screener: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
},
|
||||
(ProviderKind::Msn, false) => SelectedProvider {
|
||||
kind: ProviderKind::Msn,
|
||||
market: Box::new(msn::MsnProvider::new(verbose)),
|
||||
market: mock.unwrap_or_else(|| Box::new(msn::MsnProvider::new(verbose))),
|
||||
profile: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
financials: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
earnings: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||
|
|
@ -254,13 +264,10 @@ pub fn history_provider(
|
|||
},
|
||||
};
|
||||
|
||||
if std::env::var("IDX_USE_MOCK_PROVIDER").is_ok() {
|
||||
return Ok((
|
||||
resolved,
|
||||
Box::new(MockProvider::from_fixtures_with_history_verbose(
|
||||
resolved, verbose,
|
||||
)),
|
||||
));
|
||||
if mock_enabled()
|
||||
&& let Some(mock) = mock_history(resolved, verbose)
|
||||
{
|
||||
return Ok((resolved, mock));
|
||||
}
|
||||
|
||||
match resolved {
|
||||
|
|
@ -269,114 +276,6 @@ pub fn history_provider(
|
|||
}
|
||||
}
|
||||
|
||||
pub struct MockProvider {
|
||||
quote: Result<Quote, IdxError>,
|
||||
fundamentals: Result<Fundamentals, IdxError>,
|
||||
history: Result<Vec<Bar>, IdxError>,
|
||||
}
|
||||
|
||||
impl MockProvider {
|
||||
pub fn from_fixtures(provider: ProviderKind) -> Self {
|
||||
Self::from_fixtures_with_history_verbose(provider, false)
|
||||
}
|
||||
|
||||
pub fn from_fixtures_with_history_verbose(
|
||||
provider: ProviderKind,
|
||||
history_verbose: bool,
|
||||
) -> Self {
|
||||
if std::env::var("IDX_MOCK_ERROR").is_ok() {
|
||||
return Self::with_error(IdxError::ProviderUnavailable);
|
||||
}
|
||||
|
||||
match provider {
|
||||
ProviderKind::Yahoo => Self::from_yahoo_fixtures(history_verbose),
|
||||
ProviderKind::Msn => Self::from_msn_fixtures(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_yahoo_fixtures(history_verbose: bool) -> Self {
|
||||
let quote_raw = std::fs::read_to_string("tests/fixtures/chart_bbca_1d.json")
|
||||
.unwrap_or_else(|_| "{}".to_string());
|
||||
let history_path = std::env::var("IDX_MOCK_YAHOO_HISTORY_FIXTURE")
|
||||
.unwrap_or_else(|_| "tests/fixtures/chart_bbca_3mo.json".to_string());
|
||||
let history_raw =
|
||||
std::fs::read_to_string(&history_path).unwrap_or_else(|_| "{}".to_string());
|
||||
let fundamentals_raw = std::fs::read_to_string("tests/fixtures/quotesummary_bbca.json")
|
||||
.unwrap_or_else(|_| "{}".to_string());
|
||||
|
||||
let quote = yahoo::parse_quote_from_str("BBCA.JK", "e_raw)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
let fundamentals = yahoo::parse_fundamentals_from_str("BBCA.JK", &fundamentals_raw)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
let history =
|
||||
yahoo::parse_history_from_str_with_verbose("BBCA.JK", &history_raw, history_verbose)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
|
||||
Self {
|
||||
quote,
|
||||
fundamentals,
|
||||
history,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_msn_fixtures() -> Self {
|
||||
let quote_raw = std::fs::read_to_string("tests/fixtures/msn_quote_bbca.json")
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let fundamentals_path = std::env::var("IDX_MOCK_MSN_KEYRATIOS_FIXTURE")
|
||||
.unwrap_or_else(|_| "tests/fixtures/msn_keyratios_bbca.json".to_string());
|
||||
let fundamentals_raw =
|
||||
std::fs::read_to_string(&fundamentals_path).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
let quote = msn::parse_quote_from_str("BBCA.JK", "e_raw)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
let fundamentals = msn::parse_fundamentals_from_str(&fundamentals_raw, Some("e_raw))
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
let history_raw = std::fs::read_to_string("tests/fixtures/msn_chart_bbca_3m.json")
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let history = msn::parse_history_from_str("BBCA.JK", &history_raw)
|
||||
.map_err(|e| IdxError::ParseError(e.to_string()));
|
||||
|
||||
Self {
|
||||
quote,
|
||||
fundamentals,
|
||||
history,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_error(err: IdxError) -> Self {
|
||||
Self {
|
||||
quote: Err(err.clone()),
|
||||
fundamentals: Err(err.clone()),
|
||||
history: Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QuoteProvider for MockProvider {
|
||||
fn quote(&self, symbol: &str) -> Result<Quote, IdxError> {
|
||||
let mut q = self.quote.clone()?;
|
||||
q.symbol = symbol.to_string();
|
||||
Ok(q)
|
||||
}
|
||||
}
|
||||
|
||||
impl FundamentalsProvider for MockProvider {
|
||||
fn fundamentals(&self, _symbol: &str) -> Result<Fundamentals, IdxError> {
|
||||
self.fundamentals.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryProvider for MockProvider {
|
||||
fn history(
|
||||
&self,
|
||||
_symbol: &str,
|
||||
_period: &Period,
|
||||
_interval: &Interval,
|
||||
) -> Result<Vec<Bar>, IdxError> {
|
||||
self.history.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ProviderKind, build_selected_provider, resolve_symbol};
|
||||
|
|
@ -410,6 +309,7 @@ mod tests {
|
|||
assert!(provider.screener_provider("screen").is_ok());
|
||||
}
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
#[test]
|
||||
fn selected_provider_exposes_expected_capabilities_for_mock_msn() {
|
||||
let provider = build_selected_provider(ProviderKind::Msn, false, true);
|
||||
|
|
@ -438,6 +338,7 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
#[test]
|
||||
fn selected_provider_rejects_msn_only_capabilities_for_mock_yahoo() {
|
||||
let provider = build_selected_provider(ProviderKind::Yahoo, false, true);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,17 @@ impl MsnClient {
|
|||
Self { agent }
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "testing"))]
|
||||
fn mock_error() -> Option<IdxError> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "testing"))]
|
||||
fn mock_body(_endpoint: &str) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
fn mock_error() -> Option<IdxError> {
|
||||
if std::env::var("IDX_USE_MOCK_PROVIDER").is_ok() && std::env::var("IDX_MOCK_ERROR").is_ok()
|
||||
{
|
||||
|
|
@ -43,6 +54,7 @@ impl MsnClient {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
fn mock_body(endpoint: &str) -> Option<&'static str> {
|
||||
if std::env::var("IDX_USE_MOCK_PROVIDER").is_err() {
|
||||
return None;
|
||||
|
|
@ -271,6 +283,7 @@ impl MsnClient {
|
|||
url: &str,
|
||||
request: &ScreenerRequest,
|
||||
) -> Result<RawScreenerResponse, IdxError> {
|
||||
#[cfg(feature = "testing")]
|
||||
if std::env::var("IDX_USE_MOCK_PROVIDER").is_ok() {
|
||||
if let Some(err) = Self::mock_error() {
|
||||
return Err(err);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ use map::{
|
|||
parse_news, parse_profile, parse_quote, parse_screener_results, parse_sentiment,
|
||||
};
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
pub(crate) use parse::{parse_fundamentals_from_str, parse_history_from_str, parse_quote_from_str};
|
||||
|
||||
pub struct MsnProvider {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use client::YahooClient;
|
|||
use map::{parse_fundamentals, parse_quote};
|
||||
use parse::parse_history_with_verbose;
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
pub(crate) use parse::{
|
||||
parse_fundamentals_from_str, parse_history_from_str_with_verbose, parse_quote_from_str,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
use crate::api::types::{Fundamentals, Ohlc, Quote};
|
||||
use crate::api::types::Ohlc;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
use crate::api::types::{Fundamentals, Quote};
|
||||
use crate::error::IdxError;
|
||||
|
||||
use super::map::{parse_fundamentals, parse_history, parse_quote};
|
||||
use super::raw_types::{ChartResponse, QuoteSummaryResponse};
|
||||
use super::map::parse_history;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
use super::map::{parse_fundamentals, parse_quote};
|
||||
use super::raw_types::ChartResponse;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
use super::raw_types::QuoteSummaryResponse;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub(crate) fn parse_quote_from_str(symbol: &str, raw: &str) -> Result<Quote, IdxError> {
|
||||
let chart: ChartResponse =
|
||||
serde_json::from_str(raw).map_err(|e| IdxError::ParseError(e.to_string()))?;
|
||||
|
|
@ -15,6 +22,7 @@ pub(crate) fn parse_history_from_str(symbol: &str, raw: &str) -> Result<Vec<Ohlc
|
|||
parse_history_from_str_with_verbose(symbol, raw, false)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub(crate) fn parse_history_from_str_with_verbose(
|
||||
symbol: &str,
|
||||
raw: &str,
|
||||
|
|
@ -25,6 +33,7 @@ pub(crate) fn parse_history_from_str_with_verbose(
|
|||
parse_history_with_verbose(symbol, &chart, verbose)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub(crate) fn parse_fundamentals_from_str(
|
||||
symbol: &str,
|
||||
raw: &str,
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ let conn = Connection::open(path)
|
|||
- **Output contracts:** parse JSON from stdout on success and stderr on failure; errors must leave stdout empty. Assert meaningful values, types, and stable error codes rather than substrings or diagnostic wording.
|
||||
- **Stateful workflows:** compare cached data across offline/failure transitions, and verify failed snapshot updates preserve both database bytes and query results. Put behavioral coverage in `tests/cli.rs`, not shell smoke.
|
||||
- **Fixtures:** `tests/fixtures/*.json` — real API responses, sanitized
|
||||
- **No live API calls in CI** — `IDX_USE_MOCK_PROVIDER=1`
|
||||
- **No live API calls in CI** — `IDX_USE_MOCK_PROVIDER=1` selects the fixture-backed mock provider, which exists only when idx-core's `testing` feature is on (idx-cli enables it as a dev-dependency, so `cargo test` has it and release builds do not)
|
||||
- **Test naming:** `test_<function>_<scenario>` (e.g., `test_parse_id_number_with_dots`)
|
||||
- **Coverage over counts:** replacing redundant unit assertions with stronger integration scenarios must retain meaningful error, boundary, and data-integrity coverage.
|
||||
|
||||
|
|
|
|||
|
|
@ -94,8 +94,9 @@ Key decisions:
|
|||
- **Explicit configuration.** Core reads no environment variables. `IDX_*` env
|
||||
parsing, `config.toml` loading, and XDG resolution move to a `CoreConfig::load()`
|
||||
helper that frontends call; tests construct `CoreConfig` directly.
|
||||
- **Mock provider behind a `testing` feature.** Today `MockProvider` ships in the
|
||||
release binary and reads fixtures relative to the working directory.
|
||||
- **Mock provider behind a `testing` feature.** Done: `MockProvider` and the
|
||||
embedded MSN fixtures compile only with idx-core's `testing` feature, which
|
||||
idx-cli enables as a dev-dependency.
|
||||
- **Typed inputs.** Screener filters, regions, statement and earnings filters
|
||||
become enums with `FromStr`, so validation is identical across frontends.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ offline with pinned tools:
|
|||
| `fmt`, `clippy` | formatting and lints (`--all-targets`) |
|
||||
| `test` | unit/module tests, `tests/cli.rs` CLI integration tests, and `tests/golden.rs` output parity |
|
||||
| `default`, `package` | release build and `cargo package` verification |
|
||||
| `packaged-cli` | the wrapped Nix binary starts, emits JSON, and generates completions |
|
||||
| `packaged-cli` | the wrapped Nix release binary emits JSON, generates completions, and returns a JSON `CACHEMISS` error with exit 3 offline |
|
||||
| `ownership-publisher`, `publisher-test` | packaged publisher entry point and its regression scenarios |
|
||||
| `install-script-sh`, `install-script-dash`, `shellcheck` | installer regressions and shell lint |
|
||||
|
||||
|
|
|
|||
15
flake.nix
15
flake.nix
|
|
@ -217,14 +217,17 @@
|
|||
packaged-cli = pkgs.runCommand "idx-packaged-cli"
|
||||
{ nativeBuildInputs = with pkgs; [ bash jq ]; }
|
||||
''
|
||||
# The mock provider reads fixtures relative to the working dir.
|
||||
mkdir -p tests
|
||||
cp -r ${./crates/idx-core/tests/fixtures} tests/fixtures
|
||||
export HOME="$TMPDIR"
|
||||
export HOME="$TMPDIR" XDG_CACHE_HOME="$TMPDIR/cache" XDG_CONFIG_HOME="$TMPDIR/config"
|
||||
${idxPackage}/bin/idx -o json version | jq -e '.version | type == "string"'
|
||||
${idxPackage}/bin/idx completions bash | bash -n
|
||||
IDX_USE_MOCK_PROVIDER=1 ${idxPackage}/bin/idx -o json stocks quote BBCA \
|
||||
| jq -e '.[0].price | type == "number"'
|
||||
# Release builds have no mock provider; an offline cache miss
|
||||
# exercises the JSON error contract and exit-code classes.
|
||||
set +e
|
||||
${idxPackage}/bin/idx --offline -o json stocks quote BBCA 2>err.json
|
||||
status=$?
|
||||
set -e
|
||||
test "$status" -eq 3
|
||||
jq -e '.code == "CACHEMISS"' err.json
|
||||
touch "$out"
|
||||
'';
|
||||
install-script-sh = installerCheck "sh";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue