Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/robot-integrity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: ROBOT integrity checks

on:
push:
branches:
- '**'

env:
ROBOT_VERSION: 1.9.10
ROBOT_JAVA_ARGS: -Xmx4g

jobs:
robot-integrity:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Set up Java
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '21'

- name: Cache ROBOT jar
uses: actions/cache@v6
with:
path: tests/scripts/.robot
key: robot-${{ env.ROBOT_VERSION }}

- name: Install ROBOT
run: tests/scripts/install-robot.sh

- name: Run ROBOT integrity checks
id: robot-check
run: tests/scripts/robot-check.sh

- name: Upload unsatisfiability explanation
if: failure() && steps.robot-check.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: robot-unsatisfiable-explanation
path: unsatisfiable-explanation.owl
if-no-files-found: ignore

- name: Run ROBOT report tests
id: robot-report
run: tests/scripts/robot-report-tests.sh

- name: Upload ROBOT report
if: failure() && steps.robot-report.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: robot-report
path: robot-report.tsv
if-no-files-found: ignore
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<<<<<<< HEAD
# OS
.DS_Store

Expand All @@ -14,3 +15,8 @@ tmp/
# Sphinx documentation
docs/_build/
docs/source/

# ROBOT testing temporary artifacts
tests/scripts/.robot/
unsatisfiable-explanation.owl
robot-report.tsv
6 changes: 3 additions & 3 deletions RadLex.owl
Original file line number Diff line number Diff line change
Expand Up @@ -926927,7 +926927,7 @@ of the nodule.</RID:Definition>
</rdfs:subClassOf>
<RID:ExternalRefID>FMA:308886</RID:ExternalRefID>
<RID:RadLexID xml:lang="en">RID47581</RID:RadLexID>
<RID:Synonym xml:lang="en">superior subscapular nerve component of dorsal root of right fifth cervical </RID:Synonym>
<RID:Synonym xml:lang="en">superior subscapular nerve component of dorsal root of right fifth cervical nerve</RID:Synonym>
<rdfs:label xml:lang="en">superior subscapular nerve component of posterior root of right fifth cervical nerve</rdfs:label>
<rdfs:label xml:lang="de">zum Nervus subscapularis superior gehörender Teil der Radix posterior des rechten Nervus cervicalis V</rdfs:label>
</owl:Class>
Expand Down Expand Up @@ -926964,7 +926964,7 @@ of the nodule.</RID:Definition>
</rdfs:subClassOf>
<RID:ExternalRefID>FMA:308888</RID:ExternalRefID>
<RID:RadLexID xml:lang="en">RID47582</RID:RadLexID>
<RID:Synonym xml:lang="en">superior subscapular nerve component of dorsal root of left fifth cervical nerve </RID:Synonym>
<RID:Synonym xml:lang="en">superior subscapular nerve component of dorsal root of left fifth cervical nerve</RID:Synonym>
<rdfs:label xml:lang="en">superior subscapular nerve component of posterior root of left fifth cervical nerve</rdfs:label>
<rdfs:label xml:lang="de">zum Nervus subscapularis superior gehörender Teil der Radix posterior des linken Nervus cervicalis V</rdfs:label>
</owl:Class>
Expand Down Expand Up @@ -979983,7 +979983,7 @@ clean (individual elements are visible )
<RID:Radlex_version_of_class_change>4.2 pref, syn</RID:Radlex_version_of_class_change>
<RID:Source>PI-RADS 2</RID:Source>
<RID:Synonym xml:lang="en">extraprostatic or periprostatic tumor extension</RID:Synonym>
<RID:Synonym xml:lang="en">periprostatic or extraprostatic tumor extension</RID:Synonym>
<RID:Synonym xml:lang="en">periprostatic or extraprostatic tumor extension</RID:Synonym>
<rdfs:label xml:lang="de">Tumorinvasion des periprostatischen Bereichs</rdfs:label>
<rdfs:label xml:lang="en">tumor invasion of periprostatic or extraprostatic area</rdfs:label>
</owl:Class>
Expand Down
115 changes: 115 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Testing RadLex.owl

