mirror of
https://github.com/Gnucash/gnucash.git
synced 2025-02-25 18:55:30 -06:00
[price-quotes] Implement error codes for currency and quote failures.
This commit is contained in:
parent
6db7800ca5
commit
4c47e91180
@ -96,6 +96,8 @@ public:
|
||||
const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; }
|
||||
const QuoteSources& sources() noexcept { return m_sources; }
|
||||
GList* sources_as_glist ();
|
||||
const QFVec& failures() noexcept;
|
||||
std::string report_failures() noexcept;
|
||||
|
||||
private:
|
||||
std::string query_fq (const CommVec&);
|
||||
@ -106,6 +108,7 @@ private:
|
||||
std::unique_ptr<GncQuoteSource> m_quotesource;
|
||||
std::string m_version;
|
||||
QuoteSources m_sources;
|
||||
QFVec m_failures;
|
||||
QofBook *m_book;
|
||||
gnc_commodity *m_dflt_curr;
|
||||
};
|
||||
@ -129,6 +132,8 @@ private:
|
||||
|
||||
};
|
||||
|
||||
static const std::string empty_string{};
|
||||
|
||||
GncFQQuoteSource::GncFQQuoteSource() :
|
||||
c_cmd{bp::search_path("perl")},
|
||||
c_fq_wrapper{std::string(gnc_path_get_bindir()) + "/finance-quote-wrapper"},
|
||||
@ -227,8 +232,9 @@ GncFQQuoteSource::run_cmd (const StrVec& args, const std::string& json_string) c
|
||||
|
||||
/* GncQuotes implementation */
|
||||
GncQuotesImpl::GncQuotesImpl() : m_quotesource{new GncFQQuoteSource},
|
||||
m_version{}, m_sources{}, m_book{qof_session_get_book(gnc_get_current_session())},
|
||||
m_dflt_curr{gnc_default_currency()}
|
||||
m_version{}, m_sources{}, m_failures{},
|
||||
m_book{qof_session_get_book(gnc_get_current_session())},
|
||||
m_dflt_curr{gnc_default_currency()}
|
||||
{
|
||||
if (!m_quotesource->usable())
|
||||
return;
|
||||
@ -283,6 +289,7 @@ GncQuotesImpl::fetch (gnc_commodity *comm)
|
||||
void
|
||||
GncQuotesImpl::fetch (CommVec& commodities)
|
||||
{
|
||||
m_failures.clear();
|
||||
if (commodities.empty())
|
||||
return;
|
||||
|
||||
@ -290,6 +297,66 @@ GncQuotesImpl::fetch (CommVec& commodities)
|
||||
parse_quotes (quote_str, commodities);
|
||||
}
|
||||
|
||||
const QFVec&
|
||||
GncQuotesImpl::failures() noexcept
|
||||
{
|
||||
return m_failures;
|
||||
}
|
||||
|
||||
static std::string
|
||||
explain(GncQuoteError err, const std::string& errmsg)
|
||||
{
|
||||
std::string retval;
|
||||
switch (err)
|
||||
{
|
||||
case GncQuoteError::NO_RESULT:
|
||||
if (errmsg.empty())
|
||||
retval += _("Finance::Quote returned no data and set no error.");
|
||||
else
|
||||
retval += _("Finance::Quote returned an error: ") + errmsg;
|
||||
break;
|
||||
case GncQuoteError::QUOTE_FAILED:
|
||||
if (errmsg.empty())
|
||||
retval += _("Finance::Quote reported failure set no error.");
|
||||
else
|
||||
retval += _("Finance::Quote reported failure with error: ") + errmsg;
|
||||
break;
|
||||
case GncQuoteError::NO_CURRENCY:
|
||||
retval += _("Finance::Quote returned a quote with no currency.");
|
||||
break;
|
||||
case GncQuoteError::UNKNOWN_CURRENCY:
|
||||
retval += _("Finance::Quote returned a quote with a currency GnuCash doesn't know about.");
|
||||
break;
|
||||
case GncQuoteError::NO_PRICE:
|
||||
retval += _("Finance::Quote returned a quote with no price element.");
|
||||
break;
|
||||
case GncQuoteError::PRICE_PARSE_FAILURE:
|
||||
retval += _("Finance::Quote returned a quote with a price that GnuCash was unable to covert to a number.");
|
||||
break;
|
||||
case GncQuoteError::SUCCESS:
|
||||
default:
|
||||
retval += _("The quote has no error set.");
|
||||
break;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
std::string
|
||||
GncQuotesImpl::report_failures() noexcept
|
||||
{
|
||||
std::string retval{_("Quotes for the following commodities were unavailable or unusable:\n")};
|
||||
std::for_each(m_failures.begin(), m_failures.end(),
|
||||
[&retval](auto failure)
|
||||
{
|
||||
auto [ns, sym, reason, err] = failure;
|
||||
retval += "* " + ns + ":" + sym + " " +
|
||||
explain(reason, err) + "\n";
|
||||
});
|
||||
return retval;
|
||||
}
|
||||
|
||||
/* **** Private function implementations ****/
|
||||
|
||||
std::string
|
||||
GncQuotesImpl::comm_vec_to_json_string (const CommVec& comm_vec) const
|
||||
{
|
||||
@ -368,11 +435,13 @@ get_price_and_type(PriceParams& p, const bpt::ptree& comm_pt)
|
||||
{
|
||||
p.type = "last";
|
||||
p.price = comm_pt.get_optional<std::string> (p.type);
|
||||
|
||||
if (!p.price)
|
||||
{
|
||||
p.type = "nav";
|
||||
p.price = comm_pt.get_optional<std::string> (p.type);
|
||||
}
|
||||
|
||||
if (!p.price)
|
||||
{
|
||||
p.type = "price";
|
||||
@ -472,11 +541,13 @@ get_price(const PriceParams& p)
|
||||
}
|
||||
|
||||
static gnc_commodity*
|
||||
get_currency(const PriceParams& p, QofBook* book)
|
||||
get_currency(const PriceParams& p, QofBook* book, QFVec& failures)
|
||||
{
|
||||
if (!p.currency)
|
||||
{
|
||||
PWARN("Skipped %s:%s - Finance::Quote didn't return a currency",
|
||||
failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::NO_CURRENCY,
|
||||
empty_string);
|
||||
PWARN("Skipped %s:%s - Finance::Quote returned a quote with no currency",
|
||||
p.ns, p.mnemonic);
|
||||
return nullptr;
|
||||
}
|
||||
@ -487,6 +558,8 @@ get_currency(const PriceParams& p, QofBook* book)
|
||||
|
||||
if (!currency)
|
||||
{
|
||||
failures.emplace_back(p.ns, p.mnemonic,
|
||||
GncQuoteError::UNKNOWN_CURRENCY, empty_string);
|
||||
PWARN("Skipped %s:%s - failed to parse returned currency '%s'",
|
||||
p.ns, p.mnemonic, p.currency->c_str());
|
||||
return nullptr;
|
||||
@ -507,6 +580,8 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
|
||||
auto comm_pt_ai{pt.find(p.mnemonic)};
|
||||
if (comm_pt_ai == pt.not_found())
|
||||
{
|
||||
m_failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::NO_RESULT,
|
||||
empty_string);
|
||||
PINFO("Skipped %s:%s - Finance::Quote didn't return any data.",
|
||||
p.ns, p.mnemonic);
|
||||
return nullptr;
|
||||
@ -517,6 +592,8 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
|
||||
|
||||
if (!p.success)
|
||||
{
|
||||
m_failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::QUOTE_FAILED,
|
||||
p.errormsg ? *p.errormsg : empty_string);
|
||||
PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
|
||||
p.ns, p.mnemonic,
|
||||
(p.errormsg ? p.errormsg->c_str() : "unknown"));
|
||||
@ -525,6 +602,8 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
|
||||
|
||||
if (!p.price)
|
||||
{
|
||||
m_failures.emplace_back(p.ns, p.mnemonic,
|
||||
GncQuoteError::NO_PRICE, empty_string);
|
||||
PWARN("Skipped %s:%s - Finance::Quote didn't return a valid price",
|
||||
p.ns, p.mnemonic);
|
||||
return nullptr;
|
||||
@ -532,11 +611,16 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
|
||||
|
||||
auto price{get_price(p)};
|
||||
if (!price)
|
||||
{
|
||||
m_failures.emplace_back(p.ns, p.mnemonic,
|
||||
GncQuoteError::PRICE_PARSE_FAILURE,
|
||||
empty_string);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto currency{get_currency(p, m_book)};
|
||||
auto currency{get_currency(p, m_book, m_failures)};
|
||||
if (!currency)
|
||||
return nullptr;
|
||||
return nullptr;
|
||||
|
||||
auto quotedt{calc_price_time(p)};
|
||||
auto gnc_price = gnc_price_create (m_book);
|
||||
@ -737,3 +821,14 @@ GList* GncQuotes::sources_as_glist ()
|
||||
|
||||
GncQuotes::~GncQuotes() = default;
|
||||
|
||||
const QFVec&
|
||||
GncQuotes::failures() noexcept
|
||||
{
|
||||
return m_impl->failures();
|
||||
}
|
||||
|
||||
const std::string
|
||||
GncQuotes::report_failures() noexcept
|
||||
{
|
||||
return m_impl->report_failures();
|
||||
}
|
||||
|
@ -33,9 +33,27 @@ extern "C" {
|
||||
#include <qofbook.h>
|
||||
}
|
||||
|
||||
using StrVec = std::vector <std::string>;
|
||||
using StrVec = std::vector<std::string>;
|
||||
using QuoteSources = StrVec;
|
||||
using CmdOutput = std::pair <StrVec, StrVec>;
|
||||
|
||||
enum class GncQuoteError
|
||||
{
|
||||
SUCCESS,
|
||||
NO_RESULT,
|
||||
QUOTE_FAILED,
|
||||
NO_CURRENCY,
|
||||
UNKNOWN_CURRENCY,
|
||||
NO_PRICE,
|
||||
UNKNOWN_PRICE_TYPE,
|
||||
PRICE_PARSE_FAILURE,
|
||||
};
|
||||
|
||||
/** QuoteFailure elements are namespace, mnemonic, error code, and
|
||||
* F::Q errormsg if there is one.
|
||||
*/
|
||||
using QuoteFailure = std::tuple<std::string, std::string,
|
||||
GncQuoteError, std::string>;
|
||||
using QFVec = std::vector<QuoteFailure>;
|
||||
|
||||
struct GncQuoteException : public std::runtime_error
|
||||
{
|
||||
@ -93,6 +111,23 @@ public:
|
||||
*/
|
||||
GList* sources_as_glist () ;
|
||||
|
||||
/** Report the commodities for which quotes were requested but not successfully retrieved.
|
||||
*
|
||||
* This does not include requested commodities that didn't have a quote source.
|
||||
*
|
||||
* @return a reference to a vector of QuoteFailure tuples.
|
||||
* @note The vector and its contents belong to the GncQuotes object and will be destroyed with it.
|
||||
*/
|
||||
const QFVec& failures() noexcept;
|
||||
|
||||
/* Report the commodities for which quotes were requested but not successfully retrieved.
|
||||
*
|
||||
* This does not include requested commodities that didn't have a quote source.
|
||||
*
|
||||
* @return A localized std::string with an intro and a list of the quote failures with a cause. The string is owned by the caller.
|
||||
*/
|
||||
const std::string report_failures() noexcept;
|
||||
|
||||
private:
|
||||
std::unique_ptr<GncQuotesImpl> m_impl;
|
||||
};
|
||||
|
@ -141,7 +141,11 @@ TEST_F(GncQuotesTest, online_wiggle)
|
||||
GncQuotes quotes;
|
||||
quotes.fetch(m_book);
|
||||
auto pricedb{gnc_pricedb_get_db(m_book)};
|
||||
EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
|
||||
auto failures{quotes.failures()};
|
||||
ASSERT_EQ(2u, failures.size());
|
||||
EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[0]));
|
||||
EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[1]));
|
||||
EXPECT_EQ(2u, gnc_pricedb_get_num_prices(pricedb));
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -153,6 +157,9 @@ TEST_F(GncQuotesTest, offline_wiggle)
|
||||
StrVec err_vec;
|
||||
GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
|
||||
quotes.fetch(m_book);
|
||||
auto failures{quotes.failures()};
|
||||
ASSERT_EQ(1u, failures.size());
|
||||
EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[0]));
|
||||
auto pricedb{gnc_pricedb_get_db(m_book)};
|
||||
EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb));
|
||||
}
|
||||
@ -169,7 +176,9 @@ TEST_F(GncQuotesTest, comvec_fetch)
|
||||
CommVec comms{hpe, aapl};
|
||||
GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
|
||||
quotes.fetch(comms);
|
||||
auto pricedb{gnc_pricedb_get_db(m_book)};
|
||||
auto failures{quotes.failures()};
|
||||
EXPECT_TRUE(failures.empty());
|
||||
auto pricedb{gnc_pricedb_get_db(m_book)};
|
||||
EXPECT_EQ(2u, gnc_pricedb_get_num_prices(pricedb));
|
||||
}
|
||||
|
||||
@ -184,6 +193,8 @@ TEST_F(GncQuotesTest, fetch_one_commodity)
|
||||
auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
|
||||
GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
|
||||
quotes.fetch(hpe);
|
||||
auto failures{quotes.failures()};
|
||||
EXPECT_TRUE(failures.empty());
|
||||
auto pricedb{gnc_pricedb_get_db(m_book)};
|
||||
auto price{gnc_pricedb_lookup_latest(pricedb, hpe, usd)};
|
||||
auto datetime{static_cast<time64>(GncDateTime("20220901160000"))};
|
||||
@ -208,8 +219,11 @@ TEST_F(GncQuotesTest, fetch_one_currency)
|
||||
auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
|
||||
GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
|
||||
quotes.fetch(eur);
|
||||
auto failures{quotes.failures()};
|
||||
EXPECT_TRUE(failures.empty());
|
||||
auto pricedb{gnc_pricedb_get_db(m_book)};
|
||||
auto price{gnc_pricedb_lookup_latest(pricedb, eur, usd)};
|
||||
EXPECT_EQ(1u, gnc_pricedb_get_num_prices(pricedb));
|
||||
auto datetime{static_cast<time64>(GncDateTime())};
|
||||
|
||||
EXPECT_EQ(usd, gnc_price_get_currency(price));
|
||||
@ -222,3 +236,65 @@ TEST_F(GncQuotesTest, fetch_one_currency)
|
||||
EXPECT_STREQ("last", gnc_price_get_typestr(price));
|
||||
}
|
||||
|
||||
TEST_F(GncQuotesTest, no_currency)
|
||||
{
|
||||
StrVec quote_vec{
|
||||
"{\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"success\":1}}"
|
||||
};
|
||||
StrVec err_vec;
|
||||
auto commtable{gnc_commodity_table_get_table(m_book)};
|
||||
auto hpe{gnc_commodity_table_lookup(commtable, "NYSE", "HPE")};
|
||||
auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
|
||||
GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
|
||||
quotes.fetch(hpe);
|
||||
auto failures{quotes.failures()};
|
||||
ASSERT_EQ(1u, failures.size());
|
||||
EXPECT_EQ(GncQuoteError::NO_CURRENCY, std::get<2>(failures[0]));
|
||||
auto pricedb{gnc_pricedb_get_db(m_book)};
|
||||
EXPECT_EQ(0u, gnc_pricedb_get_num_prices(pricedb));
|
||||
}
|
||||
|
||||
TEST_F(GncQuotesTest, bad_currency)
|
||||
{
|
||||
StrVec quote_vec{
|
||||
"{\"HPE\":{\"date\":\"09/01/2022\",\"last\":13.37,\"currency\":\"BTC\",\"success\":1}}"
|
||||
};
|
||||
StrVec err_vec;
|
||||
auto commtable{gnc_commodity_table_get_table(m_book)};
|
||||
auto hpe{gnc_commodity_table_lookup(commtable, "NYSE", "HPE")};
|
||||
auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
|
||||
GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
|
||||
quotes.fetch(hpe);
|
||||
auto failures{quotes.failures()};
|
||||
ASSERT_EQ(1u, failures.size());
|
||||
EXPECT_EQ(GncQuoteError::UNKNOWN_CURRENCY, std::get<2>(failures[0]));
|
||||
auto pricedb{gnc_pricedb_get_db(m_book)};
|
||||
EXPECT_EQ(0u, gnc_pricedb_get_num_prices(pricedb));
|
||||
}
|
||||
|
||||
TEST_F(GncQuotesTest, no_date)
|
||||
{
|
||||
StrVec quote_vec{
|
||||
"{\"HPE\":{\"last\":13.37,\"currency\":\"USD\",\"success\":1}}"
|
||||
};
|
||||
StrVec err_vec;
|
||||
auto commtable{gnc_commodity_table_get_table(m_book)};
|
||||
auto hpe{gnc_commodity_table_lookup(commtable, "NYSE", "HPE")};
|
||||
auto usd{gnc_commodity_table_lookup(commtable, "ISO4217", "USD")};
|
||||
GncQuotesImpl quotes(m_book, std::make_unique<GncMockQuoteSource>(std::move(quote_vec), std::move(err_vec)));
|
||||
quotes.fetch(hpe);
|
||||
auto failures{quotes.failures()};
|
||||
EXPECT_TRUE(failures.empty());
|
||||
auto pricedb{gnc_pricedb_get_db(m_book)};
|
||||
auto price{gnc_pricedb_lookup_latest(pricedb, hpe, usd)};
|
||||
auto datetime{static_cast<time64>(GncDateTime())};
|
||||
|
||||
EXPECT_EQ(usd, gnc_price_get_currency(price));
|
||||
EXPECT_EQ(datetime, gnc_price_get_time64(price));
|
||||
EXPECT_EQ(PRICE_SOURCE_FQ, gnc_price_get_source(price));
|
||||
EXPECT_TRUE(gnc_numeric_equal(GncNumeric{1337, 100},
|
||||
gnc_price_get_value(price)));
|
||||
EXPECT_STREQ("Finance::Quote", gnc_price_get_source_string(price));
|
||||
EXPECT_STREQ("last", gnc_price_get_typestr(price));
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user