Skip to content

Commit

Permalink
[wip] update tests and docs for expected handling of multi-output all…
Browse files Browse the repository at this point in the history
… by-ref
  • Loading branch information
fmigneault committed Oct 2, 2024
1 parent aadc2ed commit bf2b539
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 17 deletions.
30 changes: 21 additions & 9 deletions docs/source/processes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -874,8 +874,10 @@ Following is a detailed listing of the expected response structure according to
| | | | | - using embedded content parts with data |
+---------------------+--------------+---------------+-----------+-------------------------------------------------+
| |na| | ``raw`` | ``reference`` | >1 | - :ref:`Multipart <job-results-raw-multi>` |
| [#resPreferReturn]_ | | (for *all*) | | content [#resCTypeMulti]_ |
| | | | | - using embedded content parts with data |
| [#resPreferReturn]_ | | (for *all*) | | content with embedded part links if requested |
| | | | | by ``Accept`` header [#resCTypeMulti]_ |
| | | | | - otherwise, similar to |res-ref|, but with |
| | | | | a ``Link`` header for each requested output |
+---------------------+--------------+---------------+-----------+-------------------------------------------------+
| |none| | ``document`` | |none| | |any| | - :ref:`Results <job-results-document-minimal>` |
| | | | | content |
Expand Down Expand Up @@ -987,10 +989,14 @@ Following is a detailed listing of the expected response structure according to
representation using other encoding (e.g.: :term:`XML` or :term:`YAML`) could be returned if requested by
the ``Accept`` header.
For every other case where a return ``representation`` or ``raw`` results are explicitly requested,
For cases where a return ``representation`` or ``raw`` response results are explicitly requested,
and that no ``Accept`` header explicitly requests an alternative representation,
the :ref:`Multipart Results <job-results-raw-multi>` structure
using ``multipart`` contents (:rfc:`2046#section-5.1`) is employed by default.
The representation of each part (as literal data or link reference [#resValRef]_)
using ``multipart`` contents (:rfc:`2046#section-5.1`) is employed by default, unless *all* requested
outputs resolve to a :ref:`File Reference <file_ref_types>`. In such case, the references will be contained
in ``Link`` headers, similar to the |res-ref|_ response, but with multiple links for all requested outputs.
When resolved as ``multipart``, the representation of each part (as literal data or link reference [#resValRef]_)
is established by the ``transmissionMode`` parameter combinations, or as applicable according to the ``Accept``
and the ``Prefer: return`` headers. Alternatively to requesting ``representation`` or ``raw`` results,
the :ref:`Multipart Results <job-results-raw-multi>` structure *could* also be requested explicitly
Expand Down Expand Up @@ -2154,10 +2160,16 @@ a combination of ``Content-ID``, ``Content-Type`` and ``Content-Location`` will
To respect :rfc:`2392` definitions, ``Content-ID`` will use pattern ``<{outputID}@{jobID}>`` as unique identifier,
and ``<{outputID}.{index}@{jobID}>`` in the case of an array of :ref:`File References <file_ref_types>`.

When the number of *requested* ``outputs`` [#outN]_ is more than one, the response will
either be ``multipart`` contents (:rfc:`2046#section-5.1`) or similar to
the :ref:`Document Result <job-results-document-minimal>` contents,
accordingly to the negotiated ``Accept`` content header. An example of a ``multipart`` representation is shown below.
When the number of *requested* ``outputs`` [#outN]_ is more than one, the obtained response will depend
on the negotiated ``Accept`` content header and the data/link resolution of each output.

1. If all outputs are :ref:`File References <file_ref_types>` and no ``Accept`` header was specified, a no-content
response with a ``Link`` for each output similarly to the above :ref:`job-results-raw-single-ref` is returned.
2. If a ``response=document`` or ``Prefer: return=minimal`` resolution is requested, outputs are
embedded in the :ref:`Document Result <job-results-document-minimal>` contents.
3. If either ``multipart`` contents (:rfc:`2046#section-5.1`) are explicitly requested by ``Accept`` header, or that
the above cases were not encountered, a multipart content response as shown below is returned [#resCTypeMulti]_.

The resolution of the nested outputs within each boundary, either by value or reference, will resolve
for each respective output according to the same rule conditions specified above for single output.

Expand Down
102 changes: 94 additions & 8 deletions tests/functional/test_wps_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -3739,7 +3739,7 @@ def test_execute_single_output_prefer_header_return_minimal_complex(self):
assert results.headers["Content-Location"] == results_href
assert ("Link", output_json_link) in results.headerlist
assert not any(
any(out_id in link[-1] for out_id in ["output_datta", "output_text"])
any(out_id in link[-1] for out_id in ["output_data", "output_text"])
for link in results.headerlist if link[0] == "Link"
), "Filtered outputs should not be found in results response links."
outputs = self.app.get(f"/jobs/{job_id}/outputs", params={"schema": JobInputsOutputsSchema.OGC_STRICT})
Expand Down Expand Up @@ -3875,15 +3875,15 @@ def test_execute_single_output_response_raw_reference_literal(self):
out_url = get_wps_output_url(self.settings)
results = self.app.get(f"/jobs/{job_id}/results")
results_href = f"{self.url}/processes/{p_id}/jobs/{job_id}/results"
output_json_href = f"{out_url}/{job_id}/output_json/result.json"
output_json_link = f"<{output_json_href}>; rel=\"output_json\"; type=\"{ContentType.APP_JSON}\""
output_data_href = f"{out_url}/{job_id}/output_data/output_data.txt"
output_data_link = f"<{output_data_href}>; rel=\"output_data\"; type=\"{ContentType.TEXT_PLAIN}\""
assert results.status_code == 204, "No contents expected for minimal reference result."
assert results.body == b""
assert results.content_type is None
assert results.headers["Content-Location"] == results_href
assert ("Link", output_json_link) in results.headerlist
assert ("Link", output_data_link) in results.headerlist
assert not any(
any(out_id in link[-1] for out_id in ["output_datta", "output_text"])
any(out_id in link[-1] for out_id in ["output_json", "output_text"])
for link in results.headerlist if link[0] == "Link"
), "Filtered outputs should not be found in results response links."
outputs = self.app.get(f"/jobs/{job_id}/outputs", params={"schema": JobInputsOutputsSchema.OGC_STRICT})
Expand Down Expand Up @@ -3936,7 +3936,7 @@ def test_execute_single_output_response_raw_reference_complex(self):
assert results.headers["Content-Location"] == results_href
assert ("Link", output_json_link) in results.headerlist
assert not any(
any(out_id in link[-1] for out_id in ["output_datta", "output_text"])
any(out_id in link[-1] for out_id in ["output_data", "output_text"])
for link in results.headerlist if link[0] == "Link"
), "Filtered outputs should not be found in results response links."
outputs = self.app.get(f"/jobs/{job_id}/outputs", params={"schema": JobInputsOutputsSchema.OGC_STRICT})
Expand Down Expand Up @@ -4490,7 +4490,15 @@ def test_execute_multi_output_response_raw_value(self):
},
}

def test_execute_multi_output_response_raw_reference(self):
def test_execute_multi_output_response_raw_reference_default_links(self):
"""
All outputs resolved as reference (explicitly or inferred) with raw representation should be all Link headers.
The multipart representation of the corresponding request must ask for it explicitly.
.. seealso::
- :func:`test_execute_multi_output_response_raw_reference_accept_multipart`
"""
proc = "EchoResultsTester"
p_id = self.fully_qualified_test_process_name(proc)
body = self.retrieve_payload(proc, "deploy", local=True)
Expand Down Expand Up @@ -4525,11 +4533,89 @@ def test_execute_multi_output_response_raw_reference(self):

job_id = status["jobID"]
out_url = get_wps_output_url(self.settings)
results = self.app.get(f"/jobs/{job_id}/results")
results_href = f"{self.url}/processes/{p_id}/jobs/{job_id}/results"
output_data_href = f"{out_url}/{job_id}/output_data/output_data.txt"
output_data_link = f"<{output_data_href}>; rel=\"output_data\"; type=\"{ContentType.TEXT_PLAIN}\""
output_json_href = f"{out_url}/{job_id}/output_json/result.json"
output_json_link = f"<{output_json_href}>; rel=\"output_json\"; type=\"{ContentType.APP_JSON}\""
assert results.status_code == 204, "No contents expected for minimal reference result."
assert results.body == b""
assert results.content_type is None
assert results.headers["Content-Location"] == results_href
assert ("Link", output_data_link) in results.headerlist
assert ("Link", output_json_link) in results.headerlist
assert not any(
any(out_id in link[-1] for out_id in ["output_text"])
for link in results.headerlist if link[0] == "Link"
), "Filtered outputs should not be found in results response links."
outputs = self.app.get(f"/jobs/{job_id}/outputs", params={"schema": JobInputsOutputsSchema.OGC_STRICT})
assert outputs.content_type.startswith(ContentType.APP_JSON)
assert outputs.json["outputs"] == {
"output_data": {
"value": "test"
},
"output_json": {
"href": f"{out_url}/{job_id}/output_json/result.json",
"type": ContentType.APP_JSON,
},
}

def test_execute_multi_output_response_raw_reference_accept_multipart(self):
"""
Requesting ``multipart`` explicitly should return it instead of default ``Link`` headers response.
.. seealso::
- :func:`test_execute_multi_output_response_raw_reference_default_links`
- :func:`test_execute_multi_output_multipart_accept_async_alt_acceptable`
- :func:`test_execute_multi_output_multipart_accept_async_not_acceptable`
"""
proc = "EchoResultsTester"
p_id = self.fully_qualified_test_process_name(proc)
body = self.retrieve_payload(proc, "deploy", local=True)
self.deploy_process(body, process_id=p_id)

# NOTE:
# No 'response' nor 'Prefer: return' to ensure resolution is done by 'Accept' header
# without 'Accept' using multipart, it is expected that JSON document is used
# Also, use 'Prefer: wait' to avoid 'respond-async', since async always respond with the Job status.
exec_headers = {
"Accept": ContentType.MULTIPART_MIXED,
"Content-Type": ContentType.APP_JSON,
"Prefer": "wait=5",
}
exec_content = {
"inputs": {
"message": "test"
},
"outputs": {
"output_json": {}, # should use 'reference' by default
"output_data": {"transmissionMode": ExecuteTransmissionMode.REFERENCE},
}
}
with contextlib.ExitStack() as stack:
for mock_exec in mocked_execute_celery():
stack.enter_context(mock_exec)
path = f"/processes/{p_id}/execution"
resp = mocked_sub_requests(self.app, "post_json", path, timeout=5,
data=exec_content, headers=exec_headers, only_local=True)
assert resp.status_code == 200, f"Failed with: [{resp.status_code}]\nReason:\n{resp.json}"

# rely on location that should be provided to find the job ID
results_url = get_header("Content-Location", resp.headers)
assert results_url, (
"Content-Location should have been provided in"
"results response pointing at where they can be found."
)
job_id = results_url.rsplit("/results")[0].rsplit("/jobs/")[-1]
assert is_uuid(job_id), f"Failed to retrieve the job ID: [{job_id}] is not a UUID"
out_url = get_wps_output_url(self.settings)

results = self.app.get(f"/jobs/{job_id}/results")
boundary = parse_kvp(results.headers["Content-Type"])["boundary"][0]
results_body = inspect.cleandoc(f"""
--{boundary}
Content-Disposition: attachment; name="output_data" filename="output_data.txt"
Content-Disposition: attachment; name="output_data"
Content-Type: {ContentType.TEXT_PLAIN}
Content-Location: {out_url}/{job_id}/output_data/output_data.txt
Content-ID: <output_data@{job_id}>
Expand Down

0 comments on commit bf2b539

Please sign in to comment.