RadLex is tested for logical and structural integrity using
[ROBOT](https://robot.obolibrary.org/), a command-line tool for working with
OWL ontologies. These checks catch problems like inconsistent logic,
accidental duplicate/merged terms, and other curation mistakes before they
land in `RadLex.owl`.

## Continuous integration

Every push to any branch triggers the **ROBOT integrity checks** GitHub
Actions workflow, defined in
[`.github/workflows/robot-integrity.yml`](../.github/workflows/robot-integrity.yml).
The workflow:

1. Installs ROBOT.
2. Runs a reasoner-based consistency check (`tests/scripts/robot-check.sh`).
3. Runs SPARQL-query-based checks (`tests/scripts/robot-report-tests.sh`).

If either check fails, the workflow run is marked failed and the relevant
output is uploaded as a build artifact:

- `robot-unsatisfiable-explanation` — a minimal explanation of why the
ontology is logically inconsistent or incoherent, if the reasoner check
fails.
- `robot-report`: The `robot-report.tsv` violations report, if the
SPARQL-query-based checks fail.

To download an artifact from a failed run, open the run on GitHub and scroll
to the **Artifacts** section at the bottom of the summary page (or use
`gh run download <run-id>`).

## Running the tests locally

All the scripts below live in `tests/scripts/` and are meant to be run from
the repository root.

### 1. Install ROBOT

```sh
tests/scripts/install-robot.sh
```

This downloads the pinned ROBOT version and verifies it against a pinned
checksum, installing it to `tests/scripts/.robot/`. The script prints the
`export PATH=...` line you need; run it (or add it to your shell profile) so
that plain `robot` commands resolve to this install:

```sh
export PATH="$PWD/tests/scripts/.robot:$PATH"
```

You only need to re-run `install-robot.sh` when the pinned `ROBOT_VERSION`
changes — it's a no-op if the correct version is already installed.

### 2. Run the reasoner-based consistency check

```sh
tests/scripts/robot-check.sh
```

This runs the ELK reasoner over `RadLex.owl` and fails if the ontology is
logically inconsistent, has unsatisfiable classes, or if the reasoner infers
equivalences between classes that weren't explicitly asserted (a common
symptom of accidentally duplicated or merged terms). On failure, see
`unsatisfiable-explanation.owl` for details.

### 3. Run the SPARQL-query-based checks

```sh
tests/scripts/robot-report-tests.sh
```

This runs `robot report` using the queries and severity levels listed in
[`tests/robot-report-profile.txt`](../tests/robot-report-profile.txt), and
writes the full results to `robot-report.tsv`. Open that file to see exactly
which terms triggered which violation.

### Adding a new SPARQL check

1. **Add the query file.** Create a new `.rq` SPARQL query file under
`tests/sparql/robot/`, e.g. `tests/sparql/robot/my_new_check.rq`.

