• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kimata / server-list / 21122078926

19 Jan 2026 01:15AM UTC coverage: 73.466% (-0.8%) from 74.273%
21122078926

push

github

kimata
fix: TestCollectAllData に collect_cpu_benchmark_data のモックを追加

collect_all_data() に collect_cpu_benchmark_data() の呼び出しが
追加されたことにより、テストで cpu_benchmark テーブルへの
アクセスエラーが発生していた問題を修正。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

1437 of 1956 relevant lines covered (73.47%)

0.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.59
/src/server_list/spec/cpu_benchmark.py
1
#!/usr/bin/env python3
2
"""
3
CPU Benchmark scraper from cpubenchmark.net
4
Fetches multi-thread and single-thread performance scores and stores them in SQLite database.
5
"""
6

7
import logging
1✔
8
import re
1✔
9
import threading
1✔
10
import time
1✔
11
from typing import Any
1✔
12

13
import bs4
1✔
14
import requests
1✔
15

16
from server_list.spec.db import get_connection
1✔
17
from server_list.spec.db_config import get_cpu_spec_db_path
1✔
18
from server_list.spec.models import CPUBenchmark
1✔
19

20

21
# =============================================================================
22
# In-memory cache with TTL for benchmark data
23
# =============================================================================
24

25
class BenchmarkCache:
1✔
26
    """Thread-safe in-memory cache with TTL for benchmark data."""
27

28
    def __init__(self, ttl_seconds: int = 3600):
1✔
29
        self._cache: dict[str, Any] = {}
1✔
30
        self._timestamps: dict[str, float] = {}
1✔
31
        self._ttl = ttl_seconds
1✔
32
        self._lock = threading.Lock()
1✔
33

34
    def get(self, key: str) -> Any | None:
1✔
35
        """Get cached value if not expired."""
36
        with self._lock:
1✔
37
            if key not in self._cache:
1✔
38
                return None
1✔
39
            if time.time() - self._timestamps[key] > self._ttl:
×
40
                del self._cache[key]
×
41
                del self._timestamps[key]
×
42
                return None
×
43
            return self._cache[key]
×
44

45
    def set(self, key: str, value: Any) -> None:
1✔
46
        """Set cache value with current timestamp."""
47
        with self._lock:
1✔
48
            self._cache[key] = value
1✔
49
            self._timestamps[key] = time.time()
1✔
50

51
    def invalidate(self, key: str | None = None) -> None:
1✔
52
        """Invalidate specific key or all cache."""
53
        with self._lock:
1✔
54
            if key is None:
1✔
55
                self._cache.clear()
×
56
                self._timestamps.clear()
×
57
            elif key in self._cache:
1✔
58
                del self._cache[key]
1✔
59
                del self._timestamps[key]
1✔
60

61

62
# Global cache instance (1 hour TTL)
63
_benchmark_cache = BenchmarkCache(ttl_seconds=3600)
1✔
64

65

66
# =============================================================================
67
# Background fetch queue for on-demand benchmark retrieval
68
# =============================================================================
69

70
class BackgroundFetchQueue:
1✔
71
    """Thread-safe queue for background CPU benchmark fetches.
72

73
    Prevents duplicate fetches and provides status tracking.
74
    """
75

76
    def __init__(self):
1✔
77
        self._pending: set[str] = set()
1✔
78
        self._lock = threading.Lock()
1✔
79

80
    def is_pending(self, cpu_name: str) -> bool:
1✔
81
        """Check if a fetch is already pending for this CPU."""
82
        with self._lock:
×
83
            return cpu_name in self._pending
×
84

85
    def add(self, cpu_name: str) -> bool:
1✔
86
        """Add CPU to pending set. Returns False if already pending."""
87
        with self._lock:
×
88
            if cpu_name in self._pending:
×
89
                return False
×
90
            self._pending.add(cpu_name)
×
91
            return True
×
92

93
    def remove(self, cpu_name: str) -> None:
