diff --git a/bin/src/cli_exit_codes.rs b/bin/src/cli_exit_codes.rs new file mode 100644 index 0000000..9c0a417 --- /dev/null +++ b/bin/src/cli_exit_codes.rs @@ -0,0 +1,7 @@ +pub const SUCCESS_WITHOUT_CONFLICTS: i32 = 0; +pub const SUCCESS_WITH_CONFLICTS: i32 = 1; + +pub const READING_FILE_ERROR: i32 = 129; +pub const GUESS_LANGUAGE_ERROR: i32 = 130; +pub const WRITING_FILE_ERROR: i32 = 131; +pub const INTERNAL_EXECUTION_ERROR: i32 = 132; diff --git a/bin/src/control.rs b/bin/src/control.rs new file mode 100644 index 0000000..17f6c8f --- /dev/null +++ b/bin/src/control.rs @@ -0,0 +1,89 @@ +use std::{error::Error, fmt}; + +use parsing::ParserConfiguration; + +#[derive(Debug)] +pub enum ExecutionError { + ParsingError, +} + +impl fmt::Display for ExecutionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ExecutionError::ParsingError => write!(f, "Parsing error occurred"), + } + } +} + +impl Error for ExecutionError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + None + } +} + +#[derive(Debug)] +pub enum ExecutionResult { + WithConflicts(String), + WithoutConflicts(String), +} + +impl ToString for ExecutionResult { + fn to_string(&self) -> String { + match self { + ExecutionResult::WithConflicts(value) => value.to_owned(), + ExecutionResult::WithoutConflicts(value) => value.to_owned(), + } + } +} + +pub fn run_tool_on_merge_scenario( + language: model::Language, + base: &str, + left: &str, + right: &str, +) -> Result { + if base == left { + return Ok(ExecutionResult::WithoutConflicts(right.to_string())); + } + + if base == right { + return Ok(ExecutionResult::WithoutConflicts(left.to_string())); + } + + let parser_configuration = ParserConfiguration::from(language); + + let base_tree = parsing::parse_string(base, &parser_configuration) + .map_err(|_| ExecutionError::ParsingError)?; + let left_tree = parsing::parse_string(left, &parser_configuration) + .map_err(|_| ExecutionError::ParsingError)?; + let right_tree = parsing::parse_string(right, &parser_configuration) + .map_err(|_| ExecutionError::ParsingError)?; + + let matchings_left_base = matching::calculate_matchings(&left_tree, &base_tree); + let matchings_right_base = matching::calculate_matchings(&right_tree, &base_tree); + let matchings_left_right = matching::calculate_matchings(&left_tree, &right_tree); + + let result = merge::merge( + &base_tree, + &left_tree, + &right_tree, + &matchings_left_base, + &matchings_right_base, + &matchings_left_right, + ); + + match has_conflict(&result) { + true => Ok(ExecutionResult::WithConflicts(result.to_string())), + false => Ok(ExecutionResult::WithoutConflicts(result.to_string())), + } +} + +fn has_conflict(result: &merge::MergedCSTNode) -> bool { + match result { + merge::MergedCSTNode::NonTerminal { children, .. } => { + children.iter().any(|child| has_conflict(child)) + } + merge::MergedCSTNode::Terminal { .. } => false, + merge::MergedCSTNode::Conflict { .. } => true, + } +} diff --git a/bin/src/parser_configuration.rs b/bin/src/language.rs similarity index 64% rename from bin/src/parser_configuration.rs rename to bin/src/language.rs index 8e9deb5..a7528cd 100644 --- a/bin/src/parser_configuration.rs +++ b/bin/src/language.rs @@ -1,6 +1,4 @@ -pub fn get_parser_configuration_by_file_path( - file_path: &std::path::Path, -) -> Result { +pub fn get_language_by_file_path(file_path: &std::path::Path) -> Result { file_path .extension() .and_then(std::ffi::OsStr::to_str) @@ -8,7 +6,6 @@ pub fn get_parser_configuration_by_file_path( "java" => Some(model::Language::Java), _ => None, }) - .map(parsing::ParserConfiguration::from) .ok_or(format!( "Could not retrieve parsing configuration for file {}", file_path.display() @@ -17,17 +14,20 @@ pub fn get_parser_configuration_by_file_path( #[cfg(test)] mod tests { - use crate::parser_configuration::get_parser_configuration_by_file_path; + use crate::language::get_language_by_file_path; #[test] fn if_the_file_extension_has_no_parser_available_it_returns_error() { let file_path = std::path::PathBuf::from("/path/without/extension"); - assert!(get_parser_configuration_by_file_path(&file_path).is_err()) + assert!(get_language_by_file_path(&file_path).is_err()) } #[test] fn if_the_file_extension_has_a_parser_available_it_returns_a_parser_configuration() { let file_path = std::path::PathBuf::from("/path/for/java/file/Example.java"); - assert!(get_parser_configuration_by_file_path(&file_path).is_ok()) + assert_eq!( + get_language_by_file_path(&file_path).unwrap(), + model::Language::Java + ) } } diff --git a/bin/src/lib.rs b/bin/src/lib.rs new file mode 100644 index 0000000..96a7e8b --- /dev/null +++ b/bin/src/lib.rs @@ -0,0 +1,5 @@ +mod cli_exit_codes; +mod control; + +pub use cli_exit_codes::*; +pub use control::run_tool_on_merge_scenario; diff --git a/bin/src/main.rs b/bin/src/main.rs index 8b40b5a..771307b 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -1,46 +1,35 @@ mod cli_args; -mod parser_configuration; +mod cli_exit_codes; +mod control; +mod language; use clap::Parser; -fn main() -> Result<(), Box> { +fn main() { let args = cli_args::CliArgs::parse(); - let base = std::fs::read_to_string(&args.base_path)?; - let left = std::fs::read_to_string(args.left_path)?; - let right = std::fs::read_to_string(args.right_path)?; - - let parser_configuration = - parser_configuration::get_parser_configuration_by_file_path(&args.base_path)?; - - if base == left { - std::fs::write(args.merge_path, right)?; - return Ok(()); + let base = std::fs::read_to_string(&args.base_path) + .unwrap_or_else(|_| std::process::exit(cli_exit_codes::READING_FILE_ERROR)); + let left = std::fs::read_to_string(args.left_path) + .unwrap_or_else(|_| std::process::exit(cli_exit_codes::READING_FILE_ERROR)); + let right = std::fs::read_to_string(args.right_path) + .unwrap_or_else(|_| std::process::exit(cli_exit_codes::READING_FILE_ERROR)); + + let language = language::get_language_by_file_path(&args.base_path) + .unwrap_or_else(|_| std::process::exit(cli_exit_codes::GUESS_LANGUAGE_ERROR)); + + let result = control::run_tool_on_merge_scenario(language, &base, &left, &right) + .unwrap_or_else(|_| std::process::exit(cli_exit_codes::INTERNAL_EXECUTION_ERROR)); + + std::fs::write(args.merge_path, result.to_string()) + .unwrap_or_else(|_| std::process::exit(cli_exit_codes::WRITING_FILE_ERROR)); + + match result { + control::ExecutionResult::WithConflicts(_) => { + std::process::exit(cli_exit_codes::SUCCESS_WITH_CONFLICTS) + } + control::ExecutionResult::WithoutConflicts(_) => { + std::process::exit(cli_exit_codes::SUCCESS_WITHOUT_CONFLICTS) + } } - - if base == right { - std::fs::write(args.merge_path, left)?; - return Ok(()); - } - - let base_tree = parsing::parse_string(&base, &parser_configuration).unwrap(); - let left_tree = parsing::parse_string(&left, &parser_configuration).unwrap(); - let right_tree = parsing::parse_string(&right, &parser_configuration).unwrap(); - - let matchings_left_base = matching::calculate_matchings(&left_tree, &base_tree); - let matchings_right_base = matching::calculate_matchings(&right_tree, &base_tree); - let matchings_left_right = matching::calculate_matchings(&left_tree, &right_tree); - - let result = merge::merge( - &base_tree, - &left_tree, - &right_tree, - &matchings_left_base, - &matchings_right_base, - &matchings_left_right, - ); - - std::fs::write(args.merge_path, result.to_string())?; - - Ok(()) } diff --git a/bin/tests/cli.rs b/bin/tests/cli.rs index 5c77a3f..df15a20 100644 --- a/bin/tests/cli.rs +++ b/bin/tests/cli.rs @@ -3,99 +3,23 @@ use std::process::Command; use assert_cmd::prelude::*; #[test] -fn it_does_not_crash_and_produces_the_expected_output() { +fn if_there_is_a_conflict_it_returns_valid_exit_code() { let mut cmd = Command::cargo_bin("generic-merge").unwrap(); - cmd.arg("--base-path=tests/samples/smoke_java/base.java") - .arg("--left-path=tests/samples/smoke_java/left.java") - .arg("--right-path=tests/samples/smoke_java/right.java") - .arg("--merge-path=tests/samples/smoke_java/merge.output.java") + cmd.arg("--base-path=tests/scenarios/smoke_java/base.java") + .arg("--left-path=tests/scenarios/smoke_java/left.java") + .arg("--right-path=tests/scenarios/smoke_java/right.java") + .arg("--merge-path=tests/scenarios/smoke_java/merge.java") .assert() - .code(0); - - assert_eq!( - std::fs::read_to_string("tests/samples/smoke_java/merge.expected.java").unwrap(), - std::fs::read_to_string("tests/samples/smoke_java/merge.output.java").unwrap() - ) -} - -#[test] -fn if_left_equals_base_then_output_right_as_result() { - let mut cmd = Command::cargo_bin("generic-merge").unwrap(); - cmd.arg("--base-path=tests/samples/one_parent_equals_base/base.java") - .arg("--left-path=tests/samples/one_parent_equals_base/base.java") - .arg("--right-path=tests/samples/one_parent_equals_base/changed_parent.java") - .arg("--merge-path=tests/samples/one_parent_equals_base/merge.output.right.java") - .assert() - .code(0); - - assert_eq!( - std::fs::read_to_string("tests/samples/one_parent_equals_base/changed_parent.java") - .unwrap(), - std::fs::read_to_string("tests/samples/one_parent_equals_base/merge.output.right.java") - .unwrap() - ) + .code(bin::SUCCESS_WITH_CONFLICTS); } #[test] -fn if_right_equals_base_then_output_left_as_result() { +fn if_there_is_no_conflict_it_returns_valid_exit_code() { let mut cmd = Command::cargo_bin("generic-merge").unwrap(); - cmd.arg("--base-path=tests/samples/one_parent_equals_base/base.java") - .arg("--left-path=tests/samples/one_parent_equals_base/base.java") - .arg("--right-path=tests/samples/one_parent_equals_base/changed_parent.java") - .arg("--merge-path=tests/samples/one_parent_equals_base/merge.output.left.java") + cmd.arg("--base-path=tests/scenarios/no_conflicts/base.java") + .arg("--left-path=tests/scenarios/no_conflicts/left.java") + .arg("--right-path=tests/scenarios/no_conflicts/right.java") + .arg("--merge-path=tests/scenarios/no_conflicts/merge.java") .assert() - .code(0); - - assert_eq!( - std::fs::read_to_string("tests/samples/one_parent_equals_base/changed_parent.java") - .unwrap(), - std::fs::read_to_string("tests/samples/one_parent_equals_base/merge.output.left.java") - .unwrap() - ) -} - -#[test] -fn it_works_on_semi_structured_merge() { - run_tool_on_scenario("semi_structured") -} - -#[test] -fn it_works_on_node_reordering() { - run_tool_on_scenario("node_reordering") -} - -#[test] -fn it_works_on_no_conflicts() { - run_tool_on_scenario("no_conflicts") -} - -#[test] -fn it_works_on_arithmetic_expression() { - run_tool_on_scenario("arithmetic_expression") -} - -#[test] -fn it_works_on_consecutive_lines() { - run_tool_on_scenario("consecutive_lines") -} - -fn run_tool_on_scenario(scenario_name: &str) { - let mut cmd = Command::cargo_bin("generic-merge").unwrap(); - cmd.arg(format!("-b=tests/samples/{}/base.java", scenario_name)) - .arg(format!("-l=tests/samples/{}/left.java", scenario_name)) - .arg(format!("-r=tests/samples/{}/right.java", scenario_name)) - .arg(format!( - "-m=tests/samples/{}/merge.output.java", - scenario_name - )) - .assert() - .code(0); - - let expected_result_path = format!("tests/samples/{}/merge.expected.java", scenario_name); - let actual_result_path = format!("tests/samples/{}/merge.output.java", scenario_name); - - assert_eq!( - std::fs::read_to_string(expected_result_path).unwrap(), - std::fs::read_to_string(actual_result_path).unwrap() - ) + .code(bin::SUCCESS_WITHOUT_CONFLICTS); } diff --git a/bin/tests/samples/one_parent_equals_base/base.java b/bin/tests/samples/one_parent_equals_base/base.java deleted file mode 100644 index 82579a7..0000000 --- a/bin/tests/samples/one_parent_equals_base/base.java +++ /dev/null @@ -1,3 +0,0 @@ -public interface Main { - create(Pessoa pessoa); -} \ No newline at end of file diff --git a/bin/tests/samples/one_parent_equals_base/changed_parent.java b/bin/tests/samples/one_parent_equals_base/changed_parent.java deleted file mode 100644 index cda7866..0000000 --- a/bin/tests/samples/one_parent_equals_base/changed_parent.java +++ /dev/null @@ -1,4 +0,0 @@ -public interface Main { - create(Pessoa pessoa); - delete(Pessoa pessoa); -} \ No newline at end of file diff --git a/bin/tests/scenarios.rs b/bin/tests/scenarios.rs new file mode 100644 index 0000000..2422811 --- /dev/null +++ b/bin/tests/scenarios.rs @@ -0,0 +1,29 @@ +#[test] +fn all_java_samples_work_correctly() -> Result<(), Box> { + let sample_names = get_samples_names()?; + + for sample_path in sample_names { + let base = std::fs::read_to_string(format!("{}/base.java", sample_path.display()))?; + let left = std::fs::read_to_string(format!("{}/left.java", sample_path.display()))?; + let right = std::fs::read_to_string(format!("{}/right.java", sample_path.display()))?; + + let expected = std::fs::read_to_string(format!("{}/merge.java", sample_path.display()))?; + let result = bin::run_tool_on_merge_scenario(model::Language::Java, &base, &left, &right)?; + + assert_eq!(expected, result.to_string()) + } + + Ok(()) +} + +fn get_samples_names() -> Result, std::io::Error> { + std::fs::read_dir("tests/scenarios")? + .filter(|sample| { + sample + .as_ref() + .map(|sample| sample.path().is_dir()) + .unwrap_or(false) + }) + .map(|sample| sample.map(|sample| sample.path())) + .collect() +} diff --git a/bin/tests/samples/.gitignore b/bin/tests/scenarios/.gitignore similarity index 100% rename from bin/tests/samples/.gitignore rename to bin/tests/scenarios/.gitignore diff --git a/bin/tests/samples/arithmetic_expression/base.java b/bin/tests/scenarios/arithmetic_expression/base.java similarity index 100% rename from bin/tests/samples/arithmetic_expression/base.java rename to bin/tests/scenarios/arithmetic_expression/base.java diff --git a/bin/tests/samples/arithmetic_expression/left.java b/bin/tests/scenarios/arithmetic_expression/left.java similarity index 100% rename from bin/tests/samples/arithmetic_expression/left.java rename to bin/tests/scenarios/arithmetic_expression/left.java diff --git a/bin/tests/samples/arithmetic_expression/merge.expected.java b/bin/tests/scenarios/arithmetic_expression/merge.java similarity index 100% rename from bin/tests/samples/arithmetic_expression/merge.expected.java rename to bin/tests/scenarios/arithmetic_expression/merge.java diff --git a/bin/tests/samples/arithmetic_expression/right.java b/bin/tests/scenarios/arithmetic_expression/right.java similarity index 100% rename from bin/tests/samples/arithmetic_expression/right.java rename to bin/tests/scenarios/arithmetic_expression/right.java diff --git a/bin/tests/samples/consecutive_lines/base.java b/bin/tests/scenarios/consecutive_lines/base.java similarity index 100% rename from bin/tests/samples/consecutive_lines/base.java rename to bin/tests/scenarios/consecutive_lines/base.java diff --git a/bin/tests/samples/consecutive_lines/left.java b/bin/tests/scenarios/consecutive_lines/left.java similarity index 100% rename from bin/tests/samples/consecutive_lines/left.java rename to bin/tests/scenarios/consecutive_lines/left.java diff --git a/bin/tests/samples/consecutive_lines/merge.expected.java b/bin/tests/scenarios/consecutive_lines/merge.java similarity index 100% rename from bin/tests/samples/consecutive_lines/merge.expected.java rename to bin/tests/scenarios/consecutive_lines/merge.java diff --git a/bin/tests/samples/consecutive_lines/right.java b/bin/tests/scenarios/consecutive_lines/right.java similarity index 100% rename from bin/tests/samples/consecutive_lines/right.java rename to bin/tests/scenarios/consecutive_lines/right.java diff --git a/bin/tests/samples/no_conflicts/base.java b/bin/tests/scenarios/no_conflicts/base.java similarity index 100% rename from bin/tests/samples/no_conflicts/base.java rename to bin/tests/scenarios/no_conflicts/base.java diff --git a/bin/tests/samples/no_conflicts/left.java b/bin/tests/scenarios/no_conflicts/left.java similarity index 100% rename from bin/tests/samples/no_conflicts/left.java rename to bin/tests/scenarios/no_conflicts/left.java diff --git a/bin/tests/samples/no_conflicts/merge.expected.java b/bin/tests/scenarios/no_conflicts/merge.java similarity index 100% rename from bin/tests/samples/no_conflicts/merge.expected.java rename to bin/tests/scenarios/no_conflicts/merge.java diff --git a/bin/tests/samples/no_conflicts/right.java b/bin/tests/scenarios/no_conflicts/right.java similarity index 100% rename from bin/tests/samples/no_conflicts/right.java rename to bin/tests/scenarios/no_conflicts/right.java diff --git a/bin/tests/samples/node_reordering/base.java b/bin/tests/scenarios/node_reordering/base.java similarity index 100% rename from bin/tests/samples/node_reordering/base.java rename to bin/tests/scenarios/node_reordering/base.java diff --git a/bin/tests/samples/node_reordering/left.java b/bin/tests/scenarios/node_reordering/left.java similarity index 100% rename from bin/tests/samples/node_reordering/left.java rename to bin/tests/scenarios/node_reordering/left.java diff --git a/bin/tests/samples/node_reordering/merge.expected.java b/bin/tests/scenarios/node_reordering/merge.java similarity index 100% rename from bin/tests/samples/node_reordering/merge.expected.java rename to bin/tests/scenarios/node_reordering/merge.java diff --git a/bin/tests/samples/node_reordering/right.java b/bin/tests/scenarios/node_reordering/right.java similarity index 100% rename from bin/tests/samples/node_reordering/right.java rename to bin/tests/scenarios/node_reordering/right.java diff --git a/bin/tests/samples/semi_structured/base.java b/bin/tests/scenarios/semi_structured/base.java similarity index 100% rename from bin/tests/samples/semi_structured/base.java rename to bin/tests/scenarios/semi_structured/base.java diff --git a/bin/tests/samples/semi_structured/left.java b/bin/tests/scenarios/semi_structured/left.java similarity index 100% rename from bin/tests/samples/semi_structured/left.java rename to bin/tests/scenarios/semi_structured/left.java diff --git a/bin/tests/samples/semi_structured/merge.expected.java b/bin/tests/scenarios/semi_structured/merge.java similarity index 100% rename from bin/tests/samples/semi_structured/merge.expected.java rename to bin/tests/scenarios/semi_structured/merge.java diff --git a/bin/tests/samples/semi_structured/right.java b/bin/tests/scenarios/semi_structured/right.java similarity index 100% rename from bin/tests/samples/semi_structured/right.java rename to bin/tests/scenarios/semi_structured/right.java diff --git a/bin/tests/samples/smoke_java/base.java b/bin/tests/scenarios/smoke_java/base.java similarity index 100% rename from bin/tests/samples/smoke_java/base.java rename to bin/tests/scenarios/smoke_java/base.java diff --git a/bin/tests/samples/smoke_java/left.java b/bin/tests/scenarios/smoke_java/left.java similarity index 100% rename from bin/tests/samples/smoke_java/left.java rename to bin/tests/scenarios/smoke_java/left.java diff --git a/bin/tests/samples/smoke_java/merge.expected.java b/bin/tests/scenarios/smoke_java/merge.java similarity index 100% rename from bin/tests/samples/smoke_java/merge.expected.java rename to bin/tests/scenarios/smoke_java/merge.java diff --git a/bin/tests/samples/smoke_java/right.java b/bin/tests/scenarios/smoke_java/right.java similarity index 100% rename from bin/tests/samples/smoke_java/right.java rename to bin/tests/scenarios/smoke_java/right.java diff --git a/merge/src/lib.rs b/merge/src/lib.rs index b04db11..059aab8 100644 --- a/merge/src/lib.rs +++ b/merge/src/lib.rs @@ -3,8 +3,8 @@ mod merged_cst_node; mod ordered_merge; mod unordered_merge; -use merged_cst_node::MergedCSTNode; use ordered_merge::ordered_merge; use unordered_merge::unordered_merge; pub use merge::merge; +pub use merged_cst_node::MergedCSTNode; diff --git a/model/src/language.rs b/model/src/language.rs index bf06bfa..fb847eb 100644 --- a/model/src/language.rs +++ b/model/src/language.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Eq, PartialEq, Debug)] pub enum Language { Java, }