patch: sonarqube fixes.

This commit is contained in:
Sebastian Dine
2026-05-09 19:57:02 +02:00
committed by GitHub
parent 6ff4406638
commit c1d42bdadd
15 changed files with 123 additions and 45 deletions
+1
View File
@@ -38,6 +38,7 @@ GitHub Actions workflows for CI, release automation, and policy checks.
- Prefer minimal, surgical edits; avoid large workflow rewrites unless requested.
- Reusable workflows should declare explicit `workflow_call` inputs for required context (e.g., version, merge SHA).
- Sonar coverage steps that use `gcovr` must exclude third-party build trees at discovery time with `--exclude-directories` (for example `build/_deps`) so gcov does not process dependency `.gcda` files.
- The Sonar scan step passes `-Dsonar.coverage.exclusions=**/ui_wx/**,**/app/**` so the coverage quality gate reflects **`ccm_core_tests`** only (wx UI and the composition root are not executed under test). `sonar.sources` stays `core,ui_wx,app`; bugs/security/duplications still analyze those trees.
- For Linux Sonar coverage jobs, keep compiler and gcov toolchain aligned; because `cmake/Toolchain.cmake` prefers Clang by default, set `-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++` explicitly in the coverage configure step when using gcovr default `gcov`.
- Keep `permissions` least-privilege:
- reusable build workflows: `contents: read`
+1
View File
@@ -69,6 +69,7 @@ jobs:
-Dsonar.sources=core,ui_wx,app
-Dsonar.cfamily.compile-commands=build/compile_commands.json
-Dsonar.coverageReportPaths=build/sonarqube-coverage.xml
-Dsonar.coverage.exclusions=**/ui_wx/**,**/app/**
linux:
name: Linux build + tests
+1
View File
@@ -69,6 +69,7 @@ jobs:
-Dsonar.sources=core,ui_wx,app
-Dsonar.cfamily.compile-commands=build/compile_commands.json
-Dsonar.coverageReportPaths=build/sonarqube-coverage.xml
-Dsonar.coverage.exclusions=**/ui_wx/**,**/app/**
compute-version:
name: Determine semantic version
+2 -2
View File
@@ -52,7 +52,7 @@ Run from the **workspace root**.
- Run the app:
`./build/bin/ccm3` (`.\build\bin\ccm3.exe` on Windows)
- Run tests (CCM_BUILD_TESTS defaults to ON):
`ctest --test-dir build --output-on-failure` — current baseline: **180 tests, all green**.
`ctest --test-dir build --output-on-failure` — current baseline: **185 tests, all green**.
- Build tests only:
`cmake --build build --target ccm_core_tests`
@@ -96,7 +96,7 @@ Run from the **workspace root**.
- After modifying a domain type's fields or JSON layout you **must** update the matching round-trip test in `tests/domain_json_tests.cpp` and re-run tests.
- After adding a new `.cpp` to `core/` or `ui_wx/` you **must** add it to that package's `CMakeLists.txt`. There is no glob.
- After adding a new dependency you **must** verify its license is compatible with this repository's MIT license before merging.
- After changing SonarQube coverage generation, keep dependency build outputs excluded at gcov discovery time (for example `gcovr --exclude-directories "build/_deps"`); output-only excludes are not enough for third-party `.gcda` files.
- After changing SonarQube coverage generation, keep dependency build outputs excluded at gcov discovery time (for example `gcovr --exclude-directories "build/_deps"`); output-only excludes are not enough for third-party `.gcda` files. The Sonar scan uses `sonar.coverage.exclusions` for `**/ui_wx/**` and `**/app/**` so the coverage percentage matches the hermetic `ccm_core_tests` surface (`core/`); analyzed sources are unchanged for other Sonar metrics.
- After adding a new game module you **must**: (1) extend `Game` enum + string mappings in `core/include/ccm/domain/Enums.hpp`, (2) register the module in `app/main.cpp`, (3) add a directory mapping in `app/main.cpp::dirNameForGame`, (4) implement an `IGameView` derived class (or `<Name>GameView`) and add it to `AppContext::gameViews` in the composition root.
- After changing the per-game seams (`IGameModule`, `IGameView`, the `BaseCard*Panel` template hooks) you **must** update `docs/adding-a-new-game.md` so the canonical "add a new game" walkthrough stays in sync with the code.
- After changing `formatTextForFs` or `parseIndexFromFilename` you **must** update `tests/fs_names_tests.cpp` — these functions exist to stay byte-compatible with the original Rust `util/fs.rs`.
+4
View File
@@ -17,6 +17,10 @@ The repository uses GitHub Actions workflows split by branch intent, with one or
- `master-ci.yml`: single workflow run on merged PRs to `master`; computes semver, invokes Windows reusable build, then tags/publishes release assets.
- `master-windows.yml`: reusable Windows build/test/package workflow invoked by `master-ci.yml`.
### SonarQube Cloud (coverage quality gate)
Both `feature-ci.yml` and `master-ci.yml` include a Linux job that configures with GCC coverage flags, builds, runs `ctest`, generates `build/sonarqube-coverage.xml` via `gcovr`, and runs the SonarCloud scan. **`sonar.coverage.exclusions`** omit `ui_wx/` and `app/` from the coverage denominator because only `ccm_core` is exercised by automated tests; Sonar still analyzes those directories for bugs, vulnerabilities, and duplications. See [Testing Guide And Test Code Of Conduct](testing-and-test-code-of-conduct.md).
## Version Flow
Feature branches and `master` use different version modes because they solve different problems: feature builds need traceability to a commit, while `master` builds need stable semantic releases.
+2
View File
@@ -30,6 +30,8 @@ Windows and Linux use the same logical flow; only generator and compiler setup d
## Coverage Surface
**SonarCloud:** The CI Sonar scan reports coverage against `core/` paths that `ccm_core_tests` can execute. `ui_wx/` and the `app/` composition root are excluded from Sonars **coverage** calculation (`sonar.coverage.exclusions`) because they are not run under the doctest suite; UI behavior is covered by manual validation below. Other Sonar metrics still include those directories.
Current automated tests cover non-UI behavior, including:
- filesystem naming and parsing behavior
+1
View File
@@ -12,6 +12,7 @@
- `collection_service_tests.cpp``CollectionService<MagicCard>` (uses inline `InMemoryRepo` + `StubImageStore`).
- `config_service_tests.cpp``ConfigService` against `InMemoryFileSystem`.
- `json_collection_repository_tests.cpp`, `json_set_repository_tests.cpp` — repository round-trips against `InMemoryFileSystem`.
- `local_image_store_tests.cpp``LocalImageStore` against `InMemoryFileSystem` + `ConfigService`: `copyIn` (extension preserved, missing source errors), `remove` (existing file deleted; absent path is a no-op), `resolvePath` layout under `dataStorage/<game>/images/`.
- `set_service_tests.cpp``SetService` with `FakeSetSource` + `InMemSetRepo`.
- `magic_set_source_tests.cpp``MagicSetSource::parseResponse` (Scryfall mapping). Drives `fetchAll` via `FixedHttpClient` fake.
- `magic_card_preview_source_tests.cpp``MagicCardPreviewSource::buildSearchUrl` URL-encoding rules + `parseResponse` (`data[0].image_uris.normal`). Drives `fetchImageUrl` via `FixedHttpClient`.
+1
View File
@@ -12,6 +12,7 @@ add_executable(ccm_core_tests
config_service_tests.cpp
json_collection_repository_tests.cpp
json_set_repository_tests.cpp
local_image_store_tests.cpp
set_service_tests.cpp
magic_set_source_tests.cpp
magic_card_preview_source_tests.cpp
+90
View File
@@ -0,0 +1,90 @@
#include <doctest/doctest.h>
#include "ccm/infra/LocalImageStore.hpp"
#include "ccm/services/ConfigService.hpp"
#include "fakes/InMemoryFileSystem.hpp"
#include <filesystem>
using namespace ccm;
using ccm::testing::InMemoryFileSystem;
namespace {
std::string dirNameForGame(Game g) {
switch (g) {
case Game::Magic: return "magic";
case Game::Pokemon: return "pokemon";
case Game::YuGiOh: return "yugioh";
}
return "magic";
}
} // namespace
TEST_SUITE("LocalImageStore") {
TEST_CASE("copyIn creates images directory and preserves source extension") {
InMemoryFileSystem mem;
ConfigService cfg{mem, "/app/config.json", "/coll"};
REQUIRE(cfg.initialize().isOk());
LocalImageStore store(mem, cfg, dirNameForGame);
REQUIRE(mem.writeText("/incoming/card.PNG", "img-bytes").isOk());
auto r = store.copyIn(Game::Magic, "/incoming/card.PNG", "id001");
REQUIRE(r.isOk());
CHECK(r.value() == "id001.PNG");
const std::filesystem::path dest =
std::filesystem::path(cfg.current().dataStorage) / "magic" / "images" / "id001.PNG";
REQUIRE(mem.exists(dest));
auto body = mem.readText(dest);
REQUIRE(body.isOk());
CHECK(body.value() == "img-bytes");
}
TEST_CASE("copyIn errors when source file is missing") {
InMemoryFileSystem mem;
ConfigService cfg{mem, "/app/config.json", "/coll"};
REQUIRE(cfg.initialize().isOk());
LocalImageStore store(mem, cfg, dirNameForGame);
auto r = store.copyIn(Game::Pokemon, "/nope/missing.jpg", "x");
REQUIRE(r.isErr());
}
TEST_CASE("remove deletes an existing image") {
InMemoryFileSystem mem;
ConfigService cfg{mem, "/app/config.json", "/coll"};
REQUIRE(cfg.initialize().isOk());
LocalImageStore store(mem, cfg, dirNameForGame);
const std::filesystem::path imagePath =
std::filesystem::path(cfg.current().dataStorage) / "magic" / "images" / "a.png";
REQUIRE(mem.ensureDirectory(imagePath.parent_path()).isOk());
REQUIRE(mem.writeText(imagePath, "x").isOk());
REQUIRE(store.remove(Game::Magic, "a.png").isOk());
CHECK_FALSE(mem.exists(imagePath));
}
TEST_CASE("remove succeeds when file is already absent") {
InMemoryFileSystem mem;
ConfigService cfg{mem, "/app/config.json", "/coll"};
REQUIRE(cfg.initialize().isOk());
LocalImageStore store(mem, cfg, dirNameForGame);
REQUIRE(store.remove(Game::YuGiOh, "ghost.bin").isOk());
}
TEST_CASE("resolvePath joins data storage game images and filename") {
InMemoryFileSystem mem;
ConfigService cfg{mem, "/app/config.json", "/coll"};
REQUIRE(cfg.initialize().isOk());
LocalImageStore store(mem, cfg, dirNameForGame);
const std::filesystem::path got = store.resolvePath(Game::Pokemon, "pic.jpg");
CHECK(got.generic_string() == "/coll/pokemon/images/pic.jpg");
}
}
+1 -1
View File
@@ -53,7 +53,7 @@
- Apply this rule consistently in shared templates (`BaseCardListPanel`, `BaseSelectedCardPanel`, `BaseCardEditDialog`) because a single implicit conversion in those bases affects every game view.
14. **Theme consistency rules (Windows):**
- Treat dialog roots as `panelBg`, not a separate shade, otherwise label rows can look like mismatched darker boxes.
- Theme dialogs before `ShowModal()` with `applyThemeToWindowTree(...)`; this includes Settings, Create/Edit dialogs, image viewer, About, and custom popup dialogs.
- Theme dialogs before `ShowModal()` with `applyThemeToWindowTree(...)` (and root background/foreground colors as needed); this includes Settings, image viewer, About, and custom popup dialogs. Per-game **Add/Edit** flows use `themeModalDialog(wxDialog*, Theme)` from `Theme.hpp` so `MagicGameView` / `PokemonGameView` / `YuGiOhGameView` share one path instead of duplicating palette wiring.
- Do not use native `wxMessageBox` / `wxAboutBox` for app-facing flows that must match dark mode. Use themed popup helpers (or a custom themed `wxDialog`) so body/buttons stay in sync with the app palette.
- Center popup dialogs on the app window (`CentreOnParent()`) so confirmations/info boxes open relative to the current app window.
- Include `wxSpinCtrl` in themed input controls (Amount field) or it will keep a mismatched native background.
+2
View File
@@ -4,6 +4,7 @@
#include <wx/colour.h>
class wxDialog;
class wxWindow;
class wxString;
@@ -22,6 +23,7 @@ struct ThemePalette {
ThemePalette paletteForTheme(Theme theme);
Theme inferThemeFromWindow(const wxWindow* window);
void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme theme);
void themeModalDialog(wxDialog* dlg, Theme theme);
int showThemedMessageDialog(wxWindow* parent, const wxString& message, const wxString& caption, long style);
int showThemedConfirmDialog(wxWindow* parent, const wxString& message, const wxString& caption);
+3 -14
View File
@@ -3,6 +3,7 @@
#include "ccm/ui/MagicCardEditDialog.hpp"
#include "ccm/ui/MagicCardListPanel.hpp"
#include "ccm/ui/MagicSelectedCardPanel.hpp"
#include "ccm/ui/Theme.hpp"
#include <wx/msgdlg.h>
@@ -94,13 +95,7 @@ void MagicGameView::onAddCard(wxWindow* parentWindow) {
MagicCardEditDialog dlg(parentWindow, images_, sets_, EditMode::Create, fresh,
&setsForDialog());
{
const Theme currentTheme = config_.current().theme;
const ThemePalette palette = paletteForTheme(currentTheme);
applyThemeToWindowTree(&dlg, palette, currentTheme);
dlg.SetBackgroundColour(palette.panelBg);
dlg.SetForegroundColour(palette.text);
}
themeModalDialog(&dlg, config_.current().theme);
if (dlg.ShowModal() != wxID_OK) return;
auto added = collection_.add(Game::Magic, dlg.card());
@@ -139,13 +134,7 @@ void MagicGameView::onEditCard(wxWindow* parentWindow) {
}
MagicCardEditDialog dlg(parentWindow, images_, sets_, EditMode::Edit, *sel,
&setsForDialog());
{
const Theme currentTheme = config_.current().theme;
const ThemePalette palette = paletteForTheme(currentTheme);
applyThemeToWindowTree(&dlg, palette, currentTheme);
dlg.SetBackgroundColour(palette.panelBg);
dlg.SetForegroundColour(palette.text);
}
themeModalDialog(&dlg, config_.current().theme);
if (dlg.ShowModal() != wxID_OK) return;
auto updated = collection_.update(Game::Magic, dlg.card());
if (!updated) {
+3 -14
View File
@@ -3,6 +3,7 @@
#include "ccm/ui/PokemonCardEditDialog.hpp"
#include "ccm/ui/PokemonCardListPanel.hpp"
#include "ccm/ui/PokemonSelectedCardPanel.hpp"
#include "ccm/ui/Theme.hpp"
#include <wx/msgdlg.h>
@@ -91,13 +92,7 @@ void PokemonGameView::onAddCard(wxWindow* parentWindow) {
PokemonCardEditDialog dlg(parentWindow, images_, sets_, EditMode::Create, fresh,
&setsForDialog());
{
const Theme currentTheme = config_.current().theme;
const ThemePalette palette = paletteForTheme(currentTheme);
applyThemeToWindowTree(&dlg, palette, currentTheme);
dlg.SetBackgroundColour(palette.panelBg);
dlg.SetForegroundColour(palette.text);
}
themeModalDialog(&dlg, config_.current().theme);
if (dlg.ShowModal() != wxID_OK) return;
auto added = collection_.add(Game::Pokemon, dlg.card());
@@ -136,13 +131,7 @@ void PokemonGameView::onEditCard(wxWindow* parentWindow) {
}
PokemonCardEditDialog dlg(parentWindow, images_, sets_, EditMode::Edit, *sel,
&setsForDialog());
{
const Theme currentTheme = config_.current().theme;
const ThemePalette palette = paletteForTheme(currentTheme);
applyThemeToWindowTree(&dlg, palette, currentTheme);
dlg.SetBackgroundColour(palette.panelBg);
dlg.SetForegroundColour(palette.text);
}
themeModalDialog(&dlg, config_.current().theme);
if (dlg.ShowModal() != wxID_OK) return;
auto updated = collection_.update(Game::Pokemon, dlg.card());
if (!updated) {
+8
View File
@@ -609,6 +609,14 @@ void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme t
}
}
void themeModalDialog(wxDialog* dlg, Theme theme) {
if (dlg == nullptr) return;
const ThemePalette palette = paletteForTheme(theme);
applyThemeToWindowTree(dlg, palette, theme);
dlg->SetBackgroundColour(palette.panelBg);
dlg->SetForegroundColour(palette.text);
}
int showThemedMessageDialog(wxWindow* parent, const wxString& message, const wxString& caption, long style) {
wxDialog dlg(parent, wxID_ANY, caption, wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
+3 -14
View File
@@ -3,6 +3,7 @@
#include "ccm/ui/YuGiOhCardEditDialog.hpp"
#include "ccm/ui/YuGiOhCardListPanel.hpp"
#include "ccm/ui/YuGiOhSelectedCardPanel.hpp"
#include "ccm/ui/Theme.hpp"
#include <wx/msgdlg.h>
@@ -100,13 +101,7 @@ void YuGiOhGameView::onAddCard(wxWindow* parentWindow) {
YuGiOhCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Create, fresh,
&setsForDialog());
{
const Theme currentTheme = config_.current().theme;
const ThemePalette palette = paletteForTheme(currentTheme);
applyThemeToWindowTree(&dlg, palette, currentTheme);
dlg.SetBackgroundColour(palette.panelBg);
dlg.SetForegroundColour(palette.text);
}
themeModalDialog(&dlg, config_.current().theme);
if (dlg.ShowModal() != wxID_OK) return;
auto added = collection_.add(Game::YuGiOh, dlg.card());
@@ -145,13 +140,7 @@ void YuGiOhGameView::onEditCard(wxWindow* parentWindow) {
}
YuGiOhCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Edit, *sel,
&setsForDialog());
{
const Theme currentTheme = config_.current().theme;
const ThemePalette palette = paletteForTheme(currentTheme);
applyThemeToWindowTree(&dlg, palette, currentTheme);
dlg.SetBackgroundColour(palette.panelBg);
dlg.SetForegroundColour(palette.text);
}
themeModalDialog(&dlg, config_.current().theme);
if (dlg.ShowModal() != wxID_OK) return;
auto updated = collection_.update(Game::YuGiOh, dlg.card());
if (!updated) {