megaglest-0001: Unit::updateAttackBoostProgress() std::find on currentAttackBoostUnits vector<int> inside per-frame candidate loop O(C*B), plus reverse lookup on candidateValidIdList O(B*C). Fix: unordered_set<int> for O(1) membership. MEDIUM, 8.3x at N=2000. megaglest-0002: UnitUpdater::findUnitsForCell() linear dedup scan of units vector inside findUnitsInRange grid loop O(R^2 * U). Fix: unordered_set<int> seenIds for O(1) dedup. HIGH, 9.1x at cells=5000, units=500. MOAD-0002 (Intertangle): CLEAN, standard game engine singletons. MOAD-0003 (Leaked Context): CLEAN, no thread_local usage. MOAD-0004 (Logged Secret): FTP password logged in miniftpclient.cpp lines 358, 360, 968, 975 via szBuf containing ftp://user:pass@host URL. CWE-312 confirmed but in bundled third-party FTP client code. MOAD-0005 (Thundering Herd): CLEAN, single-threaded game logic. 4/4 unit tests PASS.
188 lines
6.4 KiB
C++
188 lines
6.4 KiB
C++
// Unit test for megaglest-0001: Attack boost unit membership lookup
|
|
// Defect: std::find on vector<int> inside per-frame loop = O(C * B)
|
|
// Fix: std::unordered_set<int> for O(1) membership test
|
|
//
|
|
// Simulates our attack boost update loop where we check if each candidate
|
|
// unit is already in our boosted-units list, and check if each boosted unit
|
|
// is still a valid candidate.
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
// --- DEFECTIVE: O(C * B) linear scan ---
|
|
struct AttackBoostDefective {
|
|
std::vector<int> currentAttackBoostUnits;
|
|
|
|
void updateBoost(const std::vector<int> &candidateIds) {
|
|
std::vector<int> candidateValidIdList;
|
|
candidateValidIdList.reserve(candidateIds.size());
|
|
|
|
for (size_t i = 0; i < candidateIds.size(); ++i) {
|
|
int id = candidateIds[i];
|
|
candidateValidIdList.push_back(id);
|
|
|
|
// O(B) linear scan per candidate
|
|
auto iterFound = std::find(currentAttackBoostUnits.begin(),
|
|
currentAttackBoostUnits.end(), id);
|
|
if (iterFound == currentAttackBoostUnits.end()) {
|
|
// New unit: apply boost
|
|
currentAttackBoostUnits.push_back(id);
|
|
}
|
|
}
|
|
|
|
// Remove units no longer in range: O(B * C) linear scan
|
|
for (int i = (int)currentAttackBoostUnits.size() - 1; i >= 0; --i) {
|
|
int findUnitId = currentAttackBoostUnits[i];
|
|
auto iterFound = std::find(candidateValidIdList.begin(),
|
|
candidateValidIdList.end(), findUnitId);
|
|
if (iterFound == candidateValidIdList.end()) {
|
|
currentAttackBoostUnits.erase(
|
|
currentAttackBoostUnits.begin() + i);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// --- FIXED: O(C + B) with hash sets ---
|
|
struct AttackBoostFixed {
|
|
std::vector<int> currentAttackBoostUnits;
|
|
|
|
void updateBoost(const std::vector<int> &candidateIds) {
|
|
std::unordered_set<int> candidateValidIdSet;
|
|
candidateValidIdSet.reserve(candidateIds.size());
|
|
|
|
// Build hash set of currently boosted IDs for O(1) lookup
|
|
std::unordered_set<int> boostedIdSet(
|
|
currentAttackBoostUnits.begin(),
|
|
currentAttackBoostUnits.end());
|
|
|
|
for (size_t i = 0; i < candidateIds.size(); ++i) {
|
|
int id = candidateIds[i];
|
|
candidateValidIdSet.insert(id);
|
|
|
|
// O(1) hash lookup
|
|
if (boostedIdSet.count(id) == 0) {
|
|
currentAttackBoostUnits.push_back(id);
|
|
boostedIdSet.insert(id);
|
|
}
|
|
}
|
|
|
|
// Remove units no longer in range: O(1) hash lookup per boosted unit
|
|
for (int i = (int)currentAttackBoostUnits.size() - 1; i >= 0; --i) {
|
|
int findUnitId = currentAttackBoostUnits[i];
|
|
if (candidateValidIdSet.count(findUnitId) == 0) {
|
|
currentAttackBoostUnits.erase(
|
|
currentAttackBoostUnits.begin() + i);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Verify correctness: both produce same result
|
|
void test_correctness() {
|
|
printf("test_correctness... ");
|
|
|
|
// Initial boosted set
|
|
std::vector<int> initialBoosted = {10, 20, 30, 40, 50};
|
|
// New candidates: some overlap, some new, some old ones gone
|
|
std::vector<int> candidates = {20, 30, 60, 70, 80};
|
|
|
|
AttackBoostDefective defective;
|
|
defective.currentAttackBoostUnits = initialBoosted;
|
|
defective.updateBoost(candidates);
|
|
|
|
AttackBoostFixed fixed;
|
|
fixed.currentAttackBoostUnits = initialBoosted;
|
|
fixed.updateBoost(candidates);
|
|
|
|
// Sort both for comparison
|
|
std::vector<int> defResult = defective.currentAttackBoostUnits;
|
|
std::vector<int> fixResult = fixed.currentAttackBoostUnits;
|
|
std::sort(defResult.begin(), defResult.end());
|
|
std::sort(fixResult.begin(), fixResult.end());
|
|
|
|
assert(defResult == fixResult);
|
|
// Should contain: 20, 30, 60, 70, 80 (10, 40, 50 removed; 60, 70, 80 added)
|
|
std::vector<int> expected = {20, 30, 60, 70, 80};
|
|
assert(defResult == expected);
|
|
|
|
printf("PASS\n");
|
|
}
|
|
|
|
// Verify empty candidates removes all boosted
|
|
void test_empty_candidates() {
|
|
printf("test_empty_candidates... ");
|
|
|
|
std::vector<int> initialBoosted = {1, 2, 3, 4, 5};
|
|
std::vector<int> candidates = {};
|
|
|
|
AttackBoostDefective defective;
|
|
defective.currentAttackBoostUnits = initialBoosted;
|
|
defective.updateBoost(candidates);
|
|
|
|
AttackBoostFixed fixed;
|
|
fixed.currentAttackBoostUnits = initialBoosted;
|
|
fixed.updateBoost(candidates);
|
|
|
|
assert(defective.currentAttackBoostUnits.empty());
|
|
assert(fixed.currentAttackBoostUnits.empty());
|
|
|
|
printf("PASS\n");
|
|
}
|
|
|
|
// Benchmark: measure O(C*B) vs O(C+B)
|
|
void test_performance() {
|
|
printf("test_performance... ");
|
|
|
|
const int B = 2000; // boosted units
|
|
const int C = 2000; // candidates
|
|
const int ITERS = 20;
|
|
|
|
// Build initial boosted list (1..B)
|
|
std::vector<int> initialBoosted;
|
|
for (int i = 1; i <= B; ++i) initialBoosted.push_back(i);
|
|
|
|
// Candidates: half overlap (1..C/2), half new (B+1..B+C/2)
|
|
std::vector<int> candidates;
|
|
for (int i = 1; i <= C / 2; ++i) candidates.push_back(i);
|
|
for (int i = B + 1; i <= B + C / 2; ++i) candidates.push_back(i);
|
|
|
|
// Defective
|
|
auto t0 = std::chrono::high_resolution_clock::now();
|
|
for (int iter = 0; iter < ITERS; ++iter) {
|
|
AttackBoostDefective d;
|
|
d.currentAttackBoostUnits = initialBoosted;
|
|
d.updateBoost(candidates);
|
|
}
|
|
auto t1 = std::chrono::high_resolution_clock::now();
|
|
|
|
// Fixed
|
|
auto t2 = std::chrono::high_resolution_clock::now();
|
|
for (int iter = 0; iter < ITERS; ++iter) {
|
|
AttackBoostFixed f;
|
|
f.currentAttackBoostUnits = initialBoosted;
|
|
f.updateBoost(candidates);
|
|
}
|
|
auto t3 = std::chrono::high_resolution_clock::now();
|
|
|
|
double defective_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
|
|
double fixed_ms = std::chrono::duration<double, std::milli>(t3 - t2).count();
|
|
double ratio = defective_ms / fixed_ms;
|
|
|
|
printf("defective=%.1fms fixed=%.1fms ratio=%.1fx ", defective_ms, fixed_ms, ratio);
|
|
assert(ratio > 2.0);
|
|
printf("PASS\n");
|
|
}
|
|
|
|
int main() {
|
|
printf("=== megaglest-0001: Attack boost unit membership O(C*B) -> O(C+B) ===\n");
|
|
test_correctness();
|
|
test_empty_candidates();
|
|
test_performance();
|
|
printf("All tests passed.\n");
|
|
return 0;
|
|
}
|