Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adjust exit codes for CLI #24

Merged
merged 8 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions bin/src/cli_exit_codes.rs
Original file line number Diff line number Diff line change
@@ -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;
89 changes: 89 additions & 0 deletions bin/src/control.rs
Original file line number Diff line number Diff line change
@@ -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<ExecutionResult, ExecutionError> {
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,
}
}
14 changes: 7 additions & 7 deletions bin/src/parser_configuration.rs → bin/src/language.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
pub fn get_parser_configuration_by_file_path(
file_path: &std::path::Path,
) -> Result<parsing::ParserConfiguration, String> {
pub fn get_language_by_file_path(file_path: &std::path::Path) -> Result<model::Language, String> {
file_path
.extension()
.and_then(std::ffi::OsStr::to_str)
.and_then(|extension| match extension {
"java" => Some(model::Language::Java),
_ => None,
})
.map(parsing::ParserConfiguration::from)
.ok_or(format!(
"Could not retrieve parsing configuration for file {}",
file_path.display()
Expand All @@ -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
)
}
}
5 changes: 5 additions & 0 deletions bin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod cli_exit_codes;
mod control;

pub use cli_exit_codes::*;
pub use control::run_tool_on_merge_scenario;
65 changes: 27 additions & 38 deletions bin/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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(())
}
100 changes: 12 additions & 88 deletions bin/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
3 changes: 0 additions & 3 deletions bin/tests/samples/one_parent_equals_base/base.java

This file was deleted.

4 changes: 0 additions & 4 deletions bin/tests/samples/one_parent_equals_base/changed_parent.java

This file was deleted.

29 changes: 29 additions & 0 deletions bin/tests/scenarios.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#[test]
fn all_java_samples_work_correctly() -> Result<(), Box<dyn std::error::Error>> {
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<Vec<std::path::PathBuf>, 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()
}
File renamed without changes.
2 changes: 1 addition & 1 deletion merge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion model/src/language.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum Language {
Java,
}
Loading