1✔
94
        """Remove CPU from pending set."""
95
        with self._lock:
×
96
            self._pending.discard(cpu_name)
×
97

98

99
_fetch_queue = BackgroundFetchQueue()
1✔
100

101

102
def queue_background_fetch(cpu_name: str) -> bool:
1✔
103
    """Queue a background fetch for a CPU benchmark.
104

105
    If the CPU is already being fetched, returns False.
106
    Otherwise, starts a background thread to fetch the data
107
    and returns True.
108

109
    When fetch completes, notifies frontend via SSE.
110
    """
111
    if not _fetch_queue.add(cpu_name):
×
112
        logging.debug("Fetch already pending for: %s", cpu_name)
×
113
        return False
×
114

115
    def _fetch_task():
×
116
        try:
×
117
            result = fetch_and_save_benchmark(cpu_name)
×
118
            if result:
×
119
                # Import here to avoid circular import
120
                import my_lib.webapp.event
×
121
                my_lib.webapp.event.notify_event(my_lib.webapp.event.EVENT_TYPE.DATA)
×
122
                logging.info("Background fetch completed for: %s", cpu_name)
×
123
        except Exception:
×
124
            logging.exception("Background fetch failed for: %s", cpu_name)
×
125
        finally:
126
            _fetch_queue.remove(cpu_name)
×
127

128
    thread = threading.Thread(target=_fetch_task, daemon=True)
×
129
    thread.start()
×
130
    logging.info("Queued background fetch for: %s", cpu_name)
×
131
    return True
×
132

133

134
def queue_background_fetch_batch(cpu_names: list[str]) -> int:
1✔
135
    """Queue background fetches for multiple CPUs.
136

137
    Returns the number of CPUs that were queued (not already pending).
138
    """
139
    queued = 0
×
140
    for cpu_name in cpu_names:
×
141
        if queue_background_fetch(cpu_name):
×
142
            queued += 1
×
143
    return queued
×
144

145

146
def is_fetch_pending(cpu_name: str) -> bool:
1✔
147
    """Check if a background fetch is pending for this CPU."""
148
    return _fetch_queue.is_pending(cpu_name)
×
149

150

151
MULTITHREAD_URL = "https://www.cpubenchmark.net/multithread/"
1✔
152
SINGLETHREAD_URL = "https://www.cpubenchmark.net/singleThread.html"
1✔
153
CPU_LIST_URL = "https://www.cpubenchmark.net/cpu_list.php"
1✔
154

155
HEADERS = {
1✔
156
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
157
}
158

159

160
CPU_BENCHMARK_SCHEMA = """
1✔
161
CREATE TABLE IF NOT EXISTS cpu_benchmark (
162
    id INTEGER PRIMARY KEY AUTOINCREMENT,
163
    cpu_name TEXT UNIQUE NOT NULL,
164
    multi_thread_score INTEGER,
165
    single_thread_score INTEGER,
166
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
167
)
168
"""
169

170

171
def init_db():
1✔
172
    """Initialize the SQLite database."""
173
    with get_connection(get_cpu_spec_db_path()) as conn:
1✔
174
        conn.executescript(CPU_BENCHMARK_SCHEMA)
1✔
175
        conn.commit()
1✔
176

177

178
def extract_model_number(cpu_name: str) -> str | None:
1✔
179
    """Extract the model number from CPU name for precise matching."""
180
    patterns = [
1✔
181
        r"(E5-\d{4}\s*v\d)",      # Xeon E5-2699 v4
182
        r"(i[3579]-\d{4,5}\w*)",   # Core i5-1135G7, i7-12700K
183
        r"(Ryzen\s+\d+\s+\d{4}\w*)",  # Ryzen 9 5900X
184
        r"(EPYC\s+\d{4}\w*)",      # EPYC 7742
185
        r"(\d{4,5}\w*)",           # Generic model number
186
    ]
187

188
    for pattern in patterns:
1✔
189
        match = re.search(pattern, cpu_name, re.IGNORECASE)
