1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Tester\Runner;
9:
10: use Tester,
11: Tester\Dumper,
12: Tester\Helpers;
13:
14:
15: 16: 17: 18: 19:
20: class TestHandler
21: {
22: const HTTP_OK = 200;
23:
24:
25: private $runner;
26:
27:
28: public function __construct(Runner $runner)
29: {
30: $this->runner = $runner;
31: }
32:
33:
34: 35: 36:
37: public function initiate($file)
38: {
39: list($annotations, $testName) = $this->getAnnotations($file);
40: $php = clone $this->runner->getPhp();
41: $job = FALSE;
42:
43: foreach (get_class_methods($this) as $method) {
44: if (!preg_match('#^initiate(.+)#', strtolower($method), $m) || !isset($annotations[$m[1]])) {
45: continue;
46: }
47: foreach ((array) $annotations[$m[1]] as $arg) {
48: $res = $this->$method($arg, $php, $file);
49: if ($res === TRUE) {
50: $job = TRUE;
51: } elseif ($res) {
52: $this->runner->writeResult($testName, $res[0], $res[1]);
53: return;
54: }
55: }
56: }
57:
58: if (!$job) {
59: $this->runner->addJob(new Job($file, $php));
60: }
61: }
62:
63:
64: 65: 66:
67: public function assess(Job $job)
68: {
69: list($annotations, $testName) = $this->getAnnotations($job->getFile());
70: $testName .= (strlen($job->getArguments()) ? " [{$job->getArguments()}]" : '');
71: $annotations += array(
72: 'exitcode' => Job::CODE_OK,
73: 'httpcode' => self::HTTP_OK,
74: );
75:
76: foreach (get_class_methods($this) as $method) {
77: if (!preg_match('#^assess(.+)#', strtolower($method), $m) || !isset($annotations[$m[1]])) {
78: continue;
79: }
80: foreach ((array) $annotations[$m[1]] as $arg) {
81: if ($res = $this->$method($job, $arg)) {
82: $this->runner->writeResult($testName, $res[0], $res[1]);
83: return;
84: }
85: }
86: }
87: $this->runner->writeResult($testName, Runner::PASSED);
88: }
89:
90:
91: private function initiateSkip($message)
92: {
93: return array(Runner::SKIPPED, $message);
94: }
95:
96:
97: private function initiatePhpVersion($version, PhpExecutable $php)
98: {
99: if (preg_match('#^(<=|<|==|=|!=|<>|>=|>)?\s*(.+)#', $version, $matches)
100: && version_compare($matches[2], $php->getVersion(), $matches[1] ?: '>='))
101: {
102: return array(Runner::SKIPPED, "Requires PHP $version.");
103: }
104: }
105:
106:
107: private function initiatePhpIni($value, PhpExecutable $php)
108: {
109: $php->arguments .= ' -d ' . Helpers::escapeArg($value);
110: }
111:
112:
113: private function initiateDataProvider($provider, PhpExecutable $php, $file)
114: {
115: try {
116: list($dataFile, $query, $optional) = Tester\DataProvider::parseAnnotation($provider, $file);
117: $data = Tester\DataProvider::load($dataFile, $query);
118: } catch (\Exception $e) {
119: return array(empty($optional) ? Runner::FAILED : Runner::SKIPPED, $e->getMessage());
120: }
121:
122: foreach (array_keys($data) as $item) {
123: $this->runner->addJob(new Job($file, $php, Helpers::escapeArg($item) . ' ' . Helpers::escapeArg($dataFile)));
124: }
125: return TRUE;
126: }
127:
128:
129: private function initiateMultiple($count, PhpExecutable $php, $file)
130: {
131: foreach (range(0, (int) $count - 1) as $arg) {
132: $this->runner->addJob(new Job($file, $php, (string) $arg));
133: }
134: return TRUE;
135: }
136:
137:
138: private function initiateTestCase($foo, PhpExecutable $php, $file)
139: {
140: $job = new Job($file, $php, Helpers::escapeArg(Tester\TestCase::LIST_METHODS));
141: $job->run();
142:
143: if (in_array($job->getExitCode(), array(Job::CODE_ERROR, Job::CODE_FAIL, Job::CODE_SKIP))) {
144: return array($job->getExitCode() === Job::CODE_SKIP ? Runner::SKIPPED : Runner::FAILED, $job->getOutput());
145: }
146:
147: $methods = json_decode(strrchr($job->getOutput(), '['));
148: if (!is_array($methods)) {
149: return array(Runner::FAILED, "Cannot list TestCase methods in file '$file'. Do you call TestCase::run() in it?");
150: } elseif (!$methods) {
151: return array(Runner::SKIPPED, "TestCase in file '$file' does not contain test methods.");
152: }
153:
154: foreach ($methods as $method) {
155: $this->runner->addJob(new Job($file, $php, Helpers::escapeArg($method)));
156: }
157: return TRUE;
158: }
159:
160:
161: private function assessExitCode(Job $job, $code)
162: {
163: $code = (int) $code;
164: if ($job->getExitCode() === Job::CODE_SKIP) {
165: $lines = explode("\n", trim($job->getOutput()));
166: return array(Runner::SKIPPED, end($lines));
167:
168: } elseif ($job->getExitCode() !== $code) {
169: $message = $job->getExitCode() !== Job::CODE_FAIL ? "Exited with error code {$job->getExitCode()} (expected $code)" : '';
170: return array(Runner::FAILED, trim($message . "\n" . $job->getOutput()));
171: }
172: }
173:
174:
175: private function assessHttpCode(Job $job, $code)
176: {
177: if (!$this->runner->getPhp()->isCgi()) {
178: return;
179: }
180: $headers = $job->getHeaders();
181: $actual = isset($headers['Status']) ? (int) $headers['Status'] : self::HTTP_OK;
182: $code = (int) $code;
183: if ($code && $code !== $actual) {
184: return array(Runner::FAILED, "Exited with HTTP code $actual (expected $code)");
185: }
186: }
187:
188:
189: private function assessOutputMatchFile(Job $job, $file)
190: {
191: $file = dirname($job->getFile()) . DIRECTORY_SEPARATOR . $file;
192: if (!is_file($file)) {
193: return array(Runner::FAILED, "Missing matching file '$file'.");
194: }
195: return $this->assessOutputMatch($job, file_get_contents($file));
196: }
197:
198:
199: private function assessOutputMatch(Job $job, $content)
200: {
201: if (!Tester\Assert::isMatching($content, $job->getOutput())) {
202: Dumper::saveOutput($job->getFile(), $job->getOutput(), '.actual');
203: Dumper::saveOutput($job->getFile(), $content, '.expected');
204: return array(Runner::FAILED, 'Failed: output should match ' . Dumper::toLine(rtrim($content)));
205: }
206: }
207:
208:
209: private function getAnnotations($file)
210: {
211: $annotations = Helpers::parseDocComment(file_get_contents($file));
212: $testName = (isset($annotations[0]) ? preg_replace('#^TEST:\s*#i', '', $annotations[0]) . ' | ' : '')
213: . implode(DIRECTORY_SEPARATOR, array_slice(explode(DIRECTORY_SEPARATOR, $file), -3));
214: return array($annotations, $testName);
215: }
216:
217: }
218: