3 # This is the open-adventure dungeon text coverage report generator. It
4 # consumes a YAML description of the dungeon and determines whether the
5 # various strings contained are present within the test check files.
7 # The default HTML output is appropriate for use with Gitlab CI.
8 # You can override it with a command-line argument.
16 YAML_PATH = "../adventure.yaml"
17 HTML_TEMPLATE_PATH = "coverage_dungeon.html.tpl"
18 DEFAULT_HTML_OUTPUT_PATH = "../coverage/adventure.yaml.html"
20 STDOUT_REPORT_CATEGORY = " {name:.<19}: {percent}% covered ({covered} of {total}))\n"
22 HTML_SUMMARY_ROW = """
24 <td class="headerItem"><a href="#{name}">{name}:</a></td>
25 <td class="headerCovTableEntry">{total}</td>
26 <td class="headerCovTableEntry">{covered}</td>
27 <td class="headerCovTableEntry">{percent}%</td>
31 HTML_CATEGORY_TABLE = """
39 HTML_CATEGORY_TABLE_HEADER_3_FIELDS = """
41 <td class="tableHead" width="60%">{}</td>
42 <td class="tableHead" width="20%">{}</td>
43 <td class="tableHead" width="20%">{}</td>
47 HTML_CATEGORY_TABLE_HEADER_2_FIELDS = """
49 <td class="tableHead" colspan="2">{}</td>
50 <td class="tableHead">{}</td>
54 HTML_CATEGORY_ROW_3_FIELDS = """
56 <td class="coverFile">{}</td>
57 <td class="{}"> </td>
58 <td class="{}"> </td>
62 HTML_CATEGORY_ROW_2_FIELDS = """
64 <td class="coverFile" colspan="2">{}</td>
65 <td class="{}"> </td>
69 def search(needle, haystack):
70 # Search for needle in haystack, first escaping needle for regex, then
71 # replacing %s, %d, etc. with regex wildcards, so the variable messages
72 # within the dungeon definition will actually match
73 needle = re.escape(needle) \
74 .replace("\\n", "\n") \
75 .replace("\\t", "\t") \
76 .replace("\%S", ".*") \
77 .replace("\%s", ".*") \
78 .replace("\%d", ".*") \
81 return re.search(needle, haystack)
83 def loc_coverage(locations, text):
84 # locations have a long and a short description, that each have to
85 # be checked seperately
86 for locname, loc in locations:
87 if loc["description"]["long"] == None or loc["description"]["long"] == '':
88 loc["description"]["long"] = True
89 if loc["description"]["long"] != True:
90 if search(loc["description"]["long"], text):
91 loc["description"]["long"] = True
92 if loc["description"]["short"] == None or loc["description"]["short"] == '':
93 loc["description"]["short"] = True
94 if loc["description"]["short"] != True:
95 if search(loc["description"]["short"], text):
96 loc["description"]["short"] = True
98 def arb_coverage(arb_msgs, text):
99 # arbitrary messages are a map to tuples
100 for i, msg in enumerate(arb_msgs):
101 (msg_name, msg_text) = msg
102 if msg_text == None or msg_text == '':
103 arb_msgs[i] = (msg_name, True)
104 elif msg_text != True:
105 if search(msg_text, text):
106 arb_msgs[i] = (msg_name, True)
108 def obj_coverage(objects, text):
109 # objects have multiple descriptions based on state
110 for i, objouter in enumerate(objects):
111 (obj_name, obj) = objouter
112 if obj["descriptions"]:
113 for j, desc in enumerate(obj["descriptions"]):
114 if desc == None or desc == '':
115 obj["descriptions"][j] = True
116 objects[i] = (obj_name, obj)
118 if search(desc, text):
119 obj["descriptions"][j] = True
120 objects[i] = (obj_name, obj)
122 def hint_coverage(hints, text):
123 # hints have a "question" where the hint is offered, followed
124 # by the actual hint if the player requests it
125 for i, hint in enumerate(hints):
126 if hint["hint"]["question"] != True:
127 if search(hint["hint"]["question"], text):
128 hint["hint"]["question"] = True
129 if hint["hint"]["hint"] != True:
130 if search(hint["hint"]["hint"], text):
131 hint["hint"]["hint"] = True
133 def obit_coverage(obituaries, text):
134 # obituaries have a "query" where it asks the player for a resurrection,
135 # followed by a snarky comment if the player says yes
136 for i, obit in enumerate(obituaries):
137 if obit["query"] != True:
138 if search(obit["query"], text):
140 if obit["yes_response"] != True:
141 if search(obit["yes_response"], text):
142 obit["yes_response"] = True
144 def threshold_coverage(classes, text):
145 # works for class thresholds and turn threshold, which have a "message"
147 for i, msg in enumerate(classes):
148 if msg["message"] == None:
149 msg["message"] = True
150 elif msg["message"] != True:
151 if search(msg["message"], text):
152 msg["message"] = True
154 def specials_actions_coverage(items, text):
155 # works for actions or specials
156 for name, item in items:
157 if item["message"] == None or item["message"] == "NO_MESSAGE":
158 item["message"] = True
159 if item["message"] != True:
160 if search(item["message"], text):
161 item["message"] = True
163 if __name__ == "__main__":
164 with open(YAML_PATH, "r") as f:
167 # Create report for each catagory, including HTML table, total items,
168 # and number of items covered
170 for name in db.keys():
171 # initialize each catagory
173 "name" : name, # convenience for string formatting
179 motions = db["motions"]
180 locations = db["locations"]
181 arb_msgs = db["arbitrary_messages"]
182 objects = db["objects"]
184 classes = db["classes"]
185 turn_thresholds = db["turn_thresholds"]
186 obituaries = db["obituaries"]
187 actions = db["actions"]
188 specials = db["specials"]
191 for filename in os.listdir(TEST_DIR):
192 if filename.endswith(".chk"):
193 with open(filename, "r") as chk:
195 loc_coverage(locations, text)
196 arb_coverage(arb_msgs, text)
197 obj_coverage(objects, text)
198 hint_coverage(hints, text)
199 threshold_coverage(classes, text)
200 threshold_coverage(turn_thresholds, text)
201 obit_coverage(obituaries, text)
202 specials_actions_coverage(actions, text)
203 specials_actions_coverage(specials, text)
205 report["locations"]["total"] = len(locations) * 2
206 report["locations"]["html"] = HTML_CATEGORY_TABLE_HEADER_3_FIELDS.format("Location ID", "long", "short")
208 for locouter in locations:
209 locname = locouter[0]
211 if loc["description"]["long"] != True:
212 long_success = "uncovered"
214 long_success = "covered"
215 report["locations"]["covered"] += 1
217 if loc["description"]["short"] != True:
218 short_success = "uncovered"
220 short_success = "covered"
221 report["locations"]["covered"] += 1
223 report["locations"]["html"] += HTML_CATEGORY_ROW_3_FIELDS.format(locname, long_success, short_success)
226 report["arbitrary_messages"]["total"] = len(arb_msgs)
227 report["arbitrary_messages"]["html"] = HTML_CATEGORY_TABLE_HEADER_2_FIELDS.format("Arbitrary Message ID", "covered")
228 for name, msg in arb_msgs:
230 success = "uncovered"
233 report["arbitrary_messages"]["covered"] += 1
234 report["arbitrary_messages"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(name, success)
237 report["objects"]["html"] = HTML_CATEGORY_TABLE_HEADER_2_FIELDS.format("Object ID", "covered")
238 for (obj_name, obj) in objects:
239 if obj["descriptions"]:
240 for j, desc in enumerate(obj["descriptions"]):
241 report["objects"]["total"] += 1
243 success = "uncovered"
246 report["objects"]["covered"] += 1
247 report["objects"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format("%s[%d]" % (obj_name, j), success)
250 report["hints"]["total"] = len(hints) * 2
251 report["hints"]["html"] = HTML_CATEGORY_TABLE_HEADER_3_FIELDS.format("Hint ID", "question", "hint")
252 for i, hint in enumerate(hints):
253 hintname = hint["hint"]["name"]
254 if hint["hint"]["question"] != True:
255 question_success = "uncovered"
257 question_success = "covered"
258 report["hints"]["covered"] += 1
259 if hint["hint"]["hint"] != True:
260 hint_success = "uncovered"
262 hint_success = "covered"
263 report["hints"]["covered"] += 1
264 report["hints"]["html"] += HTML_CATEGORY_ROW_3_FIELDS.format(name, question_success, hint_success)
266 report["classes"]["total"] = len(classes)
267 report["classes"]["html"] = HTML_CATEGORY_TABLE_HEADER_2_FIELDS.format("Adventurer Class #", "covered")
268 for name, msg in enumerate(classes):
269 if msg["message"] != True:
270 success = "uncovered"
273 report["classes"]["covered"] += 1
274 report["classes"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(msg["threshold"], success)
276 report["turn_thresholds"]["total"] = len(turn_thresholds)
277 report["turn_thresholds"]["html"] = HTML_CATEGORY_TABLE_HEADER_2_FIELDS.format("Turn Threshold", "covered")
278 for name, msg in enumerate(turn_thresholds):
279 if msg["message"] != True:
280 success = "uncovered"
283 report["turn_thresholds"]["covered"] += 1
284 report["turn_thresholds"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(msg["threshold"], success)
286 report["obituaries"]["total"] = len(obituaries) * 2
287 report["obituaries"]["html"] = HTML_CATEGORY_TABLE_HEADER_3_FIELDS.format("Obituary #", "query", "yes_response")
288 for i, obit in enumerate(obituaries):
289 if obit["query"] != True:
290 query_success = "uncovered"
292 query_success = "covered"
293 report["obituaries"]["covered"] += 1
294 if obit["yes_response"] != True:
295 obit_success = "uncovered"
297 obit_success = "covered"
298 report["obituaries"]["covered"] += 1
299 report["obituaries"]["html"] += HTML_CATEGORY_ROW_3_FIELDS.format(i, query_success, obit_success)
302 report["actions"]["total"] = len(actions)
303 report["actions"]["html"] = HTML_CATEGORY_TABLE_HEADER_2_FIELDS.format("Action ID", "covered")
304 for name, action in actions:
305 if action["message"] != True:
306 success = "uncovered"
309 report["actions"]["covered"] += 1
310 report["actions"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(name, success)
312 report["specials"]["total"] = len(specials)
313 report["specials"]["html"] = HTML_CATEGORY_TABLE_HEADER_2_FIELDS.format("Special ID", "covered")
314 for name, special in specials:
315 if special["message"] != True:
316 success = "uncovered"
319 report["specials"]["covered"] += 1
320 report["specials"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(name, success)
322 # calculate percentages for each catagory and HTML for category tables
325 summary_stdout = "adventure.yaml coverage rate:\n"
326 for name, category in sorted(report.items()):
327 if(category["total"] > 0):
328 report[name]["percent"] = round((category["covered"] / float(category["total"])) * 100, 1)
329 summary_stdout += STDOUT_REPORT_CATEGORY.format(**report[name])
330 categories_html += HTML_CATEGORY_TABLE.format(id=name, rows=category["html"])
331 summary_html += HTML_SUMMARY_ROW.format(**report[name])
333 report[name]["percent"] = 100;
335 # output some quick report stats
336 print(summary_stdout)
338 if len(sys.argv) > 1:
339 html_output_path = sys.argv[1]
341 html_output_path = DEFAULT_HTML_OUTPUT_PATH
345 with open(HTML_TEMPLATE_PATH, "r") as f:
346 # read in HTML template
347 html_template = f.read()
349 print 'ERROR: reading HTML report template failed (%s)' % e.strerror
352 # parse template with report and write it out
354 with open(html_output_path, "w") as f:
355 f.write(html_template.format(categories=categories_html, summary=summary_html))
357 print 'ERROR: writing HTML report failed (%s)' % e.strerror