Add TurtleGeneratorTests (#1528)

* Add TurtleGeneratorTests for R5

* Finish TurtleGeneratorTests setup

* Switch unit test to added example resource, disable others
This commit is contained in:
Tim Prudhomme 2024-02-20 15:25:12 -05:00 committed by GitHub
parent e42079e482
commit 7761be6197
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 333 additions and 0 deletions

View File

@ -0,0 +1,199 @@
package org.hl7.fhir.r5.test;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Properties;
import org.fhir.ucum.UcumException;
import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.elementmodel.Manager;
import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
import org.hl7.fhir.r5.elementmodel.ResourceParser;
import org.hl7.fhir.r5.elementmodel.TurtleParser;
import org.hl7.fhir.r5.elementmodel.XmlParser;
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.test.utils.TestingUtilities;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.hl7.fhir.utilities.turtle.Turtle;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
* TurtleGeneratorTests
* Generates turtle files from specified resources, including example "instances"
* Unit tests for the generated turtle files
* For generic RDF parsing tests, see `TurtleTests.java`
* For ShEx validation tests, see `ShExGeneratorTests.java`
* Author: Tim Prudhomme <tmprdh@gmail.com>
*/
public class TurtleGeneratorTests {
private static IWorkerContext workerContext;
private static ResourceParser resourceParser;
private static XmlParser xmlParser;
private static TurtleParser turtleParser;
private static Path inputXmlDirectory;
private static Path outputTurtleDirectory;
@BeforeAll
public static void setup() throws IOException {
workerContext = TestingUtilities.getSharedWorkerContext();
resourceParser = new org.hl7.fhir.r5.elementmodel.ResourceParser(workerContext);
xmlParser = (XmlParser) Manager.makeParser(workerContext, FhirFormat.XML);
turtleParser = (TurtleParser) Manager.makeParser(workerContext, FhirFormat.TURTLE);
// Temporary directory of files that should be discarded after testing
outputTurtleDirectory = FileSystems.getDefault().getPath(System.getProperty("java.io.tmpdir"));
// Directory of XML files used for generating Turtle files
String currentDirectory = System.getProperty("user.dir");
inputXmlDirectory = FileSystems.getDefault().getPath(currentDirectory, "src", "test", "resources", "testUtilities", "xml", "examples");
}
@Test
public void testExamples() throws IOException, UcumException {
var exampleInstanceName = "codesystem-contact-point-use";
testInstanceGeneration(exampleInstanceName);
}
@Disabled("TODO this doesn't pass due to existing issues in R5 RDF")
@Test
public void testProfiles() throws IOException, UcumException {
var profileName = "Encounter";
testClassGeneration(profileName);
}
@Disabled("Run manually for testing with XML resources generated from FHIR specification publishing library")
@Test
public void testPublishedExamples() throws IOException, UcumException {
inputXmlDirectory = getPublishedXmlDirectory();
var exampleInstanceName = "codesystem-contact-point-use";
testInstanceGeneration(exampleInstanceName);
}
/*
* Generate a Turtle file from the name of an XML resource, then parse it
*/
private void testInstanceGeneration(String resourceName) throws IOException, UcumException {
// Generate Turtle
var generatedTurtleFilePath = generateTurtleFromResourceName(resourceName, inputXmlDirectory, outputTurtleDirectory);
// Try parsing again ("round-trip test") -- this only tests for valid RDF
parseGeneratedTurtle(generatedTurtleFilePath);
}
/*
* Generate a Turtle file from the name of a profile, then parse it
*/
private void testClassGeneration(String profileName) throws IOException, UcumException {
var generatedTurtleFilePath = generateTurtleClassFromProfileName(profileName);
// Try parsing again ("round-trip test") -- this only tests for valid RDF
parseGeneratedTurtle(generatedTurtleFilePath);
}
private void parseGeneratedTurtle(String generatedTurtleFilePath) throws IOException {
try (
InputStream turtleStream = new FileInputStream(generatedTurtleFilePath);
) {
var generatedTurtleString = new String(turtleStream.readAllBytes());
Turtle ttl = new Turtle();
ttl.parse(generatedTurtleString);
}
}
/**
* Generate a Turtle version of a resource, given its name, input directory of its XML source, and output directory of the Turtle file
* @return the path of the generated Turtle file
*/
private String generateTurtleFromResourceName(String resourceName, Path inputXmlDirectory, Path outputTurtleDirectory) throws IOException, UcumException {
// Specify source xml path and destination turtle path
var xmlFilePath = inputXmlDirectory.resolve(resourceName + ".xml").toString();
var turtleFilePath = outputTurtleDirectory.resolve(resourceName + ".ttl").toString();
try (
InputStream inputXmlStream = new FileInputStream(xmlFilePath);
OutputStream outputTurtleStream = new FileOutputStream(turtleFilePath);
) {
// print out file names using string interpolation
System.out.println("Generating " + turtleFilePath);
generateTurtleFromXmlStream(inputXmlStream, outputTurtleStream);
return turtleFilePath;
}
}
/**
* Generate a Turtle file from an XML resource
*/
private void generateTurtleFromXmlStream(InputStream xmlStream, OutputStream turtleStream) throws IOException, UcumException {
var errorList = new ArrayList<ValidationMessage>();
Element resourceElement = xmlParser.parseSingle(xmlStream, errorList);
turtleParser.compose(resourceElement, turtleStream, OutputStyle.PRETTY, null);
// Check errors
for (ValidationMessage m : errorList) {
System.out.println(m.getDisplay());
}
}
/**
* Generate a Turtle file from an org.hl7.fhir.r5.model.Resource profile
* @return the path of the generated Turtle file
*/
private String generateTurtleClassFromProfileName(String profileName) throws IOException, UcumException {
String resourceUri = ProfileUtilities.sdNs(profileName, null);
Resource resource = workerContext.fetchResource(Resource.class, resourceUri);
Element resourceElement = resourceParser.parse(resource);
var turtleFilePath = outputTurtleDirectory.resolve(profileName + ".ttl").toString();
try (OutputStream outputStream = new FileOutputStream(turtleFilePath)) {
turtleParser.compose(resourceElement, outputStream, OutputStyle.PRETTY, null);
return turtleFilePath;
}
}
/**
* Generate a Turtle file from a "test case" resource -- those only available on https://github.com/FHIR/fhir-test-cases/
* @return the path of the generated Turtle file
*/
private String generateTurtleFromTestCaseResource(String resourceName) throws IOException, UcumException {
var turtleFilePath = outputTurtleDirectory.resolve(resourceName + ".ttl").toString();
try (
// Follows pattern in `TestingUtilities.java`
InputStream inputXmlStream = TestingUtilities.loadTestResourceStream("r5", resourceName + ".xml");
OutputStream outputTurtleStream = new FileOutputStream(turtleFilePath);
) {
generateTurtleFromXmlStream(inputXmlStream, outputTurtleStream);
return turtleFilePath;
}
}
/**
* This could be the "publish" directory of XML resources built using the FHIR specification publishing library.
* Use this for testing with other generated XML resources
*/
private static Path getPublishedXmlDirectory() throws IOException {
Properties properties = new Properties();
String currentDirectory = System.getProperty("user.dir");
// Add your directory path to "org.hl7.fhir.r5/src/test/resources/local.properties"
String localPropertiesPath = FileSystems.getDefault().getPath(currentDirectory, "src", "test", "resources", "local.properties").toString();
try (FileInputStream input = new FileInputStream(localPropertiesPath)) {
properties.load(input);
} catch (IOException e) {
// You should create this local.properties file if it doesn't exist. It should already be listed in .gitignore.
e.printStackTrace();
throw e;
}
var filePath = properties.getProperty("xmlResourceDirectory");
return FileSystems.getDefault().getPath(filePath);
}
}

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<CodeSystem xmlns="http://hl7.org/fhir">
<id value="contact-point-use"/>
<meta>
<lastUpdated value="2023-10-03T22:51:29.574-04:00"/>
</meta>
<text>
<status value="generated"/>
<div xmlns="http://www.w3.org/1999/xhtml">
<p>This case-sensitive code system
<code>http://hl7.org/fhir/contact-point-use</code> defines the following codes:
</p>
<table class="codes">
<tr>
<td style="white-space:nowrap">
<b>Code</b>
</td>
<td>
<b>Display</b>
</td>
<td>
<b>Definition</b>
</td>
</tr>
<tr>
<td style="white-space:nowrap">home
<a name="contact-point-use-home"> </a>
</td>
<td>Home</td>
<td>A communication contact point at a home; attempted contacts for business purposes might intrude privacy and chances are one will contact family or other household members instead of the person one wishes to call. Typically used with urgent cases, or if no other contacts are available.</td>
</tr>
<tr>
<td style="white-space:nowrap">work
<a name="contact-point-use-work"> </a>
</td>
<td>Work</td>
<td>An office contact point. First choice for business related contacts during business hours.</td>
</tr>
<tr>
<td style="white-space:nowrap">temp
<a name="contact-point-use-temp"> </a>
</td>
<td>Temp</td>
<td>A temporary contact point. The period can provide more detailed information.</td>
</tr>
<tr>
<td style="white-space:nowrap">old
<a name="contact-point-use-old"> </a>
</td>
<td>Old</td>
<td>This contact point is no longer in use (or was never correct, but retained for records).</td>
</tr>
<tr>
<td style="white-space:nowrap">mobile
<a name="contact-point-use-mobile"> </a>
</td>
<td>Mobile</td>
<td>A telecommunication device that moves and stays with its owner. May have characteristics of all other use codes, suitable for urgent matters, not the first choice for routine business.</td>
</tr>
</table>
</div>
</text>
<extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-wg">
<valueCode value="fhir"/>
</extension>
<extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status">
<valueCode value="normative"/>
</extension>
<extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-normative-version">
<valueCode value="4.0.0"/>
</extension>
<extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm">
<valueInteger value="5"/>
</extension>
<url value="http://hl7.org/fhir/contact-point-use"/>
<identifier>
<system value="urn:ietf:rfc:3986"/>
<value value="urn:oid:2.16.840.1.113883.4.642.1.74"/>
</identifier>
<version value="6.0.0-cibuild"/>
<name value="ContactPointUse"/>
<title value="ContactPointUse"/>
<status value="active"/>
<experimental value="false"/>
<date value="2023-10-03T22:51:29-04:00"/>
<publisher value="HL7 (FHIR Project)"/>
<contact>
<telecom>
<system value="url"/>
<value value="http://hl7.org/fhir"/>
</telecom>
<telecom>
<system value="email"/>
<value value="fhir@lists.hl7.org"/>
</telecom>
</contact>
<description value="Use of contact point."/>
<jurisdiction>
<coding>
<system value="http://unstats.un.org/unsd/methods/m49/m49.htm"/>
<code value="001"/>
<display value="World"/>
</coding>
</jurisdiction>
<caseSensitive value="true"/>
<valueSet value="http://hl7.org/fhir/ValueSet/contact-point-use"/>
<content value="complete"/>
<concept>
<code value="home"/>
<display value="Home"/>
<definition value="A communication contact point at a home; attempted contacts for business purposes might intrude privacy and chances are one will contact family or other household members instead of the person one wishes to call. Typically used with urgent cases, or if no other contacts are available."/>
</concept>
<concept>
<code value="work"/>
<display value="Work"/>
<definition value="An office contact point. First choice for business related contacts during business hours."/>
</concept>
<concept>
<code value="temp"/>
<display value="Temp"/>
<definition value="A temporary contact point. The period can provide more detailed information."/>
</concept>
<concept>
<code value="old"/>
<display value="Old"/>
<definition value="This contact point is no longer in use (or was never correct, but retained for records)."/>
</concept>
<concept>
<code value="mobile"/>
<display value="Mobile"/>
<definition value="A telecommunication device that moves and stays with its owner. May have characteristics of all other use codes, suitable for urgent matters, not the first choice for routine business."/>
</concept>
</CodeSystem>