44import os
55import pytest
66import re
7+ import requests
78from requests .exceptions import HTTPError , ReadTimeout
89import semver
10+ import shutil
11+ import subprocess
12+ import tempfile
913import threading
1014import time
15+ from urllib3 .exceptions import ProtocolError
1116
1217from kbase import sdk_baseclient
1318
1419
1520_VERSION = "0.1.0"
16- _MOCKSERVER_PORT = 31590 # should be fine, find an empty port otherwise
21+ # should be fine, find an empty ports otherwise
22+ _MOCKSERVER_PORT = 31590
23+ _CALLBACK_SERVER_PORT = 31591
24+ _CALLBACK_SERVER_IMAGE = "ghcr.io/kbase/jobrunner:pr-116"
1725
1826
1927@pytest .fixture (scope = "module" )
@@ -54,6 +62,72 @@ def mockserver():
5462 server .shutdown ()
5563
5664
65+ def _wait_for_callback (callback_url : str ):
66+ interval = 1
67+ limit = 120
68+ start = time .monotonic ()
69+ err = None
70+ print ("waiting for callback server to start" )
71+ while time .monotonic () - start < limit :
72+ try :
73+ res = requests .get (callback_url )
74+ restext = res .text
75+ if res .status_code == 200 and res .text == "[{}]" :
76+ print (f"Callback server is up at { callback_url } " )
77+ return
78+ except Exception as e :
79+ err = e
80+ print ("waiting for CBS" )
81+ time .sleep (interval )
82+ raise IOError (f"Callback server did not start, last response: { restext } " ) from err
83+
84+
85+ @pytest .fixture (scope = "module" )
86+ def callback (url_and_token ):
87+ # Tried using the temp path pytest fixture but kept getting lots of warnings
88+ tmpdir = tempfile .mkdtemp (prefix = "callback_server_data_" )
89+ container_name = f"sdk_baseclient_test_{ str (time .time ()).replace ('.' , '_' )} "
90+ dockercmd = [
91+ "docker" , "run" ,
92+ "--platform=linux/amd64" , # until we have multiarch images
93+ "--name" , container_name ,
94+ "--rm" ,
95+ # TODO SECURITY when CBS allows, use a file instead
96+ # https://github.com/kbase/JobRunner/issues/90
97+ "-e" , f"KB_AUTH_TOKEN={ url_and_token [1 ]} " ,
98+ "-e" , f"KB_BASE_URL={ url_and_token [0 ]} /services/" ,
99+ "-e" , f"JOB_DIR={ tmpdir } " ,
100+ "-e" , "CALLBACK_IP=localhost" ,
101+ "-e" , f"CALLBACK_PORT={ _CALLBACK_SERVER_PORT } " ,
102+ "-e" , "DEBUG_RUNNER=true" , # prints logs from containers
103+ "-v" , "/var/run/docker.sock:/run/docker.sock" ,
104+ "-v" , f"{ tmpdir } :{ tmpdir } " ,
105+ "-p" , f"{ _CALLBACK_SERVER_PORT } :{ _CALLBACK_SERVER_PORT } " ,
106+ _CALLBACK_SERVER_IMAGE
107+ ]
108+ proc = subprocess .Popen (dockercmd )
109+ callback_url = f"http://localhost:{ _CALLBACK_SERVER_PORT } "
110+ _wait_for_callback (callback_url )
111+
112+ try :
113+ yield callback_url
114+ finally :
115+ subprocess .check_call (["docker" , "stop" , container_name ])
116+ proc .wait (timeout = 10 )
117+ dockercmd = [
118+ "docker" , "run" ,
119+ "--platform=linux/amd64" , # until we have multiarch images
120+ "--name" , container_name ,
121+ "--rm" ,
122+ "-v" , f"{ tmpdir } :{ tmpdir } " ,
123+ "--entrypoint" , "bash" ,
124+ _CALLBACK_SERVER_IMAGE ,
125+ "-c" , f"rm -rf { tmpdir } /*" , # need to use bash for the globbing
126+ ]
127+ subprocess .check_call (dockercmd )
128+ shutil .rmtree (tmpdir )
129+
130+
57131def test_version ():
58132 assert sdk_baseclient .__version__ == _VERSION
59133
@@ -82,6 +156,7 @@ def test_call_method_basic_passed_token(url_and_token):
82156
83157
84158def test_call_method_basic_env_token (url_and_token ):
159+ # Tests returning a single value
85160 os .environ ["KB_AUTH_TOKEN" ] = url_and_token [1 ]
86161 try :
87162 _test_call_method_basic (url_and_token [0 ] + "/services/ws" , None )
@@ -103,12 +178,11 @@ def _test_call_method_basic(url: str, token: str | None):
103178 assert res is None
104179
105180
106- def test_serialize_sets_and_list_return (url_and_token ):
107- """
108- Tests
109- * Serializing set and frozenset
110- * Methods that return a list vs. a single value (save_objects).
111- """
181+ # TODO add test for service that returns > 1 value. Not sure if any services do this
182+
183+
184+ def test_serialize_sets (url_and_token ):
185+ # Tests serializing set and frozenset
112186 bc = sdk_baseclient .SDKBaseClient (url_and_token [0 ] + "/services/ws" , token = url_and_token [1 ])
113187 ws_name = f"sdk_baseclient_test_{ time .time ()} "
114188 try :
@@ -231,9 +305,9 @@ def test_dynamic_service(url_and_token):
231305 ver = res ["version" ]
232306 del res ["version" ]
233307 assert res == {
234- ' git_url' : ' https://github.com/kbaseapps/HTMLFileSetServ' ,
235- ' message' : '' ,
236- ' state' : 'OK' ,
308+ " git_url" : " https://github.com/kbaseapps/HTMLFileSetServ" ,
309+ " message" : "" ,
310+ " state" : "OK" ,
237311 }
238312 assert semver .Version .parse (ver ) > semver .Version .parse ("0.0.8" )
239313
@@ -246,8 +320,72 @@ def test_dynamic_service_with_service_version(url_and_token):
246320 res = bc .call_method ("HTMLFileSetServ.status" , [], service_ver = "0.0.8" )
247321 del res ["git_commit_hash" ]
248322 assert res == {
249- ' git_url' : ' https://github.com/kbaseapps/HTMLFileSetServ' ,
250- ' message' : '' ,
251- ' state' : 'OK' ,
323+ " git_url" : " https://github.com/kbaseapps/HTMLFileSetServ" ,
324+ " message" : "" ,
325+ " state" : "OK" ,
252326 "version" : "0.0.8"
253327 }
328+
329+
330+ ###
331+ # Async job tests
332+ #
333+ # All of the 3 ways of calling services use the same underlying _call method, so we don't
334+ # reiterate those tests every time.
335+ ###
336+
337+
338+ def test_run_job_with_service_ver (url_and_token , callback ):
339+ bc = sdk_baseclient .SDKBaseClient (callback , token = url_and_token [1 ], timeout = 10 )
340+ res = bc .run_job (
341+ "njs_sdk_test_2.run" ,
342+ # force backoff with a wait
343+ [{"id" : "simplejob2" , "wait" : 1 }],
344+ # it seems semvers don't work for unreleased modules
345+ service_ver = "9d6b868bc0bfdb61c79cf2569ff7b9abffd4c67f"
346+ )
347+ assert res == {
348+ "id" : "simplejob2" ,
349+ "name" : "njs_sdk_test_2" ,
350+ "hash" : "9d6b868bc0bfdb61c79cf2569ff7b9abffd4c67f" ,
351+ "wait" : 1 ,
352+ }
353+
354+
355+ def test_run_job_no_return (url_and_token , callback ):
356+ bc = sdk_baseclient .SDKBaseClient (callback , token = url_and_token [1 ], timeout = 10 )
357+ res = bc .run_job ("HelloServiceDeluxe.how_rude" , ["Georgette" ])
358+ assert res is None
359+
360+
361+ def test_run_job_list_return (url_and_token , callback ):
362+ bc = sdk_baseclient .SDKBaseClient (callback , token = url_and_token [1 ], timeout = 10 )
363+ res = bc .run_job ("HelloServiceDeluxe.say_hellos" , ["JimBob" , "Gengulphus" ])
364+ assert res == [
365+ 'Hi JimBob, you santimonious lickspittle' , # the dork that wrote this module can't spell
366+ 'Hi Gengulphus, what a lovely and scintillating person you are' ,
367+ ]
368+
369+
370+ def test_run_job_failure (url_and_token , callback , requests_mock ):
371+ requests_mock .post (callback , [
372+ {"json" : {"result" : ["job_id" ]}},
373+ {"exc" : ConnectionError ("oopsie" )},
374+ {"exc" : ProtocolError ("oh dang" )},
375+ {"exc" : ConnectionError ("so unreliable omg" )},
376+ ])
377+ bc = sdk_baseclient .SDKBaseClient (callback , token = url_and_token [1 ], timeout = 10 )
378+ with pytest .raises (RuntimeError , match = "_check_job failed 3 times and exceeded limit" ):
379+ bc .run_job ("HelloServiceDeluxe.say_hellos" , ["JimBob" ])
380+
381+
382+ def test_run_job_failure_recovery (url_and_token , callback , requests_mock ):
383+ requests_mock .post (callback , [
384+ {"json" : {"result" : ["job_id" ]}},
385+ {"exc" : ConnectionError ("oopsie" )},
386+ {"exc" : ProtocolError ("oh dang" )},
387+ {"json" : {"result" : [{"finished" : 1 , "result" : ["meh" ]}]}},
388+ ])
389+ bc = sdk_baseclient .SDKBaseClient (callback , token = url_and_token [1 ], timeout = 10 )
390+ res = bc .run_job ("HelloServiceDeluxe.say_hellos" , ["JimBob" ])
391+ assert res == "meh"
0 commit comments