<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\tests;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\executables\Fetcher;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\UnitTest;
use seekquarry\yioop\models\Model;
/* Both FetcherTest and QueueServerTest load an executable that only
starts its process when this is not defined, and the suite loads both. */
if (!C\nsdefined("UNIT_TEST_MODE")) {
C\nsdefine("UNIT_TEST_MODE", true);
}
/**
* FetcherTest checks how a Fetcher decides when to send what it has
* found to a queue server.
*
* @author Chris Pollett
*/
class FetcherTest extends UnitTest
{
/**
* CLOSED_PORT is a port of this machine that nothing listens on, the
* discard port, so a download from it fails at once with no answer
*/
const CLOSED_PORT = 9;
/**
* LONGEST_HOLD is the seconds fetcher 5 held schedule 415 in the crawl
* logs of 18 October, the longest any fetcher held one there
*/
const LONGEST_HOLD = 683;
/**
* TEST_FETCHER is the fetcher number the restart case gives its
* fetchers, one no crawl uses, so its files are its own
*/
const TEST_FETCHER = 97;
/**
* TEST_CRAWL_TIME is the crawl timestamp the restart case gives its
* fetchers, used in the names of their files
*/
const TEST_CRAWL_TIME = 1700000099;
/**
* setUp makes SCHEDULES, the folder each case's fetcher keeps its
* schedules files in.
*/
public function setUp()
{
L\makePath(self::SCHEDULES);
}
/**
* tearDown deletes SCHEDULES, the folder the cases' fetchers keep
* their schedules files in.
*/
public function tearDown()
{
(new Model())->db->unlinkRecursive(self::SCHEDULES);
}
/**
* SCHEDULES is the folder, under tests/test_files, that the cases'
* fetchers keep their schedules files in.
*/
const SCHEDULES = C\TEST_DIR . "/test_files/fetcher_schedules";
/**
* passTakesManyUrlsOfFewHostsTestCase checks that a download pass of
* urls whose host addresses are not yet known stops at the allowance
* of hosts, not of urls. Three hundred urls of three hosts make one
* pass of NUM_MULTI_CURL_PAGES urls, and three hundred urls of three
* hundred hosts still stop at the allowance. The fetcher is made
* without its constructor, which would start a crawl process.
*/
public function passTakesManyUrlsOfFewHostsTestCase()
{
$fetcher = new class extends Fetcher {
/**
* __construct leaves the fetcher's settings at their defaults
* rather than starting a crawl process.
*/
public function __construct()
{
}
};
$fetcher->num_multi_curl = C\NUM_MULTI_CURL_PAGES;
$fetcher->hosts_with_errors = [];
$allowance = (int) ceil(C\NUM_MULTI_CURL_PAGES /
Fetcher::URLS_PER_HOST_LOOKUP);
foreach ([3 => C\NUM_MULTI_CURL_PAGES, 300 => $allowance] as
$num_hosts => $expected) {
$fetcher->to_crawl = [];
$fetcher->to_crawl_again = [];
for ($i = 0; $i < 300; $i++) {
$fetcher->to_crawl[] = ["https://host" . ($i % $num_hosts) .
".example.com/page$i.html", 1, 0, 0];
}
$this->assertEqual(count($fetcher->getFetchSites()), $expected,
"a pass of urls from $num_hosts hosts takes $expected urls");
}
}
/**
* firstSendComesEarlyAndTheGapDoublesTestCase checks that a web crawl's
* fetcher sends what it found once it has downloaded
* NUM_MULTI_CURL_PAGES urls, so the first pages of a crawl show up
* after the first download, and that each later send waits for twice
* as many downloads as the one before. Most urls in a download find
* nothing new, so a send is asked for by downloads, not by pages seen.
* The fetcher is made without its constructor, which would start a
* crawl process, and its send is replaced with a count of sends.
*/
public function firstSendComesEarlyAndTheGapDoublesTestCase()
{
$fetcher = new class extends Fetcher {
/**
* sends counts the calls a real fetcher would make to send.
* @var int
*/
public $sends = 0;
/**
* __construct leaves the fetcher's settings at their defaults
* rather than starting a crawl process.
*/
public function __construct()
{
}
/**
* selectCurrentServerAndUpdateIfNeeded counts a send and
* clears what would have been sent.
*
* @param bool $at_least_current_server whether a send must
* happen, as for the real method
*/
public function selectCurrentServerAndUpdateIfNeeded(
$at_least_current_server)
{
$this->sends++;
$this->found_sites[self::SEEN_URLS] = [];
}
};
$fetcher->crawl_type = Fetcher::WEB_CRAWL;
$fetcher->to_crawl = [["https://www.example.com/", 1, 0, 0]];
$fetcher->to_crawl_again = [];
$fetcher->archive_iterator = null;
$fetcher->which_fetcher = self::TEST_FETCHER;
$fetcher->schedules_folder = self::SCHEDULES;
$fetcher->channel = 0;
$fetcher->crawl_time = self::TEST_CRAWL_TIME;
$download = function ($count) use ($fetcher) {
$fetcher->found_sites[Fetcher::SEEN_URLS] = [[]];
$fetcher->updateFoundSites(array_fill(0, $count, []));
return $fetcher->sends;
};
$first = C\NUM_MULTI_CURL_PAGES;
$this->assertEqual($download($first), 1,
"the first send comes after one download of $first urls");
$this->assertEqual($download($first), 1,
"the next waits for more than $first more");
$this->assertEqual($download($first), 2,
"the second send comes after " . (2 * $first) . " more");
$download(2 * $first);
$this->assertEqual($download(2 * $first), 3,
"the third send comes after " . (4 * $first) . " more");
$this->assertEqual($fetcher->send_after, 8 * $first,
"the gap keeps doubling");
$download(8 * $first);
$this->assertEqual($fetcher->send_after,
Fetcher::SEEN_URLS_BEFORE_SEND,
"the gap stops at SEEN_URLS_BEFORE_SEND for a web crawl");
$download(Fetcher::SEEN_URLS_BEFORE_SEND);
$this->assertEqual($fetcher->sends, 5,
"a web crawl then sends every " .
Fetcher::SEEN_URLS_BEFORE_SEND . " downloads");
}
/**
* longHeldScheduleIsAnsweredTestCase checks that a web crawl's fetcher
* that has held its schedule for ANSWER_SCHEDULE_AFTER seconds sends
* what it has after its next download, though it has downloaded fewer
* urls than a send waits for. In the crawl of 18 October one fetcher
* held schedule 415 for 683 seconds on pages taking 4 seconds each,
* and the other fetchers slept until it came back. A schedule given
* just now waits for its downloads as before. The fetcher is made
* without its constructor, and its send is replaced with a count.
*/
public function longHeldScheduleIsAnsweredTestCase()
{
$fetcher = new class extends Fetcher {
/**
* sends counts the calls a real fetcher would make to send.
* @var int
*/
public $sends = 0;
/**
* __construct leaves the fetcher's settings at their defaults
* rather than starting a crawl process.
*/
public function __construct()
{
}
/**
* selectCurrentServerAndUpdateIfNeeded counts a send and
* clears what would have been sent.
*
* @param bool $at_least_current_server whether a send must
* happen, as for the real method
*/
public function selectCurrentServerAndUpdateIfNeeded(
$at_least_current_server)
{
$this->sends++;
$this->found_sites[self::SEEN_URLS] = [];
}
};
$fetcher->crawl_type = Fetcher::WEB_CRAWL;
$fetcher->to_crawl = [["https://www.example.com/", 1, 0, 0]];
$fetcher->to_crawl_again = [];
$fetcher->archive_iterator = null;
$fetcher->which_fetcher = self::TEST_FETCHER;
$fetcher->schedules_folder = self::SCHEDULES;
$fetcher->channel = 0;
$fetcher->crawl_time = self::TEST_CRAWL_TIME;
$fetcher->send_after = Fetcher::SEEN_URLS_BEFORE_SEND;
$fetcher->sequence_number = 415;
$fetcher->answered_sequence_number = 414;
$fetcher->schedule_received_time = time();
$fetcher->found_sites[Fetcher::SEEN_URLS] = [[]];
$fetcher->updateFoundSites(array_fill(0, 1, []));
$this->assertEqual($fetcher->sends, 0,
"a schedule given just now is not sent after one download");
$fetcher->schedule_received_time = time() - self::LONGEST_HOLD;
$fetcher->updateFoundSites(array_fill(0, 1, []));
$this->assertEqual($fetcher->sends, 1,
"a schedule held " . self::LONGEST_HOLD . " seconds is sent");
}
/**
* stoppedFetcherGoesOnWithoutItsPageTestCase checks what a fetcher
* restarted after it stopped while processing a page does with its
* schedule. In the crawl of 18 October a fetcher stopped on
* 2024LobbyGroupByLobbyist.xlsx three times: each restart dropped the
* schedule, the queue server waited out its timeout, then sent it to
* the next fetcher, which stopped on the same page. The restarted
* fetcher here must go on with the schedule's urls not yet sent back,
* under the same sequence number, without the page that stopped it.
* A schedule past the time the queue server sends it elsewhere is left
* alone. The fetchers are made without their constructors.
*/
public function stoppedFetcherGoesOnWithoutItsPageTestCase()
{
$make_fetcher = function () {
$fetcher = new class extends Fetcher {
/**
* __construct leaves the fetcher's settings at their
* defaults rather than starting a crawl process.
*/
public function __construct()
{
}
};
$fetcher->which_fetcher = self::TEST_FETCHER;
$fetcher->schedules_folder = self::SCHEDULES;
$fetcher->channel = 0;
$fetcher->crawl_time = self::TEST_CRAWL_TIME;
$fetcher->to_crawl = [];
$fetcher->to_crawl_again = [];
return $fetcher;
};
$stopping_url = "https://www.ethics.texas.gov/data/search/lobby/" .
"2024/2024LobbyGroupByLobbyist.xlsx";
$before = $make_fetcher();
$before->sequence_number = 860;
$before->schedule_resend_time = time() + C\ONE_MINUTE;
$before->to_crawl = [["https://www.example.com/a###1.2.3.4", 1, 0, 0],
["$stopping_url###5.6.7.8", 1, 0, 0],
["https://www.example.com/b", 1, 0, 0]];
$before->saveScheduleLeft();
file_put_contents($before->pageInProcessName(), $stopping_url);
$after = $make_fetcher();
$after->resumeScheduleLeft();
$this->assertEqual([$after->sequence_number,
array_column($after->to_crawl, 0)], [860,
["https://www.example.com/a###1.2.3.4",
"https://www.example.com/b"]],
"the restarted fetcher goes on with the schedule less the page");
$before->schedule_resend_time = time() - 1;
$before->saveScheduleLeft();
$late = $make_fetcher();
$late->resumeScheduleLeft();
$this->assertEqual($late->to_crawl, [],
"a schedule past its resend time is left to another fetcher");
}
/**
* pageWithNoAnswerIsSentBackTestCase checks that a page that gets no
* answer in both of its tries in a batch is sent back to the queue
* server to be fetched again, rather than dropped, as a seed page once
* was. The page's url is on a port of this machine nothing listens on,
* so its download fails at once. The fetcher downloads twice: first
* the batch, after which the page waits for its second try, then the
* pages that failed. Its send is replaced with nothing.
*/
public function pageWithNoAnswerIsSentBackTestCase()
{
$fetcher = new class extends Fetcher {
/**
* __construct leaves the fetcher's settings at their defaults
* rather than starting a crawl process.
*/
public function __construct()
{
}
/**
* selectCurrentServerAndUpdateIfNeeded sends nothing, so what
* would be sent stays to be read.
*
* @param bool $at_least_current_server whether a send must
* happen, as for the real method
*/
public function selectCurrentServerAndUpdateIfNeeded(
$at_least_current_server)
{
}
};
$url = "http://127.0.0.1:" . self::CLOSED_PORT . "/seed.html";
$fetcher->crawl_type = Fetcher::WEB_CRAWL;
$fetcher->queue_servers = ["http://localhost/"];
$fetcher->to_crawl = [[$url, 1, 0, 0]];
$fetcher->to_crawl_again = [];
$fetcher->found_sites = [];
$fetcher->num_multi_curl = C\NUM_MULTI_CURL_PAGES;
$fetcher->downloadPagesWebCrawl();
$this->assertEqual(count($fetcher->to_crawl_again), 1,
"after the first try the page waits for its second");
$fetcher->downloadPagesWebCrawl();
$sent_back = array_column(
$fetcher->found_sites[Fetcher::RETRY_URLS][0] ?? [], 0);
$this->assertEqual($sent_back, [$url],
"after the second the page is sent back to the queue server");
}
/**
* challengedHomePageIsIndexedByUrlWordsTestCase checks how a page that
* is a bot-protection challenge is processed. The page is the one
* https://www.pepsi.com/ sent on 21 October 2026, a 403 from Imperva
* Incapsula with a robots meta tag of NOINDEX, NOFOLLOW. In a crawl
* that allows landing pages, the home page is only sent back to be
* fetched again while the Scheduler has tries left. On the Scheduler's
* last try, which the schedule lists in LAST_TRY_URLS, the home page
* is indexed by the words of its url and still sent back once. A page
* below the home page, or the home page in a crawl that follows
* robots.txt, is only sent back, even on its last try. A home page
* that answers with no text, as a page drawn by scripts does, keeps
* its title and is described by the words of its url.
*/
public function challengedHomePageIsIndexedByUrlWordsTestCase()
{
$challenge = '<html style="height:100%"><head><META NAME="ROBOTS" ' .
'CONTENT="NOINDEX, NOFOLLOW"><script type="text/javascript" ' .
'src="/_Incapsula_Resource?SWJIYLWA=719d34d31c8e3a6e6fffd425f7e' .
'032f3"></script></head><body style="margin:0px;height:100%">' .
'<iframe id="main-iframe" src="/_Incapsula_Resource?CWUDNSAI=' .
'23" frameborder=0 width="100%" height="100%">Request ' .
'unsuccessful. Incapsula incident ID: 361000420216756332-' .
'189006456860248749</iframe></body></html>';
$process = function ($robots_txt, $url, $last_try = false,
$page = null, $code = 403) use ($challenge) {
return $this->processOnePage($robots_txt, [
Fetcher::URL => $url, Fetcher::HTTP_CODE => $code,
Fetcher::TYPE => "text/html",
Fetcher::PAGE => $page ?? $challenge], $last_try);
};
$home = "https://www.pepsi.com/";
[$summaries, $sent_back] = $process(C\ALLOW_LANDING_ROBOTS, $home);
$this->assertEqual([count($summaries), $sent_back], [0, [$home]],
"before the last try the home page is only sent back");
[$summaries, $sent_back] = $process(C\ALLOW_LANDING_ROBOTS, $home,
true);
$this->assertEqual([count($summaries),
$summaries[0][Fetcher::DESCRIPTION] ?? "",
$summaries[0][Fetcher::TITLE] ?? ""],
[1, "pepsi www.pepsi.com", "www.pepsi.com"],
"on the last try the home page is indexed by the words of its " .
"url");
$this->assertEqual($sent_back, [$home],
"on the last try the home page is also sent back once");
[$summaries, $sent_back] = $process(C\ALLOW_LANDING_ROBOTS,
$home . "about/", true);
$this->assertEqual([count($summaries), $sent_back],
[0, [$home . "about/"]],
"a page below the home page is only sent back");
[$summaries, $sent_back] = $process(C\ALWAYS_FOLLOW_ROBOTS, $home,
true);
$this->assertEqual([count($summaries), $sent_back], [0, [$home]],
"in a crawl that follows robots.txt the home page is only " .
"sent back");
[$summaries, ] = $process(C\ALLOW_LANDING_ROBOTS,
"https://coke.com/", false, "<html><head><title>Coke</title>" .
"</head><body><div id='app'></div></body></html>", 200);
$this->assertEqual([$summaries[0][Fetcher::DESCRIPTION] ?? "",
$summaries[0][Fetcher::TITLE] ?? ""], ["coke coke.com", "Coke"],
"a home page that gave no text keeps its title and is " .
"described by the words of its url");
}
/**
* redirectKeepsItsUrlsTestCase checks the record processFetchPages
* makes for a page that answers with a redirect. Its LOCATION field
* lists the page's url and then the url it redirects to, made whole
* from a relative Location header. The record is described as the
* page's url, an arrow and the target. The target is also a link of
* the record, so it gets fetched.
*/
public function redirectKeepsItsUrlsTestCase()
{
$url = "https://coke.com/";
$target = "https://coke.com/us/en";
[$summaries, ] = $this->processOnePage(C\ALWAYS_FOLLOW_ROBOTS, [
Fetcher::URL => $url, Fetcher::HTTP_CODE => 301,
Fetcher::TYPE => "text/html", Fetcher::PAGE => "",
Fetcher::LOCATION => ["/us/en"]]);
$this->assertEqual($summaries[0][Fetcher::LOCATION] ?? null,
[$url, $target], "the redirect lists the page and its target");
$this->assertEqual([$summaries[0][Fetcher::DESCRIPTION] ?? "",
isset($summaries[0][Fetcher::LINKS][$target])],
["$url => $target", true],
"the record names the target and links to it");
}
/**
* linkGetsNoQuestionAnswersOfPageBeforeItTestCase checks the question
* answers compressAndUnsetSeenUrls packs with each site it sends. A page
* whose title and summary hold a question and its answer gets them. A
* link found on an earlier page, packed after that page, gets none of
* them: each site gets the answers of its own text only.
*/
public function linkGetsNoQuestionAnswersOfPageBeforeItTestCase()
{
$fetcher = new Fetcher();
$fetcher->queue_servers = ["http://localhost/"];
$fetcher->crawl_stat_info = ["NUM_SEEN_SITES" => 0];
$fetcher->found_sites[Fetcher::SEEN_URLS] = [[
Fetcher::URL => "https://www.example.com/bob",
Fetcher::TITLE => "Who is Bob?",
Fetcher::DESCRIPTION => "Bob is a cat. The cat sat on the mat.",
Fetcher::LANG => "en", Fetcher::TIMESTAMP => self::TEST_CRAWL_TIME,
Fetcher::LINKS => [], Fetcher::HASH => "bob"]];
$fetcher->found_sites[Fetcher::LINK_SEEN_URLS][0] = [[
Fetcher::URL => "url|https://www.example.com/cats|text|cats|" .
"iref|https://www.example.com/", Fetcher::TITLE => "cats",
Fetcher::DESCRIPTION => "cats", Fetcher::TYPE => "link",
Fetcher::LANG => "en", Fetcher::TIMESTAMP => self::TEST_CRAWL_TIME,
Fetcher::HASH => "cats"]];
$packed = $fetcher->compressAndUnsetSeenUrls(0);
$sites = [];
for ($offset = 0; $offset < strlen($packed);
$offset += 4 + $length) {
$length = L\unpackInt(substr($packed, $offset, 4));
$sites[] = L\unpackSeenSite(substr($packed, $offset + 4,
$length));
}
$this->assertEqual(count($sites), 2, "both sites are packed");
$this->assertTrue(!empty($sites[0][Fetcher::QUESTION_ANSWERS]),
"the page gets the answers of its text");
$this->assertTrue(empty($sites[1][Fetcher::QUESTION_ANSWERS]),
"the link gets no answers");
}
/**
* processOnePage runs processFetchPages on one downloaded page with a
* fetcher made for the purpose, and gives what came back. The fetcher
* is numbered TEST_FETCHER, so the crawl statistics file it writes is
* one tearDown removes. challengedHomePageIsIndexedByUrlWordsTestCase
* and redirectKeepsItsUrlsTestCase use it.
*
* @param int $robots_txt the crawl's robots.txt mode, such as
* C\ALLOW_LANDING_ROBOTS
* @param array $site the downloaded page's fields; the encoding,
* weight, depth and address are filled in where not given
* @param bool $last_try whether the schedule names the page as on the
* Scheduler's last try
* @return array the page summaries processFetchPages gave, and the
* urls the fetcher would send back to be fetched again
*/
public function processOnePage($robots_txt, $site, $last_try = false)
{
$fetcher = new Fetcher();
$fetcher->which_fetcher = self::TEST_FETCHER;
$fetcher->schedules_folder = self::SCHEDULES;
$fetcher->crawl_stat_info = ["NUM_SUMMARIZED_PAGES" => 0,
"NUM_CACHED_PAGES" => 0];
$fetcher->crawl_stat_filename = self::SCHEDULES . "/" .
self::TEST_FETCHER . "-0-CrawlStat" . self::TEST_CRAWL_TIME .
".txt";
$fetcher->queue_servers = ["http://localhost/"];
$fetcher->robots_txt = $robots_txt;
if ($last_try) {
$fetcher->last_try_urls = [$site[Fetcher::URL] => true];
}
$summaries = $fetcher->processFetchPages([$site + [
Fetcher::ENCODING => "UTF-8", Fetcher::WEIGHT => 1,
Fetcher::DOC_DEPTH => 1,
Fetcher::IP_ADDRESSES => ["45.223.17.132"]]]);
$sent_back = array_column(
$fetcher->found_sites[Fetcher::RETRY_URLS][0] ?? [], 0);
return [$summaries, $sent_back];
}
/**
* ratePerHourStartsAtTheFetchersStartTestCase checks the per-hour
* figures a fetcher logs and sends, such as its downloads per hour.
* The first figure divides by the time since the fetcher was made,
* where it divided by the hours since 1970 and came out near 0. Until
* the window moves again the figure is the total over the time since
* then. Once it has moved, the figure is the amount since the total it
* kept, over the time since the move before. The fetcher
* is made without its constructor, which would start a crawl process.
*/
public function ratePerHourStartsAtTheFetchersStartTestCase()
{
$fetcher = (new \ReflectionClass(Fetcher::class))
->newInstanceWithoutConstructor();
$start = self::TEST_CRAWL_TIME;
$fetcher->rate_start = $start;
$this->assertEqual($fetcher->ratePerHour("DOWNLOADS", 500,
$start + C\ONE_HOUR / 2), 1000,
"500 pages in the first half hour are 1000 an hour");
$this->assertEqual($fetcher->ratePerHour("DOWNLOADS", 800,
$start + C\ONE_HOUR), 800,
"800 pages in the first hour are 800 an hour");
$this->assertEqual($fetcher->ratePerHour("DOWNLOADS", 2000,
$start + 3 * C\ONE_HOUR), 2000 / 3,
"within the window the figure counts from the start");
$this->assertEqual($fetcher->ratePerHour("DOWNLOADS", 2600,
$start + 4 * C\ONE_HOUR), 2100 / 3.5,
"a moved window counts from its last move before");
$this->assertEqual($fetcher->ratePerHour("UPLOADS", 50,
$start + 4 * C\ONE_HOUR), 12.5,
"each figure has its own window");
}
/**
* uploadPartSizeSettlesOnLargestThatWorksTestCase checks how a fetcher
* picks the part size of its uploads to each queue server. Fetchers
* used to keep 2,000,000 byte parts unless a server reported a smaller
* limit, and dropped to 1,000,000 bytes for good after one failed first
* part, so a 76 megabyte upload took 38 posts. Now a fetcher starts at
* DEFAULT_POST_MAX_SIZE, takes the limit a server reports, and after a
* size fails searches between the largest size that went through and
* the smallest that failed.
*/
public function uploadPartSizeSettlesOnLargestThatWorksTestCase()
{
$fetcher = new Fetcher();
$server = "https://queue.example.com/";
$other = "https://other.example.com/";
$this->assertEqual($fetcher->postSize($server),
Fetcher::DEFAULT_POST_MAX_SIZE, "a new server gets the default");
$fetcher->noteServerPostLimit($server, 9000000);
$this->assertEqual($fetcher->postSize($server), 9000000,
"the size grows to the limit the server reports");
$fetcher->notePostResult($server, 9000000, false);
$this->assertEqual($fetcher->postSize($server), 4500000,
"with nothing through yet, a failed size is halved");
$fetcher->notePostResult($server, 4500000, true);
$this->assertEqual($fetcher->postSize($server), 6750000,
"the next size is halfway between what worked and what failed");
$fetcher->notePostResult($server, 6750000, false);
$fetcher->notePostResult($server, 5625000, true);
$fetcher->notePostResult($server, 6187500, false);
$fetcher->notePostResult($server, 5906250, true);
$fetcher->notePostResult($server, 6046875, false);
$this->assertEqual($fetcher->postSize($server), 5906250,
"the search stops at the largest size that went through");
$fetcher->noteServerPostLimit($server, 9000000);
$this->assertEqual($fetcher->postSize($server), 5906250,
"a reported limit does not raise a size past one that failed");
$this->assertEqual($fetcher->postSize($other),
Fetcher::DEFAULT_POST_MAX_SIZE, "each server has its own size");
$fetcher->noteServerPostLimit($server, 5000000);
$this->assertEqual($fetcher->postSize($server), 5000000,
"a lower reported limit lowers the size");
$fetcher->notePostResult($other, 1000000, false);
$fetcher->notePostResult($other, 600000, false);
$this->assertEqual($fetcher->postSize($other),
Fetcher::MIN_POST_SIZE, "the size stays at MIN_POST_SIZE or more");
}
/**
* hostWithManySlowPagesGetsFastProcessorTestCase checks that once a
* host has given a fetcher SLOW_PAGES_BEFORE_FAST slow pages within
* an hour, its pages get the fast variant of their processor, an
* HtmlProcessor whose summary comes from the ScrapeSummarizer, until
* the hour has passed. Other hosts keep the processor the crawl asks
* for. The fetcher is made without its constructor, which would start
* a crawl process.
*/
public function hostWithManySlowPagesGetsFastProcessorTestCase()
{
$fetcher = (new \ReflectionClass(Fetcher::class))
->newInstanceWithoutConstructor();
$fetcher->page_processors = ["text/html" => "HtmlProcessor"];
$fetcher->plugin_processors = [];
$fetcher->plugin_hash = "fast processor case";
$fetcher->max_description_len = 2000;
$fetcher->summarizer_option = Fetcher::CENTROID_WEIGHTED_SUMMARIZER;
$start = self::TEST_CRAWL_TIME;
for ($i = 1; $i < Fetcher::SLOW_PAGES_BEFORE_FAST; $i++) {
$fetcher->noteSlowPage("slow.example.com", $start + $i);
}
$this->assertTrue(!$fetcher->usesFastProcessor("slow.example.com",
$start + 60), "one slow page short of the limit is not enough");
$this->assertEqual($fetcher->noteSlowPage("slow.example.com",
$start + 61), Fetcher::SLOW_PAGES_BEFORE_FAST,
"the slow pages of the hour are counted");
$this->assertTrue($fetcher->usesFastProcessor("slow.example.com",
$start + 62), "the host then gets the fast processor");
$this->assertTrue(!$fetcher->usesFastProcessor("other.example.com",
$start + 62), "another host does not");
$this->assertTrue(!$fetcher->usesFastProcessor("slow.example.com",
$start + 1 + C\ONE_HOUR), "once the hour has passed, it does not");
$this->assertEqual(get_class($fetcher->pageProcessor("text/html",
true)->summarizer), C\NS_SUMMARIZERS . "ScrapeSummarizer",
"the fast processor summarizes with the ScrapeSummarizer");
$this->assertEqual(get_class($fetcher->pageProcessor("text/html")
->summarizer), C\NS_SUMMARIZERS . "CentroidWeightedSummarizer",
"the usual processor keeps the crawl's summarizer");
}
}