Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ jobs:
- name: Run tests
run: |
cd build
ctest -C Release --output-on-failure
ctest -C Release --output-on-failure --no-tests=error
15 changes: 13 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ project(salma VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

enable_testing()

# --- mo2-core (shared library) ---

find_package(pugixml CONFIG REQUIRED)
Expand All @@ -14,6 +16,7 @@ find_package(unofficial-bit7z CONFIG REQUIRED)
add_library(mo2-core SHARED
src/Utils.cpp
src/Logger.cpp
src/SecurityContext.cpp
src/FileOperations.cpp
src/ArchiveService.cpp
src/ModStructureDetector.cpp
Expand Down Expand Up @@ -69,6 +72,7 @@ add_executable(mo2-server
src/Mo2Helpers.cpp
src/ConfigService.cpp
src/MultipartHandler.cpp
src/SecurityMiddleware.cpp
src/StaticFileHandler.cpp
)

Expand All @@ -89,10 +93,12 @@ set_target_properties(mo2-server PROPERTIES
# --- salma_tests (Google Test) ---

find_package(GTest CONFIG REQUIRED)
include(GoogleTest)

add_executable(salma_tests
tests/utils_test.cpp
tests/fomod_inference_test.cpp
tests/security_context_test.cpp
)

target_link_libraries(salma_tests
Expand All @@ -105,6 +111,11 @@ set_target_properties(salma_tests PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)

gtest_discover_tests(salma_tests
DISCOVERY_MODE PRE_TEST
PROPERTIES TIMEOUT 60
)

# --- Copy test suite + scripts next to the exe ---

# The output dir varies by config: bin/Release, bin/Debug, etc.
Expand All @@ -114,7 +125,7 @@ set(OUTDIR "$<TARGET_FILE_DIR:mo2-server>")
add_custom_command(TARGET mo2-server POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/test.bat"
"${CMAKE_SOURCE_DIR}/test.py"
"${CMAKE_SOURCE_DIR}/test_all.py"
"${CMAKE_SOURCE_DIR}/test_one.py"
"${CMAKE_SOURCE_DIR}/deploy.bat"
"${CMAKE_SOURCE_DIR}/purge.bat"
Expand Down Expand Up @@ -220,7 +231,7 @@ install(DIRECTORY "${CMAKE_SOURCE_DIR}/scripts/"
REGEX "/__pycache__($|/)" EXCLUDE
PATTERN "*.pyc" EXCLUDE)
install(FILES
"${CMAKE_SOURCE_DIR}/test.py"
"${CMAKE_SOURCE_DIR}/test_all.py"
"${CMAKE_SOURCE_DIR}/test_one.py"
"${CMAKE_SOURCE_DIR}/test.bat"
"${CMAKE_SOURCE_DIR}/deploy.bat"
Expand Down
146 changes: 73 additions & 73 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ salma reads a small set of environment variables at runtime. None are required f
|------------------------|---------------------------------------------------------------------------------|--------------------------------------|
| `VCPKG_ROOT` | vcpkg toolchain path used by the default CMake preset. | `CMakePresets.json` |
| `SALMA_BIND_ADDR` | Override the default loopback bind. Non-loopback values log a security warning. | `main.cpp` |
| `SALMA_MODS_PATH` | MO2 mods directory. Used by the deploy fallback chain and the round-trip tests. | `Mo2Helpers.cpp`, `test.py` |
| `SALMA_MODS_PATH` | MO2 mods directory. Used by the deploy fallback chain and the round-trip tests. | `Mo2Helpers.cpp`, `test_all.py` |
| `SALMA_DEPLOY_PATH` | Override the `<MO2 instance>/MO2/plugins` deploy target used by `deploy.bat`. | `Mo2Helpers.cpp`, `deploy.bat` |
| `SALMA_DOWNLOADS_PATH` | Lookup root for `resolve_mod_archive` when `installationFile` is relative. | `FomodArchiveResolver.cpp` |

Expand All @@ -276,11 +276,11 @@ Builds the `salma_tests` target and runs `salma_tests.exe`. Sources live under `
### Round-trip integration tests (Python)

```powershell
python test.py # walks every mod under SALMA_MODS_PATH
python test_all.py # walks every mod under SALMA_MODS_PATH
python test_one.py <archive> <mod> # single-mod variant for debugging
```

`test.py` enumerates every mod folder under `SALMA_MODS_PATH`, infers FOMOD selections through the DLL, reinstalls the result into a temporary directory, and diffs the produced file tree against the original install. Output goes to `test.log` and `logs/salma.log`.
`test_all.py` enumerates every mod folder under `SALMA_MODS_PATH`, infers FOMOD selections through the DLL, reinstalls the result into a temporary directory, and diffs the produced file tree against the original install. Output goes to `test.log` and `logs/salma.log`.

Round-trip tests require the **three `SALMA_*` env vars** listed in [Configuration & Environment](#configuration--environment). Run `scripts\setup-env.bat` once to set them via `setx` (persists across shells); the test scripts no longer fall back to a hardcoded path and exit with a setup hint if any required variable is unset.

Expand Down Expand Up @@ -389,76 +389,76 @@ Both paths converge in `mo2-core`. The DLL exposes it as a flat C ABI; the serve

```
salma/
|-- .github/ # CI workflows
| +-- workflows/ # build.yml, sonar.yml, test.yml
|-- src/ # C++ source code
| |-- main.cpp # Crow HTTP server entry point
| |-- CApi.h/cpp # C-linkage DLL API (ctypes)
| |-- Export.h # MO2_API export macro
| |-- Types.h # Shared type definitions
| |-- Utils.h/cpp # Shared string/path helpers
| |-- BackgroundJob.h # Async job runner
| |-- Logger.h/cpp # Thread-safe logging
| |-- ArchiveService.h/cpp # Archive extraction
| |-- FileOperations.h/cpp # Queued file operations
| |-- ModStructureDetector.h/cpp # Mod folder structure detection
| |-- FomodArchiveResolver.h/cpp # Resolves mod source archive paths
| |-- FomodService.h/cpp # FOMOD installation logic
| |-- FomodDependencyEvaluator.h/cpp # FOMOD dependency evaluation
| |-- FomodInferenceService.h/cpp # Selection inference engine (orchestrator)
| |-- FomodIR.h # FOMOD IR types (header-only)
| |-- FomodIRParser.h/cpp # XML to IR parser
| |-- FomodCSP*.h/cpp # CSP solver, options, precompute, types
| |-- FomodPropagator.h/cpp # Constraint propagator
| |-- FomodForwardSimulator.h/cpp # Forward-simulates installs against the IR
| |-- FomodInferenceAtoms.h/cpp # Atom-level inference helpers
| |-- FomodAtom.h # Atom type definitions
| |-- InstallationService.h/cpp # Main orchestrator
| |-- InstallationController.h/cpp # REST endpoint handlers
| |-- Mo2Controller.h/cpp # MO2 dashboard controller (shared state)
| |-- Mo2...Controller.cpp # Per-subsystem endpoints
| |-- Mo2Helpers.h/cpp # Shared helpers for MO2 controllers
| |-- ConfigService.h/cpp # Configuration management
| |-- MultipartHandler.h/cpp # Form data parsing
| +-- StaticFileHandler.h/cpp # SPA serving
|-- web/ # React frontend
| |-- src/ # TypeScript source
| |-- dist/ # Built SPA (served by Crow)
| |-- package.json # Dependencies
| +-- vite.config.ts # Dev proxy to :5000
|-- tests/ # GoogleTest C++ unit tests
|-- scripts/ # MO2 plugin & utilities
| |-- mo2-salma.py # MO2 Python plugin
| |-- setup-env.bat # Persist SALMA_* env vars (one-time setup)
| |-- common.py # Shared utilities
| |-- install.py # Installation helper
| |-- compare.py # Round-trip diff utility
| |-- scan.py # Mod scanning utility
| |-- _clean_docs.py # Doc post-processing
| +-- _promote_subgroups.py # FOMOD subgroup promotion tool
|-- triplets/ # Custom vcpkg triplet (x64-windows-static-md)
|-- overrides/ # MkDocs theme overrides
|-- logs/ # Runtime logs
| +-- salma.log # Application log
|-- .clang-format # Code formatting rules
|-- .gitignore # Git ignore rules
|-- CMakeLists.txt # Build configuration (mo2-core + mo2-server targets)
|-- CMakePresets.json # Build presets (vcpkg)
|-- vcpkg.json # Dependency manifest
|-- sonar-project.properties # SonarCloud configuration
|-- build.bat # Build pipeline
|-- deploy.bat # Deploy to MO2
|-- purge.bat # Remove plugin & clean output
|-- test.bat # Run C++ unit tests (salma_tests.exe)
|-- test.py # Round-trip test runner (Python)
|-- test_one.py # Round-trip test for a single mod
|-- test.log # Round-trip test output
|-- fomod_archives.txt # Test archive manifest
|-- doxide.yml # API doc config
|-- mkdocs.yml # Documentation site config
|-- LICENSE # License
|-- CONTRIBUTING.md # Contributor guide
+-- PREVIEW.png # README banner image
|-- .github/ # CI workflows
| +-- workflows/ # build.yml, sonar.yml, test.yml
|-- src/ # C++ source code
| |-- main.cpp # Crow HTTP server entry point
| |-- CApi.h/cpp # C-linkage DLL API (ctypes)
| |-- Export.h # MO2_API export macro
| |-- Types.h # Shared type definitions
| |-- Utils.h/cpp # Shared string/path helpers
| |-- BackgroundJob.h # Async job runner
| |-- Logger.h/cpp # Thread-safe logging
| |-- ArchiveService.h/cpp # Archive extraction
| |-- FileOperations.h/cpp # Queued file operations
| |-- ModStructureDetector.h/cpp # Mod folder structure detection
| |-- FomodArchiveResolver.h/cpp # Resolves mod source archive paths
| |-- FomodService.h/cpp # FOMOD installation logic
| |-- FomodDependencyEvaluator.h/cpp # FOMOD dependency evaluation
| |-- FomodInferenceService.h/cpp # Selection inference engine (orchestrator)
| |-- FomodIR.h # FOMOD IR types (header-only)
| |-- FomodIRParser.h/cpp # XML to IR parser
| |-- FomodCSP*.h/cpp # CSP solver, options, precompute, types
| |-- FomodPropagator.h/cpp # Constraint propagator
| |-- FomodForwardSimulator.h/cpp # Forward-simulates installs against the IR
| |-- FomodInferenceAtoms.h/cpp # Atom-level inference helpers
| |-- FomodAtom.h # Atom type definitions
| |-- InstallationService.h/cpp # Main orchestrator
| |-- InstallationController.h/cpp # REST endpoint handlers
| |-- Mo2Controller.h/cpp # MO2 dashboard controller (shared state)
| |-- Mo2...Controller.cpp # Per-subsystem endpoints
| |-- Mo2Helpers.h/cpp # Shared helpers for MO2 controllers
| |-- ConfigService.h/cpp # Configuration management
| |-- MultipartHandler.h/cpp # Form data parsing
| +-- StaticFileHandler.h/cpp # SPA serving
|-- web/ # React frontend
| |-- src/ # TypeScript source
| |-- dist/ # Built SPA (served by Crow)
| |-- package.json # Dependencies
| +-- vite.config.ts # Dev proxy to :5000
|-- tests/ # GoogleTest C++ unit tests
|-- scripts/ # MO2 plugin & utilities
| |-- mo2-salma.py # MO2 Python plugin
| |-- setup-env.bat # Persist SALMA_* env vars (one-time setup)
| |-- common.py # Shared utilities
| |-- install.py # Installation helper
| |-- compare.py # Round-trip diff utility
| |-- scan.py # Mod scanning utility
| |-- _clean_docs.py # Doc post-processing
| +-- _promote_subgroups.py # FOMOD subgroup promotion tool
|-- triplets/ # Custom vcpkg triplet (x64-windows-static-md)
|-- overrides/ # MkDocs theme overrides
|-- logs/ # Runtime logs
| +-- salma.log # Application log
|-- .clang-format # Code formatting rules
|-- .gitignore # Git ignore rules
|-- CMakeLists.txt # Build configuration (mo2-core + mo2-server targets)
|-- CMakePresets.json # Build presets (vcpkg)
|-- vcpkg.json # Dependency manifest
|-- sonar-project.properties # SonarCloud configuration
|-- build.bat # Build pipeline
|-- deploy.bat # Deploy to MO2
|-- purge.bat # Remove plugin & clean output
|-- test.bat # Run C++ unit tests (salma_tests.exe)
|-- test_all.py # Round-trip test runner (Python)
|-- test_one.py # Round-trip test for a single mod
|-- test.log # Round-trip test output
|-- fomod_archives.txt # Test archive manifest
|-- doxide.yml # API doc config
|-- mkdocs.yml # Documentation site config
|-- LICENSE # License
|-- CONTRIBUTING.md # Contributor guide
+-- PREVIEW.png # README banner image
```

## Documentation
Expand Down
4 changes: 2 additions & 2 deletions scripts/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def _required_env(name: str) -> Path:
return Path(val)


# Reading the env vars at import time is intentional: callers (test.py,
# Reading the env vars at import time is intentional: callers (test_all.py,
# scripts/scan.py, scripts/install.py) pass these around as constants.
# To keep error messages helpful when somebody runs the harness without
# having configured the environment, we let _required_env raise the
Expand Down Expand Up @@ -99,7 +99,7 @@ def load_dll(dll_path: Path):
are declared as ``c_void_p`` so we get the raw heap pointer back from
ctypes; callers must pass each return value through ``call_owned_string``
(or remember to call ``lib.freeResult(addr)``) so the ``_strdup`` buffer
is not leaked. Across the integration suite (test.py runs hundreds of
is not leaked. Across the integration suite (test_all.py runs hundreds of
mods) the previous c_char_p declaration leaked tens of MB per run.
"""
lib = ctypes.CDLL(str(dll_path))
Expand Down
14 changes: 7 additions & 7 deletions src/Mo2Controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ namespace mo2server
* **`POST /api/test/run`**
* - Request: `{ "args"?: "..." }`
* - Response: `{ "running": true, "pid": int }`
* - Errors: 404 test.py missing, 409 busy, 500 CreateProcess, 501 non-Windows
* - Errors: 404 test_all.py missing, 409 busy, 500 CreateProcess, 501 non-Windows
*
* **`GET /api/test/status`**
* - Response: `{ "running": bool }` -- when finished includes `"exitCode": int`
Expand Down Expand Up @@ -343,7 +343,7 @@ class Mo2Controller
crow::response clear_test_logs();

/**
* @brief Spawns `test.py` as a detached Win32 process. Only one test run may be active at a
* @brief Spawns `test_all.py` as a detached Win32 process. Only one test run may be active at a
* time.
*
* Unlike `scan_fomods` / `deploy_plugin` / `purge_plugin`, this
Expand All @@ -361,24 +361,24 @@ class Mo2Controller
* whitelist (since `.` is allowed individually).
*
* @param req Optional JSON body with `"args"` (whitelist-sanitized as above).
* @return 200 with PID on success, 400 on bad args, 404 if test.py missing, 409 if already
* @return 200 with PID on success, 400 on bad args, 404 if test_all.py missing, 409 if already
* running, 500 on CreateProcess failure, 501 on non-Windows.
*/
crow::response run_tests(const crow::request& req);

/**
* @brief Checks whether the test.py process is still running. Cleans up the process handle on
* completion.
* @brief Checks whether the test_all.py process is still running. Cleans up the process handle
* on completion.
* @return 200 with `running` flag; includes `exitCode` once the process has finished.
*/
crow::response get_test_status();

private:
// -- Test runner state --
std::mutex test_mutex_; // guards test_process_
bool test_running_{false}; // true while test.py is executing
bool test_running_{false}; // true while test_all.py is executing
#ifdef _WIN32
HANDLE test_process_{nullptr}; // Win32 process handle for test.py
HANDLE test_process_{nullptr}; // Win32 process handle for test_all.py
#endif

/** Result of a FOMOD scan job. */
Expand Down
2 changes: 1 addition & 1 deletion src/Mo2LogController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ crow::response Mo2Controller::get_logs(const crow::request& req)

crow::response Mo2Controller::get_test_logs(const crow::request& req)
{
// test.log lives next to test.py, which itself lives next to the exe.
// test.log lives next to test_all.py, which itself lives next to the exe.
return read_log_file(mo2core::executable_directory() / "test.log", req);
}

Expand Down
14 changes: 7 additions & 7 deletions src/Mo2TestController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace mo2server
{

// ---------------------------------------------------------------------------
// POST /api/test/run -- spawn test.py in background
// POST /api/test/run -- spawn test_all.py in background
// ---------------------------------------------------------------------------

crow::response Mo2Controller::run_tests(const crow::request& req)
Expand Down Expand Up @@ -85,12 +85,12 @@ crow::response Mo2Controller::run_tests(const crow::request& req)
}

auto exe_dir = mo2core::executable_directory();
auto py_path = exe_dir / "test.py";
auto py_path = exe_dir / "test_all.py";
if (!fs::exists(py_path))
return json_response(404,
{{"error", std::format("test.py not found in {}", exe_dir.string())}});
return json_response(
404, {{"error", std::format("test_all.py not found in {}", exe_dir.string())}});

// Build command line - test.py handles its own logging to test.log
// Build command line - test_all.py handles its own logging to test.log
std::string cmd = std::format("python \"{}\" {}", py_path.string(), args);

STARTUPINFOA si{};
Expand All @@ -111,8 +111,8 @@ crow::response Mo2Controller::run_tests(const crow::request& req)
if (!ok)
{
auto err = GetLastError();
return json_response(500,
{{"error", std::format("Failed to start test.py (error {})", err)}});
return json_response(
500, {{"error", std::format("Failed to start test_all.py (error {})", err)}});
}

CloseHandle(pi.hThread);
Expand Down
Loading
Loading