feat(migrate): add test cases and rendering implementation

This commit is contained in:
Federico Terzi 2021-05-29 13:08:32 +02:00
parent 4388fc0ba6
commit 21c988c2b4
12 changed files with 223 additions and 30 deletions

7
Cargo.lock generated
View File

@ -509,6 +509,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce", "dunce",
"fs_extra",
"glob", "glob",
"include_dir", "include_dir",
"lazy_static", "lazy_static",
@ -616,6 +617,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "fs_extra"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
[[package]] [[package]]
name = "fuchsia-cprng" name = "fuchsia-cprng"
version = "0.1.1" version = "0.1.1"

View File

@ -15,9 +15,10 @@ dunce = "1.0.1"
walkdir = "2.3.1" walkdir = "2.3.1"
yaml-rust = "0.4.5" yaml-rust = "0.4.5"
path-slash = "0.1.4" path-slash = "0.1.4"
tempdir = "0.3.7"
fs_extra = "1.2.0"
[dev-dependencies] [dev-dependencies]
tempdir = "0.3.7"
tempfile = "3.2.0" tempfile = "3.2.0"
include_dir = { version = "0.6.0", features = ["search"] } include_dir = { version = "0.6.0", features = ["search"] }
test-case = "1.1.0" test-case = "1.1.0"

View File

