diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /tools/code-coverage | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | tools/code-coverage/CodeCoverageHandler.cpp | 158 | ||||
-rw-r--r-- | tools/code-coverage/CodeCoverageHandler.h | 38 | ||||
-rw-r--r-- | tools/code-coverage/PerTestCoverageUtils.jsm | 126 | ||||
-rw-r--r-- | tools/code-coverage/components.conf | 14 | ||||
-rw-r--r-- | tools/code-coverage/docs/index.rst | 209 | ||||
-rw-r--r-- | tools/code-coverage/moz.build | 39 | ||||
-rw-r--r-- | tools/code-coverage/nsCodeCoverage.cpp | 96 | ||||
-rw-r--r-- | tools/code-coverage/nsCodeCoverage.h | 22 | ||||
-rw-r--r-- | tools/code-coverage/nsICodeCoverage.idl | 24 | ||||
-rw-r--r-- | tools/code-coverage/tests/mochitest/mochitest.ini | 3 | ||||
-rw-r--r-- | tools/code-coverage/tests/mochitest/test_coverage_specialpowers.html | 38 | ||||
-rw-r--r-- | tools/code-coverage/tests/xpcshell/head.js | 100 | ||||
-rw-r--r-- | tools/code-coverage/tests/xpcshell/support.js | 5 | ||||
-rw-r--r-- | tools/code-coverage/tests/xpcshell/test_basic.js | 115 | ||||
-rw-r--r-- | tools/code-coverage/tests/xpcshell/test_basic_child_and_parent.js | 120 | ||||
-rw-r--r-- | tools/code-coverage/tests/xpcshell/xpcshell.ini | 7 |
16 files changed, 1114 insertions, 0 deletions
diff --git a/tools/code-coverage/CodeCoverageHandler.cpp b/tools/code-coverage/CodeCoverageHandler.cpp new file mode 100644 index 0000000000..fbe7494c6b --- /dev/null +++ b/tools/code-coverage/CodeCoverageHandler.cpp @@ -0,0 +1,158 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdio.h> +#ifdef XP_WIN +# include <process.h> +# define getpid _getpid +#else +# include <signal.h> +# include <unistd.h> +#endif +#include "js/experimental/CodeCoverage.h" +#include "mozilla/Atomics.h" +#include "mozilla/dom/ScriptSettings.h" // for AutoJSAPI +#include "mozilla/CodeCoverageHandler.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DebugOnly.h" +#include "nsAppRunner.h" +#include "nsIFile.h" +#include "nsIOutputStream.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "prtime.h" + +using namespace mozilla; + +// The __gcov_flush function writes the coverage counters to gcda files and then +// resets them to zero. It is defined at +// https://github.com/gcc-mirror/gcc/blob/aad93da1a579b9ae23ede6b9cf8523360f0a08b4/libgcc/libgcov-interface.c. +// __gcov_flush is protected by a mutex in GCC, but not in LLVM, so we are using +// a CrossProcessMutex to protect it. + +extern "C" void __gcov_flush(); +extern "C" void __gcov_dump(); +extern "C" void __gcov_reset(); + +StaticAutoPtr<CodeCoverageHandler> CodeCoverageHandler::instance; + +void CodeCoverageHandler::FlushCounters(const bool initialized) { + static Atomic<bool> hasBeenInitialized(false); + if (!hasBeenInitialized) { + hasBeenInitialized = initialized; + return; + } + + printf_stderr("[CodeCoverage] Requested flush for %d.\n", getpid()); + + CrossProcessMutexAutoLock lock(*CodeCoverageHandler::Get()->GetMutex()); + +#if defined(__clang__) && __clang_major__ >= 12 + __gcov_dump(); + __gcov_reset(); +#else + __gcov_flush(); +#endif + + printf_stderr("[CodeCoverage] flush completed.\n"); + + const char* outDir = getenv("JS_CODE_COVERAGE_OUTPUT_DIR"); + if (!outDir || *outDir == 0) { + return; + } + + dom::AutoJSAPI jsapi; + jsapi.Init(); + size_t length; + JS::UniqueChars result = js::GetCodeCoverageSummaryAll(jsapi.cx(), &length); + if (!result) { + return; + } + + nsCOMPtr<nsIFile> file; + + nsresult rv = NS_NewNativeLocalFile(nsDependentCString(outDir), false, + getter_AddRefs(file)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = file->AppendNative( + nsPrintfCString("%lu-%d.info", PR_Now() / PR_USEC_PER_MSEC, getpid())); + + rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIOutputStream> outputStream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), file); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + char* data = result.get(); + while (length) { + uint32_t n = 0; + rv = outputStream->Write(data, length, &n); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + data += n; + length -= n; + } + + rv = outputStream->Close(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + printf_stderr("[CodeCoverage] JS flush completed.\n"); +} + +void CodeCoverageHandler::FlushCountersSignalHandler(int) { FlushCounters(); } + +void CodeCoverageHandler::SetSignalHandlers() { +#ifndef XP_WIN + printf_stderr("[CodeCoverage] Setting handlers for process %d.\n", getpid()); + + struct sigaction dump_sa; + dump_sa.sa_handler = CodeCoverageHandler::FlushCountersSignalHandler; + dump_sa.sa_flags = SA_RESTART; + sigemptyset(&dump_sa.sa_mask); + DebugOnly<int> r1 = sigaction(SIGUSR1, &dump_sa, nullptr); + MOZ_ASSERT(r1 == 0, "Failed to install GCOV SIGUSR1 handler"); +#endif +} + +CodeCoverageHandler::CodeCoverageHandler() : mGcovLock("GcovLock") { + SetSignalHandlers(); +} + +CodeCoverageHandler::CodeCoverageHandler(CrossProcessMutexHandle aHandle) + : mGcovLock(std::move(aHandle)) { + SetSignalHandlers(); +} + +void CodeCoverageHandler::Init() { + MOZ_ASSERT(!instance); + MOZ_ASSERT(XRE_IsParentProcess()); + instance = new CodeCoverageHandler(); + ClearOnShutdown(&instance); + + // Don't really flush but just make FlushCounters usable. + FlushCounters(true); +} + +void CodeCoverageHandler::Init(CrossProcessMutexHandle aHandle) { + MOZ_ASSERT(!instance); + MOZ_ASSERT(!XRE_IsParentProcess()); + instance = new CodeCoverageHandler(std::move(aHandle)); + ClearOnShutdown(&instance); + + // Don't really flush but just make FlushCounters usable. + FlushCounters(true); +} + +CodeCoverageHandler* CodeCoverageHandler::Get() { + MOZ_ASSERT(instance); + return instance; +} + +CrossProcessMutex* CodeCoverageHandler::GetMutex() { return &mGcovLock; } + +CrossProcessMutexHandle CodeCoverageHandler::GetMutexHandle() { + return mGcovLock.CloneHandle(); +} diff --git a/tools/code-coverage/CodeCoverageHandler.h b/tools/code-coverage/CodeCoverageHandler.h new file mode 100644 index 0000000000..7c1b33ec8a --- /dev/null +++ b/tools/code-coverage/CodeCoverageHandler.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_codecoveragehandler_h +#define mozilla_codecoveragehandler_h + +#include "mozilla/StaticPtr.h" +#include "mozilla/ipc/CrossProcessMutex.h" + +namespace mozilla { + +class CodeCoverageHandler { + public: + static void Init(); + static void Init(CrossProcessMutexHandle aHandle); + static CodeCoverageHandler* Get(); + CrossProcessMutex* GetMutex(); + CrossProcessMutexHandle GetMutexHandle(); + static void FlushCounters(const bool initialized = false); + static void FlushCountersSignalHandler(int); + + private: + CodeCoverageHandler(); + explicit CodeCoverageHandler(CrossProcessMutexHandle aHandle); + + static StaticAutoPtr<CodeCoverageHandler> instance; + CrossProcessMutex mGcovLock; + + DISALLOW_COPY_AND_ASSIGN(CodeCoverageHandler); + + void SetSignalHandlers(); +}; + +} // namespace mozilla + +#endif // mozilla_codecoveragehandler_h diff --git a/tools/code-coverage/PerTestCoverageUtils.jsm b/tools/code-coverage/PerTestCoverageUtils.jsm new file mode 100644 index 0000000000..f1945273c1 --- /dev/null +++ b/tools/code-coverage/PerTestCoverageUtils.jsm @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* exported PerTestCoverageUtils */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["PerTestCoverageUtils"]; + +// This is the directory where gcov is emitting the gcda files. +const gcovPrefixPath = Services.env.get("GCOV_PREFIX"); +// This is the directory where codecoverage.py is expecting to see the gcda files. +const gcovResultsPath = Services.env.get("GCOV_RESULTS_DIR"); +// This is the directory where the JS engine is emitting the lcov files. +const jsvmPrefixPath = Services.env.get("JS_CODE_COVERAGE_OUTPUT_DIR"); +// This is the directory where codecoverage.py is expecting to see the lcov files. +const jsvmResultsPath = Services.env.get("JSVM_RESULTS_DIR"); + +const gcovPrefixDir = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile +); +if (gcovPrefixPath) { + gcovPrefixDir.initWithPath(gcovPrefixPath); +} + +let gcovResultsDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); +if (gcovResultsPath) { + gcovResultsDir.initWithPath(gcovResultsPath); +} + +const jsvmPrefixDir = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile +); +if (jsvmPrefixPath) { + jsvmPrefixDir.initWithPath(jsvmPrefixPath); +} + +let jsvmResultsDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); +if (jsvmResultsPath) { + jsvmResultsDir.initWithPath(jsvmResultsPath); +} + +function awaitPromise(promise) { + let ret; + let complete = false; + let error = null; + promise + .catch(e => (error = e)) + .then(v => { + ret = v; + complete = true; + }); + Services.tm.spinEventLoopUntil( + "PerTestCoverageUtils.jsm:awaitPromise", + () => complete + ); + if (error) { + throw new Error(error); + } + return ret; +} + +function removeDirectoryContents(dir) { + let entries = dir.directoryEntries; + while (entries.hasMoreElements()) { + entries.nextFile.remove(true); + } +} + +function moveDirectoryContents(src, dst) { + let entries = src.directoryEntries; + while (entries.hasMoreElements()) { + entries.nextFile.moveTo(dst, null); + } +} + +var PerTestCoverageUtils = class PerTestCoverageUtilsClass { + // Resets the counters to 0. + static async beforeTest() { + if (!PerTestCoverageUtils.enabled) { + return; + } + + // Flush the counters. + let codeCoverageService = Cc[ + "@mozilla.org/tools/code-coverage;1" + ].getService(Ci.nsICodeCoverage); + await codeCoverageService.flushCounters(); + + // Remove coverage files created by the flush, and those that might have been created between the end of a previous test and the beginning of the next one (e.g. some tests can create a new content process for every sub-test). + removeDirectoryContents(gcovPrefixDir); + removeDirectoryContents(jsvmPrefixDir); + + // Move coverage files from the GCOV_RESULTS_DIR and JSVM_RESULTS_DIR directories, so we can accumulate the counters. + moveDirectoryContents(gcovResultsDir, gcovPrefixDir); + moveDirectoryContents(jsvmResultsDir, jsvmPrefixDir); + } + + static beforeTestSync() { + awaitPromise(this.beforeTest()); + } + + // Dumps counters and moves the gcda files in the directory expected by codecoverage.py. + static async afterTest() { + if (!PerTestCoverageUtils.enabled) { + return; + } + + // Flush the counters. + let codeCoverageService = Cc[ + "@mozilla.org/tools/code-coverage;1" + ].getService(Ci.nsICodeCoverage); + await codeCoverageService.flushCounters(); + + // Move the coverage files in GCOV_RESULTS_DIR and JSVM_RESULTS_DIR, so that the execution from now to shutdown (or next test) is not counted. + moveDirectoryContents(gcovPrefixDir, gcovResultsDir); + moveDirectoryContents(jsvmPrefixDir, jsvmResultsDir); + } + + static afterTestSync() { + awaitPromise(this.afterTest()); + } +}; + +PerTestCoverageUtils.enabled = !!gcovResultsPath; diff --git a/tools/code-coverage/components.conf b/tools/code-coverage/components.conf new file mode 100644 index 0000000000..a30600525c --- /dev/null +++ b/tools/code-coverage/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{93576af0-a62f-4c88-bc12-f1855d4e0173}', + 'contract_ids': ['@mozilla.org/tools/code-coverage;1'], + 'type': 'nsCodeCoverage', + 'headers': ['/tools/code-coverage/nsCodeCoverage.h'] + }, +] diff --git a/tools/code-coverage/docs/index.rst b/tools/code-coverage/docs/index.rst new file mode 100644 index 0000000000..36333b4829 --- /dev/null +++ b/tools/code-coverage/docs/index.rst @@ -0,0 +1,209 @@ +Code coverage +============= + +What is Code Coverage? +---------------------- + +**Code coverage** essentially measures how often certain lines are hit, +branches taken or conditions met in a program, given some test that you +run on it. + +There are two very important things to keep in mind when talking about +code coverage: + +- If a certain branch of code is not hit at all while running tests, + then those tests will never be able to find a bug in this particular + piece of the code. +- If a certain branch of code is executed (even very often), this still + is not a clear indication of the *quality of a test*. It could be + that a test exercises the code but does not actually check that the + code performs *correctly*. + +As a conclusion, we can use code coverage to find areas that need (more) +tests, but we cannot use it to confirm that certain areas are well +tested. + + +Firefox Code Coverage reports +----------------------------- + +We automatically run code coverage builds and tests on all +mozilla-central runs, for Linux and Windows. C/C++, Rust and JavaScript +are supported. + +The generated reports can be found at https://coverage.moz.tools/. The +reports can be filtered by platform and/or test suite. + +We also generate a report of all totally uncovered files, which can be +found at https://coverage.moz.tools/#view=zero. You can use this to find +areas of code that should be tested, or code that is no longer used +(dead code, which could be removed). + + +C/C++ Code Coverage on Firefox +------------------------------ + +There are several ways to get C/C++ coverage information for +mozilla-central, including creating your own coverage builds. The next +sections describe the available options. + + +Generate Code Coverage report from a try build (or any other CI build) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To spin a code coverage build, you need to select the linux64-ccov +platform (use --full when using the fuzzy selector to get the ccov +builds to show up). + +E.g. for a try build: + +.. code:: shell + + ./mach try fuzzy -q 'linux64-ccov' + +There are two options now, you can either generate the report locally or +use a one-click loaner. + + +Generate report using a one-click loaner +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Select the B job on Treeherder and get a one-click loaner. + +In the loaner, download and execute the script +https://github.com/mozilla/code-coverage/blob/master/report/firefox_code_coverage/codecoverage.py: + +.. code:: shell + + wget https://raw.githubusercontent.com/mozilla/code-coverage/master/report/firefox_code_coverage/codecoverage.py + python codecoverage.py + +This command will automatically generate a HTML report of the code +coverage information in the **report** subdirectory in your current +working directory. + + +Generate report locally +^^^^^^^^^^^^^^^^^^^^^^^ + +Prerequisites: + +- Create and activate a new `virtualenv`_, then run: + +.. code:: shell + + pip install firefox-code-coverage + +Given a treeherder linux64-ccov build (with its branch, e.g. +\`mozilla-central\` or \`try`, and revision, the tip commit hash of your +push), run the following command: + +.. code:: shell + + firefox-code-coverage PATH/TO/MOZILLA/SRC/DIR/ BRANCH REVISION + +This command will automatically download code coverage artifacts from +the treeherder build and generate an HTML report of the code coverage +information. The report will be stored in the **report** subdirectory in +your current working directory. + +.. _virtualenv: https://docs.python.org/3/tutorial/venv.html + +Creating your own Coverage Build +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +On Linux, Windows and Mac OS X it is straightforward to generate an +instrumented build using GCC or Clang. Adding the following lines to +your ``.mozconfig`` file should be sufficient: + +.. code:: shell + + # Enable code coverage + ac_add_options --enable-coverage + + # Needed for e10s: + # With the sandbox, content processes can't write updated coverage counters in the gcda files. + ac_add_options --disable-sandbox + +Some additional options might be needed, check the code-coverage +mozconfigs used on CI to be sure: +browser/config/mozconfigs/linux64/code-coverage, +browser/config/mozconfigs/win64/code-coverage, +browser/config/mozconfigs/macosx64/code-coverage. + +Make sure you are not running with :ref:`artifact build <Understanding Artifact Builds>` +enabled, as it can prevent coverage artifacts from being created. + +You can then create your build as usual. Once the build is complete, you +can run any tests/tools you would like to run and the coverage data gets +automatically written to special files. In order to view/process this +data, we recommend using the +`grcov <https://github.com/mozilla/grcov>`__ tool, a tool to manage and +visualize gcov results. You can also use the same process explained +earlier for CI builds. + + +Debugging Failing Tests on the Try Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When code coverage is run through a push to try, all the data that is +created is ingested by ActiveData and processed into a different data +format for analysis. Anytime a code coverage run generates \*.gcda and +\*.gcno files, ActiveData starts working. Now, sometimes, a test will +permanently fail when it is running on a build that is instrumented with +GCOV. To debug these issues without overloading ActiveData with garbage +coverage data, open the file +`taskcluster/gecko_taskgraph/transforms/test/__init__.py <https://searchfox.org/mozilla-central/source/taskcluster/gecko_taskgraph/transforms/test/__init__.py#516>`__ +and add the following line, + +.. code:: python + + test['mozharness'].setdefault('extra-options', []).append('--disable-ccov-upload') + +right after this line of code: + +.. code:: python + + test['mozharness'].setdefault('extra-options', []).append('--code-coverage') + +Now when you push to try to debug some failing tests, or anything else, +there will not be any code coverage artifacts uploaded from the build +machines or from the test machines. + + +JS Debugger Per Test Code Coverage on Firefox +--------------------------------------------- + +There are two ways to get javascript per test code coverage information +for mozilla-central. The next sections describe these options. + + +Generate Per Test Code Coverage from a try build (or any other treeherder build) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To spin a code coverage build, you need to select the linux64-jsdcov +platform. E.g. for a try build: + +.. code:: shell + + ./mach try fuzzy -q 'linux64-jsdcov' + +This produces JavaScript Object Notation (JSON) files that can be +downloaded from the treeherder testing machines and processed or +analyzed locally. + + +Generate Per Test Code Coverage Locally +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To generate the JSON files containing coverage information locally, simply +add an extra argument called ``--jscov-dir-prefix`` which accepts a +directory as it's input and stores the resulting data in that directory. +For example, to collect code coverage for the entire Mochitest suite: + +.. code:: shell + + ./mach mochitest --jscov-dir-prefix /PATH/TO/COVERAGE/DIR/ + +Currently, only the Mochitest and Xpcshell test suites have this +capability. diff --git a/tools/code-coverage/moz.build b/tools/code-coverage/moz.build new file mode 100644 index 0000000000..c5ee53cf68 --- /dev/null +++ b/tools/code-coverage/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +TESTING_JS_MODULES += ["PerTestCoverageUtils.jsm"] + +if CONFIG["MOZ_CODE_COVERAGE"]: + XPIDL_MODULE = "code-coverage" + + XPIDL_SOURCES += [ + "nsICodeCoverage.idl", + ] + + SOURCES += [ + "CodeCoverageHandler.cpp", + "nsCodeCoverage.cpp", + ] + + XPCOM_MANIFESTS += [ + "components.conf", + ] + + EXPORTS.mozilla += [ + "CodeCoverageHandler.h", + ] + + LOCAL_INCLUDES += [ + "/ipc/chromium/src", + "/xpcom/base", + ] + + include("/ipc/chromium/chromium-config.mozbuild") + + XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"] + MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.ini"] + + FINAL_LIBRARY = "xul" diff --git a/tools/code-coverage/nsCodeCoverage.cpp b/tools/code-coverage/nsCodeCoverage.cpp new file mode 100644 index 0000000000..5d7ed1927c --- /dev/null +++ b/tools/code-coverage/nsCodeCoverage.cpp @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCodeCoverage.h" +#include "mozilla/CodeCoverageHandler.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Promise.h" + +using mozilla::dom::ContentParent; +using mozilla::dom::Promise; + +NS_IMPL_ISUPPORTS(nsCodeCoverage, nsICodeCoverage) + +nsCodeCoverage::nsCodeCoverage() {} + +nsCodeCoverage::~nsCodeCoverage() {} + +enum RequestType { Flush }; + +class ProcessCount final { + NS_INLINE_DECL_REFCOUNTING(ProcessCount); + + public: + explicit ProcessCount(uint32_t c) : mCount(c) {} + operator uint32_t() const { return mCount; } + ProcessCount& operator--() { + mCount--; + return *this; + } + + private: + ~ProcessCount() {} + uint32_t mCount; +}; + +namespace { + +nsresult Request(JSContext* cx, Promise** aPromise, RequestType requestType) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + nsIGlobalObject* global = xpc::CurrentNativeGlobal(cx); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + mozilla::ErrorResult result; + RefPtr<Promise> promise = Promise::Create(global, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + uint32_t processCount = 0; + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + mozilla::Unused << cp; + ++processCount; + } + + if (requestType == RequestType::Flush) { + mozilla::CodeCoverageHandler::FlushCounters(); + } + + if (processCount == 0) { + promise->MaybeResolveWithUndefined(); + } else { + RefPtr<ProcessCount> processCountHolder(new ProcessCount(processCount)); + + auto resolve = [processCountHolder, promise](bool unused) { + if (--(*processCountHolder) == 0) { + promise->MaybeResolveWithUndefined(); + } + }; + + auto reject = [promise](mozilla::ipc::ResponseRejectReason&& aReason) { + promise->MaybeReject(NS_ERROR_FAILURE); + }; + + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + if (requestType == RequestType::Flush) { + cp->SendFlushCodeCoverageCounters(resolve, reject); + } + } + } + + promise.forget(aPromise); + return NS_OK; +} + +} // anonymous namespace + +NS_IMETHODIMP nsCodeCoverage::FlushCounters(JSContext* cx, Promise** aPromise) { + return Request(cx, aPromise, RequestType::Flush); +} diff --git a/tools/code-coverage/nsCodeCoverage.h b/tools/code-coverage/nsCodeCoverage.h new file mode 100644 index 0000000000..936566ac02 --- /dev/null +++ b/tools/code-coverage/nsCodeCoverage.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef tools_codecoverage_nscodecoverage_h +#define tools_codecoverage_nscodecoverage_h + +#include "nsICodeCoverage.h" + +class nsCodeCoverage final : nsICodeCoverage { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICODECOVERAGE + + nsCodeCoverage(); + + private: + ~nsCodeCoverage(); +}; + +#endif // tools_codecoverage_nscodecoverage_h diff --git a/tools/code-coverage/nsICodeCoverage.idl b/tools/code-coverage/nsICodeCoverage.idl new file mode 100644 index 0000000000..ec4ad40ae1 --- /dev/null +++ b/tools/code-coverage/nsICodeCoverage.idl @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +/** + * The nsICodeCoverage component allows controlling the code coverage counters + * collected by Firefox during execution. + * By flushing the counters, one can analyze the coverage information + * for a subset of the program execution (e.g. startup code coverage). + * + */ + +[scriptable, uuid(57d92056-37b4-4d0a-a52f-deb8f6dac8bc)] +interface nsICodeCoverage : nsISupports +{ + /** + * Write the coverage counters to disk, and reset them in memory to 0. + */ + [implicit_jscontext] + Promise flushCounters(); +}; diff --git a/tools/code-coverage/tests/mochitest/mochitest.ini b/tools/code-coverage/tests/mochitest/mochitest.ini new file mode 100644 index 0000000000..f2c8782e76 --- /dev/null +++ b/tools/code-coverage/tests/mochitest/mochitest.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_coverage_specialpowers.html] diff --git a/tools/code-coverage/tests/mochitest/test_coverage_specialpowers.html b/tools/code-coverage/tests/mochitest/test_coverage_specialpowers.html new file mode 100644 index 0000000000..301206ac48 --- /dev/null +++ b/tools/code-coverage/tests/mochitest/test_coverage_specialpowers.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1380659 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 123456</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1380659 **/ + + SimpleTest.waitForExplicitFinish(); + + (async function() { + await SpecialPowers.requestDumpCoverageCounters(); + SimpleTest.ok(true, "Counters dumped."); + + await SpecialPowers.requestResetCoverageCounters(); + SimpleTest.ok(true, "Counters reset."); + + SimpleTest.finish(); + })(); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1380659">Mozilla Bug 1380659</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/tools/code-coverage/tests/xpcshell/head.js b/tools/code-coverage/tests/xpcshell/head.js new file mode 100644 index 0000000000..3642c5794c --- /dev/null +++ b/tools/code-coverage/tests/xpcshell/head.js @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +function getFiles() { + // This is the directory where gcov is emitting the gcda files. + const jsCoveragePath = Services.env.get("JS_CODE_COVERAGE_OUTPUT_DIR"); + + const jsCoverageDir = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + jsCoverageDir.initWithPath(jsCoveragePath); + + let files = []; + + let entries = jsCoverageDir.directoryEntries; + while (entries.hasMoreElements()) { + files.push(entries.nextFile); + } + + return files; +} + +function diffFiles(files_after, files_before) { + let files_before_set = new Set(files_before.map(file => file.leafName)); + return files_after.filter(file => !files_before_set.has(file.leafName)); +} + +const BASENAME_RE = new RegExp("([^/\\\\]+)$"); + +function parseRecords(files) { + let records = new Map(); + + for (let file of files) { + const lines = Cu.readUTF8File(file).split("\n"); + let currentSF = null; + + for (let line of lines) { + let [recordType, ...recordContent] = line.split(":"); + recordContent = recordContent.join(":"); + + switch (recordType) { + case "FNDA": { + if (currentSF == null) { + throw new Error("SF missing"); + } + + let [hits, name] = recordContent.split(","); + currentSF.push({ + type: "FNDA", + hits, + name, + }); + break; + } + + case "FN": { + if (currentSF == null) { + throw new Error("SF missing"); + } + + let name = recordContent.split(",")[1]; + currentSF.push({ + type: "FN", + name, + }); + break; + } + + case "SF": { + if ( + recordContent.startsWith("resource:") || + recordContent.startsWith("chrome:") + ) { + recordContent = recordContent.split("/").at(-1); + } else { + if (AppConstants.platform == "win") { + recordContent = recordContent.replace(/\//g, "\\"); + } + const match = BASENAME_RE.exec(recordContent); + if (match.length) { + recordContent = match[0]; + } + } + + currentSF = []; + + records.set(recordContent, currentSF); + break; + } + } + } + } + + return records; +} diff --git a/tools/code-coverage/tests/xpcshell/support.js b/tools/code-coverage/tests/xpcshell/support.js new file mode 100644 index 0000000000..9189427111 --- /dev/null +++ b/tools/code-coverage/tests/xpcshell/support.js @@ -0,0 +1,5 @@ +function test_code_coverage_func2() { + return 22; +} + +test_code_coverage_func2(); diff --git a/tools/code-coverage/tests/xpcshell/test_basic.js b/tools/code-coverage/tests/xpcshell/test_basic.js new file mode 100644 index 0000000000..9523a37ca2 --- /dev/null +++ b/tools/code-coverage/tests/xpcshell/test_basic.js @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test_code_coverage_func1() { + return 22; +} + +function test_code_coverage_func2() { + return 22; +} + +async function run_test() { + do_test_pending(); + + Assert.ok("@mozilla.org/tools/code-coverage;1" in Cc); + + const codeCoverageCc = Cc["@mozilla.org/tools/code-coverage;1"]; + Assert.ok(!!codeCoverageCc); + + const codeCoverage = codeCoverageCc.getService(Ci.nsICodeCoverage); + Assert.ok(!!codeCoverage); + + const files_orig = getFiles(); + + test_code_coverage_func1(); + + // Flush counters for the first time, we should see this function executed, but test_code_coverage_func not executed. + await codeCoverage.flushCounters(); + + const first_flush_files = getFiles(); + const first_flush_records = parseRecords( + diffFiles(first_flush_files, files_orig) + ); + + Assert.ok(first_flush_records.has("test_basic.js")); + let fnRecords = first_flush_records + .get("test_basic.js") + .filter(record => record.type == "FN"); + let fndaRecords = first_flush_records + .get("test_basic.js") + .filter(record => record.type == "FNDA"); + Assert.ok(fnRecords.some(record => record.name == "top-level")); + Assert.ok(fnRecords.some(record => record.name == "run_test")); + Assert.ok( + fnRecords.some(record => record.name == "test_code_coverage_func1") + ); + Assert.ok( + fndaRecords.some(record => record.name == "run_test" && record.hits == 1) + ); + Assert.ok( + !fndaRecords.some(record => record.name == "run_test" && record.hits != 1) + ); + Assert.ok( + fndaRecords.some( + record => record.name == "test_code_coverage_func1" && record.hits == 1 + ) + ); + Assert.ok( + !fndaRecords.some( + record => record.name == "test_code_coverage_func1" && record.hits != 1 + ) + ); + Assert.ok( + !fndaRecords.some(record => record.name == "test_code_coverage_func2") + ); + + test_code_coverage_func2(); + + // Flush counters for the second time, we should see this function not executed, but test_code_coverage_func executed. + await codeCoverage.flushCounters(); + + const second_flush_files = getFiles(); + const second_flush_records = parseRecords( + diffFiles(second_flush_files, first_flush_files) + ); + + Assert.ok(second_flush_records.has("test_basic.js")); + fnRecords = second_flush_records + .get("test_basic.js") + .filter(record => record.type == "FN"); + fndaRecords = second_flush_records + .get("test_basic.js") + .filter(record => record.type == "FNDA"); + Assert.ok(fnRecords.some(record => record.name == "top-level")); + Assert.ok(fnRecords.some(record => record.name == "run_test")); + Assert.ok( + fnRecords.some(record => record.name == "test_code_coverage_func1") + ); + Assert.ok( + fnRecords.some(record => record.name == "test_code_coverage_func2") + ); + Assert.ok( + fndaRecords.some( + record => record.name == "test_code_coverage_func1" && record.hits == 0 + ) + ); + Assert.ok( + !fndaRecords.some( + record => record.name == "test_code_coverage_func1" && record.hits != 0 + ) + ); + Assert.ok( + fndaRecords.some( + record => record.name == "test_code_coverage_func2" && record.hits == 1 + ) + ); + Assert.ok( + !fndaRecords.some( + record => record.name == "test_code_coverage_func2" && record.hits != 1 + ) + ); + + do_test_finished(); +} diff --git a/tools/code-coverage/tests/xpcshell/test_basic_child_and_parent.js b/tools/code-coverage/tests/xpcshell/test_basic_child_and_parent.js new file mode 100644 index 0000000000..b2e8f32915 --- /dev/null +++ b/tools/code-coverage/tests/xpcshell/test_basic_child_and_parent.js @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test_code_coverage_func1() { + return 22; +} + +async function run_test() { + do_load_child_test_harness(); + do_test_pending(); + + const codeCoverage = Cc["@mozilla.org/tools/code-coverage;1"].getService( + Ci.nsICodeCoverage + ); + + const files_orig = getFiles(); + + test_code_coverage_func1(); + + await codeCoverage.flushCounters(); + + const first_flush_files = getFiles(); + const first_flush_records = parseRecords( + diffFiles(first_flush_files, files_orig) + ); + + Assert.ok(first_flush_records.has("test_basic_child_and_parent.js")); + Assert.ok(!first_flush_records.has("support.js")); + let fnRecords = first_flush_records + .get("test_basic_child_and_parent.js") + .filter(record => record.type == "FN"); + let fndaRecords = first_flush_records + .get("test_basic_child_and_parent.js") + .filter(record => record.type == "FNDA"); + Assert.ok(fnRecords.some(record => record.name == "top-level")); + Assert.ok(fnRecords.some(record => record.name == "run_test")); + Assert.ok( + fnRecords.some(record => record.name == "test_code_coverage_func1") + ); + Assert.ok( + fndaRecords.some(record => record.name == "run_test" && record.hits == 1) + ); + Assert.ok( + !fndaRecords.some(record => record.name == "run_test" && record.hits != 1) + ); + Assert.ok( + fndaRecords.some( + record => record.name == "test_code_coverage_func1" && record.hits == 1 + ) + ); + Assert.ok( + !fndaRecords.some( + record => record.name == "test_code_coverage_func1" && record.hits != 1 + ) + ); + + sendCommand("load('support.js');", async function() { + await codeCoverage.flushCounters(); + + const second_flush_files = getFiles(); + const second_flush_records = parseRecords( + diffFiles(second_flush_files, first_flush_files) + ); + + Assert.ok(second_flush_records.has("test_basic_child_and_parent.js")); + fnRecords = second_flush_records + .get("test_basic_child_and_parent.js") + .filter(record => record.type == "FN"); + fndaRecords = second_flush_records + .get("test_basic_child_and_parent.js") + .filter(record => record.type == "FNDA"); + Assert.ok(fnRecords.some(record => record.name == "top-level")); + Assert.ok(fnRecords.some(record => record.name == "run_test")); + Assert.ok( + fnRecords.some(record => record.name == "test_code_coverage_func1") + ); + Assert.ok( + fndaRecords.some( + record => record.name == "test_code_coverage_func1" && record.hits == 0 + ) + ); + Assert.ok( + !fndaRecords.some( + record => record.name == "test_code_coverage_func1" && record.hits != 0 + ) + ); + Assert.ok(second_flush_records.has("support.js")); + fnRecords = second_flush_records + .get("support.js") + .filter(record => record.type == "FN"); + fndaRecords = second_flush_records + .get("support.js") + .filter(record => record.type == "FNDA"); + Assert.ok(fnRecords.some(record => record.name == "top-level")); + Assert.ok( + fnRecords.some(record => record.name == "test_code_coverage_func2") + ); + Assert.ok( + fndaRecords.some(record => record.name == "top-level" && record.hits == 1) + ); + Assert.ok( + !fndaRecords.some( + record => record.name == "top-level" && record.hits != 1 + ) + ); + Assert.ok( + fndaRecords.some( + record => record.name == "test_code_coverage_func2" && record.hits == 1 + ) + ); + Assert.ok( + !fndaRecords.some( + record => record.name == "test_code_coverage_func2" && record.hits != 1 + ) + ); + + do_test_finished(); + }); +} diff --git a/tools/code-coverage/tests/xpcshell/xpcshell.ini b/tools/code-coverage/tests/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..93f756be8b --- /dev/null +++ b/tools/code-coverage/tests/xpcshell/xpcshell.ini @@ -0,0 +1,7 @@ +[DEFAULT] +head = head.js +support-files = + support.js + +[test_basic.js] +[test_basic_child_and_parent.js] |