2. **Bind exactly three variables.** `robot report`
([documentation](https://robot.obolibrary.org/report.html)) requires every custom
query to be a `SELECT` that returns exactly `?entity`, `?property`, and
`?value` - no more, no fewer:
- `?entity`: The offending entity (e.g. a class or annotation subject).
If `?entity` is unbound for a row, that row can't be reported.
- `?property`: The property involved in the violation (e.g.
`rdfs:label`).
- `?value`: The value that's causing the violation (e.g. the malformed
string).

A row returned by the query is a violation; a query that returns no rows
means no violations. See
[`tests/sparql/robot/leading_trailing_multi_whitespace.rq`](../tests/sparql/robot/leading_trailing_multi_whitespace.rq)
for a working example.

3. **Reference it from the profile.** Add a line to
[`tests/robot-report-profile.txt`](../tests/robot-report-profile.txt) with
the severity level, a tab, then `file:` followed by the query's path
relative to the repo root:

```
ERROR file:./tests/sparql/robot/my_new_check.rq
```

Severity is one of `ERROR`, `WARN`, or `INFO`. `ERROR`-level violations
fail the check (and therefore the CI job); `WARN`/`INFO` are reported but
don't fail it.

4. **Run it locally** with `tests/scripts/robot-report-tests.sh` to confirm
the check behaves as expected — check `robot-report.tsv` for the results —
before pushing.
1 change: 1 addition & 0 deletions tests/robot-report-profile.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ERROR file:./tests/sparql/robot/leading_trailing_multi_whitespace.rq
55 changes: 55 additions & 0 deletions tests/scripts/install-robot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/bash
set -euo pipefail

#
# install ROBOT (https://robot.obolibrary.org/) for local development or CI
#
# Downloads the pinned ROBOT_VERSION jar and the official `robot` launcher
# script from the ontodev/robot GitHub repo, verifying both against pinned
# checksums, into ROBOT_INSTALL_DIR. Add that directory to your PATH to run
# `robot` directly, e.g.:
# export PATH="$PWD/tests/scripts/.robot:$PATH"
#

ROBOT_VERSION="${ROBOT_VERSION:-1.9.10}"
ROBOT_JAR_SHA256="${ROBOT_JAR_SHA256:-16a73c074f3df359a7338a84b4e0788785fe06117f931bb9796e9619ea776105}"
ROBOT_SCRIPT_SHA256="${ROBOT_SCRIPT_SHA256:-0f2c4bb7dc25be9fb1dad37f2b8c07336d0e2c0ef3d3983e952c23d8b180a5b0}"
ROBOT_INSTALL_DIR="${ROBOT_INSTALL_DIR:-tests/scripts/.robot}"

sha256_check() {
local file="$1" expected="$2" actual
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$file" | awk '{print $1}')"
else
actual="$(shasum -a 256 "$file" | awk '{print $1}')"
fi
if [ "$actual" != "$expected" ]; then
echo "Checksum mismatch for $file: expected $expected, got $actual" >&2
return 1
fi
}

mkdir -p "$ROBOT_INSTALL_DIR"
ROBOT_INSTALL_DIR="$(cd "$ROBOT_INSTALL_DIR" && pwd)"

if [ "$(cat "$ROBOT_INSTALL_DIR/VERSION" 2>/dev/null)" != "$ROBOT_VERSION" ]; then
echo "Downloading ROBOT v${ROBOT_VERSION}..." >&2

curl -fsSL "https://github.com/ontodev/robot/releases/download/v${ROBOT_VERSION}/robot.jar" \
--output "$ROBOT_INSTALL_DIR/robot.jar"
sha256_check "$ROBOT_INSTALL_DIR/robot.jar" "$ROBOT_JAR_SHA256"

curl -fsSL "https://raw.githubusercontent.com/ontodev/robot/v${ROBOT_VERSION}/bin/robot" \
--output "$ROBOT_INSTALL_DIR/robot"
sha256_check "$ROBOT_INSTALL_DIR/robot" "$ROBOT_SCRIPT_SHA256"
chmod +x "$ROBOT_INSTALL_DIR/robot"

echo "$ROBOT_VERSION" > "$ROBOT_INSTALL_DIR/VERSION"
fi

if [ -n "${GITHUB_PATH:-}" ]; then
echo "$ROBOT_INSTALL_DIR" >> "$GITHUB_PATH"
else
echo "ROBOT installed to $ROBOT_INSTALL_DIR/robot" >&2
echo "Add it to your PATH: export PATH=\"$ROBOT_INSTALL_DIR:\$PATH\"" >&2
fi
13 changes: 13 additions & 0 deletions tests/scripts/robot-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
set -ex

#
# run ROBOT reasoner-based ontology integrity tests
#

robot reason \
--input RadLex.owl \
--reasoner ELK \
--equivalent-classes-allowed asserted-only \
--dump-unsatisfiable unsatisfiable-explanation.owl \
--output /tmp/RadLex-reasoned.owl
13 changes: 13 additions & 0 deletions tests/scripts/robot-report-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
set -ex

#
# run SPARQL-query-based ontology integrity tests
#

robot report \
--input RadLex.owl \
--profile tests/robot-report-profile.txt \
--labels true \
--output robot-report.tsv \
--limit 50
23 changes: 23 additions & 0 deletions tests/sparql/robot/leading_trailing_multi_whitespace.rq
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX oio: <http://www.geneontology.org/formats/oboInOwl#>
PREFIX rid: <http://www.radlex.org/RID/>

#
# Part of the RadLex ontology SPARQL test suite. Bindings to this query represent violations of the test.
#
# Test for stray whitespace on values (strings) of the annotation properties used for names. Specifically, this test
# prohibits: leading or trailing whitespace, or consecutive whitespace characters within a string.
#
# VIOLATIONS will be bindings that have, on ?entity, ?property, and ?value respectively:
# - The entity on which the violation occurs (e.g., the class).
# - The property on which the violation occurs (i.e., the annotation property, e.g. rdfs:label).
# - The value violating the constraint (e.g., "anatomy " or "anatomic part")
#
SELECT ?entity ?property ?value
WHERE {
VALUES ?constrained_property {rdfs:label rid:Synonym rid:UMLS_Term} .
?property rdfs:subPropertyOf ?constrained_property .
?entity ?property ?value .
FILTER(REGEX(STR(?value), "^\\s|\\s+$|\\s\\s"))
}