1✔
190
        if match:
1✔
191
            return match.group(1).lower().replace(" ", "")
1✔
192

193
    return None
1✔
194

195

196
def normalize_cpu_name(cpu_name: str) -> str:
1✔
197
    """Normalize CPU name for matching."""
198
    name = " ".join(cpu_name.split())
1✔
199
    # Remove clock speed info
200
    name = re.sub(r"@.*$", "", name).strip()
1✔
201
    # Remove trademark symbols
202
    name = name.replace("(R)", "").replace("(TM)", "").replace("®", "").replace("™", "")
1✔
203
    # Normalize whitespace again after removing symbols
204
    name = " ".join(name.split())
1✔
205
    return name
1✔
206

207

208
def _match_by_model_number(
1✔
209
    search_name: str, candidate_name: str, search_lower: str, candidate_lower: str
210
) -> float | None:
211
    """モデル番号による精密マッチング."""
212
    search_model = extract_model_number(search_name)
1✔
213
    candidate_model = extract_model_number(candidate_name)
1✔
214

215
    if not search_model or not candidate_model:
1✔
216
        return None
1✔
217

218
    if search_model == candidate_model:
1✔
219
        return 1.0
1✔
220

221
    if search_model not in candidate_model and candidate_model not in search_model:
1✔
222
        return None
1✔
223

224
    # 部分一致の場合、バージョンチェック
225
    search_version = re.search(r"v(\d)", search_lower)
1✔
226
    candidate_version = re.search(r"v(\d)", candidate_lower)
1✔
227
    if search_version and candidate_version:
1✔
228
        if search_version.group(1) != candidate_version.group(1):
1✔
229
            return 0.3
1✔
230
    return 0.9
1✔
231

232

233
def _match_xeon_e5(search_lower: str, candidate_lower: str) -> float | None:
1✔
234
    """Xeon E5 シリーズの特別マッチング."""
235
    search_id = re.search(r"e5-(\d{4})", search_lower)
1✔
236
    candidate_id = re.search(r"e5-(\d{4})", candidate_lower)
1✔
237

238
    if not search_id or not candidate_id:
1✔
239
        return None
1✔
240

241
    if search_id.group(1) != candidate_id.group(1):
1✔
242
        return 0.2
1✔
243

244
    # 同一モデル - バージョンチェック
245
    search_v = re.search(r"v(\d)", search_lower)
1✔
246
    candidate_v = re.search(r"v(\d)", candidate_lower)
1✔
247

248
    if search_v and candidate_v and search_v.group(1) == candidate_v.group(1):
1✔
249
        return 0.95
×
250
    if not search_v and not candidate_v:
1✔
251
        return 0.95
×
252

253
    return 0.2
1✔
254

255

256
def _match_core_i(search_lower: str, candidate_lower: str) -> float | None:
1✔
257
    """Intel Core i シリーズの特別マッチング."""
258
    search_core = re.search(r"i([3579])-(\d{4,5})", search_lower)
1✔
259
    candidate_core = re.search(r"i([3579])-(\d{4,5})", candidate_lower)
1✔
260

261
    if not search_core or not candidate_core:
1✔
262
        return None
1✔
263

264
    if (
1✔
265
        search_core.group(1) == candidate_core.group(1)
266
        and search_core.group(2) == candidate_core.group(2)
267
    ):
268
        return 0.95
×
269

270
    return 0.2
1✔
271

272

273
def _match_by_word_overlap(search_lower: str, candidate_lower: str) -> float:
1✔
274
    """単語の重複によるファジーマッチング."""
275
    search_words = set(re.findall(r"\w+", search_lower))
1✔
276
    candidate_words = set(re.findall(r"\w+", candidate_lower))
1✔
277

278
    if not search_words:
1✔
279
        return 0.0
1✔
280

281
    common_words = search_words & candidate_words
1✔
282
    return len(common_words) / len(search_words) * 0.5
1✔
283

284

