Skip to content

Commit 169c15a

Browse files
committed
added initial unit tests
1 parent 5c13277 commit 169c15a

11 files changed

Lines changed: 611 additions & 3 deletions

‎.github/workflows/tests.yml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: unit-tests
2+
3+
on:
4+
push:
5+
branches:
6+
- "**"
7+
pull_request:
8+
9+
jobs:
10+
test:
11+
name: unit-tests (${{ matrix.os }})
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os:
17+
- ubuntu-latest
18+
- macos-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Setup Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.11"
27+
28+
- name: Run unit tests
29+
run: python -m unittest discover -s test -p "test_*_unit.py" -v

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ RMLStreamer-v2.5.0-standalone.jar
1010
out/
1111
tsv/
1212
run_metrics/
13+
__pycache__/
14+
*.py[cod]
15+
.venv/

‎README.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ The wrapper runs RMLStreamer with working directory `/data/rules`.
7171
If your mapping refers to TSVs using relative paths, make them relative to the rules file.
7272
You can also use absolute container paths like `/data/tsv/<file>.tsv`.
7373

74+
## Testing
75+
76+
Run unit tests:
77+
```
78+
python -m unittest discover -s test -p "test_*_unit.py" -v
79+
```
80+
81+
Test suite notes:
82+
- Tests live in `test/`.
83+
- External tools are mocked (Docker, Java/RMLStreamer, gzip, brotli, rdf2hdt).
84+
- GitHub Actions runs this suite on each push and pull request (`.github/workflows/tests.yml`).
85+
7486

7587
## Manual Setup (Legacy)
7688

‎src/compression.sh‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ while getopts ":m:h" opt; do
4747
esac
4848
done
4949

