def test_jsonarray2netcdf_execute_async_output_by_reference_dontcare_response_document( self): """ Jobs submitted with ``response=document`` are not impacted by ``transmissionMode``. The results schema should always be returned when document is requested. .. seealso:: https://docs.ogc.org/is/18-062r2/18-062r2.html#req_core_process-execute-sync-document """ with contextlib.ExitStack() as stack_exec: body, nc_data = self.setup_inputs(stack_exec) body.update({ "response": ExecuteResponse. DOCUMENT, # by value/reference don't care because of this "outputs": [{ "id": "output", "transmissionMode": ExecuteTransmissionMode.REFERENCE }], }) for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) path = "/processes/jsonarray2netcdf/jobs" resp = mocked_sub_requests(self.app, "post_json", path, data=body, headers=self.json_headers, only_local=True) assert resp.content_type in ContentType.APP_JSON assert resp.status_code == 201, f"Error: {resp.json}" job_url = resp.json["location"] self.monitor_job( job_url, return_status=True) # don't fetch results automatically resp = self.app.get(f"{job_url}/results", headers=self.json_headers) assert resp.status_code == 200, f"Error: {resp.text}" assert resp.content_type == ContentType.APP_JSON result_links = [ hdr for hdr in resp.headers if hdr[0].lower() == "link" ] assert len(result_links) == 0 results = resp.json # even though results are requested by Link reference, # Weaver still offers them with document on outputs endpoint output_url = job_url + "/outputs" resp = self.app.get(output_url, headers=self.json_headers) assert resp.status_code == 200, f"Error job outputs:\n{resp.text}" outputs = resp.json self.validate_results(results, outputs, nc_data, result_links)
def test_jsonarray2netcdf_execute_async_output_by_value_response_raw(self): """ Jobs submitted with ``response=raw`` and single output as ``transmissionMode=value`` must return its raw data. .. seealso:: https://docs.ogc.org/is/18-062r2/18-062r2.html#req_core_process-execute-sync-raw-value-one """ with contextlib.ExitStack() as stack_exec: body, nc_data = self.setup_inputs(stack_exec) body.update({ "response": ExecuteResponse.RAW, # by value/reference important here # NOTE: quantity of outputs important as well # since single output, content-type is directly that output (otherwise should be multipart) "outputs": [{ "id": "output", "transmissionMode": ExecuteTransmissionMode.VALUE }], # data dump }) for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) path = "/processes/jsonarray2netcdf/jobs" resp = mocked_sub_requests(self.app, "post_json", path, data=body, headers=self.json_headers, only_local=True) assert resp.content_type in ContentType.APP_JSON assert resp.status_code == 201, f"Error: {resp.json}" job_url = resp.json["location"] self.monitor_job( job_url, return_status=True) # don't fetch results automatically resp = self.app.get(f"{job_url}/results", headers=self.json_headers) assert resp.status_code < 400, f"Error: {resp.text}" assert resp.status_code == 200, "Body should contain literal raw data dump" assert resp.content_type in ContentType.APP_NETCDF, "raw result by value should be directly the content-type" assert resp.text == nc_data, "raw result by value should be directly the data content" assert resp.headers result_links = [ hdr for hdr in resp.headers if hdr[0].lower() == "link" ] assert len(result_links) == 0 # even though results are requested by raw data, # Weaver still offers them with document on outputs endpoint output_url = job_url + "/outputs" resp = self.app.get(output_url, headers=self.json_headers) assert resp.status_code == 200, f"Error job outputs:\n{resp.text}" outputs = resp.json self.validate_results(None, outputs, nc_data, result_links)
def test_jsonarray2netcdf_execute_async_output_by_reference_response_raw( self): """ Jobs submitted with ``response=raw`` and single output as ``transmissionMode=reference`` must a link. Contents should be empty, and the reference should be provided with HTTP ``Link`` header. .. seealso:: https://docs.ogc.org/is/18-062r2/18-062r2.html#req_core_process-execute-sync-raw-ref """ with contextlib.ExitStack() as stack_exec: body, nc_data = self.setup_inputs(stack_exec) body.update({ "response": ExecuteResponse.RAW, # by value/reference important here "outputs": [{ "id": "output", "transmissionMode": ExecuteTransmissionMode.REFERENCE }], # Link header }) for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) path = "/processes/jsonarray2netcdf/jobs" resp = mocked_sub_requests(self.app, "post_json", path, data=body, headers=self.json_headers, only_local=True) assert resp.content_type in ContentType.APP_JSON assert resp.status_code == 201, f"Error: {resp.json}" job_url = resp.json["location"] self.monitor_job( job_url, return_status=True) # don't fetch results automatically resp = self.app.get(f"{job_url}/results", headers=self.json_headers) assert resp.status_code < 400, f"Error: {resp.text}" assert resp.status_code == 204, "Body should be empty since all outputs requested by reference (Link header)" assert resp.content_type is None assert resp.headers result_links = [hdr for hdr in resp.headers if hdr[0] == "Link"] # even though results are requested by Link reference, # Weaver still offers them with document on outputs endpoint resp = self.app.get(f"{job_url}/outputs", headers=self.json_headers) assert resp.status_code == 200, f"Error job outputs:\n{resp.json}" outputs = resp.json self.validate_results(None, outputs, nc_data, result_links)
def test_execute_allowed_demo(self): template = "service=wps&request=execute&version=1.0.0&identifier={}&datainputs=name=tux" params = template.format(HelloWPS.identifier) url = self.make_url(params) with contextlib.ExitStack() as stack_exec: for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) resp = self.app.get(url) assert resp.status_code == 200 # FIXME: replace by 202 Accepted (?) https://github.com/crim-ca/weaver/issues/14 assert resp.content_type in ContentType.ANY_XML resp.mustcontain("<wps:ExecuteResponse") resp.mustcontain("<wps:ProcessAccepted") resp.mustcontain(f"PyWPS Process {HelloWPS.identifier}")
def test_execute_deployed_with_visibility_allowed(self): headers = {"Accept": ContentType.APP_XML} params_template = "service=wps&request=execute&version=1.0.0&identifier={}&datainputs=test_input=test" url = self.make_url( params_template.format(self.process_public.identifier)) with contextlib.ExitStack() as stack_exec: for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) resp = self.app.get(url, headers=headers) assert resp.status_code == 200 # FIXME: replace by 202 Accepted (?) https://github.com/crim-ca/weaver/issues/14 assert resp.content_type in ContentType.ANY_XML resp.mustcontain("<wps:ExecuteResponse") resp.mustcontain("<wps:ProcessAccepted") resp.mustcontain(f"PyWPS Process {self.process_public.identifier}")
def test_execute_deployed_with_visibility_denied(self): headers = {"Accept": ContentType.APP_XML} params_template = "service=wps&request=execute&version=1.0.0&identifier={}&datainputs=test_input=test" url = self.make_url( params_template.format(self.process_private.identifier)) with contextlib.ExitStack() as stack_exec: for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) resp = self.app.get(url, headers=headers, expect_errors=True) assert resp.status_code == 403 assert resp.content_type in ContentType.ANY_XML, f"Error Response: {resp.text}" resp.mustcontain( "<Exception exceptionCode=\"AccessForbidden\" locator=\"service\">" ) err_desc = f"Process with ID '{self.process_private.identifier}' is not accessible." resp.mustcontain(f"<ExceptionText>{err_desc}</ExceptionText>")
def test_quote_atomic_process(self, mocked_estimate): with contextlib.ExitStack() as stack_quote: for mock_quote in mocked_execute_celery( celery_task= "weaver.quotation.estimation.process_quote_estimator"): stack_quote.enter_context(mock_quote) data = { "inputs": { "message": "test quote" }, "outputs": { "output": { "transmissionMode": ExecuteTransmissionMode.VALUE } } } path = sd.process_quotes_service.path.format(process_id="Echo") resp = mocked_sub_requests(self.app, "POST", path, json=data, headers=self.json_headers, only_local=True) assert resp.status_code == 202 # 'Accepted' async task, but already finished by skipping celery assert mocked_estimate.called body = resp.json assert body["status"] == QuoteStatus.SUBMITTED path = sd.process_quote_service.path.format(process_id="Echo", quote_id=body["id"]) resp = mocked_sub_requests(self.app, "GET", path, headers=self.json_headers, only_local=True) assert resp.status_code == 200 body = resp.json assert body["status"] == QuoteStatus.COMPLETED assert isinstance(body["price"], float) and body["price"] > 0 assert isinstance(body["currency"], str) and body["currency"] == "CAD" assert isinstance(body["estimatedSeconds"], int) and body["estimatedSeconds"] > 0
def test_jsonarray2netcdf_execute_async(self): with contextlib.ExitStack() as stack_exec: body, nc_data = self.setup_inputs(stack_exec) body.update({ "mode": ExecuteMode.ASYNC, "response": ExecuteResponse.DOCUMENT, "outputs": [{ "id": "output", "transmissionMode": ExecuteTransmissionMode.VALUE }], }) for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) path = "/processes/jsonarray2netcdf/jobs" resp = mocked_sub_requests(self.app, "post_json", path, data=body, headers=self.json_headers, only_local=True) assert resp.status_code == 201, f"Error: {resp.json}" assert resp.content_type in ContentType.APP_JSON # following details not available yet in async, but are in sync assert "created" not in resp.json assert "finished" not in resp.json assert "duration" not in resp.json assert "progress" not in resp.json job_url = resp.json["location"] results = self.monitor_job(job_url) output_url = job_url + "/outputs" resp = self.app.get(output_url, headers=self.json_headers) assert resp.status_code == 200, f"Error job outputs:\n{resp.json}" outputs = resp.json self.validate_results(results, outputs, nc_data, None)
def test_quote_workflow_process(self, mocked_estimate): with contextlib.ExitStack() as stack_quote: for mock_quote in mocked_execute_celery( celery_task= "weaver.quotation.estimation.process_quote_estimator"): stack_quote.enter_context(mock_quote) path = os.path.join(APP_PKG_ROOT, "WorkflowChainStrings", "execute.json") with open(path, mode="r", encoding="utf-8") as exec_file: data = yaml.safe_load(exec_file) path = sd.process_quotes_service.path.format( process_id="WorkflowChainStrings") resp = mocked_sub_requests(self.app, "POST", path, json=data, headers=self.json_headers, only_local=True) assert resp.status_code == 202 # 'Accepted' async task, but already finished by skipping celery assert mocked_estimate.called body = resp.json assert body["status"] == QuoteStatus.SUBMITTED path = sd.process_quote_service.path.format(process_id="Echo", quote_id=body["id"]) resp = mocked_sub_requests(self.app, "GET", path, headers=self.json_headers, only_local=True) assert resp.status_code == 200 body = resp.json assert body["status"] == QuoteStatus.COMPLETED assert isinstance(body["price"], float) and body["price"] > 0 assert isinstance(body["currency"], str) and body["currency"] == "CAD" assert isinstance(body["estimatedSeconds"], int) and body["estimatedSeconds"] > 0
def test_jsonarray2netcdf_execute_sync(self): """ Job submitted with ``mode=sync`` or ``Prefer`` header for sync should respond directly with the results schema. .. seealso:: https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_execute_response """ with contextlib.ExitStack() as stack_exec: body, nc_data = self.setup_inputs(stack_exec) body.update({ "response": ExecuteResponse.DOCUMENT, "outputs": [{ "id": "output", "transmissionMode": ExecuteTransmissionMode.VALUE }] }) for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) headers = {"Prefer": "wait=10"} headers.update(self.json_headers) path = "/processes/jsonarray2netcdf/jobs" resp = mocked_sub_requests(self.app, "post_json", path, data=body, headers=headers, only_local=True) assert resp.status_code == 200, f"Error: {resp.text}" assert resp.content_type in ContentType.APP_JSON # since sync, results are directly available instead of job status # even if results are returned directly (instead of status), # status location link is available for reference as needed assert "Location" in resp.headers # validate sync was indeed applied (in normal situation, not considering mock test that runs in sync) assert resp.headers["Preference-Applied"] == headers["Prefer"] # following details should not be available since results are returned in sync instead of async job status for field in ["status", "created", "finished", "duration", "progress"]: assert field not in resp.json # validate that job can still be found and its metadata are defined although executed in sync job_url = resp.headers["Location"] resp = self.app.get(job_url, headers=self.json_headers) assert resp.status_code == 200 assert resp.content_type == ContentType.APP_JSON for field in ["status", "created", "finished", "duration", "progress"]: assert field in resp.json assert resp.json["status"] == Status.SUCCEEDED assert resp.json["progress"] == 100 out_url = f"{job_url}/results" resp = self.app.get(out_url, headers=self.json_headers) assert resp.status_code == 200 assert resp.content_type == ContentType.APP_JSON results = resp.json output_url = job_url + "/outputs" resp = self.app.get(output_url, headers=self.json_headers) assert resp.status_code == 200, f"Error job outputs:\n{resp.json}" outputs = resp.json self.validate_results(results, outputs, nc_data, None)
def test_execute_docker_embedded_python_script(self): test_proc = "test-docker-python-script" cwl = load_file( os.path.join(WEAVER_ROOT_DIR, "docs/examples/docker-python-script-report.cwl")) body = { "processDescription": { "process": { "id": test_proc } }, "executionUnit": [{ "unit": cwl }], "deploymentProfileName": "http://www.opengis.net/profiles/eoc/dockerizedApplication" } self.deploy_process(body) with contextlib.ExitStack() as stack: for mock_exec in mocked_execute_celery(): stack.enter_context(mock_exec) path = f"/processes/{test_proc}/execution" cost = 2.45 amount = 3 body = { "mode": ExecuteMode.ASYNC, "response": ExecuteResponse.DOCUMENT, "inputs": [{ "id": "amount", "value": amount }, { "id": "cost", "value": cost }], "outputs": [ { "id": "quote", "transmissionMode": ExecuteTransmissionMode.VALUE }, ] } resp = mocked_sub_requests(self.app, "POST", path, json=body, headers=self.json_headers, only_local=True) status_url = resp.headers["Location"] results = self.monitor_job(status_url) assert results["quote"]["href"].startswith("http") stack.enter_context(mocked_wps_output(self.settings)) tmpdir = stack.enter_context(tempfile.TemporaryDirectory()) report_file = fetch_file(results["quote"]["href"], tmpdir, self.settings) report_data = load_file(report_file, text=True) assert report_data == f"Order Total: {amount * cost:0.2f}$\n"
def wps_execute(self, version, accept, url=None): if url: wps_url = url else: wps_url = get_wps_url(self.settings) if version == "1.0.0": test_content = "Test file in Docker - WPS KVP" wps_method = "GET" elif version == "2.0.0": test_content = "Test file in Docker - WPS XML" wps_method = "POST" else: raise ValueError(f"Invalid WPS version: {version}") accept_type = accept.split("/")[-1].upper() test_content += f" {wps_method} request - Accept {accept_type}" with contextlib.ExitStack() as stack_exec: # setup dir_name = tempfile.gettempdir() tmp_file = stack_exec.enter_context( tempfile.NamedTemporaryFile(dir=dir_name, mode="w", suffix=".txt")) tmp_file.write(test_content) tmp_file.seek(0) for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) # execute if version == "1.0.0": wps_inputs = [ f"file={tmp_file.name}@mimeType={ContentType.TEXT_PLAIN}" ] wps_params = { "service": "WPS", "request": "Execute", "version": version, "identifier": self.process_id, "DataInputs": wps_inputs, } wps_headers = {"Accept": accept} wps_data = None else: wps_inputs = [ ("file", ComplexDataInput(tmp_file.name, mimeType=ContentType.TEXT_PLAIN)) ] wps_outputs = [(self.out_key, True)] # as reference wps_exec = WPSExecution(version=version, url=wps_url) wps_req = wps_exec.buildRequest(self.process_id, wps_inputs, wps_outputs) wps_data = xml_util.tostring(wps_req) wps_headers = { "Accept": accept, "Content-Type": ContentType.APP_XML } wps_params = None resp = mocked_sub_requests(self.app, wps_method, wps_url, params=wps_params, data=wps_data, headers=wps_headers, only_local=True) assert resp.status_code in [ 200, 201 ], (f"Failed with: [{resp.status_code}]\nTest: [{test_content}]\nReason:\n{resp.text}" ) # parse response status if accept == ContentType.APP_XML: assert resp.content_type in ContentType.ANY_XML, test_content xml_body = xml_util.fromstring(str2bytes(resp.text)) status_url = xml_body.get("statusLocation") job_id = status_url.split("/")[-1].split(".")[0] elif accept == ContentType.APP_JSON: assert resp.content_type == ContentType.APP_JSON, test_content status_url = resp.json["location"] job_id = resp.json["jobID"] assert status_url assert job_id if accept == ContentType.APP_XML: wps_out_url = self.settings["weaver.wps_output_url"] weaver_url = self.settings["weaver.url"] assert status_url == f"{wps_out_url}/{job_id}.xml", "Status URL should be XML file for WPS-1 request" # remap to employ JSON monitor method (could be done with XML parsing otherwise) status_url = f"{weaver_url}/jobs/{job_id}" # job monitoring results = self.monitor_job(status_url) outputs = self.get_outputs(status_url) # validate XML status is updated accordingly wps_xml_status = os.path.join( self.settings["weaver.wps_output_dir"], job_id + ".xml") assert os.path.isfile(wps_xml_status) with open(wps_xml_status, mode="r", encoding="utf-8") as status_file: assert "ProcessSucceeded" in status_file.read() self.validate_outputs(job_id, results, outputs, test_content)
def test_execute_wps_rest_resp_json(self): """ Test validates that basic Docker application runs successfully, fetching the reference as needed. The job execution is launched using the WPS-REST endpoint for this test. Both the request body and response content are JSON. .. seealso:: - :meth:`test_execute_wps_kvp_get_resp_xml` - :meth:`test_execute_wps_kvp_get_resp_json` - :meth:`test_execute_wps_xml_post_resp_xml` - :meth:`test_execute_wps_xml_post_resp_json` """ test_content = "Test file in Docker - WPS-REST job endpoint" with contextlib.ExitStack() as stack_exec: # setup dir_name = tempfile.gettempdir() tmp_file = stack_exec.enter_context( tempfile.NamedTemporaryFile(dir=dir_name, mode="w", suffix=".txt")) tmp_file.write(test_content) tmp_file.seek(0) exec_body = { "mode": ExecuteMode.ASYNC, "response": ExecuteResponse.DOCUMENT, "inputs": [ { "id": "file", "href": tmp_file.name }, ], "outputs": [ { "id": self.out_key, "transmissionMode": ExecuteTransmissionMode.VALUE }, ] } for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) # execute proc_url = f"/processes/{self.process_id}/jobs" resp = mocked_sub_requests(self.app, "post_json", proc_url, data=exec_body, headers=self.json_headers, only_local=True) assert resp.status_code in [ 200, 201 ], f"Failed with: [{resp.status_code}]\nReason:\n{resp.json}" status_url = resp.json["location"] job_id = resp.json["jobID"] # job monitoring results = self.monitor_job(status_url) outputs = self.get_outputs(status_url) self.validate_outputs(job_id, results, outputs, test_content)
def test_register_describe_execute_ncdump(self, mock_responses): # type: (RequestsMock) -> None """ Test the full workflow from remote WPS-1 provider registration, process description, execution and fetch result. The complete execution and definitions (XML responses) of the "remote" WPS are mocked. Requests and response negotiation between Weaver and that "remote" WPS are effectively executed and validated. Validation is accomplished against the same process and mocked server from corresponding test deployment server in order to detect early any breaking feature. Responses XML bodies employed to simulate the mocked server are pre-generated from real request calls to the actual service that was running on a live platform. .. seealso:: - Reference notebook testing the same process on a live server: https://github.com/Ouranosinc/pavics-sdi/blob/master/docs/source/notebook-components/weaver_example.ipynb - Evaluate format of submitted Execute body (see `#340 <https://github.com/crim-ca/weaver/issues/340>`_). """ self.service_store.clear_services() # register the provider remote_provider_name = "test-wps-remote-provider-hummingbird" path = "/providers" data = {"id": remote_provider_name, "url": resources.TEST_REMOTE_SERVER_URL} resp = self.app.post_json(path, params=data, headers=self.json_headers) assert resp.status_code == 201 # validate service capabilities path = f"/providers/{remote_provider_name}" resp = self.app.get(path, headers=self.json_headers) assert resp.status_code == 200 body = resp.json assert "id" in body and body["id"] == remote_provider_name assert "hummingbird" in body["title"].lower() assert body["type"] == ProcessType.WPS_REMOTE # validate processes capabilities path = f"/providers/{remote_provider_name}/processes" resp = self.app.get(path, headers=self.json_headers) body = resp.json assert resp.status_code == 200 assert "processes" in body and len(body["processes"]) == len(resources.TEST_HUMMINGBIRD_WPS1_PROCESSES) processes = {process["id"]: process for process in body["processes"]} assert "ncdump" in processes assert processes["ncdump"]["version"] == "4.4.1.1" assert processes["ncdump"]["metadata"][0]["rel"] == "birdhouse" assert processes["ncdump"]["metadata"][1]["rel"] == "user-guide" # keyword 'Hummingbird' in this case is from GetCapabilities ProviderName # keyword of the service name within Weaver is also provided, which can be different than provider expect_keywords = [ProcessType.WPS_REMOTE, "Hummingbird", remote_provider_name] assert all(key in processes["ncdump"]["keywords"] for key in expect_keywords) proc_desc_url = processes["ncdump"]["processDescriptionURL"] proc_wps1_url = processes["ncdump"]["processEndpointWPS1"] proc_exec_url = processes["ncdump"]["executeEndpoint"] assert proc_wps1_url.startswith(resources.TEST_REMOTE_SERVER_URL) assert proc_desc_url == self.app_url + path + "/ncdump" assert proc_exec_url == self.app_url + path + "/ncdump/jobs" # validate process description resp = self.app.get(proc_desc_url, headers=self.json_headers) assert resp.status_code == 200 body = resp.json assert "inputs" in body and len(body["inputs"]) == 2 assert all(iid in body["inputs"] for iid in ["dataset", "dataset_opendap"]) assert body["inputs"]["dataset"]["minOccurs"] == 0 assert body["inputs"]["dataset"]["maxOccurs"] == 100 assert "formats" in body["inputs"]["dataset"] assert len(body["inputs"]["dataset"]["formats"]) == 1 assert body["inputs"]["dataset"]["formats"][0]["default"] is True assert "literalDataDomains" not in body["inputs"]["dataset"] assert body["inputs"]["dataset"]["formats"][0]["mediaType"] == ContentType.APP_NETCDF assert body["inputs"]["dataset_opendap"]["minOccurs"] == 0 assert body["inputs"]["dataset_opendap"]["maxOccurs"] == 100 assert "formats" not in body["inputs"]["dataset_opendap"] assert "literalDataDomains" in body["inputs"]["dataset_opendap"] assert len(body["inputs"]["dataset_opendap"]["literalDataDomains"]) == 1 assert body["inputs"]["dataset_opendap"]["literalDataDomains"][0]["dataType"]["name"] == "string" assert body["inputs"]["dataset_opendap"]["literalDataDomains"][0]["valueDefinition"]["anyValue"] is True assert body["inputs"]["dataset_opendap"]["literalDataDomains"][0]["default"] is True assert "outputs" in body and len(body["outputs"]) == 1 assert "output" in body["outputs"] assert "formats" in body["outputs"]["output"] assert len(body["outputs"]["output"]["formats"]) == 1 assert body["outputs"]["output"]["formats"][0]["default"] is True assert body["outputs"]["output"]["formats"][0]["mediaType"] == ContentType.TEXT_PLAIN assert "literalDataDomains" not in body["outputs"]["output"] assert body["processDescriptionURL"] == proc_desc_url assert body["processEndpointWPS1"] == proc_wps1_url assert body["executeEndpoint"] == proc_exec_url job_exec_url = proc_exec_url.replace("/execution", "/jobs") # both are aliases, any could be returned ogc_exec_url = proc_exec_url.replace("/jobs", "/execution") links = {link["rel"].rsplit("/")[-1]: link["href"] for link in body["links"]} assert links["execute"] in [job_exec_url, ogc_exec_url] assert links["process-meta"] == proc_desc_url # WPS-1 URL also includes relevant query parameters to obtain a valid response directly from remote service assert links["process-desc"] == proc_wps1_url assert links["service-desc"].startswith(resources.TEST_REMOTE_SERVER_URL) assert "DescribeProcess" in links["process-desc"] assert "GetCapabilities" in links["service-desc"] assert ExecuteControlOption.ASYNC in body["jobControlOptions"] assert ExecuteTransmissionMode.VALUE in body["outputTransmission"] # validate execution submission # (don't actually execute because server is mocked, only validate parsing of I/O and job creation) # first setup all expected contents and files exec_file = "http://localhost.com/dont/care.nc" exec_body = { "mode": ExecuteMode.ASYNC, "response": ExecuteResponse.DOCUMENT, "inputs": [{"id": "dataset", "href": exec_file}], "outputs": [{"id": "output", "transmissionMode": ExecuteTransmissionMode.VALUE}] } status_url = resources.TEST_REMOTE_SERVER_URL + "/status.xml" output_url = resources.TEST_REMOTE_SERVER_URL + "/output.txt" with open(resources.TEST_HUMMINGBIRD_STATUS_WPS1_XML, mode="r", encoding="utf-8") as status_file: status = status_file.read().format( TEST_SERVER_URL=resources.TEST_REMOTE_SERVER_URL, LOCATION_XML=status_url, OUTPUT_FILE=output_url, ) xml_headers = {"Content-Type": ContentType.TEXT_XML} ncdump_data = "Fake NetCDF Data" with contextlib.ExitStack() as stack_exec: # mock direct execution bypassing celery for mock_exec in mocked_execute_celery(): stack_exec.enter_context(mock_exec) # mock responses expected by "remote" WPS-1 Execute request and relevant documents mock_responses.add("GET", exec_file, body=ncdump_data, headers={"Content-Type": ContentType.APP_NETCDF}) mock_responses.add("POST", resources.TEST_REMOTE_SERVER_URL, body=status, headers=xml_headers) mock_responses.add("GET", status_url, body=status, headers=xml_headers) mock_responses.add("GET", output_url, body=ncdump_data, headers={"Content-Type": ContentType.TEXT_PLAIN}) # add reference to specific provider execute class to validate it was called # (whole procedure must run even though a lot of parts are mocked) real_wps1_process_execute = Wps1Process.execute handle_wps1_process_execute = stack_exec.enter_context( mock.patch.object(Wps1Process, "execute", side_effect=real_wps1_process_execute, autospec=True) ) # type: MockPatch # launch job execution and validate resp = mocked_sub_requests(self.app, "post_json", proc_exec_url, timeout=5, data=exec_body, headers=self.json_headers, only_local=True) assert resp.status_code in [200, 201], f"Failed with: [{resp.status_code}]\nReason:\n{resp.json}" assert handle_wps1_process_execute.called, "WPS-1 handler should have been called by CWL runner context" status_url = resp.json["location"] job_id = resp.json["jobID"] assert status_url == proc_exec_url + "/" + job_id results = self.monitor_job(status_url) wps_dir = self.settings["weaver.wps_output_dir"] wps_url = self.settings["weaver.wps_output_url"] output_url = f"{wps_url}/{job_id}/output.txt" output_path = f"{wps_dir}/{job_id}/output.txt" assert results["output"]["format"]["mediaType"] == ContentType.TEXT_PLAIN assert results["output"]["href"] == output_url with open(output_path, mode="r", encoding="utf-8") as out_file: data = out_file.read() assert data == ncdump_data