summaryrefslogtreecommitdiffstats
path: root/tools/code-coverage
diff options
context:
space:
mode:
Diffstat (limited to 'tools/code-coverage')
-rw-r--r--tools/code-coverage/CodeCoverageHandler.cpp158
-rw-r--r--tools/code-coverage/CodeCoverageHandler.h38
-rw-r--r--tools/code-coverage/PerTestCoverageUtils.sys.mjs88
-rw-r--r--tools/code-coverage/components.conf14
-rw-r--r--tools/code-coverage/docs/index.rst209
-rw-r--r--tools/code-coverage/moz.build39
-rw-r--r--tools/code-coverage/nsCodeCoverage.cpp96
-rw-r--r--tools/code-coverage/nsCodeCoverage.h22
-rw-r--r--tools/code-coverage/nsICodeCoverage.idl24
-rw-r--r--tools/code-coverage/tests/mochitest/mochitest.toml3
-rw-r--r--tools/code-coverage/tests/mochitest/test_coverage_specialpowers.html38
-rw-r--r--tools/code-coverage/tests/xpcshell/head.js100
-rw-r--r--tools/code-coverage/tests/xpcshell/support.js5
-rw-r--r--tools/code-coverage/tests/xpcshell/test_basic.js115
-rw-r--r--tools/code-coverage/tests/xpcshell/test_basic_child_and_parent.js120
-rw-r--r--tools/code-coverage/tests/xpcshell/xpcshell.toml7
16 files changed, 1076 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.sys.mjs b/tools/code-coverage/PerTestCoverageUtils.sys.mjs
new file mode 100644
index 0000000000..179dd905a5
--- /dev/null
+++ b/tools/code-coverage/PerTestCoverageUtils.sys.mjs
@@ -0,0 +1,88 @@
+/* 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/. */
+
+// 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");
+
+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.sys.mjs:awaitPromise",
+ () => complete
+ );
+ if (error) {
+ throw new Error(error);
+ }
+ return ret;
+}
+
+export 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).
+ await IOUtils.remove(gcovPrefixPath, {
+ recursive: true,
+ ignoreAbsent: true,
+ });
+ await IOUtils.remove(jsvmPrefixPath, {
+ recursive: true,
+ ignoreAbsent: true,
+ });
+
+ // Move coverage files from the GCOV_RESULTS_DIR and JSVM_RESULTS_DIR directories, so we can accumulate the counters.
+ await IOUtils.move(gcovResultsPath, gcovPrefixPath);
+ await IOUtils.move(jsvmResultsPath, jsvmPrefixPath);
+ }
+
+ 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.
+ await IOUtils.move(gcovPrefixPath, gcovResultsPath);
+ await IOUtils.move(jsvmPrefixPath, jsvmResultsPath);
+ }
+
+ 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..a4df736a72
--- /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.sys.mjs"]
+
+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.toml"]
+ MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.toml"]
+
+ 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.toml b/tools/code-coverage/tests/mochitest/mochitest.toml
new file mode 100644
index 0000000000..66f6fd02d1
--- /dev/null
+++ b/tools/code-coverage/tests/mochitest/mochitest.toml
@@ -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..f074c20776
--- /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.toml b/tools/code-coverage/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..e1aacfeada
--- /dev/null
+++ b/tools/code-coverage/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+head = "head.js"
+support-files = ["support.js"]
+
+["test_basic.js"]
+
+["test_basic_child_and_parent.js"]