diff --git a/bin/cli.js b/bin/cli.js index 79858a1..3732417 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -570,8 +570,14 @@ program .command("stats") .description("Generate an overview with some statistics") .requiredOption("-s, --since , Specify the date which is going to be used to filter the data from (format: YYYY-MM-DD) (mandatory)") + .option("-w, --workflow [handle], Optionally filter test statistics by workflow (optional)") .action((options) => { - stats.generateOverview(options.since); + if (options.workflow) { + const workflowHandle = typeof options.workflow === "string" ? options.workflow : undefined; + stats.generateWorkflowOverview(options.since, workflowHandle); + } else { + stats.generateOverview(options.since); + } }); // Set/Get FIRM ID diff --git a/lib/cli/stats.js b/lib/cli/stats.js index 0aa4f94..3e39269 100644 --- a/lib/cli/stats.js +++ b/lib/cli/stats.js @@ -10,16 +10,33 @@ async function generateOverview(sinceDate) { const TODAY = new Date().toJSON().toString().slice(0, 10); const templateSummary = await getTemplatesSummary(); const yamlSummary = await getYamlSummary(sinceDate); - // Terminal displayOverview(sinceDate, TODAY, templateSummary, yamlSummary); - // File const row = createRow(sinceDate, TODAY, templateSummary, yamlSummary); saveOverviewToFile(row); } +async function generateWorkflowOverview(sinceDate, workflowHandle) { + const TODAY = new Date().toJSON().toString().slice(0, 10); + let workflowHandles; + if (workflowHandle) { + workflowHandles = [workflowHandle]; + } else { + const workflowsFolder = path.join(process.cwd(), "workflows"); + workflowHandles = fs.readdirSync(workflowsFolder).map((file) => path.basename(file, ".json")); + } + for (const handle of workflowHandles) { + const workflow = await fsUtils.getWorkflow(handle); + const templateSummary = await getWorkflowTemplateSummary(workflow); + const yamlSummary = await getYamlSummary(sinceDate, workflow); + displayWorkflowOverview(sinceDate, TODAY, templateSummary, yamlSummary); + const row = createWorkflowRow(sinceDate, TODAY, templateSummary, yamlSummary); + saveWorkflowOverviewToFile(row, handle); + } +} + // Return an object with the count of activities by file and by type // Type could be: A (added), M (modified), D (deleted) -async function yamlFilesActivity(sinceDate) { +async function yamlFilesActivity(sinceDate, workflow) { const countByType = {}; const filesChanged = exec.execSync(`git whatchanged --since="${sinceDate}" --name-status --pretty="format:"`); if (!filesChanged) { @@ -35,7 +52,9 @@ async function yamlFilesActivity(sinceDate) { } // Files to Search (YAML) - const YAML_EXPRESSION = `.*/.*/tests/.*_liquid_test.*.y(a)?ml`; + const templatesInWorkflow = workflow ? workflow.templates.reconciliations.concat(workflow.templates.accounts) : []; + const TEMPLATE_PATTERN = workflow ? `(${templatesInWorkflow.join("|")})` : `.*`; + const YAML_EXPRESSION = `.*/${TEMPLATE_PATTERN}/tests/.*_liquid_test.*.y(a)?ml`; const fileTypeRegExp = RegExp(YAML_EXPRESSION, "g"); for (const row of nonEmptyRows) { @@ -74,10 +93,12 @@ async function yamlFilesActivity(sinceDate) { // Count how many YAML files are stored. We base on the presence of a non empty file // Count how many unit tests are stored. We base on the presence of a title for each unit test // Count how many YAML files have at least two unit tests -async function countYamlFiles(templateType) { +async function countYamlFiles(templateType, templatesInWorkflow) { const files = fsUtils.listExistingFiles("yml"); const FOLDER = fsUtils.FOLDERS[templateType]; - const YAML_EXPRESSION = `.*${FOLDER}/.*/tests/.*_liquid_test.*.y(a)?ml`; + const TEMPLATE_PATTERN = templatesInWorkflow ? `(${templatesInWorkflow.join("|")})` : `.*`; + // Issue with counting multiple yml files in the same tests folder? + const YAML_EXPRESSION = `.*${FOLDER}/${TEMPLATE_PATTERN}/tests/.*_liquid_test.*.y(a)?ml`; const re = new RegExp(YAML_EXPRESSION, "g"); let countFiles = 0; let countFilesWithAtLeastTwoTests = 0; @@ -255,8 +276,99 @@ async function getTemplatesSummary() { return summary; } -async function getYamlSummary(sinceDate) { - const yamlActivity = await yamlFilesActivity(sinceDate); +async function getWorkflowTemplateSummary(workflow) { + const summary = { + workflow_name: "", + reconciliations: { + total: 0, + externallyManaged: 0, + externallyManagedPerc: 0, + yamlFiles: 0, + yamlFilesPerc: 0, + unitTests: 0, + yamlFilesWithAtLeastTwoTests: 0, + yamlFilesWithAtLeastTwoTestsPerc: 0, + }, + exportFiles: { + total: 0, + externallyManaged: 0, + externallyManagedPerc: 0, + }, + accountTemplates: { + total: 0, + externallyManaged: 0, + externallyManagedPerc: 0, + yamlFiles: 0, + yamlFilesPerc: 0, + unitTests: 0, + yamlFilesWithAtLeastTwoTests: 0, + yamlFilesWithAtLeastTwoTestsPerc: 0, + }, + all: { + total: 0, + externallyManaged: 0, + externallyManagedPerc: 0, + yamlFiles: 0, + yamlFilesPerc: 0, + unitTests: 0, + yamlFilesWithAtLeastTwoTests: 0, + yamlFilesWithAtLeastTwoTestsPerc: 0, + }, + }; + + // Fetch workflow + // Assume no empty items if present within the workflow. + summary.workflow_name = workflow.name; + + // Reconciliations + const reconciliationsInWorkflow = workflow.templates.reconciliations; + const reconciliationsExtMan = await listExternallyManagedTemplates("reconciliationText", reconciliationsInWorkflow); + const reconciliationsTests = await countYamlFiles("reconciliationText", reconciliationsInWorkflow); + summary.reconciliations.total = reconciliationsInWorkflow.length; + summary.reconciliations.externallyManaged = reconciliationsExtMan.length; + summary.reconciliations.externallyManagedPerc = percentageRoundTwo(summary.reconciliations.externallyManaged, summary.reconciliations.total); + summary.reconciliations.yamlFiles = reconciliationsTests.files; + summary.reconciliations.yamlFilesPerc = percentageRoundTwo(summary.reconciliations.yamlFiles, summary.reconciliations.total); + summary.reconciliations.unitTests = reconciliationsTests.tests; + summary.reconciliations.yamlFilesWithAtLeastTwoTests = reconciliationsTests.filesWithAtLeastTwoTests; + summary.reconciliations.yamlFilesWithAtLeastTwoTestsPerc = percentageRoundTwo(summary.reconciliations.yamlFilesWithAtLeastTwoTests, summary.reconciliations.total); + + // Export Files + const exportFilesInWorkflow = workflow.templates.exports; + const exportFilesExtMan = await listExternallyManagedTemplates("exportFile", exportFilesInWorkflow); + summary.exportFiles.total = exportFilesInWorkflow.length; + summary.exportFiles.externallyManaged = exportFilesExtMan.length; + summary.exportFiles.externallyManagedPerc = percentageRoundTwo(summary.exportFiles.externallyManaged, summary.exportFiles.total); + + // Account Templates + const accountTemplatesInWorkflow = workflow.templates.accounts; + const accountTemplatesExtMan = await listExternallyManagedTemplates("accountTemplate", accountTemplatesInWorkflow); + const accountTemplatesTests = await countYamlFiles("accountTemplate", accountTemplatesInWorkflow); + summary.accountTemplates.total = accountTemplatesInWorkflow.length; + summary.accountTemplates.externallyManaged = accountTemplatesExtMan.length; + summary.accountTemplates.externallyManagedPerc = percentageRoundTwo(summary.accountTemplates.externallyManaged, summary.accountTemplates.total); + summary.accountTemplates.yamlFiles = accountTemplatesTests.files; + summary.accountTemplates.yamlFilesPerc = percentageRoundTwo(summary.accountTemplates.yamlFiles, summary.accountTemplates.total); + summary.accountTemplates.unitTests = accountTemplatesTests.tests; + summary.accountTemplates.yamlFilesWithAtLeastTwoTests = accountTemplatesTests.filesWithAtLeastTwoTests; + summary.accountTemplates.yamlFilesWithAtLeastTwoTestsPerc = percentageRoundTwo(summary.accountTemplates.yamlFilesWithAtLeastTwoTests, summary.accountTemplates.total); + + // All + summary.all.total = summary.reconciliations.total + summary.exportFiles.total + summary.accountTemplates.total; + summary.all.externallyManaged = + summary.reconciliations.externallyManaged + summary.exportFiles.externallyManaged + summary.accountTemplates.externallyManaged; + summary.all.externallyManagedPerc = percentageRoundTwo(summary.all.externallyManaged, summary.all.total); + summary.all.yamlFiles = summary.reconciliations.yamlFiles + summary.accountTemplates.yamlFiles; + summary.all.yamlFilesPerc = percentageRoundTwo(summary.all.yamlFiles, summary.reconciliations.total + summary.accountTemplates.total); + summary.all.unitTests = summary.reconciliations.unitTests + summary.accountTemplates.unitTests; + summary.all.yamlFilesWithAtLeastTwoTests = summary.reconciliations.yamlFilesWithAtLeastTwoTests + summary.accountTemplates.yamlFilesWithAtLeastTwoTests; + summary.all.yamlFilesWithAtLeastTwoTestsPerc = percentageRoundTwo(summary.all.yamlFilesWithAtLeastTwoTests, summary.reconciliations.total + summary.accountTemplates.total); + + return summary; +} + +async function getYamlSummary(sinceDate, templatesInWorkflow) { + const yamlActivity = await yamlFilesActivity(sinceDate, templatesInWorkflow); const summary = { created: 0, updated: 0 }; summary.created = (yamlActivity["A"] || 0) - (yamlActivity["D"] || 0); summary.updated = yamlActivity["M"] || 0; @@ -309,6 +421,54 @@ function displayOverview(sinceDate, today, templateSummary, yamlSummary) { consola.log("------------------------------------"); } +function displayWorkflowOverview(sinceDate, today, templateSummary, yamlSummary) { + // Header + consola.log(""); + consola.info(`${chalk.bold(`Workflow Summary - ${templateSummary.workflow_name} ( ${sinceDate} - ${today} ):`)}`); + consola.log("------------------------------------"); + consola.log(""); + // YAML file changes + consola.log(`New YAML files created in the period: ${yamlSummary.created}`); + consola.log(`Updates to existing YAML files in the period: ${yamlSummary.updated}`); + consola.log(""); + consola.log("------------------------------------"); + consola.log(""); + // Reconciliations + consola.log(`${chalk.bold("Reconciliations:")}`); + consola.log(`Templates: ${templateSummary.reconciliations.total}`); + consola.log(`Externally Managed: ${templateSummary.reconciliations.externallyManaged} (${templateSummary.reconciliations.externallyManagedPerc}%)`); + consola.log(`YAML files: ${templateSummary.reconciliations.yamlFiles} (${templateSummary.reconciliations.yamlFilesPerc}%)`); + consola.log(`Unit Tests: ${templateSummary.reconciliations.unitTests}`); + consola.log( + `YAML files with at least two unit tests: ${templateSummary.reconciliations.yamlFilesWithAtLeastTwoTests} (${templateSummary.reconciliations.yamlFilesWithAtLeastTwoTestsPerc}%)` + ); + consola.log(""); + // Account Templates + consola.log(`${chalk.bold("Account Templates:")}`); + consola.log(`Templates: ${templateSummary.accountTemplates.total}`); + consola.log(`Externally Managed: ${templateSummary.accountTemplates.externallyManaged} (${templateSummary.accountTemplates.externallyManagedPerc}%)`); + consola.log(`YAML files: ${templateSummary.accountTemplates.yamlFiles} (${templateSummary.accountTemplates.yamlFilesPerc}%)`); + consola.log(`Unit Tests: ${templateSummary.accountTemplates.unitTests}`); + consola.log( + `YAML files with at least two unit tests: ${templateSummary.accountTemplates.yamlFilesWithAtLeastTwoTests} (${templateSummary.accountTemplates.yamlFilesWithAtLeastTwoTestsPerc}%)` + ); + consola.log(""); + // Export Files + consola.log(`${chalk.bold("Export Files:")}`); + consola.log(`Templates: ${templateSummary.exportFiles.total}`); + consola.log(`Externally Managed: ${templateSummary.exportFiles.externallyManaged} (${templateSummary.exportFiles.externallyManagedPerc}%)`); + consola.log(""); + // All + consola.log(`${chalk.bold("All:")}`); + consola.log(`Templates: ${templateSummary.all.total}`); + consola.log(`Externally Managed: ${templateSummary.all.externallyManaged} (${templateSummary.all.externallyManagedPerc}%)`); + consola.log(`YAML files: ${templateSummary.all.yamlFiles} (${templateSummary.all.yamlFilesPerc}%)`); + consola.log(`Unit Tests: ${templateSummary.all.unitTests}`); + consola.log(`YAML files with at least two unit tests: ${templateSummary.all.yamlFilesWithAtLeastTwoTests} (${templateSummary.all.yamlFilesWithAtLeastTwoTestsPerc}%)`); + consola.log(""); + consola.log("------------------------------------"); +} + function createRow(sinceDate, today, templateSummary, yamlSummary) { // Row to append to file const rowContent = [ @@ -349,6 +509,43 @@ function createRow(sinceDate, today, templateSummary, yamlSummary) { return row; } +function createWorkflowRow(sinceDate, today, templateSummary, yamlSummary) { + const rowContent = [ + templateSummary.workflow_name, + sinceDate, + today, + yamlSummary.created, + yamlSummary.updated, + templateSummary.all.total, + templateSummary.all.externallyManaged, + templateSummary.all.yamlFiles, + templateSummary.all.unitTests, + templateSummary.reconciliations.total, + templateSummary.reconciliations.externallyManaged, + templateSummary.reconciliations.yamlFiles, + templateSummary.reconciliations.unitTests, + templateSummary.accountTemplates.total, + templateSummary.accountTemplates.externallyManaged, + templateSummary.accountTemplates.yamlFiles, + templateSummary.accountTemplates.unitTests, + templateSummary.exportFiles.total, + templateSummary.exportFiles.externallyManaged, + templateSummary.all.externallyManagedPerc, + templateSummary.reconciliations.externallyManagedPerc, + templateSummary.accountTemplates.externallyManagedPerc, + templateSummary.exportFiles.externallyManagedPerc, + templateSummary.all.yamlFilesPerc, + templateSummary.reconciliations.yamlFilesPerc, + templateSummary.accountTemplates.yamlFilesPerc, + templateSummary.reconciliations.yamlFilesWithAtLeastTwoTests, + templateSummary.reconciliations.yamlFilesWithAtLeastTwoTestsPerc, + templateSummary.accountTemplates.yamlFilesWithAtLeastTwoTests, + templateSummary.accountTemplates.yamlFilesWithAtLeastTwoTestsPerc, + ]; + const row = `\r\n${rowContent.join(";")}`; + return row; +} + // content row must be a string with each column separated by ";" function saveOverviewToFile(row) { const COLUMNS = [ @@ -400,4 +597,53 @@ function saveOverviewToFile(row) { fs.appendFileSync(CSV_PATH, row); } -module.exports = { generateOverview }; +// content row must be a string with each column separated by ";" +function saveWorkflowOverviewToFile(row, workflowHandle) { + const COLUMNS = [ + "Workflow Name", + "Period - Start", + "Period - End", + "yaml files created in period", + "yaml files modified in period", + "All - templates", + "All - externally managed", + "All - yaml files", + "All - unit tests", + "Reconciliations - templates", + "Reconciliations - externally managed", + "Reconciliations - yaml files", + "Reconciliations - unit tests", + "Account Templates - templates", + "Account Templates - externally managed", + "Account Templates - yaml files", + "Account Templates - unit tests", + "Export Files - templates", + "Export Files - externally managed", + "All - externally managed (%)", + "Reconciliations - externally managed (%)", + "Account Templates - externally managed (%)", + "Export Files - externally managed (%)", + "All - yaml files (%)", + "Reconciliations - yaml files (%)", + "Account Templates - yaml files (%)", + "Reconciliations - yaml files with at least two tests", + "Reconciliations - yaml files with at least two tests (%)", + "Account Templates - yaml files with at least two tests", + "Account Templates - yaml files with at least two tests (%)", + ]; + const ROW_HEADER = `${COLUMNS.join(";")}`; + const CSV_PATH = `./stats/${workflowHandle}_stats.csv`; + // Create file and header columns + if (!fs.existsSync("./stats")) { + fs.mkdirSync("stats"); + } + if (!fs.existsSync(CSV_PATH)) { + fs.writeFileSync(CSV_PATH, ROW_HEADER, (err) => { + consola.error(err); + }); + } + // Append content + fs.appendFileSync(CSV_PATH, row); +} + +module.exports = { generateOverview, generateWorkflowOverview }; diff --git a/lib/utils/fsUtils.js b/lib/utils/fsUtils.js index cf4b835..9591ab9 100644 --- a/lib/utils/fsUtils.js +++ b/lib/utils/fsUtils.js @@ -480,6 +480,20 @@ function checkLiquidTestDependencies(targetHandle) { return dependentHandles; } +function getWorkflow(workflowHandle) { + try { + const workflowPath = path.join(process.cwd(), "workflows", `${workflowHandle}.json`); + if (!fs.existsSync(workflowPath)) { + throw new Error(`Workflow "${workflowHandle}" not found`); + } + return JSON.parse(fs.readFileSync(workflowPath).toString()); + } catch (error) { + consola.error(`An error occurred when trying to read the workflow "${workflowHandle}"`); + consola.error(error); + process.exit(1); + } +} + // Recursive option for fs.watch is not available in every OS (e.g. Linux) function recursiveInspectDirectory({ basePath, collection, pathsArray = [], typeCheck = "liquid" }) { collection.forEach((filePath) => { @@ -567,4 +581,5 @@ module.exports = { getTemplateId, setTemplateId, checkLiquidTestDependencies, + getWorkflow, };