@ -18,9 +18,14 @@
*/ */
use std::{cmp::Ordering, collections::HashMap, path::PathBuf}; use std::{cmp::Ordering, collections::HashMap, path::PathBuf};
use yaml_rust::{yaml::Hash, Yaml, YamlEmitter}; use yaml_rust::{yaml::Hash, Yaml};
pub fn convert(input_files: HashMap<String, Hash>) -> HashMap<String, Hash> { pub struct ConvertedFile {
pub origin: String,
pub content: Hash,
}
pub fn convert(input_files: HashMap<String, Hash>) -> HashMap<String, ConvertedFile> {
let mut output_files = HashMap::new(); let mut output_files = HashMap::new();
let sorted_input_files = sort_input_files(&input_files); let sorted_input_files = sort_input_files(&input_files);
@ -57,10 +62,13 @@ pub fn convert(input_files: HashMap<String, Hash>) -> HashMap<String, Hash> {
let output_yaml = output_files let output_yaml = output_files
.entry(match_output_path.clone()) .entry(match_output_path.clone())
.or_insert(Hash::new()); .or_insert(ConvertedFile {
origin: input_path.to_string(),
content: Hash::new(),
});
if let Some(global_vars) = yaml_global_vars { if let Some(global_vars) = yaml_global_vars {
let output_global_vars = output_yaml let output_global_vars = output_yaml.content
.entry(Yaml::String("global_vars".to_string())) .entry(Yaml::String("global_vars".to_string()))
.or_insert(Yaml::Array(Vec::new())); .or_insert(Yaml::Array(Vec::new()));
if let Yaml::Array(out_global_vars) = output_global_vars { if let Yaml::Array(out_global_vars) = output_global_vars {
@ -71,7 +79,7 @@ pub fn convert(input_files: HashMap<String, Hash>) -> HashMap<String, Hash> {
} }
if let Some(matches) = yaml_matches { if let Some(matches) = yaml_matches {
let output_matches = output_yaml let output_matches = output_yaml.content
.entry(Yaml::String("matches".to_string())) .entry(Yaml::String("matches".to_string()))
.or_insert(Yaml::Array(Vec::new())); .or_insert(Yaml::Array(Vec::new()));
if let Yaml::Array(out_matches) = output_matches { if let Yaml::Array(out_matches) = output_matches {
@ -162,7 +170,10 @@ pub fn convert(input_files: HashMap<String, Hash>) -> HashMap<String, Hash> {
output_yaml.insert(Yaml::String(key_name.to_string()), Yaml::Array(includes)); output_yaml.insert(Yaml::String(key_name.to_string()), Yaml::Array(includes));
} }
output_files.insert(config_output_path, output_yaml); output_files.insert(config_output_path, ConvertedFile {
origin: input_path,
content: output_yaml,
});
} }
// TODO: create config file // TODO: create config file

View File

@ -28,11 +28,16 @@ extern crate include_dir;
#[cfg(test)] #[cfg(test)]
extern crate test_case; extern crate test_case;
use std::path::Path;
use anyhow::Result; use anyhow::Result;
use fs_extra::dir::CopyOptions;
use tempdir::TempDir;
use thiserror::Error; use thiserror::Error;
mod convert; mod convert;
mod load; mod load;
mod render;
// TODO: implement // TODO: implement
// Use yaml-rust with "preserve-order" = true // Use yaml-rust with "preserve-order" = true
@ -51,33 +56,94 @@ mod load;
// creates the new config on a new temporary directory and then "swaps" // creates the new config on a new temporary directory and then "swaps"
// the old with the new one // the old with the new one
// TODO: test case with packages
// TODO: keep other non-standard directories such as "images/" and "script/"
// TODO: test also with non-lowercase file names // TODO: test also with non-lowercase file names
// TODO: test packages in another directory (a possible strategy is to copy pub fn migrate(config_dir: &Path, packages_dir: &Path, output_dir: &Path) -> Result<()> {
// the packages dir and the config dir into a temporary one, with the packages if !config_dir.is_dir() {
// as a directory at the same level of user/) return Err(MigrationError::InvalidConfigDir.into());
}
// TODO: when dumping the output file, remove the front-matter at the top (generated by YamlEmitter) let working_dir = TempDir::new("espanso-migration")?;
// and insert a comment with "Automatically generated from {{file_name}} by migration tool"
fs_extra::dir::copy(
config_dir,
working_dir.path(),
&CopyOptions {
content_only: true,
..Default::default()
},
)?;
// If packages are located within the config_dir, no need to copy them in
// the working directory
if packages_dir.parent() != Some(config_dir) {
fs_extra::dir::copy(packages_dir, working_dir.path(), &CopyOptions::new())?;
}
// Create the output directory
if output_dir.exists() {
return Err(MigrationError::OutputDirAlreadyPresent.into());
}
std::fs::create_dir_all(output_dir)?;
// Convert the configurations
let legacy_files = load::load(working_dir.path())?;
let converted_files = convert::convert(legacy_files);
let rendered_files = render::render(converted_files)?;
for (file, content) in rendered_files {
let target = output_dir.join(file);
if let Some(parent) = target.parent() {
if !parent.is_dir() {
std::fs::create_dir_all(parent)?;
}
}
std::fs::write(target, content)?;
}
// Copy all non-YAML directories
for entry in std::fs::read_dir(working_dir.path())? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let dir_name = path.file_name();
if let Some(name) = dir_name.map(|s| s.to_string_lossy().to_string().to_lowercase()) {
if name != "user" && name != "packages" {
fs_extra::dir::copy(path, output_dir, &CopyOptions::new())?;
}
}
}
}
Ok(())
}
#[derive(Error, Debug)]
pub enum MigrationError {
#[error("invalid config directory")]
InvalidConfigDir,
#[error("output directory already present")]
OutputDirAlreadyPresent,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{collections::HashMap, fs::create_dir_all, path::{Path}}; use std::{collections::HashMap, fs::create_dir_all, path::Path};
use super::*; use super::*;
use include_dir::{include_dir, Dir}; use include_dir::{include_dir, Dir};
use tempdir::TempDir; use tempdir::TempDir;
use test_case::test_case; use test_case::test_case;
use pretty_assertions::{assert_eq as assert_peq}; use pretty_assertions::assert_eq as assert_peq;
use yaml_rust::{yaml::Hash, Yaml}; use yaml_rust::{yaml::Hash, Yaml};
fn run_with_temp_dir(test_data: &Dir, action: impl FnOnce(&Path, &Path)) { fn run_with_temp_dir(test_data: &Dir, action: impl FnOnce(&Path, &Path)) {
let tmp_dir = TempDir::new("espanso-migration").unwrap(); let tmp_dir = TempDir::new("espanso-migrate").unwrap();
let tmp_path = tmp_dir.path(); let tmp_path = tmp_dir.path();
let legacy_path = tmp_dir.path().join("legacy"); let legacy_path = tmp_dir.path().join("legacy");
let expected_path = tmp_dir.path().join("expected"); let expected_path = tmp_dir.path().join("expected");
@ -109,30 +175,54 @@ use yaml_rust::{yaml::Hash, Yaml};
} }
fn to_sorted_hash(hash: &Hash) -> Vec<(String, &Yaml)> { fn to_sorted_hash(hash: &Hash) -> Vec<(String, &Yaml)> {
let mut tuples: Vec<(String, &Yaml)> = hash.into_iter().map(|(k, v)| (k.as_str().unwrap().to_string(), v)).collect(); let mut tuples: Vec<(String, &Yaml)> = hash
.into_iter()
.map(|(k, v)| (k.as_str().unwrap().to_string(), v))
.collect();
tuples.sort_by_key(|(k, _)| k.clone()); tuples.sort_by_key(|(k, _)| k.clone());
tuples tuples
} }
fn list_files_in_dir(dir: &Path) -> Vec<String> {
let prefix = dir.to_string_lossy().to_string();
fs_extra::dir::get_dir_content(&dir)
.unwrap()
.files
.into_iter()
.map(|file| file.trim_start_matches(&prefix).to_string())
.collect()
}
static SIMPLE_CASE: Dir = include_dir!("test/simple"); static SIMPLE_CASE: Dir = include_dir!("test/simple");
static BASE_CASE: Dir = include_dir!("test/base"); static BASE_CASE: Dir = include_dir!("test/base");
static ALL_PARAMS_CASE: Dir = include_dir!("test/all_params"); static ALL_PARAMS_CASE: Dir = include_dir!("test/all_params");
static OTHER_DIRS_CASE: Dir = include_dir!("test/other_dirs");
#[test_case(&SIMPLE_CASE; "simple case")] #[test_case(&SIMPLE_CASE; "simple case")]
#[test_case(&BASE_CASE; "base case")] #[test_case(&BASE_CASE; "base case")]
#[test_case(&ALL_PARAMS_CASE; "all config parameters case")] #[test_case(&ALL_PARAMS_CASE; "all config parameters case")]
#[test_case(&OTHER_DIRS_CASE; "other directories case")]
fn test_migration(test_data: &Dir) { fn test_migration(test_data: &Dir) {
run_with_temp_dir(test_data, |legacy, expected| { run_with_temp_dir(test_data, |legacy, expected| {
let legacy_files = load::load(legacy).unwrap(); let tmp_out_dir = TempDir::new("espanso-migrate-out").unwrap();
let output_dir = tmp_out_dir.path().join("out");
migrate(legacy, &legacy.join("packages"), &output_dir).unwrap();
let converted_files = load::load(&output_dir).unwrap();
// Verify configuration content
let expected_files = load::load(expected).unwrap(); let expected_files = load::load(expected).unwrap();
let converted_files = convert::convert(legacy_files);
assert_eq!(converted_files.len(), expected_files.len()); assert_eq!(converted_files.len(), expected_files.len());
for (file, converted) in to_sorted_list(converted_files) {
for (file, content) in to_sorted_list(converted_files) { assert_peq!(
assert_peq!(to_sorted_hash(&content), to_sorted_hash(&expected_files.get(&file).unwrap())); to_sorted_hash(&converted),
to_sorted_hash(&expected_files.get(&file).unwrap())
);
} }
// Verify file structure
assert_peq!(list_files_in_dir(expected), list_files_in_dir(&output_dir));
}); });
} }
} }

View File

@ -0,0 +1,49 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use std::collections::HashMap;
use yaml_rust::{Yaml, YamlEmitter};
use crate::convert::ConvertedFile;
pub fn render(converted_files: HashMap<String, ConvertedFile>) -> Result<Vec<(String, String)>> {
let mut output = Vec::new();
for (file, content) in converted_files {
let rendered = render_file(content)?;
output.push((file, rendered));
}
Ok(output)
}
fn render_file(file: ConvertedFile) -> Result<String> {
let mut dump_str = String::new();
let mut emitter = YamlEmitter::new(&mut dump_str);
emitter.dump(&Yaml::Hash(file.content))?;
let lines: Vec<&str> = dump_str.lines().collect();
let header = format!("# Original file: {}", file.origin);
let mut output_lines: Vec<&str> = vec!["# Automatically generated by espanso migration tool", &header, ""];
if !lines.is_empty() {
output_lines.extend(&lines[1..]);
}
let output = output_lines.join("\n");
Ok(output)
}

View File

@ -0,0 +1 @@
backend: Clipboard

View File

@ -0,0 +1,3 @@
filter_title: "Disabled program"
enable: false

View File

@ -0,0 +1,12 @@
global_vars:
- name: "name"
type: "dummy"
params:
echo: "John"
matches:
- name: ":hi"
trigger: "Hello"
- name: ":greet"
trigger: "Hi {{name}}"

View File

@ -0,0 +1 @@
print("this is a test")

View File

@ -0,0 +1,14 @@
backend: Clipboard
global_vars:
- name: "name"
type: "dummy"
params:
echo: "John"
matches:
- name: ":hi"
trigger: "Hello"
- name: ":greet"
trigger: "Hi {{name}}"

View File

@ -0,0 +1 @@
print("this is a test")

View File

@ -0,0 +1,3 @@
filter_title: "Disabled program"
enable_active: false