d2d4db492b
maxParallelThreads was unset everywhere and only 3 of ~20 test projects carried an xunit.runner.json at all, so xunit ran Environment.ProcessorCount collections at once (14 on the dev box) in every suite. That is the general load multiplier behind the row-82/91/95 flake family. Policy: per-suite caps, NOT a global one. Pure unit suites stay uncapped for speed; a suite gets a config only when a test CLASS costs a real external or OS-level resource, so ProcessorCount of them at once is a resource question rather than a CPU question. Four suites qualify, all capped at maxParallelThreads: 4. - ConfigurationDatabase.Tests, AuditLog.Tests, SiteCallAudit.Tests — verified as the three that actually take MsSqlMigrationFixture (the latter two by project reference). Every MSSQL-backed test class means a CREATE DATABASE plus the ~100-migration chain against the one shared dev SQL Server. 4 is chosen to MATCH the default slot count of row 82's cross-process sp_getapplock migrate semaphore, so the in-process and cross-process limits agree rather than fighting. - Host.Tests — classes boot real ASP.NET Core hosts and real ActorSystems; Akka.Remote binds real sockets, Kestrel binds real ports, gRPC dials them. Threads are capped, collections are NOT serialized. IntegrationTests (parallelizeAssembly/parallelizeTestCollections false) and CentralUI.PlaywrightTests (maxParallelThreads 1) already carry stricter pre-existing settings and are left alone — the Playwright suite drives a browser against a live rig, where shared UI state, not CPU, is the constraint. Each capped csproj gains <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />; the SDK does not copy it automatically and without it the file is silently ignored. Verified by moving the copied file out of bin/ to take an uncapped baseline: ConfigurationDatabase.Tests ran at 216 % CPU uncapped vs 145 % capped, so the cap demonstrably takes effect. Cost is close to nil — these suites are latency-bound on an external resource, not CPU-bound (14-25 % utilisation for the two slowest). Standalone, uncapped -> capped: AuditLog 383 tests 1m52s -> 1m51s; Host 520 tests 4m05s -> 4m08s; ConfigurationDatabase 396 tests 10s -> 12s; SiteCallAudit 66 tests 8s -> 8s. All green. docs/test_infra/test_infra.md gains a Test Parallelism Policy section: the two axes (in-process collection threads = this cap; cross-process solution-level dotnet test = unbounded in general, bounded on the DB-migration path by the sp_getapplock semaphore), the per-suite table with reasons, the rules (cap threads rather than serialize; a cap is not a substitute for test isolation; the Content copy is required), and the default for new projects (no config until the project acquires a per-class external resource). test_infra_db.md cross-links it from the semaphore bullet.
200 lines
8.7 KiB
Markdown
200 lines
8.7 KiB
Markdown
# Test Infrastructure: MS SQL 2022 Database
|
|
|
|
## Overview
|
|
|
|
The test database uses Microsoft SQL Server 2022 Developer Edition running in Docker. It provides two empty databases for ScadaBridge — schema creation is handled by EF Core migrations at application startup (dev mode).
|
|
|
|
## Image & Ports
|
|
|
|
- **Image**: `mcr.microsoft.com/mssql/server:2022-latest`
|
|
- **Port**: 1433
|
|
- **Edition**: Developer (free, full-featured)
|
|
|
|
## Credentials
|
|
|
|
| Account | Username | Password | Purpose |
|
|
|---------|----------|----------|---------|
|
|
| SA | `sa` | `ScadaBridge_Dev1#` | Server admin (setup only) |
|
|
| App | `scadabridge_app` | `ScadaBridge_Dev1#` | Application login (db_owner on both databases) |
|
|
|
|
## Databases
|
|
|
|
| Database | Purpose |
|
|
|----------|---------|
|
|
| `ScadaBridgeConfig` | Configuration Database component — templates, deployments, users, audit log |
|
|
| `ScadaBridgeMachineData` | Machine/operational data storage |
|
|
|
|
Both databases are created by `infra/mssql/setup.sql`. EF Core migrations populate the `ScadaBridgeConfig` schema. The `ScadaBridgeMachineData` database is seeded with sample tables and stored procedures by `infra/mssql/machinedata_seed.sql`.
|
|
|
|
### Ephemeral per-test-class databases
|
|
|
|
`MsSqlMigrationFixture` (`tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Migrations/`) creates a
|
|
throwaway `ScadaBridgeAuditMigTest_<guid>` database per test class, applies the full EF migration chain
|
|
to it, and drops it on teardown. Both `ConfigurationDatabase.Tests` and `AuditLog.Tests` use it — the
|
|
latter by project reference, so there is one fixture implementation, not a copy. Seeing a handful of
|
|
these databases come and go while tests run is normal; seeing them survive a run is not (see below).
|
|
|
|
Three behaviours are worth knowing when you look at this server during a test run:
|
|
|
|
- **Concurrent create+migrate is throttled machine-wide.** Fixtures take a slot from a counting
|
|
semaphore built on `sp_getapplock` in `master` (resources `ScadaBridgeTestMigrate/0..N-1`) and hold it
|
|
across `CREATE DATABASE` + the migration chain. The default is 4 slots; override with
|
|
`SCADABRIDGE_MSSQL_TEST_MIGRATE_SLOTS`. An xunit collection attribute cannot do this job — a
|
|
solution-level `dotnet test` runs several MSSQL-backed test *processes* at once, so the throttle has
|
|
to live on the shared server. Without it, dozens of simultaneous fresh-database migrations saturated
|
|
the container and individual DDL statements blew past EF's default 30-second command timeout,
|
|
failing whole test classes at fixture construction (deferred-work register row 82). The
|
|
*in-process* half of the same problem is bounded separately, by a `maxParallelThreads: 4` cap in
|
|
each MSSQL-backed suite's `xunit.runner.json` — deliberately the same number, so the two limits
|
|
agree. See [test_infra.md → Test Parallelism Policy](test_infra.md#test-parallelism-policy).
|
|
- **Migration commands use a 300s timeout**, and CREATE/DROP DATABASE use 120s. Under a bounded queue
|
|
the server is busy but progressing, so the correct response to a slow statement is to wait.
|
|
- **Strays are swept.** Each test process drops, once at start and best-effort, any
|
|
`ScadaBridgeAuditMigTest_*` database older than 2 hours, so a run killed before teardown does not
|
|
leave databases accumulating. To clear them by hand:
|
|
|
|
```bash
|
|
docker exec -i scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \
|
|
-S localhost -U sa -P 'ScadaBridge_Dev1#' -C \
|
|
-Q "SELECT name, create_date FROM sys.databases WHERE name LIKE 'ScadaBridgeAuditMigTest_%'"
|
|
```
|
|
|
|
Overriding the server the fixture targets (CI, a non-default port) is done with
|
|
`SCADABRIDGE_MSSQL_TEST_CONN`. When no server answers, the fixture reports `Available = false` and the
|
|
tests report as **Skipped**, not passed.
|
|
|
|
## Data Persistence
|
|
|
|
SQL data is stored in the named Docker volume `scadabridge-mssql-data`. Data survives container restarts and `docker compose down`. To reset the database completely:
|
|
|
|
```bash
|
|
docker compose down -v
|
|
docker compose up -d
|
|
# Re-run setup.sql after the container starts
|
|
```
|
|
|
|
## First-Time Setup
|
|
|
|
After the first `docker compose up -d`, run the setup script:
|
|
|
|
```bash
|
|
docker exec -i scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \
|
|
-S localhost -U sa -P 'ScadaBridge_Dev1#' -C \
|
|
-i /docker-entrypoint-initdb.d/setup.sql
|
|
```
|
|
|
|
This creates the databases and the `scadabridge_app` login. Then seed the Machine Data database with sample tables, stored procedures, and data:
|
|
|
|
```bash
|
|
docker exec -i scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \
|
|
-S localhost -U sa -P 'ScadaBridge_Dev1#' -C \
|
|
-i /docker-entrypoint-initdb.d/machinedata_seed.sql
|
|
```
|
|
|
|
You only need to run these once (or again after deleting the volume).
|
|
|
|
## Connection Strings
|
|
|
|
For `appsettings.Development.json`:
|
|
|
|
```
|
|
Server=localhost,1433;Database=ScadaBridgeConfig;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true
|
|
```
|
|
|
|
```
|
|
Server=localhost,1433;Database=ScadaBridgeMachineData;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true
|
|
```
|
|
|
|
## Verification
|
|
|
|
1. Check the container is running:
|
|
|
|
```bash
|
|
docker ps --filter name=scadabridge-mssql
|
|
```
|
|
|
|
2. Query using `sqlcmd` inside the container:
|
|
|
|
```bash
|
|
docker exec -it scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \
|
|
-S localhost -U sa -P 'ScadaBridge_Dev1#' -C \
|
|
-Q "SELECT name FROM sys.databases"
|
|
```
|
|
|
|
3. Verify the app login:
|
|
|
|
```bash
|
|
docker exec -it scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \
|
|
-S localhost -U scadabridge_app -P 'ScadaBridge_Dev1#' -C \
|
|
-d ScadaBridgeConfig \
|
|
-Q "SELECT DB_NAME()"
|
|
```
|
|
|
|
## CLI Tool
|
|
|
|
The `infra/tools/mssql_tool.py` script provides a convenient CLI for interacting with the SQL Server.
|
|
|
|
**Install dependencies** (one-time):
|
|
```bash
|
|
pip install -r infra/tools/requirements.txt
|
|
```
|
|
|
|
**Commands**:
|
|
|
|
```bash
|
|
# Check connectivity and verify expected databases exist
|
|
python infra/tools/mssql_tool.py check
|
|
|
|
# Run the first-time setup script (uses autocommit mode for CREATE DATABASE)
|
|
python infra/tools/mssql_tool.py setup --script infra/mssql/setup.sql
|
|
|
|
# List tables in a database
|
|
python infra/tools/mssql_tool.py tables --database ScadaBridgeConfig
|
|
|
|
# Run an ad-hoc query
|
|
python infra/tools/mssql_tool.py query --database ScadaBridgeConfig --sql "SELECT name FROM sys.tables"
|
|
```
|
|
|
|
Use `--host`, `--port`, `--user`, `--password` to override defaults (localhost:1433, sa, ScadaBridge_Dev1#). Run with `--help` for full usage.
|
|
|
|
## Machine Data Tables & Stored Procedures
|
|
|
|
The `machinedata_seed.sql` script creates the following in `ScadaBridgeMachineData`:
|
|
|
|
**Tables**:
|
|
|
|
| Table | Description | Sample Data |
|
|
|-------|-------------|-------------|
|
|
| `TagHistory` | Time-series tag values from OPC UA / custom protocols | Pressure, flow, level, temperature, speed readings for SiteA/SiteB |
|
|
| `ProductionCounts` | Shift/line production totals (good, reject, efficiency) | 3 days of 2-shift data across 2 sites |
|
|
| `EquipmentEvents` | State changes, faults, maintenance, alarm events | Pump faults, belt inspections, batch starts |
|
|
| `BatchRecords` | Production batch tracking (start, complete, abort) | 5 batches including one in-progress |
|
|
| `AlarmHistory` | Historical alarm activations, acks, and clears | Active, acknowledged, and cleared alarms |
|
|
|
|
**Stored Procedures**:
|
|
|
|
| Procedure | Description |
|
|
|-----------|-------------|
|
|
| `usp_GetTagHistory` | Get tag values for a tag path within a date range |
|
|
| `usp_GetProductionSummary` | Aggregate production by line over a date range |
|
|
| `usp_InsertBatchRecord` | Insert a new batch (for `CachedWrite` testing) |
|
|
| `usp_CompleteBatch` | Complete or abort a batch |
|
|
| `usp_GetEquipmentEvents` | Get recent equipment events with optional filters |
|
|
| `usp_GetActiveAlarms` | Get active/acknowledged alarms by severity |
|
|
|
|
## Relevance to ScadaBridge Components
|
|
|
|
- **Configuration Database** — primary consumer; EF Core context targets `ScadaBridgeConfig`.
|
|
- **Deployment Manager** — reads/writes deployment records in `ScadaBridgeConfig`.
|
|
- **Template Engine** — reads/writes template definitions in `ScadaBridgeConfig`.
|
|
- **Security & Auth** — user/role data stored in `ScadaBridgeConfig`.
|
|
- **External System Gateway** — scripts use `Database.Connection("machineDataConnection")` to query `ScadaBridgeMachineData` tables and stored procedures.
|
|
- **Site Runtime** — scripts call stored procedures via `Database.Connection()` and `Database.CachedWrite()` for batch recording and data queries.
|
|
- **Inbound API** — methods can query machine data via named database connections.
|
|
|
|
## Notes
|
|
|
|
- The `sa` password must meet SQL Server complexity requirements (uppercase, lowercase, digit, special character, 8+ characters).
|
|
- If the container fails to start, check Docker has at least 2GB RAM allocated (SQL Server minimum requirement).
|
|
- The setup script is idempotent — safe to run multiple times.
|