Coverage for gws-app/gws/test/team_engine/main.py: 0%

132 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 23:09 +0200

1""" 

2 

3https://opengeospatial.github.io/teamengine/users.html 

4https://github.com/opengeospatial/teamengine 

5 

6""" 

7 

8import os 

9import sys 

10import re 

11import base64 

12import html 

13import shutil 

14 

15LOCAL_APP_DIR = os.path.abspath(os.path.dirname(__file__) + '/../../..') 

16sys.path.insert(0, LOCAL_APP_DIR) 

17 

18import gws 

19import gws.lib.cli as cli 

20import gws.lib.net 

21import gws.lib.xmlx as xmlx 

22 

23USERNAME = "gwstest" 

24PASSWORD = "gws" 

25 

26USER_DIR = os.path.dirname(__file__) + f'/te_base/users/{USERNAME}' 

27 

28USAGE = """ 

29GWS TeamEngine runner 

30~~~~~~~~~~~~~~~~~~~~~ 

31 

32Commands: 

33 

34 main.py ls 

35 - show available test suites 

36 

37 main.py args <suite> 

38 - show arguments for the given suite 

39 

40 main.py run <suite> <url> 

41 - run a suite against the given URL 

42 

43Options: 

44 --host - TeamEngine host (default: 127.0.0.1) 

45 --port - TeamEngine port (default: 8090) 

46 --level - list of error levels to report (default: 4,6) or 'all' 

47  

48 --save-xml <path> - save raw XML output 

49  

50The TE docker container must be started before running this script (see `./docker-compose.yml`). 

51  

52""" 

53 

54OPTIONS = {} 

55 

56# https://www.w3.org/TR/EARL10-Schema/#OutcomeValue + http://cite.opengeospatial.org/earl#inheritedFailure" 

57# using CTL XML error levels from https://opengeospatial.github.io/teamengine/users.html 

58 

59ERROR_LEVELS = { 

60 'passed': ('pass', 1), 

61 'inapplicable': ('pass', 2), 

62 'untested': ('warn', 3), 

63 'cantTell': ('warn', 4), 

64 'inheritedFailure': ('FAIL', 5), 

65 'failed': ('FAIL', 6), 

66} 

67 

68STATUS_MARKS = { 

69 'FAIL': '\u274C', 

70 'warn': '\u2754', 

71 'pass': '\u2705', 

72} 

73 

74 

75def main(args): 

76 OPTIONS['host'] = args.get('host', 'localhost') 

77 OPTIONS['port'] = args.get('port', '8080') 

78 OPTIONS['save-xml'] = args.get('save-xml', '') 

79 

80 s = args.get('level', '4,6') 

81 if s == 'all': 

82 OPTIONS['level'] = list(range(7)) 

83 else: 

84 OPTIONS['level'] = [int(x) for x in gws.u.to_list(s)] 

85 

86 cmd = args.get(1) 

87 

88 if cmd == 'ls': 

89 print(cli.text_table(_get_suites(), header='auto')) 

90 return 0 

91 

92 if cmd == 'args': 

93 suite = args.get(2) 

94 print(cli.text_table(_get_args(suite))) 

95 return 0 

96 

97 if cmd == 'run': 

98 shutil.rmtree(f'{USER_DIR}/rest', ignore_errors=True) 

99 OPTIONS['suite'] = args.get(2) 

100 OPTIONS['url'] = args.get(3) 

101 xml = _invoke_test() 

102 results = _parse_test_results(xml) 

103 _report_test(results) 

104 return 0 

105 

106 

107def _get_suites(): 

108 xml = xmlx.from_string(_invoke('suites')) 

109 return sorted( 

110 [el.textdict() for el in xml.findall('.//testSuite')], 

111 key=str 

112 ) 

113 

114 

115def _get_args(suite): 

116 xml = xmlx.from_string(_invoke(f'suites/{suite}')) 

117 return [el.textdict() for el in xml.findall('.//testrunargument')] 

118 

119 

120URL_ARG = { 

121 'wfs20': 'wfs', 

122 'wms13': 'capabilities-url' 

123} 

124 

125 

126def _invoke_test(): 

127 if not OPTIONS['url'].startswith('http'): 

128 OPTIONS['url'] = 'http://' + OPTIONS['url'] 

129 url = OPTIONS['url'] 

130 if '?' not in url: 

131 url += '?request=GetCapabilities' 

132 url = url.replace('&', '&amp;') 

133 

134 suite = OPTIONS['suite'] 

135 text = _invoke(f'suites/{suite}/run?{URL_ARG[suite]}={url}', accept='application/rdf+xml') 

136 if OPTIONS['save-xml']: 

