Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions src/apps/fastcache-cc/LauncherCli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ namespace
.arity = Arity::None,
.operands = " [options]",
.summary = "Report cache statistics for this machine." },
FlagSpec { .action = Action::HtmlStats,
.primary = "--html-stats",
.aliases = NoAliases,
.arity = Arity::None,
.operands = " [options]",
.summary = "Render cache statistics as a self-contained HTML dashboard." },
FlagSpec { .action = Action::ZeroStats,
.primary = "--zero-stats",
.aliases = ZeroStatsAliases,
Expand Down Expand Up @@ -62,6 +68,22 @@ namespace
.summary = "Report only this cohort." },
};

/// The sub-options accepted after `--html-stats`.
constexpr std::array HtmlStatsOptionTable {
FlagSpec { .action = Action::Cohort,
.primary = "--cohort",
.aliases = NoAliases,
.arity = Arity::Value,
.operands = " <id>",
.summary = "Report only this cohort." },
FlagSpec { .action = Action::OutputPath,
.primary = "--out",
.aliases = NoAliases,
.arity = Arity::Value,
.operands = " <path>",
.summary = "Write the dashboard here instead of the default report path." },
};

/// True when `token` was meant as a launcher option rather than as the
/// compiler to front.
///
Expand Down Expand Up @@ -106,15 +128,15 @@ namespace
/// @return The command.
[[nodiscard]] Command Selected(Action action)
{
return { .action = action, .cohortFilter = {}, .diagnostic = {} };
return { .action = action, .cohortFilter = {}, .outputPath = {}, .diagnostic = {} };
}

/// A rejected command line.
/// @param diagnostic Why the arguments could not be used.
/// @return A `UsageError` command carrying `diagnostic`.
[[nodiscard]] Command Rejected(std::string diagnostic)
{
return { .action = Action::UsageError, .cohortFilter = {}, .diagnostic = std::move(diagnostic) };
return { .action = Action::UsageError, .cohortFilter = {}, .outputPath = {}, .diagnostic = std::move(diagnostic) };
}

/// The `FASTCACHE_*` variables, in the order `--help` documents them.
Expand Down Expand Up @@ -190,9 +212,9 @@ namespace

} // namespace

Command ParseStatsOptions(std::span<std::string const> args, std::span<FlagSpec const> options)
Command ParseStatsOptions(std::span<std::string const> args, std::span<FlagSpec const> options, Action baseAction)
{
Command cmd = Selected(Action::ShowStats);
Command cmd = Selected(baseAction);

// Walk a shrinking span rather than an index so consuming a flag's value
// is an explicit step; a value that is never consumed has already been a
Expand Down Expand Up @@ -235,6 +257,8 @@ Command ParseStatsOptions(std::span<std::string const> args, std::span<FlagSpec

if (option->action == Action::Cohort)
cmd.cohortFilter = value;
else if (option->action == Action::OutputPath)
cmd.outputPath = value;
}
return cmd;
}
Expand All @@ -244,6 +268,11 @@ std::span<FlagSpec const> TopLevelFlags() noexcept
return TopLevelTable;
}

std::span<FlagSpec const> HtmlStatsOptions() noexcept
{
return HtmlStatsOptionTable;
}

