1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
use std::{
    collections::HashMap,
    path::{Path, PathBuf},
    string::FromUtf8Error,
};

use orchestrator::{
    executor::AsyncDefault,
    prelude::{CompilationResult, RunResult, TestResult},
};
use serde_json::Value;
use tempdir::TempDir;
use tokio::{fs, process::Command};

use super::RustExercise;

/// Error that can get generated in a compilation with cargo
#[derive(Debug, thiserror::Error)]
pub enum CompileError {
    /// From UTF8 Error
    #[error("It's not a valid utf-8 file. Got error: {0}")]
    FromUTF8Error(#[from] FromUtf8Error),
    /// Input Output Error
    #[error("IoError {0}")]
    IoError(#[from] std::io::Error),
}

#[derive(Clone)]
/// Rust Generated Files
pub struct RustGeneratedFiles {
    /// each files is saved in this hashmap where:
    /// the key is the name
    /// the parameter String is the source code generated
    /// the f64 is how much points that test is worth
    pub files: HashMap<String, (String, f64)>,
}
impl AsyncDefault for RustGeneratedFiles {
    async fn async_default() -> Self {
        RustExercise::default()
            .generate_files("fn nothing(){}".to_string())
            .await
            .unwrap()
    }
}
/**
    it extracts the various error from the output
*/
pub fn parse_errors(inp: &str, tests: &mut HashMap<String, TestResult>) {
    let _: Vec<Option<()>> = inp
        .lines()
        .map(|t| -> Option<()> {
            let error = serde_json::from_str::<Value>(t).ok()?;
            let error = error.as_object()?;
            let message = error.get("message")?.as_object()?;
            let level = message.get("level")?;
            let rendered = message.get("rendered")?.as_str()?.to_string();
            let rendered = rendered.replace("\\n", &String::from_utf8(b"\n".to_vec()).unwrap());

            let target = error.get("target")?.as_object()?;
            let name = target.get("name")?.as_str()?;
            if level != "error" {
                return None;
            }
            if let Some(test_result) = tests.get_mut(name) {
                if let CompilationResult::Error(msg) = &mut test_result.compiled {
                    msg.push('\n');
                    *msg += &rendered;
                } else {
                    test_result.points_given = 0.0;
                    test_result.compiled = CompilationResult::Error(rendered)
                }
            }
            Some(())
        })
        .collect();
}

/// function used to create a valid Cargo Project
pub async fn create_cargo_project(path: &Path) -> Result<(), CompileError> {
    //TODO clean well (delete target, overwrite other files)
    if path.exists() {
        fs::remove_dir_all(path).await?;
    }

    fs::create_dir(path).await?;
    let toml = include_str!("./default_cargo.toml");
    fs::write(path.join("Cargo.toml"), toml).await?;
    fs::create_dir(path.join("src")).await?;
    fs::create_dir(path.join("src/bin")).await?;
    Ok(())
}

impl RustGeneratedFiles {
    /// Compiles each files creating a project in a temporary directory, or in path if specified
    pub async fn compile(self, path: Option<PathBuf>) -> Result<RustCompiled, CompileError> {
        let (tmpdir, path) = if let Some(path) = path {
            (None, path)
        } else {
            let tmp_dir = TempDir::new("tmp_compile")?;
            let path = tmp_dir.path().to_owned();
            (Some(tmp_dir), path)
        };

        //generate crate
        create_cargo_project(&path).await?;

        for (name, (content, _)) in &self.files {
            fs::write(
                path.join("src").join("bin").join(name.clone() + ".rs"),
                content,
            )
            .await?;
        }
        let compilation_output = Command::new("cargo")
            .arg("+nightly")
            .arg("build")
            .arg("--bins")
            .arg("--manifest-path")
            .arg(path.join("Cargo.toml"))
            .arg("--keep-going")
            .arg("--message-format=json")
            .output()
            .await?;
        let message = String::from_utf8(compilation_output.stdout)?;
        let mut results: HashMap<String, TestResult> = self
            .files
            .into_iter()
            .map(|(name, (_, points))| {
                let test_result = TestResult {
                    compiled: CompilationResult::Built,
                    runned: RunResult::NotRun,
                    points_given: points,
                };
                (name, test_result)
            })
            .collect();

        //println!("{} {}", message, String::from_utf8(compilation_output.stderr)?);
        parse_errors(&message, &mut results);
        Ok(RustCompiled {
            _tmpdir: tmpdir,
            path,
            results,
        })
    }
}

/// Result of a rust compilation, it contains the path to be used.
pub struct RustCompiled {
    /// Temporary directory
    _tmpdir: Option<TempDir>,
    /// path where the project is stored
    pub path: PathBuf,
    /// results of the compilation
    pub results: HashMap<String, TestResult>,
}

impl AsyncDefault for RustCompiled {
    async fn async_default() -> Self {
        RustGeneratedFiles::async_default()
            .await
            .compile(None)
            .await
            .unwrap()
    }
}
impl Clone for RustCompiled {
    fn clone(&self) -> Self {
        let _tmpdir = match &self._tmpdir {
            Some(dir) => {
                let new_dir = TempDir::new("tmp_compile").unwrap();

                copy_dir::copy_dir(dir.path(), new_dir.path().join("*")).unwrap();
                Some(new_dir)
            }
            None => None,
        };
        Self {
            _tmpdir,
            path: self.path.clone(),
            results: self.results.clone(),
        }
    }
}