coverage_html.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. // Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
  2. // For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
  3. // Coverage.py HTML report browser code.
  4. /*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */
  5. /*global coverage: true, document, window, $ */
  6. coverage = {};
  7. // Find all the elements with shortkey_* class, and use them to assign a shortcut key.
  8. coverage.assign_shortkeys = function () {
  9. $("*[class*='shortkey_']").each(function (i, e) {
  10. $.each($(e).attr("class").split(" "), function (i, c) {
  11. if (/^shortkey_/.test(c)) {
  12. $(document).bind('keydown', c.substr(9), function () {
  13. $(e).click();
  14. });
  15. }
  16. });
  17. });
  18. };
  19. // Create the events for the help panel.
  20. coverage.wire_up_help_panel = function () {
  21. $("#keyboard_icon").click(function () {
  22. // Show the help panel, and position it so the keyboard icon in the
  23. // panel is in the same place as the keyboard icon in the header.
  24. $(".help_panel").show();
  25. var koff = $("#keyboard_icon").offset();
  26. var poff = $("#panel_icon").position();
  27. $(".help_panel").offset({
  28. top: koff.top-poff.top,
  29. left: koff.left-poff.left
  30. });
  31. });
  32. $("#panel_icon").click(function () {
  33. $(".help_panel").hide();
  34. });
  35. };
  36. // Create the events for the filter box.
  37. coverage.wire_up_filter = function () {
  38. // Cache elements.
  39. var table = $("table.index");
  40. var table_rows = table.find("tbody tr");
  41. var table_row_names = table_rows.find("td.name a");
  42. var no_rows = $("#no_rows");
  43. // Create a duplicate table footer that we can modify with dynamic summed values.
  44. var table_footer = $("table.index tfoot tr");
  45. var table_dynamic_footer = table_footer.clone();
  46. table_dynamic_footer.attr('class', 'total_dynamic hidden');
  47. table_footer.after(table_dynamic_footer);
  48. // Observe filter keyevents.
  49. $("#filter").on("keyup change", $.debounce(150, function (event) {
  50. var filter_value = $(this).val();
  51. if (filter_value === "") {
  52. // Filter box is empty, remove all filtering.
  53. table_rows.removeClass("hidden");
  54. // Show standard footer, hide dynamic footer.
  55. table_footer.removeClass("hidden");
  56. table_dynamic_footer.addClass("hidden");
  57. // Hide placeholder, show table.
  58. if (no_rows.length > 0) {
  59. no_rows.hide();
  60. }
  61. table.show();
  62. }
  63. else {
  64. // Filter table items by value.
  65. var hidden = 0;
  66. var shown = 0;
  67. // Hide / show elements.
  68. $.each(table_row_names, function () {
  69. var element = $(this).parents("tr");
  70. if ($(this).text().indexOf(filter_value) === -1) {
  71. // hide
  72. element.addClass("hidden");
  73. hidden++;
  74. }
  75. else {
  76. // show
  77. element.removeClass("hidden");
  78. shown++;
  79. }
  80. });
  81. // Show placeholder if no rows will be displayed.
  82. if (no_rows.length > 0) {
  83. if (shown === 0) {
  84. // Show placeholder, hide table.
  85. no_rows.show();
  86. table.hide();
  87. }
  88. else {
  89. // Hide placeholder, show table.
  90. no_rows.hide();
  91. table.show();
  92. }
  93. }
  94. // Manage dynamic header:
  95. if (hidden > 0) {
  96. // Calculate new dynamic sum values based on visible rows.
  97. for (var column = 2; column < 20; column++) {
  98. // Calculate summed value.
  99. var cells = table_rows.find('td:nth-child(' + column + ')');
  100. if (!cells.length) {
  101. // No more columns...!
  102. break;
  103. }
  104. var sum = 0, numer = 0, denom = 0;
  105. $.each(cells.filter(':visible'), function () {
  106. var ratio = $(this).data("ratio");
  107. if (ratio) {
  108. var splitted = ratio.split(" ");
  109. numer += parseInt(splitted[0], 10);
  110. denom += parseInt(splitted[1], 10);
  111. }
  112. else {
  113. sum += parseInt(this.innerHTML, 10);
  114. }
  115. });
  116. // Get footer cell element.
  117. var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')');
  118. // Set value into dynamic footer cell element.
  119. if (cells[0].innerHTML.indexOf('%') > -1) {
  120. // Percentage columns use the numerator and denominator,
  121. // and adapt to the number of decimal places.
  122. var match = /\.([0-9]+)/.exec(cells[0].innerHTML);
  123. var places = 0;
  124. if (match) {
  125. places = match[1].length;
  126. }
  127. var pct = numer * 100 / denom;
  128. footer_cell.text(pct.toFixed(places) + '%');
  129. }
  130. else {
  131. footer_cell.text(sum);
  132. }
  133. }
  134. // Hide standard footer, show dynamic footer.
  135. table_footer.addClass("hidden");
  136. table_dynamic_footer.removeClass("hidden");
  137. }
  138. else {
  139. // Show standard footer, hide dynamic footer.
  140. table_footer.removeClass("hidden");
  141. table_dynamic_footer.addClass("hidden");
  142. }
  143. }
  144. }));
  145. // Trigger change event on setup, to force filter on page refresh
  146. // (filter value may still be present).
  147. $("#filter").trigger("change");
  148. };
  149. // Loaded on index.html
  150. coverage.index_ready = function ($) {
  151. // Look for a localStorage item containing previous sort settings:
  152. var sort_list = [];
  153. var storage_name = "COVERAGE_INDEX_SORT";
  154. var stored_list = undefined;
  155. try {
  156. stored_list = localStorage.getItem(storage_name);
  157. } catch(err) {}
  158. if (stored_list) {
  159. sort_list = JSON.parse('[[' + stored_list + ']]');
  160. }
  161. // Create a new widget which exists only to save and restore
  162. // the sort order:
  163. $.tablesorter.addWidget({
  164. id: "persistentSort",
  165. // Format is called by the widget before displaying:
  166. format: function (table) {
  167. if (table.config.sortList.length === 0 && sort_list.length > 0) {
  168. // This table hasn't been sorted before - we'll use
  169. // our stored settings:
  170. $(table).trigger('sorton', [sort_list]);
  171. }
  172. else {
  173. // This is not the first load - something has
  174. // already defined sorting so we'll just update
  175. // our stored value to match:
  176. sort_list = table.config.sortList;
  177. }
  178. }
  179. });
  180. // Configure our tablesorter to handle the variable number of
  181. // columns produced depending on report options:
  182. var headers = [];
  183. var col_count = $("table.index > thead > tr > th").length;
  184. headers[0] = { sorter: 'text' };
  185. for (i = 1; i < col_count-1; i++) {
  186. headers[i] = { sorter: 'digit' };
  187. }
  188. headers[col_count-1] = { sorter: 'percent' };
  189. // Enable the table sorter:
  190. $("table.index").tablesorter({
  191. widgets: ['persistentSort'],
  192. headers: headers
  193. });
  194. coverage.assign_shortkeys();
  195. coverage.wire_up_help_panel();
  196. coverage.wire_up_filter();
  197. // Watch for page unload events so we can save the final sort settings:
  198. $(window).unload(function () {
  199. try {
  200. localStorage.setItem(storage_name, sort_list.toString())
  201. } catch(err) {}
  202. });
  203. };
  204. // -- pyfile stuff --
  205. coverage.pyfile_ready = function ($) {
  206. // If we're directed to a particular line number, highlight the line.
  207. var frag = location.hash;
  208. if (frag.length > 2 && frag[1] === 't') {
  209. $(frag).addClass('highlight');
  210. coverage.set_sel(parseInt(frag.substr(2), 10));
  211. }
  212. else {
  213. coverage.set_sel(0);
  214. }
  215. $(document)
  216. .bind('keydown', 'j', coverage.to_next_chunk_nicely)
  217. .bind('keydown', 'k', coverage.to_prev_chunk_nicely)
  218. .bind('keydown', '0', coverage.to_top)
  219. .bind('keydown', '1', coverage.to_first_chunk)
  220. ;
  221. $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");});
  222. $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");});
  223. $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");});
  224. $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");});
  225. coverage.assign_shortkeys();
  226. coverage.wire_up_help_panel();
  227. coverage.init_scroll_markers();
  228. // Rebuild scroll markers when the window height changes.
  229. $(window).resize(coverage.build_scroll_markers);
  230. };
  231. coverage.toggle_lines = function (btn, cls) {
  232. btn = $(btn);
  233. var show = "show_"+cls;
  234. if (btn.hasClass(show)) {
  235. $("#source ." + cls).removeClass(show);
  236. btn.removeClass(show);
  237. }
  238. else {
  239. $("#source ." + cls).addClass(show);
  240. btn.addClass(show);
  241. }
  242. coverage.build_scroll_markers();
  243. };
  244. // Return the nth line div.
  245. coverage.line_elt = function (n) {
  246. return $("#t" + n);
  247. };
  248. // Return the nth line number div.
  249. coverage.num_elt = function (n) {
  250. return $("#n" + n);
  251. };
  252. // Set the selection. b and e are line numbers.
  253. coverage.set_sel = function (b, e) {
  254. // The first line selected.
  255. coverage.sel_begin = b;
  256. // The next line not selected.
  257. coverage.sel_end = (e === undefined) ? b+1 : e;
  258. };
  259. coverage.to_top = function () {
  260. coverage.set_sel(0, 1);
  261. coverage.scroll_window(0);
  262. };
  263. coverage.to_first_chunk = function () {
  264. coverage.set_sel(0, 1);
  265. coverage.to_next_chunk();
  266. };
  267. // Return a string indicating what kind of chunk this line belongs to,
  268. // or null if not a chunk.
  269. coverage.chunk_indicator = function (line_elt) {
  270. var klass = line_elt.attr('class');
  271. if (klass) {
  272. var m = klass.match(/\bshow_\w+\b/);
  273. if (m) {
  274. return m[0];
  275. }
  276. }
  277. return null;
  278. };
  279. coverage.to_next_chunk = function () {
  280. var c = coverage;
  281. // Find the start of the next colored chunk.
  282. var probe = c.sel_end;
  283. var chunk_indicator, probe_line;
  284. while (true) {
  285. probe_line = c.line_elt(probe);
  286. if (probe_line.length === 0) {
  287. return;
  288. }
  289. chunk_indicator = c.chunk_indicator(probe_line);
  290. if (chunk_indicator) {
  291. break;
  292. }
  293. probe++;
  294. }
  295. // There's a next chunk, `probe` points to it.
  296. var begin = probe;
  297. // Find the end of this chunk.
  298. var next_indicator = chunk_indicator;
  299. while (next_indicator === chunk_indicator) {
  300. probe++;
  301. probe_line = c.line_elt(probe);
  302. next_indicator = c.chunk_indicator(probe_line);
  303. }
  304. c.set_sel(begin, probe);
  305. c.show_selection();
  306. };
  307. coverage.to_prev_chunk = function () {
  308. var c = coverage;
  309. // Find the end of the prev colored chunk.
  310. var probe = c.sel_begin-1;
  311. var probe_line = c.line_elt(probe);
  312. if (probe_line.length === 0) {
  313. return;
  314. }
  315. var chunk_indicator = c.chunk_indicator(probe_line);
  316. while (probe > 0 && !chunk_indicator) {
  317. probe--;
  318. probe_line = c.line_elt(probe);
  319. if (probe_line.length === 0) {
  320. return;
  321. }
  322. chunk_indicator = c.chunk_indicator(probe_line);
  323. }
  324. // There's a prev chunk, `probe` points to its last line.
  325. var end = probe+1;
  326. // Find the beginning of this chunk.
  327. var prev_indicator = chunk_indicator;
  328. while (prev_indicator === chunk_indicator) {
  329. probe--;
  330. probe_line = c.line_elt(probe);
  331. prev_indicator = c.chunk_indicator(probe_line);
  332. }
  333. c.set_sel(probe+1, end);
  334. c.show_selection();
  335. };
  336. // Return the line number of the line nearest pixel position pos
  337. coverage.line_at_pos = function (pos) {
  338. var l1 = coverage.line_elt(1),
  339. l2 = coverage.line_elt(2),
  340. result;
  341. if (l1.length && l2.length) {
  342. var l1_top = l1.offset().top,
  343. line_height = l2.offset().top - l1_top,
  344. nlines = (pos - l1_top) / line_height;
  345. if (nlines < 1) {
  346. result = 1;
  347. }
  348. else {
  349. result = Math.ceil(nlines);
  350. }
  351. }
  352. else {
  353. result = 1;
  354. }
  355. return result;
  356. };
  357. // Returns 0, 1, or 2: how many of the two ends of the selection are on
  358. // the screen right now?
  359. coverage.selection_ends_on_screen = function () {
  360. if (coverage.sel_begin === 0) {
  361. return 0;
  362. }
  363. var top = coverage.line_elt(coverage.sel_begin);
  364. var next = coverage.line_elt(coverage.sel_end-1);
  365. return (
  366. (top.isOnScreen() ? 1 : 0) +
  367. (next.isOnScreen() ? 1 : 0)
  368. );
  369. };
  370. coverage.to_next_chunk_nicely = function () {
  371. coverage.finish_scrolling();
  372. if (coverage.selection_ends_on_screen() === 0) {
  373. // The selection is entirely off the screen: select the top line on
  374. // the screen.
  375. var win = $(window);
  376. coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop()));
  377. }
  378. coverage.to_next_chunk();
  379. };
  380. coverage.to_prev_chunk_nicely = function () {
  381. coverage.finish_scrolling();
  382. if (coverage.selection_ends_on_screen() === 0) {
  383. var win = $(window);
  384. coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height()));
  385. }
  386. coverage.to_prev_chunk();
  387. };
  388. // Select line number lineno, or if it is in a colored chunk, select the
  389. // entire chunk
  390. coverage.select_line_or_chunk = function (lineno) {
  391. var c = coverage;
  392. var probe_line = c.line_elt(lineno);
  393. if (probe_line.length === 0) {
  394. return;
  395. }
  396. var the_indicator = c.chunk_indicator(probe_line);
  397. if (the_indicator) {
  398. // The line is in a highlighted chunk.
  399. // Search backward for the first line.
  400. var probe = lineno;
  401. var indicator = the_indicator;
  402. while (probe > 0 && indicator === the_indicator) {
  403. probe--;
  404. probe_line = c.line_elt(probe);
  405. if (probe_line.length === 0) {
  406. break;
  407. }
  408. indicator = c.chunk_indicator(probe_line);
  409. }
  410. var begin = probe + 1;
  411. // Search forward for the last line.
  412. probe = lineno;
  413. indicator = the_indicator;
  414. while (indicator === the_indicator) {
  415. probe++;
  416. probe_line = c.line_elt(probe);
  417. indicator = c.chunk_indicator(probe_line);
  418. }
  419. coverage.set_sel(begin, probe);
  420. }
  421. else {
  422. coverage.set_sel(lineno);
  423. }
  424. };
  425. coverage.show_selection = function () {
  426. var c = coverage;
  427. // Highlight the lines in the chunk
  428. $(".linenos .highlight").removeClass("highlight");
  429. for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) {
  430. c.num_elt(probe).addClass("highlight");
  431. }
  432. c.scroll_to_selection();
  433. };
  434. coverage.scroll_to_selection = function () {
  435. // Scroll the page if the chunk isn't fully visible.
  436. if (coverage.selection_ends_on_screen() < 2) {
  437. // Need to move the page. The html,body trick makes it scroll in all
  438. // browsers, got it from http://stackoverflow.com/questions/3042651
  439. var top = coverage.line_elt(coverage.sel_begin);
  440. var top_pos = parseInt(top.offset().top, 10);
  441. coverage.scroll_window(top_pos - 30);
  442. }
  443. };
  444. coverage.scroll_window = function (to_pos) {
  445. $("html,body").animate({scrollTop: to_pos}, 200);
  446. };
  447. coverage.finish_scrolling = function () {
  448. $("html,body").stop(true, true);
  449. };
  450. coverage.init_scroll_markers = function () {
  451. var c = coverage;
  452. // Init some variables
  453. c.lines_len = $('#source p').length;
  454. c.body_h = $('body').height();
  455. c.header_h = $('div#header').height();
  456. // Build html
  457. c.build_scroll_markers();
  458. };
  459. coverage.build_scroll_markers = function () {
  460. var c = coverage,
  461. min_line_height = 3,
  462. max_line_height = 10,
  463. visible_window_h = $(window).height();
  464. c.lines_to_mark = $('#source').find('p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par');
  465. $('#scroll_marker').remove();
  466. // Don't build markers if the window has no scroll bar.
  467. if (c.body_h <= visible_window_h) {
  468. return;
  469. }
  470. $("body").append("<div id='scroll_marker'>&nbsp;</div>");
  471. var scroll_marker = $('#scroll_marker'),
  472. marker_scale = scroll_marker.height() / c.body_h,
  473. line_height = scroll_marker.height() / c.lines_len;
  474. // Line height must be between the extremes.
  475. if (line_height > min_line_height) {
  476. if (line_height > max_line_height) {
  477. line_height = max_line_height;
  478. }
  479. }
  480. else {
  481. line_height = min_line_height;
  482. }
  483. var previous_line = -99,
  484. last_mark,
  485. last_top,
  486. offsets = {};
  487. // Calculate line offsets outside loop to prevent relayouts
  488. c.lines_to_mark.each(function() {
  489. offsets[this.id] = $(this).offset().top;
  490. });
  491. c.lines_to_mark.each(function () {
  492. var id_name = $(this).attr('id'),
  493. line_top = Math.round(offsets[id_name] * marker_scale),
  494. line_number = parseInt(id_name.substring(1, id_name.length));
  495. if (line_number === previous_line + 1) {
  496. // If this solid missed block just make previous mark higher.
  497. last_mark.css({
  498. 'height': line_top + line_height - last_top
  499. });
  500. }
  501. else {
  502. // Add colored line in scroll_marker block.
  503. scroll_marker.append('<div id="m' + line_number + '" class="marker"></div>');
  504. last_mark = $('#m' + line_number);
  505. last_mark.css({
  506. 'height': line_height,
  507. 'top': line_top
  508. });
  509. last_top = line_top;
  510. }
  511. previous_line = line_number;
  512. });
  513. };