285
def calculate_match_score(search_name: str, candidate_name: str) -> float:
1✔
286
    """Calculate how well the candidate matches the search name."""
287
    search_lower = normalize_cpu_name(search_name).lower()
1✔
288
    candidate_lower = normalize_cpu_name(candidate_name).lower()
1✔
289

290
    # 1. モデル番号による精密マッチング
291
    if (score := _match_by_model_number(search_name, candidate_name, search_lower, candidate_lower)) is not None:
1✔
292
        return score
1✔
293

294
    # 2. 完全一致
295
    if search_lower == candidate_lower:
1✔
296
        return 1.0
1✔
297

298
    # 3. Xeon E5 シリーズ特別処理
299
    if (score := _match_xeon_e5(search_lower, candidate_lower)) is not None:
1✔
300
        return score
1✔
301

302
    # 4. Core i シリーズ特別処理
303
    if (score := _match_core_i(search_lower, candidate_lower)) is not None:
1✔
304
        return score
1✔
305

306
    # 5. 単語重複によるファジーマッチング
307
    return _match_by_word_overlap(search_lower, candidate_lower)
1✔
308

309

310
def _extract_benchmark_score_from_chart_entry(entry_text: str) -> int | None:
1✔
311
    """チャートエントリからベンチマークスコアを抽出.
312

313
    Args:
314
        entry_text: エントリのテキスト (例: "CPU Name(XX%)12,345$XXX")
315

316
    Returns:
317
        ベンチマークスコア (int) または None
318
    """
319
    score_match = re.search(r"\)\s*([\d,]+)", entry_text)
1✔
320
    if not score_match:
1✔
321
        return None
1✔
322

323
    try:
1✔
324
        return int(score_match.group(1).replace(",", ""))
1✔
325
    except ValueError:
×
326
        return None
×
327

328

329
def _extract_benchmark_score_from_table_cell(cell_text: str) -> int | None:
1✔
330
    """テーブルセルからベンチマークスコアを抽出.
331

332
    Args:
333
        cell_text: セルのテキスト
334

335
    Returns:
336
        ベンチマークスコア (int) または None
337
    """
338
    try:
1✔
339
        return int(re.sub(r"[^\d]", "", cell_text))
1✔
340
    except ValueError:
1✔
341
        return None
1✔
342

343

344
def search_chart_page(url: str, cpu_name: str) -> tuple[str | None, int | None]:
1✔
345
    """Search for CPU on a chart page (multithread or singlethread)."""
346
    try:
1✔
347
        response = requests.get(url, headers=HEADERS, timeout=30)
1✔
348
        response.raise_for_status()
1✔
349
    except requests.RequestException as e:
1✔
350
        logging.warning("Error fetching %s: %s", url, e)
1✔
351
        return None, None
1✔
352

353
    soup = bs4.BeautifulSoup(response.text, "html.parser")
1✔
354
    entries = soup.select("ul.chartlist li")
1✔
355

356
    best_match_name = None
1✔
357
    best_match_score_value = None
1✔
358
    best_score = 0.0
1✔
359

360
    for entry in entries:
1✔
361
        link = entry.select_one("a")
1✔
362
        if not link:
1✔
363
            continue
1✔
364

365
        entry_cpu_name = link.get_text(strip=True)
1✔
366
        match_score = calculate_match_score(cpu_name, entry_cpu_name)
1✔
367
        if match_score <= best_score or match_score <= 0.5:
1✔
368
            continue
1✔
369

370
        benchmark_score = _extract_benchmark_score_from_chart_entry(entry.get_text())
1✔
371
        if benchmark_score is not None:
1✔
372
            best_match_name = entry_cpu_name
1✔
373
            best_match_score_value = benchmark_score
1✔
374
            best_score = match_score
1✔
375

376
    return best_match_name, best_match_score_value
1✔
377

378

379
def search_cpu_list(cpu_name: str) -> tuple[str | None, int | None]:
1✔
380
    """Search for CPU on the CPU list page (for multi-thread score)."""