50+
COMPRESSION_METHODS_CSV=${COMPRESSION_METHODS//,/|}
51+
5052
DO_GZIP=0
5153
DO_BROTLI=0
5254
DO_HDT=0
@@ -285,9 +287,9 @@ for OUT in "${OUTPUT_DIRS[@]}"; do
285287
EXIT_CODE_BROTLI=0
286288

287289
if have_gnu_time; then
288-
/usr/bin/time -v -o "$TIME_LOG_BROTLI" -- brotli -q 7 "$BIG_NQ" || EXIT_CODE_BROTLI=$?
290+
/usr/bin/time -v -o "$TIME_LOG_BROTLI" -- brotli -q 7 -k "$BIG_NQ" || EXIT_CODE_BROTLI=$?
289291
else
290-
{ time -p brotli -q 7 "$BIG_NQ"; } >"$TIME_LOG_BROTLI" 2>&1 || EXIT_CODE_BROTLI=$?
292+
{ time -p brotli -q 7 -k "$BIG_NQ"; } >"$TIME_LOG_BROTLI" 2>&1 || EXIT_CODE_BROTLI=$?
291293
fi
292294

293295
BROTLI_SIZE=$(stat_size "$BROTLI_PATH")
@@ -436,7 +438,7 @@ EOF
436438
-v user_seconds_hdt="$USER_SEC_HDT" \
437439
-v sys_seconds_hdt="$SYS_SEC_HDT" \
438440
-v max_rss_kb_hdt="$MAX_RSS_KB_HDT" \
439-
-v compression_methods="$COMPRESSION_METHODS" \
441+
-v compression_methods="$COMPRESSION_METHODS_CSV" \
440442
-v conv_exit_code="" \
441443
-v conv_wall="" \
442444
-v conv_user="" \

‎test/README.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Test Suite Coverage Rationale
2+
3+
This repository uses `unittest` (Python standard library) to isolate orchestration logic and shell script behavior without calling real external tools (Docker daemon, RMLStreamer, Brotli, HDT).
4+
5+
## What is covered
6+
7+
- `test/test_vcf_rdfizer_unit.py`
8+
- Verifies wrapper control flow for the 5-step pipeline.
9+
- Verifies image/version resolution behavior and error handling.
10+
- Verifies CLI compression option propagation into `compression.sh`.
11+
12+
- `test/test_vcf_as_tsv_unit.py`
13+
- Verifies `.vcf` and `.vcf.gz` input handling.
14+
- Verifies header normalization (`#CHROM` -> `CHROM`) and data row retention.
15+
- Verifies error path for empty input directories.
16+
17+
- `test/test_run_conversion_unit.py`
18+
- Replaces `java` with a fake executable to avoid real RMLStreamer.
19+
- Verifies output normalization to `.nq`.
20+
- Verifies unified metrics CSV row creation and schema consistency.
21+
22+
- `test/test_compression_unit.py`
23+
- Replaces `gzip`, `brotli`, and `rdf2hdt` with fake executables.
24+
- Verifies compression artifact generation and metrics row update.
25+
- Verifies `-m none` behavior (no compression outputs, metrics still updated).
26+
27+
## Why this coverage is useful
28+
29+
- It tests the highest-risk logic in this codebase: orchestration, branching, path wiring, and metrics consistency.
30+
- It catches regressions in command construction without requiring heavyweight dependencies.
31+
- It validates that metrics remain comparable across runs by enforcing one shared CSV schema.

‎test/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

‎test/helpers.py‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import csv
2+
import os
3+
from pathlib import Path
4+
5+
6+
METRICS_HEADER = [
7+
"run_id",
8+
"timestamp",
9+
"output_name",
10+
"output_dir",
11+
"exit_code_java",
12+
"wall_seconds_java",
13+
"user_seconds_java",
14+
"sys_seconds_java",
15+
"max_rss_kb_java",
16+
"input_mapping_size_bytes",
17+
"input_vcf_size_bytes",
18+
"output_dir_size_bytes",
19+
"output_triples",
20+
"jar",
21+
"mapping_file",
22+
"output_path",
23+
"combined_nq_size_bytes",
24+
"gzip_size_bytes",
25+
"brotli_size_bytes",
26+
"hdt_size_bytes",
27+
"exit_code_gzip",
28+
"exit_code_brotli",
29+
"exit_code_hdt",
30+
"wall_seconds_gzip",
31+
"user_seconds_gzip",
32+
"sys_seconds_gzip",
33+
"max_rss_kb_gzip",
34+
"wall_seconds_brotli",
35+
"user_seconds_brotli",
36+
"sys_seconds_brotli",
37+
"max_rss_kb_brotli",
38+
"wall_seconds_hdt",
39+
"user_seconds_hdt",
40+
"sys_seconds_hdt",
41+
"max_rss_kb_hdt",
42+
"compression_methods",
43+
]
44+
45+
46+
def make_executable(path: Path, content: str) -> None:
47+
path.write_text(content)
48+
path.chmod(path.stat().st_mode | 0o111)
49+
50+
51+
def seed_conversion_metrics_row(
52+
metrics_csv: Path, run_id: str, timestamp: str, output_name: str, output_dir: Path
53+
) -> None:
54+
metrics_csv.parent.mkdir(parents=True, exist_ok=True)
55+
with metrics_csv.open("w", newline="") as f:
56+
writer = csv.writer(f, lineterminator="\n")
57+
writer.writerow(METRICS_HEADER)
58+
writer.writerow(
59+
[
60+
run_id,
61+
timestamp,
62+
output_name,
63+
str(output_dir),
64+
"0",
65+
"0.12",
66+
"0.10",
67+
"0.02",
68+
"10240",
69+
"100",
70+
"200",
71+
"300",
72+
"3",
73+
"fake.jar",
74+
"rules.ttl",
75+
str(output_dir),
76+
]
77+
+ [""] * 20
78+
)
79+
80+
81+
def env_with_path(bin_dir: Path) -> dict[str, str]:
82+
env = os.environ.copy()
83+
env["PATH"] = f"{bin_dir}:{env['PATH']}"
84+
return env

‎test/test_compression_unit.py‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import csv
2+
import subprocess
3+
import tempfile
4+
import unittest
5+
from pathlib import Path
6+
7+
from test.helpers import env_with_path, make_executable, seed_conversion_metrics_row
8+
9+
10+
REPO_ROOT = Path(__file__).resolve().parents[1]
11+
SCRIPT = REPO_ROOT / "src" / "compression.sh"
12+
13+
14+
def prepare_fake_tools(bin_dir: Path):
15+
make_executable(
16+
bin_dir / "gzip",
17+
"""#!/usr/bin/env bash
18+
set -euo pipefail
19+
file="${@: -1}"
20+
cp "$file" "$file.gz"
21+
""",
22+
)
23+
make_executable(
24+
bin_dir / "brotli",
25+
"""#!/usr/bin/env bash
26+
set -euo pipefail
27+
file="${@: -1}"
28+
cp "$file" "$file.br"
29+
""",
30+
)
31+
hdt = bin_dir / "rdf2hdt.sh"
32+
make_executable(
33+
hdt,
34+
"""#!/usr/bin/env bash
35+
set -euo pipefail
36+
cp "$1" "$2"
37+
""",
38+
)
39+
return hdt
40+
41+
42+
def read_metrics_row(metrics_csv: Path, run_id: str, output_name: str):
43+
with metrics_csv.open() as f:
44+
rows = list(csv.DictReader(f))
45+
for row in rows:
46+
if row["run_id"] == run_id and row["output_name"] == output_name:
47+
return row
48+
raise AssertionError(f"Metrics row not found for run_id={run_id}, output_name={output_name}")
49+
50+
51+
class CompressionUnitTests(unittest.TestCase):
52+
def test_compression_updates_existing_metrics_row_with_mocked_tools(self):
53+
with tempfile.TemporaryDirectory() as td:
54+
tmp_path = Path(td)
55+
out_root = tmp_path / "out"
56+
output = out_root / "rdf"
57+
output.mkdir(parents=True)
58+
(output / "chunk-a.nq").write_text("<s> <p> <o> .\n")
59+
(output / "chunk-b.nq").write_text("<s2> <p2> <o2> .\n")
60+
61+
logdir = tmp_path / "metrics"
62+
run_id = "run-compress-1"
63+
timestamp = "2026-01-01T00:00:00"
64+
metrics_csv = logdir / "metrics.csv"
65+
seed_conversion_metrics_row(metrics_csv, run_id, timestamp, "rdf", output)
66+
67+
fake_bin = tmp_path / "bin"
68+
fake_bin.mkdir()
69+
hdt_path = prepare_fake_tools(fake_bin)
70+
71+
env = env_with_path(fake_bin)
72+
env.update(
73+
{
74+
"OUT_ROOT_DIR": str(out_root),
75+
"OUT_NAME": "rdf",
76+
"LOGDIR": str(logdir),
77+
"RUN_ID": run_id,
78+
"TIMESTAMP": timestamp,
79+
"RDF2HDT": str(hdt_path),
80+
}
81+
)
82+
83+
result = subprocess.run(
84+
["bash", str(SCRIPT), "-m", "gzip,brotli,hdt"],
85+
env=env,
86+
capture_output=True,
87+
text=True,
88+
)
89+
90+
self.assertEqual(result.returncode, 0, msg=result.stderr)
91+
self.assertTrue((output / "rdf.nq.gz").exists())
92+
self.assertTrue((output / "rdf.nq.br").exists())
93+
self.assertTrue((output / "rdf.hdt").exists())
94+
95+
row = read_metrics_row(metrics_csv, run_id, "rdf")
96+
self.assertEqual(row["run_id"], run_id)
97+
self.assertEqual(row["output_name"], "rdf")
98+
self.assertEqual(row["exit_code_java"], "0")
99+
self.assertEqual(row["compression_methods"], "gzip|brotli|hdt")
100+
self.assertEqual(row["exit_code_gzip"], "0")
101+
self.assertEqual(row["exit_code_brotli"], "0")
102+
self.assertEqual(row["exit_code_hdt"], "0")
103+
self.assertGreater(int(row["combined_nq_size_bytes"]), 0)
104+
105+
def test_compression_none_updates_metrics_without_generating_outputs(self):
106+
with tempfile.TemporaryDirectory() as td:
107+
tmp_path = Path(td)
108+
out_root = tmp_path / "out"
109+
output = out_root / "rdf"
110+
output.mkdir(parents=True)
111+
(output / "chunk-a.nq").write_text("<s> <p> <o> .\n")
112+
113+
logdir = tmp_path / "metrics"
114+
run_id = "run-compress-2"
115+
timestamp = "2026-01-01T00:00:00"
116+
metrics_csv = logdir / "metrics.csv"
117+
seed_conversion_metrics_row(metrics_csv, run_id, timestamp, "rdf", output)
118+
119+
fake_bin = tmp_path / "bin"
120+
fake_bin.mkdir()
121+
hdt_path = prepare_fake_tools(fake_bin)
122+
123+
env = env_with_path(fake_bin)
124+
env.update(
125+
{
126+
"OUT_ROOT_DIR": str(out_root),
127+
"OUT_NAME": "rdf",
128+
"LOGDIR": str(logdir),
129+
"RUN_ID": run_id,
130+
"TIMESTAMP": timestamp,
131+
"RDF2HDT": str(hdt_path),
132+
}
133+
)
134+
135+
result = subprocess.run(
136+
["bash", str(SCRIPT), "-m", "none"],
137+
env=env,
138+
capture_output=True,
139+
text=True,
140+
)
141+
142+
self.assertEqual(result.returncode, 0, msg=result.stderr)
143+
self.assertFalse((output / "rdf.nq.gz").exists())
144+
self.assertFalse((output / "rdf.nq.br").exists())
145+
self.assertFalse((output / "rdf.hdt").exists())
146+
147+
row = read_metrics_row(metrics_csv, run_id, "rdf")
148+
self.assertEqual(row["compression_methods"], "none")
149+
self.assertEqual(row["combined_nq_size_bytes"], "0")
150+
self.assertEqual(row["gzip_size_bytes"], "0")
151+
self.assertEqual(row["brotli_size_bytes"], "0")
152+
self.assertEqual(row["hdt_size_bytes"], "0")
153+
154+
155+
if __name__ == "__main__":
156+
unittest.main()

0 commit comments

Comments
 (0)