diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8c5b81d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,40 @@ +on: + push: + branches: + - main + +jobs: + build-nix: + name: Auto Nix Build + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + + # - name: Config Cachix TODO + # uses: cachix/cachix-action@v15 + # with: + # name: devenv + + - name: Build project with Nix + run: | + nix build --print-build-logs --jobs $(nproc) + + - name: Check build outputs + run: | + ls -lh result/bin/ + + # - name: Upload artefact TODO + # uses: actions/upload-artifact@v4 + # with: + # name: smart-piano-engine + # path: result/bin/engine + # retention-days: 30 diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml new file mode 100644 index 0000000..10a0b83 --- /dev/null +++ b/.github/workflows/validation.yml @@ -0,0 +1,60 @@ +on: + pull_request: + branches: + - main + +jobs: + test-coverage: + name: Tests & Code Coverage + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Install ALSA & MIDI + run: | + sudo apt-get update + sudo apt-get install -y alsa-utils linux-modules-extra-$(uname -r) + # No virmidi module on Actions TODO mock MIDI for this env + + # - name: Config Cachix TODO + # uses: cachix/cachix-action@v15 + # with: + # name: devenv + + - name: Configure & build project + run: | + nix develop --command bash -c "cmake --build build -j$(nproc)" + + - name: Run tests + run: | + nix develop --command bash -c "cmake --build build --target tests" + + - name: Run main + run: | + nix develop --command bash -c "cmake --build build --target run" + + - name: Measure code coverage + run: | + nix develop --command bash -c "cmake --build build --target coverage" + + - name: Export coverage report totals as text + run: | + nix develop --command bash -c "llvm-cov report build/src/main -instr-profile=build/coverage.profdata -ignore-filename-regex='test/.*' > build/coverage.txt" + cat build/coverage.txt + + - name: Verify functions coverage is (almost) 100% (MIDI tests impossible on CI) + run: | + grep TOTAL build/coverage.txt | awk '{ if ($7 + 0 > 90) exit 0; else exit 1 }' + + - name: Verify line coverage is at least 90% + run: | + grep TOTAL build/coverage.txt | awk '{ if ($10 + 0 > 90) exit 0; else exit 1 }' diff --git a/flake.nix b/flake.nix index 69f2b87..364ef13 100644 --- a/flake.nix +++ b/flake.nix @@ -67,8 +67,7 @@ # Export compile commands JSON for LSP and other tools shellHook = '' mkdir --verbose build - cd build - cmake -DCMAKE_BUILD_TYPE=Debug -DCOVERAGE=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. + cmake -DCMAKE_BUILD_TYPE=Debug -DCOVERAGE=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -S . -B build ''; }; } diff --git a/test/ChordGameTest.cpp b/test/ChordGameTest.cpp index f356bc8..f34026c 100644 --- a/test/ChordGameTest.cpp +++ b/test/ChordGameTest.cpp @@ -42,6 +42,7 @@ TEST_CASE("ChordGame Flow") { CHECK(res1.getType() == "result"); CHECK(res1.hasField("correct")); + game.stop(); if (gameThread.joinable()) gameThread.join(); } @@ -67,12 +68,8 @@ TEST_CASE("ChordGame Partial and Incorrect") { while (std::getline(ss, segment, ' ')) notes.push_back(segment); // Envoyer seulement 1 note correcte (Partiel) - if (!notes.empty()) { - midi.pushNotes({notes[0]}); - } else { - midi.pushNotes(std::vector{}); - } - + if (!notes.empty()) midi.pushNotes({notes[0]}); + else midi.pushNotes(std::vector{}); Message res1 = transport.waitForSentMessage(); CHECK(res1.getType() == "result"); @@ -85,7 +82,7 @@ TEST_CASE("ChordGame Partial and Incorrect") { // "incorrect" n'est PAS présent CHECK_FALSE(res1.hasField("incorrect")); } - + game.stop(); if (gameThread.joinable()) gameThread.join(); } @@ -122,12 +119,10 @@ TEST_CASE("ChordGame Inversions") { transport.waitForSentMessage(); // result // Envoyer ready seulement si on attend un autre tour - if (i < 19) { - transport.pushIncoming(Message("ready")); - } + if (i < 19) transport.pushIncoming(Message("ready")); } - CHECK(seenInversion); // Devrait être très probable + game.stop(); if (gameThread.joinable()) gameThread.join(); } @@ -151,6 +146,7 @@ TEST_CASE("ChordGame Unknown Scale") { midi.pushNotes(std::vector{}); transport.waitForSentMessage(); + game.stop(); if (gameThread.joinable()) gameThread.join(); } @@ -179,6 +175,7 @@ TEST_CASE("ChordGame Completely Incorrect") { CHECK(res.getType() == "result"); CHECK(res.hasField("incorrect")); + game.stop(); if (gameThread.joinable()) gameThread.join(); } @@ -206,6 +203,7 @@ TEST_CASE("ChordGame Ready Message Error") { transport.pushIncoming(Message("wrong")); std::this_thread::sleep_for(std::chrono::milliseconds(100)); + game.stop(); if (gameThread.joinable()) gameThread.join(); } @@ -239,12 +237,10 @@ TEST_CASE("ChordGame With Inversions Coverage") { midi.pushNotes(std::vector{}); transport.waitForSentMessage(); // result - - if (i < 49) { - transport.pushIncoming(Message("ready")); - } + if (i < 49) transport.pushIncoming(Message("ready")); } CHECK(foundInversion); // Très probable avec 50 tentatives + game.stop(); if (gameThread.joinable()) gameThread.join(); } diff --git a/test/NoteGameTest.cpp b/test/NoteGameTest.cpp index cfea2a2..3a9f856 100644 --- a/test/NoteGameTest.cpp +++ b/test/NoteGameTest.cpp @@ -17,36 +17,29 @@ TEST_CASE("NoteGame Flow") { config.mode = "Majeur"; config.maxChallenges = 2; // Test avec 2 défis pour vérifier l'enchaînement NoteGame game(transport, midi, config); - game.start(); std::thread gameThread([&game]() { game.play(); }); - // Premier défi Message msg1 = transport.waitForSentMessage(); CHECK(msg1.getType() == "note"); std::string expectedNote = msg1.getField("note"); - // Simulation de la réponse correcte via MIDI midi.pushNotes({expectedNote}); - // Vérification du résultat (attendu correct) Message res1 = transport.waitForSentMessage(); CHECK(res1.getType() == "result"); CHECK(res1.hasField("correct")); - // Signal pour passer au défi suivant transport.pushIncoming(Message("ready")); - // Deuxième défi Message msg2 = transport.waitForSentMessage(); CHECK(msg2.getType() == "note"); expectedNote = msg2.getField("note"); midi.pushNotes({expectedNote}); - Message res2 = transport.waitForSentMessage(); CHECK(res2.getType() == "result"); CHECK(res2.hasField("correct")); - + game.stop(); if (gameThread.joinable()) gameThread.join(); } @@ -60,22 +53,18 @@ TEST_CASE("NoteGame Incorrect Answer") { config.mode = "maj"; config.maxChallenges = 1; NoteGame game(transport, midi, config); - game.start(); std::thread gameThread([&game]() { game.play(); }); - Message msg1 = transport.waitForSentMessage(); std::string expectedNote = msg1.getField("note"); - // Envoyer note incorrecte std::string wrongNote = (expectedNote == "c4") ? "d4" : "c4"; midi.pushNotes({wrongNote}); - Message res1 = transport.waitForSentMessage(); CHECK(res1.getType() == "result"); CHECK(res1.hasField("incorrect")); CHECK_FALSE(res1.hasField("correct")); - + game.stop(); if (gameThread.joinable()) gameThread.join(); } @@ -89,15 +78,12 @@ TEST_CASE("NoteGame Unknown Scale Fallback") { config.mode = "mode"; config.maxChallenges = 1; NoteGame game(transport, midi, config); - game.start(); std::thread gameThread([&game]() { game.play(); }); - // Devrait fallback sur Do Majeur, donc générer une note Message msg1 = transport.waitForSentMessage(); CHECK(msg1.getType() == "note"); - - midi.close(); // Arrêter attente notes + game.stop(); if (gameThread.joinable()) gameThread.join(); } @@ -111,26 +97,22 @@ TEST_CASE("NoteGame Multiple Incorrect Notes") { config.mode = "maj"; config.maxChallenges = 1; NoteGame game(transport, midi, config); - game.start(); std::thread gameThread([&game]() { game.play(); }); - Message msg1 = transport.waitForSentMessage(); std::string expectedNote = msg1.getField("note"); - // Envoyer plusieurs notes incorrectes (couvre ligne 60 branch) std::vector wrongNotes; if (expectedNote != "c4") wrongNotes.push_back("c4"); if (expectedNote != "d4") wrongNotes.push_back("d4"); if (expectedNote != "e4") wrongNotes.push_back("e4"); midi.pushNotes(wrongNotes); - Message res1 = transport.waitForSentMessage(); CHECK(res1.getType() == "result"); CHECK(res1.hasField("incorrect")); // Vérifier que champ incorrect contient espaces (ligne 60) std::string incorrect = res1.getField("incorrect"); CHECK(incorrect.find(" ") != std::string::npos); - + game.stop(); if (gameThread.joinable()) gameThread.join(); }