137 gws.u.write_file(OPTIONS['save-xml'], text) 

138 

139 return xmlx.from_string(text, remove_namespaces=True) 

140 

141 

142def _parse_test_results(xml): 

143 # parse test results in the EARL format (https://www.w3.org/TR/EARL10-Guide) 

144 

145 tc_map = {} 

146 results = [] 

147 

148 for case_el in xml.findall('.//TestCase'): 

149 uid = case_el.get('about') 

150 tc = dict( 

151 testCase=uid, 

152 title=_nows(case_el.textof('title')), 

153 description=_nows(html.unescape(case_el.textof('description'))), 

154 details='', 

155 ) 

156 if re.match(r'^s\d+', uid): 

157 # uids starting with sNNNN reference log files in the rest dir 

158 path = USER_DIR + '/rest/' + uid.split('#')[0] + '/log.xml' 

159 tc['details'] = _parse_ctl_log(path) 

160 

161 tc_map[uid] = tc 

162 

163 for ass_el in xml.findall('.//Assertion'): 

164 test_el = ass_el.find('test') 

165 

166 # it's either an inline <test><TestCase about=ID> or a reference <test resource=ID> 

167 case_el = test_el.find('TestCase') 

168 tc = tc_map[case_el.get('about')] if case_el else tc_map[test_el.get('resource')] 

169 

170 res_el = ass_el.find('result/TestResult') 

171 

172 r = dict(tc) 

173 # eg <earl:outcome rdf:resource="http://www.w3.org/ns/earl#passed"/> 

174 r['status2'] = res_el.find('outcome').get('resource').split('#')[-1] 

175 r['status'], r['level'] = ERROR_LEVELS[r['status2']] 

176 

177 # eg <earl:Assertion rdf:about="assert-7"> 

178 r['uid'] = int(re.sub(r'\D+', '', ass_el.get('about'))) 

179 

180 s = res_el.textof('description') 

181 if s and s != 'No details available.': 

182 r['details'] = s + '\n' + r['details'] 

183 

184 results.append(r) 

185 

186 return sorted(results, key=lambda r: r['uid']) 

187 

188def _parse_ctl_log(path): 

189 if not os.path.exists(path): 

190 return '' 

191 

192 xml = xmlx.from_path(path, remove_namespaces=True) 

193 

194 desc = [el.text for el in xml.findall('.//message')] 

195 

196 params = [] 

197 for el in xml.findall('.//param'): 

198 if el.get('name'): 

199 params.append(el.get('name', '') + '=' + (el.text or '')) 

200 if params: 

201 desc.append(OPTIONS['url'].split('?')[0] + '?' + '&'.join(params)) 

202 

203 return '\n'.join(desc) 

204 

205 

206def _report_test(results): 

207 stats = f'TOTAL={len(results)}' 

208 

209 by_status = {} 

210 for r in results: 

211 by_status[r['status2']] = by_status.get(r['status2'], 0) + 1 

212 for k, v in sorted(by_status.items()): 

213 stats += f' {k}={v}' 

214 

215 cli.info(f'') 

216 cli.info(stats) 

217 cli.info(f'') 

218 

219 results = [r for r in results if r['level'] in OPTIONS['level']] 

220 

221 for r in results: 

222 print(_nows(f""" 

223 {STATUS_MARKS[r['status']]}  

224 {r['status']}  

225 ({r['status2']}, {r['level']}):  

226 {r['title']}. {r['description']} 

227 [{r['testCase']}] 

228 """)) 

229 

230 if r['details']: 

231 for ln in r['details'].splitlines(): 

232 print(f' | {ln}') 

233 

234 print() 

235 

236 return 

237 

238 

239def _invoke(path, **kwargs): 

240 url = f'http://{OPTIONS["host"]}:{OPTIONS["port"]}/teamengine/rest/{path}' 

241 headers = { 

242 'Authorization': f'Basic ' + base64.b64encode(f"{USERNAME}:{PASSWORD}".encode()).decode(), 

243 'Accept': kwargs.pop('accept', 'application/xml'), 

244 } 

245 cli.info(f'>> {url}') 

246 try: 

247 res = gws.lib.net.http_request(url, headers=headers, **kwargs) 

248 if res.status_code != 200: 

249 cli.error(res.text) 

250 cli.fatal(f'HTTP ERROR {res.status_code}') 

251 return res.text 

252 except gws.lib.net.Error as exc: 

253 cli.fatal(f'HTTP ERROR: {exc!r}') 

254 

255 

256def _nows(s): 

257 return re.sub(r'\s+', ' ', s.strip()) 

258 

259 

260if __name__ == '__main__': 

261 cli.main('test', main, USAGE)