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

crazy_train: Fuzzy cli tests #832

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
31 changes: 31 additions & 0 deletions .github/workflows/crazt-train.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Crazy Train - Fuzzy loco cli generator

on:
pull_request:

# on:
# schedule:
# - cron: 0 * * * *

env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal

jobs:
generate_template:
name: Generate Template
runs-on: ubuntu-latest

steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Install loco-cli
run: |
cargo install loco-cli
- name: generate loco template
run: |
cargo xtask fuzzy generate-template --times 100

4 changes: 4 additions & 0 deletions xtask/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ path = "src/bin/main.rs"
required-features = []

[dependencies]
serde = "1.0.210"
serde_yaml = "0.9"
clap = { version = "4.4.7", features = ["derive"] }
eyre = "0.6"
duct = "0.13.6"
Expand All @@ -24,5 +26,7 @@ lazy_static = "1.4.0"
thiserror = "1"
tabled = "0.14.0"
colored = "2.1.0"
crazy-train = "0.1.0"
shell-escape = "0.1.5"

[dev-dependencies]
55 changes: 53 additions & 2 deletions xtask/src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::env;

use cargo_metadata::{semver::Version, MetadataCommand, Package};
use clap::{
ArgAction::{SetFalse, SetTrue},
Parser, Subcommand,
};
use std::env;
use xtask::fuzzy_steps;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
Expand All @@ -29,6 +29,20 @@ enum Commands {
#[arg(short, long, action = SetFalse)]
exclude_starters: bool,
},
Fuzzy {
#[arg(global = true, short, long, default_value_t = 1)]
times: u64,
#[arg(global = true, short, long, value_parser = clap::value_parser!(u64))]
seed: Option<u64>,
#[command(subcommand)]
command: FuzzyCommands,
},
}

#[derive(Subcommand)]
enum FuzzyCommands {
GenerateTemplate,
Scaffold,
}

fn main() -> eyre::Result<()> {
Expand Down Expand Up @@ -69,6 +83,43 @@ fn main() -> eyre::Result<()> {
}
xtask::CmdExit::ok()
}
Commands::Fuzzy {
command,
seed,
times,
} => {
for _ in 1..=times {
let randomizer = seed.map_or_else(crazy_train::Randomizer::default, |seed| {
crazy_train::Randomizer::with_seed(seed)
});
let seed = randomizer.seed;
let temp_dir = env::temp_dir().join("loco");

let runner = match command {
FuzzyCommands::GenerateTemplate => {
fuzzy_steps::generate_project::run(randomizer, temp_dir.as_path())
}
FuzzyCommands::Scaffold => {
fuzzy_steps::scaffold::run(randomizer, temp_dir.as_path())
}
};

let result: Result<(), crazy_train::Error> = runner.run();

if temp_dir.exists() {
std::fs::remove_dir_all(temp_dir).expect("remove dir");
}

if let Err(err) = result {
println!("seed {seed}");
println!("{err}");

xtask::CmdExit::error_with_message("failed").exit();
}
}

xtask::CmdExit::ok()
}
};

res.exit();
Expand Down
127 changes: 127 additions & 0 deletions xtask/src/fuzzy_steps/generate_project.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use crazy_train::{executer, step, Randomizer, StringDef};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

#[derive(Serialize, Deserialize, Debug)]
pub struct GenerateProjectStep {
pub location: PathBuf,
pub project_name: String,
pub run_check: bool,
pub run_test: bool,
}

impl GenerateProjectStep {
pub fn new(
randomizer: &Randomizer,
root_dir: &Path,
project_name: Option<&str>,
run_check: bool,
run_test: bool,
) -> Self {
let project_name = project_name.map_or_else(
|| {
randomizer
.string(StringDef::from_randomizer(randomizer))
.to_string()
},
std::string::ToString::to_string,
);

Self {
location: root_dir.join(randomizer.path()),
project_name,
run_check,
run_test,
}
}
}

impl step::StepTrait for GenerateProjectStep {
fn setup(&self) -> crazy_train::Result<()> {
Ok(std::fs::create_dir_all(&self.location)?)
}

fn plan(&self, _randomizer: &Randomizer) -> crazy_train::Result<step::Plan> {
// TODO:: --template and --assets should be random also
let escaped_project_name =
shell_escape::escape(self.project_name.clone().into()).to_string();
let command = format!("loco new --name {} --template saas --db sqlite --bg async --assets serverside --path {}", escaped_project_name,self.location.display());

Ok(step::Plan {
id: std::any::type_name::<Self>().to_string(),
command,
})
}

fn is_success(
&self,
execution_result: &executer::Output,
) -> std::result::Result<bool, &'static str> {
let re_invalid_project_name = Regex::new(
r"(the first character must be a|characters must be Unicode XID characters|the name cannot start with a digit)",
)
.unwrap();
let re_folder_exists = Regex::new(r"🙀 The specified path '.*.' already exist\n").unwrap();
let re_successfully = Regex::new(r"\n🚂 Loco app generated successfully in:\n.*").unwrap();

if StringDef::contains_unicode(&self.project_name)
|| self
.project_name
.chars()
.any(|c| StringDef::contains_symbols(&format!("{c}")) && c != '_')
{
if execution_result.status_code != Some(1) {
return Err("expected status code 1");
} else if !re_invalid_project_name.is_match(&execution_result.stderr) {
return Err("stderr not match to the error pattern");
} else if !execution_result.stdout.is_empty() {
return Err("stdout should be empty");
}
Ok(false)
} else if re_folder_exists.is_match(&execution_result.stderr) {
if execution_result.status_code != Some(1) {
return Err("when folder exists expected to get exit code 1");
}
Ok(false)
} else if execution_result.status_code == Some(0) {
if re_successfully.is_match(&execution_result.stderr) {
Ok(true)
} else {
return Err("command success with unexpected stderr");
}
} else {
Err("error not handled")
}
}