381
    try:
1✔
382
        response = requests.get(CPU_LIST_URL, headers=HEADERS, timeout=30)
1✔
383
        response.raise_for_status()
1✔
384
    except requests.RequestException as e:
1✔
385
        logging.warning("Error fetching CPU list page: %s", e)
1✔
386
        return None, None
1✔
387

388
    soup = bs4.BeautifulSoup(response.text, "html.parser")
1✔
389
    table = soup.find("table", id="cputable")
1✔
390
    if not table:
1✔
391
        return None, None
1✔
392

393
    tbody = table.find("tbody")
1✔
394
    if not tbody:
1✔
395
        return None, None
1✔
396

397
    best_match_name = None
1✔
398
    best_match_score_value = None
1✔
399
    best_score = 0.0
1✔
400

401
    for row in tbody.find_all("tr"):
1✔
402
        cells = row.find_all("td")
1✔
403
        if len(cells) < 2:
1✔
404
            continue
1✔
405

406
        name_link = cells[0].find("a")
1✔
407
        if not name_link:
1✔
408
            continue
1✔
409

410
        entry_cpu_name = name_link.get_text(strip=True)
1✔
411
        match_score = calculate_match_score(cpu_name, entry_cpu_name)
1✔
412
        if match_score <= best_score or match_score <= 0.5:
1✔
413
            continue
×
414

415
        benchmark_score = _extract_benchmark_score_from_table_cell(cells[1].get_text(strip=True))
1✔
416
        if benchmark_score is not None:
1✔
417
            best_match_name = entry_cpu_name
1✔
418
            best_match_score_value = benchmark_score
1✔
419
            best_score = match_score
1✔
420

421
    return best_match_name, best_match_score_value
1✔
422

423

424
def search_cpu_benchmark(cpu_name: str) -> CPUBenchmark | None:
1✔
425
    """
426
    Search for CPU benchmark scores on cpubenchmark.net.
427

428
    Fetches both multi-thread and single-thread scores.
429

430
    Returns CPUBenchmark with multi_thread_score and single_thread_score, or None if not found.
431
    """
432
    normalized_name = normalize_cpu_name(cpu_name)
1✔
433

434
    # Get multi-thread score (try multithread page first, then CPU list)
435
    multi_name, multi_score = search_chart_page(MULTITHREAD_URL, normalized_name)
1✔
436
    if not multi_score:
1✔
437
        multi_name, multi_score = search_cpu_list(normalized_name)
1✔
438

439
    # Get single-thread score
440
    single_name, single_score = search_chart_page(SINGLETHREAD_URL, normalized_name)
1✔
441

442
    # Use the best matched name
443
    result_name = multi_name or single_name
1✔
444

445
    if not result_name:
1✔
446
        return None
1✔
447

448
    return CPUBenchmark(
1✔
449
        cpu_name=result_name,
450
        multi_thread_score=multi_score,
451
        single_thread_score=single_score,
452
    )
453

454

455
def save_benchmark(cpu_name: str, multi_thread: int | None, single_thread: int | None):
1✔
456
    """Save benchmark data to database."""
457
    with get_connection(get_cpu_spec_db_path()) as conn:
1✔
458
        cursor = conn.cursor()
1✔
459
        cursor.execute("""
1✔
460
            INSERT OR REPLACE INTO cpu_benchmark (cpu_name, multi_thread_score, single_thread_score, updated_at)
461
            VALUES (?, ?, ?, CURRENT_TIMESTAMP)
462
        """, (cpu_name, multi_thread, single_thread))
463
        conn.commit()
1✔
464

465
    # Invalidate cache when new data is saved
466
    _benchmark_cache.invalidate("all_benchmarks")
1✔
467

468

469
def get_benchmark(cpu_name: str) -> CPUBenchmark | None:
1✔
470
    """Get benchmark data from database."""
471
    normalized_name = normalize_cpu_name(cpu_name)
