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