1+ /*
2+ * Copyright 2023 Basis Technology Corp.
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+ package com .basistech .rosette .api ;
17+
18+ import com .basistech .rosette .api .common .AbstractRosetteAPI ;
19+ import com .basistech .rosette .apimodel .DocumentRequest ;
20+ import com .basistech .rosette .apimodel .EntitiesOptions ;
21+ import com .basistech .rosette .apimodel .EntitiesResponse ;
22+ import com .basistech .rosette .apimodel .ErrorResponse ;
23+ import com .basistech .rosette .apimodel .LanguageOptions ;
24+ import com .basistech .rosette .apimodel .LanguageResponse ;
25+ import com .basistech .rosette .apimodel .Response ;
26+ import org .apache .http .impl .client .CloseableHttpClient ;
27+ import org .apache .http .impl .client .HttpClients ;
28+ import org .junit .jupiter .api .AfterEach ;
29+ import org .junit .jupiter .api .BeforeEach ;
30+ import org .junit .jupiter .api .Test ;
31+ import org .junit .jupiter .api .extension .ExtendWith ;
32+ import org .mockserver .client .MockServerClient ;
33+ import org .mockserver .junit .jupiter .MockServerExtension ;
34+ import org .mockserver .matchers .Times ;
35+ import org .mockserver .model .HttpRequest ;
36+ import org .mockserver .model .HttpResponse ;
37+
38+ import java .io .IOException ;
39+ import java .util .ArrayList ;
40+ import java .util .Date ;
41+ import java .util .List ;
42+ import java .util .concurrent .ExecutionException ;
43+ import java .util .concurrent .Future ;
44+ import java .util .concurrent .TimeUnit ;
45+
46+ import static org .junit .jupiter .api .Assertions .assertEquals ;
47+ import static org .junit .jupiter .api .Assertions .assertInstanceOf ;
48+ import static org .junit .jupiter .api .Assertions .assertNull ;
49+ import static org .junit .jupiter .api .Assertions .assertTrue ;
50+
51+ @ ExtendWith (MockServerExtension .class )
52+ class RosetteRequestTest {
53+ private MockServerClient mockServer ;
54+ private HttpRosetteAPI api ;
55+
56+ @ BeforeEach
57+ void setup (MockServerClient mockServer ) {
58+ this .mockServer = mockServer ;
59+ }
60+
61+ private void setupResponse (String requestPath , String responseString , int statusCode , int delayMillis , int requestTimes ) {
62+ this .mockServer .when (HttpRequest .request ().withPath (requestPath ), Times .exactly (requestTimes ))
63+ .respond (HttpResponse .response ()
64+ .withHeader ("Content-Type" , "application/json" )
65+ .withHeader ("X-RosetteAPI-Concurrency" , "5" )
66+ .withStatusCode (statusCode )
67+ .withBody (responseString )
68+ .withDelay (TimeUnit .MILLISECONDS , delayMillis ));
69+ }
70+
71+
72+ @ Test
73+ void successfulRequest () throws ExecutionException , InterruptedException {
74+ //Api client setup
75+ this .api = new HttpRosetteAPI .Builder ().url (String .format ("http://localhost:%d/rest/v1" , mockServer .getPort ())).build ();
76+
77+ //response setup
78+ String entitiesResponse = "{\" entities\" : [ { \" type\" : \" ORGANIZATION\" , \" mention\" : \" Securities and Exchange Commission\" , \" normalized\" : \" U.S. Securities and Exchange Commission\" , \" count\" : 1, \" mentionOffsets\" : [ { \" startOffset\" : 4, \" endOffset\" : 38 } ], \" entityId\" : \" Q953944\" , \" confidence\" : 0.39934742, \" linkingConfidence\" : 0.67404154 } ] }" ;
79+ setupResponse ("/rest/v1/entities" , entitiesResponse , 200 , 0 , 1 );
80+
81+ //request setup
82+ String entitiesTextData = "The Securities and Exchange Commission today announced the leadership of the agency’s trial unit." ;
83+ DocumentRequest <EntitiesOptions > entitiesRequestData = DocumentRequest .<EntitiesOptions >builder ()
84+ .content (entitiesTextData )
85+ .build ();
86+ RosetteRequest entitiesRequest = this .api .createRosetteRequest (AbstractRosetteAPI .ENTITIES_SERVICE_PATH , entitiesRequestData , EntitiesResponse .class );
87+
88+ //testing the request
89+ Future <Response > response = this .api .submitRequest (entitiesRequest );
90+ assertInstanceOf (EntitiesResponse .class , response .get ());
91+ assertEquals (response .get (), entitiesRequest .getResponse ());
92+ }
93+
94+
95+ @ Test
96+ void errorResponse () throws ExecutionException , InterruptedException {
97+ //Api client setup
98+ this .api = new HttpRosetteAPI .Builder ().url (String .format ("http://localhost:%d/rest/v1" , mockServer .getPort ())).build ();
99+
100+ //response setup
101+ String entitiesResponse = "{ \" code\" : \" badRequestFormat\" , \" message\" : \" no content provided; must be one of an attachment, an inline \\ \" content\\ \" field, or an external \\ \" contentUri\\ \" \" }" ;
102+ setupResponse ("/rest/v1/entities" , entitiesResponse , 400 , 0 , 1 );
103+
104+ //request setup
105+ DocumentRequest <EntitiesOptions > entitiesRequestData = DocumentRequest .<EntitiesOptions >builder ()
106+ .build ();
107+ RosetteRequest entitiesRequest = this .api .createRosetteRequest (AbstractRosetteAPI .ENTITIES_SERVICE_PATH , entitiesRequestData , EntitiesResponse .class );
108+
109+ //testing the request
110+ Future <Response > response = this .api .submitRequest (entitiesRequest );
111+ assertInstanceOf (ErrorResponse .class , response .get ());
112+ assertEquals (response .get (), entitiesRequest .getResponse ());
113+ }
114+
115+ @ Test
116+ void testTiming () throws ExecutionException , InterruptedException {
117+ int delay = 100 ;
118+ //api setup
119+ this .api = new HttpRosetteAPI .Builder ().url (String .format ("http://localhost:%d/rest/v1" , mockServer .getPort ()))
120+ .connectionConcurrency (1 ).build ();
121+
122+ //responses setup
123+ int entitiesRespCount = 10 ;
124+ int languageRespCount = 4 ;
125+ assertEquals (0 , entitiesRespCount % 2 );
126+ assertEquals (0 , entitiesRespCount % 2 );
127+ String entitiesResponse = "{\" entities\" : [ { \" type\" : \" ORGANIZATION\" , \" mention\" : \" Securities and Exchange Commission\" , \" normalized\" : \" U.S. Securities and Exchange Commission\" , \" count\" : 1, \" mentionOffsets\" : [ { \" startOffset\" : 4, \" endOffset\" : 38 } ], \" entityId\" : \" Q953944\" , \" confidence\" : 0.39934742, \" linkingConfidence\" : 0.67404154 } ] }" ;
128+ setupResponse ("/rest/v1/entities" , entitiesResponse , 200 , delay , entitiesRespCount );
129+ String languageResponse = " {\" code\" : \" badRequestFormat\" , \" message\" : \" no content provided; must be one of an attachment, an inline \\ \" content\\ \" field, or an external \\ \" contentUri\\ \" \" }" ;
130+ setupResponse ("/rest/v1/language" , languageResponse , 400 , delay , languageRespCount );
131+
132+ //requests setup
133+ String entitiesTextData = "The Securities and Exchange Commission today announced the leadership of the agency’s trial unit." ;
134+ DocumentRequest <EntitiesOptions > entitiesRequestData = DocumentRequest .<EntitiesOptions >builder ()
135+ .content (entitiesTextData )
136+ .build ();
137+ DocumentRequest <LanguageOptions > languageRequestData = DocumentRequest .<LanguageOptions >builder ().build ();
138+ List <RosetteRequest > requests = new ArrayList <>();
139+ for (int i = 0 ; i < entitiesRespCount / 2 ; i ++) {
140+ requests .add (this .api .createRosetteRequest (AbstractRosetteAPI .ENTITIES_SERVICE_PATH , entitiesRequestData , EntitiesResponse .class ));
141+ }
142+ for (int i = 0 ; i < languageRespCount / 2 ; i ++) {
143+ requests .add (this .api .createRosetteRequest (AbstractRosetteAPI .LANGUAGE_SERVICE_PATH , languageRequestData , LanguageResponse .class ));
144+ }
145+
146+ //run requests
147+ Date d1 = new Date ();
148+ List <Future <Response >> responses = this .api .submitRequests (requests );
149+ for (int i = 0 ; i < responses .size (); i ++) {
150+ responses .get (i ).get ();
151+ }
152+ Date d2 = new Date ();
153+
154+ assertTrue (d2 .getTime () - d1 .getTime () > delay * requests .size ()); // at least as long as the delay in the request
155+
156+ //run requests concurrency
157+ int concurrency = 3 ;
158+ this .api = new HttpRosetteAPI .Builder ().url (String .format ("http://localhost:%d/rest/v1" , mockServer .getPort ()))
159+ .connectionConcurrency (3 ).build ();
160+
161+ requests = new ArrayList <>();
162+ for (int i = 0 ; i < entitiesRespCount / 2 ; i ++) {
163+ requests .add (this .api .createRosetteRequest (AbstractRosetteAPI .ENTITIES_SERVICE_PATH , entitiesRequestData , EntitiesResponse .class ));
164+ }
165+ for (int i = 0 ; i < entitiesRespCount / 2 ; i ++) {
166+ requests .add (this .api .createRosetteRequest (AbstractRosetteAPI .LANGUAGE_SERVICE_PATH , languageRequestData , LanguageResponse .class ));
167+ }
168+
169+
170+ d1 = new Date ();
171+ responses = this .api .submitRequests (requests );
172+ for (int i = 0 ; i < responses .size (); i ++) {
173+ responses .get (i ).get ();
174+ }
175+ d2 = new Date ();
176+
177+ assertTrue (d2 .getTime () - d1 .getTime () < delay * requests .size ()); // less than serial requests
178+ assertTrue (d2 .getTime () - d1 .getTime () > requests .size () / concurrency * delay ); // running faster than this would suggest it exceeds the maximum concurrency
179+ }
180+
181+ private RosetteRequest setupShutdownTest (int shutdownWaitSeconds , int responseDelayMillis , CloseableHttpClient client ) {
182+ this .api = new HttpRosetteAPI .Builder ()
183+ .url (String .format ("http://localhost:%d/rest/v1" , mockServer .getPort ()))
184+ .shutdownWait (shutdownWaitSeconds )
185+ .httpClient (client )
186+ .build ();
187+
188+ //response setup
189+ String entitiesResponse = "{\" entities\" : [ { \" type\" : \" ORGANIZATION\" , \" mention\" : \" Securities and Exchange Commission\" , \" normalized\" : \" U.S. Securities and Exchange Commission\" , \" count\" : 1, \" mentionOffsets\" : [ { \" startOffset\" : 4, \" endOffset\" : 38 } ], \" entityId\" : \" Q953944\" , \" confidence\" : 0.39934742, \" linkingConfidence\" : 0.67404154 } ] }" ;
190+ setupResponse ("/rest/v1/entities" , entitiesResponse , 200 , responseDelayMillis , 1 );
191+
192+ //request setup
193+ String entitiesTextData = "The Securities and Exchange Commission today announced the leadership of the agency’s trial unit." ;
194+ DocumentRequest <EntitiesOptions > entitiesRequestData = DocumentRequest .<EntitiesOptions >builder ()
195+ .content (entitiesTextData )
196+ .build ();
197+ return this .api .createRosetteRequest (AbstractRosetteAPI .ENTITIES_SERVICE_PATH , entitiesRequestData , EntitiesResponse .class );
198+ }
199+
200+ @ Test
201+ void successfulShutdown () throws IOException {
202+ /* creating http client to avoid closing the httpClient of HttpRosetteApi during close()
203+ * which automatically closes the pending threads. This way the shutdown behaviour can be tested.*/
204+ CloseableHttpClient httpClient = HttpClients .createDefault ();
205+ int shutdownWait = 3 ;
206+ RosetteRequest request = setupShutdownTest (shutdownWait , 1000 , httpClient );
207+ this .api .submitRequest (request );
208+ this .api .close ();
209+ httpClient .close ();
210+
211+ assertInstanceOf (EntitiesResponse .class , request .getResponse ()); //got response successfully
212+ }
213+
214+ @ Test
215+ void timeoutShutdown () throws IOException {
216+ /* creating http client to avoid closing the httpClient of HttpRosetteApi during close()
217+ * which automatically closes the pending threads. This way the shutdown behaviour can be tested.*/
218+ CloseableHttpClient httpClient = HttpClients .createDefault ();
219+ int shutdownWait = 3 ;
220+ RosetteRequest request = setupShutdownTest (shutdownWait , 5000 , httpClient );
221+ this .api .submitRequest (request );
222+ this .api .close ();
223+
224+ httpClient .close ();
225+ assertNull (request .getResponse ()); //didn't get a response, it was forcefully shutdown
226+ }
227+
228+
229+ @ AfterEach
230+ void after () throws IOException {
231+ this .api .close ();
232+ }
233+
234+ }
0 commit comments