test.pl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. # Copyright (C) 2004–2015 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} = $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/^.*?(<html)/$1/s; # strip headers
  180. $page =~ s/^.*?<\?xml.*?>\s*//s; # strip xml processing
  181. my $page_shown = 0;
  182. my $parser = XML::LibXML->new();
  183. my $doc;
  184. my @result;
  185. SKIP: {
  186. eval { $doc = $parser->parse_html_string($page) };
  187. eval { $doc = $parser->parse_string($page) } if $@;
  188. skip("Cannot parse ".name($page).": $@", $#tests + 1) if $@;
  189. foreach my $test (@tests) {
  190. my $nodelist;
  191. # libxml2 is not aware of UTF8 flag
  192. eval { $nodelist = $doc->findnodes(encode_utf8($test)) };
  193. if ($@) {
  194. fail(&$check(1) ? "$test: $@" : "not $test: $@");
  195. } elsif (ok(&$check($nodelist->size()),
  196. name(&$check(1) ? $test : "not $test"))) {
  197. push(@result, $nodelist->string_value());
  198. } else {
  199. $page =~ s/^.*?<html/<html/s;
  200. diag($message, substr($page,0,30000)) unless $page_shown;
  201. $page_shown = 1;
  202. }
  203. }
  204. }
  205. return wantarray ? @result : $result[0]; # list or string of first result
  206. }
  207. sub xpath_test {
  208. xpath_do(sub { shift > 0; }, "No Matches\n", @_);
  209. }
  210. sub xpath_test_file {
  211. my ($file, @tests) = @_;
  212. if (open(F, '< :encoding(UTF-8)', $file)) {
  213. local $/ = undef;
  214. xpath_test(<F>, @tests);
  215. close(F);
  216. } else {
  217. warn "cannot open $file\n";
  218. }
  219. }
  220. sub negative_xpath_test {
  221. xpath_do(sub { shift == 0; }, "Unexpected Matches\n", @_);
  222. }
  223. # alias
  224. sub xpath_test_negative {
  225. return negative_xpath_test(@_);
  226. }
  227. sub xpath_run_tests {
  228. # translate embedded newlines (other backslashes remain untouched)
  229. my @tests = newlines(@_);
  230. my ($input, $output);
  231. while (($input, $output, @tests) = @tests) {
  232. my $result = apply_rules($input);
  233. xpath_test("<div>$result</div>", $output);
  234. }
  235. }
  236. sub xpath_run_tests_negative {
  237. # translate embedded newlines (other backslashes remain untouched)
  238. my @tests = newlines(@_);
  239. my ($input, $output);
  240. while (($input, $output, @tests) = @tests) {
  241. my $result = apply_rules($input);
  242. xpath_test_negative("<div>$result</div>", $output);
  243. }
  244. }
  245. sub remove_rule {
  246. my $rule = shift;
  247. my @list = ();
  248. my $found = 0;
  249. foreach my $item (@MyRules) {
  250. if ($item ne $rule) {
  251. push @list, $item;
  252. } else {
  253. $found = 1;
  254. }
  255. }
  256. die "Rule not found" unless $found;
  257. @MyRules = @list;
  258. }
  259. sub add_module {
  260. my ($mod, $subdir) = @_;
  261. $subdir .= '/' if $subdir and substr($subdir, -1) ne '/';
  262. mkdir $ModuleDir unless -d $ModuleDir;
  263. my $dir = `/bin/pwd`;
  264. chop($dir);
  265. if (-l "$ModuleDir/$mod") {
  266. # do nothing
  267. } elsif (eval{ symlink("$dir/modules/$subdir$mod", "$ModuleDir/$mod"); 1; }) {
  268. # do nothing
  269. } else {
  270. system('copy', "$dir/modules/$subdir$mod", "$ModuleDir/$mod");
  271. }
  272. die "Cannot symlink $mod: $!" unless -e "$ModuleDir/$mod";
  273. do "$ModuleDir/$mod";
  274. @MyRules = sort {$RuleOrder{$a} <=> $RuleOrder{$b}} @MyRules;
  275. }
  276. sub remove_module {
  277. my $mod = shift;
  278. mkdir $ModuleDir unless -d $ModuleDir;
  279. unlink("$ModuleDir/$mod") or die "Cannot unlink: $!";
  280. }
  281. sub write_config_file {
  282. open(F, '>:encoding(utf-8)', "$DataDir/config");
  283. print F "\$AdminPass = 'foo';\n";
  284. # this used to be the default in earlier CGI.pm versions
  285. print F "\$ScriptName = 'http://localhost/wiki.pl';\n";
  286. print F "\$SurgeProtection = 0;\n";
  287. close(F);
  288. $ScriptName = 'http://localhost/test.pl'; # different!
  289. $IndexInit = 0;
  290. %IndexHash = ();
  291. @IndexList = ();
  292. $InterSiteInit = 0;
  293. %InterSite = ();
  294. $NearSiteInit = 0;
  295. %NearSite = ();
  296. %NearSearch = ();
  297. }
  298. sub clear_pages {
  299. if (-f "/bin/rm") {
  300. system('/bin/rm', '-rf', $DataDir);
  301. } else {
  302. system('c:/cygwin/bin/rm.exe', '-rf', $DataDir);
  303. }
  304. die "Cannot remove '$DataDir'!\n" if -e $DataDir;
  305. mkdir $DataDir;
  306. if ($^O eq 'darwin') {
  307. # On a Mac we are probably using the HFS filesystem which uses NFD instead
  308. # of NFC for filenames. Since clear_pages runs as the very first thing, the
  309. # modules directory doesn't exist, yet. And as Init() hasn't run, $ModuleDir
  310. # is not set either. All we have is $DataDir.
  311. $ModuleDir = "$DataDir/modules";
  312. add_module('mac.pl');
  313. }
  314. write_config_file();
  315. }
  316. # Find an unused port
  317. sub random_port {
  318. use Errno qw( EADDRINUSE );
  319. use Socket qw( PF_INET SOCK_STREAM INADDR_ANY sockaddr_in );
  320. my $family = PF_INET;
  321. my $type = SOCK_STREAM;
  322. my $proto = getprotobyname('tcp') or die "getprotobyname: $!";
  323. my $host = INADDR_ANY; # Use inet_aton for a specific interface
  324. for my $i (1..3) {
  325. my $port = 1024 + int(rand(65535 - 1024));
  326. socket(my $sock, $family, $type, $proto) or die "socket: $!";
  327. my $name = sockaddr_in($port, $host) or die "sockaddr_in: $!";
  328. setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, 1);
  329. bind($sock, $name)
  330. and close($sock)
  331. and return $port;
  332. die "bind: $!" if $! != EADDRINUSE;
  333. print "Port $port in use, retrying...\n";
  334. }
  335. die "Tried 3 random ports and failed.\n"
  336. }
  337. my $pid;
  338. # Fork a simple test server
  339. sub start_server {
  340. die "A server already exists: $pid\n" if $pid;
  341. my $port = random_port();
  342. $ScriptName = "http://localhost:$port";
  343. AppendStringToFile($ConfigFile, "\$ScriptName = '$ScriptName';\n");
  344. $pid = fork();
  345. if (!defined $pid) {
  346. die "Cannot fork: $!";
  347. } elsif ($pid == 0) {
  348. use Config;
  349. my $secure_perl_path = $Config{perlpath};
  350. exec($secure_perl_path, "stuff/server.pl", "wiki.pl", $port) or die "Cannot exec: $!";
  351. }
  352. }
  353. # Fork a Mojolicious server
  354. sub start_mojolicious_server {
  355. die "A server already exists: $pid\n" if $pid;
  356. my $port = random_port();
  357. my $listen = "http://127.0.0.1:$port";
  358. $ScriptName = "http://127.0.0.1:$port/wiki";
  359. AppendStringToFile($ConfigFile, "\$ScriptName = '$ScriptName';\n");
  360. $pid = fork();
  361. if (!defined $pid) {
  362. die "Cannot fork: $!";
  363. } elsif ($pid == 0) {
  364. use Config;
  365. my $secure_perl_path = $Config{perlpath};
  366. exec($secure_perl_path, "server.pl", "daemon", "-l", $listen)
  367. or die "Cannot exec: $!";
  368. }
  369. }
  370. END {
  371. # kill server
  372. if ($pid) {
  373. kill 'KILL', $pid or warn "Could not kill server $pid";
  374. }
  375. }
  376. sub RunAndTerminate { # runs a command for 1 second and then sends SIGTERM
  377. my $pid = fork();
  378. if (not $pid) { # child
  379. open(STDOUT, '>', '/dev/null'); # we don't want to see the output
  380. open(STDERR, '>', '/dev/null');
  381. exec(@_) or die "Cannot start a new process: $!";
  382. }
  383. # parent
  384. sleep 1;
  385. kill 'TERM', $pid;
  386. wait; # let it finish
  387. }
  388. sub AppendToConfig {
  389. my @data = @_; # one or more strings
  390. open(my $fh, '>>', "$DataDir/config") or die "Could not append to config file: $!";
  391. print $fh join("\n", @data);
  392. close $fh;
  393. }
  394. 1;