use std::env::set_current_dir; use std::fs; use std::io::{BufRead, BufReader}; use std::path::Path; use std::process::{Command, Stdio}; use std::thread::sleep; use std::time::Duration; use rand::prelude::*; trait ExitStatusExt { fn check(self); } impl ExitStatusExt for std::process::ExitStatus { fn check(self) { assert!(self.success()); } } const CARGO: &str = env!("CARGO"); const PROJECT_DIR: &str = env!("CARGO_MANIFEST_DIR"); const TOSIN_ADMIN: &str = env!("CARGO_BIN_EXE_tosin-admin"); fn get(url: &str) -> (hyper::StatusCode, String) { let get = async { use hyper::Client; let client = Client::new(); let res = client.get(url.parse().unwrap()).await.unwrap(); let status = res.status(); let body = hyper::body::to_bytes(res).await.unwrap(); let body = String::from_utf8_lossy(&body).into_owned(); (status, body) }; tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(get) } pub fn step1() { // tosin-admin start-project {dest} set_current_dir(PROJECT_DIR).unwrap(); set_current_dir("target").unwrap(); if fs::metadata("tests").is_err() { fs::create_dir("tests").unwrap(); } set_current_dir("tests").unwrap(); fs::write("Cargo.toml", "[workspace]\nmembers = ['tutorial']").unwrap(); if fs::metadata("tutorial").is_ok() { fs::remove_dir_all("tutorial").unwrap(); } Command::new(TOSIN_ADMIN) .args(&["start-project", "tutorial"]) .status() .unwrap() .check(); set_current_dir("tutorial").unwrap(); assert!(fs::metadata("Cargo.toml").is_ok()); assert!(fs::read_to_string("Cargo.toml") .unwrap() .contains("tosin = ")); assert!(fs::metadata("src/main.rs").is_ok()); assert!(fs::read_to_string("src/main.rs") .unwrap() .contains("tosin::main!")); // cargo run run-server let port = thread_rng().gen_range(8081u16..9000u16); let mut server = Command::new(CARGO) .args(&["run", "run-server", &format!("{}", port)]) .stdout(Stdio::piped()) .spawn() .unwrap(); let mut server_output = String::new(); let server_stdout = server.stdout.take().unwrap(); let mut server_stdout = BufReader::new(server_stdout); server_stdout.read_line(&mut server_output).unwrap(); assert!(server_output.contains(&format!("http://127.0.0.1:{}", port))); sleep(Duration::from_secs_f32(0.5)); let server_poke = get(&format!("http://127.0.0.1:{}/", port)); assert_eq!(server_poke.0, hyper::StatusCode::NOT_FOUND); if let Ok(Some(exit_status)) = server.try_wait() { exit_status.check(); } server.kill().unwrap(); // tosin-admin start-app polls Command::new(TOSIN_ADMIN) .args(&["start-app", "polls"]) .status() .unwrap() .check(); assert!(fs::metadata("src/polls/mod.rs").is_ok()); // write views.rs fs::write( "src/polls/views.rs", r#" use tosin::http::{Reply, Response}; pub fn index() -> Response { "Hello, world. You're at the polls index.".into_response() } "#, ) .unwrap(); // write urls.rs fs::write( "src/polls/urls.rs", r#" use tosin::urls::{UrlMap, url_map}; use super::views; pub fn urls() -> UrlMap { url_map! { => views::index, // TODO name: "index" } } "#, ) .unwrap(); // update main.rs fs::write( "src/main.rs", r#" #[macro_use] extern crate diesel; use tosin::Settings; use tosin::contrib::admin; use tosin::db::backend::Connectable; use tosin::urls::{UrlMap, url_map}; mod polls; fn urls() -> UrlMap { url_map! { "polls" / ..polls::urls(), "admin" / ..admin::site::urls(), } } fn settings() -> Settings { Settings { ..Settings::default() } } tosin::main!(urls(), settings()); "#, ) .unwrap(); // poke that new route let port = thread_rng().gen_range(8081u16..9000u16); let mut server = Command::new(CARGO) .args(&["run", "run-server", &format!("{}", port)]) .stdout(Stdio::piped()) .spawn() .unwrap(); let mut server_output = String::new(); let server_stdout = server.stdout.take().unwrap(); let mut server_stdout = BufReader::new(server_stdout); server_stdout.read_line(&mut server_output).unwrap(); assert!(server_output.contains(&format!("http://127.0.0.1:{}", port))); sleep(Duration::from_secs_f32(0.5)); let server_poke = get(&format!("http://127.0.0.1:{}/polls/", port)); assert_eq!(server_poke.0, hyper::StatusCode::OK); assert_eq!(server_poke.1, "Hello, world. You're at the polls index."); server.kill().unwrap(); // vibe check let test_tutorial1 = Path::new("src"); let example_tutorial1 = Path::new(PROJECT_DIR).join("examples/tutorial01"); for file in &[ "main.rs", "polls/mod.rs", "polls/migrations/mod.rs", "polls/models.rs", "polls/urls.rs", "polls/views.rs", ] { let test_path = test_tutorial1.join(file); let test_file = fs::read_to_string(test_path).unwrap(); let example_path = example_tutorial1.join(file); let example_file = fs::read_to_string(example_path).unwrap(); assert_eq!(test_file.trim(), example_file.trim()); } } pub fn step2() { step1(); // update main.rs fs::write( "src/main.rs", r#" #[macro_use] extern crate diesel; use tosin::Settings; use tosin::contrib::admin; use tosin::db::backend::Connectable; use tosin::urls::{UrlMap, url_map}; mod polls; fn urls() -> UrlMap { url_map! { "polls" / ..polls::urls(), "admin" / ..admin::site::urls(), } } fn settings() -> Settings { Settings { installed_apps: &[ &admin::APP, ], ..Settings::default() } } tosin::main!(urls(), settings()); "#, ) .unwrap(); // cargo run migrate Command::new(CARGO) .args(&["run", "migrate"]) .status() .unwrap() .check(); // update Cargo.toml fs::write( "Cargo.toml", r#" [package] name = "tutorial" version = "0.1.0" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] chrono = "0.4" diesel = { version = "1.4.4", features = ["sqlite"] } tosin = { path = "../../.." } "#, ) .unwrap(); // update polls/models.rs fs::write( "src/polls/models.rs", r#" use tosin::db::models::{Model, Id, gather}; #[derive(Model)] pub struct Question { id: Option, #[model(max_length=200)] question_text: String, /// date published pub_date: chrono::NaiveDateTime, } #[derive(Model)] pub struct Choice { id: Option, #[model(Question, on_delete=Cascade)] question: Id, #[model(max_length=200)] choice_text: String, #[model(default = 0)] votes: usize, } gather!(); "#, ) .unwrap(); // update main.rs fs::write( "src/main.rs", r#" #[macro_use] extern crate diesel; use tosin::Settings; use tosin::contrib::admin; use tosin::db::backend::Connectable; use tosin::urls::{UrlMap, url_map}; mod polls; fn urls() -> UrlMap { url_map! { "polls" / ..polls::urls(), "admin" / ..admin::site::urls(), } } fn settings() -> Settings { Settings { installed_apps: &[ &polls::APP, &admin::APP, ], ..Settings::default() } } tosin::main!(urls(), settings()); "#, ) .unwrap(); // cargo run make-migrations Command::new(CARGO) .args(&["run", "make-migrations"]) .status() .unwrap() .check(); // cargo run migrate Command::new(CARGO) .args(&["run", "migrate"]) .status() .unwrap() .check(); }