feat: 添加单元测试框架和验证角色分配算法的测试用例 (#16245)

This commit is contained in:
lhhxxxxx
2026-04-18 20:48:29 +08:00
committed by GitHub
parent 110db7ab85
commit 1a689cbf0b
5 changed files with 392 additions and 1 deletions

View File

@@ -1,5 +1,14 @@
If:
PathMatch: unit_test/.*\.(c|cc|cpp|cxx|h|hh|hpp)$
CompileFlags:
CompilationDatabase: build/unit_test
Add: [-Wunused-variables]
---
If:
PathExclude: unit_test/.*\.(c|cc|cpp|cxx|h|hh|hpp)$
CompileFlags:
CompilationDatabase: build
Add: [-Wunused-variables]
---
Diagnostics:
UnusedIncludes: None

122
.github/workflows/unit-tests.yml vendored Normal file
View File

@@ -0,0 +1,122 @@
name: Unit Tests
on:
# push:
# branches:
# - "dev-v2"
# paths:
# - ".github/workflows/unit-tests.yml"
# - "unit_test/**"
# - "src/**"
# pull_request:
# paths:
# - ".github/workflows/unit-tests.yml"
# - "unit_test/**"
# - "src/**"
workflow_dispatch:
permissions:
contents: read
jobs:
select-tests:
name: Select Unit Test Suites
runs-on: ubuntu-latest
outputs:
has_tests: ${{ steps.select.outputs.has_tests }}
matrix: ${{ steps.select.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
show-progress: false
- name: Select affected test suites
id: select
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
HEAD_SHA: ${{ github.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
python3 - <<'PY'
import json
import os
import subprocess
def matches(path: str, rule: str) -> bool:
if rule.endswith('/**'):
return path.startswith(rule[:-3])
return path == rule
with open('unit_test/test-suites.json', encoding='utf-8') as f:
mapping = json.load(f)
event_name = os.environ['EVENT_NAME']
changed_files = []
run_all = event_name == 'workflow_dispatch'
if not run_all:
if event_name == 'pull_request':
base_sha = os.environ['PR_BASE_SHA']
head_sha = os.environ['PR_HEAD_SHA']
else:
base_sha = os.environ['BEFORE_SHA']
head_sha = os.environ['HEAD_SHA']
if not base_sha or set(base_sha) == {'0'}:
run_all = True
else:
diff = subprocess.check_output(
['git', 'diff', '--name-only', base_sha, head_sha],
text=True,
)
changed_files = [line for line in diff.splitlines() if line]
print('Changed files:')
for file_path in changed_files:
print(f' - {file_path}')
if run_all or any(any(matches(path, rule) for rule in mapping['runAllOnChanges']) for path in changed_files):
selected = mapping['suites']
else:
selected = []
for suite in mapping['suites']:
if any(any(matches(path, rule) for rule in suite['paths']) for path in changed_files):
selected.append(suite)
matrix = {'suite': selected}
with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as f:
f.write(f"has_tests={'true' if selected else 'false'}\n")
f.write(f"matrix={json.dumps(matrix, separators=(',', ':'))}\n")
PY
unit-tests:
name: ${{ matrix.suite.name }} Unit Tests
needs: select-tests
if: ${{ needs.select-tests.outputs.has_tests == 'true' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.select-tests.outputs.matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
show-progress: false
- name: Configure unit tests
run: |
cmake -S unit_test -B build/unit_test -DCMAKE_BUILD_TYPE=Release
- name: Build selected unit test target
run: |
cmake --build build/unit_test --parallel --target ${{ matrix.suite.buildTarget }}
- name: Run selected unit tests
run: |
ctest --test-dir build/unit_test --output-on-failure -R '${{ matrix.suite.ctestRegex }}'

37
unit_test/CMakeLists.txt Normal file
View File

@@ -0,0 +1,37 @@
cmake_minimum_required(VERSION 3.24)
project(MAAUnitTests LANGUAGES CXX)
include(CTest)
include(FetchContent)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "" FORCE)
set(CATCH_INSTALL_EXTRAS ON CACHE BOOL "" FORCE)
set(CATCH_DEVELOPMENT_BUILD OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.8.1
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(Catch2)
include(Catch)
add_executable(maa-algorithm-test
MaaCore/AlgorithmTest.cpp
)
target_compile_features(maa-algorithm-test PRIVATE cxx_std_20)
target_link_libraries(maa-algorithm-test PRIVATE Catch2::Catch2WithMain)
target_include_directories(maa-algorithm-test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../src/MaaCore)
catch_discover_tests(maa-algorithm-test TEST_PREFIX "algorithm::")

View File

@@ -0,0 +1,204 @@
#include <catch2/catch_test_macros.hpp>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "Utils/Algorithm.hpp"
namespace
{
using GroupList = std::unordered_map<std::string, std::vector<std::string>>;
using CharSet = std::unordered_set<std::string>;
void require_valid_allocation(const GroupList& group_list, const CharSet& char_set,
const asst::algorithm::CharAllocationResult& result)
{
REQUIRE(result.status == asst::algorithm::CharAllocationStatus::Success);
REQUIRE(result.has_value());
REQUIRE(result.allocation.size() == group_list.size());
for (const auto& [group_name, allocated_char] : result.allocation) {
INFO("group_name=" << group_name);
REQUIRE(group_list.contains(group_name));
REQUIRE(char_set.contains(allocated_char));
}
std::unordered_set<std::string> used_chars;
for (const auto& [group_name, candidates] : group_list) {
INFO("group_name=" << group_name);
const auto allocation_it = result.allocation.find(group_name);
REQUIRE(allocation_it != result.allocation.end());
const auto& assigned_char = allocation_it->second;
REQUIRE(char_set.contains(assigned_char));
bool candidate_found = false;
for (const auto& candidate : candidates) {
if (candidate == assigned_char) {
candidate_found = true;
break;
}
}
REQUIRE(candidate_found);
REQUIRE(used_chars.emplace(assigned_char).second);
}
}
} // namespace
TEST_CASE("Empty group list returns empty success result")
{
const GroupList groups;
const CharSet chars { "Amiya" };
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
REQUIRE(result.status == asst::algorithm::CharAllocationStatus::Success);
REQUIRE(result.has_value());
REQUIRE(result.allocation.empty());
}
TEST_CASE("Empty char set returns no solution")
{
const GroupList groups { { "先锋", { "德克萨斯" } } };
const CharSet chars;
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
REQUIRE(result.status == asst::algorithm::CharAllocationStatus::NoSolution);
REQUIRE_FALSE(result.has_value());
}
TEST_CASE("Exact matching returns expected allocation")
{
const GroupList groups {
{ "先锋", { "德克萨斯" } },
{ "术师", { "阿米娅" } },
};
const CharSet chars { "德克萨斯", "阿米娅" };
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
REQUIRE(result.status == asst::algorithm::CharAllocationStatus::Success);
REQUIRE(result.has_value());
REQUIRE(result.allocation == std::unordered_map<std::string, std::string> {
{ "先锋", "德克萨斯" },
{ "术师", "阿米娅" },
});
}
TEST_CASE("Duplicate candidates do not break matching")
{
const GroupList groups {
{ "先锋", { "德克萨斯", "德克萨斯" } },
{ "术师", { "阿米娅", "阿米娅" } },
};
const CharSet chars { "德克萨斯", "阿米娅" };
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
REQUIRE(result.status == asst::algorithm::CharAllocationStatus::Success);
REQUIRE(result.has_value());
REQUIRE(result.allocation == std::unordered_map<std::string, std::string> {
{ "先锋", "德克萨斯" },
{ "术师", "阿米娅" },
});
}
TEST_CASE("Unowned candidates are filtered before matching")
{
const GroupList groups {
{ "先锋", { "风笛", "德克萨斯" } },
{ "术师", { "刻俄柏", "阿米娅" } },
};
const CharSet chars { "德克萨斯", "阿米娅" };
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
REQUIRE(result.status == asst::algorithm::CharAllocationStatus::Success);
REQUIRE(result.has_value());
REQUIRE(result.allocation == std::unordered_map<std::string, std::string> {
{ "先锋", "德克萨斯" },
{ "术师", "阿米娅" },
});
}
TEST_CASE("Conflicting groups return no solution")
{
const GroupList groups {
{ "先锋", { "推进之王" } },
{ "近卫", { "推进之王" } },
};
const CharSet chars { "推进之王" };
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
REQUIRE(result.status == asst::algorithm::CharAllocationStatus::NoSolution);
REQUIRE_FALSE(result.has_value());
}
TEST_CASE("Multiple groups can find a valid allocation")
{
const GroupList groups {
{ "先锋", { "德克萨斯", "桃金娘" } },
{ "术师", { "阿米娅", "伊芙利特" } },
{ "医疗", { "闪灵", "夜莺" } },
};
const CharSet chars { "桃金娘", "阿米娅", "夜莺" };
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
require_valid_allocation(groups, chars, result);
}
TEST_CASE("Multiple groups allocation ignores extra owned chars")
{
const GroupList groups {
{ "先锋", { "德克萨斯", "桃金娘" } },
{ "术师", { "阿米娅", "伊芙利特" } },
{ "医疗", { "闪灵", "夜莺" } },
};
const CharSet chars {
"桃金娘",
"阿米娅",
"夜莺",
"德克萨斯",
"伊芙利特",
"闪灵",
"能天使",
};
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
require_valid_allocation(groups, chars, result);
}
TEST_CASE("Group without any owned candidate returns no solution")
{
const GroupList groups {
{ "先锋", { "德克萨斯" } },
{ "术师", { "阿米娅" } },
};
const CharSet chars { "德克萨斯" };
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
REQUIRE(result.status == asst::algorithm::CharAllocationStatus::NoSolution);
REQUIRE_FALSE(result.has_value());
}
TEST_CASE("Group with empty candidate list returns no solution")
{
const GroupList groups {
{ "先锋", {} },
{ "术师", { "阿米娅" } },
};
const CharSet chars { "阿米娅" };
const auto result = asst::algorithm::get_char_allocation_for_each_group(groups, chars);
REQUIRE(result.status == asst::algorithm::CharAllocationStatus::NoSolution);
REQUIRE_FALSE(result.has_value());
}

View File

@@ -0,0 +1,19 @@
{
"runAllOnChanges": [
"unit_test/CMakeLists.txt",
"unit_test/test-suites.json",
".github/workflows/unit-tests.yml"
],
"suites": [
{
"id": "maa-algorithm-test",
"name": "Algorithm",
"buildTarget": "maa-algorithm-test",
"ctestRegex": "^algorithm::",
"paths": [
"src/MaaCore/Utils/Algorithm.hpp",
"unit_test/MaaCore/AlgorithmTest.cpp"
]
}
]
}