diff --git a/Externals/CommonControls.dll b/Externals/CommonControls.dll index b0c5095..67018a0 100644 Binary files a/Externals/CommonControls.dll and b/Externals/CommonControls.dll differ diff --git a/GeneratorForm.cs b/GeneratorForm.cs index 59e99ed..d1004fa 100644 --- a/GeneratorForm.cs +++ b/GeneratorForm.cs @@ -204,9 +204,10 @@ private void SimulateToggle_CheckedChanged(object sender, EventArgs e) _ = sender; _ = e; - // Update the simulation status and button - m_Data.Simulated = !(m_Data.Simulated); - m_SimulateToggle.Checked = m_Data.Simulated; + // Take the simulation status from the toggle rather than by inverting what is recorded. The + // toggle is also set from code, such as when a session is loaded, and inverting the recorded + // value there disagrees with the control and sets the two of them toggling each other. + m_Data.Simulated = m_SimulateToggle.Checked; // Record the device interface requires initialization m_Timer.Initialized = false; diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e2b6f1 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# Random Number Generator + +A Windows desktop application for recording and analysing the output of a +[TruRNGpro](https://ubld.it/truerngpro) hardware random number generator. + +The application samples the device, records every reading to a session file, charts the results as they +arrive, and compares recorded sessions against one another. A built-in simulator stands in for the device, +so the application can be run and developed without one attached. + +## What it does + +Each sample reads a block of bits from the device and records the **average of those bits**, which for an +unbiased generator sits around 0.5. Readings are taken ten times a second. The application shows, live: + +- the running average, the number of data points, the deviation from the statistical mean, and the standard + deviation +- a chart of the individual readings against the running average, with the axis scaling itself to the data +- the elapsed session time + +Every reading is written to the session file as it is taken, so a session survives the application being +closed or stopped unexpectedly. + +### Analysis + +A recorded session can be loaded as a **baseline** and another as a **result**, and the two are compared +directly: mean, standard deviation, skewness and kurtosis for each, the difference between their means, and +a histogram of the two distributions overlaid. Statistics are calculated with +[Math.NET Numerics](https://numerics.mathdotnet.com/). + +### Targets + +A session can record a target value of `0` or `1` — the outcome the operator is attempting to influence — +or `None`. The target is stored in the session file alongside the data. + +## Requirements + +- Windows +- Visual Studio 2022 with the **.NET desktop** and **Desktop development with C++** workloads +- .NET Framework 4.8 +- A TruRNGpro on a USB serial port — optional, as the simulator can be used instead + +## Building + +Build the solution rather than the individual projects: the C# application depends on the native DLL, and +the post-build steps that place the DLL where the application and the tests can find it rely on the +solution directory. + +``` +msbuild RandomNumberGenerator.sln /p:Configuration=Debug /p:Platform="Any CPU" +``` + +MSBuild is not usually on the path; it is under the Visual Studio installation, for example +`C:\Program Files\Microsoft Visual Studio\18\Enterprise\MSBuild\Current\Bin\MSBuild.exe`. + +NuGet packages are not committed, so restore them on a fresh clone. The projects use `packages.config`, so +`dotnet restore` will not work: + +``` +nuget restore RandomNumberGenerator.sln +``` + +The application is built to `bin\Debug\Random Number Generator.exe` and copied to `bin\`. + +## Running + +Start the application, then: + +1. **Choose a source.** Leave *Simulate* unchecked and pick the COM port the device is on, or check it and + enter a seed for the simulator. The port list updates by itself as devices are connected and removed. +2. **Choose a file.** *Browse* selects the session file. An existing file is loaded and displayed, and the + session continues in it. A new file is created. +3. **Choose a target**, if the session has one. +4. **Start.** *Pause* suspends recording without ending the session; *Stop* ends it. + +To analyse previous sessions, browse for a baseline file and a result file in the comparison section. + +### Recovering a session + +A file left unfinished — by the application being stopped while recording, or by the machine losing power — +is recovered when it is next opened. The readings it holds are loaded, the file is reported as having been +recovered, and recording continues in it. Only complete readings are recovered; a reading that was still +being written when the application stopped is discarded rather than being loaded as a smaller number. + +## Session files + +Sessions are XML, written as the data is recorded: + +```xml + + + 0.499969482421875 + 0.500091552734375 + +``` + +`Simulated` records whether the readings came from the simulator. `Target` is `0`, `1`, or `-1` for a +session with no target. `Time` is the elapsed session time when the reading was taken. Values are written +in the invariant culture, so a file written on one machine reads back as the same numbers on another. + +The file is shared for reading while a session is in progress, so it can be inspected or backed up without +stopping the recording. + +## Layout + +| Project | | +|---|---| +| `RandomNumberGenerator.csproj` | The application. Windows Forms, .NET Framework 4.8, AnyCPU | +| `TruRNGpro/TruRNGpro.vcxproj` | Native DLL holding the device interface and the simulator. x64 | +| `RandomNumberGenerator.Test/` | Unit tests. MSTest and Moq | + +The application talks to the native DLL through two exported functions, `Initialize` and +`GetRandomBitAverage`, which sit behind an interface implemented by both the real device and the simulator. + +## Tests + +``` +vstest.console.exe "RandomNumberGenerator.Test\bin\Debug\Random Number Generator.Test.dll" +``` + +`vstest.console.exe` is under the Visual Studio installation, in +`Common7\IDE\CommonExtensions\Microsoft\TestWindow`. A single test or class can be selected: + +``` +vstest.console.exe "...\Random Number Generator.Test.dll" /Tests:WriteDataPoint_ValidWriter_Success +vstest.console.exe "...\Random Number Generator.Test.dll" /TestCaseFilter:"FullyQualifiedName~RNGXMLWriterTests" +``` + +Build the whole solution before running them, as the tests need the native DLL that the solution build puts +in place. + +## Contributing + +The coding standards are written down and are followed throughout: + +- `CODING_GUIDELINES.md` for the application +- `RandomNumberGenerator.Test/CODING_GUIDELINES_TESTS.md` for the tests +- `TruRNGpro/CODING_GUIDELINES_CPP.md` for the native code + +`CLAUDE.md` describes the architecture and the build for anyone, or anything, new to the codebase. + +## Third-party code + +- [Math.NET Numerics](https://numerics.mathdotnet.com/) (MIT), for the statistics +- [Moq](https://github.com/devlooped/moq) and MSTest, for the tests +- `Externals/CommonControls.dll` and `Externals/DeviceInterfaces.dll`, committed binaries with no source + in this repository +- `TruRNGpro/rng.h`, a third-party header that wraps the serial port setup and reads for the device. Its + licensing has not been established, which is worth settling before the code is distributed. + +## Copyright + +Copyright (C) Mike Pullen. All rights reserved. The source files are marked confidential and proprietary. diff --git a/RandomNumberGenerator.Test/GeneratorForm.Test.cs b/RandomNumberGenerator.Test/GeneratorForm.Test.cs index 1a88284..b686ca2 100644 --- a/RandomNumberGenerator.Test/GeneratorForm.Test.cs +++ b/RandomNumberGenerator.Test/GeneratorForm.Test.cs @@ -1667,6 +1667,61 @@ public void FileLoadingErrors_Integration_ProperErrorHandling() StringAssert.Contains(generatorForm.StatusBoxText, "Result file not found", "Should show result file not found error"); } + /// + /// Tests setting the simulate toggle from code settles rather than toggling back and forth. + /// NOTE: Loading a session sets this toggle to match the file. Taking the new state by inverting + /// what is recorded rather than by reading the toggle left the two of them setting each other + /// without end, which overflowed the stack and took the application down as a file was opened. + /// + [TestMethod] + [TestCategory("Component")] + [Timeout(15000)] + public void SimulateToggle_SetFromCode_MatchesTheToggle() + { + //**************************************************************// + // Arrange + //**************************************************************// + + // Mock the session timer and setup the properties + Mock mockSessionTimer = new Mock(); + mockSessionTimer.SetupAllProperties(); + + // Mock the session data, recording it as simulated as a loaded session would + Mock mockSessionData = new Mock(); + mockSessionData.Setup(mock => mock.Timer).Returns(mockSessionTimer.Object); + mockSessionData.SetupProperty(mock => mock.Simulated, true); + Mock mockDeviceTimer = new Mock(); + + // Create the object under test + GeneratorForm generatorForm = new GeneratorForm(mockSessionData.Object, mockDeviceTimer.Object); + + // Reach the toggle, which is not exposed outside of the form + System.Reflection.FieldInfo toggleField = typeof(GeneratorForm).GetField("m_SimulateToggle", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.IsNotNull(toggleField, "The simulate toggle should be present on the form"); + System.Windows.Forms.CheckBox simulateToggle = (System.Windows.Forms.CheckBox)toggleField.GetValue(generatorForm); + + //**************************************************************// + // Act + //**************************************************************// + + // Set the toggle the way loading a simulated session does, which disagrees with the toggle + simulateToggle.Checked = true; + + //**************************************************************// + // Assert + //**************************************************************// + + // Verify the toggle and what is recorded agree, rather than having driven each other + Assert.IsTrue(simulateToggle.Checked, "The toggle should stay set"); + Assert.IsTrue(mockSessionData.Object.Simulated, "The recorded state should match the toggle"); + + // And that setting it back settles the same way + simulateToggle.Checked = false; + Assert.IsFalse(simulateToggle.Checked, "The toggle should stay clear"); + Assert.IsFalse(mockSessionData.Object.Simulated, "The recorded state should match the toggle"); + } + #endregion } } \ No newline at end of file diff --git a/RandomNumberGenerator.csproj b/RandomNumberGenerator.csproj index e5e0a49..f327200 100644 --- a/RandomNumberGenerator.csproj +++ b/RandomNumberGenerator.csproj @@ -154,6 +154,10 @@ if errorlevel 1 goto end echo Copied to "$(SolutionDir)bin\" +xcopy /y "$(SolutionDir)bin\TruRNGpro.dll" "$(TargetDir)" +if errorlevel 1 goto end +echo Copied to "$(TargetDir)" + :end \ No newline at end of file