Skip to content

Commit dc97610

Browse files
authored
Update SBOL converter to be compatible with SynBioDex/SBOL-Converter (#3)
1 parent 328e147 commit dc97610

70 files changed

Lines changed: 7461 additions & 8471 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/sbol-cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ for the loss model.
163163
Notable flags:
164164

165165
- `--default-version <VERSION>` sets the version string assigned to
166-
top-level objects whose source didn't carry `backport:sbol2version`.
166+
top-level objects whose SBOL 3 IRI carried no version segment.
167167
Omit it to leave those objects unversioned; pass `--default-version 1`
168168
to match the SynBioHub / libSBOLj convention.
169169
- `--from <FORMAT>` overrides input-format inference (useful for SBOL 3

crates/sbol-cli/src/cli.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,11 @@ pub(crate) struct DowngradeArgs {
378378
#[arg(long, short = 'o', default_value = "-")]
379379
pub(crate) output: String,
380380

381-
/// Version string assigned to top-level objects whose source
382-
/// document didn't carry `backport:sbol2version`. Omit to leave
383-
/// such subjects unversioned (SBOL 2 makes `sbol2:version`
384-
/// optional); pass `--default-version 1` to match the libSBOLj /
385-
/// SynBioHub convention of always emitting one.
381+
/// Version string assigned to top-level objects whose SBOL 3 IRI
382+
/// carried no version segment. Omit to leave such subjects
383+
/// unversioned (SBOL 2 makes `sbol2:version` optional); pass
384+
/// `--default-version 1` to match the libSBOLj / SynBioHub
385+
/// convention of always emitting one.
386386
#[arg(long, value_name = "VERSION")]
387387
pub(crate) default_version: Option<String>,
388388

crates/sbol-cli/src/commands/downgrade.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,14 @@ pub(crate) fn downgrade(args: DowngradeArgs, styles: Styles) -> ExitCode {
105105
// Print the conversion summary to stderr, same style as upgrade.
106106
let counts = report.counts();
107107
eprintln!(
108-
"downgraded: {} CD, {} MD, {} split-into-both, {} SubComponent, \
109-
{} SequenceFeature, {} MapsTo, {} backport-restored, {} synthesized{}",
108+
"downgraded: {} CD, {} MD, {} SubComponent, \
109+
{} SequenceFeature, {} MapsTo, {} versioned, {} synthesized{}",
110110
counts.components_to_component_definition,
111111
counts.components_to_module_definition,
112-
counts.components_split_into_both,
113112
counts.sub_components_emitted,
114113
counts.sequence_features_emitted,
115114
counts.maps_to_reconstructed,
116-
counts.identities_restored_from_backport,
115+
counts.identities_versioned,
117116
counts.identities_synthesized,
118117
if report.warnings().is_empty() {
119118
String::new()
@@ -167,15 +166,6 @@ pub(crate) fn downgrade(args: DowngradeArgs, styles: Styles) -> ExitCode {
167166

168167
fn format_downgrade_warning(warning: &DowngradeWarning) -> String {
169168
match warning {
170-
DowngradeWarning::DualRoleComponent {
171-
component,
172-
component_definition,
173-
module_definition,
174-
} => format!(
175-
"Component <{component}> carries both structure and function; \
176-
split into ComponentDefinition <{component_definition}> + \
177-
ModuleDefinition <{module_definition}>"
178-
),
179169
DowngradeWarning::UnresolvableConstraintToMapsTo { constraint, reason } => {
180170
format!("Constraint <{constraint}> couldn't fold back into a MapsTo: {reason}")
181171
}

crates/sbol-cli/tests/upgrade.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,12 @@ fn upgrade_writes_sbol3_to_explicit_output() {
5555
contents.contains("http://sbols.org/v3#Component"),
5656
"output should contain SBOL 3 Component type: {contents}"
5757
);
58-
// SBOL 2 IRIs only appear as object values of backport predicates
59-
// (sbol2type, sbol2persistentIdentity, sbol2version) — never as subject
60-
// identities or active predicates.
61-
for line in contents.lines() {
62-
if let Some(predicate_start) = line.find("http://sbols.org/v2#") {
63-
let before = &line[..predicate_start];
64-
assert!(
65-
before.contains("http://sboltools.org/backport#"),
66-
"non-backport SBOL2 reference in line: {line}"
67-
);
68-
}
69-
}
58+
// The upgrade fully translates SBOL 2 classes and predicates to SBOL 3,
59+
// so no `sbols.org/v2#` IRI survives in the output.
60+
assert!(
61+
!contents.contains("http://sbols.org/v2#"),
62+
"SBOL 2 namespace leaked into the SBOL 3 output: {contents}"
63+
);
7064
}
7165

7266
#[test]

crates/sbol-convert/examples/sbol3_downgrade.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
5656
let (sbol2_graph, report) = sbol_convert::downgrade(&document)?;
5757
let counts = report.counts();
5858
println!(
59-
" {} CD, {} MD, {} SubComponent, {} SequenceFeature, {} backport-restored, {} synthesized",
59+
" {} CD, {} MD, {} SubComponent, {} SequenceFeature, {} versioned, {} synthesized",
6060
counts.components_to_component_definition,
6161
counts.components_to_module_definition,
6262
counts.sub_components_emitted,
6363
counts.sequence_features_emitted,
64-
counts.identities_restored_from_backport,
64+
counts.identities_versioned,
6565
counts.identities_synthesized,
6666
);
6767
if !report.warnings().is_empty() {

crates/sbol-convert/src/bin/generate-round-trip-report.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//!
99
//! The point is not regression gating; that lives in
1010
//! [`crates/sbol-convert/tests/upgrade_conformance.rs`] and
11-
//! [`crates/sbol-convert/tests/downgrade.rs`]. The point is empirical
11+
//! `crates/sbol-convert/tests/downgrade_round_trip.rs`. The point is empirical
1212
//! discovery: turning the question "which deferred enhancements
1313
//! actually matter on real data?" into "here are the triples that
1414
//! don't survive the round-trip, fixture by fixture."
@@ -231,8 +231,9 @@ fn render_report(results: &[FixtureResult]) -> String {
231231
preserve every triple across the committed SBOL 2 fixture corpus under \
232232
`tests/fixtures/sbol2/real/` (SBOLTestSuite, SynBioHub, and the \
233233
GenBank-derived intermediates). Native-SBOL-3 → SBOL 2 behavior (in \
234-
particular the dual-role Component split) is covered by unit tests \
235-
in [`crates/sbol-convert/tests/downgrade.rs`](../crates/sbol-convert/tests/downgrade.rs) \
234+
particular the Component → ComponentDefinition / ModuleDefinition \
235+
classification) is covered by unit tests \
236+
in `crates/sbol-convert/tests/downgrade_dual_role.rs` and \
236237
rather than this report, because doing so would require inventing \
237238
SBOL 3 fixtures from scratch.\n\n\
238239
For every fixture, the report runs:\n\n\
@@ -247,9 +248,10 @@ fn render_report(results: &[FixtureResult]) -> String {
247248
kinds of delta point at gaps in either the upgrade or the downgrade.\n\n\
248249
The report is informational. Conformance gating lives in \
249250
[`crates/sbol-convert/tests/upgrade_conformance.rs`](../crates/sbol-convert/tests/upgrade_conformance.rs) \
250-
and [`crates/sbol-convert/tests/downgrade.rs`](../crates/sbol-convert/tests/downgrade.rs). \
251-
The conversion model itself (the backport namespace, dual-role splits, \
252-
known divergences) is documented in [`conversion.md`](conversion.md).\n\n",
251+
and `crates/sbol-convert/tests/downgrade_round_trip.rs`. \
252+
The conversion model itself (the backport namespace, Component \
253+
classification, known divergences) is documented in \
254+
[`conversion.md`](conversion.md).\n\n",
253255
);
254256

255257
let n = results.len();
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
//! Regenerates the SynBioDex/SBOL-Converter reference conversions under
2+
//! `tests/fixtures/cross-impl-sbolconverter/expected/` by running the pinned
3+
//! reference converter in Docker. For each SBOL 2 fixture it records the
4+
//! reference SBOL 3 output (`<stem>.sbolconverter.to-sbol3.nt`); for each
5+
//! SBOL 3 fixture it records the reference SBOL 2 output
6+
//! (`<stem>.sbolconverter.to-sbol2.rdf`). Contributors run this locally after
7+
//! a reference version bump; CI never invokes it (CI only diffs the committed
8+
//! references via the `cross_impl_reference` integration test).
9+
//!
10+
//! Usage:
11+
//!
12+
//! docker build -t sbolconverter-pinned tests/fixtures/cross-impl-sbolconverter/
13+
//! cargo run -p sbol-convert --bin regenerate-cross-impl-reference
14+
//!
15+
//! Failure modes are fail-loud: missing Docker, image not built, or a
16+
//! non-zero exit from the container surface as an error and a non-zero exit.
17+
18+
use std::io::Write;
19+
use std::path::{Path, PathBuf};
20+
use std::process::{Command, ExitCode, Stdio};
21+
22+
const DOCKER_IMAGE: &str = "sbolconverter-pinned";
23+
24+
/// SBOL 2 fixtures whose reference SBOL 3 conversion is recorded. Paths are
25+
/// relative to `tests/fixtures/sbol2/`.
26+
const SBOL2_FIXTURES: &[(&str, &str)] = &[
27+
(
28+
"component_definition_output",
29+
"SBOLTestSuite/SBOL2/ComponentDefinitionOutput.xml",
30+
),
31+
(
32+
"module_definition_output",
33+
"SBOLTestSuite/SBOL2/ModuleDefinitionOutput.xml",
34+
),
35+
(
36+
"sequence_constraint_output",
37+
"SBOLTestSuite/SBOL2/SequenceConstraintOutput.xml",
38+
),
39+
(
40+
"repression_model",
41+
"SBOLTestSuite/SBOL2/RepressionModel.xml",
42+
),
43+
];
44+
45+
/// SBOL 3 fixtures whose reference SBOL 2 conversion is recorded. Paths are
46+
/// relative to `tests/fixtures/sbol3/`.
47+
const SBOL3_FIXTURES: &[(&str, &str)] = &[
48+
(
49+
"bba_f2620",
50+
"SBOLTestSuite/SBOL3/BBa_F2620_PoPSReceiver/BBa_F2620_PoPSReceiver.ttl",
51+
),
52+
(
53+
"model",
54+
"SBOLTestSuite/SBOL3/entity/model/model.ttl",
55+
),
56+
];
57+
58+
fn main() -> ExitCode {
59+
if let Err(err) = check_docker_available() {
60+
eprintln!("docker is required to regenerate reference conversions: {err}");
61+
return ExitCode::FAILURE;
62+
}
63+
if let Err(err) = check_image_present() {
64+
eprintln!(
65+
"image `{DOCKER_IMAGE}` not found: {err}\n\
66+
build it: docker build -t {DOCKER_IMAGE} tests/fixtures/cross-impl-sbolconverter/"
67+
);
68+
return ExitCode::FAILURE;
69+
}
70+
71+
let expected = fixture_root().join("cross-impl-sbolconverter/expected");
72+
if let Err(err) = std::fs::create_dir_all(&expected) {
73+
eprintln!("cannot create {}: {err}", expected.display());
74+
return ExitCode::FAILURE;
75+
}
76+
77+
let sbol2_root = fixture_root().join("sbol2");
78+
for (stem, rel) in SBOL2_FIXTURES {
79+
let src = sbol2_root.join(rel);
80+
let out = expected.join(format!("{stem}.sbolconverter.to-sbol3.nt"));
81+
match run_reference(&src, "to-sbol3", "nt", &out) {
82+
Ok(()) => eprintln!("regenerated {stem}.sbolconverter.to-sbol3.nt"),
83+
Err(err) => {
84+
eprintln!("FAILED {stem} (to-sbol3): {err}");
85+
return ExitCode::FAILURE;
86+
}
87+
}
88+
}
89+
90+
let sbol3_root = fixture_root().join("sbol3");
91+
for (stem, rel) in SBOL3_FIXTURES {
92+
let src = sbol3_root.join(rel);
93+
if !src.exists() {
94+
eprintln!("skip {stem}: SBOL 3 fixture not present ({})", src.display());
95+
continue;
96+
}
97+
let out = expected.join(format!("{stem}.sbolconverter.to-sbol2.rdf"));
98+
match run_reference(&src, "to-sbol2", "rdf", &out) {
99+
Ok(()) => eprintln!("regenerated {stem}.sbolconverter.to-sbol2.rdf"),
100+
Err(err) => {
101+
eprintln!("FAILED {stem} (to-sbol2): {err}");
102+
return ExitCode::FAILURE;
103+
}
104+
}
105+
}
106+
107+
ExitCode::SUCCESS
108+
}
109+
110+
fn run_reference(source: &Path, direction: &str, format: &str, output: &Path) -> std::io::Result<()> {
111+
let parent = source
112+
.parent()
113+
.ok_or_else(|| std::io::Error::other("source has no parent"))?;
114+
let file_name = source
115+
.file_name()
116+
.ok_or_else(|| std::io::Error::other("source has no file name"))?;
117+
let mount = format!("{}:/work:ro", parent.display());
118+
let container_input = format!("/work/{}", Path::new(file_name).display());
119+
120+
let result = Command::new("docker")
121+
.args([
122+
"run",
123+
"--rm",
124+
"-v",
125+
&mount,
126+
DOCKER_IMAGE,
127+
&container_input,
128+
direction,
129+
format,
130+
])
131+
.stdout(Stdio::piped())
132+
.stderr(Stdio::inherit())
133+
.output()?;
134+
if !result.status.success() {
135+
return Err(std::io::Error::other(format!(
136+
"reference container exited with status {:?}",
137+
result.status
138+
)));
139+
}
140+
141+
let mut handle = std::fs::File::create(output)?;
142+
handle.write_all(&result.stdout)?;
143+
if !result.stdout.ends_with(b"\n") {
144+
handle.write_all(b"\n")?;
145+
}
146+
Ok(())
147+
}
148+
149+
fn check_docker_available() -> std::io::Result<()> {
150+
let status = Command::new("docker").arg("info").output()?;
151+
if status.status.success() {
152+
Ok(())
153+
} else {
154+
Err(std::io::Error::other("`docker info` failed"))
155+
}
156+
}
157+
158+
fn check_image_present() -> std::io::Result<()> {
159+
let status = Command::new("docker")
160+
.args(["image", "inspect", DOCKER_IMAGE])
161+
.output()?;
162+
if status.status.success() {
163+
Ok(())
164+
} else {
165+
Err(std::io::Error::other("image not present"))
166+
}
167+
}
168+
169+
fn fixture_root() -> PathBuf {
170+
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
171+
path.pop();
172+
path.pop();
173+
path.push("tests/fixtures");
174+
path
175+
}

0 commit comments

Comments
 (0)