0949dbefba786e946caf446e18c62bc417a2b2ba
[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_name = "../adventure.yaml"
17 html_template_path = "coverage_dungeon.html.tpl"
18 html_output_path = "../coverage/adventure.yaml.html"
19
20 row_3_fields = """
21     <tr>
22         <td class="coverFile">{}</td>
23         <td class="{}">&nbsp;</td>
24         <td class="{}">&nbsp;</td>
25     </tr>
26 """
27
28 row_2_fields = """
29     <tr>
30         <td class="coverFile">{}</td>
31         <td class="{}">&nbsp;</td>
32     </tr>
33 """
34
35 def search(needle, haystack):
36     # Search for needle in haystack, first escaping needle for regex, then
37     # replacing %s, %d, etc. with regex wildcards, so the variable messages
38     # within the dungeon definition will actually match
39     needle = re.escape(needle) \
40              .replace("\\n", "\n") \
41              .replace("\\t", "\t") \
42              .replace("\%S", ".*") \
43              .replace("\%s", ".*") \
44              .replace("\%d", ".*") \
45              .replace("\%V", ".*")
46
47     return re.search(needle, haystack)
48
49 def loc_coverage(locations, text):
50     # locations have a long and a short description, that each have to 
51     # be checked seperately
52     for locname, loc in locations:
53         if loc["description"]["long"] == None or loc["description"]["long"] == '':
54             loc["description"]["long"] = True
55         if loc["description"]["long"] != True:
56             if search(loc["description"]["long"], text):
57                 loc["description"]["long"] = True
58         if loc["description"]["short"] == None or loc["description"]["short"] == '':
59             loc["description"]["short"] = True
60         if loc["description"]["short"] != True:
61             if search(loc["description"]["short"], text):
62                 loc["description"]["short"] = True
63
64 def arb_coverage(arb_msgs, text):
65     # arbitrary messages are a map to tuples
66     for i, msg in enumerate(arb_msgs):
67         (msg_name, msg_text) = msg
68         if msg_text == None or msg_text == '':
69             arb_msgs[i] = (msg_name, True)
70         elif msg_text != True:
71             if search(msg_text, text):
72                 arb_msgs[i] = (msg_name, True)
73
74 def obj_coverage(objects, text):
75     # objects have multiple descriptions based on state
76     for i, objouter in enumerate(objects):
77         (obj_name, obj) = objouter
78         if obj["descriptions"]:
79             for j, desc in enumerate(obj["descriptions"]):
80                 if desc == None or desc == '':
81                     obj["descriptions"][j] = True
82                     objects[i] = (obj_name, obj)
83                 elif desc != True:
84                     if search(desc, text):
85                         obj["descriptions"][j] = True
86                         objects[i] = (obj_name, obj)
87
88 def hint_coverage(hints, text):
89     # hints have a "question" where the hint is offered, followed
90     # by the actual hint if the player requests it
91     for name, hint in hints:
92         if hint["question"] != True:
93             if search(hint["question"], text):
94                 hint["question"] = True
95         if hint["hint"] != True:
96             if search(hint["hint"], text):
97                 hint["hint"] = True
98
99 def obit_coverage(obituaries, text):
100     # obituaries have a "query" where it asks the player for a resurrection, 
101     # followed by a snarky comment if the player says yes
102     for i, obit in enumerate(obituaries):
103         if obit["query"] != True:
104             if search(obit["query"], text):
105                 obit["query"] = True
106         if obit["yes_response"] != True:
107             if search(obit["yes_response"], text):
108                 obit["yes_response"] = True
109
110 def threshold_coverage(classes, text):
111     # works for class thresholds and turn threshold, which have a "message" 
112     # property
113     for i, msg in enumerate(classes):
114         if msg["message"] == None:
115             msg["message"] = True
116         elif msg["message"] != True:
117             if search(msg["message"], text):
118                 msg["message"] = True
119
120 def specials_actions_coverage(items, text):
121     # works for actions or specials
122     for name, item in items:
123         if item["message"] == None or item["message"] == "NO_MESSAGE":
124             item["message"] = True
125         if item["message"] != True:
126             if search(item["message"], text):
127                 item["message"] = True
128
129 if __name__ == "__main__":
130     with open(yaml_name, "r") as f:
131         db = yaml.load(f)
132
133     with open(html_template_path, "r") as f:
134         html_template = f.read()
135
136     motions = db["motions"]
137     locations = db["locations"]
138     arb_msgs = db["arbitrary_messages"]
139     objects = db["objects"]
140     hintsraw = db["hints"]
141     classes = db["classes"]
142     turn_thresholds = db["turn_thresholds"]
143     obituaries = db["obituaries"]
144     actions = db["actions"]
145     specials = db["specials"]
146
147     hints = []
148     for hint in hintsraw:
149         hints.append((hint["hint"]["name"], {"question" : hint["hint"]["question"],"hint" : hint["hint"]["hint"]}))
150
151     text = ""
152     for filename in os.listdir(test_dir):
153         if filename.endswith(".chk"):
154             with open(filename, "r") as chk:
155                 text = chk.read()
156                 loc_coverage(locations, text)
157                 arb_coverage(arb_msgs, text)
158                 obj_coverage(objects, text)
159                 hint_coverage(hints, text)
160                 threshold_coverage(classes, text)
161                 threshold_coverage(turn_thresholds, text)
162                 obit_coverage(obituaries, text)
163                 specials_actions_coverage(actions, text)
164                 specials_actions_coverage(specials, text)
165
166     location_html = ""
167     location_total = len(locations) * 2
168     location_covered = 0
169     locations.sort()
170     for locouter in locations:
171         locname = locouter[0]
172         loc = locouter[1]
173         if loc["description"]["long"] != True:
174             long_success = "uncovered"
175         else:
176             long_success = "covered"
177             location_covered += 1
178
179         if loc["description"]["short"] != True:
180             short_success = "uncovered"
181         else:
182             short_success = "covered"
183             location_covered += 1
184
185         location_html += row_3_fields.format(locname, long_success, short_success)
186     location_percent = round((location_covered / float(location_total)) * 100, 1)
187
188     arb_msgs.sort()
189     arb_msg_html = ""
190     arb_total = len(arb_msgs)
191     arb_covered = 0
192     for name, msg in arb_msgs:
193         if msg != True:
194             success = "uncovered"
195         else:
196             success = "covered"
197             arb_covered += 1
198         arb_msg_html += row_2_fields.format(name, success)
199     arb_percent = round((arb_covered / float(arb_total)) * 100, 1)
200
201     object_html = ""
202     objects_total = 0
203     objects_covered = 0
204     objects.sort()
205     for (obj_name, obj) in objects:
206         if obj["descriptions"]:
207             for j, desc in enumerate(obj["descriptions"]):
208                 objects_total += 1
209                 if desc != True:
210                     success = "uncovered"
211                 else:
212                     success = "covered"
213                     objects_covered += 1
214                 object_html += row_2_fields.format("%s[%d]" % (obj_name, j), success)
215     objects_percent = round((objects_covered / float(objects_total)) * 100, 1)
216
217     hints.sort()
218     hints_html = "";
219     hints_total = len(hints) * 2
220     hints_covered = 0
221     for name, hint in hints:
222         if hint["question"] != True:
223             question_success = "uncovered"
224         else:
225             question_success = "covered"
226             hints_covered += 1
227         if hint["hint"] != True:
228             hint_success = "uncovered"
229         else:
230             hint_success = "covered"
231             hints_covered += 1
232         hints_html += row_3_fields.format(name, question_success, hint_success)
233     hints_percent = round((hints_covered / float(hints_total)) * 100, 1)
234
235     class_html = ""
236     class_total = len(classes)
237     class_covered = 0
238     for name, msg in enumerate(classes):
239         if msg["message"] != True:
240             success = "uncovered"
241         else:
242             success = "covered"
243             class_covered += 1
244         class_html += row_2_fields.format(msg["threshold"], success)
245     class_percent = round((class_covered / float(class_total)) * 100, 1)
246
247     turn_html = ""
248     turn_total = len(turn_thresholds)
249     turn_covered = 0
250     for name, msg in enumerate(turn_thresholds):
251         if msg["message"] != True:
252             success = "uncovered"
253         else:
254             success = "covered"
255             turn_covered += 1
256         turn_html += row_2_fields.format(msg["threshold"], success)
257     turn_percent = round((turn_covered / float(turn_total)) * 100, 1)
258
259     obituaries_html = "";
260     obituaries_total = len(obituaries) * 2
261     obituaries_covered = 0
262     for i, obit in enumerate(obituaries):
263         if obit["query"] != True:
264             query_success = "uncovered"
265         else:
266             query_success = "covered"
267             obituaries_covered += 1
268         if obit["yes_response"] != True:
269             obit_success = "uncovered"
270         else:
271             obit_success = "covered"
272             obituaries_covered += 1
273         obituaries_html += row_3_fields.format(i, query_success, obit_success)
274     obituaries_percent = round((obituaries_covered / float(obituaries_total)) * 100, 1)
275
276     actions.sort()
277     actions_html = "";
278     actions_total = len(actions)
279     actions_covered = 0
280     for name, action in actions:
281         if action["message"] != True:
282             success = "uncovered"
283         else:
284             success = "covered"
285             actions_covered += 1
286         actions_html += row_2_fields.format(name, success)
287     actions_percent = round((actions_covered / float(actions_total)) * 100, 1)
288
289     special_html = ""
290     special_total = len(specials)
291     special_covered = 0
292     for name, special in specials:
293         if special["message"] != True:
294             success = "uncovered"
295         else:
296             success = "covered"
297             special_covered += 1
298         special_html += row_2_fields.format(name, success)
299     special_percent = round((special_covered / float(special_total)) * 100, 1)
300
301     # output some quick report stats
302     print("\nadventure.yaml coverage rate:")
303     print("  locations..........: {}% covered ({} of {})".format(location_percent, location_covered, location_total))
304     print("  arbitrary_messages.: {}% covered ({} of {})".format(arb_percent, arb_covered, arb_total))
305     print("  objects............: {}% covered ({} of {})".format(objects_percent, objects_covered, objects_total))
306     print("  hints..............: {}% covered ({} of {})".format(hints_percent, hints_covered, hints_total))
307     print("  classes............: {}% covered ({} of {})".format(class_percent, class_covered, class_total))
308     print("  turn_thresholds....: {}% covered ({} of {})".format(turn_percent, turn_covered, turn_total))
309     print("  obituaries.........: {}% covered ({} of {})".format(obituaries_percent, obituaries_covered, obituaries_total))
310     print("  actions............: {}% covered ({} of {})".format(actions_percent, actions_covered, actions_total))
311     print("  specials...........: {}% covered ({} of {})".format(special_percent, special_covered, special_total))
312
313     if len(sys.argv) > 1:
314         html_output_path = sys.argv[1]
315
316     # render HTML report
317     with open(html_output_path, "w") as f:
318         f.write(html_template.format(
319                 location_total, location_covered, location_percent,
320                 arb_total, arb_covered, arb_percent,
321                 objects_total, objects_covered, objects_percent,
322                 hints_total, hints_covered, hints_percent,
323                 class_total, class_covered, class_percent,
324                 turn_total, turn_covered, turn_percent,
325                 obituaries_total, obituaries_covered, obituaries_percent,
326                 actions_total, actions_covered, actions_percent,
327                 special_total, special_covered, special_percent,
328                 location_html, arb_msg_html, object_html, hints_html, 
329                 class_html, turn_html, obituaries_html, actions_html, special_html
330         ))