Skip to content

Commit

Permalink
WIP on master: d67a8c0 Add --locked v0.7.1 (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
ggwpez committed Jul 29, 2023
2 parents d67a8c0 + bf35f17 commit ffb63a7
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 310 deletions.
13 changes: 9 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ serde = "1.0.171"
toml_edit = "0.19.14"
version = "3.0.0"

tempfile = { version = "3.6.0", optional = true }
serde_yaml = { version = "0.9.23", optional = true }
assert_cmd = { version = "2.0.12", optional = true }

[dev-dependencies]
zepter = { path = ".", features = ["testing"] }

assert_cmd = "2.0.12"
criterion = "0.5"
colour = "0.7.0"
glob = "0.3.1"
Expand All @@ -38,12 +41,14 @@ pretty_assertions = "1.4.0"
rand = "0.8.5"
rstest = "0.18.1"
serde = "1.0.171"
serde_yaml = "0.9.23"
tempfile = "3.6.0"
void = "1.0.2"

[features]
testing = []
testing = [
"dep:tempfile",
"dep:serde_yaml",
"dep:assert_cmd",
]

[profile.dev]
opt-level = 3
Expand Down
90 changes: 58 additions & 32 deletions src/mock.rs → src/mock/git.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: Oliver Tale-Yazdi <oliver@tasty.limo>

//! Helpers for writing tests.
//! Helpers for cloning and checking out git repositories.

#![cfg(feature = "testing")]

use std::{path::Path, process::Command};

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Case {
pub cmd: String,

#[serde(skip_serializing_if = "String::is_empty")]
#[serde(default)]
pub stdout: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<i32>,

#[serde(skip_serializing_if = "String::is_empty")]
#[serde(default)]
pub diff: String,
}

/// Removes leading and trailing empty lines.
pub fn normalize(s: &str) -> String {
let mut lines = s.lines().collect::<Vec<_>>();
while lines.first().map(|l| l.is_empty()).is_some() {
lines.remove(0);
}
while lines.last().map(|l| l.is_empty()).is_some() {
lines.pop();
}
format!("{}\n", lines.join("\n"))
}
use std::{path::Path, path::PathBuf, process::Command};

/// Create a mocked git repository.
pub fn git_init(dir: &Path) -> Result<(), anyhow::Error> {
Expand Down Expand Up @@ -109,3 +79,59 @@ pub fn git_reset(dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
cmd.status()?;
Ok(())
}

pub fn clone_repo(repo: &str, rev: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
let dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".into());
let repos_dir = std::path::Path::new(&dir).join("test-repos");
let dir = repos_dir.join(repo);

// Check if the repo is already cloned
if Path::new(&dir).exists() {
} else {
std::fs::create_dir_all(&dir)?;

let mut cmd = Command::new("git");
cmd.current_dir(&dir);
cmd.arg("init");
cmd.arg("--quiet");
cmd.status()?;

// add remote
let mut cmd = Command::new("git");
cmd.current_dir(&dir);
cmd.arg("remote");
cmd.arg("add");
cmd.arg("origin");
cmd.arg(&format!("https://github.com/paritytech/{}", repo));
cmd.status()?;

fetch(&dir, rev)?;
}

if checkout(&dir, rev).is_err() {
fetch(&dir, rev)?;
checkout(&dir, rev)?;
}
Ok(dir)
}

pub fn fetch(dir: &PathBuf, rev: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::new("git");
cmd.current_dir(dir);
cmd.arg("fetch");
cmd.arg("--depth");
cmd.arg("1");
cmd.arg("origin");
cmd.arg(rev);
cmd.status()?;
Ok(())
}

pub fn checkout(dir: &PathBuf, rev: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::new("git");
cmd.current_dir(dir);
cmd.arg("checkout");
cmd.arg(rev);
cmd.status()?;
Ok(())
}
264 changes: 264 additions & 0 deletions src/mock/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: Oliver Tale-Yazdi <oliver@tasty.limo>

//! Helpers for writing tests.

#![cfg(feature = "testing")]

pub mod git;
pub use git::*;

use std::{path::Path, process::Command};
use assert_cmd::{assert::OutputAssertExt};
use std::{
collections::HashMap,
fs,
path::{PathBuf},
};
use std::io::Write;

pub type ModuleName = String;

/// A single test case.
///
/// Holds the input arguments, the stdout, an optional git diff and the exit code of the binary.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Case {
pub cmd: String,

#[serde(skip_serializing_if = "String::is_empty")]
#[serde(default)]
pub stdout: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<i32>,

#[serde(skip_serializing_if = "String::is_empty")]
#[serde(default)]
pub diff: String,
}

/// A specific github repo checkout.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Repo {
pub name: String,
#[serde(rename = "ref")]
pub ref_spec: String,
}

/// Describes the setup for a UI test.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct UiCaseFile {
pub crates: Vec<CrateConfig>,
pub cases: Vec<Case>,
}

impl UiCaseFile {
pub fn init(&self) -> Result<Context, anyhow::Error> {
let ctx = Context::new();
for module in self.crates.iter() {
ctx.create_crate(module)?;
}
ctx.create_workspace(&self.crates)?;
git_init(ctx.root.path())?;
Ok(ctx)
}

pub fn from_file(path: &Path) -> Self {
let content = fs::read_to_string(path).unwrap();
let content = content.replace('\t', " ");
serde_yaml::from_str(&content)
.unwrap_or_else(|_| panic!("Failed to parse: {}", &path.display()))
}
}

