---
name: dataminer-qaops-simulators
description: 'Device simulators for DataMiner QAOps integration tests: SNMP (Skyline Device Simulator / QADeviceSimulator), raw TCP (serial, HTTP, WebSocket), process-based simulators packaged with .dmtest, and large or per-run simulator assets supplied as QAOps supplementary files. Use when tests need real device communication instead of just checking element state. Covers QADeviceSimulator CLI, TcpDeviceSimulator in-test class, WebSocketDeviceSimulator console exe, SnmpSimulatorSession, simulator lifecycle/cleanup, baseline-controlled test design, loopback limitations on QAOps DaaS, and connector fixture patterns.'
argument-hint: 'Describe what device communication needs to be simulated: e.g. "simulate an SNMP device for my integration test", "create a WebSocket simulator for testing element creation", "add a TCP serial responder for a QAOps data-flow test"'
license: LicenseRef-Skyline-Agent-Marketplace
metadata:
updated: 2026-10-02
version: 1.4
---
## Changelog
| Version | Date | Changes |
|---------|------|---------|
| 1.4 | 2026-10-02 | Corrected the QADeviceSimulator launch contract: pass the bare file name of an XML in `C:\QASNMPSimulations` (a full path silently starts zero simulations), `/d` only disables logging, SNMP v1/v2c/v3 are supported, and `SnmpSimulatorSession` now polls for the UDP bind instead of sleeping. Added the raw-wire probe and DaaS elevation notes. Verified by decompiling QADeviceSimulator and on QAOps DaaS. |
| 1.3 | 2026-08-07 | Added supplementary-file delivery for simulator assets: pass variable/large simulation XML, data, or prebuilt simulator executables with `--supplementary-file`, then resolve them through machine-level `QAOPS_SUPPLEMENTARY_FILES` in C# or PowerShell. Kept packaging as the default for small stable helpers that belong to the test artifact. |
| 1.2 | 2026-06-26 | Updated the SNMP and WebSocket usage-example tests to also carry the baseline `[TestCategory("IntegrationTest")]` (additive with the feature-specific `[TestCategory("IDmsElementCreation")]`), matching the mandatory-category rule in `dataminer-qaops-integration-testing` → `references/authoring-test-project.md`. |
| 1.1 | 2026-06-26 | Clarified at the top of "QAOps DaaS Loopback Limitations" that the DaaS target is a **Windows** DataMiner and the loopback/elevation limits are networking constraints, **not** a Linux host — Windows-only connector behavior is available; do not generalize a loopback limit into "the target is Linux". |
| 1.0 | 2026-06-15 | Initial skill: SNMP/QADeviceSimulator, in-test TcpDeviceSimulator, packaged WebSocketDeviceSimulator, SnmpSimulatorSession, lifecycle/cleanup rules, QAOps DaaS loopback limitations, baseline-controlled test design, WebSocket URL format, connector fixture patterns. Derived from a full connector IDms element-creation integration test session. |
# Device Simulators for QAOps Integration Tests
Load this skill when an integration test needs an element to exchange real data with a simulated device, not just reach the Active state. Without a simulator, the only assertion is "element became Active" — with one, the test proves the creation logic stored a working port and the connector can actually poll or receive data.
## Quick Routing
| Need | Use |
|------|-----|
| Simulate an SNMP device with static OIDs | `SnmpSimulatorSession` wrapping the Skyline Device Simulator |
| Simulate a serial request/response device | `TcpDeviceSimulator.StartSerialResponder(...)` in-test |
| Simulate a push-based smart-serial device | `TcpDeviceSimulator.StartPushOnConnect(...)` or `TryPushToElement(...)` |
| Simulate an HTTP server for an HTTP connector | `TcpDeviceSimulator.StartHttpResponder(...)` in-test |
| Simulate a WebSocket server | **Packaged console exe** — not in-test (`TcpListener`-based, included in `.dmtest`) |
| Supply large or per-run simulator XML/data/executables | QAOps supplementary files; resolve machine-level `QAOPS_SUPPLEMENTARY_FILES` |
## Supplying Simulator Assets at Run Time
Use QAOps supplementary files when simulator inputs are large, change per run, or should not be baked
into the `.dmtest`. Examples include vendor simulation data, generated configuration, firmware images,
and prebuilt simulator executables.
Pass each asset with `--supplementary-file`. In C# test code, resolve it from the machine-level
environment variable:
```csharp
string? supplementaryFilesPath = Environment.GetEnvironmentVariable(
"QAOPS_SUPPLEMENTARY_FILES",
EnvironmentVariableTarget.Machine);
if (String.IsNullOrWhiteSpace(supplementaryFilesPath) || !Directory.Exists(supplementaryFilesPath))
{
Assert.Fail("This simulator requires QAOps supplementary files, but none are available.");
}
string simulationPath = Path.Combine(supplementaryFilesPath, "Simulations", "device.xml");
```
PowerShell setup/pipeline code uses the same machine-level value:
```powershell
$supplementaryFilesPath = [Environment]::GetEnvironmentVariable(
'QAOPS_SUPPLEMENTARY_FILES',
[EnvironmentVariableTarget]::Machine)
```
Relative upload paths preserve folders, so submit `.\Simulations\device.xml` when the test expects the
`Simulations` subdirectory. Absolute paths are stored by file name only.
Keep small, stable simulator helpers packaged in the `.dmtest` when they are part of the test's
versioned implementation. Supplementary files are better for independently changing runtime data and
do not require a Test Package rebuild when only the supplied file changes. Never use them for secrets.
See `dataminer-qaops/references/supplementary-files.md` for full upload, lifecycle, compatibility, and
security rules.
## The Skyline Device Simulator (QADeviceSimulator)
DataMiner ships a device simulator tool on every DMA since 10.1.5:
```text
C:\Skyline DataMiner\Tools\QADeviceSimulator\QADeviceSimulator.exe
```
It supports **SNMP (v1, v2c and v3) and HTTP** simulation. Serial/smart-serial/WebSocket are **not** supported — use `TcpDeviceSimulator` or a packaged console exe for those.
**Unattended CLI usage:**
```text
QADeviceSimulator.exe MySimulation.xml # one simulation
QADeviceSimulator.exe SimA.xml SimB.xml # several simulations in one process
```
- Pass the **bare file name** of an XML that is already in `C:\QASNMPSimulations` (created automatically on first start). A full or quoted path matches no simulation entry, so **zero simulations start and no UDP port is bound**, while the process stays alive.
- `/d` only disables logging; it is not a headless switch (the GUI always starts and auto-starts the named simulations). Omit it so `C:\QASNMPSimulations\Logging\Log.txt` stays available for diagnostics.
- The exe requires elevation (requireAdministrator manifest). On QAOps DaaS the test host is already elevated, so `runas` is silent; local runs show a UAC prompt. If elevation fails, the test must report `Assert.Inconclusive(...)` rather than failing.
- **Readiness:** a running process is not a health signal. Poll `IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners()` for the agent port (30 s budget) and fail fast, pointing at the simulator log. `SnmpSimulatorSession` below does this.
- **Diagnosis:** before creating any element, send a raw SNMP GET to the simulator port. A timeout means the simulator is not listening; an answer means look at the connector or element.
**Simulation file format:**
```xml
```
### `SnmpSimulatorSession` Pattern
A disposable wrapper that generates the XML, starts the process, and cleans up on Dispose:
```csharp
string failureReason;
SnmpSimulatorSession simulator = SnmpSimulatorSession.TryStart(
"IDmsTestSim_" + DateTime.UtcNow.Ticks,
udpPort,
new Dictionary
{
["1.3.6.1.2.1.1.1.0"] = "My Test Device",
["1.3.6.1.2.1.1.3.0"] = "12345",
},
out failureReason);
if (simulator == null)
{
Assert.Inconclusive($"SNMP simulator could not start: {failureReason}");
}
using (simulator)
{
// create element, assert PID polled sysDescr
}
```
Key rules for `SnmpSimulatorSession`:
- Always call `TryStart`, never construct directly — elevation failures return `null`.
- Always `Assert.Inconclusive(...)` when `TryStart` returns `null` — never `Assert.Fail`. The QAOps DaaS agent may not allow elevation.
- Always dispose (even on test failure) — leaked simulator processes hold UDP ports.
- Generate a unique simulation file name each time (e.g. append `DateTime.UtcNow.Ticks`) to avoid collisions between parallel runs.
- Reserve a free UDP port with a `UdpClient(new IPEndPoint(IPAddress.Loopback, 0))` probe, then close it before starting the simulator.
## In-Test `TcpDeviceSimulator`
A `TcpListener`-based TCP server that runs **inside the test process** on the QAOps DataMiner machine. No external process, no elevation, no URL ACL. Supports serial, HTTP, and push-on-connect patterns.
```csharp
///
/// Serial: responds with when the accumulated received bytes contain
/// . Pass a specific to bind a known port.
///
public static TcpDeviceSimulator StartSerialResponder(string expectedCommand, string reply, int port = 0);
/// HTTP: responds 200 OK with to any complete HTTP request.
public static TcpDeviceSimulator StartHttpResponder(string body);
/// Push: immediately sends on every accepted connection.
public static TcpDeviceSimulator StartPushOnConnect(byte[] payload);
/// Utility: reserves a free TCP port on loopback.
public static int GetFreeTcpPort();
///
/// Connects to 127.0.0.1: and pushes as a device.
/// Use for smart-serial elements in server mode (loopback polling address).
///
public static bool TryPushToElement(int port, byte[] payload, TimeSpan timeout, out string detail);
```
Always dispose `TcpDeviceSimulator` — leaked listeners hold ports:
```csharp
using (TcpDeviceSimulator simulator = TcpDeviceSimulator.StartSerialResponder("PING", "PONG"))
{
// create element pointing to 127.0.0.1:simulator.Port
// assert simulator.ReceivedText.Contains("PING")
}
```
### HTTP Simulator Body Gotcha
When using `StartHttpResponder` with an HTTP connector that deserializes the response with `XmlSerializer`, every XML attribute name must match the C# model's `[XmlAttributeAttribute]` **exactly** (case-sensitive, no hyphens unless the attribute specifies a name).
Example failure: DataMiner log shows `System.NullReferenceException at QAction.CheckTemperatureUnit(Server server)` — root cause was `temp-unit="C"` in the simulated body when the model expected `tempunit="C"` (no hyphen). Always inspect the fixture QAction's deserialization model before composing the simulator body.
## WebSocket Device Simulator
DataMiner WebSocket ports use WinHTTP upgrade: the element connects TO a server at the polling address/port/path. When the polling address is loopback, DataMiner enters **server mode** (listening) — but WebSocket ports remain **client mode** even on loopback: the element dials out.
This means a `TcpDeviceSimulator` class (which accepts inbound connections) works as a WebSocket server — but `HttpListener` does **not** work on QAOps DaaS because URL ACL registration requires elevation.
**Solution: package a standalone `net48` console exe** (`WebSocketDeviceSimulator.exe`) that uses raw `TcpListener` for the WebSocket handshake and frame exchange, and copy it into `tests.generated` alongside the integration tests.
### Packaging the Simulator Exe
Packaging is the default for a small stable helper that belongs to the tests. A large, prebuilt, or
per-run simulator executable may instead be supplied with `--supplementary-file` and started from the
machine-level `QAOPS_SUPPLEMENTARY_FILES` directory.
1. Create a `net48` console project (e.g. `IntegrationTests\Simulators\WebSocketDeviceSimulator\`).
2. Exclude it from the integration test project's SDK compilation:
```xml
```
3. Add a **build-order only** `ProjectReference` from `IntegrationTests` so the simulator always builds first:
```xml
```
4. Copy the simulator output into the IntegrationTests `bin` **before** the tests.generated harvesting step:
```xml
```
5. The existing `CopyBinToQaOpsHarvestingTestsGenerated` target will then copy `Simulators\WebSocketDeviceSimulator\` into `tests.generated` automatically.
### Starting the Simulator from MSTest
```csharp
internal sealed class WebSocketSimulatorProcess : IDisposable
{
public static WebSocketSimulatorProcess Start(int port, string expectedMessage, string replyMessage)
{
string simulatorPath = Path.Combine(AppContext.BaseDirectory, "Simulators",
"WebSocketDeviceSimulator", "WebSocketDeviceSimulator.exe");
if (!File.Exists(simulatorPath))
throw new FileNotFoundException("Simulator not packaged.", simulatorPath);
// ... start process, wait for "READY:" on stdout
}
public bool WaitForLine(Func predicate, TimeSpan timeout) { ... }
public void Dispose() { /* Kill + WaitForExit(5000) */ }
}
```
Always kill the simulator in test cleanup (even on failure):
```csharp
using (WebSocketSimulatorProcess simulator = WebSocketSimulatorProcess.Start(port, "PING", "PONG"))
{
// create WebSocket element, assert simulator saw handshake and PING, element PID has PONG
}
```
### WebSocket URL Format
DataMiner's WebSocket port (`CSLWebSocket`) parses the polling IP as a full URL:
- It strips `ws://` → `http://` and `wss://` → `https://` before passing to WinHTTP.
- A host-only string (e.g. `127.0.0.1`) with no scheme causes `The URL is invalid` in `HandleWebsocketConnectResult`.
**Always use a full `ws://...` URL in the polling address:**
```csharp
new WebSocketConnection(new Tcp("ws://127.0.0.1:" + port + "/ws", port))
```
The `Tcp(remoteHost, remotePort)` constructor puts `remoteHost` into `PollingIPAddress`; DataMiner uses that string as the URL when the connection family is WebSocket.
## QAOps DaaS Loopback Limitations
The QAOps DaaS target is a **Windows** DataMiner (see `dataminer-qaops` → "Platform: QAOps DataMiner Systems Run on Windows"). The limitations below are **networking/elevation** constraints of the provisioned environment — they are **not** signs of a Linux host, and they do not mean Windows-only connector behavior (WMI, registry, `.exe` tools) is unavailable. Do not generalize a loopback limitation into "the target is Linux".
Understanding what QAOps DaaS supports saves multiple wasted runs:
| Protocol | Loopback TCP on DaaS | Workaround |
|----------|---------------------|------------|
| Serial (IP) | ❌ Even known-good baseline elements cannot reach a local simulator | Baseline-controlled: Inconclusive when baseline fails too |
| Smart-serial (IP, server mode) | ❌ DataMiner listens but loopback connections from the same machine don't arrive | Same |
| HTTP | ✅ Works — `TcpDeviceSimulator.StartHttpResponder` | Direct use |
| SNMP (UDP) | ✅ Works — `SnmpSimulatorSession` | Direct use |
| WebSocket | ✅ Works — `WebSocketDeviceSimulator.exe` + `ws://127.0.0.1:port/path` | Packaged exe |
### Smart-Serial Loopback: Server Mode Explained
When the polling IP is loopback (127.0.0.1 or the DMA's own IP), DataMiner's smart-serial port enters **server mode**: it listens and waits for a remote device to connect to it. This means a `TcpDeviceSimulator` (which listens and accepts) would also be waiting; neither side dials out.
Use `TcpDeviceSimulator.TryPushToElement(port, payload, ...)` to play the role of the remote device:
```csharp
string pushDetail;
bool pushed = TcpDeviceSimulator.TryPushToElement(listenPort, frame, TimeSpan.FromSeconds(60), out pushDetail);
if (!environmentSupportsSmartSerialLoopback && !pushed)
Assert.Inconclusive("Baseline smart-serial element also cannot accept connections on this system.");
```
## Baseline-Controlled Data-Flow Test Design
When a data-flow test might be blocked by environment limitations (serial/smart-serial loopback), design it as a controlled experiment rather than an unconditional assertion:
1. **Baseline probe**: before testing the newly created element, run the same scenario against a known-good fixture element created with a raw `AddElementMessage` (pre-installed by the test package). This fixture uses the same protocol and the same simulator port.
2. **If the baseline fails**: `Assert.Inconclusive(...)` — the environment cannot do it; the new element cannot be blamed.
3. **If the baseline succeeds but the new element fails**: `Assert.Fail(...)` — the element creation logic produced a non-working port.
```csharp
// Baseline: known-good 'Manual Serial' element on port 9000
bool environmentSupportsIt = TryRunBaseline(baseline, simulator9000);
// Created element: our new one on a dynamic port
bool newElementWorks = TryRunTest(element, simulatorDynamic);
if (!environmentSupportsIt && !newElementWorks)
Assert.Inconclusive("Serial loopback not supported on this DaaS target.");
Assert.IsTrue(newElementWorks,
"Baseline works but new element does not — creation produced a non-working port.");
```
Additionally, always add a **port-shape equivalence gate** against the baseline: compare `SLNet MainPort` field-by-field (`ProtocolType`, `Type`, `PollingIPAddress`, `PollingIPPort`, `Number`, `IsSslTlsEnabled`). If the stored port matches the baseline exactly (only the port number differs), the creation logic is correct even if live exchange isn't supported:
```csharp
ElementPortInfo created = SLNetUtility.SendGetElementByNameMessage(comm, created.Name).MainPort;
ElementPortInfo expected = SLNetUtility.SendGetElementByNameMessage(comm, baselineName).MainPort;
Assert.AreEqual(expected.ProtocolType, created.ProtocolType, "ProtocolType mismatch");
Assert.AreEqual(expected.Type, created.Type, "Port Type mismatch");
Assert.AreEqual(expectedPollingPort, created.PollingIPPort, "Port number mismatch");
```
## Simulator Lifecycle Rules (Mandatory)
These apply to all simulator types regardless of which QAOps target is used:
1. **Always dispose/kill in `finally` or `using`** — even when the test fails. Leaked processes hold ports and break subsequent tests.
2. **Never leave simulator processes running after AssemblyCleanup** — QAOps provisions a fresh DaaS per run, but if the process persists through the cleanup phase it may prevent element deletion.
3. **Use dynamic ports by default** (`TcpListener` port 0, or `UdpClient` port 0) — fixed ports risk collisions when multiple test classes run.
4. **Log simulator output to the test's console** (`Console.WriteLine`) — it ends up in the TRX and QAOps shows it in the failure message, which is the only diagnostic channel available remotely.
5. **Include simulator state in every assertion message**: port number, connection count, and a truncated sample of received text.
## Reference Files
| Topic | File |
|-------|------|
| Canonical `TcpDeviceSimulator` implementation | `dataminer-qaops-simulators/references/tcp-device-simulator.md` |
| Canonical `SnmpSimulatorSession` implementation | `dataminer-qaops-simulators/references/snmp-simulator-session.md` |
| Canonical `WebSocketDeviceSimulator` console exe | `dataminer-qaops-simulators/references/websocket-simulator-exe.md` |
| `WebSocketSimulatorProcess` test wrapper | `dataminer-qaops-simulators/references/websocket-simulator-process.md` |
| Supplementary simulator assets and PowerShell/C# access | `dataminer-qaops/references/supplementary-files.md` |
## Related Skills
| Task | Skill |
|------|-------|
| Authoring MSTestV2 integration tests and packaging them | `dataminer-qaops-integration-testing` |
| Running `.dmtest` packages on a real QAOps DataMiner | `dataminer-qaops-test-runs` |
| QAOps background, token handling, IDs | `dataminer-qaops` |