Skip to main content

Immutable results and versioned JSON

This major-version change separates mutable execution from immutable report data.

Previous APIReplacement
Task<BDTest.Test.Scenario> from fluent builders/outlinesTask<BDTest.Contracts.ScenarioResult>
List<Scenario> from BDTestUtil.GetScenarios(run)IReadOnlyList<ScenarioResult>
Pending BDTest.Test.ScenarioDefinitionSummaryBDTest.Contracts.ScenarioDefinitionSummary
Step hook Step argumentBDTest.Contracts.StepResult
ScenarioHookContext.Scenario live scenarioImmutable ScenarioResult
BDTest.Test.Status, BDTest.Test.Steps.StepTypeBDTest.Contracts.Status, BDTest.Contracts.StepType
Result GetStoryText() / GetScenarioText()StoryText / ScenarioText properties
Result TestInformationAttribute objectsInformation strings
Result DateTime timestampsUTC DateTimeOffset timestamps

Add using BDTest.Contracts or explicit aliases when both execution argument builders and contract StepTable/StepDocString types are used. Builders still accept structured arguments from BDTest; result arguments use the contracts types. See consumer namespaces for the namespace migration.

Results contain no fixture, execution delegate or run reference. Nested lists, dictionaries, tables and exception trees are immutable. Query run.Scenarios again after teardown to obtain later output. Earlier snapshots remain unchanged after retry, clear or disposal. Use test context and the attachment/HTML writers for recording evidence, rather than modifying result collections.

Before/after hook arguments are separate snapshots. Step after snapshots include step execution timing; scenario after snapshots precede finalized attempt timing and history. See step hooks and scenario hooks.

Core JSON export, static HTML and upload share BDTestReport. New JSON has camelCase fields, independent schemaVersion and producerVersion, enum names, standard invariant durations, and UTC timestamps. Use BDTestJsonHelper.DeserializeReport or BDTestReportJson.Deserialize for new reports. The old Deserialize/DeserializeAsync APIs remain for historical unversioned data; they do not provide a versioned-to-legacy conversion.

BDTestJsonHelper.Serialize(report) writes compact JSON for a BDTestReport. Use Serialize(report, writeIndented: true) for formatted JSON. These are separate one- and two-argument overloads: the formatting parameter is required when selecting the two-argument method. Existing calls keep the same syntax and output. The legacy BDTestOutputModel serializer uses the same overload pattern and still accepts null. Reflection-based callers should select the one-argument overload when omitting formatting; neither two-argument method declares an optional default.

Hosted reports​

Upgrade hosts and clients together for schema-version-1 uploads. BDTest.Razor.Reports and BDTest.Reports.Storage.AzureBlob reference BDTest.Contracts, not execution. Existing persisted unversioned reports remain readable without a data migration.

Change IBDTestDataStore, IDataRepository, IBDTestDataReceiver and IBDTestCustomSidebarLinksProvider implementations from BDTestOutputModel to BDTestReport. Model reads remain nullable. The legacy model-store adapter still forwards JSON null to StoreTestData; modern byte stores retain raw JSON. Use HostedReportJson for version-dispatched reads and BDTestReportJson for writes.

Update copied Razor views to ScenarioResult, StepResult, ExceptionInfo and AttachmentInfo; use text properties and information strings. Full report data is in ViewData[nameof(BDTestReport)]. ReportLayoutViewModel.Timer is ReportTimer. The compact summary models and existing summary/index JSON keep their names.

Run summaries normalize timestamp offsets to UTC so history ordering, date filters and durations compare instants. Scenario and outline pages display shared failure details once even after a JSON round trip; exceptions with different diagnostic data or inner exceptions remain separate.

Contract properties are read-only. Construct a new BDTestReport when changing metadata. The upload controller assigns a fresh storage ID and attachment-token hash to a new report, ignoring uploader-supplied identity/credentials, before calling storage and receivers. Authorization, attachment limits and URLs are unchanged. Renderer extraction remains separate in #851.

Configurable streaming uploads​

BDTestReportUploader is an additive client API: pass a caller-owned HttpClient and BDTestReportUploadOptions, then call UploadAsync(run, descriptor, token). It streams schema-versioned JSON with gzip by default, skips warm-up, and supports an explicit upload key and per-attempt timeout without changing the client. Use BDTestReportUploadOptions.FromEnvironment(address, "BDTEST_REPORT_UPLOAD_API_KEY") for explicit secret configuration; environment lookup is never automatic. See sending results.

Use UploadReportAsync(run, descriptor, token) when you need both the report URL and server-assigned ID. Its BDTest.Contracts.BDTestReportUploadResult exposes ReportUrl and nullable ReportId; UploadAsync retains its Task<Uri> signature. Both methods negotiate HTTP 201 with JSON and Location through the explicit X-BDTest-Upload-Protocol: 1 header. Older server text and JSON-string responses remain readable; ReportId is null if no receipt ID was returned. Clients that omit the protocol header still receive HTTP 200 with a plain-text URL from the new server, independent of Accept.

The four BDTestReportServer.SendDataAndGetReportUriAsync signatures remain for one release as [Obsolete] forwarders. Replace them with a caller-owned client, options and an uploader:

Illustrative fragment
var options = new BDTestReportUploadOptions(serverAddress) { UploadKey = uploadKey };
var uploader = new BDTestReportUploader(httpClient, options);
var result = await uploader.UploadReportAsync(run, descriptor, cancellationToken);
Console.WriteLine(result.ReportUrl);

The forwarders now stream gzip, negotiate typed responses and skip the former warm-up ping. They preserve cancellation, retry behavior and caller-owned client lifetime. Configure UseGzip = false on uploader options if a deployment requires plain JSON. The uploader sets Accept per request without changing the client's default headers. Attachment receipt tokens remain internal to the upload and are not exposed by the typed result. Use matching contracts-based host and client package generations for schema-version-1 uploads.