Ved et lige antal spillere får kæden et trin mere: i trin 1 tegner ejeren sit eget ord, og rotationen starter et trin senere. Antal trin hedder nu steps og bruges i step-eventet, snapshot og afsløringen; self.n er antal spillere. Snapshot får step.own til trin 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
417 lines
20 KiB
Python
417 lines
20 KiB
Python
import random
|
|
|
|
from django.test import SimpleTestCase
|
|
|
|
from fupogfakta.engine import GameError
|
|
from tegnoggaet import engine
|
|
from tegnoggaet.engine import Game, clean_drawing
|
|
from party.themes import load_themes
|
|
|
|
THEMES = [{"id": "dyr", "name": "Dyr", "words": ["kat", "hund", "ko", "gris"]},
|
|
{"id": "mad", "name": "Mad", "words": ["pizza", "is"]}]
|
|
STROKE = {"color": engine.PALETTE[0], "width": 0.01, "points": [[0.1, 0.1], [0.5, 0.5]]}
|
|
|
|
|
|
def setUpModule():
|
|
assert Game.show_rules is True # standarden i drift
|
|
Game.show_rules = False # reglerne testes for sig (RulesTests)
|
|
|
|
|
|
def tearDownModule():
|
|
Game.show_rules = True
|
|
|
|
|
|
class TegnTestCase(SimpleTestCase):
|
|
def setUp(self):
|
|
self.now = 1000.0
|
|
self.g = Game("TEST", THEMES, join_url="http://x/?code=TEST", rng=random.Random(4))
|
|
|
|
def join(self, n):
|
|
return [self.g.join(f"Spiller{i}") for i in range(n)]
|
|
|
|
def start(self, n, theme="dyr"):
|
|
ps = self.join(n)
|
|
self.g.start(None, self.now)
|
|
self.g.choose_theme(None, theme, self.now)
|
|
self.assertEqual(self.g.phase, "word")
|
|
return ps
|
|
|
|
def play_step(self, ps, value=None):
|
|
for p in ps:
|
|
if p.id not in self.g.players:
|
|
continue
|
|
if self.g.phase == "word":
|
|
self.g.submit_word(p.id, value or f"ord {p.name}", self.now)
|
|
elif self.g.phase == "draw":
|
|
self.g.submit_drawing(p.id, [STROKE], self.now)
|
|
else:
|
|
self.g.submit_guess(p.id, value or f"gæt {p.name}", self.now)
|
|
|
|
def play_all(self, ps):
|
|
while self.g.phase in ("word", "draw", "guess"):
|
|
self.play_step(ps)
|
|
self.assertEqual(self.g.phase, "reveal")
|
|
|
|
|
|
class RotationTests(TegnTestCase):
|
|
def test_every_chain_ends_on_a_guess(self):
|
|
"""Ulige N: N trin. Lige N: N+1 trin, og i trin 1 tegner ejeren sit eget ord."""
|
|
for n in (3, 4, 5, 6, 7, 8, 9, 10):
|
|
with self.subTest(n=n):
|
|
self.g = Game("TEST", THEMES, rng=random.Random(n))
|
|
ps = self.start(n)
|
|
steps = n if n % 2 else n + 1
|
|
self.assertEqual(self.g.steps, steps)
|
|
owner = {k: pid for k, pid in enumerate(self.g.order)}
|
|
own = {self.g.order.index(p.id): p.id for p in ps}
|
|
for t in range(steps):
|
|
chains = [self.g.chain_for(p.id, t) for p in ps]
|
|
self.assertEqual(sorted(chains), list(range(n)), "hver kæde har præcis én spiller pr. trin")
|
|
at_home = t == 0 or (t == 1 and n % 2 == 0)
|
|
for p, c in zip(ps, chains):
|
|
self.assertEqual(own[c] == p.id, at_home, "egen kæde kun i trin 0 (og 1 ved lige N)")
|
|
t = 0
|
|
while self.g.phase in ("word", "draw", "guess"):
|
|
self.assertEqual(self.g.step, t)
|
|
snap = self.g.snapshot(self.now, host=True)
|
|
self.assertEqual(snap["step"], {"index": t, "number": t + 1, "of": steps, "kind": self.g.phase,
|
|
"own": t == 1 and n % 2 == 0})
|
|
for p in ps:
|
|
task = self.g.task(p.id)
|
|
if t == 1 and n % 2 == 0: # ejeren får sit eget ord
|
|
self.assertEqual((task["kind"], task["from"], task["text"]), ("draw", p.id, f"ord {p.name}".lower()))
|
|
elif t > 0:
|
|
self.assertNotEqual(task["from"], p.id)
|
|
self.play_step(ps)
|
|
t += 1
|
|
self.assertEqual(t, steps)
|
|
self.assertEqual(self.g.phase, "reveal")
|
|
for k, chain in enumerate(self.g.chains):
|
|
self.assertEqual(len(chain), steps)
|
|
self.assertEqual(chain[-1]["kind"], "guess", "kæden slutter på et gæt")
|
|
self.assertEqual(chain[0]["by"], owner[k])
|
|
self.assertEqual([e["kind"] for e in chain],
|
|
["word"] + ["drawing" if t % 2 else "guess" for t in range(1, steps)])
|
|
self.assertNotIn(owner[k], [e["by"] for e in chain if e["kind"] == "guess"],
|
|
"ingen gætter på sin egen kæde")
|
|
self.assertEqual(len({e["by"] for e in chain}), n, "alle bidrager til hver kæde")
|
|
if n % 2 == 0:
|
|
self.assertEqual(chain[1]["by"], owner[k], "ejeren tegner selv i trin 1")
|
|
self.assertEqual(len({e["by"] for e in chain[1 if n % 2 == 0 else 0:]}), n,
|
|
"ud over ejerens to første trin ser ingen kæden to gange")
|
|
for t in range(steps):
|
|
by = sorted(chain[t]["by"] for chain in self.g.chains)
|
|
self.assertEqual(by, sorted(p.id for p in ps), "hver spiller ét bidrag pr. trin")
|
|
|
|
def test_kicks_before_the_chains_start_go_back_to_lobby(self):
|
|
a, b, c = self.join(3)
|
|
self.g.start(None, self.now)
|
|
self.g.kick(c.id, self.now)
|
|
self.g.tick(self.now + engine.THEME_SECONDS)
|
|
self.assertEqual(self.g.phase, "lobby")
|
|
|
|
def test_too_few_players(self):
|
|
self.join(2)
|
|
with self.assertRaises(GameError) as e:
|
|
self.g.start(None, self.now)
|
|
self.assertEqual(e.exception.code, "too_few_players")
|
|
|
|
|
|
class FlowTests(TegnTestCase):
|
|
def test_theme_vip_or_host_chooses_and_timeout_picks_one(self):
|
|
a, b, c = self.join(3)
|
|
self.g.start(a.id, self.now)
|
|
self.assertEqual(self.g.phase, "theme")
|
|
with self.assertRaises(GameError):
|
|
self.g.choose_theme(b.id, "dyr", self.now)
|
|
with self.assertRaises(GameError):
|
|
self.g.choose_theme(a.id, "findes-ikke", self.now)
|
|
self.g.tick(self.now + engine.THEME_SECONDS)
|
|
self.assertEqual(self.g.phase, "word")
|
|
self.assertIn(self.g.theme["id"], {"dyr", "mad"})
|
|
|
|
def test_task_shows_only_the_previous_entry(self):
|
|
a, b, c = self.start(3)
|
|
self.assertEqual(self.g.task(a.id)["kind"], "word")
|
|
self.g.submit_word(a.id, "Kat", self.now)
|
|
self.g.submit_word(b.id, "hund", self.now)
|
|
self.g.submit_word(c.id, "ko", self.now)
|
|
self.assertEqual(self.g.phase, "draw")
|
|
# a tegner c's ord (kæde 2), og ser kun ordet
|
|
t = self.g.task(a.id)
|
|
self.assertEqual((t["kind"], t["text"], t["drawing"], t["from"]), ("draw", "ko", None, c.id))
|
|
snap = self.g.snapshot(self.now, viewer=a)
|
|
self.assertNotIn("reveal", snap)
|
|
self.assertNotIn("kat", str(snap)) # eget ord er ikke synligt mere, og andres aldrig
|
|
self.assertNotIn("hund", str(snap))
|
|
self.g.submit_drawing(a.id, [STROKE], self.now)
|
|
self.g.submit_drawing(b.id, [dict(STROKE, color=engine.PALETTE[1])], self.now)
|
|
self.g.submit_drawing(c.id, [dict(STROKE, color=engine.PALETTE[2])], self.now)
|
|
self.assertEqual(self.g.phase, "guess")
|
|
# a gætter i kæde (0 - 2) mod 3 = 1, b's kæde, som c tegnede
|
|
t = self.g.task(a.id)
|
|
self.assertEqual(t["kind"], "guess")
|
|
self.assertIsNone(t["text"])
|
|
self.assertEqual(t["drawing"][0]["color"], engine.PALETTE[2])
|
|
self.assertNotIn("hund", str(self.g.snapshot(self.now, viewer=a)))
|
|
|
|
def test_texts_are_cleaned_and_validated(self):
|
|
a, b, c = self.start(3)
|
|
with self.assertRaises(GameError) as e:
|
|
self.g.submit_word(a.id, "🙂🙂", self.now)
|
|
self.assertEqual(e.exception.code, "word_invalid")
|
|
with self.assertRaises(GameError):
|
|
self.g.submit_word(a.id, "x" * (engine.MAX_TEXT + 1), self.now)
|
|
self.g.submit_word(a.id, " Rød Kat! ", self.now)
|
|
self.assertEqual(self.g.chains[0][0]["text"], "rød kat")
|
|
with self.assertRaises(GameError) as e:
|
|
self.g.submit_word(a.id, "igen", self.now)
|
|
self.assertEqual(e.exception.code, "already_done")
|
|
|
|
def test_suggestion_avoids_taken_words(self):
|
|
a, b, c = self.start(3, theme="mad")
|
|
self.g.submit_word(a.id, "pizza", self.now)
|
|
self.assertEqual({self.g.suggest(b.id) for _ in range(5)}, {"is"})
|
|
|
|
def test_audience_and_late_joiners(self):
|
|
ps = self.start(3)
|
|
late = self.g.join("Sen")
|
|
self.assertEqual(late.role, "audience")
|
|
self.assertIsNone(self.g.snapshot(self.now, viewer=late)["me"]["task"])
|
|
with self.assertRaises(GameError):
|
|
self.g.submit_word(late.id, "hej", self.now)
|
|
|
|
|
|
class TimeoutTests(TegnTestCase):
|
|
def test_timeouts_use_drafts_or_defaults_and_never_hang(self):
|
|
a, b, c = self.start(3)
|
|
self.g.save_draft(b.id, "Halvt ord")
|
|
self.g.submit_word(a.id, "kat", self.now)
|
|
self.now += engine.WORD_SECONDS
|
|
self.assertTrue(self.g.tick(self.now))
|
|
self.assertEqual(self.g.phase, "draw")
|
|
words = {e["by"]: e for chain in self.g.chains for e in chain}
|
|
self.assertEqual(words[b.id]["text"], "halvt ord")
|
|
self.assertTrue(words[b.id]["auto"])
|
|
self.assertIn(words[c.id]["text"], THEMES[0]["words"]) # ingen kladde: et eksempel-ord fra temaet
|
|
self.g.save_draft(a.id, [STROKE])
|
|
with self.assertRaises(GameError):
|
|
self.g.save_draft(b.id, [{"color": "red", "width": 0.01, "points": [[0, 0]]}])
|
|
self.now += engine.DRAW_SECONDS
|
|
self.g.tick(self.now)
|
|
self.assertEqual(self.g.phase, "guess")
|
|
drawings = {e["by"]: e["strokes"] for chain in self.g.chains for e in chain if e["kind"] == "drawing"}
|
|
self.assertEqual(drawings[a.id], [STROKE])
|
|
self.assertEqual(drawings[b.id], []) # tom tegning sendes alligevel
|
|
self.now += engine.GUESS_SECONDS
|
|
self.g.tick(self.now)
|
|
self.assertEqual(self.g.phase, "reveal")
|
|
self.assertEqual({e["text"] for chain in self.g.chains for e in chain if e["kind"] == "guess"}, {"???"})
|
|
|
|
def test_disconnected_players_do_not_hold_the_others_back(self):
|
|
a, b, c = self.start(3)
|
|
self.g.set_connected(c.id, False, self.now)
|
|
self.g.submit_word(a.id, "kat", self.now)
|
|
self.g.submit_word(b.id, "hund", self.now)
|
|
self.assertEqual(self.g.phase, "draw")
|
|
self.g.submit_drawing(a.id, [STROKE], self.now)
|
|
self.assertEqual(self.g.phase, "draw")
|
|
self.g.submit_drawing(b.id, [STROKE], self.now)
|
|
self.assertEqual(self.g.phase, "guess")
|
|
|
|
def test_kicked_player_leaves_the_chains_intact(self):
|
|
a, b, c, d = self.start(4)
|
|
self.g.kick(d.id, self.now)
|
|
self.play_all([a, b, c])
|
|
self.assertTrue(all(len(chain) == 5 for chain in self.g.chains))
|
|
self.assertTrue(all(e["auto"] for chain in self.g.chains for e in chain if e["by"] == d.id))
|
|
|
|
def test_held_phase_waits_for_the_speaker(self):
|
|
a, b, c = self.start(3)
|
|
self.g.hold_until = self.now + 3
|
|
self.play_step([a, b, c])
|
|
self.assertEqual(self.g.phase, "word")
|
|
self.assertFalse(self.g.tick(self.now + 1))
|
|
self.assertTrue(self.g.tick(self.now + 3))
|
|
self.assertEqual(self.g.phase, "draw")
|
|
|
|
|
|
class DrawingValidationTests(SimpleTestCase):
|
|
def test_valid_drawing_is_rounded(self):
|
|
out = clean_drawing([{"color": engine.PALETTE[1], "width": 0.0123456, "points": [[0.123456, 1], [0, 0.5]]}])
|
|
self.assertEqual(out, [{"color": engine.PALETTE[1], "width": 0.0123, "points": [[0.1235, 1], [0, 0.5]]}])
|
|
self.assertEqual(clean_drawing([]), [])
|
|
|
|
def test_invalid_drawings_are_rejected(self):
|
|
ok = dict(STROKE)
|
|
bad = [
|
|
"streger", {"strokes": []}, [ok, "x"],
|
|
[dict(ok, color="red")], [dict(ok, color="javascript:alert(1)")],
|
|
[dict(ok, width=0)], [dict(ok, width=1)], [dict(ok, width=True)], [dict(ok, width=float("nan"))],
|
|
[dict(ok, points=[])], [dict(ok, points=[[0.1]])], [dict(ok, points=[[0.1, 1.2]])],
|
|
[dict(ok, points=[[-0.1, 0.5]])], [dict(ok, points=[[True, 0.5]])], [dict(ok, points=[["0.1", 0.5]])],
|
|
[ok] * (engine.MAX_STROKES + 1),
|
|
[dict(ok, points=[[0.5, 0.5]] * (engine.MAX_POINTS + 1))],
|
|
]
|
|
for strokes in bad:
|
|
with self.subTest(strokes=str(strokes)[:60]):
|
|
with self.assertRaises(GameError) as e:
|
|
clean_drawing(strokes)
|
|
self.assertEqual(e.exception.code, "drawing_invalid")
|
|
|
|
def test_size_limit(self):
|
|
# under punktgrænsen, men over 64 KB
|
|
many = [{"color": engine.PALETTE[0], "width": 0.01, "points": [[0.1234, 0.5678]] * 50} for _ in range(390)]
|
|
with self.assertRaises(GameError):
|
|
clean_drawing(many)
|
|
|
|
|
|
class RevealTests(TegnTestCase):
|
|
def play_game(self, n=3, survive=False):
|
|
ps = self.start(n)
|
|
while self.g.phase in ("word", "draw", "guess"):
|
|
if survive and self.g.phase != "draw":
|
|
self.play_step(ps, value="kat") # alle skriver/gætter "kat": alle kæder overlever
|
|
else:
|
|
self.play_step(ps)
|
|
return ps
|
|
|
|
def test_plan_covers_every_chain_and_entry(self):
|
|
self.play_game(4)
|
|
plan = self.g.plan
|
|
self.assertEqual(len(plan), 4 * (5 + 2)) # 4 spillere: 5 trin, så kæden slutter på et gæt
|
|
self.assertEqual([b["entry"] for b in plan[:7]], [-1, 0, 1, 2, 3, 4, 5])
|
|
snap = self.g.snapshot(self.now, host=True)
|
|
self.assertNotIn("strokes", str(snap["reveal"])) # tegningerne kommer i beskeden "drawings"
|
|
self.assertEqual(len(self.g.drawings()), 4 * 2)
|
|
self.assertEqual(snap["reveal"]["chains"][0]["entries"][0]["kind"], "word")
|
|
|
|
def test_phones_only_get_what_the_tv_has_shown(self):
|
|
a, b, c = self.play_game(3, survive=True)
|
|
def chains(t):
|
|
return self.g.snapshot(t, viewer=a)["reveal"]["chains"]
|
|
self.assertEqual([len(ch["entries"]) for ch in chains(self.now)], [0, 0, 0]) # kædens intro
|
|
t = self.now + (engine.CHAIN_INTRO_MS + 10) / 1000
|
|
self.assertEqual([e["id"] for e in chains(t)[0]["entries"]], ["c0e0"])
|
|
self.assertIsNone(chains(t)[0]["survived"]) # afgøres først på storskærmen
|
|
t = self.now + (engine.CHAIN_INTRO_MS + engine.WORD_MS + engine.DRAWING_MS + engine.GUESS_MS + 10) / 1000
|
|
ch = chains(t)
|
|
self.assertEqual((len(ch[0]["entries"]), ch[0]["survived"], len(ch[1]["entries"])), (3, True, 0))
|
|
self.assertNotIn("kat", str(ch[1:]))
|
|
self.g.plan[5]["audio"] = {"url": "/tts/x.mp3", "ms": 900, "text": "Den startede som kat og endte som kat."}
|
|
phone_plan = self.g.snapshot(self.now, viewer=a)["reveal"]["plan"]
|
|
self.assertTrue(all("audio" not in b for b in phone_plan)) # speakerens tekst ville røbe sidste gæt
|
|
self.assertEqual(self.g.snapshot(self.now, host=True)["reveal"]["plan"][5]["audio"]["ms"], 900)
|
|
host = self.g.snapshot(self.now, host=True)["reveal"]["chains"]
|
|
self.assertEqual([len(x["entries"]) for x in host], [3, 3, 3]) # storskærmen får alt
|
|
starts = self.g.beat_starts()
|
|
self.assertEqual(len(starts), len(self.g.plan))
|
|
self.assertEqual(starts[1], self.now + engine.CHAIN_INTRO_MS / 1000)
|
|
|
|
def test_likes_only_on_revealed_drawings_and_guesses_by_others(self):
|
|
a, b, c = self.play_game(3)
|
|
word, drawing, guess = self.g.chains[0] # a's kæde: ord af a, tegning af b, gæt af c
|
|
self.assertEqual((word["by"], drawing["by"], guess["by"]), (a.id, b.id, c.id))
|
|
with self.assertRaises(GameError):
|
|
self.g.like(b.id, drawing["id"], self.now) # endnu ikke vist
|
|
t = self.now + (engine.CHAIN_INTRO_MS + engine.WORD_MS + 10) / 1000
|
|
with self.assertRaises(GameError):
|
|
self.g.like(b.id, word["id"], t) # ord får ikke hjerter
|
|
with self.assertRaises(GameError):
|
|
self.g.like(b.id, drawing["id"], t) # ikke sin egen
|
|
self.g.like(a.id, drawing["id"], t)
|
|
self.assertEqual(self.g.players[b.id].score, engine.POINTS_LIKE)
|
|
self.g.like(a.id, drawing["id"], t) # slår fra igen
|
|
self.assertEqual(self.g.players[b.id].score, 0)
|
|
fan = self.g.join("Fan")
|
|
self.g.like(fan.id, drawing["id"], t)
|
|
self.g.like(a.id, drawing["id"], t)
|
|
self.assertEqual(self.g.players[b.id].likes, 2)
|
|
self.assertEqual(self.g.snapshot(t, viewer=fan)["me"]["likes"], [drawing["id"]])
|
|
|
|
def test_survived_bonus_and_awards(self):
|
|
a, b, c = self.play_game(3, survive=True)
|
|
self.assertEqual(self.g.survived, [True, True, True])
|
|
g = self.g.chains[0][2]
|
|
t = self.now + 60
|
|
self.g.like(a.id, g["id"], t)
|
|
self.g.tick(self.now + 600)
|
|
self.assertEqual(self.g.phase, "final")
|
|
scores = {p.id: p.score for p in self.g.players.values()}
|
|
self.assertEqual(scores[a.id], engine.POINTS_SURVIVED)
|
|
self.assertEqual(scores[g["by"]], engine.POINTS_SURVIVED + engine.POINTS_LIKE)
|
|
kinds = [x["kind"] for x in self.g.final_result["awards"]]
|
|
self.assertEqual(kinds, ["guess", "survived"])
|
|
self.assertEqual(self.g.final_result["ranking"][0]["player_id"], g["by"])
|
|
|
|
def test_biggest_transformation(self):
|
|
a, b, c = self.play_game(3)
|
|
self.g.tick(self.now + 600)
|
|
awards = {x["kind"]: x for x in self.g.final_result["awards"]}
|
|
self.assertNotIn("survived", awards)
|
|
self.assertIn("transformed", awards)
|
|
self.assertIn("blev til", awards["transformed"]["detail"])
|
|
|
|
def test_skip_jumps_to_next_chain_then_final(self):
|
|
self.play_game(3)
|
|
self.assertEqual(self.g.plan[self.g.beat_at(self.now)]["chain"], 0)
|
|
self.g.skip(self.now + 1)
|
|
self.assertEqual(self.g.beat_at(self.now + 1), 5) # kæde 1's intro (hver kæde: intro, 3 bidrag, slut)
|
|
plan_end = self.g.reveal_started_at + sum(b["ms"] for b in self.g.plan) / 1000
|
|
self.assertEqual(self.g.deadline, plan_end) # ellers står skærmen stille efter sidste kæde
|
|
self.assertTrue(self.g.tick(plan_end))
|
|
self.assertEqual(self.g.phase, "final")
|
|
|
|
def test_skip_twice_then_final_when_the_last_chain_ends(self):
|
|
self.play_game(3)
|
|
self.g.skip(self.now + 1)
|
|
self.g.skip(self.now + 1)
|
|
end = self.g.reveal_started_at + sum(b["ms"] for b in self.g.plan) / 1000
|
|
self.assertFalse(self.g.tick(end - 0.1))
|
|
self.assertTrue(self.g.tick(end))
|
|
self.assertEqual(self.g.phase, "final")
|
|
self.g = Game("TEST", THEMES, rng=random.Random(4))
|
|
self.play_game(3)
|
|
for _ in range(3):
|
|
self.g.skip(self.now + 1)
|
|
self.assertEqual(self.g.phase, "final")
|
|
|
|
def test_reveal_waits_for_audio_then_starts_by_itself(self):
|
|
self.play_game(3)
|
|
self.g.hold_reveal(self.now, max_wait=5)
|
|
self.assertIsNone(self.g.snapshot(self.now, host=True)["reveal"]["started_at"])
|
|
self.g.tick(self.now + 5)
|
|
self.assertEqual(self.g.reveal_started_at, self.now + 5)
|
|
|
|
def test_play_again_keeps_players_and_promotes_audience(self):
|
|
a, b, c = self.play_game(3)
|
|
late = self.g.join("Sen")
|
|
self.g.tick(self.now + 600)
|
|
self.g.play_again(a.id, self.now + 601)
|
|
self.assertEqual(self.g.phase, "theme") # reglerne kun første gang
|
|
self.assertIn(late.id, self.g.players)
|
|
self.assertTrue(all(p.score == 0 for p in self.g.players.values()))
|
|
|
|
|
|
class RulesTests(TegnTestCase):
|
|
def test_rules_once_then_theme(self):
|
|
self.g.show_rules = True
|
|
self.join(3)
|
|
self.g.start(None, self.now)
|
|
self.assertEqual(self.g.phase, "rules")
|
|
self.g.tick(self.now + 60)
|
|
self.assertEqual(self.g.phase, "theme")
|
|
|
|
|
|
class ThemeFileTests(SimpleTestCase):
|
|
def test_themes_file(self):
|
|
themes = load_themes()
|
|
self.assertTrue(10 <= len(themes) <= 15)
|
|
self.assertEqual(len({t["id"] for t in themes}), len(themes))
|
|
for t in themes:
|
|
with self.subTest(theme=t["name"]):
|
|
self.assertTrue(15 <= len(t["words"]) <= 20)
|
|
self.assertEqual(len(set(t["words"])), len(t["words"]))
|
|
for w in t["words"]:
|
|
self.assertEqual(engine.clean_text(w, "x", "x"), w) # kan bruges som ord uden rettelser
|