java-topology/defects/freeorion-0002/test/test_freeorion_0002.cpp
russell@unturf.com dbc058c155 naev: 2 CWE-407 defects, MOAD 0002-0005 CLEAN
naev-0001: map.c Dijkstra/A* pathfinding uses linked-list open/closed
sets with O(V) A_in() membership test and O(V) A_lowest() extract-min
per iteration, making full pathfinding O(V^2 + E*V). Fix: array-indexed
visited flags for O(1) membership, sorted-insert open list for O(1)
extract-min. 102.5x at V=500 (Naev has 538 star systems). HIGH.

naev-0002: tech.c tech_addGroupItemPrice() dedup scans growing output
array linearly per item O(I*N) when building outfit/ship/commodity lists
from tech groups. Fix: hash set for O(1) amortized dedup. 333x at
N=1000. MEDIUM.

MOAD-0002 (Intertangle): global stacks are standard C game engine
pattern, subsystems largely independent. CLEAN.
MOAD-0003 (Leaked Context): single thread_local in Rust RNG only. CLEAN.
MOAD-0004 (Logged Secret): no credentials in single-player game. CLEAN.
MOAD-0005 (Thundering Herd): single-threaded gameplay logic. CLEAN.

2/2 PASS, 2 defects.
2026-03-31 12:59:12 -04:00

148 lines
5.4 KiB
C++

// Unit test for freeorion-0002: Tech tree DFS cycle detection std::find on stack O(V*E*S)
// Demonstrates that using an unordered_set shadow of our DFS stack eliminates quadratic scanning.
//
// Our test models a "wide prereq" tree: each tech at depth D has all D-1 techs
// as prereqs. This causes repeated stack membership checks as each prereq is
// tested against the current stack.
#include <cassert>
#include <chrono>
#include <iostream>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
using TechTree = std::unordered_map<int, std::vector<int>>;
// -----------------------------------------------------------------------
// DEFECT: std::find on vector stack for cycle detection.
// For each prereq of each tech, scan the entire stack vector.
// -----------------------------------------------------------------------
static int __attribute__((noinline)) scan_ops_linear(const TechTree& tree, int num_techs) {
volatile int scan_ops = 0;
std::unordered_set<int> checked;
for (int t = num_techs - 1; t >= 0; --t) {
if (checked.count(t)) continue;
std::vector<int> stack;
stack.push_back(t);
while (!stack.empty()) {
int current = stack.back();
size_t start_size = stack.size();
auto it = tree.find(current);
if (it != tree.end()) {
for (int prereq : it->second) {
if (checked.count(prereq)) continue;
// Linear scan
bool found = false;
for (int i = static_cast<int>(stack.size()) - 1; i >= 0; --i) {
scan_ops = scan_ops + 1;
if (stack[i] == prereq) { found = true; break; }
}
if (!found) {
stack.push_back(prereq);
}
}
}
if (start_size == stack.size()) {
stack.pop_back();
checked.insert(current);
}
}
}
return scan_ops;
}
// -----------------------------------------------------------------------
// FIX: unordered_set shadow for O(1) membership
// -----------------------------------------------------------------------
static int __attribute__((noinline)) scan_ops_hashset(const TechTree& tree, int num_techs) {
volatile int scan_ops = 0;
std::unordered_set<int> checked;
for (int t = num_techs - 1; t >= 0; --t) {
if (checked.count(t)) continue;
std::vector<int> stack;
std::unordered_set<int> stack_set;
stack.push_back(t);
stack_set.insert(t);
while (!stack.empty()) {
int current = stack.back();
size_t start_size = stack.size();
auto it = tree.find(current);
if (it != tree.end()) {
for (int prereq : it->second) {
if (checked.count(prereq)) continue;
scan_ops = scan_ops + 1;
if (!stack_set.count(prereq)) {
stack.push_back(prereq);
stack_set.insert(prereq);
}
}
}
if (start_size == stack.size()) {
stack.pop_back();
stack_set.erase(current);
checked.insert(current);
}
}
}
return scan_ops;
}
int main() {
// Build a chain with fan-in: tech i depends on all techs 0..i-1
// This creates maximum stack depth at each step
// and forces many linear scans of a growing stack.
constexpr int N = 500;
TechTree tree;
for (int i = 1; i < N; ++i) {
std::vector<int> prereqs;
// Each tech depends on the previous few techs (capped to avoid explosion)
int start = std::max(0, i - 50);
for (int j = start; j < i; ++j)
prereqs.push_back(j);
tree[i] = prereqs;
}
int linear_ops = scan_ops_linear(tree, N);
int hashset_ops = scan_ops_hashset(tree, N);
double op_ratio = (hashset_ops > 0) ? static_cast<double>(linear_ops) / hashset_ops : 999.0;
std::cout << "N=" << N << " techs (fan-in DAG)" << std::endl;
std::cout << "linear scan ops (defect): " << linear_ops << std::endl;
std::cout << "hashset ops (fix): " << hashset_ops << std::endl;
std::cout << "op ratio: " << op_ratio << "x" << std::endl;
// Timing
constexpr int ITERS = 50;
auto t0 = std::chrono::high_resolution_clock::now();
for (int r = 0; r < ITERS; ++r)
scan_ops_linear(tree, N);
auto t1 = std::chrono::high_resolution_clock::now();
for (int r = 0; r < ITERS; ++r)
scan_ops_hashset(tree, N);
auto t2 = std::chrono::high_resolution_clock::now();
auto linear_us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
auto hashset_us = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
double time_ratio = (hashset_us > 0) ? static_cast<double>(linear_us) / hashset_us : 999.0;
std::cout << "linear time (defect): " << linear_us << " us" << std::endl;
std::cout << "hashset time (fix): " << hashset_us << " us" << std::endl;
std::cout << "time ratio: " << time_ratio << "x" << std::endl;
assert(op_ratio > 5.0 && "Expected significant op-count reduction from hashset membership");
std::cout << "PASS" << std::endl;
return 0;
}