File size: 11,475 Bytes
03b34b2 c96e917 03b34b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | """Unit tests untuk preprocessing pipeline + atomic functions.
Test atomic (normalizer, tokenizer, jargon, spelling) — no Sastrawi needed.
Test pipeline/stopword/stemmer — requires Sastrawi (skip kalau gak install).
Run: pytest backend/tests/test_preprocessing.py -v
"""
from __future__ import annotations
import pytest
from app.preprocessing.jargon import KOS_JARGON_DICT, jargon_count, MIN_REQUIRED
from app.preprocessing.normalizer import (
extract_prices_inline,
lowercase,
normalize_whitespace,
strip_html,
)
from app.preprocessing.spelling import correct_spelling
from app.preprocessing.tokenizer import simple_tokenize
# =============================================================================
# Strip HTML
# =============================================================================
class TestStripHTML:
def test_basic_tags(self):
assert strip_html("<p>kos putra</p>") == "kos putra"
def test_nested_tags(self):
assert strip_html("<div><p>kos <b>putra</b></p></div>") == "kos putra"
def test_strip_with_attributes(self):
assert strip_html('<a href="x">kos</a> putra') == "kos putra"
def test_empty(self):
assert strip_html("") == ""
def test_no_tags(self):
assert strip_html("kos putra dekat unila") == "kos putra dekat unila"
# =============================================================================
# Whitespace normalize
# =============================================================================
class TestNormalizeWhitespace:
def test_multiple_spaces(self):
assert normalize_whitespace("kos putra dekat") == "kos putra dekat"
def test_newlines(self):
assert normalize_whitespace("kos\nputra\n\ndekat") == "kos putra dekat"
def test_tabs(self):
assert normalize_whitespace("kos\tputra") == "kos putra"
def test_strip_leading_trailing(self):
assert normalize_whitespace(" kos putra ") == "kos putra"
def test_empty(self):
assert normalize_whitespace("") == ""
# =============================================================================
# Lowercase
# =============================================================================
class TestLowercase:
def test_basic(self):
assert lowercase("Kos PUTRA AC") == "kos putra ac"
def test_unicode(self):
assert lowercase("KOS DEKAT UNILA") == "kos dekat unila"
# =============================================================================
# Price extraction (inline, all matches)
# =============================================================================
class TestExtractPricesInline:
def test_single_rupiah(self):
assert extract_prices_inline("Sewa Rp 850.000 per bulan") == [850000]
def test_no_space_rupiah(self):
assert extract_prices_inline("Rp1.250.000") == [1250000]
def test_multiple_prices(self):
prices = extract_prices_inline("Mulai Rp 500.000 sampai Rp 1.500.000")
assert prices == [500000, 1500000]
def test_juta(self):
assert extract_prices_inline("Harga 1.5jt all in") == [1500000]
def test_juta_kata_penuh(self):
# Bug lama: cuma `jt\b`, "1,5 juta" lolos dan fragmen "Rp 1,5"
# terbaca 15 rupiah
assert extract_prices_inline("maksimal 1,5 juta per bulan") == [1500000]
assert extract_prices_inline("Rp 1,5 juta") == [1500000]
assert extract_prices_inline("sekitar 2 juta") == [2000000]
def test_rupiah_fragment_noise_dibuang(self):
# Nilai rupiah < 10rb itu fragmen, bukan harga kos
assert extract_prices_inline("Rp 1,5") == []
def test_ribu_k(self):
assert extract_prices_inline("Murah 350k aja") == [350000]
def test_ribu_rb(self):
assert extract_prices_inline("500rb/bulan") == [500000]
def test_dedup(self):
# Sama-sama 500.000, hanya muncul sekali
assert extract_prices_inline("Rp 500.000 atau 500rb") == [500000]
def test_anti_pattern_lowercase_first(self):
# Walaupun lowercase, regex tetap match (insensitive)
assert extract_prices_inline("rp 500.000") == [500000]
def test_empty(self):
assert extract_prices_inline("") == []
# =============================================================================
# Tokenizer
# =============================================================================
class TestTokenizer:
def test_simple(self):
assert simple_tokenize("kos putra dekat unila") == [
"kos", "putra", "dekat", "unila",
]
def test_punctuation_excluded(self):
assert simple_tokenize("kos, putra! dekat? unila.") == [
"kos", "putra", "dekat", "unila",
]
def test_numbers_kept(self):
tokens = simple_tokenize("kos 500k murah")
assert "kos" in tokens
assert "500k" in tokens
assert "murah" in tokens
def test_empty(self):
assert simple_tokenize("") == []
# =============================================================================
# Spelling correction
# =============================================================================
class TestSpelling:
def test_fix_fasiltas(self):
assert "fasilitas" in correct_spelling("banyak fasiltas").lower()
def test_fix_exclusive(self):
assert "eksklusif" in correct_spelling("kos ekslusive").lower()
def test_no_change_correct(self):
# "kos putra" gak ada di typo dict, return as-is
assert correct_spelling("kos putra") == "kos putra"
def test_empty(self):
assert correct_spelling("") == ""
def test_word_boundary(self):
# "rapih" → "rapi" tapi "rapihkan" jangan ke-replace
result = correct_spelling("rapih dan rapihkan")
assert "rapi" in result
# "rapihkan" gak di-replace karena \b boundary
assert "rapihkan" in result or "rapih" not in result.replace("rapi", "")
# =============================================================================
# Jargon dict
# =============================================================================
class TestJargonDict:
def test_meets_rubric_minimum(self):
count = jargon_count()
assert count >= MIN_REQUIRED, (
f"Hanya {count} entries, minimum {MIN_REQUIRED} untuk rubric "
f"Preprocessing 15%. Tim Anggota B: tambah {MIN_REQUIRED - count}"
)
def test_common_abbreviations_present(self):
assert "ac" in KOS_JARGON_DICT
assert "kmd" in KOS_JARGON_DICT
assert "wc dlm" in KOS_JARGON_DICT
def test_location_variants(self):
assert KOS_JARGON_DICT["gdg meneng"] == "gedong meneng"
assert KOS_JARGON_DICT["sumbro"] == "sumantri brojonegoro"
assert KOS_JARGON_DICT["unyila"] == "universitas lampung"
def test_type_slang(self):
assert KOS_JARGON_DICT["cowo"] == "putra"
assert KOS_JARGON_DICT["cewe"] == "putri"
# =============================================================================
# Pipeline + Sastrawi (heavy — skip kalau Sastrawi belum install)
# =============================================================================
try:
from app.preprocessing.pipeline import PipelineConfig, PreprocessingPipeline
from app.preprocessing.stemmer import SastrawiStemmer
from app.preprocessing.stopwords import StopwordRemover
SASTRAWI_AVAILABLE = True
except ImportError:
SASTRAWI_AVAILABLE = False
@pytest.mark.skipif(not SASTRAWI_AVAILABLE, reason="Sastrawi belum di-install")
class TestPipeline:
def test_full_pipeline_basic(self):
pipeline = PreprocessingPipeline()
result = pipeline.process("Kos Putra AC WiFi Rp 850.000/bulan dekat unyila")
# Price ke-extract
assert 850000 in result.extracted_prices
# Stages applied
assert "stem" in result.stages_applied
assert "apply_jargon_dict" in result.stages_applied
# Tokens non-empty
assert len(result.tokens) > 0
def test_disable_stem(self):
config = PipelineConfig(stem=False)
pipeline = PreprocessingPipeline(config)
result = pipeline.process("kos murah dekat kampus")
assert "stem" not in result.stages_applied
def test_disable_stopword(self):
config = PipelineConfig(remove_stopwords=False, stem=False)
pipeline = PreprocessingPipeline(config)
result = pipeline.process("kos yang murah di lampung")
# "yang" dan "di" gak ke-remove
tokens_lower = [t.lower() for t in result.tokens]
assert "yang" in tokens_lower or "di" in tokens_lower
def test_jargon_substitution_gdg_meneng(self):
config = PipelineConfig(stem=False, remove_stopwords=False)
pipeline = PreprocessingPipeline(config)
result = pipeline.process("kos di gdg meneng")
# "gdg meneng" → "gedong meneng"
assert "gedong" in result.processed
assert "meneng" in result.processed
def test_jargon_longest_first(self):
# "km dlm" harus ke-match dulu sebelum "dlm"
config = PipelineConfig(stem=False, remove_stopwords=False)
pipeline = PreprocessingPipeline(config)
result = pipeline.process("ada km dlm dan dapur")
# "km dlm" → "kamar mandi dalam", bukan "km dalam"
assert "kamar mandi dalam" in result.processed
def test_price_preserved_before_lowercase(self):
# Anti-pattern check
pipeline = PreprocessingPipeline()
result = pipeline.process("Sewa Rp 1.250.000")
assert 1250000 in result.extracted_prices
@pytest.mark.skipif(not SASTRAWI_AVAILABLE, reason="Sastrawi belum di-install")
class TestStopwordRemover:
def test_remove_sastrawi_default(self):
remover = StopwordRemover()
tokens = ["kos", "yang", "murah", "di", "lampung"]
result = remover.remove(tokens)
# "yang" dan "di" Sastrawi stopwords
assert "yang" not in result
assert "di" not in result
# "kos" custom stopword
assert "kos" not in result
# Informative tokens kept
assert "murah" in result
assert "lampung" in result
def test_custom_only(self):
remover = StopwordRemover(custom=["spesifik"], use_sastrawi_default=False)
assert remover.is_stopword("spesifik")
assert not remover.is_stopword("yang") # Sastrawi default off
def test_count(self):
remover = StopwordRemover()
counts = remover.count()
assert counts["sastrawi"] > 0
assert counts["custom"] > 0
@pytest.mark.skipif(not SASTRAWI_AVAILABLE, reason="Sastrawi belum di-install")
class TestStemmer:
def test_basic_stem(self):
stemmer = SastrawiStemmer()
assert stemmer.stem("berlari") == "lari"
assert stemmer.stem("pergi") == "pergi" # already stem
def test_cache_hit(self):
stemmer = SastrawiStemmer()
# First call: cache miss
r1 = stemmer.stem("memasak")
# Second call: cache hit
r2 = stemmer.stem("memasak")
assert r1 == r2
info = stemmer.cache_info()
assert info.hits >= 1
def test_stem_tokens_batch(self):
stemmer = SastrawiStemmer()
result = stemmer.stem_tokens(["berlari", "memasak", "menulis"])
assert len(result) == 3
assert "lari" in result
def test_empty(self):
stemmer = SastrawiStemmer()
assert stemmer.stem("") == ""
|