PageRenderTime 73ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/Scan/TestIn/Tests/common.py

https://gitlab.com/tylert/kiibohd-controller
Python | 1523 lines | 1393 code | 37 blank | 93 comment | 14 complexity | 3c42f1758fd5e99317cc4453db31580a MD5 | raw file
Possible License(s): GPL-3.0, MIT, BSD-3-Clause
  1. '''
  2. Common functions for Host-side KLL tests
  3. '''
  4. # Copyright (C) 2016-2019 by Jacob Alexander
  5. #
  6. # This file is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Lesser General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This file is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Lesser General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public License
  17. # along with this file. If not, see <http://www.gnu.org/licenses/>.
  18. ### Imports ###
  19. import copy
  20. import inspect
  21. import linecache
  22. import logging
  23. import sys
  24. from collections import namedtuple
  25. import kiilogger
  26. ### Logger ###
  27. logger = kiilogger.get_logger('Tests/common.py')
  28. ### Classes ###
  29. class KLLTestRunner:
  30. '''
  31. Runs KLLTest classes
  32. Given a list of test classes objects, run the tests and query the result.
  33. '''
  34. def __init__(self, tests):
  35. '''
  36. Initialize KLLTestRunner objects
  37. @param tests: List of KLLTest objects
  38. '''
  39. # Intialize
  40. self.test_results = []
  41. self.tests = tests
  42. self.overall = None
  43. # Prepare each of the tests
  44. for test in self.tests:
  45. logger.info("Initializing: {}", test.__class__.__name__)
  46. if not test.prepare():
  47. logger.warning("'{}' failed prepare step.".format(test.__class__.__name__))
  48. def run(self):
  49. '''
  50. Run list of tests.
  51. @return: Overall test result.
  52. '''
  53. self.overall = True
  54. for test in self.tests:
  55. testname = test.__class__.__name__
  56. passed = True
  57. logger.info("{}: {}", header("Running"), testname)
  58. if not test.run():
  59. logger.error("'{}' failed.", testname)
  60. self.overall = False
  61. passed = False
  62. logger.info("{} has finished", header(testname))
  63. self.test_results.append((testname, passed, test.results()))
  64. return self.overall
  65. def results(self):
  66. '''
  67. Returns a list of KLLTestResult objects for the tests.
  68. Empty list if the self.run() hasn't been called yet.
  69. @return: List of KLLTestResult objects
  70. '''
  71. return self.test_results
  72. def passed(self):
  73. '''
  74. Returns a list of KLLTestResult objects that have passed.
  75. @return: List of passing KLLTestResult objects
  76. '''
  77. passed = []
  78. for test in self.test_results:
  79. if test[2]:
  80. passed.append(test)
  81. return passed
  82. def failed(self):
  83. '''
  84. Returns a list of KLLTestResult objects that have failed.
  85. @return: List of failing KLLTestResult objects
  86. '''
  87. failed = []
  88. for test in self.test_results:
  89. if not test[2]:
  90. failed.append(test)
  91. return failed
  92. class KLLTestUnitResult:
  93. '''
  94. KLLTestUnitResult Container Class
  95. Contains the results of a single test
  96. '''
  97. def __init__(self, parent, result, unit, layer=None):
  98. '''
  99. Constructor
  100. @param parent: Parent class, i.e. the class of the test
  101. @param result: Boolean, True if test passed, False if it didn't
  102. @param unit: Unit test object
  103. @param layer: Layer under test, set to None if not used
  104. '''
  105. self.parent = parent
  106. self.result = result
  107. self.unit = unit
  108. self.layer = layer
  109. class KLLTest:
  110. '''
  111. KLLTest base class
  112. Derive all tests from this class
  113. '''
  114. def __init__(self, tests=None, test=0):
  115. '''
  116. KLLTest base class constructor
  117. @param tests: Specify number of sub-tests to run, None if all of them.
  118. @param test: Specify a specific test to start from
  119. '''
  120. import interface
  121. # kll.json helper file
  122. self.klljson = interface.control.json_input
  123. # Reference to callback datastructure
  124. self.data = interface.control.data
  125. # Artificial limit for sub-tests
  126. self.test = test
  127. self.tests = tests
  128. # Test Results
  129. self.testresults = []
  130. self.overall = None
  131. # Used by sub-test children for debugging
  132. self.cur_test = None
  133. def prepare(self):
  134. '''
  135. Prepare to run test
  136. Does necessary initialization before attempting to run.
  137. Must be done before run.
  138. @return: True if ready to run test, False otherwise
  139. '''
  140. return True
  141. def run(self):
  142. '''
  143. Evaluate tests
  144. Iterates over prepared tests
  145. @return: Result of the test
  146. '''
  147. import interface
  148. overall = True
  149. curtest = 0
  150. for index, test in enumerate(self.testresults):
  151. # Skip tests before specified index
  152. if index < self.test:
  153. continue
  154. if test.unit.key is not None:
  155. logger.info("{}:{} Layer({}) {} {} -> {}",
  156. header("Sub-test"),
  157. index,
  158. test.layer,
  159. blued(test.unit.__class__.__name__),
  160. test.unit.key,
  161. test.unit.entry['kll'],
  162. )
  163. else:
  164. logger.info("{}:{} Layer({}) {} {}",
  165. header("Sub-test"),
  166. index,
  167. test.layer,
  168. blued(test.unit.__class__.__name__),
  169. test.unit.info,
  170. )
  171. # Set current test, used by sub-test children for debugging
  172. self.cur_test = index
  173. ## TODO Run Permutation Start
  174. # Make sure we're in NKRO mode
  175. interface.control.cmd('setKbdProtocol')(1)
  176. # Prepare layer setting
  177. # Run loop, to make sure layer is engaged already
  178. interface.control.cmd('lockLayer')(test.layer)
  179. # Clear any pending trigger events
  180. interface.control.cmd('clearMacroTriggerEventBuffer')()
  181. # Run test, and record result
  182. test.result = test.unit.run()
  183. # Check if the animation stack is not empty
  184. self.clean_animation_stack()
  185. # Cleanup layer manipulations
  186. interface.control.cmd('clearLayers')()
  187. ## TODO Run Permutation Start
  188. # Check if test has failed
  189. if not test.result:
  190. overall = False
  191. curtest += 1
  192. if self.tests is not None and curtest >= self.tests:
  193. logger.warning("Stopping at test #{}", curtest)
  194. break
  195. return overall
  196. def clean_animation_stack(self):
  197. '''
  198. Clean animation stack (if necessary)
  199. Only runs if PixelMap module is compiled in.
  200. '''
  201. import interface
  202. if 'Pixel_MapEnabled' in self.klljson['Defines'] and int(self.klljson['Defines']['Pixel_MapEnabled']['value']) == 1:
  203. animation_stack = interface.control.cmd('animationStackInfo')()
  204. if animation_stack.size > 0:
  205. # Print out info on the current state of the stack
  206. logger.warning("Animation Stack is not empty! Cleaning... {} animations", animation_stack.size)
  207. for index in range(animation_stack.size):
  208. elem = animation_stack.stack[0][index]
  209. logger.warning("{} >> index={}, pos={}, subpos={}",
  210. self.klljson['AnimationSettingsIndex'][elem.index]['name'],
  211. elem.index,
  212. elem.pos,
  213. elem.subpos,
  214. )
  215. # Set AnimationControl_Stop, update FrameState then run one loop to reset PixelMap state
  216. interface.control.cmd('setAnimationControl')(3) # AnimationControl_Stop
  217. interface.control.cmd('setFrameState')(2) # FrameState_Update
  218. interface.control.loop(1)
  219. def results(self):
  220. '''
  221. Returns a list of KLLTestResult objects
  222. These objects have the results of each test
  223. '''
  224. return self.testresults
  225. class EvalBase:
  226. '''
  227. Base Eval class
  228. '''
  229. def __init__(self, parent):
  230. '''
  231. Constructor
  232. @param parent: Parent object
  233. '''
  234. self.parent = parent
  235. self.key = None
  236. self.info = ""
  237. class TriggerResultEval(EvalBase):
  238. '''
  239. Takes a trigger:result pair and processes it.
  240. Given a pair, it can be specified to do correct *or* incorrect scheduling.
  241. '''
  242. def __init__(self, parent, json_key, json_entry, schedule=None):
  243. '''
  244. Constructor
  245. @param parent: Parent object
  246. @param json_key: String name for trigger:result pair (unique id for layer)
  247. @param json_entry: Dictionary describing trigger:result pair
  248. @param schedule: Alternate Trigger schedule (Defaults to None), used in destructive testing
  249. '''
  250. EvalBase.__init__(self, parent)
  251. self.key = json_key
  252. self.entry = json_entry
  253. self.schedule = schedule
  254. # PositionStep is the "cycle counter"
  255. # For each element of a sequence, this counter is incremented
  256. # When transitioning between trigger to result, the counter is incremented
  257. # It does not have to follow the internal KLL cycle counter as the schedule may be skewed due to timing constraints
  258. self.positionstep = 0
  259. # Set to True when the test has completed
  260. self.done = False
  261. # Prepare trigger
  262. self.trigger = TriggerEval(self, self.entry['trigger'], self.schedule)
  263. # Prepare result
  264. self.result = ResultMonitor(self, self.entry['result'])
  265. def step(self, positionstep=None):
  266. '''
  267. Evaluate a single step of the Trigger:Result pair
  268. @param positionstep: Start from a given positionstep (will set positionstep)
  269. @return: True if successful/valid execution, False for a failure
  270. '''
  271. import interface as i
  272. # Check for positionstep
  273. if positionstep is not None:
  274. self.positionstep = positionstep
  275. # If positionstep is 0, reset trigger step position
  276. if self.positionstep == 0:
  277. self.trigger.reset()
  278. # Trigger Evaluation
  279. if not self.trigger.done():
  280. self.trigger.trigger_permutations()
  281. if not self.trigger.eval():
  282. return False
  283. # Reset result position, if the trigger has finished
  284. if self.trigger.done():
  285. logger.debug("{} Trigger Complete", self.__class__.__name__)
  286. self.result.reset()
  287. # Result Monitoring
  288. elif not self.result.done():
  289. if not self.result.monitor():
  290. return False
  291. # Mark Trigger:Result as done
  292. if self.result.done():
  293. logger.debug("{} Result Complete", self.__class__.__name__)
  294. self.done = True
  295. # Clear callback history, in case this is a sequence
  296. i.control.data.capability_history.unread()
  297. i.control.data.capability_history.prune()
  298. # Increment step position within libkiibohd
  299. i.control.loop(1)
  300. self.positionstep += 1
  301. # Cleanup step, if necessary
  302. self.trigger.cleanup()
  303. if not self.result.monitor_cleanup():
  304. logger.error("{} Cleanup Monitor failure", self.__class__.__name__)
  305. return False
  306. return True
  307. def run(self):
  308. '''
  309. Evaluate/run Trigger:Result pair
  310. @return: True if successful, False if not
  311. '''
  312. while not self.done:
  313. logger.debug("{}:Step {}:{}", self.__class__.__name__, self.positionstep, self.key)
  314. if not self.step():
  315. self.clean()
  316. return False
  317. # Cleanup after test
  318. self.clean()
  319. return True
  320. def clean(self):
  321. '''
  322. Cleanup between tests
  323. Capability history needs to be cleared between tests.
  324. '''
  325. import interface as i
  326. # Read all callbacks
  327. i.control.data.capability_history.unread()
  328. # Prune callback history
  329. i.control.data.capability_history.prune()
  330. class Schedule:
  331. '''
  332. Handles KLL Schedule processing
  333. '''
  334. def __init__(self):
  335. '''
  336. TODO
  337. '''
  338. class ScheduleElem:
  339. '''
  340. Scheduling for an individual element
  341. '''
  342. def __init__(self, json_entry):
  343. '''
  344. TODO
  345. By default, if no schedule is given, it is on press at the start of the cycle
  346. '''
  347. self.entry = json_entry
  348. def initial(self):
  349. '''
  350. TODO
  351. Initial evaluation of schedule.
  352. Resets internal tracking.
  353. @return: True if ready to evaluate/execute
  354. '''
  355. return True
  356. def update(self):
  357. '''
  358. TODO
  359. Updates the internal tracking
  360. @return: True if ready to evaluate/execute
  361. '''
  362. return True
  363. class TriggerEval:
  364. '''
  365. Evaluates a KLL trigger from a trigger:result pair
  366. '''
  367. def __init__(self, parent, json_entry, schedule=None):
  368. '''
  369. Constructor
  370. @param parent: Parent object
  371. @param json_entry: Trigger json entry
  372. @param schedule: Alternate Trigger schedule, if set to None will be determined from json_entry
  373. '''
  374. self.parent = parent
  375. self.entry = json_entry
  376. self.schedule = schedule
  377. # Step determines which part of the sequence we are trying to evaluate
  378. self.step = 0
  379. self.clean = -1
  380. self.cleaned = -1
  381. # Settings
  382. # TODO (HaaTa): Expose these 2 variables somehow, they are useful for testing combos
  383. self.reverse_combo = False
  384. self.delayed_combo = False
  385. self.sub_step = 0 # Used with delayed_combo
  386. # Build sequence of combos
  387. self.trigger = []
  388. for comboindex, combo in enumerate(self.entry):
  389. ncombo = []
  390. for elemindex, elem in enumerate(combo):
  391. # Determine schedule (use external schedule if specified)
  392. elemschedule = ScheduleElem(elem)
  393. if self.schedule is not None:
  394. elemschedule = self.schedule[comboindex][elemindex]
  395. # Append element to combo
  396. ncombo.append(TriggerElem(self, elem, elemschedule))
  397. self.trigger.append(ncombo)
  398. def trigger_permutations(self):
  399. '''
  400. Calculate the number of order permutations the trigger can be initiated with.
  401. For example:
  402. U"RCtrl" + U"RAlt" : U"A";
  403. can be processed 3 ways.
  404. 1) RCtrl + RAlt in the same cycle
  405. 2) RCtrl in the first cycle, RAlt in the second cycle
  406. 3) RAlt in the first cycle, RCtrl in the second cycle
  407. Only triggers need permutation testing.
  408. '''
  409. # TODO
  410. pass
  411. def eval(self):
  412. '''
  413. Attempt to evaluate TriggerEval
  414. Only a single step.
  415. @return: True on valid execution, False if something unexpected ocurred
  416. '''
  417. # Fail test if we have incremented too many steps
  418. if self.step > len(self.trigger):
  419. return False
  420. # Reverse combo
  421. combo = copy.copy(self.trigger[self.step])
  422. if self.reverse_combo:
  423. combo.reverse()
  424. # Delayed combo
  425. if self.delayed_combo:
  426. combo = [combo[self.sub_step]]
  427. self.sub_step += 1
  428. # Attempt to evaluate each element in the current combo
  429. finished = True
  430. for elem in combo:
  431. if not elem.eval():
  432. finished = False
  433. # Only increment if sub_steps are complete
  434. if self.delayed_combo:
  435. finished = finished and self.sub_step >= len(self.trigger[self.step])
  436. # Increment step if finished
  437. if finished:
  438. self.step += 1
  439. self.clean += 1
  440. self.sub_step = 0 # Reset on each combo
  441. # Always return True (even if not finished)
  442. # Only return False on an unexpected error
  443. return True
  444. def cleanup(self):
  445. '''
  446. Cleanup previously evaluated step
  447. Uses the previous step to determine what to cleanup.
  448. Does not cleanup if it's not necessary (i.e. no "double frees")
  449. '''
  450. # Don't bother doing double cleanup
  451. if self.cleaned == self.clean:
  452. return
  453. # Clean for the given step
  454. for elem in self.trigger[self.clean]:
  455. elem.cleanup()
  456. self.cleaned += 1
  457. # Reset the cleaning state
  458. if self.clean >= len(self.trigger):
  459. self.clean = -1
  460. self.cleaned = -1
  461. def reset(self):
  462. '''
  463. Reset step position
  464. '''
  465. logger.debug("{} Reset", self.__class__.__name__)
  466. self.step = 0
  467. def done(self):
  468. '''
  469. Determine if the Trigger Evaluation is complete
  470. @return: True if complete, False otherwise
  471. '''
  472. return self.step >= len(self.trigger)
  473. class ResultMonitor:
  474. '''
  475. Monitors a KLL result from a trigger:result pair
  476. '''
  477. def __init__(self, parent, json_entry):
  478. '''
  479. Constructor
  480. @param parent: Parent object
  481. @param json_entry: Result json entry
  482. '''
  483. self.parent = parent
  484. self.entry = json_entry
  485. # Step determines which part of the sequence we are trying to monitor
  486. self.step = 0
  487. self.clean = -1
  488. self.cleaned = -1
  489. # Build sequence of combos
  490. self.result = []
  491. for comboindex, combo in enumerate(self.entry):
  492. ncombo = []
  493. for elemindex, elem in enumerate(combo):
  494. elemschedule = ScheduleElem(elem)
  495. # Append element to combo
  496. ncombo.append(ResultElem(self, elem, elemschedule))
  497. self.result.append(ncombo)
  498. def monitor(self):
  499. '''
  500. Monitors a single step of the Result.
  501. @return: True if step has completed, False if schedule is out of bounds (failed).
  502. '''
  503. # Fail test if we have incremented too many steps
  504. if self.step > len(self.result):
  505. return False
  506. # Monitor current step in the Result
  507. finished = True
  508. for elem in self.result[self.step]:
  509. if not elem.monitor():
  510. return False
  511. # Increment step if finished
  512. if finished:
  513. self.step += 1
  514. self.clean += 1
  515. # TODO Determine if we are outside the bounds of the schedule
  516. # i.e. when to return False
  517. # Return true even if not finished
  518. return True
  519. def monitor_cleanup(self):
  520. '''
  521. Monitors the cleanup of the previous monitor step
  522. @return: True if cleanup was successful, False otherwise.
  523. '''
  524. # Don't bother doing double cleanup
  525. if self.cleaned == self.clean:
  526. return True
  527. # Clean for the given step
  528. clean_result = True
  529. for elem in self.result[self.clean]:
  530. if not elem.monitor_cleanup():
  531. clean_result = False
  532. self.cleaned += 1
  533. # Reset the cleaning state
  534. if self.clean >= len(self.result):
  535. self.clean = -1
  536. self.cleaned = -1
  537. return clean_result
  538. def reset(self):
  539. '''
  540. Reset step position
  541. '''
  542. logger.debug("{} Reset", self.__class__.__name__)
  543. self.step = 0
  544. def done(self):
  545. '''
  546. Determine if the Result Monitor is complete
  547. @return: True if complete, False otherwise
  548. '''
  549. return self.step >= len(self.result)
  550. class TriggerElem:
  551. '''
  552. Handles individual trigger elements and how to interface with libkiibohd
  553. '''
  554. def __init__(self, parent, elem, schedule):
  555. '''
  556. Intializer
  557. @param parent: Parent object
  558. @param elem: Trigger element
  559. @param schedule: Trigger element conditions
  560. '''
  561. self.parent = parent
  562. self.elem = elem
  563. self.schedule = schedule
  564. def eval(self):
  565. '''
  566. Evaluates TriggerElem
  567. Must satisify schedule in order to complete.
  568. @return: True if evaluated, False if not.
  569. '''
  570. # TODO (HaaTa) Handle scheduling
  571. import interface as i
  572. LayerStateType = i.control.scan.LayerStateType
  573. ScheduleState = i.control.scan.ScheduleState
  574. TriggerType = i.control.scan.TriggerType
  575. logger.debug("TriggerElem eval {} {}", self.elem, self.schedule)
  576. # Determine which kind trigger element
  577. # ScanCode
  578. if self.elem['type'] == 'ScanCode':
  579. # Press given ScanCode
  580. # TODO (HaaTa): Support uids greater than 255
  581. i.control.cmd('addScanCode')(self.elem['uid'], TriggerType.Switch1)
  582. # IndicatorCode
  583. elif self.elem['type'] == 'IndCode':
  584. # Activate Indicator
  585. i.control.cmd('addScanCode')(self.elem['uid'], TriggerType.LED1)
  586. # Layer
  587. elif self.elem['type'] in ['Layer', 'LayerShift', 'LayerLatch', 'LayerLock']:
  588. # Determine which layer type
  589. layer_state = LayerStateType.Shift
  590. if self.elem['type'] == 'LayerLatch':
  591. layer_state = LayerStateType.Latch
  592. elif self.elem['type'] == 'LayerLock':
  593. layer_state = LayerStateType.Lock
  594. # Activate layer
  595. i.control.cmd('applyLayer')(ScheduleState.P, self.elem['uid'], layer_state)
  596. # Generic Trigger
  597. elif self.elem['type'] in ['GenericTrigger']:
  598. # Activate trigger
  599. i.control.cmd('setTriggerCode')(self.elem['uid'], self.elem['idcode'], self.elem['schedule'][0]['state'] )
  600. # Unknown TriggerElem
  601. else:
  602. logger.warning("Unknown TriggerElem {}", self.elem)
  603. return True
  604. def cleanup(self):
  605. '''
  606. Completes the opposing action for a scheduled TriggerElem eval
  607. @return: True, unless there were probablems completing operation
  608. '''
  609. # TODO (HaaTa) Handle scheduling
  610. import interface as i
  611. LayerStateType = i.control.scan.LayerStateType
  612. ScheduleState = i.control.scan.ScheduleState
  613. TriggerType = i.control.scan.TriggerType
  614. logger.debug("TriggerElem cleanup {} {}", self.elem, self.schedule)
  615. # Determine which kind trigger element
  616. # ScanCode
  617. if self.elem['type'] == 'ScanCode':
  618. # Press given ScanCode
  619. # TODO (HaaTa): Support uids greater than 255
  620. i.control.cmd('removeScanCode')(self.elem['uid'], TriggerType.Switch1)
  621. # IndicatorCode
  622. elif self.elem['type'] == 'IndCode':
  623. # Activate Indicator
  624. i.control.cmd('removeScanCode')(self.elem['uid'], TriggerType.LED1)
  625. # Layer Trigger
  626. elif self.elem['type'] in ['Layer', 'LayerShift', 'LayerLatch', 'LayerLock']:
  627. # Determine which layer type
  628. state = ScheduleState.R
  629. layer_state = LayerStateType.Shift
  630. if self.elem['type'] == 'LayerLatch':
  631. state = ScheduleState.P
  632. layer_state = LayerStateType.Latch
  633. elif self.elem['type'] == 'LayerLock':
  634. state = ScheduleState.P
  635. layer_state = LayerStateType.Lock
  636. # Deactivate layer
  637. i.control.cmd('applyLayer')(state, self.elem['uid'], layer_state)
  638. # Generic Trigger
  639. elif self.elem['type'] in ['GenericTrigger']:
  640. # Do nothing as we don't know how to clean up this trigger
  641. # XXX (HaaTa): It can be possible to deduce from the idcode what type of trigger this is
  642. # However, not all triggers have an inverse (some just cleanup on their own such as rotations)
  643. pass
  644. # Unknown TriggerElem
  645. else:
  646. logger.warning("Unknown TriggerElem {}", self.elem)
  647. return True
  648. def __repr__(self):
  649. '''
  650. String representation of TriggerElem
  651. '''
  652. output = "{} {}".format(
  653. self.elem,
  654. self.schedule,
  655. )
  656. return output
  657. class ResultElem:
  658. '''
  659. Handles individual result elements and how to monitor libkiibohd
  660. '''
  661. def __init__(self, parent, elem, schedule):
  662. '''
  663. Intializer
  664. @param parent: Parent object
  665. @param elem: Result element
  666. @param schedule: Result element conditions
  667. '''
  668. self.parent = parent
  669. self.elem = elem
  670. self.schedule = schedule
  671. self.validation = NoVerification(self)
  672. import interface as i
  673. # Lookup Capability (if necessary)
  674. elemtype = self.elem['type']
  675. self.name = i.control.json_input['CodeLookup'][elemtype]
  676. self.expected_args = None
  677. if self.name is None:
  678. # Capability type has the name and arg fields
  679. if elemtype == 'Capability':
  680. self.name = self.elem['name']
  681. self.expected_args = self.elem['args']
  682. new_args = []
  683. # Some args have types
  684. for arg in self.expected_args:
  685. if isinstance(arg, dict):
  686. new_args.append(arg['value'])
  687. # If any processing of args was necessary
  688. if len(new_args) > 0:
  689. self.expected_args = new_args
  690. elif elemtype == 'Animation':
  691. # Expected arg needs to be looked up for Animations
  692. value = i.control.json_input['AnimationSettings'][self.elem['setting']]
  693. self.expected_args = [value]
  694. elif elemtype == 'None':
  695. # None is just usbKeyOut with keycode 0 (which is nothing)
  696. self.name = i.control.json_input['CodeLookup']['USBCode']
  697. self.expected_args = [0] # This is None
  698. else:
  699. # Otherwise uid is used for the arg
  700. self.expected_args = [self.elem['uid']]
  701. logger.debug("ResultElem monitor {} {} {} {}", self.elem, self.schedule, self.name, self.expected_args)
  702. assert self.name is not None, "Invalid Result type {}".format(self.elem)
  703. # Determine if there is capability verification available
  704. if elemtype == 'Animation':
  705. self.validation = AnimationVerification(self)
  706. elif elemtype == 'USBCode':
  707. self.validation = USBCodeVerification(self)
  708. elif elemtype == 'ConsCode':
  709. self.validation = ConsumerCodeVerification(self)
  710. elif elemtype == 'SysCode':
  711. self.validation = SystemCodeVerification(self)
  712. elif elemtype == 'Capability':
  713. # Determine if general capability has verification available
  714. # Layer Verification
  715. if self.name in ['layerShift', 'layerLock', 'layerLatch']:
  716. self.validation = LayerVerification(self)
  717. def monitor_state(self, state):
  718. '''
  719. Monitors result state
  720. @param state: Value of state to monitor
  721. @returns: True if valid, False if not
  722. '''
  723. # TODO (HaaTa) Handle scheduling
  724. import interface as i
  725. TriggerType = i.control.scan.TriggerType
  726. # Lookup capability history, success if any capabilities match
  727. match = None
  728. for cap in i.control.data.capability_history.all():
  729. data = cap.callbackdata
  730. # Validate state and capability name
  731. # Rotations use states beyond 0x0F
  732. if (
  733. data.read_capability()[0] == self.name and (
  734. (data.state & 0x0F) == state or
  735. (data.state == state and data.stateType == TriggerType.Rotation1)
  736. )
  737. ):
  738. # Validate args
  739. match_args = True
  740. # XXX Each data argument may consist of multiple bytes
  741. # This can be looked up using kll.json
  742. # Generally 1, 2 and 4 (8-bit, 16-bit and 32-bit)
  743. capability = i.control.json_input['Capabilities'][self.name]
  744. byte_pos = 0
  745. for index in range(capability['args_count']):
  746. value_width = capability['args'][index]['width']
  747. value = 0
  748. if value_width == 1:
  749. value = data.args[byte_pos]
  750. byte_pos += 1
  751. elif value_width == 2:
  752. value = (data.args[byte_pos + 1] << 8) | data.args[byte_pos]
  753. byte_pos += 2
  754. elif value_width == 4:
  755. value = (data.args[byte_pos + 3] << 24) | (data.args[byte_pos + 2] << 16) | (data.args[byte_pos + 1] << 8) | data.args[byte_pos]
  756. byte_pos += 4
  757. # Check read value vs. expected
  758. if value != self.expected_args[index]:
  759. match_args = False
  760. break
  761. # Determine if we've found a match
  762. if match_args:
  763. match = data
  764. logger.debug("Matched History {}", match)
  765. break
  766. # Increment test count with pass/fail
  767. result = True
  768. if match is None:
  769. result = False
  770. # Print out capability history
  771. for cap in i.control.data.capability_history.all():
  772. logger.warning(cap)
  773. return check(result, "test:{} expected:{}({}) state({}) - found:{}".format(
  774. self.parent.parent.parent.cur_test,
  775. self.name,
  776. self.expected_args,
  777. state,
  778. match,
  779. ))
  780. def monitor(self):
  781. '''
  782. Monitors for ResultElem
  783. Must satisfy schedule in order to complete.
  784. @return: True if found, False if not.
  785. '''
  786. # TODO (HaaTa) Handle scheduling
  787. if len(self.parent.parent.trigger.entry[-1][-1]['schedule']) > 0:
  788. state = self.parent.parent.trigger.entry[-1][-1]['schedule'][0]['state']
  789. else:
  790. state = 1
  791. # Check capability state
  792. result = self.monitor_state(state)
  793. # Validate capability result
  794. result = result and self.validation.verify(state)
  795. return result
  796. def monitor_cleanup(self):
  797. '''
  798. Cleanup monitor for ResultElem
  799. @return: True if expected cleanup occured, False if not.
  800. '''
  801. import interface as i
  802. TriggerType = i.control.scan.TriggerType
  803. # TODO (HaaTa) Handle scheduling
  804. # If not a generic trigger, don't check for cleanup
  805. trigger = self.parent.parent.trigger.entry[-1][-1]
  806. if trigger['type'] == 'GenericTrigger':
  807. # If this is a rotation trigger, don't check for cleanup (single-shot)
  808. if trigger['idcode'] == TriggerType.Rotation1:
  809. return True
  810. # If this is a layer trigger, don't check for clenau
  811. # Check capability state
  812. result = self.monitor_state(3)
  813. # Validate capability result cleanup
  814. result = result and self.validation.verify(3)
  815. return result
  816. return True
  817. def __repr__(self):
  818. '''
  819. String representation of ResultElem
  820. '''
  821. output = "{} {} {}".format(self.elem, self.schedule, self.validation)
  822. return output
  823. class CapabilityVerification:
  824. '''
  825. Base class for capability functional verification
  826. '''
  827. def __init__(self, parent):
  828. '''
  829. @param parent: ResultElem object used for introspection
  830. '''
  831. self.parent = parent
  832. def verify(self, state):
  833. '''
  834. Verify result capability
  835. @param: State of result
  836. @return: True if verification is successful, False otherwise
  837. '''
  838. logger.warning("verify not implemented for {}", self.__class__.__name__)
  839. return True
  840. class NoVerification(CapabilityVerification):
  841. '''
  842. No verification implemented
  843. '''
  844. def verify(self, state):
  845. '''
  846. Ignore verification
  847. @param: State of result
  848. @return: Always True
  849. '''
  850. return True
  851. class LayerVerification(CapabilityVerification):
  852. '''
  853. Layer capability verification
  854. Validates that a specified capability behaved as expected
  855. '''
  856. def verify(self, state):
  857. '''
  858. Verify Layer Function
  859. Depending on the layer function used, validate that the expected behaviour occured.
  860. @param: State of result
  861. @return: True if verification is successful, False otherwise
  862. '''
  863. import interface as i
  864. # Get current and previous layer states
  865. states = i.control.data.layer_history.last(3)
  866. cur = states[0]
  867. last = states[1]
  868. # Get expected layer manipulation
  869. type_lookup = {
  870. 'layerShift': 1,
  871. 'layerLatch': 2,
  872. 'layerLock': 3,
  873. }
  874. expected_type = self.parent.name
  875. expected_layer = self.parent.expected_args[0]
  876. # Ignore release for lock
  877. if expected_type == 'layerLock' and state == 3:
  878. return True
  879. # Ignore press for latch
  880. if expected_type == 'layerLatch' and state == 1:
  881. return True
  882. # Ignore layer if invalid
  883. if expected_layer >= len(cur.state):
  884. logger.info("Ignoring {}:{} - Invalid layer", expected_type, expected_layer)
  885. return True
  886. # Determine expected layer result, XOR last with type function layer
  887. # XXX This may get confused in some situations
  888. # i.e. Two shift capabilities activated at the same time for the same layer
  889. try:
  890. computed = last.state[expected_layer] ^ (1 << type_lookup[expected_type] - 1)
  891. # Determine if layer should be in the stack or not
  892. in_stack = False
  893. if computed != 0:
  894. in_stack = True
  895. # Match expected with actual
  896. result = True
  897. if cur.state[expected_layer] != computed:
  898. result = False
  899. # Determine if in stack or not
  900. if in_stack:
  901. if expected_layer not in cur.stack:
  902. result = False
  903. else:
  904. if expected_layer in cur.stack:
  905. result = False
  906. # Debug if failed
  907. if not result:
  908. logger.warning("Prev Layer {}", last)
  909. logger.warning("Cur Layer {}", cur)
  910. return check(result, "test:{} expectedlayer:{} expectedtype:{} expectedstate:{} instack:{} state:{} - foundstate:{}".format(
  911. self.parent.parent.parent.parent.cur_test,
  912. expected_layer,
  913. expected_type,
  914. computed,
  915. in_stack,
  916. state,
  917. cur.state[expected_layer],
  918. ))
  919. except:
  920. return check(False, "test:{} expectedlayer:{} expectedtype:{} state:{}".format(
  921. self.parent.parent.parent.parent.cur_test,
  922. expected_layer,
  923. expected_type,
  924. state,
  925. ))
  926. class USBCodeVerification(CapabilityVerification):
  927. '''
  928. USBCode capability verification
  929. Validates that a specified capability behaved as expected
  930. '''
  931. def verify(self, state):
  932. '''
  933. Verify USB Code
  934. @param: State of result
  935. @return: True if verification is successful, False otherwise
  936. '''
  937. import interface as i
  938. # Get expected data (only one argument)
  939. expected = self.parent.expected_args[0]
  940. # Get USB Keyboard data
  941. data = i.control.data.usb_keyboard()
  942. # Could not acquire keyboard data
  943. if data is None:
  944. return check(False, "Could not retrieve data from usb_keyboards() Expected: {}".format(
  945. expected,
  946. ))
  947. logger.debug("Data: {} Expected: {}", data, expected)
  948. # Check for expected data
  949. result = False
  950. match = False
  951. if expected in data.keyboardcodes:
  952. match = True
  953. # If the keycode is 0, return true regardless
  954. # There may not be any keycodes in this case (but there may be some due to other macros)
  955. if expected == 0:
  956. result = True
  957. # Off or Release
  958. if state == 0 or state == 3:
  959. if not match:
  960. result = True
  961. # Press or Hold
  962. elif state == 1 or state == 2:
  963. if match:
  964. result = True
  965. return check(result, "test:{} expectedusb:{} state({}) - found:{}".format(
  966. self.parent.parent.parent.parent.cur_test,
  967. expected,
  968. state,
  969. data,
  970. ))
  971. class ConsumerCodeVerification(CapabilityVerification):
  972. '''
  973. ConsCode capability verification
  974. Validates that a specified capability behaved as expected
  975. '''
  976. def verify(self, state):
  977. '''
  978. Verify Consumer Code
  979. @param: State of result
  980. @return: True if verification is successful, False otherwise
  981. '''
  982. import interface as i
  983. # Get expected data (only one argument)
  984. expected = self.parent.expected_args[0]
  985. # Get USB Keyboard data
  986. data = i.control.data.usb_keyboard()
  987. logger.debug("Data: {} Expected: {}", data, expected)
  988. # Check for expected data
  989. result = False
  990. match = False
  991. if expected == data.consumercode:
  992. match = True
  993. # Off or Release
  994. if state == 0 or state == 3:
  995. if not match:
  996. result = True
  997. # Press or Hold
  998. elif state == 1 or state == 2:
  999. if match:
  1000. result = True
  1001. return check(result, "test:{} expectedconsumer:{} state({}) - found:{}".format(
  1002. self.parent.parent.parent.parent.cur_test,
  1003. expected,
  1004. state,
  1005. data,
  1006. ))
  1007. class SystemCodeVerification(CapabilityVerification):
  1008. '''
  1009. SysCode capability verification
  1010. Validates that a specified capability behaved as expected
  1011. '''
  1012. def verify(self, state):
  1013. '''
  1014. Verify System Code
  1015. @param: State of result
  1016. @return: True if verification is successful, False otherwise
  1017. '''
  1018. import interface as i
  1019. # Get expected data (only one argument)
  1020. expected = self.parent.expected_args[0]
  1021. # Get USB Keyboard data
  1022. data = i.control.data.usb_keyboard()
  1023. logger.debug("Data: {} Expected: {}", data, expected)
  1024. # Check for expected data
  1025. result = False
  1026. match = False
  1027. if expected == data.systemcode:
  1028. match = True
  1029. # Off or Release
  1030. if state == 0 or state == 3:
  1031. if not match:
  1032. result = True
  1033. # Press or Hold
  1034. elif state == 1 or state == 2:
  1035. if match:
  1036. result = True
  1037. return check(result, "test:{} expectedsystem:{} state({}) - found:{}".format(
  1038. self.parent.parent.parent.parent.cur_test,
  1039. expected,
  1040. state,
  1041. data
  1042. ))
  1043. class AnimationVerification(CapabilityVerification):
  1044. '''
  1045. Animation capability verification
  1046. Validates that a specified capability behaved as expected
  1047. '''
  1048. def get_modifier_setting(self, animation, name):
  1049. '''
  1050. Retrieve modifier setting using the setting name
  1051. @param animation: Json representation of the animation setting
  1052. @param name: Name of the animation setting modifier
  1053. @return: NamedTuple of (arg, subarg), if not found will be (None, None)
  1054. '''
  1055. ntuple = namedtuple('AnimationSettingModifierArg', ['arg', 'subarg'])
  1056. output = ntuple(None, None)
  1057. for modifier in animation['modifiers']:
  1058. if modifier['name'] == name:
  1059. output = ntuple(modifier['value']['arg'], modifier['value']['subarg'])
  1060. break
  1061. return output
  1062. def verify(self, state):
  1063. '''
  1064. Verify animation was added to the stack
  1065. @param: State of result
  1066. '''
  1067. # No need to validate, other than during press
  1068. if state != 1:
  1069. return True
  1070. import interface as i
  1071. # Get expected index
  1072. expected_index = self.parent.expected_args[0]
  1073. # Determine settings from index
  1074. animation = i.control.json_input['AnimationSettingsIndex'][expected_index]
  1075. animation_id = i.control.json_input['AnimationIds'][animation['name']]
  1076. framedelay = self.get_modifier_setting(animation, 'framedelay').arg
  1077. # If not set, framedelay is 0
  1078. if framedelay is None:
  1079. framedelay = 0
  1080. frameoptions = animation['frameoptions']
  1081. ffunc = self.get_modifier_setting(animation, 'ffunc').arg
  1082. if ffunc is None:
  1083. ffunc = 'off'
  1084. pfunc = self.get_modifier_setting(animation, 'pfunc').arg
  1085. if pfunc is None:
  1086. pfunc = 'off'
  1087. # Get animation stack
  1088. stack_obj = i.control.cmd('animationStackInfo')()
  1089. size = stack_obj.size
  1090. stack = stack_obj.stack.contents
  1091. # Search animation stack for animation
  1092. # Match
  1093. # - Animation index
  1094. # - framedelay
  1095. # - frameoptions
  1096. # - ffunc
  1097. # - pfunc
  1098. result = False
  1099. for index in range(size):
  1100. elem = stack[index]
  1101. if elem.index != animation_id:
  1102. continue
  1103. if elem.framedelay != framedelay:
  1104. continue
  1105. if elem.ffunc_lookup() != ffunc:
  1106. continue
  1107. if elem.pfunc_lookup() != pfunc:
  1108. continue
  1109. if set(elem.frameoption_lookup()) != set(frameoptions):
  1110. continue
  1111. # Matched
  1112. result = True
  1113. break
  1114. # Print out stack if no match was found
  1115. if not result:
  1116. logger.warning("Animation not found, printing stack")
  1117. for index in range(size):
  1118. logger.warning(stack[index])
  1119. return check(result, "test:{} expected:{}:{}".format(
  1120. self.parent.parent.parent.parent.cur_test,
  1121. animation_id,
  1122. animation
  1123. ))
  1124. ### Functions ###
  1125. test_pass = 0
  1126. test_fail = 0
  1127. test_fail_info = []
  1128. def fail_tests( number ):
  1129. '''
  1130. Update fail count
  1131. '''
  1132. # TODO Add info
  1133. global test_fail
  1134. test_fail += number
  1135. def pass_tests( number ):
  1136. '''
  1137. Update pass count
  1138. '''
  1139. global test_pass
  1140. test_pass += number
  1141. def check(condition, info=None):
  1142. '''
  1143. Checks whether the function passed
  1144. Adds to global pass/fail counters
  1145. @param condition: Boolean condition
  1146. @param info: Additional debugging information
  1147. @returns: Result of boolean condition
  1148. '''
  1149. # Prepare info
  1150. info_str = ""
  1151. if info is not None:
  1152. info_str = " - {}".format(info)
  1153. # Only print stack (show full calling function) info if in debug mode
  1154. if logger.isEnabledFor(logging.DEBUG):
  1155. parentstack_info = inspect.stack()[-1]
  1156. logger.debug("{} {}:{}{}",
  1157. parentstack_info.code_context[0][:-1],
  1158. parentstack_info.filename,
  1159. parentstack_info.lineno,
  1160. info_str,
  1161. )
  1162. if condition:
  1163. global test_pass
  1164. test_pass += 1
  1165. else:
  1166. global test_fail
  1167. test_fail += 1
  1168. # Collect failure info
  1169. frame = inspect.currentframe().f_back
  1170. line_file = inspect.getframeinfo( frame ).filename
  1171. line_no = inspect.getlineno( frame )
  1172. line_info = linecache.getline( line_file, line_no )
  1173. logger.error("Test failed! {}:{} {}{}",
  1174. header(line_file),
  1175. blued(line_no),
  1176. line_info[:-1],
  1177. info_str,
  1178. )
  1179. # Store info for final report
  1180. test_fail_info.append( (frame, line_file, line_no, line_info, info_str) )
  1181. return condition
  1182. def result():
  1183. '''
  1184. Sums up test results and displays
  1185. Exits Python with with success (0) if there were no test failures
  1186. Exits with failure (1) otherwise
  1187. '''
  1188. logger.info(header("----Results----"))
  1189. logger.info("{0}/{1}".format(
  1190. test_pass,
  1191. test_pass + test_fail,
  1192. ) )
  1193. if test_fail == 0:
  1194. sys.exit( 0 )
  1195. else:
  1196. # Print report
  1197. logger.error(header("----Failed Tests----"))
  1198. for (frame, line_file, line_no, line_info, info_str) in test_fail_info:
  1199. logger.error( "{0}:{1} {2}{3}", header(line_file), blued(line_no), line_info[:-1], info_str)
  1200. sys.exit( 1 )
  1201. def header( val ):
  1202. '''
  1203. Emboldens a string for stdout
  1204. '''
  1205. val = "\033[1m{0}\033[0m".format( val )
  1206. return val
  1207. def blued( val ):
  1208. '''
  1209. Emboldens a string blue for stdout
  1210. '''
  1211. val = "\033[1;34m{0}\033[0m".format( val )
  1212. return val