Skip to content

Commit 17c8367

Browse files
mtinticlaude
andauthored
Add Documentation/MacTestEnv/ — Docker-based dev/test environment for macOS (#2334)
Bootstraps a working Rdmp.Core.Tests environment on macOS (Apple Silicon or Intel) using Docker. The DB-backed tests need a real SQL Server instance; this folder spins one up in a container, creates the four platform databases the tests expect, and points the test runner at it. Contents: README.md Full setup walkthrough + 'Building a Windows binary from Mac' section (cross-compile recipe + first-run notes). Pairs with the companion KeywordHelp line-endings fix on this branch. docker-compose.yml SQL Server 2022 Developer container, linux/amd64 under Rosetta on Apple Silicon, mssql-data volume for persistence between sessions. setup.sh One-shot bootstrap: starts the container, builds the rdmp CLI, creates TEST_* platform databases, installs TestDatabases.txt. teardown.sh Stops the container, restores Tests.Common/TestDatabases.txt. --purge removes the data volume too. run-tests.sh Thin wrapper around 'dotnet test' that adds the NU1902/NU1903/NU1904 warnings-as-errors workaround Rdmp.Core.csproj currently needs. TestDatabases.txt Mac-flavoured config that setup.sh copies into Tests.Common/; uses localhost,1433 + sa instead of (localdb)\\MSSQLLocalDB + integrated security. Folder name matches the PascalCase convention used by sibling docs (Catalogues, CodeTutorials, DataExtractions, LoadModules, ...). Status: verified end-to-end on darwin/arm64, .NET SDK 10.0.107, Docker 28.x. All 102 CohortCreation tests pass in ~3 minutes under amd64 emulation. Purely additive — no edits to upstream project files. The warnings-as-errors workaround sidesteps a project-level NoWarn override in Rdmp.Core.csproj (see README "Known issues") without touching it. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cd5f6ff commit 17c8367

6 files changed

Lines changed: 528 additions & 0 deletions

File tree

Documentation/MacTestEnv/README.md

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
# Mac test environment for RDMP
2+
3+
This folder bootstraps a working `Rdmp.Core.Tests` environment on macOS
4+
(Apple Silicon or Intel) using Docker. The DB-backed tests (~143 fixtures)
5+
need a real SQL Server instance; this folder spins one up in a container,
6+
creates the four platform databases the tests expect, and points the test
7+
runner at it.
8+
9+
> **Status:** verified end-to-end on `darwin/arm64`, .NET SDK 10.0.107,
10+
> Docker 28.x. All 102 `CohortCreation` tests pass.
11+
12+
---
13+
14+
## TL;DR
15+
16+
```bash
17+
# from the repo root, one-time setup
18+
bash Documentation/MacTestEnv/setup.sh
19+
20+
# run the cohort-creation slice (the slice this folder was created to support)
21+
bash Documentation/MacTestEnv/run-tests.sh
22+
23+
# tear it down at the end of the day
24+
bash Documentation/MacTestEnv/teardown.sh # keeps the DB volume
25+
bash Documentation/MacTestEnv/teardown.sh --purge # wipes the DB volume too
26+
```
27+
28+
---
29+
30+
## What's in this folder
31+
32+
| File | Purpose |
33+
|---|---|
34+
| `README.md` | This file. |
35+
| `docker-compose.yml` | Declarative SQL Server 2022 container definition. |
36+
| `setup.sh` | One-shot bootstrap: starts the container, builds the `rdmp` CLI, creates `TEST_*` databases, installs `TestDatabases.txt`. |
37+
| `teardown.sh` | Stops the container and restores `Tests.Common/TestDatabases.txt`. |
38+
| `run-tests.sh` | Thin wrapper around `dotnet test` that adds the warnings-as-errors workaround. |
39+
| `TestDatabases.txt` | The Mac-flavoured config that `setup.sh` copies into `Tests.Common/`. |
40+
41+
`setup.sh` overwrites `Tests.Common/TestDatabases.txt` in your working
42+
tree. `teardown.sh` runs `git restore` on it to bring the repo back to a
43+
clean state — **do not commit the modified file**.
44+
45+
---
46+
47+
## Prerequisites
48+
49+
| Tool | How to install on macOS |
50+
|---|---|
51+
| Docker Desktop (or Colima) running, ≥ 28.x | <https://docs.docker.com/desktop/install/mac-install/> |
52+
| .NET SDK 10.x | `brew install dotnet` |
53+
| Bash & standard `coreutils` | shipped with macOS |
54+
55+
Apple Silicon users: Docker Desktop must have **Rosetta** enabled
56+
(Settings → General → "Use Rosetta for x86_64/amd64 emulation").
57+
The SQL Server image is x86_64-only and runs under Rosetta.
58+
59+
Verify before continuing:
60+
61+
```bash
62+
dotnet --version # 10.x
63+
docker version # daemon must respond
64+
uname -m # arm64 (Apple Silicon) or x86_64 (Intel)
65+
```
66+
67+
---
68+
69+
## Step-by-step (what `setup.sh` does, manually)
70+
71+
If you want to understand or audit every step, this is what the script
72+
does.
73+
74+
### 1. Start SQL Server 2022 in Docker
75+
76+
```bash
77+
docker compose -f Documentation/MacTestEnv/docker-compose.yml up -d
78+
```
79+
80+
Equivalent direct command:
81+
82+
```bash
83+
docker run --platform linux/amd64 \
84+
--name rdmp-mssql \
85+
-e "ACCEPT_EULA=Y" \
86+
-e "MSSQL_SA_PASSWORD=YourStrong!Passw0rd" \
87+
-e "MSSQL_PID=Developer" \
88+
-p 1433:1433 \
89+
-v mssql-data:/var/opt/mssql \
90+
-d mcr.microsoft.com/mssql/server:2022-latest
91+
```
92+
93+
Key choices:
94+
* `--platform linux/amd64` — required on Apple Silicon (no native arm64 image).
95+
* `MSSQL_PID=Developer` — Developer edition is free and full-featured for testing.
96+
* The password **must** be ≥ 8 chars including upper, lower, digit, symbol —
97+
SQL Server refuses to start otherwise.
98+
* `mssql-data` volume persists the databases across `stop`/`start` cycles.
99+
100+
### 2. Wait for the engine to accept logins
101+
102+
Under Rosetta emulation, SQL Server takes ~60–90 s on first boot:
103+
104+
```bash
105+
until docker exec rdmp-mssql \
106+
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -C \
107+
-Q "SELECT 1" >/dev/null 2>&1; do
108+
echo "waiting..."; sleep 5
109+
done
110+
```
111+
112+
### 3. Build the `rdmp` CLI
113+
114+
```bash
115+
dotnet build Tools/rdmp/rdmp.csproj -c Release \
116+
-p:WarningsNotAsErrors='"NU1902;NU1903;NU1904"'
117+
```
118+
119+
The `WarningsNotAsErrors` flag is a workaround — see "Known issues" below.
120+
121+
### 4. Create the four platform databases
122+
123+
```bash
124+
dotnet Tools/rdmp/bin/Release/net10.0/rdmp.dll install \
125+
"localhost,1433" TEST_ -u sa -p 'YourStrong!Passw0rd' -d
126+
```
127+
128+
This creates: `TEST_Catalogue`, `TEST_DataExport`, `TEST_DQE`,
129+
`TEST_Logging`. The `-d` flag drops them first if they already exist (so
130+
this command is idempotent). Verify with:
131+
132+
```bash
133+
docker exec rdmp-mssql /opt/mssql-tools18/bin/sqlcmd \
134+
-S localhost -U sa -P 'YourStrong!Passw0rd' -C \
135+
-Q "SELECT name FROM sys.databases WHERE name LIKE 'TEST_%' ORDER BY name"
136+
```
137+
138+
### 5. Point the test runner at the container
139+
140+
Copy this folder's `TestDatabases.txt` into `Tests.Common/`:
141+
142+
```bash
143+
cp Documentation/MacTestEnv/TestDatabases.txt Tests.Common/TestDatabases.txt
144+
```
145+
146+
The replacement differs from the committed default in three lines:
147+
148+
```diff
149+
- ServerName: (localdb)\MSSQLLocalDB
150+
+ ServerName: localhost,1433
151+
+ Username: sa
152+
+ Password: YourStrong!Passw0rd
153+
- MySql: server=127.0.0.1;Uid=root;Pwd=YourStrong!Passw0rd;AllowPublicKeyRetrieval=True
154+
+ #MySql: server=127.0.0.1;Uid=root;Pwd=YourStrong!Passw0rd;AllowPublicKeyRetrieval=True
155+
```
156+
157+
* `(localdb)\MSSQLLocalDB` only exists on Windows.
158+
* SA credentials replace integrated security (which the container doesn't support).
159+
* MySQL is commented out because we don't run a MySQL container here.
160+
If a fixture is annotated with `[RequiresMySql]` or similar it'll be
161+
skipped rather than fail.
162+
163+
### 6. Run the tests
164+
165+
```bash
166+
dotnet test Rdmp.Core.Tests/Rdmp.Core.Tests.csproj \
167+
-p:WarningsNotAsErrors='"NU1902;NU1903;NU1904"' \
168+
--filter "FullyQualifiedName~CohortCreation"
169+
```
170+
171+
Expected: `Passed: 102, Failed: 0`, total ~3 minutes on Apple Silicon
172+
(amd64 emulation is the bottleneck — Intel Macs are noticeably faster).
173+
174+
---
175+
176+
## Daily commands
177+
178+
```bash
179+
# Container lifecycle
180+
docker compose -f Documentation/MacTestEnv/docker-compose.yml start
181+
docker compose -f Documentation/MacTestEnv/docker-compose.yml stop
182+
docker compose -f Documentation/MacTestEnv/docker-compose.yml logs -f mssql
183+
184+
# Reset the platform DBs without touching the container
185+
dotnet Tools/rdmp/bin/Release/net10.0/rdmp.dll install \
186+
"localhost,1433" TEST_ -u sa -p 'YourStrong!Passw0rd' -d
187+
188+
# Run a single test
189+
bash Documentation/MacTestEnv/run-tests.sh "FullyQualifiedName~SimpleCohortIdentificationTests.ContainerCreate"
190+
191+
# Run a fixture by class
192+
bash Documentation/MacTestEnv/run-tests.sh "FullyQualifiedName~CohortCompilerTests"
193+
194+
# Run everything (slow under emulation)
195+
bash Documentation/MacTestEnv/run-tests.sh ""
196+
197+
# Open a SQL prompt against the container
198+
docker exec -it rdmp-mssql /opt/mssql-tools18/bin/sqlcmd \
199+
-S localhost -U sa -P 'YourStrong!Passw0rd' -C
200+
```
201+
202+
---
203+
204+
## Building a Windows binary from Mac
205+
206+
`dotnet` is a cross-compiler. As long as the project declares
207+
`<EnableWindowsTargeting>true</EnableWindowsTargeting>` (the GUI project
208+
does) and the host has the .NET 10 SDK, you can produce a self-contained
209+
Windows `.exe` from macOS in one command. Useful when you want to
210+
validate a UI patch on Windows without setting up a full Windows dev
211+
environment, ship a one-off build to a colleague, or reproduce a
212+
release-style binary locally.
213+
214+
### GUI client (`ResearchDataManagementPlatform.exe`)
215+
216+
```bash
217+
dotnet publish Application/ResearchDataManagementPlatform/ResearchDataManagementPlatform.csproj \
218+
-c Release \
219+
-r win-x64 \
220+
--self-contained true \
221+
-p:WarningsNotAsErrors='"NU1902;NU1903;NU1904"' \
222+
-p:PublishReadyToRun=true
223+
```
224+
225+
Output: `Application/ResearchDataManagementPlatform/bin/Release/net10.0-windows/win-x64/publish/`
226+
227+
Expect 2-4 minutes the first time; subsequent publishes are faster
228+
thanks to the package cache. Zip the `publish/` folder and transfer
229+
to a Windows machine — no .NET install is needed on the target because
230+
the runtime is bundled (`--self-contained true`).
231+
232+
```bash
233+
( cd Application/ResearchDataManagementPlatform/bin/Release/net10.0-windows/win-x64/ && \
234+
zip -r ~/Downloads/rdmp-win-x64.zip publish/ )
235+
```
236+
237+
### CLI tool (`rdmp.exe`)
238+
239+
Same pattern, different project:
240+
241+
```bash
242+
dotnet publish Tools/rdmp/rdmp.csproj \
243+
-c Release \
244+
-r win-x64 \
245+
--self-contained true \
246+
-p:WarningsNotAsErrors='"NU1902;NU1903;NU1904"'
247+
```
248+
249+
Output: `Tools/rdmp/bin/Release/net10.0/win-x64/publish/`
250+
251+
### Companion fix on this branch
252+
253+
This branch also ships a one-line fix in
254+
`Rdmp.Core/Repositories/Managers/CommentStoreWithKeywords.cs` that
255+
makes the help-text parser tolerate any line-ending convention.
256+
Without it, a binary built on a non-Windows host crashes on startup
257+
with *"Malformed line in Resources.KeywordHelp"* because the embedded
258+
`KeywordHelp.txt` is checked out with LF endings on Linux/macOS while
259+
the parser splits on `Environment.NewLine` (i.e. `\r\n` at runtime on
260+
Windows). See the commit message for the full story.
261+
262+
### First launch on Windows
263+
264+
The .exe is unsigned, so Windows SmartScreen typically shows
265+
"Windows protected your PC" on first launch — click **More info**
266+
**Run anyway**. Files transferred from another machine may also be
267+
marked untrusted: right-click the .exe → **Properties** → tick
268+
**Unblock**. Both are expected for any locally-built binary, not a
269+
security issue with the build.
270+
271+
### Why this works
272+
273+
WinForms code compiles to MSIL exactly the same way on Mac as it would
274+
on Windows; what's not portable is the *runtime*, which relies on
275+
Win32 APIs to actually draw a window. Publishing with
276+
`--self-contained true -r win-x64` bundles the .NET runtime for x64
277+
Windows into the output folder so the target machine doesn't need a
278+
separate .NET install.
279+
280+
That's also why **the resulting binaries cannot be launched on macOS**
281+
they're Windows-targeted assemblies plus the Windows runtime. On a
282+
Mac, `dotnet build` succeeds but `dotnet test` against the WinForms
283+
project will fail to load the assemblies — Linux/macOS .NET can't host
284+
a `net10.0-windows` target. See "Known issues" below.
285+
286+
---
287+
288+
## Credentials
289+
290+
Stored in plaintext on purpose — this is a throwaway local dev environment,
291+
not production.
292+
293+
| Field | Value |
294+
|---|---|
295+
| Server | `localhost,1433` |
296+
| User | `sa` |
297+
| Password | `YourStrong!Passw0rd` |
298+
| Prefix | `TEST_` |
299+
300+
If you change the password, update it in **three** places:
301+
`docker-compose.yml`, `setup.sh`, and `TestDatabases.txt`.
302+
303+
---
304+
305+
## Known issues / workarounds
306+
307+
### Warnings-as-errors during build/test
308+
309+
`Rdmp.Core/Rdmp.Core.csproj` sets `TreatWarningsAsErrors=true` and *overrides*
310+
the inherited `<NoWarn>` from `Directory.Build.props` instead of appending to
311+
it (line 20: `<NoWarn>1701;1702;CS1591;SCS0018</NoWarn>` — drops
312+
`NU1902;NU1903;NU1904`). The result: package-vulnerability warnings get
313+
promoted back to errors and `dotnet restore`/`build`/`test` all fail.
314+
315+
We side-step it on the command line via:
316+
317+
```
318+
-p:WarningsNotAsErrors='"NU1902;NU1903;NU1904"'
319+
```
320+
321+
A clean fix in the .csproj would be:
322+
323+
```xml
324+
<NoWarn>$(NoWarn);1701;1702;CS1591;SCS0018</NoWarn>
325+
```
326+
327+
(That isn't done here so this folder stays purely additive — no edits to
328+
upstream project files.)
329+
330+
### Performance under amd64 emulation
331+
332+
On Apple Silicon, SQL Server runs through Rosetta and is noticeably slower
333+
than native. Expect ~3 minutes for the `CohortCreation` slice (102 tests)
334+
vs ~1 minute on native Windows. This is acceptable for develop-test loops
335+
but worth knowing before running the full suite.
336+
337+
### Tests that need MySQL/Oracle/PostgreSQL
338+
339+
Some fixtures iterate over multiple DBMS types via `[TestCase(DatabaseType.MySql)]`
340+
etc. With MySQL/Oracle/PostgreSQL absent from `TestDatabases.txt`, those
341+
cases either skip with `Assert.Inconclusive` or fail with a connection
342+
error depending on how they're written. If you need full coverage, add
343+
matching containers (e.g. a `mysql:8` service in `docker-compose.yml`) and
344+
re-enable the corresponding line in `TestDatabases.txt`.
345+
346+
### Tests that need Windows-specific features
347+
348+
`Rdmp.UI` is Windows Forms and can be *compiled* on macOS (`dotnet build`
349+
succeeds) but **cannot be launched** — Windows Forms has no Mac runtime.
350+
Use a Windows machine or Windows CI runner for any end-to-end UI testing.
351+
352+
---
353+
354+
## Troubleshooting
355+
356+
**`docker exec rdmp-mssql … sqlcmd: not found`**
357+
Older SQL Server images put the tool at `/opt/mssql-tools/bin/sqlcmd`
358+
(without `18`). The 2022 image we use has it at
359+
`/opt/mssql-tools18/bin/sqlcmd` and requires the `-C` flag (trust the
360+
self-signed cert).
361+
362+
**`Login failed for user 'sa'`**
363+
The password didn't meet complexity rules. Stop & remove the container,
364+
fix the password in `docker-compose.yml`, and re-run `setup.sh`.
365+
366+
**Tests fail with `Could not find file 'TestDatabases.txt'`**
367+
The file isn't being copied to the test output directory. Make sure
368+
`Tests.Common/Tests.Common.csproj` still has the `<Content Include="TestDatabases.txt">`
369+
item with `<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>`.
370+
371+
**Container starts but `setup.sh` hangs at "Waiting for SQL Server"**
372+
Check `docker logs rdmp-mssql`. Common causes:
373+
* Insufficient memory (Docker Desktop default 2 GB; bump to ≥ 4 GB).
374+
* Password rejected (see above).
375+
* Rosetta disabled on Apple Silicon.

0 commit comments

Comments
 (0)