diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..a2e4982 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,87 @@ +name: "CodeQL Advanced" + +on: + push: + branches: [ "main", "dev" ] + pull_request: + branches: [ "main", "dev" ] + schedule: + - cron: '34 0 * * 2' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ubuntu-latest + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: c-cpp + build-mode: autobuild + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..fb0d976 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,27 @@ +name: CTest + +on: + push: + branches: ["main", "dev"] + pull_request: + branches: ["main", "dev"] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y cmake g++ + + - name: Configure CMake + run: cmake -S . -B build + + - name: Build + run: cmake --build build + + - name: Run tests + run: ctest --test-dir build --output-on-failure \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae09150 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Project structure +build/* + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# CMake +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +CMakeUserPresets.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..fcd947e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) Launch", + "type": "cppdbg", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": ["--algorithm", "xor", "-m", "0", "-M=10", "--count", "5", "-t", "int"], + "stopAtEntry": false, + "cwd": "${fileDirname}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ], + "preLaunchTask": "CMake: build" + } + + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..694dd56 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,83 @@ +{ + "files.associations": { + "random": "cpp", + "algorithm": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "chrono": "cpp", + "compare": "cpp", + "complex": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "cstdint": "cpp", + "deque": "cpp", + "forward_list": "cpp", + "list": "cpp", + "map": "cpp", + "set": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "ratio": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "ranges": "cpp", + "semaphore": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "cinttypes": "cpp", + "typeindex": "cpp", + "typeinfo": "cpp", + "__nullptr": "cpp", + "any": "cpp", + "variant": "cpp", + "__locale": "cpp", + "charconv": "cpp", + "__hash_table": "cpp", + "__split_buffer": "cpp", + "__tree": "cpp", + "queue": "cpp", + "stack": "cpp", + "filesystem": "cpp" + }, + "cmake.sourceDirectory": "/home/eric/Projects/Randomizer/" +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1b218fb --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.22) +project(randomizer VERSION 0.1.0) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + + +# Set output directories +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +# Include directories +include_directories(include) + +# Automatically collect all source and header files +file(GLOB_RECURSE SOURCES "src/*.cpp") +file(GLOB_RECURSE HEADERS "include/*.hpp") + +# Define a library target for the main code +add_library(randomizer_lib ${SOURCES} ${HEADERS}) +target_include_directories(randomizer_lib PUBLIC ${CMAKE_SOURCE_DIR}/include) + + +# Define main executable +add_executable(${PROJECT_NAME} ${SOURCES}) +target_link_libraries(${PROJECT_NAME} PRIVATE randomizer_lib) + +# Make CPack install the binary in /usr/bin +install(TARGETS ${PROJECT_NAME} DESTINATION /usr/bin) + +# Make CPack install the man page +include(GNUInstallDirs) +install(FILES "${CMAKE_SOURCE_DIR}/docs/randomizer.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1") + +# Add compiler warnings only for GCC and Clang +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(randomizer_lib PRIVATE + -Wall + -Wextra + -Wshadow + -Wconversion + -Wpedantic + -Werror + ) + target_compile_options(${PROJECT_NAME} PRIVATE + -Wall + -Wextra + -Wshadow + -Wconversion + -Wpedantic + -Werror + ) +endif() + + +enable_testing() + +add_subdirectory(tests) + +add_subdirectory(packaging) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..706584b --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +Copyright (c) 2025, Eric Butcher \ No newline at end of file diff --git a/README.md b/README.md index f5a173d..27da911 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,79 @@ # Randomizer -A UNIX command line utility written in C++ for generating psuedorandom numbers and sequences. +A UNIX command line utility written in C++ for generating pseudorandom numbers and sequences. A variety of different pseudorandom number generators can be used including XORShift, a Linear-Congruential-Generator, and the famous Mersenne-Twister. This program is not necessarily intended to generate pseudorandom values quickly, in bulk quantities, or for scientific purposes. + +This program was mainly written so that I could learn modern C++ and how to build C++ projects with CMake. The build scripts are setup to package this utility as a `.deb` or a `.rpm` for easy installation alongside the included man page on most popular and modern linux distributions. There are also container files included that come with the utility installed inside of them in case you just want to try it out without installing it on your host machine. + +For usage instructions, use `randomizer --help` or view the [included man page](./docs/randomizer.1) using `man randomizer` once the application is installed. + +## Building + +The project is built using CMake. First, make sure you have cmake >= 3.22 installed on your machine: + +```shell +cmake --version +``` + +If you do not have CMake installed, or your version is <3.22, you can install it using [these instructions](https://cmake.org/cmake/help/book/mastering-cmake/chapter/Getting%20Started.html#getting-and-installing-cmake-on-your-computer). + +Also ensure that you have a modern version of `gcc` or `clang` installed that supports C++ versions 20 or later. CMake will also need to use a build tool such as `make` or `ninja-build`, make sure one of those is installed on your machine. These packages should be included by most Linux distribution app repositories when you install CMake with tools like `apt` or `dnf`. + +Once CMake is installed, clone or download this repo and open the root directory of this repo. You can then configure the project using: +```shell +cmake -S . -B build +``` + +and then build the project using: +```shell +cmake --build build +``` + +You should now find that there is a binary called `randomizer` in `./build/out/`. This is the binary executable for the program. You can run the program by using `./randomizer`. + + +## Testing + +When the project was built there should have also been a binary called `RandomizerTests` which contains the executable for unit testing. You can run all of the unit tests by using the command `ctest` from the `./build/` directory. + +## Packaging + +To package the program in `.deb` and `.rpm` formats change into the `./build/` directory and use the command `cpack`. This will create a `.deb` archive for installing on Debian based systems and a `.rpm` archive for installing on RedHat based systems. Both packages will install the binary executable and the included man page. These archives will be placed in the `./build/out/` directory. + +Once packaged, you can install the `.deb` archive on any modern Debian-based system with: +```shell +sudo dpkg -i ./build/out/randomizer-.deb +``` + +To install the `.rpm` archive on any modern RedHat-based distribution, use: +```shell +sudo dnf install ./build/out/randomizer-.rpm +``` + +> These packages were tested on Ubuntu 24.04, Debian 12, and Fedora 42. + +## Containers + +If you do not wish to install the application directly on your host machine, container files are included which will install the application to be used in the container. One image is Debian based and installs the `.deb` archive and the other is Fedora based and installs the `.rpm` archive. Make sure you go through the steps to build and package the application with CMake before building the container(s). + +The containers can be created using [Podman](https://podman.io/) or [Docker](https://www.docker.com/). You must have the CLI for either program installed on your computer. The commands should be the same regardless of what container application you choose to use, just use the appropriate command name. + +The Debian image can be created using the following command from the project's root directory: +```shell +podman build -f containers/Containerfile.debian -t randomizer:debian +``` +and the Fedora image can be created using the command: +```shell +podman build -f containers/Containerfile.fedora -t randomizer:fedora +``` +also from the project's root directory. + +The container can then be accessed via shell using the command: +```shell +podman run --rm -it randomizer:debian +``` +or +```shell +podman run --rm -it randomizer:fedora +``` + +Once inside the container, the application can be ran like normal from the command line using the command `randomizer` and the manual page can be viewed using `man randomizer`. diff --git a/containers/Containerfile.debian b/containers/Containerfile.debian new file mode 100644 index 0000000..a665e07 --- /dev/null +++ b/containers/Containerfile.debian @@ -0,0 +1,11 @@ +FROM debian:12 + +RUN apt-get update && apt-get install -y man man-db less + +COPY ../build/out/*.deb . +RUN dpkg -i $(ls *.deb | head -n1) && rm -f ./*.deb + +RUN groupadd unprivileged-group && \ + useradd -g unprivileged-group -N -m unprivileged-user +WORKDIR /home/unprivileged-user +USER unprivileged-user:unprivileged-group diff --git a/containers/Containerfile.fedora b/containers/Containerfile.fedora new file mode 100644 index 0000000..414c380 --- /dev/null +++ b/containers/Containerfile.fedora @@ -0,0 +1,11 @@ +FROM fedora:42 + +RUN dnf install -y man man-pages man-db less --setopt='tsflags=' + +COPY ../build/out/*.rpm . +RUN dnf install -y $(ls *.rpm | head -n1) --setopt='tsflags=' && rm -rf ./*.rpm + +RUN groupadd unprivileged-group && \ + useradd -g unprivileged-group -N -m unprivileged-user +WORKDIR /home/unprivileged-user +USER unprivileged-user:unprivileged-group diff --git a/docs/randomizer.1 b/docs/randomizer.1 new file mode 100644 index 0000000..e41d9ce --- /dev/null +++ b/docs/randomizer.1 @@ -0,0 +1,50 @@ +.TH RANDOMIZER 1 "2025-07-05" GNU +.SH NAME +randomizer \- a tool to generate pseudorandom numbers and sequences +.SH SYNOPSIS +.B randomizer +.RI [ options ] + +.SH DESCRIPTION +.B randomizer +is a UNIX command line utility for generating pseudorandom numbers and sequences. +A variety of different pseudorandom number generators can be used. This program is not +necessarily intended to generate pseudorandom values quickly, in bulk quantities, or for scientific purposes. +.SH OPTIONS + +.TP +.BR \-a ", " \-\-algorithm " " \fIalgorithm\fP +Specify the algorithm. Can be one of xorshift, linear-congruential-generator, or mersenne-twister. +If no algorithm is specified then xorshift will be used by default. +The shorthands xor, lcg, and mt can also be used for their respective algorithms. + +.TP +.BR \-c ", " \-\-count " " \fIamount\fP +Specify the amount of random numbers to generate. If no amount is specified only one number will be generated. + +.TP +.BR \-h ", " \-\-help +Show help message and exit. + +.TP +.BR \-M ", " \-\-max " " \fInumber\fP +Specify the maximum value that can be generated. +If generating an integer type the bound will be inclusive; the bound will be exclusive if generating a floating point type. +This must always be used in conjunction with \-\-min. Ranges cannot be specified when generating unit-normal type values. + +.TP +.BR \-m ", " \-\-min " " \fInumber\fP +Specify the minimum value that can be generated. +The minimum value will always be an inclusive bound regardless of the type being generated. +This must always be used in conjunction with \-\-max. Ranges cannot be specified when generating unit-normal type values. + +.TP +.BR \-t ", " \-\-type " " \fItype\fP +Specify the type of numbers to be generated. Can be unit-normal, floating, or integer. +If generating floating or integer values the user must also specify a range using min and max. +Do not specify a range when creating unit-normal values. +The shorthands unit, float, and int can also be used for their respective values. + +.TP +.BR \-v ", " \-\-version +Show version information. diff --git a/include/linear_congruential_generator.hpp b/include/linear_congruential_generator.hpp new file mode 100644 index 0000000..6b5f614 --- /dev/null +++ b/include/linear_congruential_generator.hpp @@ -0,0 +1,56 @@ +#ifndef LINEAR_CONGRUENTIAL_GENERATOR_H +#define LINEAR_CONGRUENTIAL_GENERATOR_H + +#include +#include "prng.hpp" + +class LinearCongruentialGenerator : public PseudoRandomNumberGenerator { + // https://en.wikipedia.org/wiki/Linear_congruential_generator# + + /* + LCGs are defined as X_{n+1} = (aX_{n} + c) \mod{m} + Where X is the vector of pseudorandom values produced + The values a, c, and m are constants + m is the modulus + a is the multiplier + c is the increment + + glibc uses m=2^{31} a=1103515245 c=12345 and masks the result with 0x3FFFFFFF + which is what is used for the default implementation here + */ +private: + + // Defaults used from the glibc implementation, see: + /* @misc{ enwiki:1280426923, + author = "{Wikipedia contributors}", + title = "Linear congruential generator --- {Wikipedia}{,} The Free Encyclopedia", + year = "2025", + url = "https://en.wikipedia.org/w/index.php?title=Linear_congruential_generator&oldid=1280426923", + note = "[Online; accessed 10-May-2025]" + } */ + + static constexpr uint64_t DefaultModulus = 0x7FFFFFFF; + static constexpr uint64_t DefaultMultiplier = 1103515245; + static constexpr uint64_t DefaultIncrement = 12345; + static constexpr uint64_t DefaultMask = 0x7FFFFFFF; // bits 0 through 30 + + const uint64_t m_modulus; + const uint64_t m_multiplier; + const uint64_t m_increment; + const uint64_t m_mask; + uint64_t m_current_value = 0; + + + static uint64_t getMinimumValue(const std::uint64_t mask); + static uint64_t getMaximumValue(const uint64_t modulus, const uint64_t mask); + +public: + LinearCongruentialGenerator(); + LinearCongruentialGenerator(const uint64_t seed); + LinearCongruentialGenerator(const uint64_t modulus, const uint64_t multiplier, const uint64_t increment, const uint64_t mask); + LinearCongruentialGenerator(const uint64_t seed, const uint64_t modulus, const uint64_t multiplier, const uint64_t increment, const uint64_t mask); + + uint64_t generateRandomValue() override; +}; + +#endif \ No newline at end of file diff --git a/include/mersenne_twister.hpp b/include/mersenne_twister.hpp new file mode 100644 index 0000000..ffabd77 --- /dev/null +++ b/include/mersenne_twister.hpp @@ -0,0 +1,105 @@ +#ifndef MERSENNE_TWISTER_H +#define MERSENNE_TWISTER_H + +#include +#include +#include "prng.hpp" + +class MersenneTwister : public PseudoRandomNumberGenerator { + +private: + + /* MT19937-64 coefficients: + w = 64 , word size in bits + n = 312, degree of recurrence + m = 156, middle word + r = 31, number of bits of the lower bit mask + a = 0xb5026f5aa96619e9, coefficients + u = 29, tempering bit shift/mask + d = 0x5555555555555555 + s = 17, tempering bit shift/mask + b = 0x71d67fffeda60000, tempering bit mask + t = 37, tempering bit shift/mask + c = 0xfff7eee000000000, tempering bit mask + l = 43, tempering bit shift/mask + f = 6364136223846793005 + taken from @misc{ enwiki:1290406357, + author = "{Wikipedia contributors}", + title = "Mersenne Twister --- {Wikipedia}{,} The Free Encyclopedia", + year = "2025", + url = "https://en.wikipedia.org/w/index.php?title=Mersenne_Twister&oldid=1290406357", + note = "[Online; accessed 5-June-2025]" + } + */ + + static constexpr int Default_w = 64; + static constexpr int Default_n = 312; + static constexpr int Default_m = 156; + static constexpr int Default_r = 31; + static constexpr uint64_t Default_a = 0xb5026f5aa96619e9; + static constexpr uint64_t Default_u = 29; + static constexpr uint64_t Default_d = 0x5555555555555555; + static constexpr uint64_t Default_s = 17; + static constexpr uint64_t Default_b = 0x71d67fffeda60000; + static constexpr uint64_t Default_t = 37; + static constexpr uint64_t Default_c = 0xfff7eee000000000; + static constexpr uint64_t Default_l = 43; + static constexpr uint64_t Default_f = 6364136223846793005; + + // word size + int m_w; + + // degree of the recurrence + int m_n; + + // middle word offset + int m_m; + + // separation point where the upper bitmask begins + int m_r; + + // twist matrix coefficient + uint64_t m_a; + + // MT bitwise constant + uint64_t m_u; + + // MT bitwise constant + uint64_t m_d; + + // bit shift used for tempering + uint64_t m_s; + + // bit mask used for tempering + uint64_t m_b; + + // bit shift used for tempering + uint64_t m_t; + + // bit mask used for tempering + uint64_t m_c; + + // MT bitwise constant + uint64_t m_l; + + // initialization constant + uint64_t m_f; + + uint64_t m_upper_bit_mask; + uint64_t m_lower_bit_mask; + std::vector m_recurrence_state; + int m_state_index; + + + std::vector initializeRecurrenceState(const uint64_t seed); + uint64_t generateNextStateValue(); + uint64_t tempering(uint64_t); + +public: + MersenneTwister(); + MersenneTwister(const uint64_t seed); + + uint64_t generateRandomValue() override; +}; + +#endif \ No newline at end of file diff --git a/include/prng.hpp b/include/prng.hpp new file mode 100644 index 0000000..69ccf65 --- /dev/null +++ b/include/prng.hpp @@ -0,0 +1,28 @@ +#ifndef PRNG_H +#define PRNG_H + +#include + +class PseudoRandomNumberGenerator +{ + protected: + const uint64_t m_seed; + const uint64_t m_minimum_value; + const uint64_t m_maximum_value; + + + public: + PseudoRandomNumberGenerator(); + PseudoRandomNumberGenerator(const uint64_t seed); + PseudoRandomNumberGenerator(const uint64_t minimum_value, const uint64_t maximum_value); + PseudoRandomNumberGenerator(const uint64_t seed, const uint64_t minimum_value, const uint64_t maximum_value); + + uint64_t generateCryptographicallyInsecureSeed(); + + virtual uint64_t generateRandomValue() = 0; + double generateUnitNormalRandomValue(); + double generateFloatingPointRandomValue(float min, float max); + int64_t generateIntegerRandomValue(int32_t min, int32_t max); +}; + +#endif \ No newline at end of file diff --git a/include/program_runner.hpp b/include/program_runner.hpp new file mode 100644 index 0000000..563a73c --- /dev/null +++ b/include/program_runner.hpp @@ -0,0 +1,134 @@ +#include +#include +#include +#include +#include +#include +#include +#include "prng.hpp" + +class ProgramRunner { +public: + ProgramRunner(int argc, char **argv, int warmup_iterations = 0); + + struct ProgramStatus { + std::optional stdout_message; + std::optional stderr_message; + std::optional exit_code; + }; + + ProgramStatus iterate(); + + // will return the ProgramStatus of the last iteration + ProgramStatus iterate(uint64_t iterations); + + // runs until the program runner finishes, will return the ProgramStatus of the last iteration + ProgramStatus run(); + + bool is_finished(); + + const std::string version = "0.1"; + const std::string program_name = "randomizer"; + + + + + +private: + + enum class Algorithm { + XORShift, + LinearCongruentialGenerator, + MersenneTwister + // Add other algorithms here + }; + + const std::map algorithm_choices { + {"xorshift", Algorithm::XORShift}, + {"xor", Algorithm::XORShift}, + {"linear-congruential-generator", Algorithm::LinearCongruentialGenerator}, + {"lcg", Algorithm::LinearCongruentialGenerator}, + {"mersenne", Algorithm::MersenneTwister}, + {"mersenne-twister", Algorithm::MersenneTwister}, + {"mt", Algorithm::MersenneTwister} + }; + + + + enum class ProgramBehaviour { + Error, + Help, + Version, + GenerateUnitNormal, + GenerateFloating, + GenerateInteger + }; + + const std::map generation_types { + {"unit", ProgramBehaviour::GenerateUnitNormal}, + {"normal", ProgramBehaviour::GenerateUnitNormal}, + {"unit-normal", ProgramBehaviour::GenerateUnitNormal}, + {"normalized", ProgramBehaviour::GenerateUnitNormal}, + {"float", ProgramBehaviour::GenerateFloating}, + {"floating", ProgramBehaviour::GenerateFloating}, + {"decimal", ProgramBehaviour::GenerateFloating}, + {"floating-point", ProgramBehaviour::GenerateFloating}, + {"int", ProgramBehaviour::GenerateInteger}, + {"integer", ProgramBehaviour::GenerateInteger} + }; + + struct RawArguments { + // what special strings should we show? + + bool error; + bool show_help; + bool show_version; + + // how do we want to generate the random numbers? + std::optional type; + + std::optional algorithm_str; + std::optional min_str; + std::optional max_str; + std::optional count_str; + }; + + + // Defaults + + static constexpr uint32_t DefaultCount = 1; + static constexpr ProgramBehaviour DefaultGenerationType = ProgramBehaviour::GenerateInteger; + + RawArguments parse_args(int argc, char **argv); + void determine_program_configuration(const RawArguments &raw_arguments); + void determine_user_message_configuration(const bool error, const bool show_version, const bool show_help); + void determine_generation_range_configuration(const std::optional &min_str, const std::optional &max_str); + void determine_generation_type_configuration(const std::optional &generation_type); + void determine_count_configuration(const std::optional &count_str); + void determine_algorithm_configuration(const std::optional &alg_str); + void create_prng(); + void warmup(uint64_t iterations); + + + std::string error_string(); + std::string help_string(); + std::string version_string(); + + std::optional behaviour = std::nullopt; + std::optional algorithm = std::nullopt; + std::optional> min = std::nullopt; + std::optional> max = std::nullopt; + std::optional count = std::nullopt; + + uint32_t iteration = 0; + bool finished = false; + + std::unique_ptr prng = nullptr; + + // Exit codes + static constexpr int ExitCodeSuccess = 0; + static constexpr int ExitCodeError = 1; + + + +}; \ No newline at end of file diff --git a/include/xorshift.hpp b/include/xorshift.hpp new file mode 100644 index 0000000..b936e9c --- /dev/null +++ b/include/xorshift.hpp @@ -0,0 +1,39 @@ +#ifndef XOR_SHIFT_H +#define XOR_SHIFT_H + +#include +#include "prng.hpp" + +class XORShift : public PseudoRandomNumberGenerator { + +// @misc{ enwiki:1287473197, +// author = "{Wikipedia contributors}", +// title = "Xorshift --- {Wikipedia}{,} The Free Encyclopedia", +// year = "2025", +// url = "https://en.wikipedia.org/w/index.php?title=Xorshift&oldid=1287473197", +// note = "[Online; accessed 11-May-2025]" +// } +private: + + + // Default values from https://en.wikipedia.org/wiki/Xorshift + static constexpr uint64_t DefaultA = 13; + static constexpr uint64_t DefaultB = 7; + static constexpr uint64_t DefaultC = 17; + + // XORShift has 3 constants that are used for shifting + const uint64_t m_a; + const uint64_t m_b; + const uint64_t m_c; + uint64_t m_current_value = 0; + +public: + XORShift(); + XORShift(const uint64_t seed); + XORShift(const uint64_t seed, const uint64_t a, const uint64_t b, const uint64_t c); + + + uint64_t generateRandomValue() override; +}; + +#endif \ No newline at end of file diff --git a/packaging/CMakeLists.txt b/packaging/CMakeLists.txt new file mode 100644 index 0000000..dc103b0 --- /dev/null +++ b/packaging/CMakeLists.txt @@ -0,0 +1,28 @@ + +set(CPACK_PACKAGE_NAME ${PROJECT_NAME}) +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A UNIX command line utility written in C++ for generating pseudorandom numbers and sequences.") +set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE") + +set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}/out") + +set(CPACK_GENERATOR "DEB;RPM") + +set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) + +if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") +elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") + set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64") +else() + set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "${CMAKE_SYSTEM_PROCESSOR}") +endif() +set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) +set(CPACK_DEBIAN_PACKAGE_SECTION Miscellaneous) +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Eric Butcher") + +set(CPACK_RPM_PACKAGE_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR}) + +include(CPack) diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..73b6635 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,24 @@ +#include +#include +#include "prng.hpp" +#include "linear_congruential_generator.hpp" +#include "xorshift.hpp" +#include "program_runner.hpp" + +int main(int argc, char *argv[]){ + ProgramRunner runner = ProgramRunner(argc, argv, 1000); + ProgramRunner::ProgramStatus status; + while (!runner.is_finished()) { + status = runner.iterate(); + if (status.stdout_message.has_value()) { + std::cout << status.stdout_message.value() << std::endl; + } + if (status.stderr_message.has_value()) { + std::cerr << status.stderr_message.value() << std::endl; + } + if (status.exit_code.has_value()) { + exit(status.exit_code.value()); + } + } + exit(1); // If we reach here, something went wrong +} \ No newline at end of file diff --git a/src/prngs/linear_congruential_generator.cpp b/src/prngs/linear_congruential_generator.cpp new file mode 100644 index 0000000..e405f90 --- /dev/null +++ b/src/prngs/linear_congruential_generator.cpp @@ -0,0 +1,74 @@ +#include +#include +#include "linear_congruential_generator.hpp" +#include "prng.hpp" + + +// Default constructor +LinearCongruentialGenerator::LinearCongruentialGenerator() + : PseudoRandomNumberGenerator(getMinimumValue(DefaultMask), getMaximumValue(DefaultModulus, DefaultMask)), + m_modulus(DefaultModulus), + m_multiplier(DefaultMultiplier), + m_increment(DefaultIncrement), + m_mask(DefaultMask), + m_current_value(m_seed) {} + +// Constructor with seed +LinearCongruentialGenerator::LinearCongruentialGenerator(const uint64_t seed) + : PseudoRandomNumberGenerator(seed, getMinimumValue(DefaultMask), getMaximumValue(DefaultModulus, DefaultMask)), + m_modulus(DefaultModulus), + m_multiplier(DefaultMultiplier), + m_increment(DefaultIncrement), + m_mask(DefaultMask), + m_current_value(m_seed) {} + +// Constructor with custom parameters +LinearCongruentialGenerator::LinearCongruentialGenerator(const uint64_t modulus, const uint64_t multiplier, const uint64_t increment, const uint64_t mask) + : PseudoRandomNumberGenerator(getMinimumValue(mask), getMaximumValue(modulus, mask)), + m_modulus(modulus), + m_multiplier(multiplier), + m_increment(increment), + m_mask(mask), + m_current_value(m_seed) {} + +// Constructor with seed and custom parameters +LinearCongruentialGenerator::LinearCongruentialGenerator(const uint64_t seed, const uint64_t modulus, const uint64_t multiplier, const uint64_t increment, const uint64_t mask) + : PseudoRandomNumberGenerator(seed, getMinimumValue(mask), getMaximumValue(modulus, mask)), + m_modulus(modulus), + m_multiplier(multiplier), + m_increment(increment), + m_mask(mask), + m_current_value(m_seed) {} + + +uint64_t LinearCongruentialGenerator::getMinimumValue(const uint64_t mask) { + + // Find the index of the least significant bit + uint64_t least_significant_bit_index = 0; + uint64_t least_significant_compare_mask = mask; + + // We shift bits until we reach a 1, incrementing the counter each time + while ((least_significant_compare_mask & 1) == 0) { + least_significant_compare_mask >>= 1; + least_significant_bit_index++; + } + return (1 << least_significant_bit_index) - 1; +} + +uint64_t LinearCongruentialGenerator::getMaximumValue(const uint64_t modulus, const uint64_t mask) { + return std::min(modulus, mask); +} + +// Generate a random value anywhere in the range of the LCG +uint64_t LinearCongruentialGenerator::generateRandomValue() { + // Compute the standard LCG formula for the next value + m_current_value = (m_multiplier * m_current_value + m_increment) % m_modulus; + + // Apply the mask + m_current_value = (m_current_value & m_mask); + + // Ensure the value is within the specified range + m_current_value = m_current_value - m_minimum_value; + + return m_current_value; +} diff --git a/src/prngs/mersenne_twister.cpp b/src/prngs/mersenne_twister.cpp new file mode 100644 index 0000000..61ccd6d --- /dev/null +++ b/src/prngs/mersenne_twister.cpp @@ -0,0 +1,113 @@ +#include +#include +#include +#include "prng.hpp" +#include "mersenne_twister.hpp" + + +MersenneTwister::MersenneTwister() + : PseudoRandomNumberGenerator() { + m_w = Default_w; + m_n = Default_n; + m_m = Default_m; + m_r = Default_r; + m_a = Default_a; + m_u = Default_u; + m_d = Default_d; + m_s = Default_s; + m_b = Default_b; + m_t = Default_t; + m_c = Default_c; + m_l = Default_l; + m_f = Default_f; + m_lower_bit_mask = std::numeric_limits::max() >> (m_w - m_r); + m_upper_bit_mask = std::numeric_limits::max() << (m_r); + m_recurrence_state = initializeRecurrenceState(m_seed); + m_state_index = 0; +} + +MersenneTwister::MersenneTwister(const uint64_t seed) + : PseudoRandomNumberGenerator(seed) { + m_w = Default_w; + m_n = Default_n; + m_m = Default_m; + m_r = Default_r; + m_a = Default_a; + m_u = Default_u; + m_d = Default_d; + m_s = Default_s; + m_b = Default_b; + m_t = Default_t; + m_c = Default_c; + m_l = Default_l; + m_f = Default_f; + m_lower_bit_mask = std::numeric_limits::max() >> (m_w - m_r); + m_upper_bit_mask = std::numeric_limits::max() << (m_r); + m_recurrence_state = initializeRecurrenceState(m_seed); + m_state_index = 0; +} + +std::vector MersenneTwister::initializeRecurrenceState(const uint64_t seed) { + std::vector state(m_n); + state[0] = seed; + for (int i = 1; i < m_n; ++i) { + state[i] = (m_f * (state[i - 1] ^ (state[i - 1] >> (m_w - 2)))) + i; + } + return state; +} + +uint64_t MersenneTwister::generateNextStateValue(){ + + int k = m_state_index; + + // x_{k-n}, but since we are using circular indexing it is actually just the same as x_k! + int upper_index = k; + + // x_{k-(n-1)} + int lower_index = k - (m_n - 1); + if (lower_index < 0){ // wraparound the index for the buffer + lower_index += m_n; + } + + uint64_t upper_part = m_recurrence_state[upper_index] & m_upper_bit_mask; + uint64_t lower_part = m_recurrence_state[lower_index] & m_lower_bit_mask; + uint64_t concatenated_value = upper_part | lower_part; + + uint64_t matrix_mul_result = concatenated_value >> 1; + if (concatenated_value & 0b1){ + matrix_mul_result ^= m_a; + } + + + int middle_index = k - (m_n - m_m); + if (middle_index < 0){ + middle_index += m_n; + } + uint64_t middle_value = m_recurrence_state[middle_index]; + + uint64_t state_value = matrix_mul_result ^ middle_value; + + m_recurrence_state[k] = state_value; + + m_state_index++; + if (m_state_index >= m_n){ + m_state_index = 0; + } + + return state_value; + +} + +uint64_t MersenneTwister::tempering(uint64_t val){ + uint64_t tempered_value = val ^ (val >> m_u); + tempered_value ^= ((tempered_value << m_s) & m_b); + tempered_value ^= ((tempered_value << m_t) & m_c); + tempered_value ^= (tempered_value >> 1); + return tempered_value; +} + +uint64_t MersenneTwister::generateRandomValue(){ + uint64_t raw_value = generateNextStateValue(); + uint64_t tempered_value = tempering(raw_value); + return tempered_value; +} diff --git a/src/prngs/prng.cpp b/src/prngs/prng.cpp new file mode 100644 index 0000000..b22693b --- /dev/null +++ b/src/prngs/prng.cpp @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include "prng.hpp" + +/* The most default constructor will create a random seed from the system time +and assume that the range of the PRNG is the full range of the unsigned 64-bit +integer. While this is the only sensible default option, it is dangerous as it is +not generally true and will likely have the PRNG produce highly incorrect values when +generating uniform random floats. Use with caution! */ +PseudoRandomNumberGenerator::PseudoRandomNumberGenerator() + : m_seed(generateCryptographicallyInsecureSeed()), + m_minimum_value(std::numeric_limits::min()), + m_maximum_value(std::numeric_limits::max()) {} + + +PseudoRandomNumberGenerator::PseudoRandomNumberGenerator(const uint64_t seed) + : m_seed(seed), + m_minimum_value(std::numeric_limits::min()), + m_maximum_value(std::numeric_limits::max()) {} + +PseudoRandomNumberGenerator::PseudoRandomNumberGenerator(const uint64_t minimum_value, const uint64_t maximum_value) + : m_seed(generateCryptographicallyInsecureSeed()), + m_minimum_value(minimum_value), + m_maximum_value(maximum_value) {} + + +PseudoRandomNumberGenerator::PseudoRandomNumberGenerator(const uint64_t seed, const uint64_t minimum_value, const uint64_t maximum_value) + : m_seed(seed), + m_minimum_value(minimum_value), + m_maximum_value(maximum_value) {} + +std::uint64_t PseudoRandomNumberGenerator::generateCryptographicallyInsecureSeed() { + auto current_time = std::chrono::system_clock::now(); + auto current_time_duration = current_time.time_since_epoch(); // convert a bare time to a duration + auto time_as_milliseconds = std::chrono::duration_cast(current_time_duration); // get the duration as a value in milliseconds + uint64_t seed_from_milliseconds = time_as_milliseconds.count(); // convert the milliseconds to a bare integer and use that as our seed + return seed_from_milliseconds; +}; + +double PseudoRandomNumberGenerator::generateFloatingPointRandomValue(float min, float max) { + double random_val = generateUnitNormalRandomValue(); + double scaled_value = min + (random_val * (max - min)); + return scaled_value; +} + +int64_t PseudoRandomNumberGenerator::generateIntegerRandomValue(int32_t min, int32_t max) { + double random_val = generateUnitNormalRandomValue(); + int64_t inclusive_range = max - min + 1; + double random_val_magnitude = random_val * static_cast(inclusive_range); + int64_t scaled_value = static_cast(random_val_magnitude) + min; + return scaled_value; +} + +// Generate a random value normalized to the range [0, 1) +double PseudoRandomNumberGenerator::generateUnitNormalRandomValue() { + + uint64_t random_value = generateRandomValue(); + + uint64_t range = m_maximum_value - m_minimum_value; + if (range == 0) { + return 0.0; // Avoid division by zero + } + + // Normalize the random value to [0, 1) + double normalized_value = static_cast(random_value - m_minimum_value) / static_cast(range); + return normalized_value; +} diff --git a/src/prngs/xorshift.cpp b/src/prngs/xorshift.cpp new file mode 100644 index 0000000..ce3a3f7 --- /dev/null +++ b/src/prngs/xorshift.cpp @@ -0,0 +1,38 @@ +#include +#include "xorshift.hpp" +#include "prng.hpp" + + +// Default constructor +XORShift::XORShift() + : PseudoRandomNumberGenerator(), + m_a(DefaultA), + m_b(DefaultB), + m_c(DefaultC), + m_current_value(m_seed) {} + +XORShift::XORShift(const uint64_t seed) + : PseudoRandomNumberGenerator(seed), + m_a(DefaultA), + m_b(DefaultB), + m_c(DefaultC), + m_current_value(m_seed) {} + +XORShift::XORShift(const uint64_t seed, const uint64_t a, const uint64_t b, const uint64_t c) + : PseudoRandomNumberGenerator(seed), + m_a(a), + m_b(b), + m_c(c), + m_current_value(m_seed) {} + + + +// Generate a random value anywhere in the range of the XORShift algorithm +uint64_t XORShift::generateRandomValue() { + // Compute the standard LCG formula for the next value + m_current_value ^= (m_current_value << m_a); + m_current_value ^= (m_current_value >> m_b); + m_current_value ^= (m_current_value << m_c); + + return m_current_value; +} diff --git a/src/runner.cpp b/src/runner.cpp new file mode 100644 index 0000000..fa13db5 --- /dev/null +++ b/src/runner.cpp @@ -0,0 +1,374 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "program_runner.hpp" +#include "prng.hpp" +#include "xorshift.hpp" +#include "linear_congruential_generator.hpp" +#include "mersenne_twister.hpp" + + +std::string ProgramRunner::error_string() { + std::string error_string = program_name + ": bad usage\n" + + "Try '" + program_name + " --help' for more information."; + return error_string; +} + +std::string ProgramRunner::help_string() { + std::string help_string = "Usage: " + program_name + " [OPTIONS]\n\n" + + "Options:\n" + + " -a ALGORITHM, --algorithm=ALGORITHM Specify the algorithm [default: xorshift]\n" + + " -c AMOUNT, --count=AMOUNT Amount of random numbers to generate [default: 1]\n" + + " -M NUMBER, --max=NUMBER Maximum value\n" + + " -m NUMBER, --min=NUMBER Minimum value\n" + + " -t TYPE, --type=TYPE Specify the output type [default: int]\n" + + " -h, --help Display this help and exit\n" + + " -v, --version Output version information and exit"; + return help_string; +} + +std::string ProgramRunner::version_string() { + std::string version_string = program_name + " " + version; + return version_string; +} + +template +concept FromCharsParsable = std::is_integral_v || std::is_floating_point_v; + + +// Helper function for parsing a string to a numeric type using std::from_chars +template +std::optional parse_value(const std::string& str) { + T value; + auto result = std::from_chars(str.data(), str.data() + str.size(), value); + if (result.ec != std::errc() || result.ptr != str.data() + str.size()) { + return std::nullopt; + } + return value; +} + + +template +std::optional> parse_min_and_max_numbers(const std::string &min_str, const std::string &max_str) { + std::optional min_value = parse_value(min_str); + std::optional max_value = parse_value(max_str); + if (!min_value.has_value() || !max_value.has_value()) return std::nullopt; + if (min_value.value() <= max_value.value()) return std::make_pair(min_value.value(), max_value.value()); + return std::nullopt; +} + + +void ProgramRunner::determine_generation_range_configuration(const std::optional &min_str, const std::optional &max_str){ + bool both_max_and_min_specified = min_str.has_value() && max_str.has_value(); + bool neither_max_or_min_specified = !min_str.has_value() && !max_str.has_value(); + if (both_max_and_min_specified && this->behaviour == ProgramBehaviour::GenerateInteger){ + auto min_and_max = parse_min_and_max_numbers(min_str.value(), max_str.value()); + if (!min_and_max.has_value()){ + this->behaviour = ProgramBehaviour::Error; + return; + } + this->min = min_and_max.value().first; + this->max = min_and_max.value().second; + } else if (both_max_and_min_specified && this->behaviour == ProgramBehaviour::GenerateFloating){ + auto min_and_max = parse_min_and_max_numbers(min_str.value(), max_str.value()); + if (!min_and_max.has_value()){ + this->behaviour = ProgramBehaviour::Error; + return; + } + this->min = min_and_max.value().first; + this->max = min_and_max.value().second; + } else if (neither_max_or_min_specified && this->behaviour == ProgramBehaviour::GenerateUnitNormal){ + return; + } else { + this->behaviour = ProgramBehaviour::Error; + return; + } +} + +void ProgramRunner::determine_user_message_configuration(const bool error, const bool show_version, const bool show_help){ + if (error){ + this->behaviour = ProgramBehaviour::Error; + return; + } else if (show_version){ + this->behaviour = ProgramBehaviour::Version; + return; + } else if (show_help){ + this->behaviour = ProgramBehaviour::Help; + return; + } +} + +void ProgramRunner::determine_algorithm_configuration(const std::optional &alg_str){ + if (!alg_str.has_value()){ + this->algorithm = Algorithm::XORShift; // default algorithm to use + } else if (algorithm_choices.contains(alg_str.value())) { // TODO: this check/get can be made more efficient using find + this->algorithm = algorithm_choices.at(alg_str.value()); + } else { + this->behaviour = ProgramBehaviour::Error; + } +} + +void ProgramRunner::determine_generation_type_configuration(const std::optional &generation_type){ + if (!generation_type.has_value()){ + this->behaviour = DefaultGenerationType; + } else if (generation_types.contains(generation_type.value())){ + this->behaviour = generation_types.at(generation_type.value()); + } else { + this->behaviour = ProgramBehaviour::Error; + } +} + +void ProgramRunner::determine_count_configuration(const std::optional &count_str){ + if (count_str.has_value()){ + std::optional count_val = parse_value(count_str.value()); + if (!count_val.has_value() || count_val.value() <= 0){ + this->behaviour = ProgramBehaviour::Error; + return; + } + this->count = count_val; + } else { + this->count = DefaultCount; + } +} + + +void ProgramRunner::determine_program_configuration(const ProgramRunner::RawArguments &raw_args){ + + determine_user_message_configuration(raw_args.error, raw_args.show_version, raw_args.show_help); + auto &behave = this->behaviour; + if (behave.has_value() && (behave == ProgramBehaviour::Error || behave == ProgramBehaviour::Version || behave == ProgramBehaviour::Help)){ + return; + } + + determine_algorithm_configuration(raw_args.algorithm_str); + if (behave.has_value() && behave == ProgramBehaviour::Error){ + return; + } + + determine_generation_type_configuration(raw_args.type); + if (behave.has_value() && behave == ProgramBehaviour::Error){ + return; + } + + determine_count_configuration(raw_args.count_str); + if (behave.has_value() && behave == ProgramBehaviour::Error){ + return; + } + + determine_generation_range_configuration(raw_args.min_str, raw_args.max_str); + + return; +} + +ProgramRunner::RawArguments ProgramRunner::parse_args(int argc, char **argv) { + const char* const short_opts = "+hva:m:M:c:t:"; + const ::option long_opts[] = { + {"help", no_argument, nullptr, 'h'}, + {"version", no_argument, nullptr, 'v'}, + {"algorithm", required_argument, nullptr, 'a'}, + {"min", required_argument, nullptr, 'm'}, + {"max", required_argument, nullptr, 'M'}, + {"count", required_argument, nullptr, 'c'}, + {"type", required_argument, nullptr, 't'}, + {nullptr, 0, nullptr, 0} + }; + + RawArguments raw_arguments; + + raw_arguments.error = false; + raw_arguments.show_help = false; + raw_arguments.show_version = false; + raw_arguments.algorithm_str = std::nullopt; + raw_arguments.min_str = std::nullopt; + raw_arguments.max_str = std::nullopt; + raw_arguments.count_str = std::nullopt; + raw_arguments.type = std::nullopt; + + while (true) { + const auto opt = getopt_long(argc, argv, short_opts, long_opts, nullptr); + + if (opt == -1) + break; + + switch (opt) { + case 'h': + raw_arguments.show_help = true; + break; + case 'v': + raw_arguments.show_version = true; + break; + case 'a': + raw_arguments.algorithm_str = std::string(optarg); + break; + case 'm': + raw_arguments.min_str = std::string(optarg); + break; + case 'M': + raw_arguments.max_str = std::string(optarg); + break; + case 'c': + raw_arguments.count_str = std::string(optarg); + break; + case 't': + raw_arguments.type = std::string(optarg); + break; + case '?': // Unrecognized option + raw_arguments.error = true; + return raw_arguments; + default: + raw_arguments.error = true; + return raw_arguments; + } + } + + return raw_arguments; +} + +void ProgramRunner::create_prng() { + + if (!this->algorithm.has_value()) { + throw std::runtime_error("Algorithm not set, cannot create PseudoRandomNumberGenerator instance"); + } + + switch (this->algorithm.value()) { + case ProgramRunner::Algorithm::XORShift: + this->prng = std::make_unique(); + break; + case ProgramRunner::Algorithm::LinearCongruentialGenerator: + this->prng = std::make_unique(); + break; + case ProgramRunner::Algorithm::MersenneTwister: + this->prng = std::make_unique(); + break; + default: + throw std::runtime_error("Unsupported algorithm"); + } + + if (this->prng == nullptr) { + throw std::runtime_error("Failed to create PseudoRandomNumberGenerator instance"); + } + + return; +} + +ProgramRunner::ProgramRunner(int argc, char **argv, int warmup_iterations){ + RawArguments raw_arguments = parse_args(argc, argv); + determine_program_configuration(raw_arguments); + if (this->algorithm.has_value()) { + create_prng(); + warmup(warmup_iterations); + } + +} + +bool ProgramRunner::is_finished() { + if (this->finished) { + return true; + } else if ((this->count.has_value()) && (this->iteration >= this->count)) { + this->finished = true; + return true; + } + + return false; +} + +ProgramRunner::ProgramStatus ProgramRunner::iterate() { + + if (is_finished()) { + throw std::runtime_error("ProgramRunner has finished, cannot iterate further"); + } + + if (!this->behaviour.has_value()) { + throw std::runtime_error("ProgramRunner not configured properly"); + } + + + switch (this->behaviour.value()) { + + case ProgramBehaviour::Error: + this->finished = true; + return {std::nullopt, error_string(), ProgramRunner::ExitCodeError}; + + case ProgramBehaviour::Help: + this->finished = true; + return {help_string(), std::nullopt, ProgramRunner::ExitCodeSuccess}; + + case ProgramBehaviour::Version: + this->finished = true; + return {version_string(), std::nullopt, ProgramRunner::ExitCodeSuccess}; + + case ProgramBehaviour::GenerateUnitNormal: { + this->iteration++; + double random_value = this->prng->generateUnitNormalRandomValue(); + auto exit_code_based_on_count = is_finished() ? std::optional(ProgramRunner::ExitCodeSuccess) : std::nullopt; + return {std::to_string(random_value), std::nullopt, exit_code_based_on_count}; + } + + case ProgramBehaviour::GenerateFloating: { + this->iteration++; + double random_float = this->prng->generateFloatingPointRandomValue( + std::get(this->min.value_or(0.0f)), + std::get(this->max.value_or(1.0f)) + ); + auto exit_code_based_on_count = is_finished() ? std::optional(ProgramRunner::ExitCodeSuccess) : std::nullopt; + return {std::to_string(random_float), std::nullopt, exit_code_based_on_count}; + } + + case ProgramBehaviour::GenerateInteger: { + this->iteration++; + int64_t random_int = this->prng->generateIntegerRandomValue( + std::get(this->min.value_or(0)), + std::get(this->max.value_or(1)) + ); + auto exit_code_based_on_count = is_finished() ? std::optional(ProgramRunner::ExitCodeSuccess) : std::nullopt; + return {std::to_string(random_int), std::nullopt, exit_code_based_on_count}; + } + + default: + throw std::runtime_error("ProgramRunner not configured properly, behaviour is not set"); + } + + return {std::nullopt, std::nullopt, ProgramRunner::ExitCodeError}; // Should never reach here +} + +ProgramRunner::ProgramStatus ProgramRunner::iterate(uint64_t iterations) { + if (iterations == 0) { + throw std::invalid_argument("Number of iterations must be greater than zero"); + } + + ProgramStatus last_status; + for (uint64_t i = 0; i < iterations; ++i) { + last_status = iterate(); + if (last_status.exit_code.has_value()) { + return last_status; // Return immediately if an exit code is present + } + } + return last_status; // Return the status of the last iteration +} + +ProgramRunner::ProgramStatus ProgramRunner::run() { + while (!is_finished()) { + auto status = iterate(); + if (status.exit_code.has_value()) { + return status; + } + } + throw std::runtime_error("ProgramRunner has finished without returning an exit code. This should not happen."); +} + +void ProgramRunner::warmup(uint64_t iterations) { + if (this->prng == nullptr) { + throw std::runtime_error("PRNG is not initialized, cannot warmup"); + } + + for (uint64_t i = 0; i < iterations; ++i) { + this->prng->generateRandomValue(); // Warmup the PRNG + } +} + + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..e5deeca --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.22) + + +# Download and unpack GoogleTest if not present +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/release-1.12.1.zip +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + +# Prevent GoogleTest from being installed by CPack +set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) +set(INSTALL_GMOCK OFF CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(googletest) + +# Collect test sources +file(GLOB_RECURSE TEST_SOURCES "*.cpp") + +# Add test executable +add_executable(RandomizerTests ${TEST_SOURCES}) + +# Link to GoogleTest and the library for the randomizer code +target_link_libraries(RandomizerTests PRIVATE gtest gtest_main randomizer_lib) + +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(RandomizerTests PRIVATE + -Wall + -Wextra + -Wshadow + -Wconversion + -Wpedantic + -Werror + ) +endif() + +include(GoogleTest) +gtest_discover_tests(RandomizerTests) diff --git a/tests/test_LinearCongruentialGenerator.cpp b/tests/test_LinearCongruentialGenerator.cpp new file mode 100644 index 0000000..7911e1d --- /dev/null +++ b/tests/test_LinearCongruentialGenerator.cpp @@ -0,0 +1,201 @@ +#include "gtest/gtest.h" +#include "prng.hpp" +#include "linear_congruential_generator.hpp" +#include +#include +#include + +// Test that the lcg constructors do not fail +TEST(TestLinearCongruentialGenerator, BlankConstructor) { + EXPECT_NO_THROW(LinearCongruentialGenerator()); +} + +TEST(TestLinearCongruentialGenerator, SeedConstructor) { + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::min())); + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::min() + 1)); + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::max() / 10)); + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::max() / 2)); + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::max() - 1)); + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::max())); +} + +TEST(TestLinearCongruentialGenerator, CustomConstructor) { + EXPECT_NO_THROW(LinearCongruentialGenerator(0x7FFFFFFF, 1103515245, 12345, 0x7FFFFFFF)); + EXPECT_NO_THROW(LinearCongruentialGenerator(60, 3453983, 897, 0xFFFFFFFF)); + EXPECT_NO_THROW(LinearCongruentialGenerator(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF)); + EXPECT_NO_THROW(LinearCongruentialGenerator(1, std::numeric_limits::max() / 2, -50, 0xFFF)); +} + +TEST(TestLinearCongruentialGenerator, SeedAndCustomConstructor) { + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::min(), 0x7FFFFFFF, 1103515245, 12345, 0x7FFFFFFF)); + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::max(), 60, 3453983, 897, 0xFFFFFFFF)); + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::max() / 2, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF)); + EXPECT_NO_THROW(LinearCongruentialGenerator(std::numeric_limits::max() - 1, 1, std::numeric_limits::max() / 2, 58678575, 0xFFFFFF)); +} + +TEST(TestLinearCongruentialGenerator, GenerateUnitNormalRandomValueHasCorrectRange) { + LinearCongruentialGenerator lcg = LinearCongruentialGenerator(); + + const int enough_iterations_to_be_confident = 10000; + for (int i = 0; i < enough_iterations_to_be_confident; ++i) { + const double value = lcg.generateUnitNormalRandomValue(); + EXPECT_GE(value, 0.0); + EXPECT_LE(value, 1.0); + } +} + + +/* This test of course is subject to some randomness +it is possible to fail even if everything is working +properly, but it would be incredibly unlikely to fail. +If this test ever fails it should be investigated further +manually. */ +TEST(TestLinearCongruentialGenerator, GenerateUnitNormalRandomValueEquidistributed) { + LinearCongruentialGenerator lcg = LinearCongruentialGenerator(); + const double expected_average = 0.5; + double sum = 0.0; + const int enough_iterations_to_be_confident = 100000; + for (int i = 0; i < enough_iterations_to_be_confident; ++i) { + sum += lcg.generateUnitNormalRandomValue(); + } + const double average = sum / enough_iterations_to_be_confident; + const double confidence_interval = 0.01; + EXPECT_NEAR(average, expected_average, confidence_interval); +} + + +TEST(TestLinearCongruentialGenerator, GenerateFloatingPointRandomValueInRange){ + LinearCongruentialGenerator lcg = LinearCongruentialGenerator(); + const float min = -25.678f; + const float max = 3242.342f; + const int num_iterations = 1000; + for (int i = 0; i < num_iterations; ++i){ + double value = lcg.generateFloatingPointRandomValue(min, max); + ASSERT_GE(value, min); + ASSERT_LE(value, max); + } +} + +TEST(TestLinearCongruentialGenerator, GenerateFloatingPointRandomValueEquidistributed){ + LinearCongruentialGenerator lcg = LinearCongruentialGenerator(); + const double min = -300.0; + const double max = 700.0; + const int num_iterations = 10000; + double sum = 0.0; + double variance_sum = 0.0; + double expected_value = (min + max) / 2.0; + for (int i = 0; i < num_iterations; ++i){ + double value = lcg.generateFloatingPointRandomValue(min, max); + sum += value; + variance_sum += (value - expected_value) * (value - expected_value); + } + double average = sum / num_iterations; + double variance = variance_sum / num_iterations; + double standard_deviation = std::sqrt(variance); + double three_sigma = 3 * standard_deviation; + ASSERT_NEAR(expected_value, average, three_sigma);; + +} + +TEST(TestLinearCongruentialGenerator, GenerateIntegerRandomValueInRange){ + LinearCongruentialGenerator lcg = LinearCongruentialGenerator(); + const int32_t min = -70; + const int32_t max = 141; + const int num_iterations = 1000; + for (int i = 0; i < num_iterations; ++i){ + int64_t value = lcg.generateIntegerRandomValue(min, max); + ASSERT_GE(value, min); + ASSERT_LE(value, max); + } +} + +TEST(TestLinearCongruentialGenerator, GenerateIntegerRandomValueEquidistributed){ + LinearCongruentialGenerator lcg = LinearCongruentialGenerator(); + const int32_t min = 1; + const int32_t max = 6; + const int num_iterations = 10000; + double sum = 0; + double expected_value = (min + max) / 2.0; + for (int i = 0; i < num_iterations; ++i){ + sum += static_cast(lcg.generateIntegerRandomValue(min, max)); + } + double average = sum / num_iterations; + double margin_of_error = 0.02 * expected_value; + ASSERT_NEAR(expected_value, average, margin_of_error); +} + + +TEST(TestLinearCongruentialGenerator, GenerateIntegerRandomValueProducesInclusiveRangePositive){ + LinearCongruentialGenerator lcg = LinearCongruentialGenerator(); + const int32_t min = 1; + const int32_t max = 6; + const int max_iterations = 1000; + bool found_min = false; + bool found_max = false; + + for (int i = 0; i < max_iterations; ++i) { + int64_t value = lcg.generateIntegerRandomValue(min, max); + if (value == min) { + found_min = true; + } + if (value == max) { + found_max = true; + } + if (found_min && found_max) { + break; // No need to continue if both values are found + } + } + + ASSERT_TRUE(found_min); + ASSERT_TRUE(found_max); +} + +TEST(TestLinearCongruentialGenerator, GenerateIntegerRandomValueProducesInclusiveRangeNegative){ + LinearCongruentialGenerator lcg = LinearCongruentialGenerator(); + const int32_t min = -6; + const int32_t max = -1; + const int max_iterations = 1000; + bool found_min = false; + bool found_max = false; + + for (int i = 0; i < max_iterations; ++i) { + int64_t value = lcg.generateIntegerRandomValue(min, max); + if (value == min) { + found_min = true; + } + if (value == max) { + found_max = true; + } + if (found_min && found_max) { + break; // No need to continue if both values are found + } + } + + ASSERT_TRUE(found_min); + ASSERT_TRUE(found_max); +} + +TEST(TestLinearCongruentialGenerator, GenerateIntegerRandomValueProducesInclusiveRangeNegativeAndPositive){ + LinearCongruentialGenerator lcg = LinearCongruentialGenerator(); + const int32_t min = -10; + const int32_t max = 10; + const int max_iterations = 1000; + bool found_min = false; + bool found_max = false; + + for (int i = 0; i < max_iterations; ++i) { + int64_t value = lcg.generateIntegerRandomValue(min, max); + if (value == min) { + found_min = true; + } + if (value == max) { + found_max = true; + } + if (found_min && found_max) { + break; // No need to continue if both values are found + } + } + + ASSERT_TRUE(found_min); + ASSERT_TRUE(found_max); +} \ No newline at end of file diff --git a/tests/test_MersenneTwister.cpp b/tests/test_MersenneTwister.cpp new file mode 100644 index 0000000..4f5b69b --- /dev/null +++ b/tests/test_MersenneTwister.cpp @@ -0,0 +1,187 @@ +#include "gtest/gtest.h" +#include "prng.hpp" +#include "mersenne_twister.hpp" +#include +#include +#include + +// Test that the mt constructors do not fail +TEST(TestMersenneTwister, BlankConstructor) { + EXPECT_NO_THROW(MersenneTwister()); +} + +TEST(TestMersenneTwister, SeedConstructor) { + EXPECT_NO_THROW(MersenneTwister(std::numeric_limits::min())); + EXPECT_NO_THROW(MersenneTwister(std::numeric_limits::min() + 1)); + EXPECT_NO_THROW(MersenneTwister(std::numeric_limits::max() / 10)); + EXPECT_NO_THROW(MersenneTwister(std::numeric_limits::max() / 2)); + EXPECT_NO_THROW(MersenneTwister(std::numeric_limits::max() - 1)); + EXPECT_NO_THROW(MersenneTwister(std::numeric_limits::max())); +} + +TEST(TestMersenneTwister, GenerateUnitNormalRandomValueHasCorrectRange) { + MersenneTwister mt = MersenneTwister(); + + const int enough_iterations_to_be_confident = 10000; + for (int i = 0; i < enough_iterations_to_be_confident; ++i) { + const double value = mt.generateUnitNormalRandomValue(); + EXPECT_GE(value, 0.0); + EXPECT_LE(value, 1.0); + } +} + + +/* This test of course is subject to some randomness +it is possible to fail even if everything is working +properly, but it would be incredibly unlikely to fail. +If this test ever fails it should be investigated further +manually. */ +TEST(TestMersenneTwister, GenerateUnitNormalRandomValueEquidistributed) { + MersenneTwister mt = MersenneTwister(); + const double expected_average = 0.5; + double sum = 0.0; + const int enough_iterations_to_be_confident = 100000; + for (int i = 0; i < enough_iterations_to_be_confident; ++i) { + sum += mt.generateUnitNormalRandomValue(); + } + const double average = sum / enough_iterations_to_be_confident; + const double confidence_interval = 0.01; + EXPECT_NEAR(average, expected_average, confidence_interval); +} + + +TEST(TestMersenneTwister, GenerateFloatingPointRandomValueInRange){ + MersenneTwister mt = MersenneTwister(); + const float min = -25.678f; + const float max = 3242.342f; + const int num_iterations = 1000; + for (int i = 0; i < num_iterations; ++i){ + double value = mt.generateFloatingPointRandomValue(min, max); + ASSERT_GE(value, min); + ASSERT_LE(value, max); + } +} + +TEST(TestMersenneTwister, GenerateFloatingPointRandomValueEquidistributed){ + MersenneTwister mt = MersenneTwister(); + const double min = -300.0; + const double max = 700.0; + const int num_iterations = 10000; + double sum = 0.0; + double variance_sum = 0.0; + double expected_value = (min + max) / 2.0; + for (int i = 0; i < num_iterations; ++i){ + double value = mt.generateFloatingPointRandomValue(min, max); + sum += value; + variance_sum += (value - expected_value) * (value - expected_value); + } + double average = sum / num_iterations; + double variance = variance_sum / num_iterations; + double standard_deviation = std::sqrt(variance); + double three_sigma = 3 * standard_deviation; + ASSERT_NEAR(expected_value, average, three_sigma);; + +} + +TEST(TestMersenneTwister, GenerateIntegerRandomValueInRange){ + MersenneTwister mt = MersenneTwister(); + const int32_t min = -70; + const int32_t max = 141; + const int num_iterations = 1000; + for (int i = 0; i < num_iterations; ++i){ + int64_t value = mt.generateIntegerRandomValue(min, max); + ASSERT_GE(value, min); + ASSERT_LE(value, max); + } +} + +TEST(TestMersenneTwister, GenerateIntegerRandomValueEquidistributed){ + MersenneTwister mt = MersenneTwister(); + const int32_t min = 1; + const int32_t max = 6; + const int num_iterations = 10000; + double sum = 0; + double expected_value = (min + max) / 2.0; + for (int i = 0; i < num_iterations; ++i){ + sum += static_cast(mt.generateIntegerRandomValue(min, max)); + } + double average = sum / num_iterations; + double margin_of_error = 0.02 * expected_value; + ASSERT_NEAR(expected_value, average, margin_of_error); +} + + +TEST(TestMersenneTwister, GenerateIntegerRandomValueProducesInclusiveRangePositive){ + MersenneTwister mt = MersenneTwister(); + const int32_t min = 1; + const int32_t max = 6; + const int max_iterations = 1000; + bool found_min = false; + bool found_max = false; + + for (int i = 0; i < max_iterations; ++i) { + int64_t value = mt.generateIntegerRandomValue(min, max); + if (value == min) { + found_min = true; + } + if (value == max) { + found_max = true; + } + if (found_min && found_max) { + break; // No need to continue if both values are found + } + } + + ASSERT_TRUE(found_min); + ASSERT_TRUE(found_max); +} + +TEST(TestMersenneTwister, GenerateIntegerRandomValueProducesInclusiveRangeNegative){ + MersenneTwister mt = MersenneTwister(); + const int32_t min = -6; + const int32_t max = -1; + const int max_iterations = 1000; + bool found_min = false; + bool found_max = false; + + for (int i = 0; i < max_iterations; ++i) { + int64_t value = mt.generateIntegerRandomValue(min, max); + if (value == min) { + found_min = true; + } + if (value == max) { + found_max = true; + } + if (found_min && found_max) { + break; // No need to continue if both values are found + } + } + + ASSERT_TRUE(found_min); + ASSERT_TRUE(found_max); +} + +TEST(TestMersenneTwister, GenerateIntegerRandomValueProducesInclusiveRangeNegativeAndPositive){ + MersenneTwister mt = MersenneTwister(); + const int32_t min = -10; + const int32_t max = 10; + const int max_iterations = 1000; + bool found_min = false; + bool found_max = false; + + for (int i = 0; i < max_iterations; ++i) { + int64_t value = mt.generateIntegerRandomValue(min, max); + if (value == min) { + found_min = true; + } + if (value == max) { + found_max = true; + } + if (found_min && found_max) { + break; // No need to continue if both values are found + } + } + + ASSERT_TRUE(found_min); + ASSERT_TRUE(found_max); +} \ No newline at end of file diff --git a/tests/test_ProgramRunner.cpp b/tests/test_ProgramRunner.cpp new file mode 100644 index 0000000..1e9a2a5 --- /dev/null +++ b/tests/test_ProgramRunner.cpp @@ -0,0 +1,357 @@ +#include +#include "program_runner.hpp" + +constexpr const char* highest_i32_plus_one = "2147483648"; +constexpr const char* lowest_i32_minus_one = "-2147483649"; +constexpr const char* highest_u32_plus_one = "4294967296"; +constexpr const char* highest_float_plus_more = "3.402823467e+38"; +constexpr const char* lowest_float_minus_more = "-3.402823467e+38"; + +struct ArgvBuilder { + std::vector args; + std::vector argv; + + ArgvBuilder(std::initializer_list init) : args(init) { + std::string program_name = "randomizer"; + argv.push_back(const_cast(program_name.c_str())); + for (auto& s : args) { + argv.push_back(const_cast(s.c_str())); + } + argv.push_back(nullptr); // argv must be null-terminated + } + + int argc() const { return static_cast(argv.size() - 1); } + char** argv_ptr() { return argv.data(); } +}; + +TEST(TestProgramRunner, NoArgs){ + ArgvBuilder builder({}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code, 0); +} + +TEST(TestProgramRunner, JustHelp){ + ArgvBuilder builder({"--help"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code, 0); +} + +TEST(TestProgramRunner, JustVersion){ + ArgvBuilder builder({"--version"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code, 0); +} + +TEST(TestProgramRunner, HelpThenVersion){ + ArgvBuilder builder({"--help", "--version"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, VersionThenHelp){ + ArgvBuilder builder({"--version", "--help"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, HelpThenOther){ + ArgvBuilder builder({"--help", "other", "stuff", "which", "is", "ignored"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, VersionThenOther){ + ArgvBuilder builder({"--version", "ignore", "this", "stuff", "please", "and", "thank", "you"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, BadSubcommands){ + ArgvBuilder builder({"badsubcommand", "--help"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, DefaultAlgorithm){ + ArgvBuilder builder({"--min", "0", "--max=10", "-c", "5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, ExplicitAlgorithmXOR){ + ArgvBuilder builder({"--algorithm", "xor", "-m", "0", "-M10", "--count", "5", "-t", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, ExplicitAlgorithmLCG){ + ArgvBuilder builder({"--algorithm=linear-congruential-generator", "-m0", "-M", "10", "--count=5", "-tint"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, ExplicitAlgorithmMersenne){ + ArgvBuilder builder({"-a", "mersenne", "-m", "0", "-M", "10", "--count=5", "--type=int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, ExplicitAlgorithmInvalid){ + ArgvBuilder builder({"--algorithm", "invalid-algorithm", "--min", "0", "-M" "10", "--count", "5", "-t", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, NoMinOrMax){ + ArgvBuilder builder({"--count", "5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, MinNoMax){ + ArgvBuilder builder({"--min", "0", "--count", "5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, MaxNoMin){ + ArgvBuilder builder({"-M", "11.1", "--count", "5", "--type", "float"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, MinAndMax){ + ArgvBuilder builder({"--min", "0", "--max", "6", "--count", "5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, MinExcessiveNegativeMagnitude){ + ArgvBuilder builder({"--min", lowest_i32_minus_one, "--max", "10", "--count", "5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, MinExcessivePositiveMagnitude){ + ArgvBuilder builder({"--min", highest_i32_plus_one, "--max", "10", "--count", "5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, MinAndMaxExcessiveNegativeMagnitude){ + ArgvBuilder builder({"--min", lowest_i32_minus_one, "--max", lowest_i32_minus_one, "--count", "5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, MinAndMaxExcessivePositiveMagnitude){ + ArgvBuilder builder({"--min", highest_i32_plus_one, "--max", highest_i32_plus_one, "--count", "5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, DefaultCount){ + ArgvBuilder builder({"--min", "0", "--max", "10", "--type", "floating"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.iterate(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, ExplicitCount){ + int explicit_count = 34539; // Arbitrary positive count for testing + int all_but_last = explicit_count - 1; + std::string explicit_count_str = std::to_string(explicit_count); + ArgvBuilder builder({"--min", "0", "--max", "10", "--count", explicit_count_str, "--type", "integer"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + for (int i = 0; i < all_but_last; ++i) { + ProgramRunner::ProgramStatus status = program_runner.iterate(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_FALSE(status.exit_code.has_value()); + } + + ProgramRunner::ProgramStatus status = program_runner.iterate(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, CountIsZero){ + ArgvBuilder builder({"--min", "0", "--max", "10", "--count", "0", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.iterate(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, CountIsOne){ + ArgvBuilder builder({"--count", "1", "--type", "unit"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.iterate(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, CountIsNegative){ + ArgvBuilder builder({"--min", "0", "--max", "10", "--count", "-5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.iterate(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, CountExcessivePositiveMagnitude){ + ArgvBuilder builder({"--min", "0", "--max", "10", "--count", highest_u32_plus_one, "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.iterate(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, DefaultType){ + ArgvBuilder builder({"-m0", "-M1", "--count", "5", "--algorithm", "xorshift"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, ExplicitTypeUnit){ + ArgvBuilder builder({"--count", "5", "--type", "unit"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, ExplicitTypeFloat){ + ArgvBuilder builder({"--min", "-4.3", "--max", "10.5", "--count", "5", "--type", "float"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, ExplicitTypeInt){ + ArgvBuilder builder({"--min", "0", "--max", "10", "--count", "5", "--type", "int"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_FALSE(status.stderr_message.has_value()); + ASSERT_TRUE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_EQ(status.exit_code.value(), 0); +} + +TEST(TestProgramRunner, ExplicitTypeInvalid){ + ArgvBuilder builder({"--min", "0", "--max", "10", "--count", "5", "--type", "invalid"}); + ProgramRunner program_runner = ProgramRunner(builder.argc(), builder.argv_ptr()); + ProgramRunner::ProgramStatus status = program_runner.run(); + ASSERT_TRUE(status.stderr_message.has_value()); + ASSERT_FALSE(status.stdout_message.has_value()); + ASSERT_TRUE(status.exit_code.has_value()); + ASSERT_NE(status.exit_code.value(), 0); +} + + diff --git a/tests/test_XORShift.cpp b/tests/test_XORShift.cpp new file mode 100644 index 0000000..56488c4 --- /dev/null +++ b/tests/test_XORShift.cpp @@ -0,0 +1,201 @@ +#include "gtest/gtest.h" +#include "prng.hpp" +#include "xorshift.hpp" +#include +#include +#include + +// Test that the lcg constructors do not fail +TEST(TestXORShift, BlankConstructor) { + EXPECT_NO_THROW(XORShift()); +} + +TEST(TestXORShift, SeedConstructor) { + EXPECT_NO_THROW(XORShift(std::numeric_limits::min())); + EXPECT_NO_THROW(XORShift(std::numeric_limits::min() + 1)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::min() + 5)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::max() / 5)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::max() / 2)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::max() - 1)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::max())); +} + + +TEST(TestXORShift, SeedAndConstantsConstructor) { + EXPECT_NO_THROW(XORShift(std::numeric_limits::min(), 1, 2, 3)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::min() + 1, 79, 55555, std::numeric_limits::max())); + EXPECT_NO_THROW(XORShift(std::numeric_limits::min() + 5, 4545, 9876567, 9876786)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::max() / 5, 1432, 6876, 3)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::max() / 2, 42, 42, 42)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::max() - 1, 2345678987, 9876543, 123422)); + EXPECT_NO_THROW(XORShift(std::numeric_limits::max(), 345679875, 2, 0)); +} + + +TEST(TestXORShift, GenerateUnitNormalRandomValueHasCorrectRange) { + XORShift xor_shift = XORShift(); + + const int enough_iterations_to_be_confident = 1000; + for (int i = 0; i < enough_iterations_to_be_confident; ++i) { + const double value = xor_shift.generateUnitNormalRandomValue(); + EXPECT_GE(value, 0.0); + EXPECT_LE(value, 1.0); + } +} + + +/* This test of course is subject to some randomness +it is possible to fail even if everything is working +properly, but it would be incredibly unlikely to fail. +If this test ever fails it should be investigated further +manually. */ +TEST(TestXORShift, GenerateUnitNormalRandomValueEquidistributed) { + XORShift xor_shift = XORShift(); + const double expected_average = 0.5; + double sum = 0.0; + const int enough_iterations_to_be_confident = 100000; + for (int i = 0; i < enough_iterations_to_be_confident; ++i) { + sum += xor_shift.generateUnitNormalRandomValue(); + } + const double average = sum / enough_iterations_to_be_confident; + const double confidence_interval = 0.01; + EXPECT_NEAR(average, expected_average, confidence_interval); +} + + +TEST(TestXORShift, GenerateFloatingPointRandomValueInRange){ + XORShift xor_shift = XORShift(); + const float min = -25.678f; + const float max = 3242.342f; + const int num_iterations = 1000; + for (int i = 0; i < num_iterations; ++i){ + double value = xor_shift.generateFloatingPointRandomValue(min, max); + ASSERT_GE(value, min); + ASSERT_LE(value, max); + } +} + +TEST(TestXORShift, GenerateFloatingPointRandomValueEquidistributed){ + XORShift xor_shift = XORShift(); + const double min = -300.0; + const double max = 700.0; + const int num_iterations = 10000; + double sum = 0.0; + double variance_sum = 0.0; + double expected_value = (min + max) / 2.0; + for (int i = 0; i < num_iterations; ++i){ + double value = xor_shift.generateFloatingPointRandomValue(min, max); + sum += value; + variance_sum += (value - expected_value) * (value - expected_value); + } + double average = sum / num_iterations; + double variance = variance_sum / num_iterations; + double standard_deviation = std::sqrt(variance); + double three_sigma = 3 * standard_deviation; + ASSERT_NEAR(expected_value, average, three_sigma); + +} + +TEST(TestXORShift, GenerateIntegerRandomValueInRange){ + XORShift xor_shift = XORShift(); + const int32_t min = -70; + const int32_t max = 141; + const int num_iterations = 1000; + for (int i = 0; i < num_iterations; ++i){ + int64_t value = xor_shift.generateIntegerRandomValue(min, max); + ASSERT_GE(value, min); + ASSERT_LE(value, max); + } +} + +TEST(TestXORShift, GenerateIntegerRandomValueEquidistributed){ + XORShift xor_shift = XORShift(); + const int32_t min = 1; + const int32_t max = 6; + const int num_iterations = 10000; + double sum = 0.0; + double expected_value = (min + max) / 2.0; + for (int i = 0; i < num_iterations; ++i){ + sum += static_cast(xor_shift.generateIntegerRandomValue(min, max)); + } + double average = sum / num_iterations; + double margin_of_error = 0.02 * expected_value; + ASSERT_NEAR(expected_value, average, margin_of_error); +} + +TEST(TestXORShift, GenerateIntegerRandomValueProducesInclusiveRangePositive){ + XORShift xor_shift = XORShift(); + const int32_t min = 1; + const int32_t max = 6; + const int max_iterations = 1000; + bool found_min = false; + bool found_max = false; + + for (int i = 0; i < max_iterations; ++i) { + int64_t value = xor_shift.generateIntegerRandomValue(min, max); + if (value == min) { + found_min = true; + } + if (value == max) { + found_max = true; + } + if (found_min && found_max) { + break; // No need to continue if both values are found + } + } + + ASSERT_TRUE(found_min); + ASSERT_TRUE(found_max); +} + +TEST(TestXORShift, GenerateIntegerRandomValueProducesInclusiveRangeNegative){ + XORShift xor_shift = XORShift(); + const int32_t min = -6; + const int32_t max = -1; + const int max_iterations = 1000; + bool found_min = false; + bool found_max = false; + + for (int i = 0; i < max_iterations; ++i) { + int64_t value = xor_shift.generateIntegerRandomValue(min, max); + if (value == min) { + found_min = true; + } + if (value == max) { + found_max = true; + } + if (found_min && found_max) { + break; // No need to continue if both values are found + } + } + + ASSERT_TRUE(found_min); + ASSERT_TRUE(found_max); +} + +TEST(TestXORShift, GenerateIntegerRandomValueProducesInclusiveRangeNegativeAndPositive){ + XORShift xor_shift = XORShift(); + const int32_t min = -10; + const int32_t max = 10; + const int max_iterations = 1000; + bool found_min = false; + bool found_max = false; + + for (int i = 0; i < max_iterations; ++i) { + int64_t value = xor_shift.generateIntegerRandomValue(min, max); + if (value == min) { + found_min = true; + } + if (value == max) { + found_max = true; + } + if (found_min && found_max) { + break; // No need to continue if both values are found + } + } + + ASSERT_TRUE(found_min); + ASSERT_TRUE(found_max); +} + +