diff --git a/CMakeLists.txt b/CMakeLists.txt index ef25bcf371..9913d453be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2511,6 +2511,7 @@ else () src/test/jtx/impl/WSClient.cpp src/test/jtx/impl/amount.cpp src/test/jtx/impl/balance.cpp + src/test/jtx/impl/check.cpp src/test/jtx/impl/delivermin.cpp src/test/jtx/impl/deposit.cpp src/test/jtx/impl/envconfig.cpp diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index d96fcef8ce..6a46c7be5f 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -20,79 +20,10 @@ #include #include #include -#include namespace ripple { - -// For the time being Checks seem pretty much self contained. So the -// functions that operate on jtx are defined here, locally. If they are -// needed by other unit tests they could put in another file. namespace test { namespace jtx { -namespace check { - -// Create a check. -Json::Value -create (jtx::Account const& account, - jtx::Account const& dest, STAmount const& sendMax) -{ - Json::Value jv; - jv[sfAccount.jsonName] = account.human(); - jv[sfSendMax.jsonName] = sendMax.getJson(JsonOptions::none); - jv[sfDestination.jsonName] = dest.human(); - jv[sfTransactionType.jsonName] = jss::CheckCreate; - jv[sfFlags.jsonName] = tfUniversal; - return jv; -} - -// Type used to specify DeliverMin for cashing a check. -struct DeliverMin -{ - STAmount value; - explicit DeliverMin (STAmount const& deliverMin) - : value (deliverMin) { } -}; - -// Cash a check. -Json::Value -cash (jtx::Account const& dest, - uint256 const& checkId, STAmount const& amount) -{ - Json::Value jv; - jv[sfAccount.jsonName] = dest.human(); - jv[sfAmount.jsonName] = amount.getJson(JsonOptions::none); - jv[sfCheckID.jsonName] = to_string (checkId); - jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; - return jv; -} - -Json::Value -cash (jtx::Account const& dest, - uint256 const& checkId, DeliverMin const& atLeast) -{ - Json::Value jv; - jv[sfAccount.jsonName] = dest.human(); - jv[sfDeliverMin.jsonName] = atLeast.value.getJson(JsonOptions::none); - jv[sfCheckID.jsonName] = to_string (checkId); - jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; - return jv; -} - -// Cancel a check. -Json::Value -cancel (jtx::Account const& dest, uint256 const& checkId) -{ - Json::Value jv; - jv[sfAccount.jsonName] = dest.human(); - jv[sfCheckID.jsonName] = to_string (checkId); - jv[sfTransactionType.jsonName] = jss::CheckCancel; - jv[sfFlags.jsonName] = tfUniversal; - return jv; -} - -} // namespace check /** Set Expiration on a JTx. */ class expiration diff --git a/src/test/jtx.h b/src/test/jtx.h index 58d3a0d140..cc11e6cabd 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/check.h b/src/test/jtx/check.h new file mode 100644 index 0000000000..5f990d5b8e --- /dev/null +++ b/src/test/jtx/check.h @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_CHECK_H_INCLUDED +#define RIPPLE_TEST_JTX_CHECK_H_INCLUDED + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Check operations. */ +namespace check { + +/** Create a check. */ +Json::Value +create (jtx::Account const& account, + jtx::Account const& dest, STAmount const& sendMax); + +/** Cash a check requiring that a specific amount be delivered. */ +Json::Value +cash (jtx::Account const& dest, + uint256 const& checkId, STAmount const& amount); + +/** Type used to specify DeliverMin for cashing a check. */ +struct DeliverMin +{ + STAmount value; + explicit DeliverMin (STAmount const& deliverMin) + : value (deliverMin) { } +}; + +/** Cash a check requiring that at least a minimum amount be delivered. */ +Json::Value +cash (jtx::Account const& dest, + uint256 const& checkId, DeliverMin const& atLeast); + +/** Cancel a check. */ +Json::Value +cancel (jtx::Account const& dest, uint256 const& checkId); + +} // check + +/** Match the number of checks on the account. */ +using checks = owner_count; + +} // jtx + +} // test +} // ripple + +#endif diff --git a/src/test/jtx/impl/check.cpp b/src/test/jtx/impl/check.cpp new file mode 100644 index 0000000000..4e7a84808f --- /dev/null +++ b/src/test/jtx/impl/check.cpp @@ -0,0 +1,88 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace check { + +// Create a check. +Json::Value +create (jtx::Account const& account, + jtx::Account const& dest, STAmount const& sendMax) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfSendMax.jsonName] = sendMax.getJson(JsonOptions::none); + jv[sfDestination.jsonName] = dest.human(); + jv[sfTransactionType.jsonName] = jss::CheckCreate; + jv[sfFlags.jsonName] = tfUniversal; + return jv; +} + +// Cash a check requiring that a specific amount be delivered. +Json::Value +cash (jtx::Account const& dest, + uint256 const& checkId, STAmount const& amount) +{ + Json::Value jv; + jv[sfAccount.jsonName] = dest.human(); + jv[sfAmount.jsonName] = amount.getJson(JsonOptions::none); + jv[sfCheckID.jsonName] = to_string (checkId); + jv[sfTransactionType.jsonName] = jss::CheckCash; + jv[sfFlags.jsonName] = tfUniversal; + return jv; +} + +// Cash a check requiring that at least a minimum amount be delivered. +Json::Value +cash (jtx::Account const& dest, + uint256 const& checkId, DeliverMin const& atLeast) +{ + Json::Value jv; + jv[sfAccount.jsonName] = dest.human(); + jv[sfDeliverMin.jsonName] = atLeast.value.getJson(JsonOptions::none); + jv[sfCheckID.jsonName] = to_string (checkId); + jv[sfTransactionType.jsonName] = jss::CheckCash; + jv[sfFlags.jsonName] = tfUniversal; + return jv; +} + +// Cancel a check. +Json::Value +cancel (jtx::Account const& dest, uint256 const& checkId) +{ + Json::Value jv; + jv[sfAccount.jsonName] = dest.human(); + jv[sfCheckID.jsonName] = to_string (checkId); + jv[sfTransactionType.jsonName] = jss::CheckCancel; + jv[sfFlags.jsonName] = tfUniversal; + return jv; +} + +} // check + +} // jtx +} // test +} // ripple diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 4bca327807..3ae162429d 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -380,18 +380,9 @@ public: BEAST_EXPECT (state[sfBalance.jsonName][jss::value].asInt() == -5); BEAST_EXPECT (state[sfHighLimit.jsonName][jss::value].asUInt() == 1000); } - { - // gw writes a check for USD(10) to alice. - Json::Value jvCheck; - jvCheck[sfAccount.jsonName] = gw.human(); - jvCheck[sfSendMax.jsonName] = - USD(10).value().getJson(JsonOptions::none); - jvCheck[sfDestination.jsonName] = alice.human(); - jvCheck[sfTransactionType.jsonName] = jss::CheckCreate; - jvCheck[sfFlags.jsonName] = tfUniversal; - env (jvCheck); - env.close(); - } + // gw writes a check for USD(10) to alice. + env (check::create (gw, alice, USD(10))); + env.close(); { // Find the check. Json::Value const resp = acct_objs (gw, jss::check); diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index 171464b88d..2eaf09b388 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -26,7 +26,7 @@ namespace ripple { namespace test { -class AccountTX_test : public beast::unit_test::suite +class AccountTx_test : public beast::unit_test::suite { void testParameters() @@ -164,15 +164,298 @@ class AccountTX_test : public beast::unit_test::suite } } + void + testContents() + { + // Get results for all transaction types that can be associated + // with an account. Start by generating all transaction types. + using namespace test::jtx; + using namespace std::chrono_literals; + + Env env(*this); + Account const alice {"alice"}; + Account const alie {"alie"}; + Account const gw {"gw"}; + auto const USD {gw["USD"]}; + + env.fund(XRP(1000000), alice, gw); + env.close(); + + // AccountSet + env (noop (alice)); + + // Payment + env (pay (alice, gw, XRP (100))); + + // Regular key set + env (regkey(alice, alie)); + env.close(); + + // Trust and Offers + env (trust (alice, USD (200)), sig (alie)); + std::uint32_t const offerSeq {env.seq(alice)}; + env (offer (alice, USD (50), XRP (150)), sig (alie)); + env.close(); + + { + Json::Value cancelOffer; + cancelOffer[jss::Account] = alice.human(); + cancelOffer[jss::OfferSequence] = offerSeq; + cancelOffer[jss::TransactionType] = jss::OfferCancel; + env (cancelOffer, sig (alie)); + } + env.close(); + + // SignerListSet + env (signers (alice, 1, {{"bogie", 1}, {"demon", 1}}), sig (alie)); + + // Escrow + { + // Create an escrow. Requires either a CancelAfter or FinishAfter. + auto escrow = [] (Account const& account, + Account const& to, STAmount const& amount) + { + Json::Value escro; + escro[jss::TransactionType] = jss::EscrowCreate; + escro[jss::Flags] = tfUniversal; + escro[jss::Account] = account.human(); + escro[jss::Destination] = to.human(); + escro[jss::Amount] = amount.getJson(JsonOptions::none); + return escro; + }; + + NetClock::time_point const nextTime {env.now() + 2s}; + + Json::Value escrowWithFinish {escrow (alice, alice, XRP (500))}; + escrowWithFinish[sfFinishAfter.jsonName] = + nextTime.time_since_epoch().count(); + + std::uint32_t const escrowFinishSeq {env.seq(alice)}; + env (escrowWithFinish, sig (alie)); + + Json::Value escrowWithCancel {escrow (alice, alice, XRP (500))}; + escrowWithCancel[sfFinishAfter.jsonName] = + nextTime.time_since_epoch().count(); + escrowWithCancel[sfCancelAfter.jsonName] = + nextTime.time_since_epoch().count() + 1; + + std::uint32_t const escrowCancelSeq {env.seq(alice)}; + env (escrowWithCancel, sig (alie)); + env.close(); + + { + Json::Value escrowFinish; + escrowFinish[jss::TransactionType] = jss::EscrowFinish; + escrowFinish[jss::Flags] = tfUniversal; + escrowFinish[jss::Account] = alice.human(); + escrowFinish[sfOwner.jsonName] = alice.human(); + escrowFinish[sfOfferSequence.jsonName] = escrowFinishSeq; + env (escrowFinish, sig (alie)); + } + { + Json::Value escrowCancel; + escrowCancel[jss::TransactionType] = jss::EscrowCancel; + escrowCancel[jss::Flags] = tfUniversal; + escrowCancel[jss::Account] = alice.human(); + escrowCancel[sfOwner.jsonName] = alice.human(); + escrowCancel[sfOfferSequence.jsonName] = escrowCancelSeq; + env (escrowCancel, sig (alie)); + } + env.close(); + } + + // PayChan + { + std::uint32_t payChanSeq {env.seq (alice)}; + Json::Value payChanCreate; + payChanCreate[jss::TransactionType] = jss::PaymentChannelCreate; + payChanCreate[jss::Flags] = tfUniversal; + payChanCreate[jss::Account] = alice.human(); + payChanCreate[jss::Destination] = gw.human(); + payChanCreate[jss::Amount] = + XRP (500).value().getJson (JsonOptions::none); + payChanCreate[sfSettleDelay.jsonName] = + NetClock::duration{100s}.count(); + payChanCreate[sfPublicKey.jsonName] = strHex (alice.pk().slice()); + env (payChanCreate, sig (alie)); + env.close(); + + std::string const payChanIndex { + strHex (keylet::payChan (alice, gw, payChanSeq).key)}; + + { + Json::Value payChanFund; + payChanFund[jss::TransactionType] = jss::PaymentChannelFund; + payChanFund[jss::Flags] = tfUniversal; + payChanFund[jss::Account] = alice.human(); + payChanFund[sfPayChannel.jsonName] = payChanIndex; + payChanFund[jss::Amount] = + XRP (200).value().getJson (JsonOptions::none); + env (payChanFund, sig (alie)); + env.close(); + } + { + Json::Value payChanClaim; + payChanClaim[jss::TransactionType] = jss::PaymentChannelClaim; + payChanClaim[jss::Flags] = tfClose; + payChanClaim[jss::Account] = gw.human(); + payChanClaim[sfPayChannel.jsonName] = payChanIndex; + payChanClaim[sfPublicKey.jsonName] = strHex(alice.pk().slice()); + env (payChanClaim); + env.close(); + } + } + + // Check + { + uint256 const aliceCheckId { + getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, gw, XRP (300)), sig (alie)); + + uint256 const gwCheckId { + getCheckIndex (gw, env.seq (gw))}; + env (check::create (gw, alice, XRP (200))); + env.close(); + + env (check::cash (alice, gwCheckId, XRP (200)), sig (alie)); + env (check::cancel (alice, aliceCheckId), sig (alie)); + env.close(); + } + + // Deposit preauthorization. + env (deposit::auth (alice, gw), sig (alie)); + env.close(); + + // Setup is done. Look at the transactions returned by account_tx. + Json::Value params; + params[jss::account] = alice.human(); + params[jss::ledger_index_min] = -1; + params[jss::ledger_index_max] = -1; + + Json::Value const result { + env.rpc("json", "account_tx", to_string(params))}; + + BEAST_EXPECT (result[jss::result][jss::status] == "success"); + BEAST_EXPECT (result[jss::result][jss::transactions].isArray()); + + Json::Value const& txs {result[jss::result][jss::transactions]}; + + // Do a sanity check on each returned transaction. They should + // be returned in the reverse order of application to the ledger. + struct NodeSanity + { + int const index; + Json::StaticString const& txType; + std::initializer_list created; + std::initializer_list deleted; + std::initializer_list modified; + }; + + auto checkSanity = [this] ( + Json::Value const& txNode, NodeSanity const& sane) + { + BEAST_EXPECT(txNode[jss::validated].asBool() == true); + BEAST_EXPECT( + txNode[jss::tx][sfTransactionType.jsonName].asString() == + sane.txType); + + // Make sure all of the expected node types are present. + std::vector createdNodes {}; + std::vector deletedNodes {}; + std::vector modifiedNodes{}; + + for (Json::Value const& metaNode : + txNode[jss::meta][sfAffectedNodes.jsonName]) + { + if (metaNode.isMember (sfCreatedNode.jsonName)) + createdNodes.push_back ( + metaNode[sfCreatedNode.jsonName] + [sfLedgerEntryType.jsonName].asString()); + + else if (metaNode.isMember (sfDeletedNode.jsonName)) + deletedNodes.push_back ( + metaNode[sfDeletedNode.jsonName] + [sfLedgerEntryType.jsonName].asString()); + + else if (metaNode.isMember (sfModifiedNode.jsonName)) + modifiedNodes.push_back ( + metaNode[sfModifiedNode.jsonName] + [sfLedgerEntryType.jsonName].asString()); + + else + fail ("Unexpected or unlabeled node type in metadata.", + __FILE__, __LINE__); + } + + auto cmpNodeTypes = [this] ( + char const* const errMsg, + std::vector& got, + std::initializer_list expList) + { + std::sort (got.begin(), got.end()); + + std::vector exp; + exp.reserve (expList.size()); + for (char const* nodeType : expList) + exp.push_back (nodeType); + std::sort (exp.begin(), exp.end()); + + if (got != exp) + { + fail (errMsg, __FILE__, __LINE__); + } + }; + + cmpNodeTypes ("Created mismatch", createdNodes, sane.created); + cmpNodeTypes ("Deleted mismatch", deletedNodes, sane.deleted); + cmpNodeTypes ("Modified mismatch", modifiedNodes, sane.modified); + }; + + static const NodeSanity sanity[] + { + // txType, created, deleted, modified + { 0, jss::DepositPreauth, {jss::DepositPreauth}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + { 1, jss::CheckCancel, {}, {jss::Check}, {jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + { 2, jss::CheckCash, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + { 3, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + { 4, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + { 5, jss::PaymentChannelClaim, {}, {jss::PayChannel}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode}}, + { 6, jss::PaymentChannelFund, {}, {}, {jss::AccountRoot, jss::PayChannel }}, + { 7, jss::PaymentChannelCreate, {jss::PayChannel}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode}}, + { 8, jss::EscrowCancel, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}}, + { 9, jss::EscrowFinish, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}}, + { 10, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + { 11, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + { 12, jss::SignerListSet, {jss::SignerList}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + { 13, jss::OfferCancel, {}, {jss::Offer, jss::DirectoryNode}, {jss::AccountRoot, jss::DirectoryNode}}, + { 14, jss::OfferCreate, {jss::Offer, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + { 15, jss::TrustSet, {jss::RippleState, jss::DirectoryNode, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::AccountRoot}}, + { 16, jss::SetRegularKey, {}, {}, {jss::AccountRoot}}, + { 17, jss::Payment, {}, {}, {jss::AccountRoot, jss::AccountRoot}}, + { 18, jss::AccountSet, {}, {}, {jss::AccountRoot}}, + { 19, jss::AccountSet, {}, {}, {jss::AccountRoot}}, + { 20, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}}, + }; + + BEAST_EXPECT (std::extent::value == + result[jss::result][jss::transactions].size()); + + for (unsigned int index {0}; + index < std::extent::value; ++index) + { + checkSanity (txs[index], sanity[index]); + } + } + public: void run() override { testParameters(); + testContents(); } }; -BEAST_DEFINE_TESTSUITE(AccountTX, app, ripple); +BEAST_DEFINE_TESTSUITE(AccountTx, app, ripple); } // namespace test - } // namespace ripple diff --git a/src/test/rpc/LedgerData_test.cpp b/src/test/rpc/LedgerData_test.cpp index 4c2b69ed14..8aa3b77e2a 100644 --- a/src/test/rpc/LedgerData_test.cpp +++ b/src/test/rpc/LedgerData_test.cpp @@ -325,15 +325,7 @@ public: env(jv); } - { - Json::Value jv; - jv[sfAccount.jsonName] = Account{"bob6"}.human (); - jv[sfSendMax.jsonName] = "100000000"; - jv[sfDestination.jsonName] = Account{"bob7"}.human (); - jv[sfTransactionType.jsonName] = jss::CheckCreate; - jv[sfFlags.jsonName] = tfUniversal; - env(jv); - } + env (check::create ("bob6", "bob7", XRP (100))); // bob9 DepositPreauths bob4 and bob8. env (deposit::auth (Account {"bob9"}, Account {"bob4"})); diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 3d2944c6ab..33e1dd7e43 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -334,20 +334,7 @@ class LedgerRPC_test : public beast::unit_test::suite uint256 const checkId { getCheckIndex (env.master, env.seq (env.master))}; - // Lambda to create a check. - auto checkCreate = [] (test::jtx::Account const& account, - test::jtx::Account const& dest, STAmount const& sendMax) - { - Json::Value jv; - jv[sfAccount.jsonName] = account.human(); - jv[sfSendMax.jsonName] = sendMax.getJson(JsonOptions::none); - jv[sfDestination.jsonName] = dest.human(); - jv[sfTransactionType.jsonName] = jss::CheckCreate; - jv[sfFlags.jsonName] = tfUniversal; - return jv; - }; - - env (checkCreate (env.master, alice, XRP(100))); + env (check::create (env.master, alice, XRP(100))); env.close(); std::string const ledgerHash {to_string (env.closed()->info().hash)}; diff --git a/src/test/unity/jtx_unity1.cpp b/src/test/unity/jtx_unity1.cpp index 8c4ff6838e..61fcd9b8b6 100644 --- a/src/test/unity/jtx_unity1.cpp +++ b/src/test/unity/jtx_unity1.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include