테스트와 CI
이 문서가 답하는 질문
- 이 저장소에 어떤 회귀 스위트가 있고 각각 무엇을 실제로 검증하는가?
- 각 스위트를 어떤 명령으로 돌리는가? 무엇이 사전 준비물인가?
- 통과/실패는 어떻게 판정되는가? 놓치는 것은 무엇인가?
- CI는 어디에 설정되어 있는가?
결론부터 — 스위트 지도
| # | 스위트 | 실행 명령 | 서버 필요 | 검증 범위 |
|---|---|---|---|---|
| 1 | Rust 유닛 테스트 | cargo test (engine/, bolt/) |
없음 | 렉서·파서·식별자 인코딩·RDF 파싱·PackStream |
| 2 | SQL 회귀 스위트 | tests/run.sh |
필요 | 확장의 인-데이터베이스 동작 전체 + 백업 왕복 + 무결성 |
| 3 | pg_regress 스캐폴딩 | (미배선) | — | 현재 동작하지 않음 — 아래 §3 |
| 4 | TypeQL 적합성 | python3 tests/typeql/run.py |
필요 | TypeDB 상류 예제 파일이 그대로 도는가 |
| 5 | Neo4j Movie Graph | python3 tests/neo4j-movies/run.py |
필요 (+ Bolt) | Neo4j 샘플 앱이 그대로 도는가, 드라이버 수준까지 |
| 6 | MCP 호환 | python3 examples/meeting-rooms/verify_mcp.py |
필요 (+ Bolt) | 공개 mcp-neo4j-cypher 서버가 무수정으로 도는가 |
| 7 | 벤치마크 회귀 게이트 | python3 bench/harness.py --compare-baseline … |
필요 | 성능 20% 회귀 — 05_benchmarking.md |
CI 설정 파일은 존재하지 않는다. 저장소 루트에
.github/디렉터리가 없고,Makefile·Jenkinsfile·.gitlab-ci.yml류도 없다. 위 스위트는 전부 수동 실행이다. → 10_improvements_ops.mdOPS-13
engine/src/lib.rs:45-48이 이 배치를 직접 설명한다:
// In-database behaviour is covered by the SQL regression suite in
// `engine/tests/sql/`, run with `tests/run.sh` against a live server. Pure Rust
// units (lexer, parser, identifier encoding, RDF parsing) live beside their
// modules and run under `cargo test`.
1. Rust 유닛 테스트 — cargo test
실행
docker exec ontological-dev bash -lc 'cd /work/engine && cargo test'
docker exec ontological-dev bash -lc 'cd /work/bolt && cargo test'
무엇이 있는가 (실측)
#[cfg(test)] mod tests가 존재하는 파일 전부:
| 파일 | 대상 |
|---|---|
engine/src/id.rs:93 |
64bit 식별자 인코딩(shard/type/local) |
engine/src/cypher/lexer.rs:266 |
Cypher 렉서 |
engine/src/cypher/parser.rs:1133 |
Cypher 파서 |
engine/src/typeql/lexer.rs:279 |
TypeQL 렉서 |
engine/src/typeql/parser.rs:964, :1058 |
TypeQL 파서 (모듈 2개) |
engine/src/typeql/compile.rs:776 |
TypeQL → SQL 컴파일 |
engine/src/adapters/rdf.rs:850 |
RDF 파싱 |
bolt/src/packstream.rs:285 |
PackStream 왕복, 청크 경계 (bolt/README.md:93) |
검증하지 않는 것
#[pg_test]어트리뷰트가 한 곳도 없다. 따라서cargo pgrx test는 실행할 인-데이터베이스 테스트가 없다.engine/Cargo.toml:18의pg_test = []feature와engine/Cargo.toml:36-37의pgrx-testsdev-dependency는 현재 활용되지 않는다.- 저장소·인접·순회·카탈로그 등 SPI를 쓰는 경로는 전부 스위트 2가 담당한다.
2. SQL 회귀 스위트 — tests/run.sh
이 저장소의 핵심 게이트다.
실행
# 컨테이너 안에서 (권장 — psql/pg_dump 버전 일치)
docker exec ontological-dev bash -lc 'cd /work && ./tests/run.sh'
# 호스트에서
PGHOST=localhost PGPORT=28816 ./tests/run.sh
적혀 있지 않은 전제: 데이터베이스 로케일.
initdb가 C 로케일로 돈 클러스터에서는04_neo4j_compat.sql이 실패한다.to_tsvector('simple', …)가 한글을 토큰화하지 못해db.index.fulltext.queryNodes가 0행을 내고, 그 파일의 단언이 걸린다. 코드 문제가 아니라 환경 문제이고, UTF-8 로케일 (initdb --encoding=UTF8 --locale=C.UTF-8)이면 통과한다.tests/run.sh는 이 의존성을 확인하지도, 알려주지도 않는다 — 실패했을 때 원인을 찾는 데 시간이 걸리는 종류다.
환경변수 (tests/run.sh:6-8):
| 변수 | 기본값 | 의미 |
|---|---|---|
PGHOST |
localhost |
|
PGPORT |
28816 |
|
OG_TEST_DB |
og_test |
테스트마다 DROP/CREATE되는 데이터베이스 이름 |
경고: 스크립트는
$OG_TEST_DB와${OG_TEST_DB}_restored를 무조건 DROP한다 (tests/run.sh:17,44,52). 기본값og_test를 다른 용도로 쓰고 있다면 반드시 바꿀 것. 운영 데이터베이스og는 건드리지 않는다.
흐름
단계 A — 파일별 회귀 (tests/run.sh:14-34)
engine/tests/sql/*.sql을 하나씩, 매번 새 데이터베이스에서 실행한다:
psql -d postgres -c "DROP DATABASE IF EXISTS $DB"
psql -d postgres -c "CREATE DATABASE $DB"
psql -d "$DB" -c "CREATE EXTENSION ontological CASCADE"
out="$(psql -d "$DB" -q -f "$f" 2>&1)"
판정 로직 (tests/run.sh:22-26):
expected="$(grep -c 'EXPECT_ERROR' "$f" || true)"
actual="$(printf '%s' "$out" | grep -c '^ERROR\|^psql.*ERROR' || true)"
if [ "$actual" -le "$expected" ]; then # ok
즉 의도된 오류는 파일 안에 EXPECT_ERROR 주석을 그 개수만큼 적어 선언한다. 실측 개수:
| 파일 | EXPECT_ERROR 수 |
검증 대상 |
|---|---|---|
engine/tests/sql/01_catalog_storage.sql |
1 | 타입 계층, 구간 라벨, 저장, 인접 (spec 001 + 002) |
engine/tests/sql/02_cypher.sql |
1 | Cypher 파싱 → 컴파일 → 실행 (spec 003) |
engine/tests/sql/03_vector_agent_rdf.sql |
0 | 벡터 / RDF / 에이전트 (spec 004 · 006 · 008) |
engine/tests/sql/04_neo4j_compat.sql |
0 | Neo4j 호환 표면 — "재작성 없이 도는가" (243줄) |
engine/tests/sql/05_reachability.sql |
0 | og_reach / og_reach_sql / CSR, 그리고 재작성 스위치. 모든 단언이 og_vlp와의 등가 비교다 |
05_reachability.sql의 헤더가 이 파일의 성격을 못 박는다: "nothing in this file measures anything — it only checks answers."
단계 B — 백업 왕복 (tests/run.sh:36-67)
별도 게이트로 분리된 이유가 주석에 있다 (tests/run.sh:37-39): 확장 소유 테이블은 pg_extension_config_dump() 등록 없이는 pg_dump에 잡히지 않고, 그것을 틀리면 조용히 빈 그래프가 복원된다.
절차:
- 새 DB + 확장 +
examples/demo.sql before=og_node_view개수/og_edge_view개수pg_dump -f /tmp/og_roundtrip.dump${DB}_restored에 복원after= 같은 카운트,query_ok=og_cypher('default','MATCH (w:Work) RETURN count(w) AS n')- 통과 조건 (
tests/run.sh:60):before = after그리고before가 비어 있지 않고 그리고before != "0/0"그리고query_ok가 비어 있지 않을 것
단계 C — 구조 무결성 (tests/run.sh:69-75)
psql -d "$DB" -tAc "SELECT count(*) FROM og_check_integrity()" | while read -r n; do
if [ "${n:-0}" = "0" ]; then echo "integrity ok"; else echo "integrity FAIL ($n violations)"; fi
done
알아둘 것: 이 블록은 파이프로
while서브셸에 들어가므로$fail카운터를 증가시키지 못한다. 즉 무결성 위반이 있어도tests/run.sh의 종료 코드는 0이 될 수 있다. 화면의integrity FAIL문구를 직접 확인해야 한다. →OPS-14
출력과 종료 코드
01_catalog_storage.sql ok
02_cypher.sql ok
...
backup round trip ok (69/104 nodes/edges preserved)
-- structural integrity --
integrity ok
5 passed, 0 failed
실패 시 실패 파일 목록을 출력하고 exit 1 (tests/run.sh:79). (위 카운트 값은 예시 형식이며 실제 숫자는 examples/demo.sql에 달려 있다.)
3. pg_regress 스캐폴딩 — 현재 동작하지 않음
engine/tests/pg_regress/에 있는 파일은 두 개뿐이다:
engine/tests/pg_regress/sql/setup.sql
engine/tests/pg_regress/expected/setup.out
engine/tests/pg_regress/sql/setup.sql 전문:
-- this setup file is run immediately after the regression database is (re)created
-- the file is optional but you likely want to create the extension
CREATE EXTENSION engine;
engine이라는 확장은 존재하지 않는다. 이 확장의 이름은 ontological이다 (engine/ontological.control, engine/Cargo.toml:2). 이 파일은 pgrx가 크레이트 디렉터리 이름(engine/)에서 생성한 템플릿 그대로이며, 수정된 적이 없다.
실제 회귀 테스트 파일(sql/*.sql / expected/*.out 쌍)도 없다.
결론: pg_regress 경로는 스캐폴딩만 있고 배선되어 있지 않다. 이 디렉터리를 근거로 "pg_regress 스위트가 있다"고 말해서는 안 된다. →
OPS-03
4. TypeQL 적합성 — tests/typeql/run.py
실행
python3 tests/typeql/run.py
python3 tests/typeql/run.py --db my_typeql_test
- 옵션은
--db하나뿐이다 (tests/typeql/run.py:95, 기본값og_typeql_test). - 환경변수:
PGHOST(localhost),PGPORT(28816) (tests/typeql/run.py:25-26). - 스크립트가
DROP DATABASE IF EXISTS <db>후CREATE DATABASE→CREATE EXTENSION ontological CASCADE→og_create_graph('bookstore')를 수행한다 (tests/typeql/run.py:100-110부근).
무엇이 검증되는가
docstring이 원칙을 명시한다 (tests/typeql/run.py:4-9): 스키마·데이터·질의가 전부 상류 TypeDB 예제 파일(examples/typedb/bookstore/)이며 무수정, 기대 결과는 그 예제의 README에 인쇄된 값이다.
판정은 순서 무관 집합 비교이고, 부동소수는 소수점 6자리로 반올림해 비교한다 (tests/typeql/run.py:63-71).
결과는 세 상태로 표시된다: ok / FAIL / unsupported (tests/typeql/run.py:39-46). unsupported는 실패가 아니라 미구현 표기다 — spec 010이 partial 상태인 것과 대응한다.
5. Neo4j Movie Graph — tests/neo4j-movies/run.py
bolt/README.md:97-100이 이 스위트를 "the real gate"라 부른다: Neo4j의 자체 드라이버로 구동하기 때문이다 — "Testing our server with our own client would not be evidence."
실행
# Ontological 양쪽 경로(PostgreSQL + Bolt)만
python3 tests/neo4j-movies/run.py
# 살아 있는 Neo4j와 비교까지
docker run -d --name bench-neo4j -p 27687:7687 \
-e NEO4J_AUTH=neo4j/benchpass123 neo4j:5
python3 tests/neo4j-movies/run.py --neo4j bolt://localhost:27687
# 공식 샘플 애플리케이션 — URI만 바꾸고 나머지 그대로
python3 tests/neo4j-movies/sample_app.py
(tests/neo4j-movies/README.md:32-43)
옵션과 환경변수 (tests/neo4j-movies/run.py:226-237)
| 옵션 | 기본값 (환경변수) |
|---|---|
--dsn |
OG_DSN, 기본 host=localhost por…(psycopg2 DSN) |
--pg-host |
localhost |
--pg-port |
OG_PGPORT |
--bolt-user |
OG_BOLT_USER, 기본 dev |
--bolt-password |
OG_BOLT_PASSWORD |
--no-bolt |
Bolt 단계 건너뜀 |
--neo4j |
비교 대상 Neo4j URI (기본 bolt://localhost:27687) |
--neo4j-user |
NEO4J_USER, 기본 neo4j |
--neo4j-password |
NEO4J_PASSWORD |
--no-neo4j |
Neo4j 비교 건너뜀 |
무엇이 검증되는가 (tests/neo4j-movies/README.md:8-25)
- 어느 포트가 Bolt를 말하는가 — raw 핸드셰이크로 확인한다. PostgreSQL 포트는 Bolt를 말하지 않으며 앞으로도 그럴 것이다 (spec 003 FR-024). 게이트웨이는 말한다.
- 데이터셋 — 상류
movies.cypher를 문장 단위로og_cypher에 통과. 차이는 하나: 타입을 먼저 선언한다(og_create_type), 여기서는 타입이 정체성의 일부이기 때문(spec 002). 가이드의CREATE CONSTRAINT두 문장은 제외된다. - 질의 24개 — 가이드 문서의 Cypher를 그대로, PostgreSQL 경로 / Bolt 경로 / 살아 있는 Neo4j 세 경로에서 실행하고 행 수를 비교. 실행은 되는데 개수가 다르면 실패다.
- 드라이버 수준 —
Node/Relationship하이드레이션, 필드 순서, 파라미터 바인딩, 실패 후RESET복구, 명시적 트랜잭션 commit/rollback(반대 경로에서 확인), 동시 세션 8개.
의존성: psycopg2 (필수), neo4j (Bolt·Neo4j 단계). movies.cypher는 최초 실행 시 raw.githubusercontent.com에서 내려받아 파일 옆에 캐시한다 (tests/neo4j-movies/run.py:40-43) — 네트워크가 필요하다.
종료 코드 0의 조건: 데이터셋이 깨끗이 적재되고, 가능한 모든 경로에서 모든 질의가 일치하고, 드라이버 검사가 통과할 때 (tests/neo4j-movies/README.md:49-50).
6. MCP 호환 — examples/meeting-rooms/verify_mcp.py
python3 examples/meeting-rooms/verify_mcp.py
공개된 mcp-neo4j-cypher 서버를 실제로 stdio로 띄워 MCP 프로토콜로 구동하고, 각 검사를 pass/fail 행렬로 출력한다 (examples/meeting-rooms/verify_mcp.py:1-9). Bolt 게이트웨이가 떠 있어야 한다 (bolt/README.md:82-88).
표준 검증 순서 (권장)
# 0. 서버가 살아 있는지
docker exec ontological-dev bash -lc 'pg_isready -h localhost -p 28816'
# 1. 순수 Rust — 가장 빠르고 서버가 필요 없다
docker exec ontological-dev bash -lc 'cd /work/engine && cargo test'
docker exec ontological-dev bash -lc 'cd /work/bolt && cargo test'
# 2. 확장을 재설치하고 SQL 회귀 스위트
docker exec ontological-dev bash -lc 'cd /work/engine && \
cargo pgrx install --features pg16 --no-default-features \
--pg-config /usr/lib/postgresql/16/bin/pg_config --sudo'
docker exec ontological-dev bash -lc 'cd /work && ./tests/run.sh'
# 3. 언어 표면 적합성
python3 tests/typeql/run.py
python3 tests/neo4j-movies/run.py
# 4. 성능 회귀
python3 bench/harness.py --compare-baseline bench/results/baseline.json
cargo pgrx install후에는 이미 열려 있는 백엔드가 옛.so를 잡고 있을 수 있다. 확실히 하려면cargo pgrx stop pg16→cargo pgrx start pg16으로 재기동할 것.
금지 / 필수
금지 (Forbidden)
cargo pgrx test를 회귀 게이트로 쓰지 말 것 —#[pg_test]가 없어 실행할 것이 없다.engine/tests/pg_regress/를 동작하는 스위트로 소개하지 말 것 (§3).tests/run.sh의 종료 코드만으로 무결성 통과를 판단하지 말 것 — 단계 C는 종료 코드에 반영되지 않는다.- 운영 데이터베이스가 있는 서버에서
OG_TEST_DB기본값을 그대로 쓰지 말 것.
필수 (Required)
- 확장 코드를 고쳤으면
cargo pgrx install→tests/run.sh순서로 돌릴 것. 재설치 없이 돌린 결과는 옛 바이너리에 대한 결과다. - 새 SQL 회귀 파일에서 오류를 의도했다면
EXPECT_ERROR를 그 개수만큼 적을 것. tests/run.sh출력의-- structural integrity --절을 눈으로 확인할 것.