test.pl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. # Copyright (C) 2004–2019 Alex Schroeder <alex@gnu.org>
  2. # Copyright (C) 2015 Alex-Daniel Jakimenko <alex.jakimenko@gmail.com>
  3. #
  4. # This program is free software; you can redistribute it and/or modify it under
  5. # the terms of the GNU General Public License as published by the Free Software
  6. # Foundation; either version 3 of the License, or (at your option) any later
  7. # version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License along with
  14. # this program. If not, see <http://www.gnu.org/licenses/>.
  15. package OddMuse;
  16. use lib '.';
  17. use XML::LibXML;
  18. use utf8;
  19. use Encode qw(encode_utf8 decode_utf8);
  20. use vars qw($raw);
  21. # Test::More explains how to fix wide character in print issues
  22. my $builder = Test::More->builder;
  23. binmode $builder->output, ":encoding(UTF-8)";
  24. binmode $builder->failure_output, ":encoding(UTF-8)";
  25. binmode $builder->todo_output, ":encoding(UTF-8)";
  26. # Import the functions
  27. $raw = 0; # capture utf8 is the default
  28. $RunCGI = 0; # don't print HTML on stdout
  29. $UseConfig = 0; # don't read module files
  30. $DataDir = 'test-data';
  31. while (not mkdir($DataDir)) {
  32. $DataDir = sprintf("test-data-%03d", int(rand(1000)));
  33. }
  34. $ENV{WikiDataDir} = $DataDir;
  35. require 'wiki.pl';
  36. # Try to guess which Perl we should be using. Since we loaded wiki.pl,
  37. # our $ENV{PATH} is set to /bin:/usr/bin in order to find diff and
  38. # grep.
  39. if ($ENV{PERLBREW_PATH}) {
  40. $ENV{PATH} = join(':', split(/ /, $ENV{PERLBREW_PATH}), $ENV{PATH});
  41. } elsif (-f '/usr/local/bin/perl') {
  42. $ENV{PATH} = '/usr/local/bin:' . $ENV{PATH};
  43. }
  44. clear_pages();
  45. Init();
  46. use vars qw($redirect);
  47. undef $/;
  48. $| = 1; # no output buffering
  49. sub url_encode {
  50. my $str = shift;
  51. return '' unless $str;
  52. my @letters = split(//, encode_utf8($str));
  53. my @safe = ('a' .. 'z', 'A' .. 'Z', '0' .. '9', '-', '_', '.'); # shell metachars are unsafe
  54. foreach my $letter (@letters) {
  55. my $pattern = quotemeta($letter);
  56. if (not grep(/$pattern/, @safe)) {
  57. $letter = sprintf("%%%02x", ord($letter));
  58. }
  59. }
  60. return join('', @letters);
  61. }
  62. # Run perl in a subprocess and make sure it prints UTF-8 and not Latin-1
  63. # If you use the download action, the output will be raw bytes. Use
  64. # something like the following:
  65. # {
  66. # local $raw = 1;
  67. # $page = get_page('action=download id=Trogs');
  68. # }
  69. sub capture {
  70. my $command = shift;
  71. if ($raw) {
  72. open (CL, '-|', $command) or die "Can't run $command: $!";
  73. } else {
  74. open (CL, '-|:encoding(utf-8)', $command) or die "Can't run $command: $!";
  75. }
  76. my $result = <CL>;
  77. close CL;
  78. return $result;
  79. }
  80. sub update_page {
  81. my ($id, $text, $summary, $minor, $admin, @rest) = @_;
  82. $id = FreeToNormal($id);
  83. my $pwd = $admin ? 'foo' : 'wrong';
  84. my $page = url_encode($id);
  85. $text = url_encode($text);
  86. $summary = url_encode($summary);
  87. $minor = $minor ? 'on' : 'off';
  88. my $rest = join(' ', @rest);
  89. $redirect = capture("perl wiki.pl 'Save=1' 'title=$page' 'summary=$summary' 'recent_edit=$minor' 'text=$text' 'pwd=$pwd' $rest");
  90. $output = capture("perl wiki.pl action=browse id=$page $rest");
  91. if ($redirect =~ /^Status: 302 /) {
  92. # just in case a new page got created or NearMap or InterMap
  93. $IndexHash{$id} = 1;
  94. @IndexList = sort(keys %IndexHash);
  95. ReInit($id); # if $id eq $InterMap, we need it to be in the $IndexHash before running ReInit()
  96. }
  97. return $output;
  98. }
  99. sub get_page {
  100. return capture("perl wiki.pl @_");
  101. }
  102. sub name {
  103. $_ = shift;
  104. s/\n/\\n/g;
  105. $_ = '...' . substr($_, -67) if length > 70;
  106. return $_;
  107. }
  108. sub newlines {
  109. my @strings = @_;
  110. return map { s/\\n/\n/g; $_; } @strings;
  111. }
  112. # alternating input and output strings for applying rules
  113. sub run_tests {
  114. # translate embedded newlines (other backslashes remain untouched)
  115. my @tests = newlines(@_);
  116. my ($input, $output);
  117. while (($input, $output, @tests) = @tests) {
  118. my $result = apply_rules($input);
  119. is($result, $output, name($input));
  120. }
  121. }
  122. # alternating input and output strings for applying rules
  123. sub run_tests_negative {
  124. # translate embedded newlines (other backslashes remain untouched)
  125. my @tests = newlines(@_);
  126. my ($input, $output);
  127. while (($input, $output, @tests) = @tests) {
  128. my $result = apply_rules($input);
  129. isnt($result, $output, name($input));
  130. }
  131. }
  132. sub apply_rules {
  133. my $input = shift;
  134. local *STDOUT;
  135. $output = '';
  136. open(STDOUT, '>', \$output) or die "Can't open memory file: $!";
  137. $FootnoteNumber = 0;
  138. ApplyRules(QuoteHtml($input), 1);
  139. return $output;
  140. }
  141. # alternating input and output strings for applying macros instead of rules
  142. sub run_macro_tests {
  143. # translate embedded newlines (other backslashes remain untouched)
  144. my %test = map { s/\\n/\n/g; $_; } @_;
  145. # Note that the order of tests is not specified!
  146. foreach my $input (keys %test) {
  147. $_ = $input;
  148. foreach my $macro (@MyMacros) { &$macro; }
  149. is($_, $test{$input}, $input);
  150. }
  151. }
  152. # one string, many tests
  153. sub test_page {
  154. my ($page, @tests) = @_;
  155. foreach my $test (@tests) {
  156. like($page, qr($test), name($test));
  157. }
  158. }
  159. # one file, many tests
  160. sub test_file {
  161. my ($file, @tests) = @_;
  162. if (open(F, '< :encoding(UTF-8)', $file)) {
  163. local $/ = undef;
  164. test_page(<F>, @tests);
  165. close(F);
  166. } else {
  167. warn "cannot open $file\n";
  168. }
  169. }
  170. # one string, many negative tests
  171. sub test_page_negative {
  172. my $page = shift;
  173. foreach my $str (@_) {
  174. unlike($page, qr($str), name("not $str"));
  175. }
  176. }
  177. sub xpath_do {
  178. my ($check, $message, $page, @tests) = @_;
  179. $page =~ s/^(.+\r\n)*\r\n//; # strip headers
  180. my $xml = $page =~ s/^.*?<\?xml.*?>\s*//s; # strip xml processing
  181. my $page_shown = 0;
  182. my $parser = XML::LibXML->new(recover => 1, suppress_errors => 1); # allow HTML5 tags
  183. my $doc;
  184. my @result;
  185. SKIP:
  186. {
  187. if ($xml) {
  188. eval { $doc = $parser->parse_string($page) };
  189. } else {
  190. eval { $doc = $parser->parse_html_string($page) };
  191. }
  192. if ($@) {
  193. skip("Cannot parse ".name($page).": $@", $#tests + 1);
  194. return;
  195. }
  196. # warn "Doc: '$doc'\n";
  197. foreach my $test (@tests) {
  198. my $nodelist;
  199. # libxml2 is not aware of UTF8 flag
  200. eval { $nodelist = $doc->findnodes(encode_utf8($test)) };
  201. if ($@) {
  202. fail(&$check(1) ? "$test: $@" : "not $test: $@");
  203. } elsif (ok(&$check($nodelist->size()),
  204. name(&$check(1) ? $test : "not $test"))) {
  205. push(@result, $nodelist->string_value());
  206. } else {
  207. $page =~ s/^.*?<html/<html/s;
  208. diag($message, substr($page,0,30000)) unless $page_shown;
  209. $page_shown = 1;
  210. }
  211. }
  212. }
  213. return wantarray ? @result : $result[0]; # list or string of first result
  214. }
  215. sub xpath_test {
  216. xpath_do(sub { shift > 0; }, "No Matches\n", @_);
  217. }
  218. sub xpath_test_file {
  219. my ($file, @tests) = @_;
  220. if (open(F, '< :encoding(UTF-8)', $file)) {
  221. local $/ = undef;
  222. xpath_test(<F>, @tests);
  223. close(F);
  224. } else {
  225. warn "cannot open $file\n";
  226. }
  227. }
  228. sub negative_xpath_test {
  229. xpath_do(sub { shift == 0; }, "Unexpected Matches\n", @_);
  230. }
  231. # alias
  232. sub xpath_test_negative {
  233. return negative_xpath_test(@_);
  234. }
  235. sub xpath_run_tests {
  236. # translate embedded newlines (other backslashes remain untouched)
  237. my @tests = newlines(@_);
  238. my ($input, $output);
  239. while (($input, $output, @tests) = @tests) {
  240. my $result = apply_rules($input);
  241. xpath_test("<div>$result</div>", $output);
  242. }
  243. }
  244. sub xpath_run_tests_negative {
  245. # translate embedded newlines (other backslashes remain untouched)
  246. my @tests = newlines(@_);
  247. my ($input, $output);
  248. while (($input, $output, @tests) = @tests) {
  249. my $result = apply_rules($input);
  250. xpath_test_negative("<div>$result</div>", $output);
  251. }
  252. }
  253. sub remove_rule {
  254. my $rule = shift;
  255. my @list = ();
  256. my $found = 0;
  257. foreach my $item (@MyRules) {
  258. if ($item ne $rule) {
  259. push @list, $item;
  260. } else {
  261. $found = 1;
  262. }
  263. }
  264. die "Rule not found" unless $found;
  265. @MyRules = @list;
  266. }
  267. sub add_module {
  268. my ($mod, $subdir) = @_;
  269. $subdir .= '/' if $subdir and substr($subdir, -1) ne '/';
  270. mkdir $ModuleDir unless -d $ModuleDir;
  271. my $dir = `/bin/pwd`;
  272. chop($dir);
  273. if (-l "$ModuleDir/$mod") {
  274. # do nothing
  275. } elsif (eval{ symlink("$dir/modules/$subdir$mod", "$ModuleDir/$mod"); 1; }) {
  276. # do nothing
  277. } else {
  278. system('copy', "$dir/modules/$subdir$mod", "$ModuleDir/$mod");
  279. }
  280. die "Cannot symlink $mod: $!" unless -e "$ModuleDir/$mod";
  281. do "$ModuleDir/$mod";
  282. @MyRules = sort {$RuleOrder{$a} <=> $RuleOrder{$b}} @MyRules;
  283. }
  284. sub remove_module {
  285. my $mod = shift;
  286. mkdir $ModuleDir unless -d $ModuleDir;
  287. unlink("$ModuleDir/$mod") or die "Cannot unlink: $!";
  288. }
  289. sub write_config_file {
  290. open(F, '>:encoding(utf-8)', "$DataDir/config");
  291. print F "\$AdminPass = 'foo';\n";
  292. # this used to be the default in earlier CGI.pm versions
  293. print F "\$ScriptName = 'http://localhost/wiki.pl';\n";
  294. print F "\$SurgeProtection = 0;\n";
  295. close(F);
  296. $ScriptName = 'http://localhost/test.pl'; # different!
  297. $IndexInit = 0;
  298. %IndexHash = ();
  299. @IndexList = ();
  300. $InterSiteInit = 0;
  301. %InterSite = ();
  302. $NearSiteInit = 0;
  303. %NearSite = ();
  304. %NearSearch = ();
  305. }
  306. sub clear_pages {
  307. if (-f "/bin/rm") {
  308. system('/bin/rm', '-rf', $DataDir);
  309. } else {
  310. system('c:/cygwin/bin/rm.exe', '-rf', $DataDir);
  311. }
  312. die "Cannot remove '$DataDir'!\n" if -e $DataDir;
  313. mkdir $DataDir;
  314. if ($^O eq 'darwin') {
  315. # On a Mac we are probably using the HFS filesystem which uses NFD instead
  316. # of NFC for filenames. Since clear_pages runs as the very first thing, the
  317. # modules directory doesn't exist, yet. And as Init() hasn't run, $ModuleDir
  318. # is not set either. All we have is $DataDir.
  319. $ModuleDir = "$DataDir/modules";
  320. add_module('mac.pl');
  321. }
  322. write_config_file();
  323. }
  324. # Find an unused port
  325. sub random_port {
  326. use Errno qw( EADDRINUSE );
  327. use Socket qw( PF_INET SOCK_STREAM INADDR_ANY sockaddr_in );
  328. my $family = PF_INET;
  329. my $type = SOCK_STREAM;
  330. my $proto = getprotobyname('tcp') or die "getprotobyname: $!";
  331. my $host = INADDR_ANY; # Use inet_aton for a specific interface
  332. for my $i (1..3) {
  333. my $port = 1024 + int(rand(65535 - 1024));
  334. socket(my $sock, $family, $type, $proto) or die "socket: $!";
  335. my $name = sockaddr_in($port, $host) or die "sockaddr_in: $!";
  336. setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, 1);
  337. bind($sock, $name)
  338. and close($sock)
  339. and return $port;
  340. die "bind: $!" if $! != EADDRINUSE;
  341. print "Port $port in use, retrying...\n";
  342. }
  343. die "Tried 3 random ports and failed.\n"
  344. }
  345. my @pids;
  346. # Fork a simple test server
  347. sub start_server {
  348. my $num = shift||1;
  349. die "A server already exists: @pids\n" unless @pids < $num;
  350. my $port = random_port();
  351. $ScriptName = "http://localhost:$port";
  352. AppendStringToFile($ConfigFile, "\$ScriptName = '$ScriptName';\n");
  353. my $pid = fork();
  354. if (!defined $pid) {
  355. die "Cannot fork: $!";
  356. } elsif ($pid == 0) {
  357. use Config;
  358. my $secure_perl_path = $Config{perlpath};
  359. exec($secure_perl_path, "stuff/server.pl", "./wiki.pl", $port) or die "Cannot exec: $!";
  360. } else {
  361. push(@pids, $pid);
  362. # give the server some time to start up
  363. sleep 1;
  364. }
  365. }
  366. # Fork a Mojolicious server
  367. sub start_mojolicious_server {
  368. my $num = shift||1;
  369. die "A server already exists: @pids\n" unless @pids < $num;
  370. my $port = random_port();
  371. my $listen = "http://127.0.0.1:$port";
  372. $ScriptName = "http://127.0.0.1:$port/wiki";
  373. AppendStringToFile($ConfigFile, "\$ScriptName = '$ScriptName';\n");
  374. my $pid = fork();
  375. if (!defined $pid) {
  376. die "Cannot fork: $!";
  377. } elsif ($pid == 0) {
  378. use Config;
  379. my $secure_perl_path = $Config{perlpath};
  380. exec($secure_perl_path, "server.pl", "daemon", "-l", $listen)
  381. or die "Cannot exec: $!";
  382. } else {
  383. push(@pids, $pid);
  384. # give the server some time to start up
  385. sleep 1;
  386. }
  387. }
  388. END {
  389. # kill servers
  390. for my $pid (@pids) {
  391. kill 'KILL', $pid or warn "Could not kill server $pid";
  392. }
  393. }
  394. sub RunAndTerminate { # runs a command for 1 second and then sends SIGTERM
  395. my $pid = fork();
  396. if (not $pid) { # child
  397. open(STDOUT, '>', '/dev/null'); # we don't want to see the output
  398. open(STDERR, '>', '/dev/null');
  399. exec(@_) or die "Cannot start a new process: $!";
  400. }
  401. # parent
  402. sleep 1;
  403. kill 'TERM', $pid;
  404. wait; # let it finish
  405. }
  406. sub AppendToConfig {
  407. my @data = @_; # one or more strings
  408. open(my $fh, '>>', "$DataDir/config") or die "Could not append to config file: $!";
  409. print $fh join("\n", @data);
  410. close $fh;
  411. }
  412. 1;