1: <?php
2: declare(strict_types=1);
3: /**
4: * +------------------------------------------------------------+
5: * | apnscp |
6: * +------------------------------------------------------------+
7: * | Copyright (c) Apis Networks |
8: * +------------------------------------------------------------+
9: * | Licensed under Artistic License 2.0 |
10: * +------------------------------------------------------------+
11: * | Author: Matt Saladna (msaladna@apisnetworks.com) |
12: * +------------------------------------------------------------+
13: */
14:
15: /**
16: * Testing module for nothing more than unit testing
17: *
18: * @author Matt Saladna <matt@apisnetworks.com>
19: */
20: class Test_Module extends Module_Skeleton
21: {
22: public $exportedFunctions = array(
23: '*' => PRIVILEGE_ALL,
24: );
25:
26: public function __construct()
27: {
28: parent::__construct();
29: if (!is_debug()) {
30: $this->exportedFunctions = array('*' => PRIVILEGE_NONE);
31: }
32: }
33:
34: /**
35: * Benchmark all callable class methods
36: *
37: * @param int $iterations
38: * @param string $testclass
39: * @return boolean
40: */
41: public function benchmark_all($iterations = 1000, $testclass = null)
42: {
43: if (!is_debug()) {
44: return error('benchmark only enabled in dev');
45: }
46: $methods = array();
47: if (is_null($testclass)) {
48: $my_class = __CLASS__;
49: } else {
50: $my_class = $testclass;
51: }
52: if (!class_exists($my_class)) {
53: return error("Unknown class `%s'", $my_class);
54: }
55: $my_method = __FUNCTION__;
56: $rfxn = new ReflectionClass($my_class);
57: $maxlen = 0;
58: foreach ($rfxn->getMethods(ReflectionMethod::IS_PUBLIC) as $m) {
59: $method = $m->name;
60: $class = $m->class;
61: if ($class === $my_class && $method !== $my_method) {
62: $methods[] = $method;
63: $maxlen = max($maxlen, strlen($method));
64: }
65: }
66: $header = 'Module ' . $my_class;
67: print $header . "\n" .
68: str_repeat('=', strlen($header)) . "\n";
69: foreach ($methods as $m) {
70: printf('%-' . $maxlen . 's: ', $m);
71: $start = microtime(true);
72: for ($i = 0, $n = $iterations; $i < $n; $i++) {
73: assert($ret = $this->$m());
74: }
75: $end = microtime(true);
76: $diff = ($end - $start);
77: printf("%.4fs (%.6fs)\n", $diff, $diff / $iterations);
78: }
79: print "\n";
80:
81: return true;
82: }
83:
84: /**
85: * Test execution using named arguments
86: *
87: * @return bool
88: */
89: public function exec_named_args()
90: {
91: $args = array(
92: 'program' => 'echo',
93: 'args' => 'Hello World!'
94: );
95: $proc = Util_Process_Safe::exec('%(program)s %(args)s %(args)s',
96: $args, array(1, 0)
97: );
98:
99: return $proc['success'];
100: }
101:
102: /**
103: * Test execution without named arguments
104: *
105: * @return bool
106: */
107: public function exec_no_args()
108: {
109: $proc = Util_Process::exec('echo "Hello"',
110: array(0)
111: );
112:
113: return $proc['success'];
114: }
115:
116: public function exec_fail()
117: {
118: $proc = Util_Process::exec('/bin/true', array(1));
119:
120: return $proc['success'] == false;
121: }
122:
123: /**
124: * -----------------------------------
125: *
126: * crap tested that I forgot to remove
127: *
128: * -----------------------------------
129: */
130:
131: public function exec_additional_args()
132: {
133: $args = array('echo', "'Hello World!'", 'test');
134: // should emit a warning
135: $proc = Util_Process::exec('%s %s', 'echo', "'Hello World!'", 'Test');
136:
137: return $proc['success'];
138: }
139:
140: public function exec_quotes()
141: {
142: $args = array("'Hello World!'", 'test');
143: // should emit a warning
144: $proc = Util_Process_Safe::exec('echo %s %s', $args);
145: print $proc['stdout'];
146:
147: return $proc['success'];
148: }
149:
150: /**
151: * Profile backend performance
152: */
153:
154: public function backend_performance($n = 10000)
155: {
156: /**
157: * Journal of cmd -d debug.com test_backend_performance
158: * 2016/09/08: 0.1031ms
159: * 2017/04/10: 0.1016ms
160: * 2017/05/15: 0.1092ms
161: * 2017/06/19: 0.0966ms <-- last i5 760
162: * 2017/06/19: 0.1201ms <-- L3426
163: * 2017/10/14: 0.1220ms
164: * 2017/11/26: 0.1286ms
165: * 2018/02/12: 0.1327ms <-- Contextables
166: * 2018/09/15: 0.1757ms <-- VM
167: * 2018/11/14: 0.1685ms <-- merge accept, no timeout
168: * 2019/08/25: 0.1655ms <-- periodic update, same VM
169: * 2020/07/13: 0.1014ms <-- Ryzen 5 3600
170: * 2023/11/05: 0.1735ms <-- Last Ryzen 5 usage, HyperV
171: * 2023/11/05: 0.1507ms <-- Ryzen 7 5700X
172: *
173: */
174: return $this->benchmark('test_backend_emitter', $n);
175: }
176:
177: /**
178: * Benchmark apnscp function
179: *
180: * @param Closure|string $func fully-qualified function
181: * @param int $iterations
182: * @return float
183: */
184: public function benchmark($func, int $iterations = 1000)
185: {
186: if (!is_debug()) {
187: return error('benchmark only enabled in dev');
188: }
189: if ($func instanceof \Closure) {
190: $fname = (new ReflectionFunction($func))->getName();
191: } else if (is_callable(array($this, $func))) {
192: $fname = $func;
193: $func = [$this, $func];
194: }
195: if (!is_callable($func)) {
196: return error("function `%s' is not callable", $fname);
197: }
198: gc_collect_cycles();
199: gc_mem_caches();
200: $bm = static function (Callable $func, string $fname) use ($iterations) {
201: $mem = memory_get_usage();
202: print 'benchmark ' . $fname . "\r\n";
203: $start = microtime(true);
204: for ($i = 0; $i < $iterations; $i++) {
205: //assert(call_user_func($func) == $resp);
206: $func();
207: }
208: $end = microtime(true);
209: $delta = $end - $start;
210: printf("time: %.2f sec (%d rounds; %.4f ms each; %.2f per second)\n\n",
211: $delta,
212: $iterations,
213: $delta / $iterations * 1000,
214: $iterations / $delta
215: );
216: printf("Mem usage: %.2f bytes\n", memory_get_usage() - $mem);
217:
218: return $delta;
219: };
220: $bmf = $bm($func, $fname);
221: $bm = null;
222:
223: return $bmf;
224: }
225:
226: /**
227: * Frontend emitter to evaluate backend performance
228: *
229: * @param null $args
230: * @return mixed
231: */
232: public function backend_emitter($args = '')
233: {
234: //return $this->query('test_backend_collector', $args);
235: /*$ds = \DataStream::get();
236: $ds->setOption(\apnscpObject::RESET);
237: $payload = $ds->pack('test_backend_collector', $args, null, Auth::get_driver()->getID());
238: return $ds->writeSocket($payload);*/
239: return $this->query('test_backend_collector', $args);
240: }
241:
242: /**
243: * Backend collector to evaluate performance
244: *
245: * @param null $args
246: * @return mixed
247: */
248: public function backend_collector($args = '')
249: {
250: return $args;
251: }
252:
253: public function config_bm()
254: {
255:
256: $this->compare('test_config', 'test_config2');
257: }
258:
259: /**
260: * Comparatively benchmark functions
261: *
262: * @param string $func1
263: * @param string $func2
264: * @param int $iterations
265: * @return int 1 if $func1 faster than $func2, -1 vice-versa
266: */
267: public function compare($func1, $func2, $iterations = 1000)
268: {
269: if (!is_debug()) {
270: return error('benchmark only enabled in dev');
271: }
272:
273: if (!$func1 || !$func2) {
274: return error('need 2 functions to compare');
275: }
276: $mem = memory_get_usage();
277: $bmf1 = $this->benchmark($func1, $iterations);
278: $bmf2 = $this->benchmark($func2, $iterations);
279: $ret = 0;
280: if ($bmf1 < $bmf2) {
281: printf('%s is quicker than %s ', $func1, $func2);
282: $diff = abs($bmf1 - $bmf2);
283: $diffp = $diff / $bmf2;
284: $ret = 1;
285: } else {
286: printf('%s is quicker than %s ', $func2, $func1);
287: $diff = abs($bmf2 - $bmf1);
288: $diffp = $diff / $bmf1;
289: $ret = -1;
290: }
291: printf("by %.2f%%\n\n", $diffp * 100);
292: printf("Mem usage: %.2f bytes\n", memory_get_usage() - $mem);
293:
294: return;
295: }
296:
297: public function config($conf = null)
298: {
299: $conf = $this->getAuthContext()->conf('ipinfo');
300:
301: //array_unshift($conf, '[DEFAULT]');
302: return "[DEFAULT]\n" . Util_Conf::build_ini($conf);
303: }
304:
305: public function config2($conf = null)
306: {
307: $conf = $this->getAuthContext()->conf('ipinfo');
308: $data = '[DEFAULT]' . "\n";
309: foreach ($conf as $srvc_var => $srvc_val) {
310: $data .= $srvc_var . ' = ' . (!is_array($srvc_val) ? $srvc_val : (!$srvc_val ? '[]' : '[\'' . implode('\', \'',
311: array_unique($srvc_val)) . '\']')) . "\n";
312: }
313:
314: return $data;
315: }
316:
317: public function sudo()
318: {
319: if (!IS_CLI) {
320: return $this->query('test_sudo');
321: }
322: $args = array('user' => 'debug');
323: if ($this->permission_level & PRIVILEGE_ADMIN) {
324: $args['domain'] = 'debug.com';
325: }
326: $ret = Util_Process_Sudo::exec('id',
327: $args);
328:
329: return $ret;
330: }
331:
332: public function fn_decompose($cmd)
333: {
334: return Util_Process::decompose($cmd);
335: }
336:
337: public function mail()
338: {
339: $address = $this->common_get_email() ?? Crm_Module::COPY_ADMIN;
340: $template = \BladeLite::factory('views/email');
341: $html = $template->make('simple',
342: [
343: 'msg' => 'This is a test email from your panel!' .
344: "\n\nPanel login source: " . \Auth_Redirect::getPreferredUri()
345: ]
346: )->render();
347:
348: $opts = array(
349: 'html_charset' => 'utf-8',
350: 'text_charset' => 'utf-8'
351: );
352: $from = \Crm_Module::FROM_NAME . ' <' . \Crm_Module::FROM_ADDRESS . '>';
353: $headers = array(
354: 'Sender' => $from,
355: 'From' => $from
356: );
357: $mime = new Mail_Mime($opts);
358:
359: $mime->setHTMLBody($html);
360: $mime->setTXTBody(strip_tags($html));
361: $headers = $mime->txtHeaders($headers);
362: $msg = $mime->get();
363:
364: return Mail::send(
365: $address,
366: PANEL_BRAND . ' test',
367: $msg,
368: $headers
369: );
370:
371: return info("Sent test email to `%s'", $address);
372: }
373:
374: /**
375: * ER wrapper
376: *
377: * @param string $class
378: * @param string $confirmation
379: * @return bool|void
380: */
381: public function message_class(string $class, string $confirmation = 'Hello!') {
382: if (!\in_array($class, ['fatal', 'error', 'warning', 'info'], true)) {
383: return error("Unknown message class `%s'", $class);
384: }
385: return $class($confirmation);
386: }
387:
388: /**
389: * Sleep backend for duration
390: *
391: * @param int $time
392: * @return bool
393: */
394: public function sleep(int $time = 10)
395: {
396: if (!IS_CLI) {
397: return $this->query('test_sleep', $time);
398: }
399: sleep($time);
400:
401: return true;
402: }
403:
404: /**
405: * Get locale-specific time
406: *
407: * @return false|string
408: */
409: public function now()
410: {
411: return date('r');
412: }
413:
414: public function context_performance()
415: {
416: /**
417: * 2019/01/29: 360 req/sec
418: */
419: if (!$this->permission_level & PRIVILEGE_SITE) {
420: return error('Context requires site admin privileges');
421: }
422:
423: $oldrep = \Error_Reporter::set_verbose(0);
424: $user = \Opcenter\Auth\Password::generate(8, 'a-z');
425: if (!$this->user_add($user, 'randompassword12345')) {
426: return error('Failed to create user');
427: }
428: \assert(spl_object_hash(\Auth::context($user, $this->site)) !== spl_object_hash(\Auth::context($user, $this->site)), 'Context uniqueness');
429: $this->benchmark(function () use ($user) {
430: \Auth::context($user, $this->site);
431: });
432: $this->user_delete($user);
433: \Error_Reporter::clear_buffer();
434: \Error_Reporter::set_verbose($oldrep);
435: }
436:
437: public function metrics(string $attr = null)
438: {
439: if (!TELEMETRY_ENABLED) {
440: return error('[telemetry] => enabled is off in config.ini');
441: }
442:
443: $pg = PostgreSQL::pdo();
444: $query = "SELECT TIME_BUCKET('5 minute', ts), name, label, value FROM metrics " .
445: 'JOIN metric_attributes USING (attr_id) WHERE site_id = ' . $this->site_id . " AND ts >= NOW() - '1 day'::INTERVAL";
446: if ($attr) {
447: $query .= ' AND name = ' . $pg->quote($attr);
448: }
449: $rs = $pg->query($query);
450: return $rs->fetchAll(PDO::FETCH_ASSOC);
451: }
452:
453: public function sigchld_exit(int $code, bool $backend = true): int
454: {
455: if ($backend && !IS_CLI) {
456: return $this->query('test_sigchld_exit', $code, $backend);
457: }
458:
459: $ret = \Util_Process_Safe::exec('exit %d', [$code]);
460: return $ret['return'];
461: }
462: }