Branch data Line data Source code
1 : : /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 : : // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 : : // SPDX-FileCopyrightText: 2014 Endless Mobile, Inc.
4 : : // SPDX-FileContributor: Authored By: Sam Spilsbury <sam@endlessm.com>
5 : :
6 : : #include <config.h>
7 : :
8 : : #include <string.h> // for strcmp, strlen
9 : :
10 : : #include <new>
11 : :
12 : : #include <gio/gio.h>
13 : : #include <glib-object.h>
14 : :
15 : : #include <js/GCAPI.h> // for JS_AddExtraGCRootsTracer, JS_Remove...
16 : : #include <js/PropertyAndElement.h>
17 : : #include <js/Realm.h>
18 : : #include <js/RootingAPI.h>
19 : : #include <js/TracingAPI.h>
20 : : #include <js/TypeDecls.h>
21 : : #include <js/Utility.h> // for UniqueChars
22 : : #include <js/Value.h>
23 : : #include <js/experimental/CodeCoverage.h> // for EnableCodeCoverage
24 : : #include <jsapi.h> // for JS_WrapObject
25 : : #include <mozilla/Result.h>
26 : : #include <mozilla/Try.h>
27 : :
28 : : #include "gjs/atoms.h"
29 : : #include "gjs/auto.h"
30 : : #include "gjs/context-private.h"
31 : : #include "gjs/context.h"
32 : : #include "gjs/coverage.h"
33 : : #include "gjs/gerror-result.h"
34 : : #include "gjs/global.h"
35 : : #include "gjs/jsapi-util.h"
36 : : #include "gjs/macros.h"
37 : :
38 : : using Gjs::GErrorResult;
39 : : using mozilla::Err, mozilla::Ok;
40 : :
41 : : static bool s_coverage_enabled = false;
42 : :
43 : : struct _GjsCoverage {
44 : : GObject parent;
45 : : };
46 : :
47 : : typedef struct {
48 : : char** prefixes;
49 : : GjsContext* coverage_context;
50 : : JS::Heap<JSObject*> global;
51 : :
52 : : GFile *output_dir;
53 : : } GjsCoveragePrivate;
54 : :
55 [ + + + - : 3072 : G_DEFINE_TYPE_WITH_PRIVATE(GjsCoverage,
+ + ]
56 : : gjs_coverage,
57 : : G_TYPE_OBJECT)
58 : :
59 : : enum {
60 : : PROP_COVERAGE_0,
61 : : PROP_PREFIXES,
62 : : PROP_CONTEXT,
63 : : PROP_CACHE,
64 : : PROP_OUTPUT_DIRECTORY,
65 : : PROP_N
66 : : };
67 : :
68 : : static GParamSpec* properties[PROP_N];
69 : :
70 : : [[nodiscard]]
71 : 793 : static char* get_file_identifier(GFile* source_file) {
72 : 793 : char *path = g_file_get_path(source_file);
73 [ - + ]: 793 : if (!path)
74 : 0 : path = g_file_get_uri(source_file);
75 : 793 : return path;
76 : : }
77 : :
78 : : [[nodiscard]]
79 : 793 : static GErrorResult<> write_source_file_header(GOutputStream* stream,
80 : : GFile* source_file) {
81 : 793 : Gjs::AutoChar path{get_file_identifier(source_file)};
82 : 793 : Gjs::AutoError error;
83 [ - + ]: 793 : if (!g_output_stream_printf(stream, nullptr, nullptr, error.out(),
84 : : "SF:%s\n", path.get()))
85 : 0 : return Err(error.release());
86 : 793 : return Ok{};
87 : 793 : }
88 : :
89 : : [[nodiscard]]
90 : 793 : static GErrorResult<> copy_source_file_to_coverage_output(
91 : : GFile* source_file, GFile* destination_file) {
92 : : /* We need to recursively make the directory we want to copy to, as
93 : : * g_file_copy doesn't do that */
94 : 793 : Gjs::AutoError error;
95 : 793 : Gjs::AutoUnref<GFile> destination_dir{g_file_get_parent(destination_file)};
96 [ + + ]: 793 : if (!g_file_make_directory_with_parents(destination_dir, nullptr,
97 : : error.out())) {
98 [ - + ]: 784 : if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_EXISTS))
99 : 0 : return Err(error.release());
100 : 784 : error.reset();
101 : : }
102 : :
103 [ - + ]: 793 : if (!g_file_copy(source_file, destination_file, G_FILE_COPY_OVERWRITE,
104 : : nullptr, nullptr, nullptr, error.out()))
105 : 0 : return Err(error.release());
106 : 793 : return Ok{};
107 : 793 : }
108 : :
109 : : // This function will strip a URI scheme and return the string with the URI
110 : : // scheme stripped or nullptr if the path was not a valid URI
111 : : [[nodiscard]]
112 : 770 : static char* strip_uri_scheme(const char* potential_uri) {
113 : 770 : char *uri_header = g_uri_parse_scheme(potential_uri);
114 : :
115 [ + - ]: 770 : if (uri_header) {
116 : 770 : size_t offset = strlen(uri_header);
117 : 770 : g_free(uri_header);
118 : :
119 : : /* g_uri_parse_scheme() only parses the name of the scheme, we also need
120 : : * to strip the characters ':///' */
121 : 1540 : return g_strdup(potential_uri + offset + 4);
122 : : }
123 : :
124 : 0 : return nullptr;
125 : : }
126 : :
127 : : /* This function will return a string of pathname components from the first
128 : : * directory indicating where two directories diverge. For instance:
129 : : *
130 : : * child: /a/b/c/d/e
131 : : * parent: /a/b/d/
132 : : *
133 : : * Will return: c/d/e
134 : : *
135 : : * If the directories are not at all similar then the full dirname of the
136 : : * child_path effectively be returned.
137 : : *
138 : : * As a special case, child paths that are a URI automatically return the full
139 : : * URI path with the URI scheme and leading slash stripped out.
140 : : */
141 : : [[nodiscard]]
142 : 793 : static char* find_diverging_child_components(GFile* child, GFile* parent) {
143 : 793 : Gjs::AutoUnref<GFile> ancestor{parent, Gjs::TakeOwnership{}};
144 [ + + ]: 5426 : while (ancestor) {
145 : 4656 : char *relpath = g_file_get_relative_path(ancestor, child);
146 [ + + ]: 4656 : if (relpath)
147 : 23 : return relpath;
148 : :
149 : 4633 : ancestor = g_file_get_parent(ancestor);
150 : : }
151 : :
152 : : /* This is a special case of getting the URI below. The difference is that
153 : : * this gives you a regular path name; getting it through the URI would give
154 : : * a URI-encoded path (%20 for spaces, etc.) */
155 : 770 : Gjs::AutoUnref<GFile> root{g_file_new_for_path("/")};
156 : 770 : char* child_path = g_file_get_relative_path(root, child);
157 [ - + ]: 770 : if (child_path)
158 : 0 : return child_path;
159 : :
160 : 770 : Gjs::AutoChar child_uri{g_file_get_uri(child)};
161 : 770 : return strip_uri_scheme(child_uri);
162 : 793 : }
163 : :
164 : : [[nodiscard]]
165 : 1319 : static bool filename_has_coverage_prefixes(GjsCoverage* self,
166 : : const char* filename) {
167 : : auto* priv = static_cast<GjsCoveragePrivate*>(
168 : 1319 : gjs_coverage_get_instance_private(self));
169 : 1319 : Gjs::AutoChar workdir{g_get_current_dir()};
170 : 1319 : Gjs::AutoChar abs_filename{g_canonicalize_filename(filename, workdir)};
171 : :
172 [ + + ]: 1861 : for (const char * const *prefix = priv->prefixes; *prefix; prefix++) {
173 : 1335 : Gjs::AutoChar abs_prefix{g_canonicalize_filename(*prefix, workdir)};
174 [ + + ]: 1335 : if (g_str_has_prefix(abs_filename, abs_prefix))
175 : 793 : return true;
176 [ + + ]: 1335 : }
177 : 526 : return false;
178 : 1319 : }
179 : :
180 : : [[nodiscard]]
181 : 220138 : static inline GErrorResult<> write_line(GOutputStream* out, const char* line) {
182 : 220138 : Gjs::AutoError error;
183 [ - + ]: 220138 : if (!g_output_stream_printf(out, nullptr, nullptr, error.out(), "%s\n",
184 : : line))
185 : 0 : return Err(error.release());
186 : 220138 : return Ok{};
187 : 220138 : }
188 : :
189 : : [[nodiscard]]
190 : 75 : GErrorResult<Gjs::AutoUnref<GFile>> write_statistics_internal(GjsCoverage* self,
191 : : JSContext* cx) {
192 [ - + ]: 75 : if (!s_coverage_enabled) {
193 : 0 : g_critical(
194 : : "Code coverage requested, but gjs_coverage_enable() was not called."
195 : : " You must call this function before creating any GjsContext.");
196 : 0 : return Gjs::AutoUnref<GFile>{};
197 : : }
198 : :
199 : : auto* priv = static_cast<GjsCoveragePrivate*>(
200 : 75 : gjs_coverage_get_instance_private(self));
201 : :
202 : : // Create output directory if it doesn't exist
203 : 75 : Gjs::AutoError error;
204 [ + + ]: 75 : if (!g_file_make_directory_with_parents(priv->output_dir, nullptr,
205 : : error.out())) {
206 [ - + ]: 73 : if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_EXISTS))
207 : 0 : return Err(error.release());
208 : 73 : error.reset();
209 : : }
210 : :
211 : 75 : GFile *output_file = g_file_get_child(priv->output_dir, "coverage.lcov");
212 : :
213 : : size_t lcov_length;
214 : 75 : JS::UniqueChars lcov = js::GetCodeCoverageSummary(cx, &lcov_length);
215 : :
216 : 75 : Gjs::AutoUnref<GOutputStream> ostream{G_OUTPUT_STREAM(g_file_append_to(
217 : 75 : output_file, G_FILE_CREATE_NONE, nullptr, error.out()))};
218 [ - + ]: 75 : if (!ostream)
219 : 0 : return Err(error.release());
220 : :
221 : 75 : Gjs::AutoStrv lcov_lines{g_strsplit(lcov.get(), "\n", -1)};
222 : 75 : const char* test_name = nullptr;
223 : 75 : bool ignoring_file = false;
224 : :
225 [ + + ]: 632643 : for (const char * const *iter = lcov_lines.get(); *iter; iter++) {
226 [ + + ]: 632568 : if (ignoring_file) {
227 [ + + ]: 411829 : if (strcmp(*iter, "end_of_record") == 0)
228 : 526 : ignoring_file = false;
229 : 411829 : continue;
230 : : }
231 : :
232 [ - + + + : 220739 : if (g_str_has_prefix(*iter, "TN:")) {
+ + ]
233 : : /* Don't write the test name if the next line shows we are ignoring
234 : : * the source file */
235 : 75 : test_name = *iter;
236 : 75 : continue;
237 [ - + + + : 220664 : } else if (g_str_has_prefix(*iter, "SF:")) {
+ + ]
238 : 1319 : const char *filename = *iter + 3;
239 [ + + ]: 1319 : if (!filename_has_coverage_prefixes(self, filename)) {
240 : 526 : ignoring_file = true;
241 : 1319 : continue;
242 : : }
243 : :
244 : : // Now we can write the test name before writing the source file
245 [ - + + - ]: 793 : MOZ_TRY(write_line(ostream, test_name));
246 : :
247 : : /* The source file could be a resource, so we must use
248 : : * g_file_new_for_commandline_arg() to disambiguate between URIs and
249 : : * filesystem paths. */
250 : : Gjs::AutoUnref<GFile> source_file{
251 : 793 : g_file_new_for_commandline_arg(filename)};
252 : : Gjs::AutoChar diverged_paths{
253 : 793 : find_diverging_child_components(source_file, priv->output_dir)};
254 : : Gjs::AutoUnref<GFile> destination_file{
255 : 793 : g_file_resolve_relative_path(priv->output_dir, diverged_paths)};
256 [ - + + - ]: 793 : MOZ_TRY(copy_source_file_to_coverage_output(source_file,
257 : : destination_file));
258 : :
259 : : /* Rewrite the source file path to be relative to the output dir so
260 : : * that genhtml will find it */
261 [ - + + - ]: 793 : MOZ_TRY(write_source_file_header(ostream, destination_file));
262 : 793 : continue;
263 [ - + - + : 2379 : }
+ - ]
264 : :
265 [ - + + - ]: 219345 : MOZ_TRY(write_line(ostream, *iter));
266 : : }
267 : :
268 : 75 : return Gjs::AutoUnref<GFile>{output_file};
269 : 75 : }
270 : :
271 : : /**
272 : : * gjs_coverage_write_statistics:
273 : : * @self: A #GjsCoverage
274 : : * @output_directory: A directory to write coverage information to.
275 : : *
276 : : * Scripts which were provided as part of the #GjsCoverage:prefixes construction
277 : : * property will be written out to @output_directory, in the same directory
278 : : * structure relative to the source dir where the tests were run.
279 : : *
280 : : * This function takes all available statistics and writes them out to either
281 : : * the file provided or to files of the pattern (filename).info in the same
282 : : * directory as the scanned files. It will provide coverage data for all files
283 : : * ending with ".js" in the coverage directories.
284 : : */
285 : 75 : void gjs_coverage_write_statistics(GjsCoverage* self) {
286 : : auto* priv = static_cast<GjsCoveragePrivate*>(
287 : 75 : gjs_coverage_get_instance_private(self));
288 : : auto* cx = static_cast<JSContext*>(
289 : 75 : gjs_context_get_native_context(priv->coverage_context));
290 : 75 : Gjs::AutoMainRealm ar{cx};
291 : :
292 : : GErrorResult<Gjs::AutoUnref<GFile>> result{
293 : 75 : write_statistics_internal(self, cx)};
294 [ - + ]: 75 : if (result.isErr()) {
295 : 0 : g_critical("Error writing coverage data: %s",
296 : : result.inspectErr()->message);
297 : 0 : return;
298 : : }
299 : :
300 : 75 : Gjs::AutoChar output_file_path{g_file_get_path(result.unwrap())};
301 : 75 : g_message("Wrote coverage statistics to %s", output_file_path.get());
302 [ + - + - ]: 75 : }
303 : :
304 : 79 : static void gjs_coverage_init(GjsCoverage*) {
305 [ - + ]: 79 : if (!s_coverage_enabled)
306 : 0 : g_critical(
307 : : "Code coverage requested, but gjs_coverage_enable() was not called."
308 : : " You must call this function before creating any GjsContext.");
309 : 79 : }
310 : :
311 : 77 : static void coverage_tracer(JSTracer* trc, void* data) {
312 : 77 : GjsCoverage* self = GJS_COVERAGE(data);
313 : : auto* priv = static_cast<GjsCoveragePrivate*>(
314 : 77 : gjs_coverage_get_instance_private(self));
315 : :
316 : 77 : JS::TraceEdge<JSObject*>(trc, &priv->global, "Coverage global object");
317 : 77 : }
318 : :
319 : : GJS_JSAPI_RETURN_CONVENTION
320 : 79 : static bool bootstrap_coverage(GjsCoverage* self) {
321 : : auto* priv = static_cast<GjsCoveragePrivate*>(
322 : 79 : gjs_coverage_get_instance_private(self));
323 : :
324 : 79 : auto* gjs = GjsContextPrivate::from_object(priv->coverage_context);
325 : 79 : JSContext* cx = gjs->context();
326 : :
327 : : JS::RootedObject debugger_global{
328 : 79 : cx, gjs_create_global_object(cx, GjsGlobalType::DEBUGGER)};
329 : : {
330 : 79 : JSAutoRealm ar{cx, debugger_global};
331 : 79 : JS::RootedObject debuggee{cx, gjs->global()};
332 [ - + ]: 79 : if (!JS_WrapObject(cx, &debuggee))
333 : 0 : return false;
334 : :
335 : 79 : JS::RootedValue v_debuggee{cx, JS::ObjectValue(*debuggee)};
336 : 79 : if (!JS_SetPropertyById(cx, debugger_global, gjs->atoms().debuggee(),
337 [ + - ]: 237 : v_debuggee) ||
338 [ - + - + ]: 158 : !gjs_define_global_properties(cx, debugger_global,
339 : : GjsGlobalType::DEBUGGER,
340 : : "GJS coverage", "coverage"))
341 : 0 : return false;
342 : :
343 : : // Add a tracer, as suggested by jdm on #jsapi
344 : 79 : JS_AddExtraGCRootsTracer(cx, coverage_tracer, self);
345 : :
346 : 79 : priv->global = debugger_global;
347 [ + - + - : 79 : }
+ - ]
348 : :
349 : 79 : return true;
350 : 79 : }
351 : :
352 : 79 : static void gjs_coverage_constructed(GObject* object) {
353 : 79 : G_OBJECT_CLASS(gjs_coverage_parent_class)->constructed(object);
354 : :
355 : 79 : GjsCoverage* self = GJS_COVERAGE(object);
356 : : auto* priv = static_cast<GjsCoveragePrivate*>(
357 : 79 : gjs_coverage_get_instance_private(self));
358 : 79 : new (&priv->global) JS::Heap<JSObject*>();
359 : :
360 [ - + ]: 79 : if (!bootstrap_coverage(self)) {
361 : : JSContext* cx = static_cast<JSContext*>(
362 : 0 : gjs_context_get_native_context(priv->coverage_context));
363 : 0 : Gjs::AutoMainRealm ar{cx};
364 : 0 : gjs_log_exception(cx);
365 : 0 : }
366 : 79 : }
367 : :
368 : 316 : static void gjs_coverage_set_property(GObject* object, unsigned prop_id,
369 : : const GValue* value, GParamSpec* pspec) {
370 : 316 : GjsCoverage* self = GJS_COVERAGE(object);
371 : : auto* priv = static_cast<GjsCoveragePrivate*>(
372 : 316 : gjs_coverage_get_instance_private(self));
373 [ + + + + : 316 : switch (prop_id) {
- ]
374 : 79 : case PROP_PREFIXES:
375 : 79 : g_assert(priv->prefixes == nullptr);
376 : 79 : priv->prefixes = static_cast<char**>(g_value_dup_boxed(value));
377 : 79 : break;
378 : 79 : case PROP_CONTEXT:
379 : 79 : priv->coverage_context = GJS_CONTEXT(g_value_dup_object(value));
380 : 79 : break;
381 : 79 : case PROP_CACHE:
382 : 79 : break;
383 : 79 : case PROP_OUTPUT_DIRECTORY:
384 : 79 : priv->output_dir = G_FILE(g_value_dup_object(value));
385 : 79 : break;
386 : 0 : default:
387 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
388 : 0 : break;
389 : : }
390 : 316 : }
391 : :
392 : 79 : static void gjs_coverage_dispose(GObject* object) {
393 : 79 : GjsCoverage* self = GJS_COVERAGE(object);
394 : : auto* priv = static_cast<GjsCoveragePrivate*>(
395 : 79 : gjs_coverage_get_instance_private(self));
396 : :
397 : : /* Decomission objects inside of the JSContext before disposing of the
398 : : * context */
399 : : auto* cx = static_cast<JSContext*>(
400 : 79 : gjs_context_get_native_context(priv->coverage_context));
401 : 79 : JS_RemoveExtraGCRootsTracer(cx, coverage_tracer, self);
402 : 79 : priv->global = nullptr;
403 : :
404 [ + - ]: 79 : g_clear_object(&priv->coverage_context);
405 : :
406 : 79 : G_OBJECT_CLASS(gjs_coverage_parent_class)->dispose(object);
407 : 79 : }
408 : :
409 : 79 : static void gjs_coverage_finalize(GObject* object) {
410 : 79 : GjsCoverage* self = GJS_COVERAGE(object);
411 : : auto* priv = static_cast<GjsCoveragePrivate*>(
412 : 79 : gjs_coverage_get_instance_private(self));
413 : :
414 : 79 : g_strfreev(priv->prefixes);
415 [ + - ]: 79 : g_clear_object(&priv->output_dir);
416 : 79 : priv->global.~Heap();
417 : :
418 : 79 : G_OBJECT_CLASS(gjs_coverage_parent_class)->finalize(object);
419 : 79 : }
420 : :
421 : 53 : static void gjs_coverage_class_init(GjsCoverageClass* klass) {
422 : 53 : GObjectClass* object_class = G_OBJECT_CLASS(klass);
423 : :
424 : 53 : object_class->constructed = gjs_coverage_constructed;
425 : 53 : object_class->dispose = gjs_coverage_dispose;
426 : 53 : object_class->finalize = gjs_coverage_finalize;
427 : 53 : object_class->set_property = gjs_coverage_set_property;
428 : :
429 : 53 : properties[PROP_PREFIXES] = g_param_spec_boxed(
430 : : "prefixes", "Prefixes",
431 : : "Prefixes of files on which to perform coverage analysis", G_TYPE_STRV,
432 : : (GParamFlags)(G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE));
433 : 53 : properties[PROP_CONTEXT] = g_param_spec_object(
434 : : "context", "Context", "A context to gather coverage stats for",
435 : : GJS_TYPE_CONTEXT,
436 : : (GParamFlags)(G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE));
437 : 53 : properties[PROP_CACHE] = g_param_spec_object(
438 : : "cache", "Deprecated property", "Has no effect", G_TYPE_FILE,
439 : : (GParamFlags)(G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE |
440 : : G_PARAM_DEPRECATED));
441 : 53 : properties[PROP_OUTPUT_DIRECTORY] = g_param_spec_object(
442 : : "output-directory", "Output directory",
443 : : "Directory handle at which to output coverage statistics", G_TYPE_FILE,
444 : : (GParamFlags)(G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE |
445 : : G_PARAM_STATIC_STRINGS));
446 : :
447 : 53 : g_object_class_install_properties(object_class,
448 : : PROP_N,
449 : : properties);
450 : 53 : }
451 : :
452 : : /**
453 : : * gjs_coverage_new:
454 : : * @prefixes: A null-terminated strv of prefixes of files on which to record
455 : : * code coverage
456 : : * @coverage_context: A #GjsContext object
457 : : * @output_dir: A #GFile handle to a directory in which to write coverage
458 : : * information
459 : : *
460 : : * Creates a new #GjsCoverage object that collects coverage information for
461 : : * any scripts run in @coverage_context.
462 : : *
463 : : * Scripts which were provided as part of @prefixes will be written out to
464 : : * @output_dir, in the same directory structure relative to the source dir where
465 : : * the tests were run.
466 : : *
467 : : * Returns: A #GjsCoverage object
468 : : */
469 : 79 : GjsCoverage* gjs_coverage_new(const char* const* prefixes,
470 : : GjsContext* coverage_context, GFile* output_dir) {
471 : 79 : return GJS_COVERAGE(g_object_new(GJS_TYPE_COVERAGE, "prefixes", prefixes,
472 : : "context", coverage_context,
473 : 79 : "output-directory", output_dir, nullptr));
474 : : }
475 : :
476 : : /**
477 : : * gjs_coverage_enable:
478 : : *
479 : : * This function must be called before creating any #GjsContext, if you intend
480 : : * to use any #GjsCoverage APIs.
481 : : *
482 : : * Since: 1.66
483 : : */
484 : 75 : void gjs_coverage_enable() {
485 : 75 : js::EnableCodeCoverage();
486 : 75 : s_coverage_enabled = true;
487 : 75 : }
|