Categories: 알고리즘
스트레스 테스트 해보기
스트레스 테스트
이전 소프트웨어 마에스트로 내부 대회에서 알고리즘 문제를 출제하면서 코드포스 폴리곤을 이용했었다. 폴리곤에는 엄밀한 검증을 위한 여러가지 툴들을 제공하는데, 그 중에 하나가 스트레스 테스트였다. 스트레스 테스트란 정답 코드와 검증이 완료되지 않은 코드를 랜덤으로 틀린 경우가 나올 때까지 실행시키는 테스트를 말한다. 사실 써볼일은 없어서 사용해보진 않았는데 대충 다음과 같은 절차로 진행되는 것 같았다.
- 테스트케이스를 만들
generator
등록 - 테스트케이스 검증을 위한
validator
등록 - 두 결과물 비교를 위한
checker
등록 - 두 솔루션 선택 및 시간 제한 설정
stress
시작
이를 좀 모방해서 직접 자동화하는 방식으로 해보면 어떨까 생각만 하고 있었다. 이번에 수열과 쿼리 27문제를 풀이하면서 이 테스트 방식으로 코너케이스를 찾았고 AC를 받을 수 있었는데, 포스팅을 진행하고자 한다.
준비물
아래는 챙겨야할 준비물이다.
bash
터미널gcc
컴파일러-
AC는 받을 수 없으나 작은 제한에서 58000% 확률로 맞을 수 있는 솔루션 코드
- (옵션)
make
물론 윈도우 환경에서 powerShell
이나 msvc
컴파일러를 다룰 수 있다면 그것으로 해도 전혀 상관없다. 이 글에서는 위 세 가지를 가지고 있다는 가정하에 진행한다. 또, make
는 컴파일 과정을 좀 편하게 해주기 때문에 써주었는데, 컴파일이야 손수 다 해주면 되기 때문에 전적으로 옵션이다.
파일 구조는 아래와 같이 해주었다.
stress-test
├── 1.cpp
├── 2.cpp
├── Makefile
├── test.cpp
└── testcase.cpp
1.cpp
와 2.cpp
는 비교할 두 솔루션 코드이고 하나는 반드시 정답을 보이는 정답코드이다. test.cpp
는 테스트를 돌려주는 메인함수, testcase.cpp
는 테스트 케이스를 만드는 프로그램이다.
쉘 스크립트를 짜듯이 자동화 스크립트를 만드는 것과 사실 동일한데, 비교 함수 작성들을 감안하면 프로그램 언어로 짜는게 좋을 것 같아 C++로 진행했다.
Makefile
CC=g++
CFLAGS=-fdiagnostics-color=always -g -Wall -O2
all: $(patsubst %.cpp,%.out,$(wildcard ./*.cpp))
%.out: %.cpp
$(CC) $(CFLAGS) -o $@ $<
clean:
rm ./*.out
make
는 어디까지나 옵션이기에 그냥 듣고 흘려도 괜찮다.
CC=g++
CFLAGS=-fdiagnostics-color=always -g -Wall -O2
- 컴파일러를
g++
로 지정한다. - 컴파일 옵션을
-fdiagnostics-color=always
-g
-Wall
-O2
준다.
각 컴파일 옵션에 대해서는 사실 크게 중요하진 않고, 문제풀이에서는 웬만하면 똑같다. 궁금하면 검색해보자.
all: $(patsubst %.cpp,%.out,$(wildcard ./*.cpp))
patsubst
은 특정 패턴을 교체할 때 사용하는 make
함수인데, $(wildcard ./*.cpp)
파일로 1.cpp
2.cpp
test.cpp
testcase.cpp
을 가져오면 %.cpp
패턴을 인식해서 %.out
패턴으로 바꿔준다. 따라서 1.out
2.out
test.out
testcase.out
이랑 똑같아진다. 귀찮은데 하드코딩하는게 더 나을지도 모르겠다.
%.out: %.cpp
$(CC) $(CFLAGS) -o $@ $<
%.out
패턴에 해당하는 명령을 인식하면 %.cpp
를 의존으로 하는 다음 명령을 실행한다는 의미의 구문이다. test.out
이 호출되면 test.out: test.cpp
가 된다. $@
는 test.out
, $<
는 test.cpp
가 되어 실질적으로 컴파일이 진행된다.
따라서 위 모든 코드는 그저 다음의 명령어를 자동으로 수행해줄 뿐이다.
g++ -fdiagnostics-color=always -g -Wall -O2 -o 1.out test.cpp
g++ -fdiagnostics-color=always -g -Wall -O2 -o 2.out test.cpp
g++ -fdiagnostics-color=always -g -Wall -O2 -o test.out test.cpp
g++ -fdiagnostics-color=always -g -Wall -O2 -o testcase.out testcase.cpp
make
는 각 파일별로 의존성을 파악해주기 때문에, 수정한 파일에 대해서만 컴파일을 진행할 수 있고, 불필요한 자원 낭비를 방지할 수 있다. 1.cpp
파일만 수정하면 알아서 1.out
에 대해서만 컴파일을 진행한다.
clean:
rm ./*.out
make clean
은 만들어진 .out
파일을 모두 제거해준다. 자동으로 해두면 여러모로 편리하다. 여하튼 make
는 완전히 옵션이고 잘 모르겠으면 다음으로 넘어가도 좋다. 복사 붙여넣기 하자
test.cpp
#include <stdlib.h>
#include <string>
#include <iostream>
#include <fstream>
using namespace std;
void compile() {
// system("g++ -g -O2 -Wall 1.cpp -o 1.out");
// system("g++ -g -O2 -Wall 2.cpp -o 2.out");
// system("g++ -g -O2 -Wall testcase.cpp -o testcase.out");
}
void make_testcase() {
system("./testcase.out > testcase.txt");
}
void run_test() {
system("./1.out < testcase.txt > 1.o");
system("./2.out < testcase.txt > 2.o");
}
bool compare() {
ifstream fcin1, fcin2;
fcin1.open("1.o");
fcin2.open("2.o");
// system("diff -c 1.o 2.o");
string R1, R2;
bool flag = 1;
while (getline(fcin1, R1) && getline(fcin2, R2)) {
if (R1 != R2) {
flag = 0; break;
}
}
if (getline(fcin1, R1) || getline(fcin2, R2))
flag = 0;
fcin1.close();
fcin2.close();
return flag;
}
int main() {
compile();
int i = 1;
do {
cout << i++ << '\n';
make_testcase();
run_test();
} while (compare());
}
사실 코드는 짜보면 간단한데, 왠지 모를 뇌정지가 오는 부분들이 조금 있다. 각 테스트는 항상 실행을 먼저 해줘야하므로 do while
문을 이용했으며, 프로그램이 돌아가는지 안돌아가는지 답답해서 i
값을 찍어주어 몇번째 테스트케이스인지 보이도록 했다. 그 외에 설명이 필요한 부분만 약간씩 하도록 하겠다.
void compile() {
// system("g++ -g -O2 -Wall 1.cpp -o 1.out");
// system("g++ -g -O2 -Wall 2.cpp -o 2.out");
// system("g++ -g -O2 -Wall testcase.cpp -o testcase.out");
}
컴파일 부분이다. make
를 사용하지 않는다면 system
함수를 이용해 컴파일을 수행하는 쉘 명령어를 실행시키자. 컴파일은 한 번만 진행하면 되기 때문에 main()
함수에서 시작과 동시에 한 번만 실행시킨다.
void make_testcase() {
system("./testcase.out > testcase.txt");
}
void run_test() {
system("./1.out < testcase.txt > 1.o");
system("./2.out < testcase.txt > 2.o");
}
마찬가지로 테스트케이스 생성과 첫번째, 두번째 솔루션을 실행시키는 쉘 명령어인데, <execute-file> > <file>
문법은 execute-file
을 실행시켜 나오는 표준입출력을 file
로 리다이렉션하는 것이다. 즉, 결과물이 파일로 출력된다.
bool compare() {
ifstream fcin1, fcin2;
fcin1.open("1.o");
fcin2.open("2.o");
system("diff -c 1.o 2.o");
string R1, R2;
bool flag = 1;
while (getline(fcin1, R1) && getline(fcin2, R2)) {
if (R1 != R2) {
flag = 0; break;
}
}
if (getline(fcin1, R1) || getline(fcin2, R2))
flag = 0;
fcin1.close();
fcin2.close();
return flag;
}
두 파일을 비교하는 함수이다. 하나씩 줄을 읽어와서 비교하며, 파일 끝까지 진행되었을때 더 얻게되는 문자열이 있는지 비교하고 종료한다. 다만 비교하고 어떤 부분이 다른지는 배쉬에 있는 diff -c <file1> <file2>
명령어를 사용하면 간단하고 예쁘게 출력할 수 있다. 나는 귀찮아서 직접 구현하지 않고, 이를 이용해 출력했다.
실행
make
를 이용한다면 이미 모두 컴파일이 되어있을 것이고, 아니라면 test.cpp
를 컴파일해주자.
g++ -fdiagnostics-color=always -g -Wall -O2 -o test.out test.cpp
컴파일이 완료되면 실행만하면 된다.
./test.out
단,
세그폴트 앞에서는 만인이 평등하니 주의하자.. 그냥 다시 풀자
아주 깔끔하게, 틀린 테스트 케이스를 찾을 수 있었다. 그리고 아직 testcase.txt
에는 마지막 테스트케이스가 기록되어 있으므로 가져다 쓰면 될 것이다.
스트레스 테스트를 적극적으로 이용하는 것을 권장한다. 스스로 테스트 케이스를 만드는 것도 분명 중요하지만, 스스로 만들 수 없는 테스트 케이스도 분명 있기 때문이다. 지쳐서 만들기도 힘들다.. 계속 그렇게 거르지도 못하는 테스트 케이스를 만들다간 스트레스 받아서 탈모가 올지도 모른다..
자동화 스크립트는 파이썬이나 shell을 활용하는게 일반적인데, 파이썬은 속도가 느리고, shell
은 숙련도가 떨어져서 사용할 엄두를 내질 못했다. 언젠가는 shell
로 만들어봐야겠다.
Leave a comment