Developing and testing interactive programs locally often proves cumbersome due to the intricacies of process communication. While tools like testlib.h are standard in competitive programming, they can sometimes be overly complex or dififcult to configure for quick debugging. An efficient alternative involves using native Linux features like named pipes or fork and exec to simulate the judging environment without heavy dependencies.
Data Generation
To facilitate testing, create a flexible data generator that accepts command-line arguments. This allows for rapid iteration over different test case constraints without recompiling the generator code.
#include <iostream>
#include <random>
#include <sstream>
#include <set>
// Basic random helper
std::mt19937 engine(std::random_device{}());
int generate_range(int low, int high) {
return engine() % (high - low + 1) + low;
}
int main(int argc, char **argv) {
int limit, value_bound;
std::istringstream(argv[1]) >> limit;
std::istringstream(argv[2]) >> value_bound;
std::cout << limit << "\n";
std::set<int> pool;
while (pool.size() < (size_t)limit) {
pool.insert(generate_range(0, value_bound));
}
for (int val : pool) std::cout << val << " ";
std::cout << std::endl;
return 0;
}
Designing the Interactor
An interactor must manage the communication bridge. By opening a local data file (cur.in) to initialize the problem state, the interactor can evaluate incoming queries from the solution process via standard I/O.
#include <fstream>
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::ifstream input_data("cur.in");
int n;
input_data >> n;
std::vector<int> secret_data(n);
for (int &val : secret_data) input_data >> val;
std::cout << n << std::endl; // Send N to solution
int query_count = 0;
while (true) {
char type;
if (!(std::cin >> type) || type != '?') break;
int val;
std::cin >> val;
query_count++;
int result = 0;
for (int secret : secret_data) result = std::max(result, secret ^ val);
std::cout << result << std::endl;
}
// Verify final result
std::vector<int> user_output(n);
for (int &val : user_output) std::cin >> val;
if (user_output != secret_data) return 1;
return 0;
}
Automating the Workflow with Bash
Named pipes (mkfifo) serve as an excellent mechanism for inter-process communication in Linux. By establishing two pipes, we create a bidirectional channel between the solution and the interactor.
#!/bin/bash
# Usage: ./test_runner.sh [data_params]
g++ -O2 generator.cpp -o gen
g++ -O2 solution.cpp -o sol
g++ -O2 interactor.cpp -o judge
mkfifo pipe_in pipe_out
for i in {1..100};
do
./gen $1 $2 > cur.in
./sol < pipe_in > pipe_out &
./judge < pipe_out > pipe_in
if [ $? -ne 0 ]; then
echo "Failed on test case $i"
break
fi
echo "Test $i passed"
done
rm pipe_in pipe_out
C++ Process Orchestration
For those who prefer a single C++ orchestrator, the fork, pipe, and dup2 system calls can manage the process lifecycle programmatically.
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv) {
int fds[2][2];
pipe(fds[0]); pipe(fds[1]);
bool is_child = fork() == 0;
// Redirect standard streams based on process role
dup2(fds[is_child][0], STDIN_FILENO);
dup2(fds[!is_child][1], STDOUT_FILENO);
// Close unused descriptors
for(int i=0; i<2; ++i) {
close(fds[i][0]);
close(fds[i][1]);
}
char *args[] = {argv[is_child + 1], nullptr};
execvp(args[0], args);
return 0;
}
This approach effectively separates logic from testing infrastructure, providing a clean, modular environment for validating complex interactive algorithms.