1111import sys
1212import tempfile
1313import threading
14+ import time
1415from http .server import BaseHTTPRequestHandler , ThreadingHTTPServer
1516from urllib .parse import parse_qs , urlsplit
1617
@@ -34,7 +35,7 @@ def log_message(self, *_args):
3435 def do_GET (self ):
3536 parsed = urlsplit (self .path )
3637 scenario , endpoint = parsed .path .lstrip ("/" ).split ("/api/v4/" , 1 )
37- self .calls .append ((scenario , endpoint , parse_qs (parsed .query )))
38+ self .calls .append ((scenario , endpoint , parse_qs (parsed .query ), time . monotonic () ))
3839 if self .headers .get ("PRIVATE-TOKEN" ) != TOKEN :
3940 self .reply (401 , {"message" : "Missing token" })
4041 return
@@ -46,7 +47,8 @@ def do_GET(self):
4647 self .reply (302 , {}, {"Location" : "/redirect/api/v4/trap" })
4748 else :
4849 self .reply (200 , {"id" : 91 , "path_with_namespace" : "engineering/platform" ,
49- "repository_access_level" : "enabled" })
50+ "repository_access_level" : "disabled" if scenario == "issues-only" else "enabled" ,
51+ "issues_access_level" : "enabled" , "wiki_access_level" : "enabled" })
5052 elif endpoint == "projects/91/members/all" :
5153 page = parse_qs (parsed .query ).get ("page" , ["1" ])[0 ]
5254 if scenario == "page-failure" and page == "2" :
@@ -114,6 +116,7 @@ def run():
114116 try :
115117 for scenario , expected_error in [
116118 ("success" , None ), ("numeric-project" , None ), ("missing-pagination" , None ),
119+ ("issues-only" , None ), ("paced" , None ),
117120 ("hidden-email" , "email" ), ("duplicate-email" , "email" ),
118121 ("wrong-identity" , "identity" ), ("denied" , "403" ),
119122 ("page-failure" , "500" ), ("pagination-loop" , "pagination" ),
@@ -128,7 +131,7 @@ def run():
128131 f"https://127.0.0.1:{ server .server_port } /{ scenario } " ,
129132 "--project" , "91" if scenario == "numeric-project" else "engineering/platform" ,
130133 "--output-dir" , str (output ),
131- "--requests-per-minute" , "600000" ]
134+ "--requests-per-minute" , "120" if scenario == "paced" else " 600000" ]
132135 result = subprocess .run (command , capture_output = True , text = True , timeout = 20 ,
133136 env = {** os .environ , "GITLAB_EXPORT_TOKEN" : TOKEN ,
134137 "SSL_CERT_FILE" : str (cert )})
@@ -169,8 +172,16 @@ def run():
169172 "subjects" : sorted ("u:" + row [1 ] for row in expected ),
170173 "otherProject" : [],
171174 }, parsed .stdout
175+ if scenario == "paced" :
176+ arrivals = [arrived for case , _ , _ , arrived in GitLabFixture .calls if case == scenario ]
177+ # The first request's transit time can consume one pacing interval.
178+ minimum_span = (len (arrivals ) - 2 ) * 0.5
179+ assert arrivals [- 1 ] - arrivals [0 ] >= minimum_span , "Requests exceeded the configured rate"
180+ report ["pacing" ] = {"requests" : len (arrivals ), "requestsPerMinute" : 120 ,
181+ "observedSeconds" : arrivals [- 1 ] - arrivals [0 ],
182+ "minimumSeconds" : minimum_span }
172183 report ["checks" ].append ({"name" : scenario , "passed" : True })
173- assert not any (endpoint == "trap" for _ , endpoint , _ in GitLabFixture .calls )
184+ assert not any (endpoint == "trap" for _ , endpoint , _ , _ in GitLabFixture .calls )
174185 report ["passed" ] = True
175186 finally :
176187 server .shutdown ()
0 commit comments