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