istanbulСoverage.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. /* global global */
  2. const fs = require("fs");
  3. const path = require("path");
  4. const TestExclude = require("test-exclude");
  5. const { recorder, event, output } = require("codeceptjs");
  6. const Container = require("codeceptjs/lib/container");
  7. const { clearString } = require("codeceptjs/lib/utils");
  8. function hashCode(str) {
  9. let hash = 0;
  10. if (str.length === 0) {
  11. return `${hash}`;
  12. }
  13. for (let i = 0; i < str.length; i++) {
  14. const char = str.charCodeAt(i);
  15. hash = (hash << 5) - hash + char;
  16. hash = hash & hash; // Convert to 32bit integer
  17. }
  18. return hash.toString(16);
  19. }
  20. const defaultConfig = {
  21. coverageDir: "output/coverage",
  22. actionCoverage: false,
  23. uniqueFileName: true,
  24. };
  25. const defaultActionCoverageConfig = {
  26. enabled: true,
  27. beginActionName: "performActionBegin",
  28. endActionName: "performActionEnd",
  29. coverageDir: "output/actionCoverage",
  30. include: true,
  31. exclude: false,
  32. };
  33. const supportedHelpers = ["Puppeteer", "Playwright"];
  34. function buildFileName(test, uniqueFileName) {
  35. let fileName = clearString(test.title);
  36. const originalName = fileName;
  37. // This prevent data driven to be included in the failed screenshot file name
  38. if (fileName.indexOf("{") !== -1) {
  39. fileName = fileName.substr(0, fileName.indexOf("{") - 3).trim();
  40. }
  41. if (test.ctx && test.ctx.test && test.ctx.test.type === "hook") {
  42. fileName = clearString(`${test.title}_${test.ctx.test.title}`);
  43. }
  44. if (uniqueFileName) {
  45. const uuid = hashCode(originalName);
  46. fileName = `${fileName.substring(0, 15)}_${uuid}.coverage.json`;
  47. } else {
  48. fileName = `${fileName}.coverage.json`;
  49. }
  50. return fileName;
  51. }
  52. function prepareActionStepConfig(actionCoverageConfig) {
  53. const config =
  54. typeof actionCoverageConfig === "boolean"
  55. ? {
  56. enabled: actionCoverageConfig,
  57. }
  58. : actionCoverageConfig;
  59. return Object.assign({}, defaultActionCoverageConfig, config);
  60. }
  61. /**
  62. * Dumps code coverage from Playwright/Puppeteer after every test.
  63. *
  64. * #### Configuration
  65. *
  66. *
  67. * ```js
  68. * plugins: {
  69. * istanbulCoverage: {
  70. * require: "./path/to/istanbulCoverage"
  71. * enabled: true
  72. * }
  73. * }
  74. * ```
  75. *
  76. * Possible config options:
  77. *
  78. * * `coverageDir`: directory to dump coverage files
  79. * * `uniqueFileName`: generate a unique filename by adding uuid
  80. */
  81. module.exports = (config) => {
  82. const helpers = Container.helpers();
  83. let helper;
  84. for (const helperName of supportedHelpers) {
  85. if (Object.keys(helpers).indexOf(helperName) > -1) {
  86. helper = helpers[helperName];
  87. }
  88. }
  89. if (!helper) {
  90. console.error("Coverage is only supported in Puppeteer, Playwright");
  91. return;
  92. }
  93. const options = Object.assign(defaultConfig, helper.options, config);
  94. options.actionCoverage = prepareActionStepConfig(options.actionCoverage);
  95. const excludeTester = new TestExclude({
  96. ...options.actionCoverage,
  97. cwd: path.resolve("../"),
  98. });
  99. let lastCoverages = {};
  100. let lastActionKeys = new Set();
  101. let actionCoverages = {};
  102. const actionsStack = [];
  103. let prevActionsStack = [];
  104. const performActionBegin = (name) => {
  105. actionsStack.push(name);
  106. };
  107. const performActionEnd = () => {
  108. actionsStack.pop();
  109. };
  110. if (options.actionCoverage.enabled) {
  111. global[options.actionCoverage.beginActionName] = performActionBegin;
  112. global[options.actionCoverage.endActionName] = performActionEnd;
  113. } else {
  114. global[options.actionCoverage.beginActionName] = global[options.actionCoverage.endActionName] = () => {};
  115. }
  116. const getCoverage = async () => {
  117. const coverageInfo = await helper.page.evaluate(() => {
  118. const coverageInfo = window.__coverage__;
  119. return coverageInfo;
  120. });
  121. return coverageInfo;
  122. };
  123. function hasActionChanges() {
  124. return (
  125. actionsStack.length !== prevActionsStack.length || actionsStack.some((val, key) => val !== prevActionsStack[key])
  126. );
  127. }
  128. function filterActionCoverage(actionCoverage) {
  129. return Object.fromEntries(Object.entries(actionCoverage).filter(([path]) => excludeTester.shouldInstrument(path)));
  130. }
  131. async function collectLastCoverage(actionKeys, endOfTest = false) {
  132. const coverageInfo = await getCoverage();
  133. if (!coverageInfo) return {};
  134. const actionCoverageInfo = filterActionCoverage(coverageInfo);
  135. actionKeys.forEach((actionKey) => {
  136. if (!lastCoverages[actionKey]) {
  137. lastCoverages[actionKey] = actionCoverageInfo;
  138. }
  139. });
  140. for (const lastActionKey of lastActionKeys) {
  141. if (endOfTest || actionKeys.indexOf(lastActionKey) === -1) {
  142. const additionalCoverage = subCoverage(actionCoverageInfo, lastCoverages[lastActionKey]);
  143. if (!actionCoverages[lastActionKey]) {
  144. actionCoverages[lastActionKey] = additionalCoverage;
  145. } else {
  146. actionCoverages[lastActionKey] = addCoverage(actionCoverages[lastActionKey], additionalCoverage);
  147. }
  148. lastCoverages[lastActionKey] = undefined;
  149. delete lastCoverages[lastActionKey];
  150. }
  151. }
  152. lastActionKeys = [...actionKeys];
  153. return coverageInfo;
  154. }
  155. function operateCoverage(aCoverage, bCoverage, op) {
  156. const resultCoverage = {};
  157. for (const [filePath, aFileCoverage] of Object.entries(aCoverage)) {
  158. const bFileCoverage = bCoverage[filePath];
  159. const resultFileCoverage = { ...aFileCoverage, s: {}, f: {}, b: {} };
  160. for (const [key, value] of Object.entries(aFileCoverage.s)) {
  161. resultFileCoverage.s[key] = op(value, bFileCoverage.s[key]);
  162. }
  163. for (const [key, value] of Object.entries(aFileCoverage.f)) {
  164. resultFileCoverage.f[key] = op(value, bFileCoverage.f[key]);
  165. }
  166. for (const [key, values] of Object.entries(aFileCoverage.b)) {
  167. resultFileCoverage.b[key] = values.map((val, idx) => op(val, bFileCoverage.b[key][idx]));
  168. }
  169. resultCoverage[filePath] = resultFileCoverage;
  170. }
  171. return resultCoverage;
  172. }
  173. function subCoverage(aCoverage, bCoverage) {
  174. return operateCoverage(aCoverage, bCoverage, (a, b) => a - b);
  175. }
  176. function addCoverage(aCoverage, bCoverage) {
  177. return operateCoverage(aCoverage, bCoverage, (a, b) => a + b);
  178. }
  179. event.dispatcher.on(event.all.before, async () => {
  180. output.debug("*** Collecting istanbul coverage for tests ****");
  181. if (!options.actionCoverage.enabled) return;
  182. actionCoverages = {};
  183. });
  184. event.dispatcher.on(event.all.after, async () => {
  185. if (!options.actionCoverage.enabled) return;
  186. recorder.add(
  187. "saving action coverage",
  188. async () => {
  189. try {
  190. const coverageDir = path.resolve(process.cwd(), options.actionCoverage.coverageDir);
  191. if (!fs.existsSync(coverageDir)) {
  192. fs.mkdirSync(coverageDir, { recursive: true });
  193. }
  194. for (const [actionName, coverage] of Object.entries(actionCoverages)) {
  195. const coveragePath = path.resolve(coverageDir, `${actionName}.coverage.json`);
  196. output.print(`writing ${coveragePath}`);
  197. fs.writeFileSync(coveragePath, JSON.stringify(coverage));
  198. }
  199. } catch (err) {
  200. console.error(err);
  201. }
  202. },
  203. true,
  204. );
  205. });
  206. event.dispatcher.on(event.test.before, async () => {
  207. if (!options.actionCoverage.enabled) return;
  208. lastCoverages = {};
  209. lastActionKeys = new Set();
  210. prevActionsStack = [];
  211. });
  212. // Save coverage data after every test run
  213. event.dispatcher.on(event.test.after, async (test) => {
  214. recorder.add(
  215. "saving coverage",
  216. async () => {
  217. try {
  218. const coverageInfo = await collectLastCoverage(actionsStack, true);
  219. const coverageDir = path.resolve(process.cwd(), options.coverageDir);
  220. if (!fs.existsSync(coverageDir)) {
  221. fs.mkdirSync(coverageDir, { recursive: true });
  222. }
  223. const coveragePath = path.resolve(coverageDir, buildFileName(test, options.uniqueFileName));
  224. output.print(`writing ${coveragePath}`);
  225. fs.writeFileSync(coveragePath, JSON.stringify(coverageInfo));
  226. } catch (err) {
  227. console.error(err);
  228. }
  229. },
  230. true,
  231. );
  232. });
  233. event.dispatcher.on(event.step.before, async () => {
  234. if (!options.actionCoverage.enabled) return;
  235. if (!hasActionChanges()) return;
  236. prevActionsStack = [...actionsStack];
  237. const stack = [...actionsStack];
  238. recorder.add("collect last coverage", async () => {
  239. try {
  240. await collectLastCoverage(stack);
  241. } catch (err) {
  242. console.error(err);
  243. }
  244. });
  245. });
  246. };