diff --git a/framework/meta-lib/src/lib.rs b/framework/meta-lib/src/lib.rs index e33c2cfb7b..61125711ca 100644 --- a/framework/meta-lib/src/lib.rs +++ b/framework/meta-lib/src/lib.rs @@ -1,14 +1,14 @@ pub mod abi_json; pub mod cargo_toml_contents; pub mod cli; -mod code_report_json; +pub mod code_report_json; pub mod contract; pub mod ei; pub mod ei_check_json; pub mod esdt_attr_file_json; -mod mxsc_file_json; +pub mod mxsc_file_json; pub mod print_util; -mod report_info_json; +pub mod report_info_json; pub mod tools; pub mod version; pub mod version_history; diff --git a/framework/meta/src/cli/cli_args_standalone.rs b/framework/meta/src/cli/cli_args_standalone.rs index 335afe41ea..0be68d805f 100644 --- a/framework/meta/src/cli/cli_args_standalone.rs +++ b/framework/meta/src/cli/cli_args_standalone.rs @@ -65,6 +65,9 @@ pub enum StandaloneCliAction { #[command(name = "test-coverage", about = "Run test coverage and output report")] TestCoverage(TestCoverageArgs), + #[command(name = "report", about = "Generate code report")] + CodeReportGen(CodeReportArgs), + #[command( about = "Generates a scenario test initialized with real data fetched from the blockchain." )] @@ -115,7 +118,7 @@ pub struct TestArgs { } #[derive(Default, Clone, PartialEq, Eq, Debug, ValueEnum)] -pub enum TestCoverageOutputFormat { +pub enum OutputFormat { /// Markdown pretty-print summary #[default] Markdown, @@ -132,13 +135,73 @@ pub struct TestCoverageArgs { /// Output format #[arg(short, long, verbatim_doc_comment)] - pub format: Option, + pub format: Option, /// Ignore files by path patterns #[arg(short = 'i', long = "ignore-filename-regex", verbatim_doc_comment)] pub ignore_filename_regex: Vec, } +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct CodeReportArgs { + #[command(subcommand)] + pub command: CodeReportAction, +} + +#[derive(Clone, PartialEq, Eq, Debug, Subcommand)] +pub enum CodeReportAction { + #[command(name = "compile", about = "Generates the contract report.")] + Compile(CompileArgs), + + #[command(name = "compare", about = "Compare two contract reports.")] + Compare(CompareArgs), + + #[command( + name = "convert", + about = "Converts a contract report to a Markdown file." + )] + Convert(ConvertArgs), +} + +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct CompileArgs { + /// Target directory where to generate code report. + #[arg(short, long, verbatim_doc_comment)] + pub path: PathBuf, + + /// Path to the Markdown or JSON file where the report results will be written. + #[arg(short, long, verbatim_doc_comment)] + pub output: PathBuf, +} + +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct CompareArgs { + /// Path to the previous version of code report JSON file + /// that will be used for comparison. + #[arg(short, long, verbatim_doc_comment)] + pub baseline: PathBuf, + + /// Path to the current version of the code report JSON file + /// that will be compared. + #[arg(short, long, verbatim_doc_comment)] + pub new: PathBuf, + + /// Path to the Markdown file where the comparison results will be written. + #[arg(short, long, verbatim_doc_comment)] + pub output: PathBuf, +} + +#[derive(Clone, PartialEq, Eq, Debug, Args)] +pub struct ConvertArgs { + /// Path to the JSON report file that needs to be converted to Markdown format. + #[arg(short, long, verbatim_doc_comment)] + pub input: PathBuf, + + /// Path to the Markdown file where the report results will be written. + #[arg(short, long, verbatim_doc_comment)] + pub output: PathBuf, +} + #[derive(Default, Clone, PartialEq, Eq, Debug, Args)] pub struct AllArgs { #[command(subcommand)] diff --git a/framework/meta/src/cli/cli_standalone_main.rs b/framework/meta/src/cli/cli_standalone_main.rs index 6c8c6a67b0..9a8e065627 100644 --- a/framework/meta/src/cli/cli_standalone_main.rs +++ b/framework/meta/src/cli/cli_standalone_main.rs @@ -3,6 +3,7 @@ use crate::cmd::retrieve_address::retrieve_address; use clap::Parser; use crate::cmd::all::call_all_meta; +use crate::cmd::code_report::report; use crate::cmd::info::call_info; use crate::cmd::install::install; use crate::cmd::local_deps::local_deps; @@ -10,6 +11,7 @@ use crate::cmd::scen_test_gen::test_gen_tool; use crate::cmd::template::{create_contract, print_template_names}; use crate::cmd::test::test; use crate::cmd::test_coverage::test_coverage; + use crate::cmd::upgrade::upgrade_sc; /// Entry point in the program when calling it as a standalone tool. @@ -35,6 +37,9 @@ pub async fn cli_main_standalone() { Some(StandaloneCliAction::TestCoverage(args)) => { test_coverage(args); }, + Some(StandaloneCliAction::CodeReportGen(args)) => { + report(args); + }, Some(StandaloneCliAction::Account(args)) => { retrieve_address(args).await; }, diff --git a/framework/meta/src/cmd.rs b/framework/meta/src/cmd.rs index a588f90162..2a3ebe65cd 100644 --- a/framework/meta/src/cmd.rs +++ b/framework/meta/src/cmd.rs @@ -1,4 +1,5 @@ pub mod all; +pub mod code_report; pub mod info; pub mod install; pub mod local_deps; diff --git a/framework/meta/src/cmd/code_report.rs b/framework/meta/src/cmd/code_report.rs new file mode 100644 index 0000000000..47e51948f0 --- /dev/null +++ b/framework/meta/src/cmd/code_report.rs @@ -0,0 +1,15 @@ +pub mod compare; +pub mod generate_report; +pub mod render_code_report; + +use generate_report::{compare_report, convert_report, create_report}; + +use crate::cli::{CodeReportAction, CodeReportArgs}; + +pub fn report(args: &CodeReportArgs) { + match &args.command { + CodeReportAction::Compile(compile_args) => create_report(compile_args), + CodeReportAction::Compare(compare_args) => compare_report(compare_args), + CodeReportAction::Convert(convert_args) => convert_report(convert_args), + } +} diff --git a/framework/meta/src/cmd/code_report/compare.rs b/framework/meta/src/cmd/code_report/compare.rs new file mode 100644 index 0000000000..28c604962d --- /dev/null +++ b/framework/meta/src/cmd/code_report/compare.rs @@ -0,0 +1,47 @@ +pub(crate) fn size_status_after_comparing(size: usize, compared_size: usize) -> String { + match size.cmp(&compared_size) { + std::cmp::Ordering::Greater => { + format!("{} :arrow_right: {} :red_circle:", compared_size, size) + }, + std::cmp::Ordering::Less => { + format!("{} :arrow_right: {} :green_circle:", compared_size, size) + }, + std::cmp::Ordering::Equal => { + format!("{}", size) + }, + } +} + +pub(crate) fn allocator_status_after_comparing( + has_allocator: bool, + compared_has_allocator: bool, +) -> String { + if compared_has_allocator == has_allocator { + return format!("{}", has_allocator); + } + + let allocator_status = format!("{} :arrow-right: {}", compared_has_allocator, has_allocator); + + if !has_allocator { + format!("{allocator_status} :green-circle:") + } else { + format!("{allocator_status} :red-circle:") + } +} + +pub(crate) fn panic_status_after_comparing( + has_panic: &String, + compared_has_panic: &String, +) -> String { + if has_panic == compared_has_panic { + return has_panic.to_string(); + } + + let panic_status = format!("{} :arrow-right: {}", compared_has_panic, has_panic); + + if has_panic == "none" { + return format!("{panic_status} :green-circle:"); + } + + panic_status +} diff --git a/framework/meta/src/cmd/code_report/generate_report.rs b/framework/meta/src/cmd/code_report/generate_report.rs new file mode 100644 index 0000000000..5c06b65295 --- /dev/null +++ b/framework/meta/src/cmd/code_report/generate_report.rs @@ -0,0 +1,159 @@ +use std::{ + fs::{read_dir, File}, + io::{BufReader, Write}, + path::{Path, PathBuf}, +}; + +use crate::{ + cli::{CompareArgs, CompileArgs, ConvertArgs}, + folder_structure::RelevantDirectories, +}; + +use multiversx_sc_meta_lib::{ + self, code_report_json::CodeReportJson, mxsc_file_json::MxscFileJson, +}; + +use super::render_code_report::CodeReportRender; + +const JSON: &str = ".json"; +const MD: &str = ".md"; + +pub fn compare_report(compare_args: &CompareArgs) { + if !is_path_ends_with(&compare_args.output, MD) { + panic!("Compare output is only available for Markdown file extension."); + } + + if !is_path_ends_with(&compare_args.baseline, JSON) + && !is_path_ends_with(&compare_args.new, JSON) + { + panic!("Compare baseline and new are only available for JSON file extension."); + } + + let mut output_file = create_file(&compare_args.output); + + let baseline_reports: Vec = match File::open(&compare_args.baseline) { + Ok(_) => extract_reports_from_json(&compare_args.baseline), + Err(_) => vec![], + }; + + let new_reports: Vec = extract_reports_from_json(&compare_args.new); + + let mut render_code_report = + CodeReportRender::new(&mut output_file, &baseline_reports, &new_reports); + render_code_report.compare_reports(); +} + +pub fn convert_report(convert_args: &ConvertArgs) { + if !is_path_ends_with(&convert_args.output, MD) { + panic!("Conversion output is only available for Markdown file extension"); + } + + if !is_path_ends_with(&convert_args.input, JSON) { + panic!("Conversion only available from JSON file extension"); + } + + let mut output_file = create_file(&convert_args.output); + + let reports: Vec = extract_reports_from_json(&convert_args.input); + + let mut convert_code_report = CodeReportRender::new_without_compare(&mut output_file, &reports); + + convert_code_report.render_report(); +} + +pub fn create_report(compile_args: &CompileArgs) { + if !is_path_ends_with(&compile_args.output, JSON) + && !is_path_ends_with(&compile_args.output, MD) + { + panic!("Create report is only available for Markdown or JSON output file.") + } + + let reports = generate_new_report(&compile_args.path); + + let mut file = create_file(&compile_args.output); + + if is_path_ends_with(&compile_args.output, MD) { + let mut render_code_report = CodeReportRender::new_without_compare(&mut file, &reports); + render_code_report.render_report(); + } else { + let json_output = serde_json::to_string(&reports).unwrap(); + file.write_all(json_output.as_bytes()).unwrap(); + } +} + +fn generate_new_report(path: &PathBuf) -> Vec { + let directors = RelevantDirectories::find_all(path, &["".to_owned()]); + + assemble_report_vec(directors) +} + +fn assemble_report_vec(directors: RelevantDirectories) -> Vec { + let mut reports: Vec = Vec::new(); + + for director in directors.iter() { + let output_path: PathBuf = director.path.join("output"); + + collect_reports(&output_path, &mut reports); + sanitize_output_path_from_report(&mut reports); + } + + reports +} + +fn find_mxsc_files(path: &PathBuf) -> Vec { + if !path.is_dir() { + return vec![]; + } + + let mut mxsc_files = Vec::new(); + for entry in read_dir(path).unwrap() { + let file_path = entry.unwrap().path(); + if file_path.to_str().unwrap().ends_with(".mxsc.json") { + mxsc_files.push(file_path); + } + } + + mxsc_files +} + +fn collect_reports(path: &PathBuf, reports: &mut Vec) { + for mxsc_path in find_mxsc_files(path) { + let mxsc_file = match File::open(mxsc_path) { + Ok(file) => file, + Err(_) => continue, + }; + let data: MxscFileJson = serde_json::from_reader(mxsc_file).unwrap(); + reports.push(data.report.code_report); + } +} + +fn create_file(file_path: &PathBuf) -> File { + File::create(file_path).expect("could not write report file") +} + +fn sanitize_output_path_from_report(reports: &mut [CodeReportJson]) { + reports.iter_mut().for_each(|report| { + report.path = report + .path + .split('/') + .last() + .unwrap_or(&report.path) + .to_string(); + }) +} + +fn is_path_ends_with(path: &Path, extension: &str) -> bool { + path.to_path_buf() + .into_os_string() + .into_string() + .unwrap() + .ends_with(extension) +} + +fn extract_reports_from_json(path: &PathBuf) -> Vec { + let file = + File::open(path).unwrap_or_else(|_| panic!("file with path {} not found", path.display())); + let reader = BufReader::new(file); + + serde_json::from_reader(reader).unwrap_or_else(|_| vec![]) +} diff --git a/framework/meta/src/cmd/code_report/render_code_report.rs b/framework/meta/src/cmd/code_report/render_code_report.rs new file mode 100644 index 0000000000..e6edcfae8e --- /dev/null +++ b/framework/meta/src/cmd/code_report/render_code_report.rs @@ -0,0 +1,122 @@ +use std::fmt::Display; + +pub struct CodeReportRender<'a> { + pub file: Option<&'a mut dyn std::io::Write>, + pub compared_reports: &'a [CodeReportJson], + pub reports: &'a [CodeReportJson], +} + +use multiversx_sc_meta_lib::code_report_json::CodeReportJson; + +use super::compare::{ + allocator_status_after_comparing, panic_status_after_comparing, size_status_after_comparing, +}; + +impl<'a> CodeReportRender<'a> { + pub fn new( + file: &'a mut dyn std::io::Write, + compared_reports: &'a [CodeReportJson], + reports: &'a [CodeReportJson], + ) -> Self { + Self { + file: Some(file), + compared_reports, + reports, + } + } + + pub fn new_without_compare( + file: &'a mut dyn std::io::Write, + reports: &'a [CodeReportJson], + ) -> Self { + Self { + file: Some(file), + compared_reports: &[], + reports, + } + } + + pub fn render_report(&mut self) { + self.render_header(); + + self.render_reports(); + } + + pub fn compare_reports(&mut self) { + self.render_header(); + + if self.compared_reports.is_empty() { + self.render_reports(); + self.writeln("\n:warning: Could not download the report for the base branch. Displaying only the report for the current branch. :warning:"); + } else { + self.render_report_and_compare(); + } + } + + fn writeln(&mut self, s: impl Display) { + let file = self.file.as_mut().unwrap(); + file.write_all(s.to_string().as_bytes()).unwrap(); + file.write_all(b"\n").unwrap(); + } + + fn write_report_for_contract( + &mut self, + path: &String, + size: &String, + has_allocator: &String, + has_panic: &String, + ) { + self.writeln(format!( + "| {} | {} | {} | {} |", + path.split('/').last().unwrap_or_else(|| path), + size, + has_allocator, + has_panic + )); + } + + fn render_header(&mut self) { + self.writeln("| Path                                                         |                                     size |                  has-allocator |                     has-format |"); + self.writeln("| :-- | --: | --: | --: |"); + } + + fn render_reports(&mut self) { + for report in self.reports { + self.write_report_for_contract( + &report.path, + &report.size.to_string(), + &report.has_allocator.to_string(), + &report.has_panic, + ); + } + } + + fn render_report_and_compare(&mut self) { + for report in self.reports.iter() { + if let Some(compared_report) = self + .compared_reports + .iter() + .find(|cr| cr.path == report.path) + { + self.print_compared_output(report, compared_report); + } + } + } + + fn print_compared_output(&mut self, report: &CodeReportJson, compared_report: &CodeReportJson) { + let size_report = size_status_after_comparing(report.size, compared_report.size); + + let has_allocator_report = + allocator_status_after_comparing(report.has_allocator, compared_report.has_allocator); + + let has_panic_report = + panic_status_after_comparing(&report.has_panic, &compared_report.has_panic); + + self.write_report_for_contract( + &report.path, + &size_report, + &has_allocator_report, + &has_panic_report, + ); + } +} diff --git a/framework/meta/src/cmd/test_coverage.rs b/framework/meta/src/cmd/test_coverage.rs index 363fcc9372..0ece2454fb 100644 --- a/framework/meta/src/cmd/test_coverage.rs +++ b/framework/meta/src/cmd/test_coverage.rs @@ -6,7 +6,7 @@ mod run; mod util; use crate::{ - cli::{TestCoverageArgs, TestCoverageOutputFormat}, + cli::{OutputFormat, TestCoverageArgs}, cmd::test_coverage::{cargo::get_workspace_root, run::run_test_coverage}, }; use std::process; @@ -16,9 +16,7 @@ pub fn test_coverage(args: &TestCoverageArgs) { if let Err(err) = run_test_coverage( &root_path, &args.output, - args.format - .as_ref() - .unwrap_or(&TestCoverageOutputFormat::default()), + args.format.as_ref().unwrap_or(&OutputFormat::default()), &args.ignore_filename_regex, ) { eprintln!("{}", err); diff --git a/framework/meta/src/cmd/test_coverage/run.rs b/framework/meta/src/cmd/test_coverage/run.rs index edb165824b..687d404009 100644 --- a/framework/meta/src/cmd/test_coverage/run.rs +++ b/framework/meta/src/cmd/test_coverage/run.rs @@ -1,7 +1,7 @@ use std::fs; use crate::{ - cli::TestCoverageOutputFormat, + cli::OutputFormat, cmd::test_coverage::{ cargo::{get_instrumented_test_binaries_paths, run_instrumented_tests}, error::TestCoverageError, @@ -16,7 +16,7 @@ use crate::{ pub fn run_test_coverage( root_path: &str, output_path: &str, - output_format: &TestCoverageOutputFormat, + output_format: &OutputFormat, ignore_filename_regex: &[String], ) -> Result<(), TestCoverageError> { ensure_dependencies_in_path()?; @@ -43,10 +43,10 @@ pub fn run_test_coverage( let mut output = String::new(); match output_format { - TestCoverageOutputFormat::Markdown => { + OutputFormat::Markdown => { render_coverage(&mut output, &coverage, root_path); }, - TestCoverageOutputFormat::Json => { + OutputFormat::Json => { output = serde_json::to_string_pretty(&coverage).unwrap(); }, };