iplist/test/AsyncTest.php
Rekryt 9d3ff3d6e0 feat: geosite.dat, sing-box rule-set (json + srs), native geoip.dat
New output formats:

- `format=geosite` — v2ray/xray geosite.dat, built natively by GeositeDatWriter
  (domain-list-community rejects 406 of 409 portal names, so an external
  generator was not an option); `domaintype=suffix|full|keyword|regex`.
- `format=singbox` — sing-box rule-set source JSON, served inline; it becomes
  an attachment only with `?filesave=1`.
- `format=srs` — the same rule-set compiled to binary by the sing-box binary
  (SINGBOX_PATH); without the binary the format returns a plain-text error.
- `format=geoip` — same contract, now built in PHP by GeoipDatWriter.
  `SYS_GEOIP_NATIVE=false` keeps the v2fly binary path for one release.

Infrastructure:

- src/Infrastructure/Codec: ProtobufWriter, GeoipDatWriter, GeositeDatWriter,
  SingboxRuleSetBuilder, EntryPayload, DatEncoder. Writers are pure functions —
  no I/O, no Amp, no singletons — so the same class runs inline and inside an
  amphp/parallel worker. Above SYS_ENCODE_WORKER_THRESHOLD encoding goes to a
  worker, below it yields to the event loop every 32 lists.
- TempWorkspace: one temp dir per request under storage/tmp, removed in a
  finally, plus a sweeper for dirs orphaned by SIGKILL (SYS_TMP_TTL,
  SYS_TMP_SWEEP_INTERVAL). Replaces the leaking geoip/input + geoip/output.
- ProcessRunner: the single place that starts external binaries — command as an
  array, both pipes buffered concurrently (not reading them deadlocked a chatty
  child), exit code and stderr surfaced in the error body.

Fixes:

- wildcard domain collapse no longer emits a bare "." for scraped junk like
  `DNSdumpster..`; such entries are dropped by SiteFactory::normalizeDomains
  on ingestion, and a matching guard sits in Site::getDomains.
- geoip.dat was served as text/plain; both engines now use
  application/octet-stream.
- HttpClient in AsyncTest is shared across the run: per-test clients kept their
  keep-alive pools alive and starved the Windows StreamSelectDriver by the end
  of a full run.

UI, docs, build:

- Form.vue: new formats, domaintype and rule-set version selects, wildcard on by
  default, filesave hidden for attachment-only formats; public/ rebuilt.
- IndexTemplate.php (no-JS form on /index) caught up with the frontend.
- README.md, README.en.md, CLAUDE.md, ROADMAP.md, .env.example.
- Dockerfile: sing-box from the release tarball (-glibc), v2fly/geoip built on
  golang:1.25 with a pinned tag; the apt Go 1.19 stage was the broken build.

https://github.com/itdoginfo/podkop/issues/418
2026-07-31 13:44:33 +03:00

97 lines
3.7 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace OpenCCK;
use Amp\ByteStream\StreamException;
use Amp\Http\Client\HttpClient;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\HttpException;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Amp\PHPUnit\AsyncTestCase;
use OpenCCK\App\Service\IPListService;
use OpenCCK\Infrastructure\API\App;
/**
* Base class for async controller tests.
*
* Boot is handled once by test/bootstrap.php (PATH_ROOT is pinned to
* test/fixtures, App + Server are started, IPListService loads fixtures
* with timeout=0 so no DNS/reload work happens during tests).
*
* Subclasses send HTTP requests via $this->get() and assert on the response.
*/
abstract class AsyncTest extends AsyncTestCase {
/**
* Один клиент на весь прогон, а не по клиенту на тест.
*
* PHPUnit держит экземпляры тест-кейсов живыми до конца прогона (они нужны
* ему для отчёта), поэтому клиент каждого теста тоже остаётся жив вместе со
* своим keep-alive пулом соединений. Число открытых сокетов росло линейно с
* числом тестов, а Revolt на Windows работает через StreamSelectDriver,
* который на каждом тике обходит весь набор дескрипторов — к концу прогона
* запросы начинали упираться в таймауты на ровном месте (падал то StressTest,
* то TextFilterTest, в зависимости от порядка). С общим клиентом соединение
* переиспользуется и набор дескрипторов не растёт.
*/
private static ?HttpClient $sharedHttpClient = null;
protected HttpClient $httpClient;
protected string $baseUrl;
protected App $app;
protected function setUp(): void {
parent::setUp();
self::$sharedHttpClient ??= (new HttpClientBuilder())->followRedirects(0)->build();
$this->httpClient = self::$sharedHttpClient;
$this->baseUrl = 'http://127.0.0.1:' . ($_ENV['HTTP_PORT'] ?? 8090);
$this->app = App::getInstance();
}
/**
* Send a GET request to the running test server.
*
* Query building preserves repeated keys like `exclude[group]=a&exclude[group]=b`.
* Pass an array value to repeat the key: ['exclude[group]' => ['casino', 'porn']].
*
* @param array<string, string|int|array<int, string|int>> $query
* @throws HttpException
*/
protected function get(string $path = '/', array $query = []): Response {
return $this->httpClient->request(new Request($this->buildUrl($path, $query), 'GET'));
}
/**
* @param array<string, string|int|array<int, string|int>> $query
*/
protected function buildUrl(string $path, array $query): string {
$url = $this->baseUrl . $path;
if (!$query) {
return $url;
}
$parts = [];
foreach ($query as $name => $value) {
foreach (is_array($value) ? $value : [$value] as $item) {
$parts[] = rawurlencode($name) . '=' . rawurlencode((string) $item);
}
}
return $url . '?' . implode('&', $parts);
}
/**
* Buffer and return the response body as a string.
*
* @throws StreamException
*/
protected function body(Response $response): string {
return $response->getBody()->buffer();
}
protected function service(): IPListService {
return IPListService::getInstance();
}
}