First stage cleanup of YAML dungeon generator. Less hard-coded stuff.
[open-adventure.git] / tests / coverage_dungeon.py
1 #!/usr/bin/env python
2
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.
6 #
7 # The default HTML output is appropriate for use with Gitlab CI.
8 # You can override it with a command-line argument.
9
10 import os
11 import sys
12 import yaml
13 import re
14
15 TEST_DIR = "."
16 YAML_PATH = "../adventure.yaml"
17 HTML_TEMPLATE_PATH = "coverage_dungeon.html.tpl"
18 DEFAULT_HTML_OUTPUT_PATH = "../coverage/adventure.yaml.html"
19
20 STDOUT_REPORT_CATEGORY = "  {name:.<19}: {percent}% covered ({covered} of {total}))\n"
21
22 HTML_SUMMARY_ROW = """
23     <tr>
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>
28     </tr>
29 """
30
31 HTML_CATEGORY_TABLE = """
32     <tr id="{id}"></tr>
33     {rows}
34     <tr>
35         <td>&nbsp;</td>
36     </tr>
37 """
38
39 HTML_CATEGORY_TABLE_HEADER_3_FIELDS = """
40     <tr>
41         <td class="tableHead" width="60%">{}</td>
42         <td class="tableHead" width="20%">{}</td>
43         <td class="tableHead" width="20%">{}</td>
44     </tr>
45 """
46
47 HTML_CATEGORY_TABLE_HEADER_2_FIELDS = """
48     <tr>
49         <td class="tableHead" colspan="2">{}</td>
50         <td class="tableHead">{}</td>
51     </tr>
52 """
53
54 HTML_CATEGORY_ROW_3_FIELDS = """
55     <tr>
56         <td class="coverFile">{}</td>
57         <td class="{}">&nbsp;</td>
58         <td class="{}">&nbsp;</td>
59     </tr>
60 """
61
62 HTML_CATEGORY_ROW_2_FIELDS = """
63     <tr>
64         <td class="coverFile" colspan="2">{}</td>
65         <td class="{}">&nbsp;</td>
66     </tr>
67 """
68
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", ".*") \
79              .replace("\%V", ".*")
80
81     return re.search(needle, haystack)
82
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
97
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)
107
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)
117                 elif desc != True:
118                     if search(desc, text):
119                         obj["descriptions"][j] = True
120                         objects[i] = (obj_name, obj)
121
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
132
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):
139                 obit["query"] = True
140         if obit["yes_response"] != True:
141             if search(obit["yes_response"], text):
142                 obit["yes_response"] = True
143
144 def threshold_coverage(classes, text):
145     # works for class thresholds and turn threshold, which have a "message"
146     # property
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
153
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
162
163 if __name__ == "__main__":
164     with open(YAML_PATH, "r") as f:
165         db = yaml.load(f)
166
167     # Create report for each catagory, including HTML table, total items,
168     # and number of items covered
169     report = {}
170     for name in db.keys():
171         # initialize each catagory
172         report[name] = {
173             "name" : name, # convenience for string formatting
174             "html" : "",
175             "total" : 0,
176             "covered" : 0
177         }
178
179     motions = db["motions"]
180     locations = db["locations"]
181     arb_msgs = db["arbitrary_messages"]
182     objects = db["objects"]
183     hints = db["hints"]
184     classes = db["classes"]
185     turn_thresholds = db["turn_thresholds"]
186     obituaries = db["obituaries"]
187     actions = db["actions"]
188     specials = db["specials"]
189
190     text = ""
191     for filename in os.listdir(TEST_DIR):
192         if filename.endswith(".chk"):
193             with open(filename, "r") as chk:
194                 text = chk.read()
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)
204
205     report["locations"]["total"] = len(locations) * 2
206     report["locations"]["html"] = HTML_CATEGORY_TABLE_HEADER_3_FIELDS.format("Location ID", "long", "short")
207     locations.sort()
208     for locouter in locations:
209         locname = locouter[0]
210         loc = locouter[1]
211         if loc["description"]["long"] != True:
212             long_success = "uncovered"
213         else:
214             long_success = "covered"
215             report["locations"]["covered"] += 1
216
217         if loc["description"]["short"] != True:
218             short_success = "uncovered"
219         else:
220             short_success = "covered"
221             report["locations"]["covered"] += 1
222
223         report["locations"]["html"] += HTML_CATEGORY_ROW_3_FIELDS.format(locname, long_success, short_success)
224
225     arb_msgs.sort()
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:
229         if msg != True:
230             success = "uncovered"
231         else:
232             success = "covered"
233             report["arbitrary_messages"]["covered"] += 1
234         report["arbitrary_messages"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(name, success)
235
236     objects.sort()
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
242                 if desc != True:
243                     success = "uncovered"
244                 else:
245                     success = "covered"
246                     report["objects"]["covered"] += 1
247                 report["objects"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format("%s[%d]" % (obj_name, j), success)
248
249     hints.sort()
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"
256         else:
257             question_success = "covered"
258             report["hints"]["covered"] += 1
259         if hint["hint"]["hint"] != True:
260             hint_success = "uncovered"
261         else:
262             hint_success = "covered"
263             report["hints"]["covered"] += 1
264         report["hints"]["html"] += HTML_CATEGORY_ROW_3_FIELDS.format(name, question_success, hint_success)
265
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"
271         else:
272             success = "covered"
273             report["classes"]["covered"] += 1
274         report["classes"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(msg["threshold"], success)
275
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"
281         else:
282             success = "covered"
283             report["turn_thresholds"]["covered"] += 1
284         report["turn_thresholds"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(msg["threshold"], success)
285
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"
291         else:
292             query_success = "covered"
293             report["obituaries"]["covered"] += 1
294         if obit["yes_response"] != True:
295             obit_success = "uncovered"
296         else:
297             obit_success = "covered"
298             report["obituaries"]["covered"] += 1
299         report["obituaries"]["html"] += HTML_CATEGORY_ROW_3_FIELDS.format(i, query_success, obit_success)
300
301     actions.sort()
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"
307         else:
308             success = "covered"
309             report["actions"]["covered"] += 1
310         report["actions"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(name, success)
311
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"
317         else:
318             success = "covered"
319             report["specials"]["covered"] += 1
320         report["specials"]["html"] += HTML_CATEGORY_ROW_2_FIELDS.format(name, success)
321
322     # calculate percentages for each catagory and HTML for category tables
323     categories_html = ""
324     summary_html = ""
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])
332         else:
333             report[name]["percent"] = 100;
334
335     # output some quick report stats
336     print(summary_stdout)
337
338     if len(sys.argv) > 1:
339         html_output_path = sys.argv[1]
340     else:
341         html_output_path = DEFAULT_HTML_OUTPUT_PATH
342
343     # render HTML report
344     try:
345         with open(HTML_TEMPLATE_PATH, "r") as f:
346             # read in HTML template
347             html_template = f.read()
348     except IOError as e:
349         print 'ERROR: reading HTML report template failed (%s)' % e.strerror
350         exit(-1)
351
352     # parse template with report and write it out
353     try:
354         with open(html_output_path, "w") as f:
355             f.write(html_template.format(categories=categories_html, summary=summary_html))
356     except IOError as e:
357         print 'ERROR: writing HTML report failed (%s)' % e.strerror