Skip to content

Smoke Mode ​

Set HERMES_SMOKE_TEST=1 and a Hermes Blazor app tests itself: it starts, shows its window, renders, runs your checks, prints a verdict and exits with 0 or 1. It is built for CI runs against packaged builds, where the question is whether this exact build starts and works on each platform. Without the variable, nothing is created or run.

Quick Reference ​

bash
HERMES_SMOKE_TEST=1 ./MyApp
text
HERMES_SMOKE_START: MyApp 1.2.0 macOS arm64
HERMES_SMOKE_MILESTONE: window-shown 412ms
HERMES_READY:880.25
HERMES_SMOKE_MILESTONE: first-render 880ms
HERMES_SMOKE_CHECK_PASS: myapp/database 12ms
HERMES_SMOKE_RESULT: PASSED (1 checks)

Any Hermes Blazor app gets this with no code changes. Checks and gates, described below, let the app test more than startup.

What a Run Checks ​

A run passes only when all of these hold before the budget (60 seconds by default) runs out:

  • The window was shown (window-shown) and every root component rendered once (first-render)
  • Every gate was completed (see Gates)
  • Every check passed
  • No errors were captured. Any of these fails the run:
SourceCaptured when
blazorA component throws an unhandled exception
logAnything logs at Error or Critical through ILogger
dispatcherAn exception escapes work on the UI dispatcher
unobserved-taskA faulted task is never awaited
unhandledAn exception would crash the process
hermesHermes logs an error of its own, for example a hosted service failing to start or the WebView process crashing
dialogThe app tries to open a native dialog
gateA gate is failed with FailGate

A run that runs out of time names what it was still waiting for, for example timed out waiting for first-render.

Adding Checks ​

A check is a named piece of work that runs once, after the first render and every gate. Throw to fail it.

csharp
using Hermes.Blazor.Diagnostics;

public sealed class DatabaseCheck(AppDatabase database) : IHermesSmokeCheck
{
    public string Name => "myapp/database";

    public async Task RunAsync(CancellationToken cancellationToken)
    {
        await database.PingAsync(cancellationToken);
    }
}
csharp
builder.Services.AddHermesSmokeCheck<DatabaseCheck>();
  • Checks are resolved from the page's service scope, so they see the same scoped services as the UI
  • Checks are never created outside smoke mode, so registering them costs nothing in normal runs
  • Each check has 10 seconds by default; override Timeout to change it
  • Name checks by layer, for example myapp/database, so the verdict shows where a failure came from

Waiting for Initialization: Gates ​

Some apps finish starting after the first render, for example by loading a workspace in the background. Register a gate, and the checks wait until the app completes it:

csharp
builder.Services.AddHermesSmokeGate("myapp/workspace-loaded");
csharp
public sealed class WorkspaceLoader(IHermesSmokeSession smoke)
{
    public async Task LoadAsync()
    {
        try
        {
            await LoadWorkspaceAsync();
            smoke.CompleteGate("myapp/workspace-loaded");
        }
        catch (Exception ex)
        {
            smoke.FailGate("myapp/workspace-loaded", ex.Message);
            throw;
        }
    }
}

IHermesSmokeSession can always be injected. Outside smoke mode every member does nothing, so app code never needs to check whether a smoke run is in progress.

A gate that is never completed fails the run with timed out waiting for myapp/workspace-loaded.

Check Sources for Framework Layers ​

A framework that wraps Hermes behind its own check interface can supply checks through IHermesSmokeCheckSource instead of registering each one:

csharp
public sealed class FrameworkCheckSource : IHermesSmokeCheckSource
{
    public IEnumerable<IHermesSmokeCheck> GetChecks(IServiceProvider scopedServices) =>
        scopedServices.GetServices<IFrameworkCheck>().Select(check => new FrameworkCheckAdapter(check));
}
csharp
builder.Services.AddHermesSmokeCheckSource<FrameworkCheckSource>();

Environment Variables ​

VariableMeaning
HERMES_SMOKE_TEST1 turns smoke mode on. Any other value, or no value, leaves it off
HERMES_SMOKE_TEST_TIMEOUTBudget for the whole run in seconds (default 60)
HERMES_SMOKE_TEST_RESULTPath for a JSON copy of the verdict
HERMES_SMOKE_TEST_EXIT0 keeps the app running after the verdict, for looking at a run by hand

HermesSmokeTest in Hermes.Contracts exposes the parsed settings (IsEnabled, Timeout, ResultPath, ExitWhenDone), for example to skip background work such as update checks during a smoke run.

Output ​

Every line starts with a fixed prefix, so CI can pick them out of other console output.

LineMeaning
HERMES_SMOKE_START: <app> <version> <platform> <arch>Smoke mode is on
HERMES_SMOKE_MILESTONE: <name> <ms>msA startup milestone was reached
HERMES_READY:<ms>First render, in the format older Hermes versions printed
HERMES_SMOKE_CHECK_PASS: <name> <ms>msA check passed
HERMES_SMOKE_CHECK_FAIL: <name> <ms>ms - <reason>A check failed or did not run
HERMES_SMOKE_ERROR: <source>: <type>: <message>An error was captured
HERMES_SMOKE_WARNING: <message>Something worth knowing that does not fail the run
HERMES_SMOKE_RESULT: PASSED (<n> checks)The verdict
HERMES_SMOKE_RESULT: FAILED (<f>/<n> checks failed, <e> error(s)[, timed out waiting for <x>])The verdict

The process exits with 0 for PASSED and 1 for FAILED. The HERMES_SMOKE_RESULT line is the authority; treat the exit code as a cross-check.

With HERMES_SMOKE_TEST_RESULT set, the same verdict is written as JSON:

json
{
  "schema": 1,
  "app": "MyApp",
  "version": "1.2.0",
  "platform": "macOS",
  "architecture": "arm64",
  "result": "passed",
  "durationMs": 905,
  "timedOutWaitingFor": null,
  "milestones": [
    { "name": "window-shown", "elapsedMs": 412 },
    { "name": "first-render", "elapsedMs": 880 }
  ],
  "checks": [
    { "name": "myapp/database", "status": "passed", "durationMs": 12, "error": null }
  ],
  "errors": []
}

A check's status is passed, failed or not-run. Each error carries source, type, message and stackTrace.

Behaviour During a Smoke Run ​

  • Native dialogs are not shown. A dialog would block an unattended run, so each attempt fails the run and returns the answer that commits to nothing: null from file and folder pickers, No for yes/no questions, Cancel otherwise
  • Notification permission prompts are suppressed, for the same reason
  • macOS: a passing run closes its window normally. A failed run exits straight away with code 1, because closing the last window makes AppKit end the process with code 0

Running in CI ​

Launch the packaged app with HERMES_SMOKE_TEST=1, capture its standard output, and fail the job unless it prints HERMES_SMOKE_RESULT: PASSED and exits with 0. The Mythetech shared desktop workflows do this on Windows, macOS and Linux, and only upload a release after every platform passes.

Migrating from SmokeTestReporter ​

SmokeTestReporter is obsolete. Smoke mode reports the first render and the verdict itself, so remove calls to SmokeTestReporter.ReportFirstRender.

HERMES_SMOKE_TEST=1 used to print HERMES_READY and keep the app running unless HERMES_SMOKE_TEST_EXIT=1 was also set. It now ends the app after the verdict; set HERMES_SMOKE_TEST_EXIT=0 to keep it running.