1✔
472
    logging.debug("Looking up CPU benchmark for: %s (normalized: %s)", cpu_name, normalized_name)
1✔
473

474
    with get_connection(get_cpu_spec_db_path()) as conn:
1✔
475
        cursor = conn.cursor()
1✔
476

477
        # First try exact match
478
        cursor.execute("""
1✔
479
            SELECT cpu_name, multi_thread_score, single_thread_score
480
            FROM cpu_benchmark
481
            WHERE cpu_name = ?
482
        """, (cpu_name,))
483

484
        row = cursor.fetchone()
1✔
485

486
        if not row:
1✔
487
            # Try fuzzy match with LIKE using original name
488
            cursor.execute("""
1✔
489
                SELECT cpu_name, multi_thread_score, single_thread_score
490
                FROM cpu_benchmark
491
                WHERE cpu_name LIKE ?
492
            """, (f"%{cpu_name}%",))
493
            row = cursor.fetchone()
1✔
494

495
        if not row:
1✔
496
            # Try fuzzy match with LIKE using normalized name
497
            cursor.execute("""
1✔
498
                SELECT cpu_name, multi_thread_score, single_thread_score
499
                FROM cpu_benchmark
500
                WHERE cpu_name LIKE ?
501
            """, (f"%{normalized_name}%",))
502
            row = cursor.fetchone()
1✔
503

504
        if not row:
1✔
505
            # Try model number based matching
506
            model = extract_model_number(cpu_name)
1✔
507
            if model:
1✔
508
                cursor.execute("""
1✔
509
                    SELECT cpu_name, multi_thread_score, single_thread_score
510
                    FROM cpu_benchmark
511
                """)
512
                all_rows = cursor.fetchall()
1✔
513
                for r in all_rows:
1✔
514
                    db_model = extract_model_number(r[0])
×
515
                    if db_model and db_model == model:
×
516
                        row = r
×
517
                        break
×
518

519
        if row:
1✔
520
            logging.debug("Found benchmark for %s: multi=%s, single=%s", cpu_name, row[1], row[2])
1✔
521
            return CPUBenchmark(
1✔
522
                cpu_name=row[0],
523
                multi_thread_score=row[1],
524
                single_thread_score=row[2],
525
            )
526

527
    logging.debug("No benchmark found for: %s", cpu_name)
1✔
528
    return None
1✔
529

530

531
def get_all_benchmarks() -> dict[str, CPUBenchmark]:
1✔
532
    """Get all benchmark data from database in a single query.
533

534
    Uses in-memory cache with 1 hour TTL to avoid repeated DB queries.
535

536
    Returns:
537
        Dict mapping CPU name to CPUBenchmark
538
    """
539
    cache_key = "all_benchmarks"
1✔
540

541
    # Try cache first
542
    cached = _benchmark_cache.get(cache_key)
1✔
543
    if cached is not None:
1✔
544
        return cached
×
545

546
    # Fetch from database
547
    with get_connection(get_cpu_spec_db_path()) as conn:
1✔
548
        cursor = conn.cursor()
1✔
549
        cursor.execute("""
1✔
550
            SELECT cpu_name, multi_thread_score, single_thread_score
551
            FROM cpu_benchmark
552
        """)
553

554
        result = {
1✔
555
            row[0]: CPUBenchmark(
556
                cpu_name=row[0],
557
                multi_thread_score=row[1],
558
                single_thread_score=row[2],
559
            )
560
            for row in cursor.fetchall()
561
        }
562

563
    # Cache the result
564
    _benchmark_cache.set(cache_key, result)
1✔
565
    return result
1✔
566

567

568
def _find_benchmark_match(
1✔
569
    cpu_name: str, all_benchmarks: dict[str, CPUBenchmark]
570
) -> CPUBenchmark | None:
571
    """Find a matching benchmark for a CPU name using various strategies.
572

573
    Matching strategies (in order of priority):
574
    1. Exact match
575
    2. Substring match (original name)
576
    3. Substring match (normalized name)
577
    4. Model number match
578

579
    Args:
580
        cpu_name: CPU name to look up
581
        all_benchmarks: Dict of all benchmarks from database
582

583
    Returns:
584
        Matching CPUBenchmark or None if not found
585
    """
