mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 22:30:57 +00:00
Add unit tests for host function
This commit is contained in:
52
src/tests/libxrpl/tx/wasm/HostContextFixture.cpp
Normal file
52
src/tests/libxrpl/tx/wasm/HostContextFixture.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#include <tx/wasm/HostContextFixture.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
rust::Slice<std::uint8_t const>
|
||||
HostContextTest::bytesOf(Bytes const& bytes)
|
||||
{
|
||||
return rust::Slice<std::uint8_t const>{bytes.data(), bytes.size()};
|
||||
}
|
||||
|
||||
HostContextTest::OutRegion::OutRegion(std::size_t capacity) : bytes(capacity, kSentinel)
|
||||
{
|
||||
}
|
||||
|
||||
rust::Slice<std::uint8_t>
|
||||
HostContextTest::OutRegion::slice()
|
||||
{
|
||||
return rust::Slice<std::uint8_t>{bytes.data(), bytes.size()};
|
||||
}
|
||||
|
||||
bool
|
||||
HostContextTest::OutRegion::wasWritten() const
|
||||
{
|
||||
return std::ranges::any_of(bytes, [](std::uint8_t b) { return b != kSentinel; });
|
||||
}
|
||||
|
||||
bool
|
||||
HostContextTest::OutRegion::holds(rust::Slice<std::uint8_t const> expected) const
|
||||
{
|
||||
if (expected.size() > bytes.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto want = std::vector<std::uint8_t>(bytes.size(), kSentinel);
|
||||
std::ranges::copy(expected, want.begin());
|
||||
return bytes == want;
|
||||
}
|
||||
|
||||
std::string
|
||||
HostContextTest::logged() const
|
||||
{
|
||||
return sink.messages();
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
56
src/tests/libxrpl/tx/wasm/HostContextFixture.h
Normal file
56
src/tests/libxrpl/tx/wasm/HostContextFixture.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/tx/wasm/HostContext.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/CaptureSink.h>
|
||||
#include <rust/cxx.h>
|
||||
#include <tx/wasm/MockHostFunctions.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// Base for the tests that construct `HostContext` directly, rather than reaching it through
|
||||
// an assembled module.
|
||||
struct HostContextTest : testing::Test
|
||||
{
|
||||
static rust::Slice<std::uint8_t const>
|
||||
bytesOf(Bytes const& bytes);
|
||||
|
||||
// Filled with a sentinel rather than left at zero: an answer can itself be all zero, so
|
||||
// only a byte no answer produces tells "wrote nothing" apart from "wrote zeros".
|
||||
struct OutRegion
|
||||
{
|
||||
static constexpr std::uint8_t kSentinel = 0xcd;
|
||||
|
||||
std::vector<std::uint8_t> bytes;
|
||||
|
||||
explicit OutRegion(std::size_t capacity);
|
||||
|
||||
rust::Slice<std::uint8_t>
|
||||
slice();
|
||||
|
||||
[[nodiscard]] bool
|
||||
wasWritten() const;
|
||||
|
||||
// Means "this value and nothing past it".
|
||||
[[nodiscard]] bool
|
||||
holds(rust::Slice<std::uint8_t const> expected) const;
|
||||
};
|
||||
|
||||
CaptureSink sink{beast::Severity::Warning};
|
||||
testing::StrictMock<MockHostFunctions> host{beast::Journal{sink}};
|
||||
HostContext hostContext{host};
|
||||
|
||||
[[nodiscard]] std::string
|
||||
logged() const;
|
||||
};
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -16,10 +16,11 @@ namespace xrpl::test {
|
||||
|
||||
// A mock of the host the wasm engine calls back into.
|
||||
//
|
||||
// Only the methods the ABI currently declares are mocked, and that is deliberate: the ~60
|
||||
// others keep `HostFunctions`' own `std::unexpected(Unimplemented)`, so a contract reaching
|
||||
// for something the ABI has not declared yet fails the way production would. Add a
|
||||
// `MOCK_METHOD` here when the matching entry is added to `host_functions!`.
|
||||
// Only a few of `HostFunctions`' methods are mocked here. That is the mock lagging the ABI,
|
||||
// not the ABI lacking coverage: `crates/xrpl-host-functions/src/lib.rs` already declares all
|
||||
// 61 entries. Each one not yet mocked keeps `HostFunctions`' own
|
||||
// `std::unexpected(Unimplemented)`, so a contract reaching for it fails the way production
|
||||
// would. Add a `MOCK_METHOD` here as tests for that method are written.
|
||||
struct MockHostFunctions : HostFunctions
|
||||
{
|
||||
explicit MockHostFunctions(beast::Journal journal) : HostFunctions(journal)
|
||||
@@ -46,6 +47,12 @@ struct MockHostFunctions : HostFunctions
|
||||
(Slice const& data),
|
||||
(const, override));
|
||||
|
||||
MOCK_METHOD(
|
||||
(std::expected<Bytes, HostFunctionError>),
|
||||
getTxField,
|
||||
(SField const& fname),
|
||||
(const, override));
|
||||
|
||||
// Takes the rendered text, not the guest's buffer: rendering is `HostContext`'s, so what
|
||||
// a test asserts here is the log line a node would write.
|
||||
MOCK_METHOD(
|
||||
|
||||
126
src/tests/libxrpl/tx/wasm/host_context/TxField.cpp
Normal file
126
src/tests/libxrpl/tx/wasm/host_context/TxField.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <tx/wasm/HostContextFixture.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
|
||||
// side, not here.
|
||||
struct TxFieldCall : HostContextTest
|
||||
{
|
||||
std::int32_t fieldCode = sfBalance.getCode();
|
||||
};
|
||||
|
||||
TEST_F(TxFieldCall, FieldCodeBecomesSFieldHostIsAskedFor)
|
||||
{
|
||||
Bytes const value{1, 2, 3};
|
||||
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
|
||||
|
||||
OutRegion out{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.getTxField(fieldCode, out.slice()), static_cast<std::int32_t>(value.size()));
|
||||
EXPECT_TRUE(out.holds(bytesOf(value)));
|
||||
}
|
||||
|
||||
TEST_F(TxFieldCall, HostErrorBecomesContractReturnValue)
|
||||
{
|
||||
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance)))
|
||||
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FieldNotFound)));
|
||||
|
||||
OutRegion out{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.getTxField(fieldCode, out.slice()),
|
||||
hfErrorToInt(HostFunctionError::FieldNotFound));
|
||||
EXPECT_FALSE(out.wasWritten());
|
||||
}
|
||||
|
||||
TEST_F(TxFieldCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
|
||||
{
|
||||
fieldCode = 0x7fff'0000; // a code nothing is registered under
|
||||
EXPECT_CALL(host, getTxField).Times(0);
|
||||
|
||||
OutRegion out{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.getTxField(fieldCode, out.slice()),
|
||||
hfErrorToInt(HostFunctionError::InvalidField));
|
||||
}
|
||||
|
||||
TEST_F(TxFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
|
||||
{
|
||||
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance)))
|
||||
.WillOnce(testing::Throw(std::runtime_error{"balance field came apart"}));
|
||||
|
||||
OutRegion out{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.getTxField(fieldCode, out.slice()),
|
||||
hfErrorToInt(HostFunctionError::InternalFatal));
|
||||
EXPECT_THAT(logged(), testing::HasSubstr("balance field came apart"));
|
||||
EXPECT_THAT(logged(), testing::HasSubstr("getTxField"));
|
||||
}
|
||||
|
||||
// `guarded`'s `catch (...)` arm, for a thrown value that is not a `std::exception`.
|
||||
TEST_F(TxFieldCall, NonStandardThrowBecomesInternalFatalAndIsLogged)
|
||||
{
|
||||
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Throw(42));
|
||||
|
||||
OutRegion out{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.getTxField(fieldCode, out.slice()),
|
||||
hfErrorToInt(HostFunctionError::InternalFatal));
|
||||
EXPECT_THAT(logged(), testing::HasSubstr("getTxField"));
|
||||
}
|
||||
|
||||
// The out-region contract: write only if the whole value fits, and return the true length
|
||||
// either way.
|
||||
TEST_F(TxFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
|
||||
{
|
||||
Bytes const value{1, 2, 3};
|
||||
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
|
||||
|
||||
OutRegion out{value.size() - 1};
|
||||
EXPECT_EQ(
|
||||
hostContext.getTxField(fieldCode, out.slice()), static_cast<std::int32_t>(value.size()));
|
||||
EXPECT_FALSE(out.wasWritten());
|
||||
}
|
||||
|
||||
TEST_F(TxFieldCall, OutRegionOfExactSizeIsWritten)
|
||||
{
|
||||
Bytes const value{1, 2, 3};
|
||||
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
|
||||
|
||||
OutRegion out{value.size()};
|
||||
EXPECT_EQ(
|
||||
hostContext.getTxField(fieldCode, out.slice()), static_cast<std::int32_t>(value.size()));
|
||||
EXPECT_TRUE(out.holds(bytesOf(value)));
|
||||
}
|
||||
|
||||
// `kMaxWasmDataLength` is the engine's cap, not `HostContext`'s: a length past it crosses
|
||||
// unchanged here, where the sibling engine test sees `DataFieldTooLarge` instead.
|
||||
TEST_F(TxFieldCall, LengthPastProtocolCapCrossesUnchanged)
|
||||
{
|
||||
Bytes const value(kMaxWasmDataLength + 1, 0xab);
|
||||
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
|
||||
|
||||
OutRegion out{value.size()};
|
||||
EXPECT_EQ(
|
||||
hostContext.getTxField(fieldCode, out.slice()), static_cast<std::int32_t>(value.size()));
|
||||
}
|
||||
|
||||
TEST_F(TxFieldCall, EmptyResultAnswersZeroAndWritesNothing)
|
||||
{
|
||||
EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(Bytes{}));
|
||||
|
||||
OutRegion out{32};
|
||||
EXPECT_EQ(hostContext.getTxField(fieldCode, out.slice()), 0);
|
||||
EXPECT_FALSE(out.wasWritten());
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
Reference in New Issue
Block a user