/// Describes the setup for an integration test.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct IntegrationCaseFile {
pub repo: Repo,
pub cases: Vec<Case>,
}

impl IntegrationCaseFile {
pub fn from_file(path: &Path) -> Self {
let content = fs::read_to_string(path).unwrap();
let content = content.replace('\t', " ");
serde_yaml::from_str(&content)
.unwrap_or_else(|_| panic!("Failed to parse: {}", &path.display()))
}

pub fn init(&self) -> Result<PathBuf, Box<dyn std::error::Error>> {
clone_repo(&self.repo.name, &self.repo.ref_spec)
}
}

/// Describes a Rust crate, its features and dependencies.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct CrateConfig {
name: ModuleName,
#[serde(skip_serializing_if = "Option::is_none")]
deps: Option<Vec<CrateDependency>>,
#[serde(skip_serializing_if = "Option::is_none")]
features: Option<HashMap<String, Option<Vec<(String, String)>>>>,
}

impl CrateConfig {
/// Return the file path of this crate.
pub fn path(&self) -> String {
crate_name_to_path(&self.name)
}
}

pub struct Context {
pub root: tempfile::TempDir,
}

impl Context {
pub fn new() -> Self {
Self { root: tempfile::tempdir().expect("Must create a temporary directory") }
}

pub fn persist(self) -> PathBuf {
self.root.into_path()
}

pub fn create_crate(&self, module: &CrateConfig) -> Result<(), anyhow::Error> {
self.cargo(
&format!("new --vcs=none --offline --lib --name {} {}", module.name, module.path()),
None,
)?;
let toml_path = self.root.path().join(&module.path()).join("Cargo.toml");
assert!(toml_path.exists(), "Crate must exist");
// Add the deps
let mut out_deps = String::from("");
for dep in module.deps.iter().flatten() {
out_deps.push_str(&dep.def());
}

let mut txt = String::from("[features]\n");
for (feature, enables) in module.features.iter().flatten() {
txt.push_str(&format!("{} = [\n", feature));
for (dep, feat) in enables.iter().flatten() {
txt.push_str(&format!("\"{}/{}\",\n", dep, feat));
}
txt.push_str("]\n");
}

let output = format!("{}\n{}", out_deps, txt);
// Append to the toml
let mut file = fs::OpenOptions::new().append(true).open(toml_path).unwrap();
file.write_all(output.as_bytes()).unwrap();
Ok(())
}

pub fn create_workspace(&self, subs: &[CrateConfig]) -> Result<(), anyhow::Error> {
let mut txt = String::from("[workspace]\nmembers = [");
for sub in subs.iter() {
txt.push_str(&format!("\"{}\",", sub.path()));
}
txt.push(']');
let toml_path = self.root.path().join("Cargo.toml");
fs::write(toml_path, txt)?;
Ok(())
}

pub fn cargo(&self, cmd: &str, sub_dir: Option<&str>) -> Result<(), anyhow::Error> {
assert!(self.root.path().exists());
let dir = match sub_dir {
Some(sub_dir) => self.root.path().join(sub_dir),
None => self.root.path().to_owned(),
};

let args = cmd.split_whitespace().collect::<Vec<_>>();
let output = Command::new("cargo")
.args(&args)
.current_dir(&dir)
.output()
.expect("failed to execute cargo");

if !output.status.success() {
Err(anyhow::Error::msg(String::from_utf8(output.stderr).unwrap()))
} else {
Ok(())
}
}
}

/// Convert a crate's name to a file path.
///
/// This is needed for case-insensitive file systems like on MacOS. It prefixes all lower-case
/// letters with an `l` and turns the upper case.
pub(crate) fn crate_name_to_path(n: &str) -> String {
n.chars()
.map(|c| if c.is_lowercase() { format!("l{}", c.to_uppercase()) } else { c.into() })
.collect()
}

/// Describes a crate dependency.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
#[serde(untagged)]
pub enum CrateDependency {
Implicit(String),
Explicit {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
rename: Option<String>,
#[serde(skip_serializing_if = "is_false")]
optional: Option<bool>,
},
}

impl CrateDependency {
fn def(&self) -> String {
let option = if self.optional() { ", optional = true".to_string() } else { String::new() };
let mut ret = match self.rename() {
Some(rename) => format!("{} = {{ package = \"{}\", ", rename, self.name()),
None => format!("{} = {{ ", self.name()),
};
ret.push_str(&format!("version = \"*\", path = \"../{}\"{}}}\n", self.path(), option));
ret
}

fn path(&self) -> String {
crate_name_to_path(&self.name())
}

fn name(&self) -> String {
match self {
Self::Explicit { name, .. } | Self::Implicit(name) => name.clone(),
}
}

fn rename(&self) -> Option<String> {
match self {
Self::Explicit { rename, .. } => rename.clone(),
_ => None,
}
}

fn optional(&self) -> bool {
match self {
Self::Explicit { optional, .. } => optional.unwrap_or_default(),
_ => false,
}
}
}


/// Removes leading and trailing empty lines.
pub fn normalize(s: &str) -> String {
let mut lines = s.lines().collect::<Vec<_>>();
while lines.first().map(|l| l.is_empty()).is_some() {
lines.remove(0);
}
while lines.last().map(|l| l.is_empty()).is_some() {
lines.pop();
}
format!("{}\n", lines.join("\n"))
}


/// Predicate for serde to skip serialization of default values.
fn is_false(b: &Option<bool>) -> bool {
!b.unwrap_or_default()
}
Loading

0 comments on commit ffb63a7

Please sign in to comment.