Phase 14 complete — v0.4 Robustness milestone closed:
- HardwareTestHelper.GetIpOrSkip: Assert.Ignore when HEIDENHAIN/SIEMENS/MITSUBISHI_IP not set (ISS-007)
- Hardware_ConnectAsync_RealMachine [Category("Hardware")] added to all 3 integration test files
- LicenseFileTests: 4 tests covering empty, whitespace, valid base64, nonexistent path
- Validate_EmptyFingerprint_ReturnsFalse: confirms validator safe against empty input (ISS-008 scaffold)
- NcProgramManager.Tests.csproj: <Compile> entries for 2 new files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
14 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 14-test-hardening | 01 | tdd | 1 |
|
true |
Purpose
Stub-based integration tests already run; without an env-var pattern there is no supported path to run against real hardware in CI or manually. LicenseFile has only one test (non-existent path) — empty-file and whitespace-only branches are untested. The IMachineFingerprint mock pattern ensures the license flow is documented as testable without WMI.
Output
Two new test files (HardwareTestHelper.cs, LicenseFileTests.cs) and additions to three existing test files. No production code changes.
<acceptance_criteria>
AC-1: Hardware tests skip when env var not set
Given no HEIDENHAIN_IP / SIEMENS_IP / MITSUBISHI_IP environment variable is set
When the [Category("Hardware")] test runs in standard CI
Then NUnit reports it as Ignored (not Failed), with message "Hardware test skipped: <VAR> not set."
AC-2: Hardware tests run when env var is set
Given HEIDENHAIN_IP (or SIEMENS_IP / MITSUBISHI_IP) is set to a valid IP string
When the [Category("Hardware")] test runs
Then it attempts ConnectAsync against that IP with the default protocol port (no stub)
AC-3: LicenseFile empty/whitespace files return false
Given a real temp file that exists but contains empty bytes or only whitespace/newlines
When LicenseFile.TryRead() is called with that path
Then it returns false and the out parameter is null
AC-4: LicenseFile valid content returns true with trimmed base64
Given a real temp file containing a valid base64 string followed by a newline
When LicenseFile.TryRead() is called with that path
Then it returns true and the out parameter equals the trimmed base64 string
AC-5: IMachineFingerprint mock — empty fingerprint does not crash validator
Given a Moq Mock<IMachineFingerprint> that returns empty string from GetFingerprint()
When LicenseValidator.Validate(mockFingerprint.GetFingerprint(), anyLicense) is called
Then it returns false without throwing
</acceptance_criteria>
Task 1: Create HardwareTestHelper and add hardware test variants to 3 integration test files NcProgramManager.Tests/Integration/HardwareTestHelper.cs, NcProgramManager.Tests/Integration/HeidenhainMachineIntegrationTests.cs, NcProgramManager.Tests/Integration/SiemensMachineIntegrationTests.cs, NcProgramManager.Tests/Integration/MitsubishiMachineIntegrationTests.cs 1. Create `NcProgramManager.Tests/Integration/HardwareTestHelper.cs`: ```csharp
using System;
using NUnit.Framework;
namespace NcProgramManager.Tests.Integration
{
internal static class HardwareTestHelper
{
internal static string GetIpOrSkip(string envVarName)
{
string ip = Environment.GetEnvironmentVariable(envVarName);
if (string.IsNullOrEmpty(ip))
Assert.Ignore("Hardware test skipped: " + envVarName + " not set.");
return ip;
}
}
}
```
2. In `HeidenhainMachineIntegrationTests.cs`, add this method inside the class (after existing tests):
```csharp
[Test]
[Category("Hardware")]
public void Hardware_ConnectAsync_RealMachine()
{
string ip = HardwareTestHelper.GetIpOrSkip("HEIDENHAIN_IP");
var config = new HeidenhainConnectionConfig
{
Name = "HeidenhainHardware",
IpAddress = ip,
Port = 19000,
Username = Environment.GetEnvironmentVariable("HEIDENHAIN_USER") ?? "user",
Password = Environment.GetEnvironmentVariable("HEIDENHAIN_PASS") ?? "pass",
ConnectTimeout = TimeSpan.FromSeconds(10)
};
using (var machine = new HeidenhainMachine(config))
{
var result = machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True, "ConnectAsync failed: " + result.Error);
}
}
```
Do NOT modify the existing stub-based tests — only add the new hardware method.
3. In `SiemensMachineIntegrationTests.cs`, add inside the class:
```csharp
[Test]
[Category("Hardware")]
public void Hardware_ConnectAsync_RealMachine()
{
string ip = HardwareTestHelper.GetIpOrSkip("SIEMENS_IP");
var config = new SiemensConnectionConfig
{
Name = "SiemensHardware",
IpAddress = ip,
Port = 21,
Username = Environment.GetEnvironmentVariable("SIEMENS_USER") ?? "user",
Password = Environment.GetEnvironmentVariable("SIEMENS_PASS") ?? "pass",
ProgramDirectory = "/_N_MPF_DIR/",
RequestTimeout = TimeSpan.FromSeconds(10)
};
using (var machine = new SiemensMachine(config))
{
var result = machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True, "ConnectAsync failed: " + result.Error);
}
}
```
4. In `MitsubishiMachineIntegrationTests.cs`, add inside the class:
```csharp
[Test]
[Category("Hardware")]
public void Hardware_ConnectAsync_RealMachine()
{
string ip = HardwareTestHelper.GetIpOrSkip("MITSUBISHI_IP");
var config = new MitsubishiConnectionConfig
{
Name = "MitsubishiHardware",
IpAddress = ip,
Port = 21,
Username = Environment.GetEnvironmentVariable("MITSUBISHI_USER") ?? "user",
Password = Environment.GetEnvironmentVariable("MITSUBISHI_PASS") ?? "pass",
ProgramDirectory = "/PRG/",
RequestTimeout = TimeSpan.FromSeconds(10)
};
using (var machine = new MitsubishiMachine(config))
{
var result = machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True, "ConnectAsync failed: " + result.Error);
}
}
```
Constraints:
- Use `System.Environment.GetEnvironmentVariable` (available .NET 4.7.2)
- Use `Assert.Ignore()` (NUnit 3) — NOT `[Ignore]` attribute (static) — so it skips at runtime when var missing
- Do NOT use `CancellationToken` or async/await — machine classes are sync-by-design
- The hardware test disposes the machine itself (using block) — does not use class-level `_machine` or `_stub` fields
Read all 4 files:
- HardwareTestHelper.cs exists with `GetIpOrSkip` calling `Assert.Ignore`
- Each integration test file has `Hardware_ConnectAsync_RealMachine` with `[Category("Hardware")]`
- No existing stub tests modified
AC-1 and AC-2 satisfied: hardware tests skip if env var absent, connect if present
Task 2: Create LicenseFileTests and add IMachineFingerprint mock test to LicenseValidatorTests
NcProgramManager.Tests/Unit/LicenseFileTests.cs,
NcProgramManager.Tests/Unit/LicenseValidatorTests.cs
1. Create `NcProgramManager.Tests/Unit/LicenseFileTests.cs`:
```csharp
using System.IO;
using NUnit.Framework;
using NcProgramManager.Licensing;
namespace NcProgramManager.Tests.Unit
{
[TestFixture]
internal class LicenseFileTests
{
private string _tempPath;
[SetUp]
public void SetUp()
{
_tempPath = Path.GetTempFileName();
}
[TearDown]
public void TearDown()
{
if (File.Exists(_tempPath))
File.Delete(_tempPath);
}
[Test]
public void TryRead_EmptyFile_ReturnsFalse()
{
File.WriteAllText(_tempPath, string.Empty);
bool result = LicenseFile.TryRead(_tempPath, out string base64);
Assert.IsFalse(result);
Assert.IsNull(base64);
}
[Test]
public void TryRead_WhitespaceOnly_ReturnsFalse()
{
File.WriteAllText(_tempPath, " \n\r\n ");
bool result = LicenseFile.TryRead(_tempPath, out string base64);
Assert.IsFalse(result);
Assert.IsNull(base64);
}
[Test]
public void TryRead_ValidBase64Content_ReturnsTrueWithTrimmedValue()
{
string expected = "SGVsbG8gV29ybGQ=";
File.WriteAllText(_tempPath, expected + "\n");
bool result = LicenseFile.TryRead(_tempPath, out string base64);
Assert.IsTrue(result);
Assert.AreEqual(expected, base64);
}
[Test]
public void TryRead_NonExistentPath_ReturnsFalse()
{
bool result = LicenseFile.TryRead(_tempPath + ".notexist", out string base64);
Assert.IsFalse(result);
Assert.IsNull(base64);
}
}
}
```
2. In `LicenseValidatorTests.cs`, add one test method at the bottom of the class (before closing brace):
```csharp
[Test]
public void Validate_EmptyFingerprint_ReturnsFalse()
{
// Empty fingerprint — matches no valid license; must not throw
Assert.IsFalse(_validator.Validate(string.Empty, Sign("CPU123")));
}
```
This uses `_validator` already set up in `[SetUp]` (test-keyed validator), so no new fields needed.
Constraints:
- `Path.GetTempFileName()` creates a 0-byte file, so `TearDown` must delete it
- `LicenseFile.TryRead` is internal + static — accessible from test project because NcProgramManager uses `[assembly: InternalsVisibleTo("NcProgramManager.Tests")]` (verify this exists; if not, note it as a blocker)
- Do NOT test `LicenseFile.TryRead` with a directory path — edge case not worth the OS-specific behavior
- Do NOT add Moq for `IMachineFingerprint` — existing `LicenseValidator(string publicKeyXml)` ctor makes the empty-fingerprint test straightforward without mock overhead
- Read `LicenseFileTests.cs` — 4 test methods present, all use temp file, TearDown deletes
- Read `LicenseValidatorTests.cs` — `Validate_EmptyFingerprint_ReturnsFalse` present at end of class
- Check `Properties/AssemblyInfo.cs` or project-level for `InternalsVisibleTo` declaration — if missing, flag it
AC-3, AC-4, AC-5 satisfied: LicenseFile edge cases covered; empty fingerprint handled gracefully
DO NOT CHANGE
Licensing/LicenseFile.cs— production code; no changes in this phaseLicensing/LicenseValidator.cs— public key constant must not be modifiedLicensing/MachineFingerprint.cs— fixed in Phase 13; not in scope- All existing test methods — no modifications; only additions
Cnc/**/*.cs— no production machine code
SCOPE LIMITS
- No Moq mocking of
IMachineFingerprintin tests — theLicenseValidator(string)ctor injection already enables unit testing without mocks; Moq mock would add complexity for no test coverage gain - Hardware tests only add
ConnectAsyncverification — no write/read against real hardware (unsafe without known machine state) - No new NuGet packages — Moq 4.18.4 already present; NUnit 3.13.3 already present
- No async/await in test methods — stay consistent with existing test style (
.GetAwaiter().GetResult())
<success_criteria>
- All 2 tasks completed
- All 5 ACs covered
- ISS-007 and ISS-008 addressed
- No existing tests broken
- No production code modified </success_criteria>