ores.cpp.shell-command.command_tests
Table of Contents
The test file for one entity's shell command unit. Every case stops at a guard or at the transport boundary, so the file needs no server and no database. It links against Catch2 and the part's own library.
Each case pins one thing the unit promises. Every derived verb appears in the menu's completion list, which is the only public view of the menu's children. The add verb refuses a signed-out session before it reads a token. It reports the arity it expects and the count it was given. It names the field when a token will not convert. It refuses a session whose account has no default party when a column reads the acting party. The delete and history verbs each refuse a signed-out session. A valid token vector converts and reaches the transport.
The shape-two units carry one more: the history verb rejects --version
with no --diff. That check runs before the login guard, so the case
needs no session.
A current_state entity has no history verb at all, so it carries none
of the history cases.
See the Template variable reference for the complete list of available variables and their semantics.
1. Mustache incantations
The valid token vector comes from {{sample_token}}, one per
user-supplied column, injected by core.py from the column's own type. It
satisfies the unit's parse, not the server's validation, because the case
stops at the transport boundary.
The malformed-token case reads {{malformed_token_column}}, which is
absent when every column accepts any token. It carries the positional
index to overwrite, the token to write there, and the message the unit
must print. The {{#has_session_party_columns}} section emits the
default-party case only when a column reads the acting party.
Every case name starts with {{entity_singular}}_commands. A component's
test files link into one binary, so without the prefix two entities in the
same component declare each case name twice. Catch2 rejects the duplicate,
and ctest does not always surface the rejection, so the prefix is what
keeps the component's suite both startable and honest.
2. Template
The full template source. Edit here and re-tangle with
compass build --direct tangle_codegen_templates to regenerate
library/templates/cpp_shell_command_tests.cpp.mustache.
{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.shell-command.command_tests.org. Edit the org source. }}
{{{cpp_license}}}
{{#domain_entity}}
#include "ores.logging/make_logger.hpp"
#include "ores.nats/service/nats_client.hpp"
#include "ores.shell/app/command_feedback.hpp"
#include "ores.shell/app/commands/{{component}}/{{entity_singular}}_commands.hpp"
#include <catch2/catch_test_macros.hpp>
#include <cli/cli.h>
#include <algorithm>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
using ores::nats::service::nats_client;
using ores::shell::app::command_feedback;
using ores::shell::app::commands::{{entity_singular}}_commands;
using namespace ores::logging;
namespace {
const std::string_view test_suite("ores.shell.{{component}}.tests");
const std::string tags("[commands]");
const std::string valid_party_id("3f2504e0-4f89-11d3-9a0c-0305e82c3301");
void log_in(nats_client& session) {
nats_client::login_info info;
info.username = "tester";
info.jwt = "token";
info.default_party_id = valid_party_id;
session.set_auth(std::move(info));
}
// One token per positional the command takes. A value the command parses is
// not a value the guard reaches, so any token does to prove the guard.
std::vector<std::string> tokens(const std::size_t count) {
return std::vector<std::string>(count, std::string{"sample"});
}
}
TEST_CASE("{{entity_singular}}_commands_registers_every_derived_verb", tags) {
auto lg(make_logger(test_suite));
cli::Menu root_menu("root");
nats_client session;
{{entity_singular}}_commands::register_commands(root_menu, session);
// The menu's completion list is the only public view of its children, so
// a verb that is missing from it was never registered.
const auto completions = root_menu.GetCompletions("{{entity_plural}} ");
for (const auto& verb : {
{{#shell.commands}}
std::string{"{{entity_plural}} {{command}}"},
{{/shell.commands}}
})
CHECK(std::find(completions.begin(), completions.end(), verb) !=
completions.end());
BOOST_LOG_SEV(lg, debug) << "Registered {{shell.command_count}} command(s).";
}
{{#shell.commands}}
TEST_CASE("{{entity_singular}}_commands_process_{{identifier}}_requires_a_session", tags) {
auto lg(make_logger(test_suite));
nats_client session;
std::ostringstream out;
command_feedback::reset();
{{entity_singular}}_commands::process_{{identifier}}(out, session,
tokens({{positional_count}}));
BOOST_LOG_SEV(lg, debug) << "Output for a signed-out session: " << out.str();
CHECK(out.str().find("You must be logged in") != std::string::npos);
CHECK(command_feedback::failed());
}
{{#positional_count}}
{{#exact_count}}
TEST_CASE("{{entity_singular}}_commands_process_{{identifier}}_reports_the_expected_count",
tags) {
auto lg(make_logger(test_suite));
nats_client session;
log_in(session);
std::ostringstream out;
command_feedback::reset();
{{entity_singular}}_commands::process_{{identifier}}(out, session, {});
BOOST_LOG_SEV(lg, debug) << "Output for an empty argument list: " << out.str();
// The arity guard names both the count it expects and the count it
// received. The expected count is shape-specific in the command (a write
// also reads its intent), so the case pins the wording and the received
// count rather than restating that arithmetic.
CHECK(out.str().find("Expected ") != std::string::npos);
CHECK(out.str().find("got 0.") != std::string::npos);
CHECK(command_feedback::failed());
}
{{/exact_count}}
{{/positional_count}}
{{/shell.commands}}
{{/domain_entity}}
3. See also
- Parent facet: ores.cpp.shell-command.
- Sibling archetypes: ores.cpp.shell-command.command_header and ores.cpp.shell-command.command_implementation.
- Template variable reference — the variables this template reads.