std::span<FlagSpec const> StatsOptions() noexcept
{
return StatsOptionTable;
Expand All @@ -267,6 +296,8 @@ Command ParseTopLevel(std::span<std::string const> args)
{
if (flag->action == Action::ShowStats)
return ParseStatsOptions(args.subspan(1), StatsOptions());
if (flag->action == Action::HtmlStats)
return ParseStatsOptions(args.subspan(1), HtmlStatsOptions(), Action::HtmlStats);
return Selected(flag->action);
}

Expand Down Expand Up @@ -296,13 +327,18 @@ std::string HelpText(UsageColor color)
for (auto const& spec: StatsOptions())
statsRows.Add(RenderForms(spec), spec.summary);

UsageRows htmlStatsRows;
for (auto const& spec: HtmlStatsOptions())
htmlStatsRows.Add(RenderForms(spec), spec.summary);

UsageRows environmentRows;
for (auto const& spec: LauncherEnvironment())
environmentRows.Add(std::string { spec.name }, spec.summary);

auto const blocks = std::to_array<UsageBlock>({
{ .entries = usageRows.Rows() },
{ .entries = statsRows.Rows() },
{ .entries = htmlStatsRows.Rows() },
{ .entries = environmentRows.Rows() },
{ .text = StateDirectoryNote, .textIndent = 2 },
{ .entries = StateDirectoryRows },
Expand All @@ -315,10 +351,11 @@ std::string HelpText(UsageColor color)
{ .subject = "fastcache-cc - a compiler launcher over the fastcached compile cache." },
{ .title = "USAGE", .blocks = allBlocks.subspan(0, 1) },
{ .title = "STATS OPTIONS", .blocks = allBlocks.subspan(1, 1) },
{ .title = "HTML STATS OPTIONS", .blocks = allBlocks.subspan(2, 1) },
// The three ENVIRONMENT blocks share one section so its two runs of rows
// keep a common column even though prose sits between them.
{ .title = "ENVIRONMENT", .blocks = allBlocks.subspan(2, 3) },
{ .blocks = allBlocks.subspan(5, 2) },
{ .title = "ENVIRONMENT", .blocks = allBlocks.subspan(3, 3) },
{ .blocks = allBlocks.subspan(6, 2) },
});

return RenderUsage({ .sections = sections }, color);
Expand Down
34 changes: 26 additions & 8 deletions src/apps/fastcache-cc/LauncherCli.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@ namespace FastCache::Cc

/// What the launcher was asked to do.
///
/// Every value except `Cohort` is a possible `Command::action`; `Cohort` names a
/// `--show-stats` sub-option and is only ever seen inside `StatsOptions()`.
/// Every value except `Cohort` and `OutputPath` is a possible
/// `Command::action`; those two name sub-options (`--show-stats`'s and
/// `--html-stats`'s respectively) and are only ever seen inside
/// `StatsOptions()`/`HtmlStatsOptions()`.
enum class Action : std::uint8_t
{
Compile, ///< The default: front a real compile.
Help, ///< Print the usage text.
Version, ///< Print the launcher version.
ShowStats, ///< Report the recorded statistics.
ShowStats, ///< Report the recorded statistics as plain text.
HtmlStats, ///< Render the recorded statistics as a self-contained HTML dashboard.
ZeroStats, ///< Discard the statistics log.
Cohort, ///< Stats sub-option: restrict the report to one cohort.
OutputPath, ///< `--html-stats` sub-option: where to write the dashboard.
NoArguments, ///< Invoked with nothing at all — usage, to stderr.
UsageError, ///< Unknown option or a missing option value.
};
Expand Down Expand Up @@ -55,6 +59,15 @@ struct FlagSpec
/// @return A view of the static table; never empty.
[[nodiscard]] std::span<FlagSpec const> StatsOptions() noexcept;

/// The sub-options accepted after `--html-stats`.
///
/// A separate table from `StatsOptions()` rather than a superset: `--out`
/// means nothing after `--show-stats` (it writes to stdout, always), so
/// accepting it there would silently ignore a flag the caller thought did
/// something.
/// @return A view of the static table; never empty.
[[nodiscard]] std::span<FlagSpec const> HtmlStatsOptions() noexcept;

/// Look a token up in a flag table, matching the primary spelling or any alias.
/// @param table The table to search.
/// @param token The command-line token to match.
Expand All @@ -67,6 +80,7 @@ struct Command
{
Action action { Action::Compile }; ///< The selected action.
std::string cohortFilter; ///< From `--cohort`; empty means no filtering.
std::string outputPath; ///< From `--html-stats`'s `--out`; empty means the default path.
std::string diagnostic; ///< Why parsing failed; set iff `action == UsageError`.
};

Expand All @@ -83,15 +97,19 @@ struct Command
/// @return The resolved command.
[[nodiscard]] Command ParseTopLevel(std::span<std::string const> args);

/// Parse the sub-options that may follow `--show-stats`.
/// Parse the sub-options that may follow `--show-stats` or `--html-stats`.
///
/// The option table is a parameter rather than a lookup so the generic
/// table-driven paths stay exercisable independently of which options happen to
/// exist today; `ParseTopLevel` passes `StatsOptions()`.
/// @param args The arguments after the `--show-stats` token itself.
/// exist today; `ParseTopLevel` passes `StatsOptions()` or `HtmlStatsOptions()`.
/// @param args The arguments after the `--show-stats`/`--html-stats` token itself.
/// @param options The table to match each token against.
/// @return A `ShowStats` command, or a usage error.
[[nodiscard]] Command ParseStatsOptions(std::span<std::string const> args, std::span<FlagSpec const> options);
/// @param baseAction The action the returned command carries on success —
/// `ShowStats` for `--show-stats`, `HtmlStats` for `--html-stats`.
/// @return A command with the given action, or a usage error.
[[nodiscard]] Command ParseStatsOptions(std::span<std::string const> args,
std::span<FlagSpec const> options,
Action baseAction = Action::ShowStats);

/// One environment variable the launcher reads.
///
Expand Down
21 changes: 20 additions & 1 deletion src/apps/fastcache-cc/LauncherCli_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Command Parse(std::vector<std::string> const& argv)
TEST_CASE("every accepted flag and alias appears in the help text")
{
auto const help = HelpText();
for (auto const& table: { TopLevelFlags(), StatsOptions() })
for (auto const& table: { TopLevelFlags(), StatsOptions(), HtmlStatsOptions() })
{
for (auto const& spec: table)
{
Expand Down Expand Up @@ -74,6 +74,25 @@ TEST_CASE("the short stats aliases match their long forms")
CHECK(Parse({ "--zero-stats" }).action == Action::ZeroStats);
}

TEST_CASE("the html-stats flag dispatches with no options set")
{
auto const cmd = Parse({ "--html-stats" });
CHECK(cmd.action == Action::HtmlStats);
CHECK(cmd.cohortFilter.empty());
CHECK(cmd.outputPath.empty());
}

TEST_CASE("html-stats accepts --out and --cohort like --show-stats does")
{
auto const cmd = Parse({ "--html-stats", "--out", "report.html", "--cohort", "ci-main" });
CHECK(cmd.action == Action::HtmlStats);
CHECK(cmd.outputPath == "report.html");
CHECK(cmd.cohortFilter == "ci-main");

auto const joined = Parse({ "--html-stats", "--out=report.html" });
CHECK(joined.outputPath == "report.html");
}

TEST_CASE("help is reachable by all three spellings")
{
CHECK(Parse({ "--help" }).action == Action::Help);
Expand Down
Loading
Loading