From 19680f886f02fd8fe4557e6146f927aaa6782d05 Mon Sep 17 00:00:00 2001 From: Max Semenik Date: Fri, 5 Feb 2021 23:31:31 +0300 Subject: run-tests.php: move JUnit stuff into a class This is part one of my work that was announced at https://externals.io/message/110391 Closes GH-6671. --- run-tests.php | 523 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 260 insertions(+), 263 deletions(-) (limited to 'run-tests.php') diff --git a/run-tests.php b/run-tests.php index 51758ce507..57751309c4 100755 --- a/run-tests.php +++ b/run-tests.php @@ -25,10 +25,16 @@ /* $Id$ */ +/* Temporary variables while this file is being refactored. */ +/** @var ?JUnit */ +$junit = null; + +/* End temporary variables. */ + /* Let there be no top-level code beyond this point: * Only functions and classes, thanks! * - * Minimum required PHP version: 7.1.0 + * Minimum required PHP version: 7.4.0 */ function show_usage(): void @@ -156,6 +162,10 @@ function main(): void global $workers, $workerID; global $context_line_count; + // Temporary for the duration of refactoring + /** @var JUnit */ + global $junit; + define('IS_WINDOWS', substr(PHP_OS, 0, 3) == "WIN"); $workerID = 0; @@ -247,7 +257,7 @@ function main(): void $DETAILED = 0; } - junit_init(); + $junit = new JUnit($environment, $workerID); if (getenv('SHOW_ONLY_GROUPS')) { $SHOW_ONLY_GROUPS = explode(",", getenv('SHOW_ONLY_GROUPS')); @@ -772,7 +782,7 @@ function main(): void save_or_mail_results(); } - junit_save_xml(); + $junit->saveXML(); if (getenv('REPORT_EXIT_STATUS') !== '0' && getenv('REPORT_EXIT_STATUS') !== 'no' && ($sum_results['FAILED'] || $sum_results['LEAKED'])) { exit(1); @@ -1364,6 +1374,8 @@ function run_all_tests_parallel(array $test_files, array $env, $redir_tested): v { global $workers, $test_idx, $test_cnt, $test_results, $failed_tests_file, $result_tests_file, $PHP_FAILED_TESTS, $shuffle, $SHOW_ONLY_GROUPS, $valgrind; + global $junit; + // The PHP binary running run-tests.php, and run-tests.php itself // This PHP executable is *not* necessarily the same as the tested version $thisPHP = PHP_BINARY; @@ -1562,9 +1574,7 @@ escape: } } } - if (junit_enabled()) { - junit_merge_results($message["junit"]); - } + $junit->mergeResults($message["junit"]); // no break case "ready": // Schedule sequential tests only once we are down to one worker. @@ -1704,6 +1714,8 @@ function run_worker(): void { global $workerID, $workerSock; + global $junit; + $sockUri = getenv("TEST_PHP_URI"); $workerSock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri"); @@ -1750,9 +1762,9 @@ function run_worker(): void run_all_tests($command["test_files"], $command["env"], $command["redir_tested"]); send_message($workerSock, [ "type" => "tests_finished", - "junit" => junit_enabled() ? $GLOBALS['JUNIT'] : null, + "junit" => $junit->isEnabled() ? $junit : null, ]); - junit_init(); + //junit_init(); TODO is this needed? break; default: send_message($workerSock, [ @@ -1789,9 +1801,11 @@ function show_file_block(string $file, string $block, ?string $section = null): } function skip_test(string $tested, string $tested_file, string $shortname, string $reason) { + global $junit; + show_result('SKIP', $tested, $tested_file, "reason: $reason"); - junit_init_suite(junit_get_suitename_for($shortname)); - junit_mark_test_as('SKIP', $shortname, $tested, 0, $reason); + $junit->initSuite($junit->getSuiteName($shortname)); + $junit->markTestAs('SKIP', $shortname, $tested, 0, $reason); return 'SKIPPED'; } @@ -1814,6 +1828,11 @@ function run_test(string $php, $file, array $env): string global $num_repeats; // Parallel testing global $workerID; + + // Temporary + /** @var JUnit */ + global $junit; + $temp_filenames = null; $org_file = $file; @@ -1963,7 +1982,7 @@ TEST $file 'info' => "$bork_info [$file]", ]; - junit_mark_test_as('BORK', $shortname, $tested_file, 0, $bork_info); + $junit->markTestAs('BORK', $shortname, $tested_file, 0, $bork_info); return 'BORKED'; } @@ -2208,14 +2227,14 @@ TEST $file $env['ZEND_DONT_UNLOAD_MODULES'] = 1; } - junit_start_timer($shortname); + $junit->startTimer($shortname); $startTime = microtime(true); $output = system_with_timeout("$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=1 -d display_startup_errors=0 \"$test_skipif\"", $env); $output = trim($output); $time = microtime(true) - $startTime; - junit_finish_timer($shortname); + $junit->stopTimer($shortname); if ($time > $slow_min_ms / 1000) { $PHP_FAILED_TESTS['SLOW'][] = [ @@ -2243,7 +2262,7 @@ TEST $file } $message = !empty($m[1]) ? $m[1] : ''; - junit_mark_test_as('SKIP', $shortname, $tested, null, $message); + $junit->markTestAs('SKIP', $shortname, $tested, null, $message); return 'SKIPPED'; } @@ -2265,7 +2284,7 @@ TEST $file 'info' => "$output [$file]", ]; - junit_mark_test_as('BORK', $shortname, $tested, null, $output); + $junit->markTestAs('BORK', $shortname, $tested, null, $output); return 'BORKED'; } } @@ -2276,7 +2295,7 @@ TEST $file || array_key_exists("DEFLATE_POST", $section_text))) { $message = "ext/zlib required"; show_result('SKIP', $tested, $tested_file, "reason: $message", $temp_filenames); - junit_mark_test_as('SKIP', $shortname, $tested, null, $message); + $junit->markTestAs('SKIP', $shortname, $tested, null, $message); return 'SKIPPED'; } @@ -2316,7 +2335,7 @@ TEST $file // a redirected test never fails $IN_REDIRECT = false; - junit_mark_test_as('PASS', $shortname, $tested); + $junit->markTestAs('PASS', $shortname, $tested); return 'REDIR'; } else { $bork_info = "Redirect info must contain exactly one TEST string to be used as redirect directory."; @@ -2346,7 +2365,7 @@ TEST $file 'info' => "$bork_info [$file]", ]; - junit_mark_test_as('BORK', $shortname, $tested, null, $bork_info); + $junit->markTestAs('BORK', $shortname, $tested, null, $bork_info); return 'BORKED'; } @@ -2417,7 +2436,7 @@ TEST $file $env['REQUEST_METHOD'] = 'POST'; if (empty($request)) { - junit_mark_test_as('BORK', $shortname, $tested, null, 'empty $request'); + $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request'); return 'BORKED'; } @@ -2448,7 +2467,7 @@ TEST $file $env['REQUEST_METHOD'] = 'PUT'; if (empty($request)) { - junit_mark_test_as('BORK', $shortname, $tested, null, 'empty $request'); + $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request'); return 'BORKED'; } @@ -2525,13 +2544,13 @@ COMMAND $cmd "; } - junit_start_timer($shortname); + $junit->startTimer($shortname); $hrtime = hrtime(); $startTime = $hrtime[0] * 1000000000 + $hrtime[1]; $out = system_with_timeout($cmd, $env, $section_text['STDIN'] ?? null, $captureStdIn, $captureStdOut, $captureStdErr); - junit_finish_timer($shortname); + $junit->stopTimer($shortname); $hrtime = hrtime(); $time = $hrtime[0] * 1000000000 + $hrtime[1] - $startTime; if ($time >= $slow_min_ms * 1000000) { @@ -2720,7 +2739,7 @@ COMMAND $cmd $info = " (warn: XLEAK section but test passes)"; } else { show_result("PASS", $tested, $tested_file, '', $temp_filenames); - junit_mark_test_as('PASS', $shortname, $tested); + $junit->markTestAs('PASS', $shortname, $tested); return 'PASSED'; } } @@ -2748,7 +2767,7 @@ COMMAND $cmd $info = " (warn: XLEAK section but test passes)"; } else { show_result("PASS", $tested, $tested_file, '', $temp_filenames); - junit_mark_test_as('PASS', $shortname, $tested); + $junit->markTestAs('PASS', $shortname, $tested); return 'PASSED'; } } @@ -2870,7 +2889,7 @@ SH; $diff = empty($diff) ? '' : preg_replace('/\e/', '', $diff); - junit_mark_test_as($restype, $shortname, $tested, null, $info, $diff); + $junit->markTestAs($restype, $shortname, $tested, null, $info, $diff); return $restype[0] . 'ED'; } @@ -3413,303 +3432,281 @@ function show_result( } -function junit_init(): void +class JUnit { - // Check whether a junit log is wanted. - global $workerID; - $JUNIT = getenv('TEST_PHP_JUNIT'); - if (empty($JUNIT)) { - $GLOBALS['JUNIT'] = false; - return; - } - if ($workerID) { - $fp = null; - } elseif (!$fp = fopen($JUNIT, 'w')) { - error("Failed to open $JUNIT for writing."); - } - $GLOBALS['JUNIT'] = [ - 'fp' => $fp, - 'name' => 'PHP', + private bool $enabled = true; + private $fp = null; + private array $suites = []; + private array $rootSuite = self::EMPTY_SUITE + ['name' => 'php']; + + private const EMPTY_SUITE = [ 'test_total' => 0, 'test_pass' => 0, 'test_fail' => 0, 'test_error' => 0, 'test_skip' => 0, 'test_warn' => 0, + 'files' => [], 'execution_time' => 0, - 'suites' => [], - 'files' => [] ]; -} -function junit_save_xml(): void -{ - global $JUNIT; - if (!junit_enabled()) { - return; + public function __construct(array $env, int $workerID) + { + // Check whether a junit log is wanted. + $fileName = $env['TEST_PHP_JUNIT'] ?? null; + if (empty($fileName)) { + $this->enabled = false; + return; + } + if (!$workerID && !$this->fp = fopen($fileName, 'w')) { + throw new Exception("Failed to open $fileName for writing."); + } } - $xml = '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>' . PHP_EOL; - $xml .= sprintf( - '' . PHP_EOL, - $JUNIT['name'], - $JUNIT['test_total'], - $JUNIT['test_fail'], - $JUNIT['test_error'], - $JUNIT['test_skip'], - $JUNIT['execution_time'] - ); - $xml .= junit_get_suite_xml(); - $xml .= ''; - fwrite($JUNIT['fp'], $xml); -} + public function isEnabled(): bool + { + return $this->enabled; + } -function junit_get_suite_xml(string $suite_name = ''): string -{ - global $JUNIT; - - $result = ""; - - foreach ($JUNIT['suites'] as $suite_name => $suite) { - $result .= sprintf( - '' . PHP_EOL, - $suite['name'], - $suite['test_total'], - $suite['test_fail'], - $suite['test_error'], - $suite['test_skip'], - $suite['execution_time'] + public function saveXML(): void + { + if (!$this->enabled) { + return; + } + + $xml = '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>' . PHP_EOL; + $xml .= sprintf( + '' . PHP_EOL, + $this->rootSuite['name'], + $this->rootSuite['test_total'], + $this->rootSuite['test_fail'], + $this->rootSuite['test_error'], + $this->rootSuite['test_skip'], + $this->rootSuite['execution_time'] ); + $xml .= $this->getSuitesXML(); + $xml .= ''; + fwrite($this->fp, $xml); + } - if (!empty($suite_name)) { - foreach ($suite['files'] as $file) { - $result .= $JUNIT['files'][$file]['xml']; + private function getSuitesXML(string $suite_name = '') + { + // FIXME: $suite_name gets overwritten + $result = ''; + + foreach ($this->suites as $suite_name => $suite) { + $result .= sprintf( + '' . PHP_EOL, + $suite['name'], + $suite['test_total'], + $suite['test_fail'], + $suite['test_error'], + $suite['test_skip'], + $suite['execution_time'] + ); + + if (!empty($suite_name)) { + foreach ($suite['files'] as $file) { + $result .= $this->rootSuite['files'][$file]['xml']; + } } + + $result .= '' . PHP_EOL; } - $result .= '' . PHP_EOL; + return $result; } - return $result; -} - -function junit_enabled(): bool -{ - global $JUNIT; - return !empty($JUNIT); -} + public function markTestAs( + $type, + string $file_name, + string $test_name, + ?int $time = null, + string $message = '', + string $details = '' + ): void { + if (!$this->enabled) { + return; + } -/** - * @param array|string $type - */ -function junit_mark_test_as( - $type, - string $file_name, - string $test_name, - ?int $time = null, - string $message = '', - string $details = '' -): void { - global $JUNIT; - if (!junit_enabled()) { - return; - } + $suite = $this->getSuiteName($file_name); - $suite = junit_get_suitename_for($file_name); + $this->record($suite, 'test_total'); - junit_suite_record($suite, 'test_total'); + $time = $time ?? $this->getTimer($file_name); + $this->record($suite, 'execution_time', $time); - $time = $time ?? junit_get_timer($file_name); - junit_suite_record($suite, 'execution_time', $time); + $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8'); + $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function ($c) { + return sprintf('[[0x%02x]]', ord($c[0])); + }, $escaped_details); + $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); - $escaped_details = htmlspecialchars($details, ENT_QUOTES, 'UTF-8'); - $escaped_details = preg_replace_callback('/[\0-\x08\x0B\x0C\x0E-\x1F]/', function (array $c): string { - return sprintf('[[0x%02x]]', ord($c[0])); - }, $escaped_details); - $escaped_message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); + $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES); + $this->rootSuite['files'][$file_name]['xml'] = "\n"; - $escaped_test_name = htmlspecialchars($file_name . ' (' . $test_name . ')', ENT_QUOTES); - $JUNIT['files'][$file_name]['xml'] = "\n"; + if (is_array($type)) { + $output_type = $type[0] . 'ED'; + $temp = array_intersect(['XFAIL', 'XLEAK', 'FAIL', 'WARN'], $type); + $type = reset($temp); + } else { + $output_type = $type . 'ED'; + } + + if ('PASS' == $type || 'XFAIL' == $type || 'XLEAK' == $type) { + $this->record($suite, 'test_pass'); + } elseif ('BORK' == $type) { + $this->record($suite, 'test_error'); + $this->rootSuite['files'][$file_name]['xml'] .= "\n"; + } elseif ('SKIP' == $type) { + $this->record($suite, 'test_skip'); + $this->rootSuite['files'][$file_name]['xml'] .= "$escaped_message\n"; + } elseif ('WARN' == $type) { + $this->record($suite, 'test_warn'); + $this->rootSuite['files'][$file_name]['xml'] .= "$escaped_message\n"; + } elseif ('FAIL' == $type) { + $this->record($suite, 'test_fail'); + $this->rootSuite['files'][$file_name]['xml'] .= "$escaped_details\n"; + } else { + $this->record($suite, 'test_error'); + $this->rootSuite['files'][$file_name]['xml'] .= "$escaped_details\n"; + } - if (is_array($type)) { - $output_type = $type[0] . 'ED'; - $temp = array_intersect(['XFAIL', 'XLEAK', 'FAIL', 'WARN'], $type); - $type = reset($temp); - } else { - $output_type = $type . 'ED'; - } - - if ('PASS' == $type || 'XFAIL' == $type || 'XLEAK' == $type) { - junit_suite_record($suite, 'test_pass'); - } elseif ('BORK' == $type) { - junit_suite_record($suite, 'test_error'); - $JUNIT['files'][$file_name]['xml'] .= "\n"; - } elseif ('SKIP' == $type) { - junit_suite_record($suite, 'test_skip'); - $JUNIT['files'][$file_name]['xml'] .= "$escaped_message\n"; - } elseif ('WARN' == $type) { - junit_suite_record($suite, 'test_warn'); - $JUNIT['files'][$file_name]['xml'] .= "$escaped_message\n"; - } elseif ('FAIL' == $type) { - junit_suite_record($suite, 'test_fail'); - $JUNIT['files'][$file_name]['xml'] .= "$escaped_details\n"; - } else { - junit_suite_record($suite, 'test_error'); - $JUNIT['files'][$file_name]['xml'] .= "$escaped_details\n"; + $this->rootSuite['files'][$file_name]['xml'] .= "\n"; } - $JUNIT['files'][$file_name]['xml'] .= "\n"; -} + private function record(string $suite, string $param, $value = 1): void + { + $this->rootSuite[$param] += $value; + $this->suites[$suite][$param] += $value; + } -function junit_suite_record(string $suite, string $param, int $value = 1): void -{ - global $JUNIT; + private function getTimer(string $file_name) + { + if (!$this->enabled) { + return 0; + } - $JUNIT[$param] += $value; - $JUNIT['suites'][$suite][$param] += $value; -} + if (isset($this->rootSuite['files'][$file_name]['total'])) { + return number_format($this->rootSuite['files'][$file_name]['total'], 4); + } -function junit_get_timer(string $file_name): int -{ - global $JUNIT; - if (!junit_enabled()) { return 0; } - if (isset($JUNIT['files'][$file_name]['total'])) { - return number_format($JUNIT['files'][$file_name]['total'], 4); - } + public function startTimer(string $file_name): void + { + if (!$this->enabled) { + return; + } - return 0; -} + if (!isset($this->rootSuite['files'][$file_name]['start'])) { + $this->rootSuite['files'][$file_name]['start'] = microtime(true); -function junit_start_timer(string $file_name): void -{ - global $JUNIT; - if (!junit_enabled()) { - return; + $suite = $this->getSuiteName($file_name); + $this->initSuite($suite); + $this->suites[$suite]['files'][$file_name] = $file_name; + } } - if (!isset($JUNIT['files'][$file_name]['start'])) { - $JUNIT['files'][$file_name]['start'] = microtime(true); - - $suite = junit_get_suitename_for($file_name); - junit_init_suite($suite); - $JUNIT['suites'][$suite]['files'][$file_name] = $file_name; + public function getSuiteName(string $file_name): string + { + return $this->pathToClassName(dirname($file_name)); } -} -function junit_get_suitename_for(string $file_name): string -{ - return junit_path_to_classname(dirname($file_name)); -} - -function junit_path_to_classname(string $file_name): string -{ - global $JUNIT; - - if (!junit_enabled()) { - return ''; - } + private function pathToClassName(string $file_name): string + { + if (!$this->enabled) { + return ''; + } - $ret = $JUNIT['name']; - $_tmp = []; + $ret = $this->rootSuite['name']; + $_tmp = []; - // lookup whether we're in the PHP source checkout - $max = 5; - if (is_file($file_name)) { - $dir = dirname(realpath($file_name)); - } else { - $dir = realpath($file_name); - } - do { - array_unshift($_tmp, basename($dir)); - $chk = $dir . DIRECTORY_SEPARATOR . "main" . DIRECTORY_SEPARATOR . "php_version.h"; - $dir = dirname($dir); - } while (!file_exists($chk) && --$max > 0); - if (file_exists($chk)) { - if ($max) { - array_shift($_tmp); + // lookup whether we're in the PHP source checkout + $max = 5; + if (is_file($file_name)) { + $dir = dirname(realpath($file_name)); + } else { + $dir = realpath($file_name); } - foreach ($_tmp as $p) { - $ret .= "." . preg_replace(",[^a-z0-9]+,i", ".", $p); + do { + array_unshift($_tmp, basename($dir)); + $chk = $dir . DIRECTORY_SEPARATOR . "main" . DIRECTORY_SEPARATOR . "php_version.h"; + $dir = dirname($dir); + } while (!file_exists($chk) && --$max > 0); + if (file_exists($chk)) { + if ($max) { + array_shift($_tmp); + } + foreach ($_tmp as $p) { + $ret .= "." . preg_replace(",[^a-z0-9]+,i", ".", $p); + } + return $ret; } - return $ret; + + return $this->rootSuite['name'] . '.' . str_replace([DIRECTORY_SEPARATOR, '-'], '.', $file_name); } - return $JUNIT['name'] . '.' . str_replace([DIRECTORY_SEPARATOR, '-'], '.', $file_name); -} + public function initSuite(string $suite_name): void + { + if (!$this->enabled) { + return; + } -function junit_init_suite(string $suite_name): void -{ - global $JUNIT; - if (!junit_enabled()) { - return; - } + if (!empty($this->suites[$suite_name])) { + return; + } - if (!empty($JUNIT['suites'][$suite_name])) { - return; + $this->suites[$suite_name] = self::EMPTY_SUITE + ['name' => $suite_name]; } - $JUNIT['suites'][$suite_name] = [ - 'name' => $suite_name, - 'test_total' => 0, - 'test_pass' => 0, - 'test_fail' => 0, - 'test_error' => 0, - 'test_skip' => 0, - 'test_warn' => 0, - 'files' => [], - 'execution_time' => 0, - ]; -} + public function stopTimer(string $file_name): void + { + if (!$this->enabled) { + return; + } -function junit_finish_timer(string $file_name): void -{ - global $JUNIT; - if (!junit_enabled()) { - return; - } + if (!isset($this->rootSuite['files'][$file_name]['start'])) { + throw new Exception("Timer for $file_name was not started!"); + } - if (!isset($JUNIT['files'][$file_name]['start'])) { - error("Timer for $file_name was not started!"); - } + if (!isset($this->rootSuite['files'][$file_name]['total'])) { + $this->rootSuite['files'][$file_name]['total'] = 0; + } - if (!isset($JUNIT['files'][$file_name]['total'])) { - $JUNIT['files'][$file_name]['total'] = 0; + $start = $this->rootSuite['files'][$file_name]['start']; + $this->rootSuite['files'][$file_name]['total'] += microtime(true) - $start; + unset($this->rootSuite['files'][$file_name]['start']); } - $start = $JUNIT['files'][$file_name]['start']; - $JUNIT['files'][$file_name]['total'] += microtime(true) - $start; - unset($JUNIT['files'][$file_name]['start']); -} + public function mergeResults(?JUnit $other): void + { + if (!$this->enabled || !$other) { + return; + } -function junit_merge_results(array $junit): void -{ - global $JUNIT; - $JUNIT['test_total'] += $junit['test_total']; - $JUNIT['test_pass'] += $junit['test_pass']; - $JUNIT['test_fail'] += $junit['test_fail']; - $JUNIT['test_error'] += $junit['test_error']; - $JUNIT['test_skip'] += $junit['test_skip']; - $JUNIT['test_warn'] += $junit['test_warn']; - $JUNIT['execution_time'] += $junit['execution_time']; - $JUNIT['files'] += $junit['files']; - foreach ($junit['suites'] as $name => $suite) { - if (!isset($JUNIT['suites'][$name])) { - $JUNIT['suites'][$name] = $suite; - continue; + $this->mergeSuites($this->rootSuite, $other->rootSuite); + foreach ($other->suites as $name => $suite) { + if (!isset($this->suites[$name])) { + $this->suites[$name] = $suite; + continue; + } + + $this->mergeSuites($this->suites[$name], $suite); } + } - $SUITE =& $JUNIT['suites'][$name]; - $SUITE['test_total'] += $suite['test_total']; - $SUITE['test_pass'] += $suite['test_pass']; - $SUITE['test_fail'] += $suite['test_fail']; - $SUITE['test_error'] += $suite['test_error']; - $SUITE['test_skip'] += $suite['test_skip']; - $SUITE['test_warn'] += $suite['test_warn']; - $SUITE['execution_time'] += $suite['execution_time']; - $SUITE['files'] += $suite['files']; + private function mergeSuites(array &$dest, array $source): void + { + $dest['test_total'] += $source['test_total']; + $dest['test_pass'] += $source['test_pass']; + $dest['test_fail'] += $source['test_fail']; + $dest['test_error'] += $source['test_error']; + $dest['test_skip'] += $source['test_skip']; + $dest['test_warn'] += $source['test_warn']; + $dest['execution_time'] += $source['execution_time']; + $dest['files'] += $source['files']; } } -- cgit v1.2.1