test_loop_abort.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. #!/usr/bin/env python3
  2. """Host-side tests for the decode-side n-gram / repeated-line abort."""
  3. import os
  4. import sys
  5. from pathlib import Path
  6. from types import SimpleNamespace
  7. ROOT = Path(__file__).resolve().parents[1]
  8. sys.path.insert(0, str(ROOT / 'adapter'))
  9. from loop_abort import ( # noqa: E402
  10. DEFAULT_IDENTICAL,
  11. DEFAULT_NGRAM,
  12. DEFAULT_REPEATS,
  13. MATCHED_STOP,
  14. detect_repetition,
  15. install,
  16. load_config,
  17. repeated_line_suffix,
  18. )
  19. def _with_env(mapping, fn):
  20. old = {k: os.environ.get(k) for k in mapping}
  21. try:
  22. for k, v in mapping.items():
  23. if v is None:
  24. os.environ.pop(k, None)
  25. else:
  26. os.environ[k] = str(v)
  27. return fn()
  28. finally:
  29. for k, v in old.items():
  30. if v is None:
  31. os.environ.pop(k, None)
  32. else:
  33. os.environ[k] = v
  34. def _cfg(**kwargs):
  35. base = dict(
  36. abort=1, ngram=DEFAULT_NGRAM, ngram_max=256, repeats=DEFAULT_REPEATS,
  37. identical=DEFAULT_IDENTICAL, line_repeats=8, line_min=16, enabled=True)
  38. base.update(kwargs)
  39. return SimpleNamespace(**base)
  40. def test_default_config():
  41. cfg = _with_env({
  42. 'DSV41_LOOP_ABORT': None,
  43. 'DSV41_LOOP_NGRAM': None,
  44. 'DSV41_LOOP_REPEATS': None,
  45. 'DSV41_LOOP_IDENTICAL': None,
  46. 'DSV41_LOOP_LINE_REPEATS': None,
  47. }, load_config)
  48. assert cfg.enabled
  49. assert cfg.ngram == DEFAULT_NGRAM
  50. assert cfg.repeats == DEFAULT_REPEATS
  51. assert cfg.identical == DEFAULT_IDENTICAL
  52. def test_abort_zero_disables():
  53. cfg = _with_env({'DSV41_LOOP_ABORT': '0'}, load_config)
  54. assert not cfg.enabled
  55. ids = [7, 8] * 200
  56. assert detect_repetition(ids, cfg=cfg) is None
  57. def test_ngram_cycle_is_detected():
  58. unit = list(range(32, 64))
  59. ids = list(range(10)) + unit * 4
  60. hit = detect_repetition(ids, cfg=_cfg())
  61. assert hit is not None
  62. matched, kept = hit
  63. assert matched == MATCHED_STOP
  64. assert kept == 10 + 32 # prefix + one copy of the unit
  65. def test_unique_sequence_is_not_a_loop():
  66. ids = list(range(400))
  67. assert detect_repetition(ids, cfg=_cfg()) is None
  68. def test_short_punctuation_repeat_does_not_fire():
  69. # Four copies of a 6-token "</div>"-sized unit stays under min ngram=32.
  70. ids = [1, 2, 3, 4, 5, 6] * 4
  71. assert detect_repetition(ids, cfg=_cfg()) is None
  72. def test_identical_token_run():
  73. ids = [9] * DEFAULT_IDENTICAL
  74. hit = detect_repetition(ids, cfg=_cfg())
  75. assert hit == (MATCHED_STOP, 1)
  76. # A long collapse keeps one token, not (n - threshold + 1) of the run.
  77. hit = detect_repetition([3] * 10 + [9] * 500, cfg=_cfg())
  78. assert hit == (MATCHED_STOP, 11)
  79. def test_three_copies_do_not_fire():
  80. ids = list(range(32, 64)) * 3
  81. assert detect_repetition(ids, cfg=_cfg()) is None
  82. def test_identical_zero_disables_run_check():
  83. ids = [9] * 200
  84. assert detect_repetition(
  85. ids, cfg=_cfg(identical=0, ngram=0, repeats=4, line_repeats=0)) is None
  86. def test_repeated_lines():
  87. line = 'ctx.fillRect(car.x, car.y, 24, 12);'
  88. text = ('header\n' + (line + '\n') * 8)
  89. assert repeated_line_suffix(text, 8, 16)
  90. ids = list(range(80))
  91. hit = detect_repetition(ids, text=text, cfg=_cfg(ngram=32, repeats=4, identical=0))
  92. assert hit == (MATCHED_STOP, 80)
  93. def test_short_or_distinct_lines_do_not_fire():
  94. assert not repeated_line_suffix('a\na\na\na\na\na\na\na\n', 8, 16)
  95. lines = '\n'.join(f'item {i} is unique enough' for i in range(12))
  96. assert not repeated_line_suffix(lines, 8, 16)
  97. def test_install_finishes_looping_req():
  98. class FakeFinish:
  99. def __init__(self, matched):
  100. self.matched = matched
  101. def to_json(self):
  102. return {'type': 'stop', 'matched': self.matched}
  103. class FakeReq:
  104. def __init__(self):
  105. self.output_ids = list(range(40, 72)) * 4
  106. self.finished_reason = None
  107. self.finished_len = None
  108. self.rid = 't'
  109. self.tokenizer = None
  110. def finished(self):
  111. return self.finished_reason is not None
  112. def update_finish_state(self, new_accepted_len=1):
  113. return None
  114. class FakeMod:
  115. Req = FakeReq
  116. FINISH_MATCHED_STR = FakeFinish
  117. def run():
  118. install(FakeMod)
  119. req = FakeReq()
  120. req.update_finish_state(1)
  121. return req
  122. req = _with_env({'DSV41_LOOP_ABORT': '1'}, run)
  123. assert req.finished()
  124. assert req.finished_reason.matched == MATCHED_STOP
  125. assert req.finished_len == 32
  126. def test_install_respects_already_finished():
  127. class FakeFinish:
  128. def __init__(self, matched):
  129. self.matched = matched
  130. class FakeReq:
  131. def __init__(self):
  132. self.output_ids = [1] * 80
  133. self.finished_reason = FakeFinish('length')
  134. self.finished_len = 80
  135. self.tokenizer = None
  136. def finished(self):
  137. return self.finished_reason is not None
  138. def update_finish_state(self, new_accepted_len=1):
  139. return None
  140. class FakeMod:
  141. Req = FakeReq
  142. FINISH_MATCHED_STR = FakeFinish
  143. def run():
  144. install(FakeMod)
  145. req = FakeReq()
  146. req.update_finish_state(1)
  147. return req
  148. req = _with_env({'DSV41_LOOP_ABORT': '1'}, run)
  149. assert req.finished_reason.matched == 'length'
  150. if __name__ == '__main__':
  151. tests = [v for k, v in list(globals().items()) if k.startswith('test_')]
  152. failed = 0
  153. for fn in tests:
  154. try:
  155. fn()
  156. print(f'[OK] {fn.__name__}')
  157. except Exception as exc:
  158. failed += 1
  159. print(f'[FAIL] {fn.__name__}: {exc}')
  160. print(f'{len(tests) - failed}/{len(tests)} passed')
  161. sys.exit(1 if failed else 0)