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
40 changes: 40 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
60 changes: 60 additions & 0 deletions .github/workflows/validation.yml
Original file line number Diff line number Diff line change
@@ -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 }'
3 changes: 1 addition & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
'';
};
}
Expand Down
26 changes: 11 additions & 15 deletions test/ChordGameTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ TEST_CASE("ChordGame Flow") {
CHECK(res1.getType() == "result");
CHECK(res1.hasField("correct"));

game.stop();
if (gameThread.joinable()) gameThread.join();
}

Expand All @@ -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<Note>{});
}

if (!notes.empty()) midi.pushNotes({notes[0]});
else midi.pushNotes(std::vector<Note>{});
Message res1 = transport.waitForSentMessage();
CHECK(res1.getType() == "result");

Expand All @@ -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();
}

Expand Down Expand Up @@ -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();
}

Expand All @@ -151,6 +146,7 @@ TEST_CASE("ChordGame Unknown Scale") {
midi.pushNotes(std::vector<Note>{});
transport.waitForSentMessage();

game.stop();
if (gameThread.joinable()) gameThread.join();
}

Expand Down Expand Up @@ -179,6 +175,7 @@ TEST_CASE("ChordGame Completely Incorrect") {
CHECK(res.getType() == "result");
CHECK(res.hasField("incorrect"));

game.stop();
if (gameThread.joinable()) gameThread.join();
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -239,12 +237,10 @@ TEST_CASE("ChordGame With Inversions Coverage") {

midi.pushNotes(std::vector<Note>{});
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();
}
26 changes: 4 additions & 22 deletions test/NoteGameTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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();
}

Expand All @@ -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();
}

Expand All @@ -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<std::string> 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();
}