Coverage for tests/e2e/fuzz/common.py : 81%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2from __future__ import annotations
4# -- stdlib --
5from itertools import chain, combinations as C, permutations as P
6from typing import Any, cast
7import logging
8import random
10# -- third party --
11import gevent
13# -- own --
14from game.base import EventHandler
15from thb.actions import ActionStageLaunchCard, CardChooser, skill_wrap, CharacterChooser
16from thb.inputlets import ActionInputlet, ChooseOptionInputlet, ChoosePeerCardInputlet
19# -- code --
20log = logging.getLogger('UserInputFuzzingHandler')
23def let_it_go(*cores):
24 while True:
25 gevent.idle(-100)
27 for i in range(10): 27 ↛ 33line 27 didn't jump to line 33, because the loop on line 27 didn't complete
28 if any(c.server._ep.active for c in cores):
29 break
31 gevent.sleep(0.05)
32 else:
33 raise Exception('STUCK!')
35 for c in cores:
36 c.server._ep.active = False
39class UserInputFuzzingHandler(EventHandler):
41 def handle(self, evt: str, arg: Any) -> Any:
42 if evt == 'user_input':
43 trans, ilet = arg
44 self.react(trans, ilet)
45 elif evt in ('action_before', 'action_apply', 'action_after'):
46 self.effect_meta(evt, arg)
48 return arg
50 def effect_meta(self, evt: str, act: Any):
51 while hasattr(act, 'ui_meta'):
52 _type = {
53 'action_before': 'effect_string_before',
54 'action_apply': 'effect_string_apply',
55 'action_after': 'effect_string',
56 }[evt]
57 prompt = getattr(act.ui_meta, _type, None)
58 if not prompt: return
59 prompt(act)
60 break
62 while hasattr(act, 'ui_meta'):
63 if evt == 'action_before':
64 rays = getattr(act.ui_meta, 'ray', None)
65 rays = rays(act) if rays else []
67 _type = {
68 'action_before': 'sound_effect_before',
69 'action_apply': 'sound_effect_apply',
70 'action_after': 'sound_effect',
71 }[evt]
72 se = getattr(act.ui_meta, _type, None)
73 se = se and se(act)
74 break
76 def react(self, trans, ilet):
77 p = ilet.actor
78 g = self.game
80 if trans.name == 'ActionStageAction':
81 if random.random() < 0.05:
82 return False
84 cards = list(p.showncards) + list(p.cards) + list(p.equips)
86 while random.random() < 0.5:
87 # Skill
88 skl = [sk for sk in p.skills if 't_None' not in sk.target.__name__]
89 [sk.ui_meta.clickable() for sk in skl]
90 if skl:
91 sk = random.choice(skl)
92 else:
93 break
95 # Card
96 cc = list(chain(*[C(cards, i) for i in range(4)]))
97 random.shuffle(cc)
99 for cl in cc:
100 for tl in self.possible_targets(g, p, sk):
101 c = skill_wrap(p, [sk], cl, {})
103 ok, reason = self.ui_meta_walk_wrapped([c])
104 if not ok: 104 ↛ 105line 104 didn't jump to line 105, because the condition on line 104 was never true
105 assert not c.check(), (c, c.associated_cards, c.check(), ok, reason)
106 continue
108 try:
109 tl, ok = c.target(p, tl)
110 except Exception as e:
111 raise Exception(f"{c}.target: {c.target} failed") from e
113 try:
114 ok2, reason = c.ui_meta.is_action_valid(c, tl)
115 except Exception as e:
116 raise Exception(f"{c}.ui_meta.is_action_valid failed") from e
118 # This can happen
119 # assert (ok and bool(c.check())) == ok2, {
120 # 'ok': ok,
121 # 'c.check()': c.check(),
122 # 'ok2': ok2,
123 # 'c': c,
124 # 'tl': tl,
125 # 'reason': reason,
126 # }
128 if not ok:
129 continue
131 # This can happen too
132 # assert c.ui_meta.clickable(), c
134 if self.try_launch(ilet, cl, tl, skills=[sk]):
135 return
136 break
138 for c in cards:
139 if not c.associated_action:
140 continue
142 for t in self.possible_targets(g, p, c):
143 if self.try_launch(ilet, [c], t):
144 return True
146 elif trans.name in ('Action', 'AskForRejectAction') and isinstance(ilet, ActionInputlet):
147 rst = {
148 'skills': [],
149 'cards': [],
150 'characters': [],
151 }
153 if ilet.categories:
154 initiator = cast(CardChooser, ilet.initiator)
155 cond = initiator.cond
156 cl = list(p.showncards) + list(p.cards)
158 found = False
159 for skcls in ilet.actor.skills:
160 sk = skcls(ilet.actor)
161 if not sk.ui_meta.clickable():
162 continue
164 uiok, uireason = self.ui_meta_walk_wrapped([sk])
165 cct_ok, cct_reason = initiator.ui_meta.choose_card_text(initiator, [sk])
166 if cond([sk]) and random.random() < 0.5:
167 for c in chain([], C(cl, 1), C(cl, 2), [cl]): 167 ↛ 177line 167 didn't jump to line 177, because the loop on line 167 didn't complete
168 sk = skill_wrap(p, [skcls], c, {})
169 uiok, uireason = self.ui_meta_walk_wrapped([sk])
170 assert uiok
171 assert cond([sk])
172 rst['skills'] = [skcls]
173 rst['cards'] = c
174 found = True
175 break
177 if found:
178 break
179 else:
180 for c in chain(C(cl, 1), C(cl, 2), [cl]):
181 cct_ok, cct_reason = initiator.ui_meta.choose_card_text(initiator, c)
182 if cond(c):
183 assert cct_ok, (c, cct_reason)
184 rst['cards'] = c
185 break
187 if ilet.candidates:
188 initiator = cast(CharacterChooser, ilet.initiator)
189 target = initiator.choose_player_target
190 # (self, pl: Sequence[Character]) -> Tuple[List[Character], bool]: ...
191 pl = ilet.candidates
192 for p in chain(C(pl, 1), C(pl, 2), [pl]):
193 p, ok = target(p)
194 ccp_ok, ccp_reason = initiator.ui_meta.target(p)
195 if ok:
196 assert ccp_ok, (p, ccp_reason)
197 rst['characters'] = p
198 break
200 if rst: 200 ↛ 203line 200 didn't jump to line 203, because the condition on line 200 was never false
201 ilet.set_result(**rst)
203 return True
205 elif trans.name == 'ChooseOption' and isinstance(ilet, ChooseOptionInputlet):
206 ilet.set_option(random.choice(list(ilet.options) * 2 + [None]))
208 elif trans.name == 'ChoosePeerCard' and isinstance(ilet, ChoosePeerCardInputlet):
209 tgt = ilet.target
210 if random.random() < 0.9:
211 cats = [getattr(tgt, i) for i in ilet.categories]
212 cl = list(chain(*cats))
213 if cl:
214 ilet.set_card(random.choice(cl))
215 elif trans.name == 'SortCharacter':
216 pass
217 elif trans.name == 'ChooseGirl':
218 from settings import TESTING_CHARACTERS as TESTS
219 choices = [c for c in ilet.mapping[ilet.actor] if c.char_cls and c.char_cls.__name__ in TESTS]
220 if choices: 220 ↛ 221line 220 didn't jump to line 221, because the condition on line 220 was never true
221 c = random.choice(choices)
222 log.info('Got %s', c.char_cls)
223 ilet.set_choice(c)
224 elif trans.name == 'HarvestChoose':
225 pass
226 elif trans.name == 'Pindian':
227 pass
228 elif trans.name == 'HopeMask':
229 pass
230 elif trans.name == 'Prophet':
231 pass
232 elif trans.name == 'ChooseIndividualCard': 232 ↛ 233line 232 didn't jump to line 233, because the condition on line 232 was never true
233 pass
234 elif trans.name == 'BanGirl':
235 pass
236 elif trans.name == 'GalgameDialog': 236 ↛ 239line 236 didn't jump to line 239, because the condition on line 236 was never false
237 pass
238 else:
239 log.warning('Not processing %s transaction', trans.name)
240 1/0
242 def ui_meta_walk_wrapped(self, cl, check_is_complete=False):
243 from thb.cards.base import Skill
244 for c in cl:
245 if not isinstance(c, Skill):
246 continue
248 if check_is_complete: 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true
249 rst, reason = c.ui_meta.is_complete(c)
250 if not rst:
251 return rst, reason
253 rst, reason = self.ui_meta_walk_wrapped(c.associated_cards, True)
254 if not rst: 254 ↛ 255line 254 didn't jump to line 255, because the condition on line 254 was never true
255 return rst, reason
257 return True, 'OK'
259 def try_launch(self, ilet, cl, tl, skills=[]):
260 p = ilet.actor
261 if skills:
262 c = skill_wrap(p, skills, cl, {})
263 else:
264 assert len(cl) == 1, cl
265 c, = cl
267 act = ActionStageLaunchCard(p, tl, c)
268 if act.can_fire():
269 ilet.set_result(skills=skills, cards=cl, characters=tl)
270 return True
272 return False
274 def possible_targets(self, g, me, c):
275 target = c.target
276 tn = target.__name__
277 pl = g.players
279 if tn == 't_None': 279 ↛ 280line 279 didn't jump to line 280, because the condition on line 279 was never true
280 raise Exception('Fuck')
282 elif tn == 't_Self': 282 ↛ 283line 282 didn't jump to line 283, because the condition on line 282 was never true
283 rl = [[me]]
285 elif tn == 't_OtherOne': 285 ↛ 286line 285 didn't jump to line 286, because the condition on line 285 was never true
286 rl = list(pl)
287 random.shuffle(rl)
288 rl = [[i] for i in rl if i is not me]
290 elif tn == 't_One': 290 ↛ 291line 290 didn't jump to line 291, because the condition on line 290 was never true
291 rl = list(pl)
292 random.shuffle(rl)
293 rl = [[i] for i in rl]
295 elif tn == 't_All': 295 ↛ 296line 295 didn't jump to line 296, because the condition on line 295 was never true
296 rl = list(pl)
297 rl.remove(me)
298 rl = [rl]
300 elif tn == 't_AllInclusive': 300 ↛ 301line 300 didn't jump to line 301, because the condition on line 300 was never true
301 rl = [pl]
303 elif tn == '_t_OtherLessEqThanN': 303 ↛ 304line 303 didn't jump to line 304, because the condition on line 303 was never true
304 n = target._for_test_OtherLessEqThanN
305 rl = []
306 for i in range(n+1):
307 rl.extend(list(P(pl, i)))
308 random.shuffle(rl)
310 elif tn == 't_OneOrNone': 310 ↛ 311line 310 didn't jump to line 311, because the condition on line 310 was never true
311 rl = [[i] for i in pl]
312 rl.append([])
313 random.shuffle(rl)
315 elif tn == '_t_OtherN': 315 ↛ 316line 315 didn't jump to line 316, because the condition on line 315 was never true
316 n = target._for_test_OtherN
317 rl = list(P(pl, n))
318 random.shuffle(rl)
319 rl = [list(i) for i in rl]
320 else:
321 rl = []
322 for i in range(3): # HACK: should be enough
323 rl.extend(list(P(pl, i)))
324 random.shuffle(rl)
326 return rl