parser = new XhprofProfileParserService(); } private function loadFixtureLine(): string { $content = file_get_contents(__DIR__ . '/../fixtures/xhprof-sample.jsonl'); self::assertNotFalse($content, 'fixture 文件缺失'); return trim($content); } /** * 真实 fixture 行:解析出的字段值与 fixture 内实际数值一致. */ public function test_parse_real_fixture_line(): void { $parsed = $this->parser->parse($this->loadFixtureLine()); self::assertNotNull($parsed); // 断言值来自 analyze 实测(见 .omo/evidence/task-3-xhprof-log-backend.txt) self::assertSame('/', $parsed['url']); self::assertSame('', $parsed['simple_url']); // 根路径 simple_url 原生即为空串 self::assertSame('GET', $parsed['method']); self::assertSame(8402968, $parsed['wall_time']); self::assertSame(340812, $parsed['cpu_time']); self::assertSame(1981664, $parsed['memory_peak']); // request_ts_micro = {"sec":1786744108,"usec":846845} self::assertEqualsWithDelta(1786744108.847, $parsed['request_time'], 0.0005); self::assertFalse($parsed['degraded']); self::assertCount(954, $parsed['profile']); } public function test_parse_rejects_bad_lines(): void { self::assertNull($this->parser->parse('')); self::assertNull($this->parser->parse('not json at all')); self::assertNull($this->parser->parse('{truncated')); self::assertNull($this->parser->parse('{"meta":{"url":"/"}}')); // 缺 profile 键 self::assertNull($this->parser->parse('["array","not","object"]')); } /** * simple_url 缺失时兜底:path 去 query、纯数字段替换 :id(与 bootstrap 回调算法一致). */ public function test_simple_url_fallback(): void { $line = json_encode([ 'profile' => ['main()' => ['ct' => 1, 'wt' => 10, 'cpu' => 1, 'mu' => 1, 'pmu' => 2]], 'meta' => ['url' => '/api/goods/45/detail?foo=bar&baz=1'], ], JSON_UNESCAPED_SLASHES); $parsed = $this->parser->parse($line); self::assertNotNull($parsed); self::assertSame('api/goods/:id/detail', $parsed['simple_url']); self::assertSame(10, $parsed['wall_time']); self::assertSame(2, $parsed['memory_peak']); self::assertSame('', $parsed['method']); self::assertSame(0.0, $parsed['request_time']); } /** * url 超 500 截断. */ public function test_url_truncated_to_500(): void { $line = json_encode([ 'profile' => ['main()' => ['ct' => 1, 'wt' => 1, 'cpu' => 0, 'mu' => 0, 'pmu' => 0]], 'meta' => ['url' => '/' . str_repeat('a', 600), 'simple_url' => 'x'], ]); $parsed = $this->parser->parse($line); self::assertNotNull($parsed); self::assertSame(500, mb_strlen($parsed['url'])); } /** * main() 缺失:降级 0 且 degraded=true(行仍可入库,由导入侧计 fail). */ public function test_parse_missing_main_degraded(): void { $line = json_encode([ 'profile' => ['foo==>bar' => ['ct' => 1, 'wt' => 5]], 'meta' => ['url' => '/x'], ]); $parsed = $this->parser->parse($line); self::assertNotNull($parsed); self::assertTrue($parsed['degraded']); self::assertSame(0, $parsed['wall_time']); self::assertSame(0, $parsed['cpu_time']); self::assertSame(0, $parsed['memory_peak']); } /** * watch 物化:按 callee 匹配,caller 匹配不命中;ct 累加、wt_sum 累加、wt_max 取最大. */ public function test_materialize_watches_by_callee(): void { $profile = [ 'main()' => ['ct' => 1, 'wt' => 1000, 'cpu' => 10, 'mu' => 0, 'pmu' => 0], 'main()==>think\\db\\PDOConnection::query' => ['ct' => 2, 'wt' => 500], 'foo==>think\\db\\PDOConnection::query' => ['ct' => 3, 'wt' => 700], 'think\\db\\PDOConnection::query==>bar' => ['ct' => 9, 'wt' => 9999], // callee=bar 不命中 'plain_function' => ['ct' => 4, 'wt' => 4], // 裸键不参与 ]; $patterns = [7 => '~^think\\\\db\\\\PDOConnection::~']; $result = $this->parser->materializeWatches($profile, $patterns); self::assertArrayHasKey(7, $result); self::assertSame(5, $result[7]['ct']); self::assertSame(1200, $result[7]['wt_sum']); self::assertSame(700, $result[7]['wt_max']); } /** * 真实 fixture 的 watch 物化:PDOConnection 规则必然命中(fixture 实测 33 条边). */ public function test_materialize_watches_on_real_fixture(): void { $parsed = $this->parser->parse($this->loadFixtureLine()); self::assertNotNull($parsed); $result = $this->parser->materializeWatches($parsed['profile'], [99 => '~PDOConnection::~']); self::assertArrayHasKey(99, $result); self::assertGreaterThan(0, $result[99]['ct']); self::assertGreaterThan(0, $result[99]['wt_sum']); } /** * 正则编译:非法正则跳过该条并计 fail,合法条目进 patterns. */ public function test_compile_watch_regexes(): void { $watches = [ ['id' => 1, 'regex' => '~^think\\\\db\\\\PDOConnection::.*$~'], ['id' => 2, 'regex' => '/[unclosed'], // 非法正则 ['id' => 3, 'regex' => ''], // 空正则 ['id' => 0, 'regex' => '~ok~'], // 非法 id ]; $compiled = $this->parser->compileWatchRegexes($watches); self::assertSame(3, $compiled['fails']); self::assertSame([1 => '~^think\\\\db\\\\PDOConnection::.*$~'], $compiled['patterns']); } /** * request_time 退化链:request_ts_micro 优先,缺失退化 REQUEST_TIME_FLOAT. */ public function test_request_time_fallback_chain(): void { $base = ['profile' => ['main()' => ['ct' => 1, 'wt' => 1]]]; $withTsMicro = $this->parser->parse(json_encode($base + [ 'meta' => ['request_ts_micro' => ['sec' => 1000000000, 'usec' => 250000]], ])); self::assertSame(1000000000.25, $withTsMicro['request_time']); $withFloatOnly = $this->parser->parse(json_encode($base + [ 'meta' => ['SERVER' => ['REQUEST_TIME_FLOAT' => 1234567890.123456]], ])); self::assertSame(1234567890.123, $withFloatOnly['request_time']); } }