Introduction
The OpenTAP Runner allows for remote starting and control of OpenTAP sessions via NATS. These sessions can create, load, and execute test plans, modify test plan and test step properties, adjust OpenTAP settings, and provide logs and results.
When to use the OpenTAP Runner?
You should consider the OpenTAP Runner if:
- Your solution requires remote interactions with Test Automation
- Your solution is implemented in another language than C#
- You want to integrate with KS8500
Concepts
OpenTAP Runner Communication
The OpenTAP Runner communicates using NATS.io, a high-performance messaging system that provides a simple yet powerful API for building distributed systems and applications.
The NATS Server can be an instance that you control, or you can install the NATS Server TapPackage to let OpenTAP Runner start the NATS Server, which is the recommended approach.
To install the NATS Server package, use the following command: tap package install "NATS Server"
After installation, the runner can be started with the command: tap runner start
The Runner can start Runner Sessions. A Runner Session is a process that also communicates on NATS and exposes the APIs to conduct the same functionality as KS8400A PathWave Test Automation. This includes creating, loading, modifying, and executing test plans.
There are two ways to start a Runner Session through the Runner: using an Image or just a simple copy of the Runner installation. An Image is a set of OpenTAP Packages and a set of OpenTAP Repositories from where to fetch the packages.
The simple case where we ask the Runner to start a Runner Session not based on an Image looks like this:
The more remote-friendly and flexible solution is to use Images. If you require a Session with a different set of packages than the current Runner installation provides, create an Image in the Runner and then use that Image to start a Runner Session:
Getting started
This guide provides a quick tutorial on setting up and running an OpenTAP project using the OpenTAP Runner and NATS Server. The tutorial will walk you through installing necessary packages, setting up a basic C# Console project, and interacting with the Runner.
Compatibility Runner Client 3.x targets the asynchronous API and is wire-compatible with Runner 2.x. Use Runner Client 2.x only when talking to Runner 1.x installations.
Prerequisites
- An OpenTAP installation
Starting the Runner
In an OpenTAP installation, install the two OpenTAP Packages, "Runner" and "NATS Server":
./tap image install "Runner,NATS Server" --merge
Then start the Runner to start the OpenTAP Runner and the NATS Server:
./tap runner start
The NATS Server process will by default start on nats://127.0.0.1:20111 and the Runner will listen on the OpenTap.Runner.{MachineName} subject.
Docker images
OpenTAP Runner publishes two Linux images to GHCR for Keysight teams:
- Static (version-tagged) image:
ghcr.io/opentap/runner:<version> - Updatable image:
ghcr.io/opentap/runner:updatable(and:updatable-beta/:updatable-rcfor prereleases)
Static images keep the OpenTAP installation inside the image and only expose data folders as volumes. Updatable images mount the full /opt/tap installation so in-container updates persist across restarts.
Example: run a static image and persist data
docker run --rm -p 20111:20111 -p 20116:20116 \
-v runner-storage:/opt/tap/Storage \
-v runner-logs:/opt/tap/SessionLogs \
-v runner-settings:/opt/tap/Settings \
ghcr.io/opentap/runner:<version>
Example: run the updatable image
docker run --rm -p 20111:20111 -p 20116:20116 \
-v runner-installation:/opt/tap \
ghcr.io/opentap/runner:updatable
These images are currently only available to Keysight teams. If you are outside Keysight and are interested in access, reach out via the contact information on the contact page.
Creating a C# Console Project
- Create a new C# Console project:
mkdir MyApp
cd MyApp
dotnet new console
- Add the Runner Client package:
dotnet add package OpenTAP.Runner.Client --version 3.*
- Modify
Program.csto interact with the Runner. The Runner Client 3.x surface is asynchronous, so we use top-levelawait(available in recent.NETtemplates):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runnerClient = new RunnerClient(connection);
SessionClient sessionClient = await runnerClient.StartSessionAsync();
sessionClient.ConnectSessionLogs(handleSessionLogs);
static Task handleSessionLogs(LogList? logList)
{
if (logList?.Logs == null)
return Task.CompletedTask;
foreach (var log in logList.Logs)
Console.WriteLine($"{log.Source,-12}: {log.Message}");
return Task.CompletedTask;
}
// Get the available step types (Similar to "Add new step" dialog)
List<TestStepType> stepTypes = await sessionClient.GetStepTypesAsync();
// Get the current test plan from the session
TestPlan testPlan = await sessionClient.GetTestPlanAsync(null);
// Add a delay step to the test plan
testPlan.ChildTestSteps.Add(stepTypes.First(step => step.TypeName.Contains("Delay")));
// Update the test plan on the session
testPlan = await sessionClient.SetTestPlanAsync(testPlan);
// Give the test plan a name
await sessionClient.SetTestPlanNameAsync("My Testplan");
// Start the test plan
RunStatus runStatus = await sessionClient.RunTestPlanAsync(new List<Parameter>());
// Wait for the test plan to finish
while (runStatus.SessionState != SessionState.Idle)
{
await Task.Delay(250);
runStatus = await sessionClient.GetStatusAsync();
}
// Shutdown the session
await sessionClient.ShutdownAsync();
await runnerClient.ShutdownSessionAsync(sessionClient.Id);
This code connects, starts a Session, connects to session logs, creates a test plan and executes it. After execution finishes, the session is closed.
- Run the project:
dotnet run
The expected output of dotnet run:
TestPlan : -----------------------------------------------------------------
TestPlan : Starting TestPlan 'My Testplan' on 04/05/2023 14:36:25, 1 of 1 TestSteps enabled.
TestPlan : Saved Test Plan XML [1.66 ms]
Settings : No settings file exists for TestPlanRunPackageParameterMonitor. A new instance with default values has been created. [200 us]
TestPlan : PrePlanRun Methods completed [2.46 ms]
TestPlan : "Delay" started.
TestPlan : "Delay" completed. [107 ms]
TestPlan : Test step runs finished. [119 ms]
Summary : ----- Summary of test plan started 04/05/2023 14:36:25 -----
Summary : Delay 107 ms
Summary : ------------------------------------------------------------
Summary : -------- Test plan completed successfully in 199 ms --------
With these steps, you have successfully set up and run an application that remotely executed a testplan using the OpenTAP Runner and NATS Server.
We can now stop the OpenTAP Runner (and NATS Server) by sending CTRL+C in the command prompt with the Runner.
Code Examples
Compatibility Runner Client 3.x exposes asynchronous APIs and is the recommended client for Runner 2.x servers.
- Working with Images and starting Sessions based on Images
- Simple session creation based on Runner installation
- Create and execute a test plan
- Editing step and test plan settings
- Load and execute a test plan
- Receiving results, logs and events from a Session
- How to work with component settings
- Configuring a Runner for default execution and using stored test plans on the Runner
- Copy, paste, undo, and redo test steps
- Working with UserInputs
- Debugging: Breakpoints, pause, and jump-to-step
- Session watchdog configuration
- Test plan validation errors
- Runner status and health monitoring
- Moving and deleting test steps
- Querying and searching session logs
Image creation
For this example we have a Runner started which has the ID: HKZ6QN3
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
Image image = new Image
{
Packages = new List<PackageSpecifier>
{
new PackageSpecifier { Name = "Demonstration" },
new PackageSpecifier { Name = "HTTP", Version = "1.0.0" },
// We can even specify a different runner version for the session
new PackageSpecifier { Name = "Runner", Version = "2" }
},
Repositories = new List<string> { "https://packages.opentap.io" }
};
Image? resolvedImage = await runner.ResolveImageAsync(new List<Image> { image });
if (resolvedImage is null)
throw new InvalidOperationException("Image resolution failed.");
SessionClient newSession = await runner.StartImageSessionAsync(resolvedImage);
// Use the session for test automation
await runner.ShutdownSessionAsync(newSession.Id);
In this example, we successfully resolved an image, started and stopped a session based on the image. This can be verified in the Runner logs:
Session creation
Although the Image approach offers flexibility, some use-cases can be kept simple by just using a Runner Session based on the Runner installation.
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient newSession = await runner.StartSessionAsync();
// Use the session for test automation. Plugins available in this session
// come from the packages installed in the Runner.
await runner.ShutdownSessionAsync(newSession.Id);
Create and Run Test Plan
In this example, we use the simple session from the Session creation example above, but before shutting down the session, we setup and run a simple test plan.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient newSession = await runner.StartSessionAsync();
List<TestStepType> types = await newSession.GetStepTypesAsync();
TestPlan plan = await newSession.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.First(step => step.TypeName.Contains("Delay")));
plan = await newSession.SetTestPlanAsync(plan);
Settings delayStepSettings = await newSession.GetSettingsAsync(plan.ChildTestSteps[0].Id);
if (delayStepSettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
textBox.StringValue = "3 s";
delayStepSettings = await newSession.SetSettingsAsync(plan.ChildTestSteps[0].Id, delayStepSettings);
RunStatus status = await newSession.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
await Task.Delay(1000);
status = await newSession.GetStatusAsync();
}
Console.WriteLine(status.Verdict);
await runner.ShutdownSessionAsync(newSession.Id);
In this example, we retrieved the available test step types using GetStepTypes and chose a Delay step and inserted into the Test Plan using SetTestPlan.
Afterwards, we retrieved the Delay step settings and modified the Time Delay to be 3 s.
Lastly, we ran the Test Plan and continually asked for the status until the SessionState turned back to Idle. The Verdict of the Test Plan was logged and the output of this program would be NotSet.
Editing Step and TestPlan Settings
In this example, we add two steps to a test plan and demonstrate how to read and modify settings on both steps and the plan itself.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();
try
{
// Add a DelayStep and a LogStep to the test plan
List<TestStepType> types = await session.GetStepTypesAsync();
TestStepType delayType = types.First(step => step.TypeName.Contains("Delay"));
TestStepType logType = types.First(step => step.TypeName.Contains("LogStep"));
TestPlan plan = await session.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(delayType);
plan.ChildTestSteps.Add(logType);
plan = await session.SetTestPlanAsync(plan);
Guid delayStepId = plan.ChildTestSteps[0].Id;
Guid logStepId = plan.ChildTestSteps[1].Id;
// Read and modify a TextBoxControl on the Delay step
Settings delaySettings = await session.GetSettingsAsync(delayStepId);
if (delaySettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
{
Console.WriteLine($"Current delay: {textBox.StringValue}");
textBox.StringValue = "5 s";
}
delaySettings = await session.SetSettingsAsync(delayStepId, delaySettings);
// Read a CheckBoxControl on the Delay step
if (delaySettings.FirstOrDefault(s => s.Display.Name == "Enabled") is CheckBoxControl checkBox)
{
Console.WriteLine($"Enabled: {checkBox.BoolValue}");
checkBox.BoolValue = !checkBox.BoolValue; // toggle the value
}
delaySettings = await session.SetSettingsAsync(delayStepId, delaySettings);
// Get plan-level settings using the test plan ID
Settings planSettings = await session.GetSettingsAsync(plan.Id);
foreach (Setting setting in planSettings)
Console.WriteLine($"Plan setting: {setting.Display.Name} ({setting.GetType().Name})");
// Set the test plan name
await session.SetTestPlanNameAsync("My Custom Plan");
}
finally
{
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);
}
GetSettingsAsync and SetSettingsAsync accept a Guid context identifier that can refer to either a test step or the test plan itself. Pass plan.ChildTestSteps[0].Id to configure a step, or plan.Id to inspect and modify plan-level properties.
The settings collection contains polymorphic controls such as TextBoxControl, CheckBoxControl, and DropdownControl. Use C# pattern matching (is TextBoxControl textBox) to handle each control type and access its specific properties — StringValue for text boxes, BoolValue for check boxes, and SelectedIndex / AvailableValues for dropdowns.
Settings are round-tripped: retrieve the current values with GetSettingsAsync, modify the returned control instances in place, then pass the entire collection back to SetSettingsAsync to apply the changes. The returned Settings object reflects the server-side state after the update.
Serialize and Deserialize Settings JSON
If you need to serialize or deserialize raw settings payloads outside request/response methods, use RunnerSerialization.SerializerOptions so polymorphic controls deserialize reliably.
using System.Text.Json;
using OpenTap.Runner.Client;
string rawSettingsJson = """
[
{
"StringValue": "5 s",
"PropertyName": "DelaySecs",
"ControlType": "TextBoxControl"
}
]
""";
Settings? parsed = JsonSerializer.Deserialize<Settings>(rawSettingsJson, RunnerSerialization.SerializerOptions);
if (parsed is { Count: > 0 } && parsed[0] is TextBoxControl delay)
delay.StringValue = "10 s";
JsonSerializerOptions writeOptions = RunnerSerialization.SerializerOptions;
writeOptions.WriteIndented = true;
string updatedJson = parsed == null ? "[]" : JsonSerializer.Serialize(parsed, writeOptions);
Load and Run Test Plan
In this example, we use the simple session from the Session creation example above, but before shutting down the session, we load and run a Test Plan locally stored on the disk.
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient newSession = await runner.StartSessionAsync();
await newSession.SetTestPlanXMLAsync(await File.ReadAllTextAsync("MyTestPlan.TapPlan"));
RunStatus status = await newSession.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
await Task.Delay(1000);
status = await newSession.GetStatusAsync();
}
Console.WriteLine(status.Verdict);
await runner.ShutdownSessionAsync(newSession.Id);
Results, Logs and Events
In this example, we use the simple session from the Create and Run Test Plan section above, but with results, logs and events logged.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient newSession = await runner.StartSessionAsync();
newSession.ConnectSessionLogs(LogHandler);
Task LogHandler(LogList? list)
{
if (list?.Logs != null)
foreach (var log in list.Logs)
Console.WriteLine($"{log.Source,-12}: {log.Message}");
return Task.CompletedTask;
}
newSession.ConnectSessionResults(ResultHandler, RunHandler);
Task ResultHandler(Result? result)
{
if (result != null)
Console.WriteLine($"Result: {result.Status}");
return Task.CompletedTask;
}
Task RunHandler(TestRun? run)
{
if (run != null)
Console.WriteLine($"Run: {run.Status}");
return Task.CompletedTask;
}
newSession.ConnectSessionEvents(EventHandler);
Task EventHandler(SessionEvent? evt)
{
if (evt != null)
Console.WriteLine($"Event: {evt.GetType().Name}");
return Task.CompletedTask;
}
List<TestStepType> types = await newSession.GetStepTypesAsync();
TestPlan plan = await newSession.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.First(step => step.TypeName.Contains("Delay")));
plan = await newSession.SetTestPlanAsync(plan);
Settings delayStepSettings = await newSession.GetSettingsAsync(plan.ChildTestSteps[0].Id);
if (delayStepSettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
textBox.StringValue = "3 s";
delayStepSettings = await newSession.SetSettingsAsync(plan.ChildTestSteps[0].Id, delayStepSettings);
RunStatus status = await newSession.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
await Task.Delay(1000);
status = await newSession.GetStatusAsync();
}
Console.WriteLine(status.Verdict);
await runner.ShutdownSessionAsync(newSession.Id);
This code should produce the following output, where events, runs and results are prepended with "Event: ", "Run: ", and "Result: ".
Working with ComponentSettings
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient newSession = await runner.StartSessionAsync();
try
{
List<ComponentSettingsIdentifier> componentSettings = await newSession.GetComponentSettingsOverviewAsync();
foreach (var componentSetting in componentSettings)
Console.WriteLine($"Component Setting: {componentSetting.Name} ({componentSetting.GroupName})");
// Outputs:
// Component Setting: DUTs(Bench)
// Component Setting: Engine(-)
// Component Setting: Instruments(Bench)
// Component Setting: Connections(Bench)
// Component Setting: Results(-)
// Component Setting: Editor(-)
// Component Setting: Cloud Drive(-)
ComponentSettingsIdentifier instrumentIdentifier = componentSettings
.First(s => s.Name == "Instruments" && s.GroupName == "Bench");
List<ListItemType> instrumentTypes = await newSession.GetComponentSettingsListAvailableTypesAsync(
instrumentIdentifier.GroupName,
instrumentIdentifier.Name);
foreach (var instrumentType in instrumentTypes)
Console.WriteLine($"Instrument Type: {string.Join("/", instrumentType.TypeDisplay.Group)}/{instrumentType.TypeDisplay.Name}");
// Outputs:
// Instrument Type: / Generic SCPI Instrument
// Instrument Type: Demo / Battery Test / Power Analyzer
// Instrument Type: Demo / Battery Test / Temperature Chamber
// Instrument Type: Demo / Results And Timing/ Timing Instrument
ComponentSettingsBase instruments = await newSession.GetComponentSettingsAsync(
instrumentIdentifier.GroupName,
instrumentIdentifier.Name);
if (instruments is ComponentSettingsList list)
foreach (var instrument in list.Items)
Console.WriteLine($"Instrument: {instrument.Name}");
// Outputs:
// Instrument: SCPI
// Instrument: PSU
// Instrument: TEMP
ComponentSettingsBase updated = await newSession.AddComponentSettingsListItemAsync(
instrumentIdentifier.GroupName,
instrumentIdentifier.Name,
instrumentTypes.First(type => type.TypeDisplay.Name == "Power Analyzer").TypeName);
if (updated is ComponentSettingsList listWithNewItem)
{
foreach (var instrument in listWithNewItem.Items)
Console.WriteLine($"Instrument: {instrument.Name}");
// Outputs:
// Instrument: SCPI
// Instrument: PSU
// Instrument: TEMP
// Instrument: PSU (1)
foreach (var setting in listWithNewItem.Items[^1].Settings)
{
Console.WriteLine($"Setting: {setting.Display.Name}");
if (setting is TextBoxControl textBox)
Console.WriteLine(textBox.StringValue);
}
}
// Outputs:
// Setting: Cell Size Factor
// 0.005
}
finally
{
await runner.ShutdownSessionAsync(newSession.Id);
}
Default Endpoints
Default endpoints are endpoints that affect each other. They are:
- SetDefaultImage: Sets a default image in the Runner environment. The image includes essential packages.
- GetDefaultImage: Retrieves the currently set default image.
- SetDefaultSettings: Applies default settings for the Runner. Settings might have various package dependencies.
- GetDefaultSettings: Retrieves the current settings from the Runner.
- StartDefaultSession: Starts a session using the default settings and image.
- StartDefaultSessionOverrideImage: Begins a session with an overridden image for specific use cases.
A Session's plugin configuration depends on:
- The dependencies of the TestPlan.
- The dependencies of the Settings.
- The packages defined in a "default image".
- Optionally, the packages defined in an "image override".
The following example demonstrates how to use the RunnerClient for managing test plans in a Runner session.
public static async Task DefaultTestPlanShowcaseAsync(RunnerClient runnerClient)
{
Image baseImage = new()
{
Packages = new List<PackageSpecifier>
{
new PackageSpecifier { Name = "Runner", Version = "1.7.0" }
},
Repositories = new List<string>
{
"https://packages.opentap.io"
}
};
await runnerClient.Default.SetImageAsync(baseImage); // Apply the base image configuration
RepositoryPackageReference settingsPackageReference = new()
{
Repository = "https://test-automation.pw.keysight.com/api/packages",
Path = "/users/dennis.rasmussen@keysight.com/BatterySettings.TapPackage",
Version = "0.2.0"
};
await runnerClient.Default.SetSettingsAsync(settingsPackageReference); // Apply settings package
RepositoryPackageReference testPlanReference = new()
{
Repository = "https://test-automation.pw.keysight.com/api/packages",
Path = "/users/dennis.rasmussen@keysight.com/BatteryPlan.TapPlan",
Version = "0.1.0-Draft.5"
};
SessionClient session = await runnerClient.Default.StartSessionAsync(testPlanReference);
try
{
RunStatus status = await session.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
await Task.Delay(1000);
status = await session.GetStatusAsync();
}
Console.WriteLine($"Verdict: {status.Verdict}");
}
finally
{
await session.ShutdownAsync();
await runnerClient.ShutdownSessionAsync(session.Id);
}
}
Copy, paste, undo, and redo test steps
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();
try
{
List<TestStepType> stepTypes = await session.GetStepTypesAsync();
TestStepType delay = stepTypes.First(step => step.TypeName == "OpenTap.Plugins.BasicSteps.DelayStep");
TestPlan plan = await session.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan = await session.SetTestPlanAsync(plan);
plan.ChildTestSteps.Add(delay);
plan = await session.SetTestPlanAsync(plan);
Guid firstStepId = plan.ChildTestSteps[0].Id;
plan.ChildTestSteps.Add(delay);
plan = await session.SetTestPlanAsync(plan);
Guid targetStepId = plan.ChildTestSteps[1].Id;
string clipboard = await session.CopyStepsAsync(new HashSet<Guid> { firstStepId });
await session.PasteStepsAsync(targetStepId, clipboard);
await session.TestPlanUndoAsync(); // removes the pasted copy
await session.TestPlanRedoAsync(); // reapplies the paste
}
finally
{
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);
}
Working with UserInputs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();
try
{
// Used to wait for the test plan to finish executing
var readyEvent = new ManualResetEventSlim(false);
bool planStarted = false;
// Subscribe to session events to handle user input requests
session.ConnectSessionEvents(async eve =>
{
if (eve == null)
return;
Console.WriteLine($"Event: {eve.GetType().Name}");
// When a test step (e.g. DialogStep) requires operator input,
// the session raises a UserInputRequestedEvent
if (eve is UserInputRequestedEvent userInputRequest)
{
// Retrieve the full Interaction model for this request
Interaction interaction = await session.GetUserInputAsync(userInputRequest.RequestId);
Console.WriteLine($"UserInput requested: {interaction.Title}");
// Find the visible ButtonsControl in the interaction settings
if (interaction.Settings.OfType<ButtonsControl>().FirstOrDefault(s => s.VisualStatus.IsVisible) is
ButtonsControl buttons)
{
// Select the first button and invoke it
buttons.SelectedIndex = 0;
buttons.InvokeMethod = true;
// Submit the response — the session resumes execution
await session.SetUserInputAsync(interaction);
}
}
// Signal when the session returns to Idle after the plan has started
if (eve is SessionStateChangedEvent executionChange)
{
if (executionChange.RunStatus.SessionState == SessionState.Executing)
planStarted = true;
if (planStarted && executionChange.RunStatus.SessionState == SessionState.Idle)
readyEvent.Set();
}
});
// Build a test plan with a DialogStep that will trigger a user input
List<TestStepType> stepTypes = await session.GetStepTypesAsync();
TestPlan plan = await session.GetTestPlanAsync(null);
plan.ChildTestSteps.Add(stepTypes.First(step => step.TypeName.Contains("Dialog")));
plan = await session.SetTestPlanAsync(plan);
// Run the test plan — execution blocks until the DialogStep's user input is answered
await session.RunTestPlanAsync(new List<Parameter>());
// Wait for the session to return to Idle
if (!readyEvent.Wait(TimeSpan.FromSeconds(30)))
Console.WriteLine("Test plan did not finish within 30 seconds.");
RunStatus status = await session.GetStatusAsync();
Console.WriteLine($"Verdict: {status.Verdict}");
}
finally
{
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);
}
User inputs are raised by test steps (such as DialogStep) that require operator interaction during execution. When a step requests input, the session publishes a UserInputRequestedEvent containing a RequestId. Use GetUserInputAsync with this ID to retrieve the full Interaction model.
The Interaction contains a Title, Modal flag, Timeout, and Settings. The settings collection uses the same control types as step settings — TextBoxControl, ButtonsControl, and others — so the same patterns for reading and modifying controls apply here. While the session is waiting for a response, its state is WaitingForUserInput.
To submit a response, set InvokeMethod = true on a ButtonsControl (optionally choosing a button via SelectedIndex) and call SetUserInputAsync. The session resumes execution after the input is answered.
Debugging: Breakpoints, pause, and jump-to-step
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();
try
{
// Track break events so we know which step the session stopped on
var breakHit = new ManualResetEventSlim(false);
Guid breakStepId = Guid.Empty;
bool planStarted = false;
var planFinished = new ManualResetEventSlim(false);
session.ConnectSessionEvents(evt =>
{
if (evt is BreakEvent breakEvent)
{
breakStepId = breakEvent.StepId;
breakHit.Set();
}
if (evt is SessionStateChangedEvent stateChange)
{
if (stateChange.RunStatus.SessionState == SessionState.Executing)
planStarted = true;
if (planStarted && stateChange.RunStatus.SessionState == SessionState.Idle)
planFinished.Set();
}
return Task.CompletedTask;
});
// Build a plan with three Delay steps
List<TestStepType> types = await session.GetStepTypesAsync();
TestStepType delayType = types.First(step => step.TypeName.Contains("Delay"));
TestPlan plan = await session.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(delayType);
plan.ChildTestSteps.Add(delayType);
plan.ChildTestSteps.Add(delayType);
plan = await session.SetTestPlanAsync(plan);
Guid step0 = plan.ChildTestSteps[0].Id;
Guid step1 = plan.ChildTestSteps[1].Id;
Guid step2 = plan.ChildTestSteps[2].Id;
// Set each delay to 0.5 seconds
foreach (Guid stepId in new[] { step0, step1, step2 })
{
Settings s = await session.GetSettingsAsync(stepId);
if (s.FirstOrDefault(x => x.PropertyName == "DelaySecs") is TextBoxControl tb)
tb.StringValue = "0.5 s";
await session.SetSettingsAsync(stepId, s);
}
// --- Set breakpoints on the first two steps ---
await session.SetBreakpointsAsync(new BreakPoints { TestSteps = new List<Guid> { step0, step1 } });
BreakPoints currentBp = await session.GetBreakpointsAsync();
Console.WriteLine($"Breakpoints set on {currentBp.TestSteps.Count} steps");
// Run the plan — it will break on step0
await session.RunTestPlanAsync(new List<Parameter>());
breakHit.Wait(TimeSpan.FromSeconds(10));
Console.WriteLine($"Broke on step: {(breakStepId == step0 ? "step0" : breakStepId.ToString())}");
// --- Resume execution — it will break on step1 ---
breakHit.Reset();
await session.RunTestPlanAsync(new List<Parameter>());
breakHit.Wait(TimeSpan.FromSeconds(10));
Console.WriteLine($"Broke on step: {(breakStepId == step1 ? "step1" : breakStepId.ToString())}");
// --- Use SetPauseNext to single-step to the next step ---
// SetPauseNext resumes execution and breaks on the very next step
breakHit.Reset();
await session.SetPauseNextAsync();
breakHit.Wait(TimeSpan.FromSeconds(10));
Console.WriteLine($"PauseNext broke on step: {(breakStepId == step2 ? "step2" : breakStepId.ToString())}");
// --- Jump back to step0 while in Breaking state ---
// Clear breakpoints first so step0 doesn't trigger another break
await session.SetBreakpointsAsync(new BreakPoints { TestSteps = new List<Guid>() });
await session.SetJumpToStepAsync(step0);
// --- Abort the run ---
await session.AbortTestPlanAsync();
planFinished.Wait(TimeSpan.FromSeconds(10));
RunStatus status = await session.GetStatusAsync();
Console.WriteLine($"Final verdict: {status.Verdict}");
}
finally
{
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);
}
Breakpoints pause execution just before a step runs. Set them with SetBreakpointsAsync by passing a BreakPoints object containing the target step IDs. When a breakpoint triggers, the session enters the Breaking state and publishes a BreakEvent with the StepId that caused the break.
While in Breaking state you can:
- Resume — call
RunTestPlanAsyncto continue execution until the next breakpoint or completion. - Step — call
SetPauseNextAsyncto resume and break on the very next step regardless of breakpoints. - Jump — call
SetJumpToStepAsyncto redirect execution to a different step. This only works while in theBreakingstate. - Abort — call
AbortTestPlanAsyncto cancel the run entirely.
Clear all breakpoints by passing an empty list to SetBreakpointsAsync.
Session watchdog configuration
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();
try
{
// Read the current watchdog configuration
WatchDog watchdog = await session.GetWatchDogAsync();
Console.WriteLine($"Current timeout: {watchdog.TerminationTimeout}s, inactive: {watchdog.InactiveSeconds}s");
// Set the watchdog to terminate the session after 120 seconds of inactivity
watchdog.TerminationTimeout = 120;
watchdog = await session.SetWatchDogAsync(watchdog);
Console.WriteLine($"Updated timeout: {watchdog.TerminationTimeout}s");
// InactiveSeconds only counts while the session is Idle and
// resets whenever a NATS request is received. After a brief wait
// the counter reflects the time since the last API call.
await Task.Delay(3000);
watchdog = await session.GetWatchDogAsync();
Console.WriteLine($"After 3s idle: {watchdog.InactiveSeconds:F1}s inactive");
// Disable the watchdog by setting TerminationTimeout to 0
watchdog.TerminationTimeout = 0;
watchdog = await session.SetWatchDogAsync(watchdog);
Console.WriteLine($"Watchdog disabled (timeout={watchdog.TerminationTimeout})");
}
finally
{
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);
}
The session watchdog monitors idle time and terminates unattended sessions. InactiveSeconds tracks how long the session has been idle — it only increments when the session state is Idle and resets whenever a NATS request is received or a test plan runs.
Set TerminationTimeout to the maximum number of idle seconds before the session is automatically shut down. Setting it to 0 disables the watchdog entirely.
Test plan validation errors
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();
try
{
// Start with a valid plan — no validation errors expected
List<TestStepType> types = await session.GetStepTypesAsync();
TestPlan plan = await session.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.First(step => step.TypeName.Contains("Delay")));
plan = await session.SetTestPlanAsync(plan);
TestPlanValidationErrors errors = await session.GetValidationErrorsAsync();
Console.WriteLine($"Validation errors (valid plan): {errors.Count}");
// Disable the step — disabled steps are excluded from validation
Guid delayStepId = plan.ChildTestSteps[0].Id;
Settings settings = await session.GetSettingsAsync(delayStepId);
if (settings.FirstOrDefault(s => s.Display.Name == "Enabled") is CheckBoxControl enabledBox)
enabledBox.BoolValue = false;
await session.SetSettingsAsync(delayStepId, settings);
errors = await session.GetValidationErrorsAsync();
Console.WriteLine($"Validation errors (disabled step): {errors.Count}");
// Print any validation errors found
foreach (TestStepValidationError stepError in errors)
{
Console.WriteLine($" Step {stepError.StepId}:");
foreach (ValidationError ve in stepError.ValidationErrors)
Console.WriteLine($" {ve.PropertyName}: {ve.Error}");
}
}
finally
{
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);
}
GetValidationErrorsAsync returns a TestPlanValidationErrors collection containing one TestStepValidationError per step that has issues. Each entry has a StepId (string GUID) and a list of ValidationError objects with PropertyName and Error message.
Only enabled steps are validated — disabling a step removes it from the validation results. This is useful for temporarily bypassing steps with missing resource dependencies during plan development.
Runner status and health monitoring
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
// --- Runner-level status ---
RunnerStatus status = await runner.GetStatusAsync();
Console.WriteLine($"Runner version: {status.Version}");
Console.WriteLine($"Hostname: {status.Hostname}");
Console.WriteLine($"IP address: {status.IpAddress}");
Console.WriteLine($"Running as service: {status.RunningAsService}");
Console.WriteLine($"Running in container: {status.RunningInContainer}");
// NATS connection info
if (status.RemoteConnection is RemoteConnection rc)
{
Console.WriteLine($"NATS endpoint: {rc.Endpoint}");
Console.WriteLine($"NATS connected: {rc.Connected}");
Console.WriteLine($"NATS RTT: {rc.Rtt}");
}
// Storage and stream health
if (status.StreamInfo is StreamInfoStatus si)
{
Console.WriteLine($"Runs max bytes: {si.RunsMaxBytes}");
Console.WriteLine($"Runs usage: {si.RunsUsage}");
Console.WriteLine($"Disk free: {si.DiskFree}");
}
// --- Active sessions ---
List<Session> sessions = await runner.GetSessionsAsync();
Console.WriteLine($"\nActive sessions: {sessions.Count}");
foreach (Session s in sessions)
{
Console.WriteLine($" {s.Id} — State: {s.SessionState}, StartedBy: {s.StartedBy}");
foreach (var kv in s.Metadata)
Console.WriteLine($" {kv.Key}: {kv.Value}");
}
GetStatusAsync on the RunnerClient returns a RunnerStatus with version, hostname, IP address, and flags indicating whether the Runner is running as a service or in a container. It also includes RemoteConnection (NATS connectivity) and StreamInfoStatus (JetStream storage usage and free disk space) for health monitoring.
GetSessionsAsync lists all active sessions with their state, the user who started them, and any attached metadata. This is useful for building dashboards or monitoring tools that need to track Runner utilization.
Moving and deleting test steps
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();
try
{
List<TestStepType> types = await session.GetStepTypesAsync();
TestStepType delayType = types.First(step => step.TypeName.Contains("Delay"));
TestStepType repeatType = types.First(step => step.TypeName.Contains("RepeatStep"));
// Build a plan: [Delay A, Delay B, Repeat C]
TestPlan plan = await session.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(delayType);
plan.ChildTestSteps.Add(delayType);
plan.ChildTestSteps.Add(repeatType);
plan = await session.SetTestPlanAsync(plan);
Guid stepA = plan.ChildTestSteps[0].Id;
Guid stepB = plan.ChildTestSteps[1].Id;
Guid stepC = plan.ChildTestSteps[2].Id;
Console.WriteLine("Initial order:");
plan = await session.GetTestPlanAsync(null);
foreach (var step in plan.ChildTestSteps)
Console.WriteLine($" {step.TypeName} {step.Id}");
// --- Reorder: move A and B after C → [C, A, B] ---
await session.MoveStepsAsync(new HashSet<Guid> { stepA, stepB }, stepC);
plan = await session.GetTestPlanAsync(null);
Console.WriteLine("\nAfter MoveSteps (A,B after C):");
foreach (var step in plan.ChildTestSteps)
Console.WriteLine($" {step.TypeName} {step.Id}");
// --- Nest: move A and B as children of C ---
await session.MoveStepsAsChildrenAsync(new HashSet<Guid> { stepA, stepB }, stepC);
plan = await session.GetTestPlanAsync(null);
Console.WriteLine("\nAfter MoveStepsAsChildren (A,B into C):");
Console.WriteLine($" Root steps: {plan.ChildTestSteps.Count}");
Console.WriteLine($" Children of C: {plan.ChildTestSteps[0].ChildTestSteps.Count}");
// --- Delete step B ---
await session.DeleteStepsAsync(new HashSet<Guid> { stepB });
plan = await session.GetTestPlanAsync(null);
Console.WriteLine("\nAfter DeleteSteps (B):");
Console.WriteLine($" Root steps: {plan.ChildTestSteps.Count}");
Console.WriteLine($" Children of C: {plan.ChildTestSteps[0].ChildTestSteps.Count}");
}
finally
{
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);
}
Three operations are available for reorganising steps in a test plan:
MoveStepsAsync(stepIds, targetStepId)— Moves the specified steps so they appear immediately after the target step at the same nesting level. This is a reorder operation.MoveStepsAsChildrenAsync(stepIds, targetStepId)— Moves the specified steps so they become children of the target step. The target must support child steps (e.g.RepeatStep,SequenceStep).DeleteStepsAsync(stepIds)— Removes the specified steps from the plan entirely.
All three accept a HashSet<Guid> of step IDs. Moving a step to itself throws an ArgumentException, and moving a parent into one of its own descendants throws an InvalidOperationException.
Querying and searching session logs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();
try
{
// Run a short plan to generate some log entries
List<TestStepType> types = await session.GetStepTypesAsync();
TestPlan plan = await session.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.First(step => step.TypeName.Contains("Delay")));
plan = await session.SetTestPlanAsync(plan);
Settings s = await session.GetSettingsAsync(plan.ChildTestSteps[0].Id);
if (s.FirstOrDefault(x => x.PropertyName == "DelaySecs") is TextBoxControl tb)
tb.StringValue = "1 s";
await session.SetSettingsAsync(plan.ChildTestSteps[0].Id, s);
RunStatus status = await session.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
await Task.Delay(500);
status = await session.GetStatusAsync();
}
// --- Discover available log levels ---
Dictionary<int, string> levels = await session.LogLevelsAsync();
Console.WriteLine("Log levels:");
foreach (var kv in levels)
Console.WriteLine($" {kv.Key}: {kv.Value}");
// --- Discover log sources ---
List<string> sources = await session.SessionLogSourcesAsync();
Console.WriteLine($"\nLog sources: {string.Join(", ", sources)}");
// --- Get per-level log counts ---
Dictionary<int, int> counts = await session.SessionLogCountsAsync();
Console.WriteLine("\nLog counts by level:");
foreach (var kv in counts)
Console.WriteLine($" {(levels.TryGetValue(kv.Key, out string? name) ? name : kv.Key.ToString())}: {kv.Value}");
// --- Fetch paginated log entries ---
List<int> allLevels = levels.Keys.ToList();
LogList? logs = await session.GetSessionLogsAsync(
level: allLevels,
excludedSource: new List<string>(),
filterText: "",
offset: 0,
limit: 20
);
if (logs != null)
{
Console.WriteLine($"\nFirst page: {logs.Logs.Count} entries (offset={logs.Offset}, filtered={logs.FilteredCount})");
foreach (LogEntry entry in logs.Logs.Take(5))
Console.WriteLine($" [{entry.Source}] {entry.Message}");
}
// --- Search logs for a keyword ---
List<int> matchIndexes = await session.SessionLogSearchAsync(
level: allLevels,
excludedSource: new List<string>(),
filterText: "",
searchText: "Delay"
);
Console.WriteLine($"\nSearch hits for 'Delay': {matchIndexes.Count} matching entries");
}
finally
{
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);
}
Session logs can be queried historically using several complementary APIs:
LogLevelsAsync— Returns aDictionary<int, string>mapping numeric level IDs to names (e.g.10 → Debug,20 → Information,30 → Warning,40 → Error).SessionLogSourcesAsync— Returns the list of source names that have emitted log entries in the current session.SessionLogCountsAsync— Returns per-level entry counts asDictionary<int, int>.GetSessionLogsAsync— Fetches a page of log entries filtered by level, excluded sources, and filter text. The returnedLogListincludes the entries, the currentOffset,FilteredCount, andTotalCountper level.SessionLogSearchAsync— Searches log entries matching the filters and returns a list of matching entry indexes. These indexes correspond to positions in the filtered log sequence.
The LogEntry model contains Source, Message, Level (numeric), Timestamp (Unix epoch ticks), and DurationNS.
Future code examples:
- Load from and save to Repository
API Reference
All Runner and Session communication uses NATS request-reply messaging. This document defines the complete set of endpoints, their request/response types, and the data transfer objects (DTOs) used on the wire.
Wire Format
All messages are JSON-serialized. Understanding how different types are encoded on the wire is essential for correct usage:
- Object types (e.g.,
Image,SetSettingsRequest) are sent as JSON objects:{"Name": "...", "Packages": [...]} - String values are sent as JSON string literals. For example,
SetTestPlanXMLexpects a JSON-encoded string containing the XML content. The XML must be wrapped in double quotes with proper JSON escaping:"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TestPlan type=\"OpenTap.TestPlan\">..." - Guid values are sent as JSON string literals in standard format:
"a8a9baa6-d3b0-446d-a3e2-07bbf9310a71" - NoInput is sent as an empty JSON object:
{} - NoResponse is returned as an empty JSON object:
{} - Errors from the server are returned as NATS error responses with an
OpenTapNatsErrorheader. Fatal errors (e.g., invalid input, server exceptions) are communicated this way rather than in the response body.
Runner APIs
The {base} is in the form of: OpenTap.Runner.{RunnerId}.Request
| Subject | Request Type | Response Type | Available since |
|---|---|---|---|
| .Status | NoInput | RunnerStatus | v1.0 |
| .Shutdown | NoInput | NoResponse | v1.0 |
| .GetImages | NoInput | List of Image | v1.0 |
| .GetImage | String | Image | v1.0 |
| .ResolveImage | List of Image | Image | v1.0 |
| .ResolveImageDryRun | List of Image | Image | v1.0 |
| .GetSessionManagerImage | NoInput | Image | v1.0 |
| .GetSessions | NoInput | List of Session | v1.0 |
| .StartSession | NoInput | Session | v1.0 |
| .StartImageSession | Image | Session | v1.0 |
| .StartDefaultSession | RepositoryPackageReference | Session | v1.6 |
| .StartDefaultSessionOverrideImage | DefaultSessionRequest | Session | v1.6 |
| .NewSession | NewSessionRequest | NewSessionResponse | v2.2 |
| .SetSession | Session | Session | v1.0 |
| .ShutdownSession | Guid | NoResponse | v1.0 |
| .RegisterSession | RegisterSessionRequest | RegisterSessionResponse | v1.0 |
| .SetDefaultSettings | RepositoryPackageReference | NoResponse | v1.0 |
| .GetDefaultSettings | NoInput | RepositoryPackageReference | v1.0 |
| .SaveDefaultSettingsInRepository | SaveDefaultSettings | NoResponse | v1.0 |
| .GetDefaultImage | NoInput | Image | v1.5 |
| .SetDefaultImage | Image | Image | v1.5 |
| .UpdateRunner | RunnerUpdateRequest | RunnerUpdateResponse | v1.4 |
| .LogsZip | NoInput | Byte[] | v2.0 |
| .GetComponentSettingsOverview | NoInput | List of ComponentSettingsIdentifier | v1.0 |
| .SetComponentSettings | SetComponentSettingsRequest | ComponentSettingsBase | v1.0 |
| .GetComponentSettings | GetComponentSettingsRequest | ComponentSettingsBase | v1.0 |
| .GetComponentSettingsListItem | GetComponentSettingsListItemRequest | ComponentSettingsListItem | v1.0 |
| .SetComponentSettingsListItem | SetComponentSettingsListItemRequest | ComponentSettingsListItem | v1.0 |
| .GetComponentSettingDataGrid | GetComponentSettingDataGridRequest | DataGridControl | v1.0 |
| .SetComponentSettingDataGrid | SetComponentSettingDataGridRequest | DataGridControl | v1.0 |
| .AddComponentSettingDataGridItemType | AddComponentSettingDataGridItemTypeRequest | DataGridControl | v1.0 |
| .AddComponentSettingDataGridItem | GetComponentSettingDataGridRequest | DataGridControl | v1.0 |
| .GetComponentSettingDataGridTypes | GetComponentSettingDataGridRequest | List of ListItemType | v1.0 |
| .SetComponentSettingsProfiles | List of ProfileGroup | List of ProfileGroup | v1.0 |
| .GetComponentSettingsProfiles | NoInput | List of ProfileGroup | v1.0 |
| .UploadComponentSettings | UploadFileRequest | List of String | v1.0 |
| .DownloadComponentSettings | DownloadTapSettingsRequest | Byte[] | v1.0 |
| .LoadComponentSettingsFromRepository | RepositoryPackageReference | List of ErrorResponse | v1.0 |
| .SaveComponentSettingsToRepository | RepositorySettingsPackageDefinition | NoResponse | v1.0 |
| .GetComponentSettingsListAvailableTypes | GetComponentSettingsRequest | List of ListItemType | v1.0 |
| .AddComponentSettingsListItem | AddComponentSettingsListItemRequest | ComponentSettingsBase | v1.0 |
| .GetSettingsPackageFiles | NoInput | List of String | v1.0 |
| .GetSettingsTypes | NoInput | List of String | v1.0 |
Session APIs
The {base} is in the form of: OpenTap.Runner.{RunnerId}.Session.{SessionId}.Request
| Subject | Request Type | Response Type | Available since |
|---|---|---|---|
| .GetStatus | NoInput | RunStatus | v1.0 |
| .GetEditStatus | NoInput | EditStatus | v2.0 |
| .GetTestPlanXML | NoInput | String | v1.0 |
| .SetTestPlanXML | String | List of String | v1.0 |
| .SetTestPlanName | String | NoResponse | v1.0 |
| .GetTestPlanReference | NoInput | GetTestPlanReferenceResponse | v1.0 |
| .LoadTestPlanFromRepository | RepositoryPackageReference | List of String | v1.0 |
| .SaveTestPlanToRepository | RepositoryPackageDefinition | SaveTestPlanToRepositoryResponse | v1.0 |
| .ResourcesOpen | NoInput | TestPlan | v1.0 |
| .ResourcesClose | NoInput | TestPlan | v1.0 |
| .GetSettings | GetSettingsRequest | Settings | v1.0 |
| .SetSettings | SetSettingsRequest | Settings | v1.0 |
| .GetTestPlan | String[] | TestPlan | v1.0 |
| .SetTestPlan | TestPlan | TestPlan | v1.0 |
| .MoveSteps | MoveRequest | NoResponse | v2.0 |
| .MoveStepsAsChildren | MoveRequest | NoResponse | v2.0 |
| .DeleteSteps | DeleteRequest | NoResponse | v2.0 |
| .GetValidationErrors | NoInput | TestPlanValidationErrors | v1.0 |
| .CommonStepSettings | CommonSettings | CommonSettings | v1.0 |
| .CommonStepSettingsContextMenu | CommonStepSettingsContext | CommonContext | v1.0 |
| .GetUserInputs | NoInput | List of Guid | v1.0 |
| .GetUserInput | Guid | Interaction | v1.0 |
| .SetUserInput | Interaction | Interaction | v1.0 |
| .GetContextMenu | PropertyReferenceRequest | List of Setting | v1.0 |
| .SetContextMenu | SetContextMenuRequest | List of Setting | v1.0 |
| .GetDataGrid | PropertyReferenceRequest | DataGridControl | v1.0 |
| .SetDataGrid | SetDataGridRequest | DataGridControl | v1.0 |
| .AddDataGridItemType | AddDataGridItemTypeRequest | DataGridControl | v1.0 |
| .AddDataGridItem | PropertyReferenceRequest | DataGridControl | v1.0 |
| .GetDataGridTypes | PropertyReferenceRequest | List of ListItemType | v1.0 |
| .GetStepTypes | NoInput | List of TestStepType | v1.0 |
| .CopySteps | CopyRequest | String | v2.0 |
| .PasteSteps | PasteRequest | NoResponse | v2.0 |
| .PasteStepsAsChildren | PasteRequest | NoResponse | v2.0 |
| .RunTestPlan | List of Parameter | RunStatus | v1.0 |
| .SetPauseNext | NoInput | NoResponse | v1.0 |
| .GetBreakpoints | NoInput | BreakPoints | v1.0 |
| .SetBreakpoints | BreakPoints | BreakPoints | v1.0 |
| .SetJumpToStep | Guid | NoResponse | v1.0 |
| .AbortTestPlan | NoInput | NoResponse | v1.0 |
| .TestPlanUndo | NoInput | NoResponse | v2.0 |
| .TestPlanRedo | NoInput | NoResponse | v2.0 |
| .GetSessionLogs | GetSessionLogsRequest | LogList | v1.0 |
| .SessionLogSearch | GetSessionSearchRequest | List of Int32 | v1.0 |
| .SessionLogSources | NoInput | List of String | v1.0 |
| .SessionLogCounts | NoInput | Dictionary of Int32 to Int32 | v1.0 |
| .LogLevels | NoInput | Dictionary of Int32 to String | v1.0 |
| .SetupMetricsPolling | EnableMetricsPollingRequest | EnableMetricsPollingResponse | v2.0 |
| .DiscoverAssets | NoInput | AssetDiscoveryResponse | v2.0 |
| .GetComponentSettingsOverview | NoInput | List of ComponentSettingsIdentifier | v1.0 |
| .SetComponentSettings | SetComponentSettingsRequest | ComponentSettingsBase | v1.0 |
| .GetComponentSettings | GetComponentSettingsRequest | ComponentSettingsBase | v1.0 |
| .GetComponentSettingsListItem | GetComponentSettingsListItemRequest | ComponentSettingsListItem | v1.0 |
| .SetComponentSettingsListItem | SetComponentSettingsListItemRequest | ComponentSettingsListItem | v1.0 |
| .GetComponentSettingDataGrid | GetComponentSettingDataGridRequest | DataGridControl | v1.0 |
| .SetComponentSettingDataGrid | SetComponentSettingDataGridRequest | DataGridControl | v1.0 |
| .AddComponentSettingDataGridItemType | AddComponentSettingDataGridItemTypeRequest | DataGridControl | v1.0 |
| .AddComponentSettingDataGridItem | GetComponentSettingDataGridRequest | DataGridControl | v1.0 |
| .GetComponentSettingDataGridTypes | GetComponentSettingDataGridRequest | List of ListItemType | v1.0 |
| .SetComponentSettingsProfiles | List of ProfileGroup | List of ProfileGroup | v1.0 |
| .GetComponentSettingsProfiles | NoInput | List of ProfileGroup | v1.0 |
| .UploadComponentSettings | UploadFileRequest | List of String | v1.0 |
| .DownloadComponentSettings | DownloadTapSettingsRequest | Byte[] | v1.0 |
| .LoadComponentSettingsFromRepository | RepositoryPackageReference | List of ErrorResponse | v1.0 |
| .SaveComponentSettingsToRepository | RepositorySettingsPackageDefinition | NoResponse | v1.0 |
| .GetComponentSettingsListAvailableTypes | GetComponentSettingsRequest | List of ListItemType | v1.0 |
| .AddComponentSettingsListItem | AddComponentSettingsListItemRequest | ComponentSettingsBase | v1.0 |
| .GetSettingsPackageFiles | NoInput | List of String | v1.0 |
| .GetSettingsTypes | NoInput | List of String | v1.0 |
| .Shutdown | NoInput | NoResponse | v1.0 |
| .GetImage | NoInput | Image | v1.0 |
| .SetWatchDog | WatchDog | WatchDog | v1.0 |
| .GetWatchDog | NoInput | WatchDog | v1.0 |
Endpoint Details
SetTestPlanXML
The String request for SetTestPlanXML must contain valid OpenTAP test plan XML (.TapPlan format). The XML content is JSON-encoded as a string literal on the wire (see Wire Format).
The List of String response contains non-fatal deserialization warnings from the OpenTAP serializer. For example, a missing plugin type or a package version mismatch will appear as a string in this list. An empty list means the test plan loaded without issues.
Fatal errors (e.g., invalid XML, corrupt data, or a test plan already executing) are returned as NATS error responses, not in the response body.
Example using the NATS CLI:
nats req OpenTap.Runner.{RunnerId}.Session.{SessionId}.Request.SetTestPlanXML '"<?xml version=\"1.0\" encoding=\"utf-8\"?><TestPlan type=\"OpenTap.TestPlan\"><Steps /></TestPlan>"'
Data Types
NoInput
Empty JSON object {}.
For example, the Session endpoint to abort a test plan takes a NoInput:
nats req OpenTap.Runner.{RunnerId}.Session.{SessionId}.Request.AbortTestPlan "{}"
NoResponse
Empty JSON object {}.
For example, the Session endpoint to abort a test plan returns a NoResponse:
nats req OpenTap.Runner.{RunnerId}.Session.{SessionId}.Request.AbortTestPlan "{}" returns {}.
RunnerStatus
| Property | Type |
|---|---|
| RemoteConnection | RemoteConnection |
| Version | String |
| RunningAsService | Boolean |
| RunningInContainer | Boolean |
| AutoUpdateDisabled | Boolean |
| AllowParallelRuns | Boolean |
| Hostname | String |
| IpAddress | String |
| StreamInfo | StreamInfoStatus |
RemoteConnection
| Property | Type |
|---|---|
| Endpoint | String |
| Connected | Boolean |
| Rtt | String |
StreamInfoStatus
| Property | Type |
|---|---|
| RunsMaxBytes | Int64 |
| RunsUsage | Int64 |
| DiskFree | Int64 |
Image
| Property | Type |
|---|---|
| Name | String |
| Packages | List of PackageSpecifier |
| Repositories | List of String |
| Id | String |
Session
| Property | Type |
|---|---|
| Subject | String |
| Id | Guid |
| ImageId | String |
| SessionState | SessionState |
| TestPlanRunId | String |
| StartedBy | String |
| Metadata | Dictionary of String to String |
SessionState
Enum with the following values:
IdleLoadingExecutingBreakingAbortingWaitingForUserInput
RunStatus
| Property | Type |
|---|---|
| SessionId | Guid |
| Verdict | Verdict |
| TestPlanRunId | Guid (nullable) |
| FailedToStart | Boolean |
| SessionState | SessionState |
| ExecutingSteps | List of Guid |
EditStatus
| Property | Type |
|---|---|
| TestPlanDirty | Boolean |
| UndoBufferSize | Int32 |
| RedoBufferSize | Int32 |
ErrorResponse
Returned by endpoints such as LoadComponentSettingsFromRepository. Represents a structured error with optional nested inner errors.
| Property | Type |
|---|---|
| Type | String |
| Message | String |
| Parameter | String (nullable) |
| Source | String (nullable) |
| StackTrace | String (nullable) |
| Inner | ErrorResponse (nullable) |
NewSessionRequest
| Property | Type |
|---|---|
| Image | Image |
| UseDefaults | Boolean |
| ImageOverride | Image |
| TestPlanReference | RepositoryPackageReference |
| Settings | Settings |
| RunTestPlan | Boolean |
| TestPlanParameters | List of Parameter |
| ShutdownOnComplete | Boolean |
| Metadata | Dictionary of String to String |
NewSessionResponse
| Property | Type |
|---|---|
| Session | Session |
DefaultSessionRequest
| Property | Type |
|---|---|
| ImageOverride | Image |
| TestPlanReference | RepositoryPackageReference |
RegisterSessionRequest
| Property | Type |
|---|---|
| SessionId | Guid |
| ProcessId | Int32 |
| ExpectedRunnerVersion | String |
| Operator | String |
| Metadata | Dictionary of String to String |
RegisterSessionResponse
| Property | Type |
|---|---|
| Success | Boolean |
| Message | String |
| Url | String |
| AccessToken | String |
| OperatorId | String |
| SettingsFileLocation | String |
| TestPlan | RepositoryPackageReference |
| Settings | Settings |
| RunTestPlan | Boolean |
| Parameters | List of Parameter |
| ShutdownOnComplete | Boolean |
SaveDefaultSettings
| Property | Type |
|---|---|
| SessionId | Guid |
| Repository | String |
Note:
Repositoryis accepted by the client library for forward-compatibility but is currently ignored by the server.
RunnerUpdateRequest
| Property | Type |
|---|---|
| UpdateTo | String |
RunnerUpdateResponse
| Property | Type |
|---|---|
| Success | Boolean |
| ErrorMessage | String |
GetSettingsRequest
| Property | Type |
|---|---|
| ContextId | Guid |
GetTestPlanReferenceResponse
| Property | Type |
|---|---|
| TestPlanReference | RepositoryPackageReference |
SaveTestPlanToRepositoryResponse
| Property | Type |
|---|---|
| TestPlanReference | RepositoryPackageReference |
MoveRequest
| Property | Type |
|---|---|
| StepIds | List of Guid |
| TargetStepId | Guid |
DeleteRequest
| Property | Type |
|---|---|
| StepIds | List of Guid |
Parameter
| Property | Type |
|---|---|
| Group | String |
| Name | String |
| Value | String |
| TypeCode | TypeCode |
EnableMetricsPollingRequest
| Property | Type |
|---|---|
| Enabled | Boolean |
EnableMetricsPollingResponse
| Property | Type |
|---|---|
| JetStreamName | String |
| Enabled | Boolean |
AssetDiscoveryResponse
| Property | Type |
|---|---|
| AssetProviders | List of AssetDiscoveryResult |
| LastSeen | DateTime |
AssetDiscoveryResult
| Property | Type |
|---|---|
| DiscoveredAssets | List of DiscoveredAsset |
| Priority | Double |
| Name | String |
| IsSuccess | Boolean |
| Error | String |
DiscoveredAsset
| Property | Type |
|---|---|
| Manufacturer | String |
| Model | String |
| AssetIdentifier | String |
SetComponentSettingsRequest
| Property | Type |
|---|---|
| ReturnedSettings | ComponentSettingsBase |
| GroupName | String |
| Name | String |
ComponentSettingsBase
| Property | Type |
|---|---|
| Name | String |
| GroupName | String |
ComponentSettingsIdentifier
Extends ComponentSettingsBase. Returned by GetComponentSettingsOverview to identify available component settings without loading their full configuration.
| Property | Type |
|---|---|
| Name | String |
| GroupName | String |
GetComponentSettingsRequest
| Property | Type |
|---|---|
| GroupName | String |
| Name | String |
GetComponentSettingsListItemRequest
| Property | Type |
|---|---|
| Index | Int32 |
| GroupName | String |
| Name | String |
ComponentSettingsListItem
| Property | Type |
|---|---|
| Settings | Settings |
| Name | String |
| EnabledResource | Boolean (nullable) |
| VisualStatus | VisualStatus |
| ValueType | String |
| UnitAttribute | UnitAttribute |
| Display | DisplayAttribute |
| MetaData | MetaData |
| ExternalParameter | ExternalParameter |
SetComponentSettingsListItemRequest
| Property | Type |
|---|---|
| Item | ComponentSettingsListItem |
| Index | Int32 |
| GroupName | String |
| Name | String |
GetComponentSettingDataGridRequest
| Property | Type |
|---|---|
| PropertyName | String |
| Index | Int32 |
| GroupName | String |
| Name | String |
DataGridControl
| Property | Type |
|---|---|
| Items | List of List of Setting |
| FixedSize | Boolean |
| Errors | List of String |
| Layout | Layout |
| ColumnDisplayName | ColumnDisplayName |
| ValueDescription | String |
| PropertyName | String |
| Icons | List of Icon |
| Submit | Boolean |
| VisualStatus | VisualStatus |
| ValueType | String |
| UnitAttribute | UnitAttribute |
| Display | DisplayAttribute |
| MetaData | MetaData |
| ExternalParameter | ExternalParameter |
SetComponentSettingDataGridRequest
| Property | Type |
|---|---|
| DataGridControl | DataGridControl |
| PropertyName | String |
| Index | Int32 |
| GroupName | String |
| Name | String |
AddComponentSettingDataGridItemTypeRequest
| Property | Type |
|---|---|
| TypeName | String |
| PropertyName | String |
| Index | Int32 |
| GroupName | String |
| Name | String |
UploadFileRequest
| Property | Type |
|---|---|
| File | Byte[] |
| FileName | String |
DownloadTapSettingsRequest
| Property | Type |
|---|---|
| GroupName | String |
RepositoryPackageReference
| Property | Type |
|---|---|
| Version | String |
| Path | String |
| PathId | String |
RepositorySettingsPackageDefinition
| Property | Type |
|---|---|
| Name | String |
| Tags | List of String |
| Version | String |
| Path | String |
| PathId | String |
AddComponentSettingsListItemRequest
| Property | Type |
|---|---|
| TypeName | String |
| GroupName | String |
| Name | String |
CopyRequest
| Property | Type |
|---|---|
| StepIds | List of Guid |
PasteRequest
| Property | Type |
|---|---|
| TargetStepId | Guid |
| Clipboard | String |
PackageSpecifier
| Property | Type |
|---|---|
| Name | String |
| Version | String |
| Architecture | String |
| OS | String |
Settings
A JSON array of Setting objects. Serialized as [{...}, {...}, ...].
VisualStatus
| Property | Type |
|---|---|
| IsReadOnly | Boolean |
| IsVisible | Boolean |
| IsEnabled | Boolean |
UnitAttribute
| Property | Type |
|---|---|
| Unit | String |
| PreScaling | Double |
| StringFormat | String |
| UseRanges | Boolean |
| UseEngineeringPrefix | Boolean |
DisplayAttribute
| Property | Type |
|---|---|
| Description | String |
| Group | List of String |
| Name | String |
| Order | Double |
| Collapsed | Boolean |
MetaData
| Property | Type |
|---|---|
| Name | String |
| MacroName | String |
| Group | String |
| Frozen | Boolean |
ExternalParameter
| Property | Type |
|---|---|
| Name | String |
Layout
| Property | Type |
|---|---|
| Mode | LayoutMode |
| RowHeight | Int32 |
| MaxRowHeight | Int32 |
ListItemType
Represents a selectable type in a component settings list or data grid.
| Property | Type |
|---|---|
| TypeName | String |
| TypeDisplay | DisplayAttribute |
ColumnDisplayName
| Property | Type |
|---|---|
| ColumnName | String |
| Order | Double |
| IsReadOnly | Boolean |
Icon
| Property | Type |
|---|---|
| IconName | String |
| Invoke | Boolean (nullable) |
| StepReference | Guid (nullable) |
| PropertyReference | String |
Setting
| Property | Type |
|---|---|
| Errors | List of String |
| Layout | Layout |
| ColumnDisplayName | ColumnDisplayName |
| ValueDescription | String |
| PropertyName | String |
| Icons | List of Icon |
| Submit | Boolean |
| VisualStatus | VisualStatus |
| ValueType | String |
| UnitAttribute | UnitAttribute |
| Display | DisplayAttribute |
| MetaData | MetaData |
| ExternalParameter | ExternalParameter |
RepositoryPackageDefinition
| Property | Type |
|---|---|
| Name | String |
| Tags | List of String |
| Version | String |
| Path | String |
| PathId | String |
TestPlan
| Property | Type |
|---|---|
| ChildTestSteps | List of TestStep |
| Settings | Settings |
| IsOpen | Boolean |
| PropertiesToInclude | List of String |
| Id | Guid |
SetSettingsRequest
| Property | Type |
|---|---|
| ContextId | Guid |
| Settings | Settings |
TestPlanValidationErrors
A JSON array of TestStepValidationError objects.
CommonSettings
| Property | Type |
|---|---|
| Step | TestStep |
| StepIds | List of Guid |
CommonStepSettingsContext
| Property | Type |
|---|---|
| CommonContext | CommonContext |
| PropertyName | String |
CommonContext
| Property | Type |
|---|---|
| ContextItems | List of Setting |
| StepIds | List of Guid |
Interaction
| Property | Type |
|---|---|
| Timeout | String |
| Title | String |
| Modal | Boolean |
| Settings | Settings |
| Id | Guid |
PropertyReferenceRequest
| Property | Type |
|---|---|
| ContextId | Guid |
| PropertyName | String |
ProfileGroup
Represents a group of component settings profiles.
| Property | Type |
|---|---|
| Profiles | List of String |
| CurrentProfile | String |
| GroupName | String |
SetContextMenuRequest
| Property | Type |
|---|---|
| ContextId | Guid |
| PropertyName | String |
| ContextMenu | List of Setting |
SetDataGridRequest
| Property | Type |
|---|---|
| ContextId | Guid |
| PropertyName | String |
| DataGridControl | DataGridControl |
AddDataGridItemTypeRequest
| Property | Type |
|---|---|
| ContextId | Guid |
| PropertyName | String |
| TypeName | String |
BreakPoints
| Property | Type |
|---|---|
| TestSteps | List of Guid |
GetSessionLogsRequest
| Property | Type |
|---|---|
| Levels | List of Int32 |
| ExcludedSources | List of String |
| FilterText | String |
| Offset | Int32 |
| Limit | Int32 |
LogList
| Property | Type |
|---|---|
| Logs | List of LogEntry |
| Offset | Int32 |
| FilteredCount | Int32 |
| TotalCount | Dictionary of String to Int32 |
GetSessionSearchRequest
| Property | Type |
|---|---|
| Levels | List of Int32 |
| ExcludedSources | List of String |
| FilterText | String |
| SearchText | String |
WatchDog
| Property | Type |
|---|---|
| InactiveSeconds | Double |
| TerminationTimeout | Int32 |
TestStep
| Property | Type |
|---|---|
| Id | Guid |
| ChildTestSteps | List of TestStep |
| IsChildTestStepsReadOnly | Boolean |
| IsReadOnly | Boolean |
| Settings | Settings |
| TypeName | String |
| TypeDisplay | DisplayAttribute |
| Name | String |
| ExpandedName | String |
TestStepType
Extends TestStep. Returned by GetStepTypes to describe an available test step type and its allowed children.
| Property | Type |
|---|---|
| Id | Guid |
| ChildTestSteps | List of TestStep |
| IsChildTestStepsReadOnly | Boolean |
| IsReadOnly | Boolean |
| Settings | Settings |
| TypeName | String |
| TypeDisplay | DisplayAttribute |
| Name | String |
| ExpandedName | String |
| AvailableChildrenTypes | List of String |
TestStepValidationError
| Property | Type |
|---|---|
| StepId | String |
| ValidationErrors | List of ValidationError |
LogEntry
| Property | Type |
|---|---|
| Source | String |
| Timestamp | Int64 |
| Message | String |
| Level | Int32 |
| DurationNS | Int64 |
ValidationError
| Property | Type |
|---|---|
| PropertyName | String |
| Error | String |
Verdict
Outcome of a test plan run. Serialized as a PascalCase string.
| Value | Integer |
|---|---|
NotSet |
0 |
Pass |
1 |
Inconclusive |
2 |
Fail |
3 |
Aborted |
4 |
Error |
5 |
SlimSession
Compact session summary included in runner heartbeat events.
| Property | Type |
|---|---|
| SessionId | Guid |
| SessionState | SessionState |
LayoutMode
Flags enum controlling UI layout of a setting. Values can be combined (bitwise OR).
| Value | Integer |
|---|---|
Normal |
1 |
FullRow |
2 |
FloatBottom |
4 |
TypeCode
Maps to System.TypeCode. Indicates the primitive type of a Parameter value. Serialized as a PascalCase string.
| Value | Integer |
|---|---|
Empty |
0 |
Object |
1 |
DBNull |
2 |
Boolean |
3 |
Char |
4 |
SByte |
5 |
Byte |
6 |
Int16 |
7 |
UInt16 |
8 |
Int32 |
9 |
UInt32 |
10 |
Int64 |
11 |
UInt64 |
12 |
Single |
13 |
Double |
14 |
Decimal |
15 |
DateTime |
16 |
String |
18 |
Implementation Guide
This document describes the wire-level protocol for building an OpenTAP Runner client in any programming language. It covers NATS connection setup, the custom request-reply pattern, chunked transfer, event subscriptions, session lifecycle, JetStream usage, and JSON serialization rules. Read it alongside the API Reference, which lists every endpoint and DTO.
A client built from these two documents alone should be able to connect to a running Runner, create a session, load and execute a test plan, collect results, and shut down cleanly.
1. NATS Connection Setup
Default URL
A locally installed Runner listens on:
nats://127.0.0.1:20111
When connecting to a cloud-hosted Runner, the URL is supplied by the cloud platform alongside JWT credentials (see Authentication below).
Recommended Connection Options
Match these options on your NATS client to align with the server's expectations:
| Option | Value | Notes |
|---|---|---|
MaxReconnect |
Forever / -1 | Never stop retrying |
ReconnectWait |
1000 ms | Pause between attempts |
AllowReconnect |
true | Required for reconnect to work |
PingInterval |
45 000 ms | Keep-alive interval |
ConnectTimeout |
5 000 ms | Initial connection timeout |
Authentication
Local Runner — No credentials are required. Connect without authentication.
Cloud Runner — Authenticate with a JWT and NKey seed pair:
- On connection, supply the JWT in the NATS
user_jwtfield. - Sign server nonces using the NKey seed (NaCl Ed25519 signature).
- The specific JWT and seed are provided by the KS8500 cloud platform.
Deriving the RunnerId
The RunnerId is the NATS server's reported name, available immediately after the connection handshake as connection.ServerInfo.Name (or the equivalent field in your NATS client library). All subsequent subject construction requires this value.
C# example:
await using INatsConnection connection = new NatsConnection(new NatsOpts
{
Url = "nats://127.0.0.1:20111"
});
// ServerInfo is populated after the connection is established.
string runnerId = connection.ServerInfo.Name;
string runnerBase = $"OpenTap.Runner.{runnerId}";
2. Subject Hierarchy
All communication uses NATS subjects derived from two base templates. Substitute {RunnerId} with the value derived in section 1.
Base Templates
| Name | Pattern |
|---|---|
| Runner base | OpenTap.Runner.{RunnerId} |
| Session base | OpenTap.Runner.{RunnerId}.Session.{SessionId} |
{SessionId} is a UUID (lowercase, no braces) returned by session-creation endpoints.
Runner Subjects
| Purpose | Subject |
|---|---|
| Runner request | OpenTap.Runner.{RunnerId}.Request.{EndpointName} |
| Runner lifecycle Started | OpenTap.Runner.{RunnerId}.Events.Lifetime.Started |
| Runner lifecycle Stopped | OpenTap.Runner.{RunnerId}.Events.Lifetime.Stopped |
| Runner lifecycle Heartbeat | OpenTap.Runner.{RunnerId}.Events.Lifetime.Heartbeat |
| Runner executing state | OpenTap.Runner.{RunnerId}.Events.Running |
| Runner metadata updated | OpenTap.Runner.{RunnerId}.Events.MetadataUpdated |
Session Subjects
| Purpose | Subject |
|---|---|
| Session request | {SessionBase}.Request.{EndpointName} |
| Session event | {SessionBase}.Events.{EventName} |
| Session logs (streaming) | {SessionBase}.SessionLogs |
| Plan run (start/complete) | {SessionBase}.PlanRun.{PlanRunId} |
| Plan run parameters | {SessionBase}.PlanRun.{PlanRunId}.Parameters |
| Plan run logs | {SessionBase}.PlanRun.{PlanRunId}.Logs |
| Plan run artifact | {SessionBase}.PlanRun.{PlanRunId}.Artifact.{Guid} |
| Step run (start/complete) | {SessionBase}.PlanRun.{PlanRunId}.StepRun.{StepRunId} |
| Step run parameters | {SessionBase}.PlanRun.{PlanRunId}.StepRun.{StepRunId}.Parameters |
| Step run result table | {SessionBase}.PlanRun.{PlanRunId}.StepRun.{StepRunId}.Result.{TableName} |
| Metrics | {SessionBase}.Metrics.{Group}.{Name} |
{TableName} has NATS-unsafe characters (spaces, dots, wildcards, >) replaced with underscores by the server.
NATS Subscription Wildcards
When subscribing to result streams, use NATS wildcards:
| Wildcard | Meaning |
|---|---|
* |
Exactly one token |
> |
One or more tokens (only at end) |
Typical result subscriptions:
{SessionBase}.PlanRun.* → plan run start and complete
{SessionBase}.PlanRun.*.StepRun.* → step run start and complete
{SessionBase}.PlanRun.*.StepRun.*.Result.> → all result tables
{SessionBase}.PlanRun.*.Logs → plan run log batches
3. Request-Reply Protocol
Overview
The Runner does not use the standard NATS request-reply pattern. Instead, the client implements a custom pattern using an explicit reply subscription:
- Client creates a unique reply subject:
{requestSubject}.{NewGuid()} - Client subscribes to that reply subject
- Client publishes the request to
{requestSubject}withreplyToset to the reply subject - Server sends the response (potentially in multiple chunks) to the reply subject
- Client reads from the reply subscription until the full response is assembled
- Client unsubscribes from the reply subject
This is necessary because the response may be chunked across multiple messages (see section 4), which standard request-reply does not support.
Headers
The following NATS message headers are used in requests:
| Header | When Present | Value |
|---|---|---|
Authorization |
Endpoints that require a token | The access token string |
RequestId |
Multi-chunk request payloads | A new UUID per request |
ChunkSize |
Multi-chunk request payloads | Chunk size in bytes (integer string) |
ChunkNumber |
Multi-chunk request payloads | 1-indexed chunk sequence number |
Default Timeout
The default request timeout is 1 minute. There is no built-in retry. If the server does not respond within the timeout, treat the request as failed.
Error Detection
Before deserializing a response, inspect its headers:
If the
OpenTapNatsErrorheader is present (any value), the response body is an error object:{ "Message": "Human-readable error description" }Raise an exception with that message.
If the NATS layer reports no subscribers (
NatsNoRespondersExceptionor equivalent), the runner or session is not available on that subject.
Pseudocode
function request(subject, payload, token=null, timeout=60s):
replySubject = subject + "." + newGuid()
subscription = nats.subscribe(replySubject)
try:
headers = {}
if token != null:
headers["Authorization"] = token
encodedPayload = jsonEncode(payload) // null → empty byte array
if len(encodedPayload) > chunkSize:
publishInChunks(subject, encodedPayload, headers, replySubject)
else:
nats.publish(subject, encodedPayload, headers, replyTo=replySubject)
chunks = []
deadline = now() + timeout
while now() < deadline:
msg = subscription.readNext(deadline - now())
if msg.hasNoResponders:
raise NoRespondersError
if "OpenTapNatsError" in msg.headers:
raise RunnerError(jsonDecode(msg.body).Message)
if msg.body.length > 0:
chunks.append(msg.body)
if "ChunkSize" not in msg.headers:
break
chunkSize = int(msg.headers["ChunkSize"])
if msg.body.length != chunkSize:
break // last chunk
raise TimeoutError if deadline passed
return jsonDecode(concatenate(chunks))
finally:
subscription.unsubscribe()
C# Example
// BaseClient.RequestAsync<T> — simplified excerpt
INatsSub<byte[]> sub = await connection.SubscribeCoreAsync<byte[]>($"{subject}.{Guid.NewGuid()}");
try
{
var headers = new NatsHeaders();
if (useToken && Token != null)
headers.Add("Authorization", Token);
byte[] payload = JsonSerializer.SerializeToUtf8Bytes(data, RunnerSerialization.DefaultSerializerOptions);
if (payload.Length > _chunkSize)
await PublishInChunksAsync(subject, payload, headers, sub.Subject, ct);
else
await connection.PublishAsync(subject, payload, headers, replyTo: sub.Subject, cancellationToken: ct);
List<byte[]> chunks = new();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(timeout);
while (!cts.Token.IsCancellationRequested)
{
NatsMsg<byte[]> msg = await sub.Msgs.ReadAsync(cts.Token);
if (msg.HasNoResponders)
throw new NatsNoRespondersException();
if (msg.Headers?.ContainsKey("OpenTapNatsError") == true)
throw new RunnerException(JsonSerializer.Deserialize<RunnerError>(msg.Data).Message);
if (msg.Data?.Length > 0)
chunks.Add(msg.Data);
if (msg.Headers == null || !msg.Headers.TryGetValue("ChunkSize", out var cs))
break;
if (msg.Data?.Length != int.Parse(cs))
break;
}
return JsonSerializer.Deserialize<T>(CombineChunks(chunks));
}
finally
{
await sub.UnsubscribeAsync();
}
4. Chunked Transfer Protocol
Large messages — in both directions — are split into chunks. Every endpoint that accepts or returns a payload may involve chunking. Clients must handle chunks even for small payloads, because the server may choose to chunk any response.
Chunk Size
| Side | Calculation | Typical Value |
|---|---|---|
| Client (sending) | MaxPayload − 10 KB |
~1 MB on a default NATS server |
| Client (fallback) | 1 MB | Used when MaxPayload is unavailable |
| Server (publishing results/events) | 90 KB (fixed) | Aimed at staying under 100 KB per message |
| Server (request handler) | MaxPayload − 50 KB |
Conservative reservation for headers |
MaxPayload is advertised by the NATS server in the connect INFO message (connection.ServerInfo.MaxPayload).
Sending Chunks (Client → Server)
- Encode the full payload as UTF-8 JSON bytes.
- If the payload fits within one chunk, send it as a single message with no chunk headers.
- If the payload exceeds one chunk:
- Add headers to every message:
RequestId: a new UUID string (same value for all chunks of one request)ChunkSize: the chunk size in bytes (integer string)ChunkNumber: 1-indexed position of this chunk (integer string)
- Send chunks sequentially.
- Trailing empty message: if the final chunk's data length is exactly equal to
ChunkSize(i.e., no partial last chunk), send one additional message with an empty body. This is also required when the total payload is empty (zero bytes). This sentinel tells the receiver that no more chunks are coming.
- Add headers to every message:
Receiving Chunks (Client ← Server)
Read messages from the reply subscription in a loop:
chunks = []
while true:
msg = read_next()
if msg.body.length > 0:
chunks.append(msg.body)
if "ChunkSize" not in msg.headers:
break // single-message response (no chunking in use)
chunkSize = int(msg.headers["ChunkSize"])
if msg.body.length != chunkSize:
break // this message is shorter than a full chunk → it is the last one
// reassemble: concatenate all chunks in order
payload = concatenate(chunks)
The last chunk is detected by its size being strictly less than ChunkSize (or empty). The server never sends a ChunkNumber count in advance; the client discovers the end by size.
Server-Side Reassembly
The server reassembles chunked requests using a RequestHandler keyed on RequestId:
- Supports out-of-order delivery (chunks are stored by index).
- Maximum 1000 chunks per request (~1 GB at the default NATS max payload).
- Incomplete requests expire after 60 seconds of inactivity and are discarded.
C# Example (Sending)
// PublishInChunksAsync — from BaseClient.cs
private async Task PublishInChunksAsync(string subject, ReadOnlyMemory<byte> payload,
NatsHeaders headers, string replyTo, CancellationToken ct)
{
headers.Add("RequestId", Guid.NewGuid().ToString());
headers.Add("ChunkSize", _chunkSize.ToString());
int sent = 0, chunkNumber = 1;
while (sent < payload.Length)
{
var chunkHeaders = CloneHeaders(headers);
chunkHeaders["ChunkNumber"] = chunkNumber.ToString();
int count = Math.Min(payload.Length - sent, _chunkSize);
await connection.PublishAsync(subject, payload.Slice(sent, count).ToArray(),
chunkHeaders, replyTo, cancellationToken: ct);
sent += count;
chunkNumber++;
}
// Sentinel: send empty message if last chunk was exactly full (or payload was empty)
if (payload.Length % _chunkSize == 0)
{
headers["ChunkNumber"] = chunkNumber.ToString();
await connection.PublishAsync(subject, Array.Empty<byte>(), headers, replyTo, cancellationToken: ct);
}
}
5. Session Lifecycle
Session States
Sessions progress through a defined set of states. The current state is always available via GetStatus and is also broadcast in SessionStateChanged events.
| Value | Integer | Description |
|---|---|---|
Idle |
0 | Session is ready; no test plan is executing |
Executing |
1 | Test plan is running |
Breaking |
2 | Execution is paused at a breakpoint |
Aborting |
3 | Abort requested; executing cleanup |
WaitingForUserInput |
4 | Execution paused; awaiting user input |
Loading |
5 | Session is initialising; not yet ready |
[Created] → Loading → Idle → Executing → Idle
↕
Breaking
↕
WaitingForUserInput
↓
Aborting → Idle
Creating a Session
Call one of the session-creation endpoints on the Runner:
| Endpoint | Description |
|---|---|
NewSession |
Full-featured creation: image, defaults, optional auto-run and auto-close |
StartSession |
Simple session using Runner defaults |
StartImageSession |
Session using a specific image |
All return a Session object containing the session's UUID (Id). Construct the session base subject immediately:
sessionBase = "OpenTap.Runner.{RunnerId}.Session.{session.Id}"
Waiting for Readiness
A newly created session starts in the Loading state and is not immediately ready to accept commands. Wait for SessionState.Idle before sending requests.
Option A — Poll:
while true:
status = request(sessionBase + ".Request.GetStatus")
if status.SessionState == "Idle":
break
sleep(250ms)
Option B — Subscribe:
subscribe(sessionBase + ".Events.SessionStateChanged", handler = function(event):
if event.RunStatus.SessionState == "Idle":
markReady()
)
Shutdown Sequence
Always shut down sessions explicitly to release resources:
- Send
Shutdownto the session:request(sessionBase + ".Request.Shutdown") - Send
ShutdownSessionto the runner (passing the session UUID):request(runnerBase + ".Request.ShutdownSession", sessionId)
Watchdog / Heartbeat
| Interval | Description |
|---|---|
| Runner heartbeat | Every 15 seconds, published to Events.Lifetime.Heartbeat |
| Session heartbeat | Every 5 seconds, published to Events.Heartbeat |
| Session inactivity timeout | Default 1800 seconds (30 minutes); configurable via TerminationTimeout |
| Runner kill threshold | Sessions not seen for 7 seconds in production (1800 s in development mode) are terminated |
If your client needs to maintain a long-lived session without activity, monitor the session heartbeat events and ensure the NATS connection stays alive.
6. Event and Subscription Model
Events are published as fire-and-forget core NATS messages (not JetStream). Subscribe before creating a session or starting a test plan to avoid missing early events.
Runner Events
Subscribe to these on the runner base subject (OpenTap.Runner.{RunnerId}.{suffix}):
StartedLifetimeEvent
Published when the Runner process has started.
Subject: Events.Lifetime.Started
No payload fields.
StoppedLifetimeEvent
Published when the Runner process is stopping.
Subject: Events.Lifetime.Stopped
No payload fields.
HeartbeatLifetimeEvent
Published every 15 seconds. Contains a snapshot of all currently active sessions.
Subject: Events.Lifetime.Heartbeat
| Property | Type | Description |
|---|---|---|
Sessions |
List of SlimSession | All sessions currently alive on this Runner |
RunningEvent
Published when the Runner's overall executing state changes.
Subject: Events.Running
| Property | Type | Description |
|---|---|---|
IsRunning |
Boolean | Whether any session is currently executing a test plan |
MetadataUpdatedEvent
Published when Runner-level metadata changes (e.g. after a session is registered or updated).
Subject: Events.MetadataUpdated
| Property | Type | Description |
|---|---|---|
SessionId |
Guid | The session whose metadata changed |
Metadata |
Dictionary of String → String | Updated key/value metadata map |
Session Events
All session events are published to {SessionBase}.Events.{EventName}. Each event type has its own subject; there is no single wildcard subject that captures all session events. Subscribe to each individually.
TypeCacheInvalidatedEvent
Published when the session's type cache has been invalidated and must be refreshed.
Subject: Events.TypeCacheInvalidated
No payload fields.
TestPlanSettingsChangedEvent
Published when the test plan settings associated with the session have changed.
Subject: Events.TestPlanSettingsChanged
No payload fields.
SessionTimeoutEvent
Published immediately before the session terminates due to watchdog inactivity timeout.
Subject: Events.SessionTimeout
No payload fields.
StartingEvent
Published when a test plan execution is about to begin (sequencer is transitioning into Executing).
Subject: Events.Starting
No payload fields.
StartedEvent
Published after a test plan execution has started and the first step is about to run.
Subject: Events.Started
No payload fields.
HeartbeatEvent
Published every 5 seconds while the session is alive. Use this to monitor session health and watchdog state.
Subject: Events.Heartbeat
| Property | Type | Description |
|---|---|---|
Timestamp |
Int64 | Unix timestamp in seconds |
WatchDog |
WatchDog | Watchdog state: inactivity counter and termination threshold |
State |
SessionState | Current session state |
TestPlanRunID |
Guid (nullable) | Active plan run ID, or null if not executing |
WatchDog fields:
| Property | Type | Description |
|---|---|---|
InactiveSeconds |
Double | Seconds elapsed since last activity |
TerminationTimeout |
Int32 | Seconds of inactivity before the session is terminated (default 1800) |
StoppingEvent
Published when a test plan execution is in the process of stopping (e.g. after abort or normal completion, before teardown is complete).
Subject: Events.Stopping
No payload fields.
StoppedEvent
Published when a test plan execution has fully stopped and the session has returned to Idle.
Subject: Events.Stopped
No payload fields.
TestPlanChangedEvent
Published whenever the in-memory test plan is modified (step added, removed, reordered, or a property changed).
Subject: Events.TestPlanChanged
| Property | Type | Description |
|---|---|---|
EditStatus |
EditStatus | Current dirty/undo/redo state of the test plan editor |
EditStatus fields:
| Property | Type | Description |
|---|---|---|
TestPlanDirty |
Boolean | Whether there are unsaved changes |
UndoBufferSize |
Int32 | Number of available undo steps |
RedoBufferSize |
Int32 | Number of available redo steps |
SessionStateChangedEvent
Published whenever the session's execution state changes (e.g. Idle → Executing, Executing → Idle). This is the primary event for tracking execution progress.
Subject: Events.SessionStateChanged
| Property | Type | Description |
|---|---|---|
RunStatus |
RunStatus | Full status snapshot including state, verdict, and active plan run ID |
SettingsChangedEvent
Published when a component settings group has been modified.
Subject: Events.SettingsChanged
| Property | Type | Description |
|---|---|---|
Group |
String | Component settings group name |
Name |
String | Component settings name within the group |
TestStepChangedEvent
Published when a specific test step has been modified.
Subject: Events.TestStepChanged
| Property | Type | Description |
|---|---|---|
StepId |
Guid | ID of the test step that changed |
BreakEvent
Published when execution pauses at a breakpoint. The session enters Breaking state.
Subject: Events.Break
| Property | Type | Description |
|---|---|---|
StepId |
Guid | ID of the step where execution broke |
UserInputRequestedEvent
Published when a test step requires user input before it can continue. The session enters WaitingForUserInput state.
Subject: Events.UserInputRequested
| Property | Type | Description |
|---|---|---|
RequestId |
Guid | Identifier of the pending input request; use it with GetUserInput and submit via SetUserInput |
UserInputRequestCompletedEvent
Published when a pending user input request has been resolved (answered, cancelled, or timed out).
Subject: Events.UserInputRequestCompleted
| Property | Type | Description |
|---|---|---|
RequestId |
Guid | Identifier of the completed input request |
Answered |
Boolean | True if the user provided input; false if cancelled |
Timeout |
Boolean | True if the request timed out without input |
Subscribing to Session Events — C# Example
// SessionClient.ConnectSessionEvents — simplified excerpt
void SubscribeEvent<TEvent>(string eventName) where TEvent : SessionEvent
{
Task.Run(async () =>
{
await foreach (var msg in connection.SubscribeAsync<TEvent>(
$"{sessionBase}.Events.{eventName}", cancellationToken: cts.Token))
{
if (msg.Data != null)
await handler(msg.Data);
}
});
}
SubscribeEvent<TypeCacheInvalidatedEvent>("TypeCacheInvalidated");
SubscribeEvent<TestPlanSettingsChangedEvent>("TestPlanSettingsChanged");
SubscribeEvent<SessionTimeoutEvent>("SessionTimeout");
SubscribeEvent<StartingEvent>("Starting");
SubscribeEvent<StartedEvent>("Started");
SubscribeEvent<HeartbeatEvent>("Heartbeat");
SubscribeEvent<StoppingEvent>("Stopping");
SubscribeEvent<StoppedEvent>("Stopped");
SubscribeEvent<TestPlanChangedEvent>("TestPlanChanged");
SubscribeEvent<SessionStateChangedEvent>("SessionStateChanged");
SubscribeEvent<SettingsChangedEvent>("SettingsChanged");
SubscribeEvent<TestStepChangedEvent>("TestStepChanged");
SubscribeEvent<BreakEvent>("Break");
SubscribeEvent<UserInputRequestedEvent>("UserInputRequested");
SubscribeEvent<UserInputRequestCompletedEvent>("UserInputRequestCompleted");
Session Logs (Streaming)
Real-time log output is published to {SessionBase}.SessionLogs as LogList messages (a list of LogEntry). Subscribe before starting the session if you need complete logs.
Result Streams
Results are published as test plan execution proceeds. Use NATS wildcard subscriptions:
| Subject pattern | Payload type | Trigger |
|---|---|---|
{SessionBase}.PlanRun.* |
JSON object with Status = "TestPlanRunStart" or "TestPlanRunCompleted" |
Plan run start / complete |
{SessionBase}.PlanRun.*.StepRun.* |
JSON object with Status = "TestStepRunStart" or "TestStepRunCompleted" |
Step run start / complete |
{SessionBase}.PlanRun.*.StepRun.*.Result.> |
Result |
Result table published by a test step |
{SessionBase}.PlanRun.*.Logs |
List<LogEntry> |
Log batch during plan run; null body signals end of run |
{SessionBase}.PlanRun.*.Artifact.* |
Binary stream | Artifact file; Name NATS header contains the filename |
Messages on plan run and step run subjects carry a Seq NATS header (unsigned integer string) which can be used to detect gaps or reorder messages. The sequence number is scoped to the session and resets to 0 at the start of each plan run.
Artifacts are published as a raw binary stream in CHUNK_SIZE (90 KB) pieces terminated by a null/empty message. Read them the same way as chunked responses (section 4), but the Name header is only present on the first message.
Metrics
Metrics are published exclusively to JetStream on {SessionBase}.Metrics.{Group}.{Name}. They are stored in the Metric stream with a subject transform to Metric.{Group}.{Name}. Use a JetStream consumer to read historical or live metrics.
7. JetStream vs Core NATS
Core NATS (fire-and-forget pub/sub)
Used for:
- All request-reply (section 3)
- All event subscriptions (section 6)
- Session log streaming
These messages are not persisted. If you are not subscribed when a message is published, it is lost.
JetStream (persistent streams)
Used for:
- Test plan run results (plan runs, step runs, result tables, parameters)
- Plan run logs
- Artifacts
- Metrics
Runs stream configuration:
| Setting | Value |
|---|---|
| Name | Runs |
| Storage | File |
| Retention | Interest (kept while at least one consumer is active) |
| Discard | New (new messages dropped when stream is full) |
| MaxBytes | Configurable (default unlimited; monitor disk usage) |
| Subjects | Conditionally: plan runs, step runs, parameters, results, logs, artifacts |
Metric stream configuration:
| Setting | Value |
|---|---|
| Name | Metric |
| Storage | File |
| Retention | Limits |
| Discard | Old |
| Max age | 2 days |
| Subject transform | {SessionBase}.Metrics.> → Metric.> |
Whether JetStream is available depends on the Runner's configuration. If the Runs stream does not exist, results and logs are published as core NATS messages instead (and may be missed if not subscribed at the right time). In practice, always subscribe to result and log subjects before starting execution regardless of JetStream availability.
8. JSON Serialization
Wire Format
All message bodies are UTF-8 encoded JSON. There is no binary envelope or framing — the raw JSON bytes are the NATS message payload (potentially across multiple chunks).
Null Handling
Both the server and client omit null-valued properties from serialized output. When deserializing, treat absent properties as null. Do not reject messages that are missing optional fields.
Enum Serialization
Enums are serialized as PascalCase strings on the wire, regardless of which side produces them:
- Server uses Newtonsoft.Json
StringEnumConverter(default PascalCase). - Client uses System.Text.Json
JsonStringEnumConverterwith per-property overrides to ensure PascalCase output on properties that carry enums.
Deserialization is case-insensitive. When building a client, always serialize enums as PascalCase strings and accept any casing.
Examples:
{ "SessionState": "Idle" }
{ "Verdict": "Pass" }
{ "LayoutMode": "FullRow" }
Polymorphic Types
Three object hierarchies use explicit discriminator properties to identify concrete types on the wire.
| Base type | Discriminator property | Example values |
|---|---|---|
ComponentSettingsBase |
ComponentSettingsType |
ComponentSettings, ComponentSettingsList, ComponentSettingsIdentifier |
TestStep |
TestStepTypeOrInstance |
TestStep, TestStepType, TestStepCopy |
Setting |
ControlType |
TextBoxControl, DataGridControl, DropdownControl, ... |
When serializing objects of these types, include the discriminator. When deserializing, use the discriminator to select the concrete class, or retain raw JSON when the type is unknown.
AllowOutOfOrderMetadataProperties
The client's System.Text.Json options include AllowOutOfOrderMetadataProperties = true. Do not assume discriminator or metadata properties appear first in the JSON object.
Recommendations for New Client Implementations
- Serialize enum values as PascalCase strings.
- Omit null properties from outgoing messages.
- Use case-insensitive enum deserialization.
- Accept unknown properties without error (forward compatibility).
- Accept discriminator / metadata properties at any position in the object.
9. Error Handling
Application-Level Errors
When the Runner or Session encounters an error processing a request, it responds with:
- NATS header
OpenTapNatsError: true - Body:
{ "Message": "Human-readable description of the error" }
Always check for the OpenTapNatsError header before attempting to deserialize a response as the expected type.
Transport-Level Errors
| Condition | Cause | Action |
|---|---|---|
| No responders | No subscriber on the request subject | The Runner or Session is not running, or the subject is wrong |
| Timeout | No response within 1 minute | The Runner is overloaded or the request subject is wrong |
| Connection closed | NATS connection dropped | Reconnect (use MaxReconnect=Forever) |
| Chunk reassembly expiry | Request chunks did not arrive within 60 s | Retry the full request |
JetStream Errors
| Error code | Meaning |
|---|---|
10077 |
Buffer full — test plan execution pauses until disk space is freed or connectivity is restored |
10047 |
Insufficient storage — the configured MaxBytes exceeds available disk space |
10. Complete Workflow Example
This example walks through the most common workflow end-to-end: connect to a Runner, create a session, run a test plan, collect results, and shut down.
Pseudocode
// 1. Connect
connection = nats.connect("nats://127.0.0.1:20111")
runnerId = connection.serverInfo.name
runnerBase = "OpenTap.Runner." + runnerId
// 2. Create a session
response = request(runnerBase + ".Request.NewSession", {
"UseDefaults": true,
"RunTestPlan": false
})
sessionId = response.Session.Id
sessionBase = runnerBase + ".Session." + sessionId
// 3. Wait for the session to become ready
while true:
status = request(sessionBase + ".Request.GetStatus")
if status.SessionState == "Idle": break
sleep(250ms)
// 4. Subscribe to events, logs, and results (before loading/running)
subscribe(sessionBase + ".Events.SessionStateChanged", onSessionStateChanged)
subscribe(sessionBase + ".Events.Heartbeat", onHeartbeat)
subscribe(sessionBase + ".SessionLogs", onSessionLog)
subscribe(sessionBase + ".PlanRun.*", onTestRun)
subscribe(sessionBase + ".PlanRun.*.StepRun.*", onTestRun)
subscribe(sessionBase + ".PlanRun.*.StepRun.*.Result.>", onResult)
// 5. Load a test plan XML
warnings = request(sessionBase + ".Request.SetTestPlanXML", testPlanXml)
// 6. Execute (optionally pass parameters)
request(sessionBase + ".Request.RunTestPlan", parameters)
// 7. Wait for execution to finish
while true:
event = waitFor(onSessionStateChanged)
if event.RunStatus.SessionState == "Idle": break
// handle Breaking / WaitingForUserInput / Aborting as needed
// 8. Shutdown
request(sessionBase + ".Request.Shutdown")
request(runnerBase + ".Request.ShutdownSession", sessionId)
// 9. Disconnect
connection.close()
C# Example
// 1. Connect
await using INatsConnection connection = new NatsConnection(new NatsOpts
{
Url = RunnerClient.DefaultUrl // "nats://127.0.0.1:20111"
});
// 2. Create clients
RunnerClient runner = new RunnerClient(connection);
SessionClient session = await runner.StartSessionAsync();
// 3. Wait for session to become ready
RunStatus status;
do
{
await Task.Delay(250);
status = await session.GetStatusAsync();
} while (status.SessionState != SessionState.Idle);
// 4. Subscribe to events and results
var cts = new CancellationTokenSource();
session.ConnectSessionEvents(async evt =>
{
if (evt is SessionStateChangedEvent sc)
Console.WriteLine($"State: {sc.RunStatus.SessionState}");
}, cts.Token);
session.ConnectSessionResults(
resultHandler : async result => Console.WriteLine($"Result: {result?.Name}"),
testRunHandler : async testRun => Console.WriteLine($"TestRun: {testRun?.Status}"),
token : cts.Token
);
session.ConnectSessionLogs(async logs =>
{
foreach (var entry in logs?.Logs ?? [])
Console.WriteLine($"[{entry.Source}] {entry.Message}");
}, cts.Token);
// 5. Load and run
List<string> loadWarnings = await session.SetTestPlanXMLAsync(testPlanXml);
foreach (string warning in loadWarnings)
Console.WriteLine($"Load warning: {warning}");
await session.RunTestPlanAsync(new List<Parameter>());
// 6. Wait for completion
do
{
await Task.Delay(500);
status = await session.GetStatusAsync();
} while (status.SessionState != SessionState.Idle);
Console.WriteLine($"Verdict: {status.Verdict}");
// 7. Shutdown
cts.Cancel();
await session.ShutdownAsync();
await runner.ShutdownSessionAsync(session.Id);
Key Points
- Always subscribe to events and results before calling
RunTestPlan. Events published during the transition fromLoading → IdleorIdle → Executingmay be missed otherwise. RunTestPlanreturns immediately; the test plan runs asynchronously. PollGetStatusor useSessionStateChangedevents to detect completion.- After
Shutdownreturns, the session process has stopped. Any further requests to the session will receive a no-responders error. - It is safe to call
ShutdownSessioneven if the session has already exited; the Runner will return success.
CLI Reference
tap.exe runner start
Starts the Runner using the config located at <OpenTapInstallation>/Config/runner.json. See Runner Config section for configuration
Arguments:
--listen : IP address of the interface and port for the internal NATS server to listen on. Default is 127.0.0.1:20111. When registered to KS8500, only the loopback interface is allowed.
Environment variables:
OPENTAP_RUNNER_TOKEN
If RunnerRegistrationUrl is specified in config, and OPENTAP_RUNNER_TOKEN environment variable is set and the CredentialsFilePath is either not set or does not exist, a registration flow will start, similar to the runner register CLI action by using the RunnerRegistrationUrl and RunnerRegistrationToken specified in the configuration file.
If RunnerRegistrationUrl is specified in config, and OPENTAP_RUNNER_TOKEN environment variable is set and the connection is refused by the server due to an Autorization Violation exception, a registration flow will start and if the registration is successful another connection will be attempted.
tap.exe runner session
Starts a Runner Session that can be connected to via NATS.
Arguments:
--id : Guid formatted ID. Used as Session ID. Default is a new random GUID.
--nats-server : NATS Servers to connect to. Multiple entries is supported. Default is nats://localhost:4222.
--log : Specify log file location. This is a standard OpenTAP option that controls where session logs are written. Default is ./SessionLogs.
Standard OpenTAP options are also available:
-v, --verbose : Show verbose/debug-level log messages.
-c, --color : Color messages according to their severity.
-q, --quiet : Quiet console logging.
The session will have the following NATS subject structure:
OpenTap.Session.{ID}
tap.exe runner register
Sets up a new <OpenTapInstallation>/Config/runner.json config file and registers the runner at the specified --url using the --token as authentication and sets the --name as the display name of the runner.
Arguments:
--config : File path to config file
--url : Runners Service Registration Url
--token : Runners Service Registration Token
--name : Display name of the runner
--nats-server : NATS Server to connect to
--capability : Add capability tags
Environment variables:
OPENTAP_RUNNER_TOKEN
Runner Configuration
{
// Display name sent to Runner Service as part of registration and also used in the connection name to NATS.
"Name": "John Runner",
// Directory to store session logs. Created Sessions will also store their sessionlogs at this location.
"LogDestination": "./SessionLogs",
// A performance optimization in image creation. If a previously resolved image is requested again, do not consult repositories, just return last resolved image
"UseImageCache": true,
// Runner Service url. If this is set, the Runner will register and store registration details in the runner.json config
"RunnerRegistrationUrl": "http://runner-url.com",
// Tags to specify the capabilities of this runner. TestPlans created on this Runner will default request to be loaded in Runners with these tags
"Capabilities": [
"PowerAnalyzer",
"SCPI"
],
// Connect to specified NATS servers. If registering using the "RunnerRegistrationUrl", leave this empty to use servers retrieved by the registration process
// If "ws://" or "wss://" scheme is used, the Runner will use the "NATS Server" TapPackage and connect as a leaf server. This eliminates firewall issues.
"NatsUrls": [
"nats://nats:4222"
],
// Enables JWT Bearer token authentication of incomming requests. The signing keys are retrieved by the runner using <AuthUrl>/.well-known/openid-configuration.
"AuthUrl": "http://keycloak.ks8500-url.com/auth/realms/Default",
// Default domain. Client's using SetAccessToken will set their access tokens against this domain.
"DefaultDomain": "http://ks8500-url.com",
// Preconfigured NATS Seed. The Runner will register towards RunnerRegistrationUrl using this seed. If not specified, a new seed will be generated
"NatsSeed": "SUAO4HD23JK4UCLUO2BCQ3FGHNTJ4UOBP3B2G5AS7MABB4ANFSN4RIF27U"
}
Configuration can be overwritten using environment variables. The variables are prefixed with OPENTAP_RUNNER_.
Examples:
- To change the
LogDestination, setOPENTAP_RUNNER_LOGDESTINATION=<DesiredLogDirectory> - To change an array, e.g. the
NatsUrls, setOPENTAP_RUNNER_NATSURLS__0=<Index 0 Nats URL>
REST-API vs Runner
The OpenTAP Runner represents an evolution from the previous OpenTAP REST-API. Our experience highlighted challenges in supporting cloud scenarios with the OpenTAP REST-API, particularly when attempting to access a PC behind a firewall or within a subnet.
To address these challenges, we integrated NATS technology, retaining the core business logic of the OpenTAP REST-API, but replacing ASP.NET Core with NATS.
Given the nature of the changes involved in this transition, particularly the shift in the underlying transport mechanism, some breaking changes were unavoidable. To signal this significant change and the departure from the REST-API model, we established the OpenTAP Runner.
In the past, we provided an autogenerated C# client for the OpenTAP REST-API (OpenTAP.RestApi.Clients.CSharp). However, in light of our experiences, we elected to manually create a C# client (OpenTAP.Runner.Client) and a TypeScript client (OpenTap Runner Client) for the OpenTAP Runner. These have been designed with improvements derived from our experiences with the REST-API client.
Compatibility Runner Client 3.x aligns with Runner 2.x and replaces the earlier synchronous 2.x client. For Runner 1.x, keep using Runner Client 2.x.
Migrating from REST-API to Runner
Migrating from OpenTAP REST-API to OpenTAP Runner is trivial if the provided C# or TypeScript clients were consumed. If so, the migration consists of two steps:
- Exchange the client package
C# Client: Remove the NuGet REST-API client dependency OpenTAP.RestApi.Clients.CSharp and instead add the Runner client dependency OpenTAP.Runner.Client
TypeScript Client: Remove the NPM REST-API client dependency opentap-restapi-clients and instead add the Runner client dependency @opentap/runner-client
- Update calling code to use the asynchronous APIs exposed by Runner Client 3.x.
The API surface is largely the same, but methods now return Task and have Async suffixes to better align with asynchronous transports.
The following C# example converts a code snippet which retrieves test step types, adds a delay step to the testplan, modifies the Time Delay property on the delay step to 3s, runs the test plan and waits for the test plan execution to complete.
The REST-API code would look like this:
var types = await sessionClient.GetTestStepTypesAsync();
var plan = await sessionClient.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.FirstOrDefault(s => s.TypeName.Contains("Delay")));
plan = await sessionClient.SetTestPlanAsync(plan);
var delayStepSettings = await sessionClient.GetSettingsAsync(plan.ChildTestSteps[0].Id);
if (delayStepSettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
textBox.StringValue = "3 s";
delayStepSettings = await sessionClient.SetSettingsAsync(plan.ChildTestSteps[0].Id, delayStepSettings);
var status = await sessionClient.RunTestPlanAsync();
while (status.ExecutionState != ExecutionState.Idle)
{
await Task.Delay(1000);
status = await sessionClient.GetStatusAsync(0);
}
The migrated Runner code:
var types = await sessionClient.GetStepTypesAsync();
var plan = await sessionClient.GetTestPlanAsync(null);
plan.ChildTestSteps.Clear();
plan.ChildTestSteps.Add(types.FirstOrDefault(s => s.TypeName.Contains("Delay")));
plan = await sessionClient.SetTestPlanAsync(plan);
var delayStepSettings = await sessionClient.GetSettingsAsync(plan.ChildTestSteps[0].Id);
if (delayStepSettings.FirstOrDefault(s => s.PropertyName == "DelaySecs") is TextBoxControl textBox)
textBox.StringValue = "3 s";
delayStepSettings = await sessionClient.SetSettingsAsync(plan.ChildTestSteps[0].Id, delayStepSettings);
var status = await sessionClient.RunTestPlanAsync(new List<Parameter>());
while (status.SessionState != SessionState.Idle)
{
await Task.Delay(1000);
status = await sessionClient.GetStatusAsync();
}
Although the majority of the code migration is trivial, the RunnerClient and SessionClient creation is different due to the underlying transport mechanism change from REST to NATS.
The following snippets demonstrates the migration of REST client construction to Runner client construction:
HttpClient httpClient = new HttpClient(handler);
SessionManagerClient sessionManagerClient = new SessionManagerClient(httpClient) { BaseUrl = "https://localhost:20116" };
var session = await sessionManagerClient.StartSessionAsync();
SessionClient sessionClient1 = new SessionClient(httpClient) { BaseUrl = session.Url };
The migrated code:
using NATS.Client.Core;
using OpenTap.Runner.Client;
await using INatsConnection connection = new NatsConnection(new NatsOpts { Url = RunnerClient.DefaultUrl });
await connection.ConnectAsync();
RunnerClient runner = new RunnerClient(connection);
SessionClient sessionClient = await runner.StartSessionAsync();
Debugging OpenTAP Plugins in the Runner
When developing an OpenTAP plugin that will run inside the Runner, you can attach a debugger to the session process and step through your plugin code. The recommended approach is to install the Runner into your plugin's build output directory, enable development mode, and then start the Runner from there.
Overview
In normal operation the Runner deploys sessions into isolated image directories under the system temp folder. This means the session process loads plugins from a deployed copy — not from your build output — making it impossible to hit breakpoints in your source code.
Development mode changes this behaviour so that every session runs directly from the Runner's own installation directory. When the Runner is installed into your build output, sessions load your locally built plugin DLLs and you can attach a debugger to the session process.
Prerequisites
- An OpenTAP installation with the Runner and NATS Server packages installed.
- Your plugin project builds into (or copies output to) a directory that also contains the OpenTAP installation.
Step-by-step setup
1. Install the Runner in your plugin's build output
Add the Runner and NATS Server packages to the OpenTAP installation located in your build output directory. If you are using the OpenTAP NuGet package, this is typically the bin/Debug/net8.0 (or equivalent) folder where tap.dll lives.
cd <your-plugin-build-output>
./tap package install "Runner"
After this step the build output directory contains both the Runner and your plugin DLLs side by side.
2. Set the development mode environment variable
Before starting the Runner, set OPENTAP_RUNNER_DEVELOPMENT_MODE to 1:
Linux / macOS
export OPENTAP_RUNNER_DEVELOPMENT_MODE=1
./tap runner start
Windows (PowerShell)
$env:OPENTAP_RUNNER_DEVELOPMENT_MODE = "1"
.\tap.exe runner start
Windows (cmd)
set OPENTAP_RUNNER_DEVELOPMENT_MODE=1
tap.exe runner start
On startup the Runner logs a warning to confirm the mode is active:
[WRN] Development mode is enabled!
3. Start a session and attach the debugger
Use any client (C# console app, KS8500, or the code examples in this documentation) to create a session on the Runner. Because development mode is enabled, the session process starts in the Runner's own directory — your build output — and loads your plugin code.
Find the session's process ID in the Runner logs or with your OS process tools and attach your IDE debugger to it.
Visual Studio: Debug > Attach to Process, filter for dotnet processes, select the session process.
JetBrains Rider: Run > Attach to Process, select the session process. Rider run configurations support AUTO_ATTACH_CHILDREN which can automatically attach to the spawned session process.
VS Code: Use the .NET: Attach launch configuration targeting the session process ID.
Once attached, set breakpoints in your plugin source and run a test plan that exercises your plugin. Execution pauses at your breakpoints as expected.
What development mode changes
Development mode adjusts several Runner behaviours to support an interactive debugging workflow:
| Behaviour | Normal | Development mode |
|---|---|---|
| Session working directory | Deployed image directory (under system temp) | Runner's own installation directory (your build output) |
Image on StartImageSession |
Client-specified image is deployed | Image is ignored — session always uses the local directory |
| Session heartbeat timeout | 7 seconds | 30 minutes |
| Watchdog termination timeout | Client-controlled | Always 30 minutes |
The extended timeouts prevent the Runner from killing a session that is paused in a debugger. In normal operation, if a session stops sending heartbeats for more than 7 seconds the Runner terminates it. Development mode raises this to 30 minutes so you can step through code without the session being reaped.
The watchdog — which terminates idle sessions after a configurable period — is similarly overridden to 30 minutes regardless of the value set by the client.
IDE run configuration examples
JetBrains Rider / IntelliJ
Create a .NET Project run configuration that builds and launches the Runner with the environment variable set. Enable Auto Attach Children Processes so that Rider automatically attaches the debugger to session processes spawned by the Runner.
<envs>
<env name="OPENTAP_RUNNER_DEVELOPMENT_MODE" value="1" />
</envs>
Visual Studio (launchSettings.json)
{
"profiles": {
"Runner (Dev)": {
"commandName": "Project",
"commandLineArgs": "runner start",
"environmentVariables": {
"OPENTAP_RUNNER_DEVELOPMENT_MODE": "1"
}
}
}
}
VS Code (launch.json)
{
"name": "Runner (Dev)",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/tap.dll",
"args": ["runner", "start"],
"env": {
"OPENTAP_RUNNER_DEVELOPMENT_MODE": "1"
}
}
Environment variable reference
| Variable | Values | Description |
|---|---|---|
OPENTAP_RUNNER_DEVELOPMENT_MODE |
1 or true |
Enables development mode |
Tips
- Rebuild and restart: After rebuilding your plugin, restart the Runner so the new DLLs are loaded. Sessions started before the rebuild still reference the old assemblies.
- Single session: In development mode, image resolution is skipped, so all sessions share the same directory and plugin set. Run one session at a time to avoid conflicts.
- Breakpoints in test steps: Development mode is about attaching a system debugger to the session process. This is separate from the Runner's built-in test plan breakpoints, which pause test plan execution at specific steps without a debugger. Both can be used together.
Security
The OpenTAP Runner is connected to KS8500 through a secure websocket connection. All traffic is encrypted with TLS between KS8500 and the Runner.
A registered Runner is identified and validated using an ed25519 asymmetric key pair, which is generated uniquely to the specific instance of the Runner. The private key never leaves the local machine, and should never be shared with anyone.
When registering a new runner, the user has to paste in a token, that can be retrieved by logging into KS8500 with valid credentials into a specific Keycloak realm.
Key revocation
When a Runner is removed from KS8500, the key is being revoked immediately. This ensures that no traffic between KS8500 and the Runner occurs after removal. The private key is added to a revocation list kept on the server. The Runner needs to be re-registered again in order to communicate with KS8500.
Runner permissions
Per default the Runner is initially registered to a single user, which becomes the owner of the Runner and granted read/write/owner permissions. The owner of a Runner can, however, be share the instance by granting read/write/owner permissions to other users and/or group of users. The fewer users that have access to the runner, the better. Follow the least privilege principle.
Firewall configuration
As all communication to KS8500 is done through a single secure websocket connection, the Runner expects to be able to reach KS8500 on port 443/tcp.
Thus only a single ingress firewall rule is required.
Software updates
Ensure to have installed the latest version of the Runner to be on the latest security level. The latest version of the Runner is available here.
Disk encryption
Consider using disk encryption on the system where you plan to use the Runner. This prevents unattended direct access to the content on the disk.
All data concerning the Runner (Test plans, artifacts, results, logs, etc.) will be stored on the device in an amount of time, until it have been transferred to KS8500.
Changelog
Bold indicates highlights
2.2.0
Added unified NewSession API for single-request session orchestration Added RunnerJson API for stable settings serialization in Runner Client
- Allow specifying Image when UseDefaults is true in NewSession endpoint
- Acknowledge all NATS messages to JetStream
- Prevent multiple Runner instances from running simultaneously
- Publish large messages in chunks in Runner Client
- Include run Id in NATS message payload for TestRun DTOs
- Added validation for websocket port CLI argument
- Added CLI action to adjust NATS log level output
- Detect NATS server architecture mismatch and show clear error
- Improved runner registration license error message
- Throttle step-complete test plan change checks
- Added python3 and py3-numpy packages to the Docker image
- Set UsageCount to not be default enabled
- Made Update Paths Dialog modal and read-only
- Fixed issue where plugins adapt the default Newtonsoft JSON serialization settings used in the Runner
- Fixed session stuck in Aborting state due to race condition
- Fixed events not being received in certain scenarios in Runner Client
- Fixed null reference exception when the Errors property is null in Settings
- Fixed null reference exception when initializing a new test step when a required plugin is missing
- Fixed Windows service auto-restarting after manual stop
- Fixed issues with auto-starting Windows service when the runner is shutdown, including during Runner update scenario
- Fixed Windows service using cached status in CLI actions
- Fixed CLI command to restart service
- Fixed capitalization in runner service status command
- Fixed cloud files not being downloadable while in use
- Fixed wrong JetStream disk space calculation
- Fixed log argument being ignored
- Fixed Publish Runs forced check to use base image instead of current installation
- Fixed Docker volumes for static image
- Fixed many SettingsChanged events firing during session startup
- Updated Runner documentation (code examples, API specifications, CLI reference)
2.1.5
- Fixed null reference exception when initializing a new test step when a required plugin is missing
- Added python3, pip, and numpy to the Docker image
2.1.4
- Fixed issues with auto-starting Windows service when the runner is shutdown, including in Runner update scenario
2.1.3
- Updated CloudDrive package with fixes for test plans with unset FilePath members
- Fixed issue causing download to fail if file exists and cannot be opened for writing
- Fixed issue causing partial file if download fails
- Fixed Publish Runs forced check to use base image instead of current installation
2.1.2
- Updated CloudDrive package with fixes for test plan load errors when dependencies do not exist
2.1.1
- Fixed race condition when removing parameters during test plan serialization
2.1.0
Implemented PasteSteps endpoint Implemented EditStatus functionality to track test plan edit state Introduced a session base class for extensibility
- Updated Metrics and Assets package
- Update test plan reference after saving with CloudDrive
- Optimized SetTestPlanName performance
- Only set capabilities on an empty TestPlan
- Moved LoggingMiddleware to NatsSessionBase
- Updated install script with new arguments and capability option
- Added quiet option to CLI
- Use MiB and GiB instead of bytes for JetStream info
- Rename (or download) a file locally if it was renamed on remote
- Ensure that XDG_RUNTIME_DIR is defined when running systemctl --user commands
- Fixed issue when setting a test plan with child steps that do not belong to a namespace
- Forcefully shut down a session process, but give threads time to complete some work
- Fixed debug mode SetPauseNext requiring multiple calls on subsequent runs
- Fixed import Sweep values bug
- Avoid DefaultSettings to be locked
- Fixed issue with parent step being moved into its own descendant
2.0.0
Implemented new in-process auto-update of the Runner and added version-locked and updatable Docker images Implemented Allow parallel runs functionality Moved Metrics and Assets NATS endpoints to the runner Implemented test plan step undo/redo and move/delete endpoints
- Updated NATS server to 2.11.9
- Implemented new handshake RegisterSession endpoint
- Implemented a wait until Session is ready in key endpoints
- Send run parameters in normal RunStart/RunCompleted messages instead of separate messages
- Improved handshake between KS8500 and the runner
- Exit immediately if KS8500 registration failed
- Renamed runner registration argument from --registrationToken to --token
- Changed Runner heartbeats according to new spec
- Cleaned up unused run metadata
- Do not send null NATS messages anymore to signal end of data
- Removed RunnerListService and utilize AutomationCloud instead
- Caching test plan when test plan changes
- Added Runs and Metrics JetStream size metrics
- Changed SessionEvents and introduced SessionState.Loading and WaitingForUserInput
- Changed KS8500 API endpoints for registering the runner
- Removed legacy pre-cached test plans functionality
- Removed legacy OnTestRun and OnResult messages
- Removed KS8500 ping request and use RRT from NATS instead
- Removed methods marked as obsolete
- Ensured a session run lock is released if session is not executing anymore
- Fixed issue where non-starting sessions reported wrong error
- Fixed bug where Source was emitted as a context menu item
- Fixed NullReferenceException when moving a child test step to the root
- Fixed NotSupportedException when the status request tried to find a local IPv6 address
- Fixed race condition in TestPlanRunning logic
- Fixed bug where sessions could get stuck if the console output buffer gets full
- Fixed issue with wrong session state being set when waiting for input and a test plan is not running anymore
- Various fixes for Windows service management
- Filter system wide packages from session image
- Fixed issue with browsable attribute
- Fixed wrong session state when test plan failed to start
- Fixed problem with waiting for input state change only when executing
1.16.1
- Fixed issue with wrong sequence number being attached to the NATS message for run results
1.16.0
Added status request as both an API endpoint and as a CLI action Added support for building arm64 Docker image
- Added support for getting and setting DataGrid from a User Input
- Improved settings and config cleanup when a runner is removed from remote
- Added component setting for enabling/disabling logging of NATS debug messages
- Redesigned the save default settings API
- Fixed upload package error when path and name do not correspond
- Moved "Publish Artifact" to "Publish Options" group in runner component settings
- Fixed bug when deleting a non-existing storage directory
- Fixed DataGrid issue with CSV import where new rows in CSV file resulted in an error
- Cleaned up Docker image with reduced size of 73MB
- Added sequence number to NATS messages during test plan execution
1.15.1
- Gracefully close sessions if a runner is unregistered from KS8500
- Ensured that installer does not fail if the tap executable does not exist anymore during uninstall/update
1.15.0
Added support for managing the runner as a service through CLI actions Now uses a new Avalonia based installer
- Updated NATS Server to 2.11.1
- Added support for MenuModel items that do not have an IconAttribute
- Set MachineName as part of Asset ID to ensure asset uniqueness
- Made the Runner able to adopt existing sessions from same runner
- Ensured the settings package is always treated as a TapPackage
- Changed Default ImageOverride to actually override resulting image
- Added all current packages as dependencies of Settings package
- Fixed bug where wrong setting could be changed in a DataGrid
- Fixed a bug in image trimming that could result in unexpected packages in created image
- Fixed regression with sessions missing subject
- Fixed permission error when requesting zip logs on Windows
1.14.1
- Auto-attaching sessions on Windows using the same launch profile for both Linux and Windows
- Added metric names and log NATS request context on exceptions
- Log leaf node output in debug level
- Fixed CultureInfo exception in Docker image
- Updated Cloud Drive dependency to version 0.6.4
1.14.0
Added new API endpoint for retrieving all logs as a single zip
- When loading a test plan, do not request for user input
- Fixed side effects update in data grids
- Ensured that remote user inputs are not requested from Runner itself
- NATS requests to sessions are now logged in the Runner log
1.13.0
Added support for ComponentSettingsLayoutAttribute Added support for auto generating Rows in DataGrids
- Added IgnoreBrowsableAttribute support to GetSettings and GetComponentSettings
- Added OPENTAP_RUNNER_SESSION_ID environment variable to session process
- Added ItemTypeDisplayName to the DTO for a ComponentSettingsList
- Fixed preserving repositories in default image
- Added logic to allow constructing component settings list elements from non plugin types
- Fixed Name property handling for ComponentSettingsListItem without a setter
- Correctly update other properties that might have changed due to a property change with side effects
- Fixed a null reference timing exception when removing a Test Plan Parameter
- Use typedata name instead of dotnet type name for ValueType
- Fixed issue where nested data grid changes were not applied
- Bumped NATS Server to 2.10.25
1.12.0
Added Runner Shutdown endpoint and CLI command Added support for streaming artifacts over NATS
- Added Test Plan Settings as Metadata to TestPlan when saving to Repository
- Added TestPlan/PathId run parameter
- Fixed datagrid control index bug
- Fixed User Inputs on Test Plan load
- Fixed removal of old config files and JetStream storage when unregistering/re-registering a Runner
- Fixed duplicate capability entries bug
- Fixed race condition between SetImage and StartSession with an Image
- Resolve images using only package cache first and fall back to remote Repositories
- Output the failed Session start logs
1.11.3
- Improvements to load packages from local cache if possible
- Skip user input interaction when loading a test plan
- Use property names instead of indexes when modifying a data grid
1.11.2
- Fixed use of correct stream name for logs
1.11.1
- Unregistering a runner now also always removes default settings and image
- Signal that a metric measurement is disabled by publishing a null data message
- Toggle between fast and reliable data transmission based on available storage
- Fixed serialization of non-integer result parameter enum types
1.11.0
Added configuration options for publishing runs, results, and logs
- NATS leaf node server is now restarted when detecting a permission violation
- Support of PathID in Cloud Drive
- Changed default metric lifetime to 1 hour
- Metrics are published on simpler subject
- Return the correct metrics configuration
- Fixes to concurrency in Runner metrics in heartbeat
- Replaced illegal characters from NATS subject where the subject is dynamically generated
- Fixed issue with "untitled" test plan name
- Wait for inactive sessions to shutdown when doing a graceful shutdown of the runner
- Publish MetadataUpdated event when a session's metadata was updated
1.10.2
- SSDF Compliance from a CI build perspective
1.10.1
Added support for Offline Runs using JetStream Added support for Metrics
- Added "SetSession" endpoint on Runner to set Metadata on Session object
- Added runner settings to modify Runs and Metric streams
- Upgraded NATS Server to version 2.10.18
- Made SessionDetails request more tolerant of Settings load error
- Improved ComponentSettings AddItem error message
- Validated arguments to help user avoid registering with http instead of https
- Fixed bug that prevented successful Runner shutdown
- Cleanup in Runner if a session process does not exist anymore
- Upgraded dependencies and OpenTAP to ensure SSDF compliance
1.9.2
- Fixed KS8500 Built-in runner register bug
1.9.1
- Set Docker health check URL to localhost IP
- Chunk log messages to chunks of 50 to avoid 'maximum payload exceeded' error
- Changed to use PATCH metadata
- Prevent error if a specified CloudDrive file already exists on disk
1.9.0
Provide ping metrics and migrate to .NET 8
- Migrated Runner Docker image to .NET 8
- Added settings output in logs
- Added
TestStepNametoOnTestStepRun - Changed strategy for providing UTC timestamps
- Implemented ping metrics (round trip time to KS8500)
- Unbrowsable steps are now shown as available child step types in
GetStepTypes - Fixed
PlanRunLogsDTO bug - Included milliseconds in datetime parameters
- Fixed
HistoryLogstimestamp and added integration tests - Fixed wrong log count in live logs
- Canceled
HandshakeCancellationTokenwhen interrupting Runner process - Fixed race condition bug in
GetStatusendpoint
1.8.0
Implemented support for storing TestPlans on the Runner
Added granular events to OnTestPlanRun
Added TestPlanRunLogs
- Added
DeleteOnExpirationandTimeSpanto UserToken generation - When
SyncLogsManagerinitializes, previous logs are added - Updated documentation with default endpoints
- Removed hardcoded DEV URL to fix unregister
- Improved unregister flow
- Added verbose logs from
LeafNode - Fixed error when removing a parameter
- Fixed bug where "Cancel" would still remove configuration files when unregistering
- Fixed bug where tokens are not updated in OpenTAP
- Added granular publishing of test plan run data
- Ensured
Configdirectory exists when re-registering - Added
TypeCodeto Parameter DTO
1.7.1
- Fixed Runner unregister issue
1.7.0
- Added support for packaging Python settings types
- Added
RepositoryPackageReferencenormalization - Bumped Runner Service and NATS Server dependencies
- Added more messages to the connection status
1.6.3 (Backport)
- Fixed Runner unregister issue
1.6.2
- Added workaround for ReadOnly
TestStepsnot having ReadOnly properties
1.6.1
- Fixed issue with files being used for image resolution
1.6.0
- Added
RunnerMockerprogram - Added
PluginTypeSelectorcontrol - Fixed issue where not started sessions would shut down due to heartbeat timeout
- Filtered
ContextMenuonIconAnnotationto avoid unwanted entries - Added JSON accept header to all repository communication
- Consider watchdog inactive if
TerminationTimeoutis set to under 1, by always returning 0 inInactiveSeconds - Added
ItemTypetoComponentSettingsListDTO
1.5.5 (Backport)
- Fixed Python dependencies issue
1.5.4 (Backport)
- Fixed issue with files being used for image resolution
1.5.3
- Fixed heartbeats
1.5.2
- Threw exception if UserToken can't be created
- Added context to exception message
- Added image override repositories to resolver
1.5.1
- Fixed "downgrade to same version" bug in
UpdateRunnerrequest
1.5.0
Added support for uploading Artifacts to KS8500 Added support for starting default session with an image overriding packages
- Various bug fixes, refactoring, and performance optimizations
1.4.3
- Fixed error where
NatsCredentialswas overwritten with null, breaking the KS8500 built-in runners auto-register - Always published lifetime started event
1.4.2
- Added NATS credentials rotation on handshake and removed re-register on startup of Runner
- Drained connection before close to ensure
StoppedLifeTimeEventis sent
1.4.1
- Fixed duplicate NATS headers issue
- Improved performance of
GetSettings - Made auto-update improvements and removed Runner restart after update
- Minor UX improvements
- Switched to always use
UserIdasStartedByinformation
1.4.0
Added local WebSocket listening on the NATS leaf node server Supported storage of base image, default settings, and enabled session creation based on TestPlan reference
- Added ability to re-register
- Set Cloud Drive Realm based on handshake
- Added system account to query server name
- Fixed issue with
DirectoryPathControl - Fixed bug with installer making the Runner stay in Registered mode
- Stopped and started the service in installer
- Added more descriptive error to failed registration
- License exception on adding
ComponentSettingsnow produces an error log - Fixed the
FailedToStartstate error when running twice - Implemented
ResolveImageDryRun - Ensured
TestPlanChangedEventArgsis emitted always when TestPlan might have changed - Modified OpenTAP log messages format for better readability in Loki
- Enhanced Docker file permissions
- Added a default value for Capabilities metadata
- Refactored Session Watchdog check
1.3.1
- Fixed
DirectoryPathControlserialization bug
1.3.0
- Added
--serverargument to specify direct NATS URL - Added heartbeat support on MacOS
- Button presses no longer block UI
- Runner asks to exchange JWT for Repo Token for sessions
- Bumped OpenTAP dependency to 9.21.1 to support User Tokens
- Added
DirectoryPathControl
1.2.3
- Added
TestPlanJsonas run parameter - Created a
FileWatcherto shut down runner at register/unregister
1.2.2
- Added default run parameters
1.2.1
- Added default parameters to
RunTestPlanto support local Runner TestPlan runs saved to KS8500
1.2.0
Added support for remotely updating the Runner installation Persisted Runner system logs (Serilog) to disk at the same location as OpenTAP logs
- Corrected Session
TestPlanRunIdvalue - Increased session startup timeout to 3 minutes
- Added "TestPlanName" to Session object
- Stopped publishing static metrics
- Fixed parameterized Dialog verdict bug
- Summed CPU & Memory usage of Runner, NATS, and Sessions
- Added safeguards to parse partially corrupt logs
1.1.0
Implemented Runner live TestPlan execution updates Supported Hybrid (online/offline) mode
- Supported
DirectoryPathAttributeby introducing aDirectoryPathControl - Changed logs to have UTC tick timestamps
- Changed parameter DateTime types to UTC
- Various bug fixes and improvements
Contact
OpenTAP Runner Support: Official support channel
Lead developer: Dennis Rasmussen
OpenTAP Manager: Janus Faaborg