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