fn run_check(&self) -> Option<String> {
if self.run_check {
Some(format!(
"cd {} && cargo check",
self.location.join(&self.project_name).display()
))
} else {
None
}
}

fn run_test(&self) -> Option<String> {
if self.run_check {
Some(format!(
"cd {} && cargo test",
self.location.join(&self.project_name).display()
))
} else {
None
}
}
fn to_yaml(&self) -> serde_yaml::Value {
serde_yaml::to_value(self).expect("to yaml")
}
}

pub fn run(randomizer: Randomizer, temp_dir: &Path) -> crazy_train::Runner {
let step = GenerateProjectStep::new(&randomizer, temp_dir, None, true, true);
crazy_train::new(vec![Box::new(step)]).randomizer(randomizer)
}
2 changes: 2 additions & 0 deletions xtask/src/fuzzy_steps/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod generate_project;
pub mod scaffold;
126 changes: 126 additions & 0 deletions xtask/src/fuzzy_steps/scaffold.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use super::generate_project::GenerateProjectStep;
use crazy_train::{executer, step, Randomizer, Result, StringDef};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

const SCAFFOLD_MAPPING: &str = include_str!("../../../src/gen/mappings.json");

#[derive(Serialize, Deserialize, Debug)]
struct FieldType {
name: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct Mappings {
field_types: Vec<FieldType>,
}

#[derive(Serialize, Deserialize, Debug)]
struct ScaffoldStep {
pub template_location: PathBuf,
pub fields: Vec<String>,
pub rand_table_name: bool,
pub rand_fields_name: bool,
}

impl ScaffoldStep {
fn new(template_location: &Path, rand_table_name: bool, rand_fields_name: bool) -> Self {
let field_mapping: Mappings = serde_yaml::from_str(SCAFFOLD_MAPPING).expect("mapping");

let fields = field_mapping
.field_types
.iter()
.map(|t| t.name.clone())
.collect::<Vec<_>>();

Self {
template_location: template_location.to_path_buf(),
fields,
rand_fields_name,
rand_table_name,
}
}
}

impl step::StepTrait for ScaffoldStep {
fn plan(&self, randomizer: &Randomizer) -> Result<step::Plan> {
let table_name = if self.rand_table_name {
randomizer.string(StringDef::from_randomizer(randomizer))
} else {
randomizer.string(StringDef::default())
}
.to_string();

let shuffled_fields = randomizer.shuffle(&self.fields);
let random_fields = randomizer.pick_random(&shuffled_fields);

let fields = random_fields
.iter()
.map(|kind| {
format!(
"'{}:{kind}'",
if self.rand_fields_name {
randomizer.string(StringDef::from_randomizer(randomizer))
} else {
randomizer.string(StringDef::default())
}
)
})
.collect::<Vec<_>>();

let command = format!(
"cd {} && cargo loco generate scaffold {} {} --api",
self.template_location.display(),
table_name,
fields.join(" ")
);
Ok(step::Plan {
id: std::any::type_name::<Self>().to_string(),
command,
})
}

fn is_success(
&self,
execution_result: &executer::Output,
) -> std::result::Result<bool, &'static str> {
if execution_result.status_code == Some(1) {
Ok(false)
} else {
Ok(true)
}
}

fn run_check(&self) -> Option<String> {
Some(format!(
"cd {} && cargo check",
self.template_location.display()
))
}

fn run_test(&self) -> Option<String> {
Some(format!(
"cd {} && cargo test",
self.template_location.display()
))
}

fn to_yaml(&self) -> serde_yaml::Value {
serde_yaml::to_value(self).expect("to yaml")
}
}

pub fn run(randomizer: Randomizer, temp_dir: &Path) -> crazy_train::Runner {
let template_step =
GenerateProjectStep::new(&randomizer, temp_dir, Some("test_scaffold"), false, false);
let scaffold_step = ScaffoldStep::new(
template_step
.location
.join(&template_step.project_name)
.as_path(),
true,
true,
);

crazy_train::new(vec![Box::new(template_step), Box::new(scaffold_step)]).randomizer(randomizer)
}
1 change: 1 addition & 0 deletions xtask/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::process::exit;
pub mod bump_version;
pub mod ci;
pub mod errors;
pub mod fuzzy_steps;
pub mod out;
pub mod prompt;
pub mod utils;
Expand Down
Loading