586
    # Try exact match first
587
    if cpu_name in all_benchmarks:
1✔
588
        return all_benchmarks[cpu_name]
1✔
589

590
    # Try fuzzy matching
591
    normalized_name = normalize_cpu_name(cpu_name)
1✔
592
    for db_name, benchmark in all_benchmarks.items():
1✔
593
        if cpu_name in db_name or normalized_name in db_name:
1✔
594
            return benchmark
1✔
595

596
    # Try model number matching
597
    if model := extract_model_number(cpu_name):
1✔
598
        for db_name, benchmark in all_benchmarks.items():
×
599
            if (db_model := extract_model_number(db_name)) and db_model == model:
×
600
                return benchmark
×
601

602
    return None
1✔
603

604

605
def get_benchmarks_batch(cpu_names: list[str]) -> dict[str, CPUBenchmark | None]:
1✔
606
    """Get benchmark data for multiple CPUs efficiently.
607

608
    Uses a single DB query to fetch all benchmarks, then matches
609
    against requested CPU names using various matching strategies.
610

611
    Args:
612
        cpu_names: List of CPU names to look up
613

614
    Returns:
615
        Dict mapping requested CPU name to CPUBenchmark (or None if not found)
616
    """
617
    all_benchmarks = get_all_benchmarks()
1✔
618
    return {cpu_name: _find_benchmark_match(cpu_name, all_benchmarks) for cpu_name in cpu_names}
1✔
619

620

621
def clear_benchmark(cpu_name: str):
1✔
622
    """Clear benchmark data from database."""
623
    with get_connection(get_cpu_spec_db_path()) as conn:
1✔
624
        cursor = conn.cursor()
1✔
625
        cursor.execute("DELETE FROM cpu_benchmark WHERE cpu_name = ?", (cpu_name,))
1✔
626
        conn.commit()
1✔
627

628
    # Invalidate cache when data is deleted
629
    _benchmark_cache.invalidate("all_benchmarks")
1✔
630

631

632
def fetch_and_save_benchmark(cpu_name: str) -> CPUBenchmark | None:
1✔
633
    """Fetch benchmark from web and save to database."""
634
    logging.info("Fetching CPU benchmark from web for: %s", cpu_name)
1✔
635
    result = search_cpu_benchmark(cpu_name)
1✔
636

637
    if result:
1✔
638
        logging.info("Found benchmark for %s: multi=%s, single=%s",
1✔
639
                     cpu_name, result.multi_thread_score, result.single_thread_score)
640
        save_benchmark(
1✔
641
            cpu_name,
642
            result.multi_thread_score,
643
            result.single_thread_score
644
        )
645
        return result
1✔
646

647
    logging.warning("Could not find benchmark data for: %s", cpu_name)
1✔
648
    return None
1✔
649

650

651
def main():
1✔
652
    """Main function to test the scraper."""
653
    logging.basicConfig(level=logging.INFO)
1✔
654
    init_db()
1✔
655

656
    test_cpus = [
1✔
657
        "Core i5-1135G7",
658
        "Intel Xeon E5-2699 v4",
659
    ]
660

661
    for cpu in test_cpus:
1✔
662
        logging.info("Searching for: %s", cpu)
1✔
663

664
        # Clear existing cache to re-fetch
665
        clear_benchmark(cpu)
1✔
666

667
        # Fetch from web
668
        result = fetch_and_save_benchmark(cpu)
1✔
669
        if result:
1✔
670
            logging.info("  Found: %s", result)
1✔
671
        else:
672
            logging.info("  Not found")
×
673

674
        # Be nice to the server
675
        time.sleep(2)
1✔
676

677

678
if __name__ == "__main__":
679
    main()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc