[price-quotes] Implement error codes for currency and quote failures.

This commit is contained in:
John Ralls 2022-09-17 11:52:15 -07:00
parent 6db7800ca5
commit 4c47e91180
3 changed files with 216 additions and 10 deletions

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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));
}