Compare commits

...

12 commits

12 changed files with 174 additions and 64 deletions

6
Cargo.lock generated
View file

@ -138,7 +138,7 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "epitls-pi"
version = "1.3.1"
version = "1.5.2"
dependencies = [
"chrono",
"clap",
@ -475,9 +475,9 @@ dependencies = [
[[package]]
name = "termion"
version = "1.5.6"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90"
dependencies = [
"libc",
"numtoa",

View file

@ -1,6 +1,6 @@
[package]
name = "epitls-pi"
version = "1.3.1"
version = "1.5.2"
edition = "2021"
license = "GPL-3.0+"
description = "A little helper tool meant to ease the developpment of the C piscine at EPITA/Toulouse."
@ -22,7 +22,7 @@ notify = "5.0"
notify-debouncer-mini = "0.2"
ron = "0.8"
serde = { version = "1.0", features = ["derive"] }
termion = "1.5"
termion = "2.0"
[profile.release]
lto = true

View file

@ -2,7 +2,7 @@
## Description
A little helper tool meant to ease the developpment of the C piscine at
A little helper tool meant to ease the developpment of the C homeworks at
EPITA/Toulouse.
## Usage
@ -19,7 +19,7 @@ OPTIONS:
SUBCOMMANDS:
check Checks a source file for conformance with piscine limitations
help Print this message or the help of the given subcommand(s)
init
init Initializes a project directory configuration, useful for custom flags, includes and custop push messages
run Runs a set of files or the default target
test Runs tests contained within a particular test file or the default test file
watch Watches changes to the project included files and runs a command on changes
@ -33,5 +33,8 @@ SUBCOMMANDS:
## TODO
- [ ] add support and switch on run/test for strict mode.
- [ ] add `--` syntax to run subcommand for parameter piping.
- [x] add `-p` flag to run subcommand with parameter piping.
- [ ] flag on push to add automatically
- [ ] prevent double includes.
- [ ] flag on init to copy personnal environment
- [ ] gc subcommand to free cache

View file

@ -7,9 +7,7 @@ use crate::{
utils::{log_failure, log_process, log_success},
};
/// TODO: fill with appropriate rules
const FORMAT_CONFIG: &str = r#"{BasedOnStyle: llvm}"#;
mod formatting;
mod testables;
pub fn main(files: Vec<String>) {
@ -35,7 +33,7 @@ pub enum Diff {
fn check_formatting(file: String) {
let content = fs::read_to_string(&file).unwrap();
let formatted = FormatTask::new(file.clone(), FORMAT_CONFIG.into()).run();
let formatted = FormatTask::new(file.clone(), formatting::formatted_config()).run();
let mut line_number = 0usize;
let mut invalid = false;
let differences = diff::lines(&content, &formatted)
@ -94,7 +92,7 @@ fn check_formatting(file: String) {
pub fn format(files: Vec<String>) {
for file in files {
let mut formatted = FormatTask::new(file.clone(), FORMAT_CONFIG.into()).run();
let mut formatted = FormatTask::new(file.clone(), formatting::formatted_config()).run();
if !formatted.ends_with('\n') {
formatted += "\n";
}

17
src/check/formatting.rs Normal file
View file

@ -0,0 +1,17 @@
pub type StaticConf = [(&'static str, &'static str)];
/// TODO: fill with appropriate rules
pub const FORMAT_CONFIG: &StaticConf = &[
// (key, value)
("BasedOnStyle", "GNU"),
("IndentWidth", "4"),
];
pub fn formatted_config() -> String {
let middle = FORMAT_CONFIG
.iter()
.map(|(key, value)| format!("{key}: {value}"))
.collect::<Vec<_>>()
.join(", ");
format!("{{ {middle} }}")
}

View file

@ -1,18 +1,12 @@
fn ends_with_newline(source: String) -> Result<(), String> {
if !source.ends_with('\n') {
Err("source does not end with newline".into())
} else {
Ok(())
}
}
pub type RuleResult = Result<(), String>;
pub struct Rule {
name: String,
test: Box<dyn Fn(String) -> Result<(), String>>,
test: Box<dyn Fn(String) -> RuleResult>,
}
impl Rule {
pub fn new(name: impl ToString, test: impl 'static + Fn(String) -> Result<(), String>) -> Self {
pub fn new(name: impl ToString, test: impl 'static + Fn(String) -> RuleResult) -> Self {
let name = name.to_string();
let test = Box::new(test);
Self { name, test }
@ -22,15 +16,32 @@ impl Rule {
&self.name
}
pub fn test(&self, source: String) -> Result<(), String> {
pub fn test(&self, source: String) -> RuleResult {
(self.test)(source)
}
}
fn ends_with_newline(source: String) -> RuleResult {
if !source.ends_with('\n') {
Err("source does not end with newline".into())
} else {
Ok(())
}
}
fn function_under_50l(source: String) -> RuleResult {
for character in source.chars() {
let _c: char = character;
}
Ok(())
}
/// TODO: fill with appropriate rules
pub fn tests() -> Vec<Rule> {
vec![
// rules
Rule::new("ends_with_newline", ends_with_newline),
Rule::new("function_under_50l", function_under_50l),
]
}

View file

@ -63,6 +63,7 @@ impl Config {
Self::try_get(&path).or_else(|| path.parent().and_then(Self::get))
}
// get path of the current config file
pub fn get_path(path: &Path) -> Option<PathBuf> {
let path = path.to_path_buf().canonicalize().unwrap();
Self::try_get_path(&path).or_else(|| path.parent().and_then(Self::get_path))
@ -72,16 +73,38 @@ impl Config {
&self.identifier
}
pub fn main_file(&self) -> &str {
&self.main_file
pub fn main_file(&self) -> String {
Self::try_absolute(self.main_file.clone())
}
pub fn test_file(&self) -> &str {
&self.test_file
pub fn test_file(&self) -> String {
Self::try_absolute(self.test_file.clone())
}
pub fn includes(&self) -> &Vec<String> {
&self.includes
fn try_absolute(path: String) -> String {
if let Some(conf_path) = Self::get_local_path() {
let dir_path = conf_path.parent().unwrap();
dir_path.join(path).to_str().unwrap().to_string()
} else {
path
}
}
pub fn includes(&self) -> Vec<String> {
self.includes
.iter()
.map(|p| Self::try_absolute(p.clone()))
.flat_map(|p| {
if p.contains('*') {
glob::glob(&p)
.unwrap()
.map(|p| p.unwrap().to_str().unwrap().to_string())
.collect()
} else {
vec![p]
}
})
.collect()
}
pub fn strict_mode(&self) -> bool {

View file

@ -1,5 +1,3 @@
use std::env;
use clap::{Parser, Subcommand};
use config::Config;
@ -35,8 +33,13 @@ pub enum Commands {
/// Runs a set of files or the default target.
run {
// disables fsanitize
#[clap(short, long)]
disable_sanitize: bool,
/// Files to run.
files: Vec<String>,
#[clap(short, long)]
pass: Vec<String>,
},
/// Runs tests contained within a particular test file or the default test file.
@ -62,10 +65,10 @@ pub enum Commands {
/// Initializes a project directory configuration, useful for custom flags, includes and custop push messages.
init {
/// Identifier for the automated tests.
prefix: String,
/// Path to the folder containing the project.
path: Option<String>,
path: String,
/// Identifier for the automated tests.
prefix: Option<String>,
/// e
#[clap(short, long)]
tests: bool,
@ -84,15 +87,17 @@ fn append_includes(list: &mut Vec<String>) {
);
}
fn compilation_args() -> Vec<String> {
fn compilation_args(sanitize: bool) -> Vec<String> {
let mut args = vec![
"-Wall".to_string(),
"-Wextra".to_string(),
"-std=c99".to_string(),
"-g".to_string(),
"-fsanitize=address".to_string(),
// "-pedantic".to_string(),
"-pedantic".to_string(),
];
if sanitize {
args.push("-fsanitize=address".to_string())
}
if Config::get_local_or_default().strict_mode() {
args.push("-Werror".to_string());
}
@ -107,13 +112,17 @@ fn main() {
Commands::format { files } => check::format(files),
Commands::run { mut files } => {
Commands::run {
disable_sanitize,
mut files,
pass,
} => {
if files.is_empty() {
files.push(Config::get_local_or_default().main_file().to_string());
files.push(Config::get_local_or_default().main_file());
}
append_includes(&mut files);
let args = compilation_args();
run::main(files, args);
let args = compilation_args(!disable_sanitize);
run::main(files, args, pass);
}
Commands::test {
@ -122,11 +131,12 @@ fn main() {
// tests,
} => {
if files.is_empty() {
files.push(Config::get_local_or_default().test_file().to_string());
files.push(Config::get_local_or_default().test_file());
}
append_includes(&mut files);
let args = compilation_args();
test::main(capture, files, args)
let mut includes = vec![];
append_includes(&mut includes);
let args = compilation_args(true);
test::main(capture, files, includes, args)
}
Commands::watch { command, files } => {
let mut files = files.unwrap_or_default();
@ -139,8 +149,7 @@ fn main() {
prefix,
tests,
} => {
let path =
path.unwrap_or_else(|| env::current_dir().unwrap().to_str().unwrap().to_string());
let prefix = prefix.unwrap_or_else(|| ".".to_string());
let prefix = prefix.trim().trim_end_matches('*');
config::create(path.clone(), prefix.to_string());
if tests {

View file

@ -3,7 +3,7 @@ use crate::{
utils::{log_failure, log_process},
};
pub fn main(files: Vec<String>, flags: Vec<String>) -> Option<()> {
pub fn main(files: Vec<String>, flags: Vec<String>, passed: Vec<String>) -> Option<()> {
let source_file = files.into_iter().map(|f| f.into()).collect();
log_process("compiling");
let mut task = CompileTask::new(source_file);
@ -19,6 +19,7 @@ pub fn main(files: Vec<String>, flags: Vec<String>) -> Option<()> {
log_process("running");
RunTask::new(compiled)
.with_args(passed)
.run()
.map(Option::from)
.unwrap_or_else(|_| {

View file

@ -1,11 +1,14 @@
use std::{
fs,
path::PathBuf,
process::{Command, ExitStatus, Stdio},
process::{exit, Command, ExitStatus, Stdio},
};
use termion::color;
use crate::utils::{
log_command_run, log_separator_bottom, log_separator_top, tmp_file_path, Apply,
log_command_run, log_failure, log_separator_bottom, log_separator_top, remove_dupes,
tmp_file_path, Apply,
};
pub struct CompileTask {
@ -41,9 +44,9 @@ impl CompileTask {
}
pub fn run(self) -> Result<PathBuf, ExitStatus> {
let proc_source = self.gen_source();
let mut sources = self.files.clone();
sources.push(proc_source);
// let proc_source = self.gen_source();
let sources = self.files.clone();
// sources.push(proc_source);
self.compile(sources)
}
@ -55,17 +58,27 @@ impl CompileTask {
}
pub fn compile(&self, sources: Vec<PathBuf>) -> Result<PathBuf, ExitStatus> {
let mut sources = sources
.iter()
.map(|s| s.to_str().unwrap().to_string())
.collect();
remove_dupes(&mut sources);
let output_path = tmp_file_path().apply(|o| o.set_extension("b"));
let output_path_ref = output_path.to_str().unwrap();
let mut command = Command::new("gcc");
command
.args(["-o", output_path_ref])
.args(self.flags.clone())
.args(sources.iter().map(|s| s.to_str().unwrap()));
.args(sources);
if self.verbose {
log_command_run(&command);
log_separator_top();
}
println!(
"{}{command:?}{}",
color::Fg(color::AnsiValue(8)),
color::Fg(color::Reset)
);
let status = command.status().unwrap();
if self.verbose {
log_separator_bottom();
@ -77,6 +90,7 @@ impl CompileTask {
pub struct RunTask {
file: PathBuf,
verbose: bool,
args: Vec<String>,
}
impl RunTask {
@ -84,17 +98,23 @@ impl RunTask {
Self {
file,
verbose: false,
args: vec![],
}
}
pub fn with_args(mut self, mut args: Vec<String>) -> Self {
self.args.append(&mut args);
self
}
pub fn with_verbose(mut self) -> Self {
self.verbose = true;
self
}
pub fn run(self) -> Result<(), ExitStatus> {
let mut command = Command::new("sh");
command.args(["-c", self.file.to_str().unwrap()]);
let mut command = Command::new(self.file.to_str().unwrap());
command.args(self.args);
if self.verbose {
log_command_run(&command);
log_separator_top();
@ -146,10 +166,20 @@ impl FormatTask {
.arg(format!("-style={config}"))
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command.status().unwrap();
let result = command.output().unwrap().stdout;
String::from_utf8(result).unwrap()
let status = command.status().unwrap();
let out = command.output().unwrap().stdout;
let out = String::from_utf8(out).unwrap();
let err = command.output().unwrap().stderr;
let err = String::from_utf8(err).unwrap();
if !status.success() {
log_failure("failed formatting");
println!("{out}");
eprintln!("{err}");
exit(1);
}
out
}
}

View file

@ -1,13 +1,14 @@
use std::{fs, thread, time::Duration};
use std::{fs, path::PathBuf, thread, time::Duration};
use crate::{
tasks::{CompileTask, GenTask, RunTask},
utils::{log_failure, log_process, log_success},
};
pub fn main(_capture: bool, files: Vec<String>, args: Vec<String>) {
pub fn main(_capture: bool, test_files: Vec<String>, includes: Vec<String>, args: Vec<String>) {
log_process("testing");
for path in files {
let includes: Vec<_> = includes.into_iter().map(PathBuf::from).collect();
for path in test_files {
let content = fs::read_to_string(&path).unwrap();
let tests = find_tests(content);
for test in tests {
@ -18,7 +19,10 @@ pub fn main(_capture: bool, files: Vec<String>, args: Vec<String>) {
thread::sleep(Duration::from_millis(100));
// compile with all files
let mut task = CompileTask::new(vec![generated_code]);
let mut files = vec![generated_code];
let mut local_includes = includes.clone(); // TODO : filter out current test : already included
files.append(&mut local_includes);
let mut task = CompileTask::new(files);
for flag in args.clone() {
task = task.with_flag(flag);
}
@ -54,6 +58,8 @@ int main(int argc, char** argv) {{
return 0;
}}
#define main __pi_hidden_main
#include \"{path}\"
void __pi_test() {{

View file

@ -1,4 +1,4 @@
use std::{fs, path::PathBuf, process::Command};
use std::{collections::HashSet, fs, mem::swap, path::PathBuf, process::Command};
use chrono::Utc;
use termion::color;
@ -97,3 +97,15 @@ pub fn log_error(input: impl AsRef<str>) {
log_pi_prefix();
println!("error: {input}");
}
pub fn remove_dupes(input: &mut Vec<String>) {
let mut tmp = vec![];
swap(input, &mut tmp);
let mut set = HashSet::new();
for str in tmp {
set.insert(str);
}
for str in set {
input.push(str);
}
}