diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -211,6 +211,7 @@ 'ArcanistXMLLinterTestCase' => 'lint/linter/__tests__/ArcanistXMLLinterTestCase.php', 'ArcanistXUnitTestResultParser' => 'unit/parser/ArcanistXUnitTestResultParser.php', 'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.php', + 'GoTestEngine' => 'unit/engine/GoTestEngine.php', 'NoseTestEngine' => 'unit/engine/NoseTestEngine.php', 'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php', 'PhpunitTestEngineTestCase' => 'unit/engine/__tests__/PhpunitTestEngineTestCase.php', @@ -389,6 +390,7 @@ 'ArcanistXMLLinter' => 'ArcanistLinter', 'ArcanistXMLLinterTestCase' => 'ArcanistLinterTestCase', 'CSharpToolsTestEngine' => 'XUnitTestEngine', + 'GoTestEngine' => 'ArcanistUnitTestEngine', 'NoseTestEngine' => 'ArcanistUnitTestEngine', 'PhpunitTestEngine' => 'ArcanistUnitTestEngine', 'PhpunitTestEngineTestCase' => 'ArcanistTestCase', diff --git a/src/unit/engine/GoTestEngine.php b/src/unit/engine/GoTestEngine.php new file mode 100644 --- /dev/null +++ b/src/unit/engine/GoTestEngine.php @@ -0,0 +1,157 @@ +projectRoot = $this->getWorkingCopy()->getProjectRoot(); + $this->cmdTmpl = $this->getCommandTemplate(); + + $futures = $this->buildFutures($this->getPaths(), $this->cmdTmpl); + if (empty($futures)) { + throw new ArcanistNoEffectException('No tests to run.'); + } + + $results = array(); + foreach ($futures as $package => $future) { + $results = array_merge( + $results, + $this->resolveFuture($package, $future)); + } + + return $results; + } + + protected function getBinary() { + return 'go'; + } + + protected function getVersion() { + $cmd = csprintf('%s version', $this->getBinary()); + list($stdout) = execx('%C', $cmd); + $matches = array(); + preg_match( + '/^go version go(?P[0-9\.]+).*/', + $stdout, + $matches); + return $matches['version']; + } + + protected function getDefaultConfig() { + return array( + self::USE_GODEP_KEY => true, + self::USE_RACE_KEY => true, + ); + } + + protected function getCommandTemplate() { + $cmd = ''; + if ($this->useGodep()) { + $cmd = 'godep '; + } + + $cmd .= 'go test -v'; + + if ($this->useRace()) { + $cmd .= ' -race'; + } + + $cmd .= ' ./'; + + return $cmd; + } + + protected function useRace() { + $default = idx($this->getDefaultConfig(), self::USE_RACE_KEY); + if ($this->getConfig(self::USE_RACE_KEY, $default) === false) { + return false; + } + + $version = explode('.', $this->getVersion()); + if ($version[0] == 1 && $version[1] < 1) { + return false; + } + + return true; + } + + protected function useGodep() { + $default = idx($this->getDefaultConfig(), self::USE_GODEP_KEY); + if ($this->getConfig(self::USE_GODEP_KEY, $default) === false) { + return false; + } + + if (is_dir(Filesystem::resolvePath('Godeps', $this->projectRoot))) { + return true; + } + + return false; + } + + protected function getConfig($key, $default = null) { + return $this->getConfigurationManager()->getConfigFromAnySource( + $key, + $default); + } + + protected function buildFutures(array $packages, $cmd_tmpl) { + $affected_packages = array(); + foreach ($packages as $package) { + // Must always test a package. + if (!is_dir($package)) { + // If it's a file but not a go file. Skip this test + if (substr($package, -3) != '.go') { + continue; + } + + $package = dirname($package); + } + + if (!array_key_exists($package, $affected_packages)) { + $affected_packages[] = $package; + } + } + + $futures = array(); + foreach ($affected_packages as $package) { + if ($package === '.') { + $package = ''; + } + + $future = new ExecFuture( + '%C%C', + $cmd_tmpl, + $package); + $future->setCWD($this->projectRoot); + $futures[$package] = $future; + } + + return $futures; + } + + protected function resolveFuture($package, Future $future) { + list($err, $stdout, $stderr) = $future->resolve(); + $parser = new ArcanistGoTestResultParser(); + $messages = $parser->parseTestResults($package, $stdout, $stderr); + + if ($messages === false) { + if ($err) { + $future->resolvex(); + } else { + throw new Exception( + sprintf( + "%s\n\nSTDOUT\n%s\n\nSTDERR\n%s", + pht('Linter failed to parse output!'), + $stdout, + $stderr)); + } + } + + return $messages; + } +} diff --git a/src/unit/parser/ArcanistGoTestResultParser.php b/src/unit/parser/ArcanistGoTestResultParser.php --- a/src/unit/parser/ArcanistGoTestResultParser.php +++ b/src/unit/parser/ArcanistGoTestResultParser.php @@ -12,11 +12,13 @@ * (e.g. `go test -v`) * * @param string $path Path to test - * @param string $test_results String containing Go test output + * @param string $stdout the Stdout of the command. + * @param string $stderr the Stderr of the command. * * @return array */ - public function parseTestResults($path, $test_results) { + public function parseTestResults($path, $stdout, $stderr = '') { + $test_results = $stderr.$stdout; $test_results = explode("\n", $test_results); $results = array(); @@ -25,7 +27,44 @@ // Temp store for test case results (in case we run multiple test cases) $test_case_results = array(); - foreach ($test_results as $i => $line) { + for ($i = 0; $i < count($test_results); $i++) { + $line = $test_results[$i]; + + if (strlen($line) >= 18 + && strncmp($line, '==================', 18) === 0 + && strncmp($test_results[$i + 1], 'WARNING: DATA RACE', 18) === 0) { + // We have a race condition + $i++; // Advance to WARNING: DATA RACE + $reason = ''; + $test_name = ''; + + // loop to collect all data and move to the === line + while (strncmp($test_results[$i], '==================', 18) !== 0) { + if (strncmp($test_results[$i], 'Goroutine', 9) === 0) { + $meta = array(); + preg_match( + '/^.*\.(?P[^\.]+)$/', + $test_results[$i + 1], + $meta); + $test_name = $meta['test_name'].' Race Detected'; + } + $reason .= $test_results[$i++]."\n"; + + // Are we out of lines? + if ($i > count($test_results)) { + return false; + } + } + + $result = new ArcanistUnitTestResult(); + $result->setName($test_name); + $result->setResult(ArcanistUnitTestResult::RESULT_FAIL); + $result->setUserData($reason); + + $test_case_results[] = $result; + + continue; + } if (strncmp($line, '--- PASS', 8) === 0) { // We have a passing test diff --git a/src/unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php b/src/unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php --- a/src/unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php +++ b/src/unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php @@ -39,6 +39,47 @@ $parsed_results[1]->getResult()); } + public function testRaceWarningTestCaseSuccess() { + $stubbed_results = Filesystem::readFile( + dirname(__FILE__).'/testresults/go.race-warning-test-case-success-go1.4'); + + $parsed_results = id(new ArcanistGoTestResultParser()) + ->parseTestResults('racepackage_test.go', $stubbed_results); + + $this->assertEqual(3, count($parsed_results)); + $this->assertEqual( + ArcanistUnitTestResult::RESULT_FAIL, + $parsed_results[0]->getResult()); + $this->assertEqual( + 'WARNING: DATA RACE', + idx(explode("\n", $parsed_results[0]->getUserData()), 0)); + $this->assertEqual( + ArcanistUnitTestResult::RESULT_PASS, + $parsed_results[1]->getResult()); + } + + public function testRaceWarningTestCaseFailure() { + $stubbed_results = Filesystem::readFile( + dirname(__FILE__).'/testresults/go.race-warning-test-case-failure-go1.4'); + + $parsed_results = id(new ArcanistGoTestResultParser()) + ->parseTestResults('racepackage_test.go', $stubbed_results); + + $this->assertEqual(3, count($parsed_results)); + $this->assertEqual( + ArcanistUnitTestResult::RESULT_FAIL, + $parsed_results[0]->getResult()); + $this->assertEqual( + 'WARNING: DATA RACE', + idx(explode("\n", $parsed_results[0]->getUserData()), 0)); + $this->assertEqual( + ArcanistUnitTestResult::RESULT_PASS, + $parsed_results[1]->getResult()); + $this->assertEqual( + "racepackage_test.go:30: got: 2, want: 1\n", + $parsed_results[2]->getUserData()); + } + public function testMultipleTestCasesSuccessful() { $stubbed_results = Filesystem::readFile( dirname(__FILE__).'/testresults/go.multiple-test-cases-successful'); diff --git a/src/unit/parser/__tests__/testresults/go.race-warning-test-case-failure-go1.4 b/src/unit/parser/__tests__/testresults/go.race-warning-test-case-failure-go1.4 new file mode 100644 --- /dev/null +++ b/src/unit/parser/__tests__/testresults/go.race-warning-test-case-failure-go1.4 @@ -0,0 +1,29 @@ +================== +WARNING: DATA RACE +Write by goroutine 8: + racepackage.func~002() + /tmp/racepackage_test.go:21 +0x5d + +Previous write by goroutine 7: + racepackage.func~001() + /tmp/racepackage_test.go:17 +0x5d + +Goroutine 8 (running) created at: + racepackage.TestBar() + /tmp/racepackage_test.go:22 +0x1e8 + testing.tRunner() + /usr/local/Cellar/go/1.4.2/libexec/src/testing/testing.go:447 +0x133 + +Goroutine 7 (finished) created at: + racepackage.TestBar() + /tmp/racepackage_test.go:18 +0x13e + testing.tRunner() + /usr/local/Cellar/go/1.4.2/libexec/src/testing/testing.go:447 +0x133 +================== +=== RUN TestFoo +--- PASS: TestFoo (0.03s) +=== RUN TestBar +--- FAIL: TestBar (0.02s) + racepackage_test.go:30: got: 2, want: 1 +PASS +ok package/racepackage 0.042s diff --git a/src/unit/parser/__tests__/testresults/go.race-warning-test-case-success-go1.4 b/src/unit/parser/__tests__/testresults/go.race-warning-test-case-success-go1.4 new file mode 100644 --- /dev/null +++ b/src/unit/parser/__tests__/testresults/go.race-warning-test-case-success-go1.4 @@ -0,0 +1,28 @@ +================== +WARNING: DATA RACE +Write by goroutine 8: + racepackage.func~002() + /tmp/racepackage_test.go:21 +0x5d + +Previous write by goroutine 7: + racepackage.func~001() + /tmp/racepackage_test.go:17 +0x5d + +Goroutine 8 (running) created at: + racepackage.TestBar() + /tmp/racepackage_test.go:22 +0x1e8 + testing.tRunner() + /usr/local/Cellar/go/1.4.2/libexec/src/testing/testing.go:447 +0x133 + +Goroutine 7 (finished) created at: + racepackage.TestBar() + /tmp/racepackage_test.go:18 +0x13e + testing.tRunner() + /usr/local/Cellar/go/1.4.2/libexec/src/testing/testing.go:447 +0x133 +================== +=== RUN TestFoo +--- PASS: TestFoo (0.03s) +=== RUN TestBar +--- PASS: TestBar (0.01s) +PASS +ok package/racepackage 0.042s