| Server IP : 138.197.107.151 / Your IP : 216.73.217.7 Web Server : Apache/2.4.58 (Ubuntu) System : Linux BloxBy-Builder 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64 User : wpbetasites_mrakzqskir ( 1022) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/bsd-crawler-parser/ |
Upload File : |
"""Unit tests for the line parser. Run: python3 test_parser.py"""
import sys
from crawlers import match_crawler
from logparser import normalize_site, parse_line, parse_timestamp, split_request
FAILURES = []
def check(name, actual, expected):
if actual != expected:
FAILURES.append("%s\n expected: %r\n actual: %r" % (name, expected, actual))
REAL = (
'74.7.242.30 buckfirelaw.com - [08/Jul/2026:00:17:29 +0000] '
'"GET /wp-content/themes/buckfire/css/responsive.min.css?1783469659&ver=all HTTP/1.1" '
'200 7354 "https://buckfirelaw.com/?p=19189" '
'"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.4; +https://openai.com/gptbot)"'
)
# --- happy path on a real line -------------------------------------------
e = parse_line(REAL, "logs/nginx/x.apachestyle.log.gz")
check("site", e["site"], "buckfirelaw.com")
check("platform", e["ai_platform"], "GPTBot")
check("category", e["category"], "ai_training")
check("url", e["url"], "/wp-content/themes/buckfire/css/responsive.min.css?1783469659&ver=all")
check("status", e["status_code"], 200)
check("bytes", e["bytes"], 7354)
check("timestamp", e["timestamp"], "2026-07-08T00:17:29Z")
check("source", e["source_file"], "logs/nginx/x.apachestyle.log.gz")
# --- non-AI traffic is dropped -------------------------------------------
human = (
'1.2.3.4 example.com - [08/Jul/2026:00:17:44 +0000] "GET / HTTP/1.1" 200 5 "-" '
'"Mozilla/5.0 (Macintosh) Chrome/146.0.0.0 Safari/537.36"'
)
check("human dropped", parse_line(human, "k"), None)
# --- longest-pattern-wins normalization ----------------------------------
check("Applebot-Extended beats Applebot",
match_crawler("Mozilla/5.0 (compatible; Applebot-Extended/1.0)"),
("Applebot-Extended", "ai_training"))
check("plain Applebot", match_crawler("Mozilla/5.0 (compatible; Applebot/0.1)"),
("Applebot", "search"))
check("Perplexity-User beats PerplexityBot substring",
match_crawler("Mozilla/5.0 (compatible; Perplexity-User/1.0)"),
("Perplexity-User", "ai_assistant"))
check("Claude-SearchBot", match_crawler("Mozilla/5.0 (compatible; Claude-SearchBot/1.0)"),
("Claude-SearchBot", "search"))
check("case insensitive", match_crawler("blah gptbot/1.4 blah"), ("GPTBot", "ai_training"))
check("bingbot tagged search", match_crawler("compatible; bingbot/2.0"), ("Bingbot", "search"))
check("no match", match_crawler("curl/8.4.0"), None)
check("empty ua", match_crawler(""), None)
# --- timestamp / timezone -------------------------------------------------
check("utc passthrough",
parse_timestamp("08/Jul/2026:00:17:35 +0000").strftime("%Y-%m-%dT%H:%M:%SZ"),
"2026-07-08T00:17:35Z")
check("positive offset converts back to UTC",
parse_timestamp("08/Jul/2026:05:30:00 +0530").strftime("%Y-%m-%dT%H:%M:%SZ"),
"2026-07-08T00:00:00Z")
check("negative offset crosses midnight",
parse_timestamp("07/Jul/2026:20:00:00 -0500").strftime("%Y-%m-%dT%H:%M:%SZ"),
"2026-07-08T01:00:00Z")
check("garbage timestamp", parse_timestamp("not a date"), None)
# --- vhost normalization --------------------------------------------------
check("www stripped", normalize_site("www.buckfirelaw.com"), "buckfirelaw.com")
check("bare unchanged", normalize_site("buckfirelaw.com"), "buckfirelaw.com")
check("staging kept", normalize_site("buckfirelaw.wpengine.com"), "buckfirelaw.wpengine.com")
check("uppercase folded", normalize_site("WWW.Example.COM"), "example.com")
# --- request splitting ----------------------------------------------------
check("normal request", split_request("GET /a/b HTTP/1.1"), ("GET", "/a/b", "HTTP/1.1"))
check("space in path", split_request("GET /a b HTTP/1.1"), ("GET", "/a b", "HTTP/1.1"))
check("no proto", split_request("GET /a"), ("GET", "/a", ""))
# --- edge cases in the line itself ----------------------------------------
dash_bytes = (
'1.2.3.4 a.com - [08/Jul/2026:00:00:00 +0000] "GET / HTTP/1.1" 304 - "-" '
'"Mozilla/5.0 (compatible; ClaudeBot/1.0)"'
)
check("dash bytes -> 0", parse_line(dash_bytes, "k")["bytes"], 0)
escaped_quote = (
r'1.2.3.4 a.com - [08/Jul/2026:00:00:00 +0000] "GET / HTTP/1.1" 200 5 "-" '
r'"Mozilla/5.0 \"quoted\" (compatible; CCBot/2.0)"'
)
ev = parse_line(escaped_quote, "k")
check("escaped quotes in UA still parse", ev is not None and ev["ai_platform"], "CCBot")
check("truncated line", parse_line("garbage line", "k"), None)
check("blank line", parse_line("", "k"), None)
# --- rebuild_summary precomputes the content-only counts ------------------
# The viewer's overview reads these instead of re-scanning event shards, so a
# real page, an asset and a probe for the same bot must split correctly.
import json as _json
import tempfile as _tempfile
from handler import LocalSink, rebuild_summary
_sink = LocalSink(_tempfile.mkdtemp())
_sink.put(
"events/date=2026-07-08/site=a.com/20260708-0000-alpha.jsonl",
"\n".join(_json.dumps(row) for row in [
{"site": "a.com", "ai_platform": "GPTBot", "category": "ai_training",
"url": "/dog-bite/", "status_code": 200, "bytes": 10}, # a real page
{"site": "a.com", "ai_platform": "GPTBot", "category": "ai_training",
"url": "/js/app.js", "status_code": 200, "bytes": 10}, # asset
{"site": "a.com", "ai_platform": "GPTBot", "category": "ai_training",
"url": "/.env", "status_code": 404, "bytes": 0}, # probe
]) + "\n",
)
_totals = rebuild_summary(_sink, "2026-07-08")
check("summary hits count everything", _totals["hits"], 3)
check("summary content_hits count only the page", _totals["content_hits"], 1)
_doc = _json.loads(_sink.get("summary/date=2026-07-08.json"))
check("by_platform splits content from hits",
(_doc["by_platform"]["GPTBot"]["hits"], _doc["by_platform"]["GPTBot"]["content_hits"]),
(3, 1))
check("by_platform_content drops asset and probe",
_doc["by_site"]["a.com"]["by_platform_content"], {"GPTBot": 1})
if FAILURES:
print("FAILED %d check(s):\n" % len(FAILURES))
for f in FAILURES:
print(" " + f)
sys.exit(1)
print("all parser checks passed")