Skip to content

Commit 430b31f

Browse files
committed
error handling
1 parent 3a6b632 commit 430b31f

File tree

23 files changed

+290
-186
lines changed

23 files changed

+290
-186
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232

3333
windows:
3434
runs-on: windows-latest
35-
steps: # TODO: install valgrind and check?
35+
steps: # TODO: install missing dependencies and run all tests on windows
3636
- uses: actions/checkout@v2
3737
- name: Build test binary
3838
run: cargo test --no-run --verbose
@@ -41,7 +41,7 @@ jobs:
4141

4242
macos:
4343
runs-on: macos-latest
44-
steps: # TODO: install valgrind and check?
44+
steps: # TODO: install missing dependencies and run all tests on macos
4545
- uses: actions/checkout@v2
4646
- name: Build test binary
4747
run: cargo test --no-run --verbose

tmc-langs-core/src/error.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,20 @@ pub enum CoreError {
2727
// network
2828
#[error("HTTP error {1} for {0}: {2}")]
2929
HttpStatus(Url, StatusCode, String),
30-
#[error("OAuth2 password exchange error: {0}")]
31-
Token(Box<TokenError>),
30+
#[error("OAuth2 password exchange error")]
31+
Token(#[source] Box<TokenError>),
3232
#[error("OAuth2 unexpected token response: {0}")]
3333
TokenParse(String, #[source] serde_json::error::Error),
3434
#[error("Failed to parse as URL: {0}")]
3535
UrlParse(String, #[source] url::ParseError),
36+
#[error("Failed to GET {0}")]
37+
HttpGet(Url, #[source] reqwest::Error),
38+
#[error("Failed to POST {0}")]
39+
HttpPost(Url, #[source] reqwest::Error),
40+
#[error("Failed to write response to {0}")]
41+
HttpWriteResponse(PathBuf, #[source] reqwest::Error),
42+
#[error("Failed to deserialize response as JSON")]
43+
HttpJsonResponse(#[source] reqwest::Error),
3644

3745
#[error("Already authenticated")]
3846
AlreadyAuthenticated,
@@ -44,13 +52,9 @@ pub enum CoreError {
4452
#[error(transparent)]
4553
TmcLangs(#[from] tmc_langs_util::Error),
4654
#[error(transparent)]
47-
Request(#[from] reqwest::Error),
48-
#[error(transparent)]
4955
Response(#[from] response::ResponseError),
5056
#[error(transparent)]
5157
ResponseErrors(#[from] response::ResponseErrors),
52-
#[error(transparent)]
53-
SystemTime(#[from] std::time::SystemTimeError),
5458
}
5559

5660
impl From<TokenError> for CoreError {

tmc-langs-core/src/tmc_core/api.rs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ trait CoreExt {
3131
impl CoreExt for ReqwestResponse {
3232
#[cfg(not(test))]
3333
fn json_res<T: DeserializeOwned>(self) -> Result<T> {
34-
let res: Response<T> = self.json()?;
34+
let res: Response<T> = self.json().map_err(|e| CoreError::HttpJsonResponse(e))?;
3535
res.into_result()
3636
}
3737

3838
// logs received JSON for easier debugging in tests
3939
#[cfg(test)]
4040
fn json_res<T: DeserializeOwned>(self) -> Result<T> {
41-
let res: Value = self.json()?;
41+
let res: Value = self.json().map_err(|e| CoreError::HttpJsonResponse(e))?;
4242
log::debug!("JSON {}", res);
4343
let res: Response<T> = serde_json::from_value(res).unwrap();
4444
res.into_result()
@@ -86,7 +86,8 @@ impl TmcCore {
8686
self.client
8787
.get(url.clone())
8888
.authenticate(&self.token)
89-
.send()?
89+
.send()
90+
.map_err(|e| CoreError::HttpGet(url.clone(), e))?
9091
.check_error(url)?
9192
.json_res()
9293
}
@@ -104,9 +105,11 @@ impl TmcCore {
104105
self.client
105106
.get(url.clone())
106107
.authenticate(&self.token)
107-
.send()?
108+
.send()
109+
.map_err(|e| CoreError::HttpGet(url.clone(), e))?
108110
.check_error(url)?
109-
.copy_to(&mut target_file)?;
111+
.copy_to(&mut target_file)
112+
.map_err(|e| CoreError::HttpWriteResponse(target.to_path_buf(), e))?;
110113
Ok(())
111114
}
112115

@@ -118,9 +121,11 @@ impl TmcCore {
118121
self.client
119122
.get(url.clone())
120123
.authenticate(&self.token)
121-
.send()?
124+
.send()
125+
.map_err(|e| CoreError::HttpGet(url.clone(), e))?
122126
.check_error(url)?
123-
.copy_to(&mut target_file)?;
127+
.copy_to(&mut target_file)
128+
.map_err(|e| CoreError::HttpWriteResponse(target.to_path_buf(), e))?;
124129
Ok(())
125130
}
126131

@@ -552,11 +557,19 @@ impl TmcCore {
552557
form = form
553558
.text(
554559
"client_time",
555-
SystemTime::UNIX_EPOCH.elapsed()?.as_secs().to_string(),
560+
SystemTime::UNIX_EPOCH
561+
.elapsed()
562+
.unwrap()
563+
.as_secs()
564+
.to_string(),
556565
)
557566
.text(
558567
"client_nanotime",
559-
SystemTime::UNIX_EPOCH.elapsed()?.as_nanos().to_string(),
568+
SystemTime::UNIX_EPOCH
569+
.elapsed()
570+
.unwrap()
571+
.as_nanos()
572+
.to_string(),
560573
)
561574
.file("submission[file]", submission)
562575
.map_err(|e| CoreError::FileOpen(submission.to_path_buf(), e))?;
@@ -573,7 +586,8 @@ impl TmcCore {
573586
.post(submission_url.clone())
574587
.multipart(form)
575588
.authenticate(&self.token)
576-
.send()?
589+
.send()
590+
.map_err(|e| CoreError::HttpPost(submission_url.clone(), e))?
577591
.check_error(submission_url)?
578592
.json_res()?;
579593
log::debug!("received {:?}", res);
@@ -612,7 +626,8 @@ impl TmcCore {
612626
.post(feedback_url.clone())
613627
.multipart(form)
614628
.authenticate(&self.token)
615-
.send()?
629+
.send()
630+
.map_err(|e| CoreError::HttpPost(feedback_url.clone(), e))?
616631
.check_error(feedback_url)?
617632
.json_res()
618633
}
@@ -636,7 +651,8 @@ impl TmcCore {
636651
.query(&[("review[review_body]", review_body)])
637652
.query(&[("review[points]", review_points)])
638653
.authenticate(&self.token)
639-
.send()?
654+
.send()
655+
.map_err(|e| CoreError::HttpPost(url.clone(), e))?
640656
.check_error(url)?
641657
.json_res()?;
642658
log::trace!("received {:?}", res);
@@ -657,7 +673,8 @@ impl TmcCore {
657673
self.client
658674
.post(url.clone())
659675
.multipart(form)
660-
.send()?
676+
.send()
677+
.map_err(|e| CoreError::HttpPost(url.clone(), e))?
661678
.check_error(url)?
662679
.json_res()
663680
}

tmc-langs-framework/src/domain/meta_syntax.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Contains utilities for parsing text files, separating lines into
22
//! strings, stubs and solutions so that they can be more easily filtered accordingly
33
4-
use crate::Result;
4+
use crate::{Error, Result};
55
use lazy_static::lazy_static;
66
use log::debug;
77
use regex::{Captures, Regex};
@@ -183,7 +183,7 @@ impl<B: BufRead> Iterator for MetaSyntaxParser<B> {
183183
Some(Ok(MetaString::String(s)))
184184
}
185185
}
186-
Err(err) => Some(Err(err.into())),
186+
Err(err) => Some(Err(Error::ReadLine(err))),
187187
}
188188
}
189189
}

tmc-langs-framework/src/error.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::io::zip;
22

33
use std::path::PathBuf;
4+
use std::time::Duration;
45
use thiserror::Error;
56

67
#[derive(Error, Debug)]
@@ -10,15 +11,37 @@ pub enum Error {
1011
OpenFile(PathBuf, #[source] std::io::Error),
1112
#[error("Failed to create file at {0}")]
1213
CreateFile(PathBuf, #[source] std::io::Error),
14+
#[error("Failed to remove file at {0}")]
15+
RemoveFile(PathBuf, #[source] std::io::Error),
1316
#[error("Failed to create dir(s) at {0}")]
1417
CreateDir(PathBuf, #[source] std::io::Error),
18+
#[error("Failed to remove dir at {0}")]
19+
RemoveDir(PathBuf, #[source] std::io::Error),
1520
#[error("Failed to rename {0} to {1}")]
1621
Rename(PathBuf, PathBuf, #[source] std::io::Error),
1722
#[error("Failed to write to {0}")]
1823
Write(PathBuf, #[source] std::io::Error),
24+
#[error("Error appending to tar")]
25+
TarAppend(#[source] std::io::Error),
26+
#[error("Error finishing tar")]
27+
TarFinish(#[source] std::io::Error),
28+
#[error("Failed to read line")]
29+
ReadLine(#[source] std::io::Error),
30+
#[error("Failed to copy file from {0} to {1}")]
31+
FileCopy(PathBuf, PathBuf, #[source] std::io::Error),
32+
#[error("Failed to open file at {0}")]
33+
FileOpen(PathBuf, #[source] std::io::Error),
34+
#[error("Failed to read file at {0}")]
35+
FileRead(PathBuf, #[source] std::io::Error),
36+
#[error("Failed to canonicalize path {0}")]
37+
Canonicalize(PathBuf, #[source] std::io::Error),
38+
#[error("Error occurred in a child process")]
39+
Process(#[source] std::io::Error),
1940

2041
#[error("Path {0} contained invalid UTF8")]
2142
UTF8(PathBuf),
43+
#[error("Path {0} contained no file name")]
44+
NoFileName(PathBuf),
2245

2346
#[error("No matching plugin found for {0}")]
2447
PluginNotFound(PathBuf),
@@ -29,14 +52,12 @@ pub enum Error {
2952

3053
#[error("Failed to spawn command: {0}")]
3154
CommandSpawn(&'static str, #[source] std::io::Error),
32-
#[error("Test timed out")]
33-
TestTimeout,
55+
#[error("Test timed out after {} seconds", .0.as_secs())]
56+
TestTimeout(Duration),
3457

3558
#[error("Error in plugin: {0}")]
3659
Plugin(#[source] Box<dyn std::error::Error + 'static + Send + Sync>),
3760

38-
#[error(transparent)]
39-
Io(#[from] std::io::Error),
4061
#[error(transparent)]
4162
YamlDeserialization(#[from] serde_yaml::Error),
4263
#[error(transparent)]

tmc-langs-framework/src/io/submission_processing.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ fn copy_file<F: Fn(&MetaString) -> bool>(
126126
entry.path(),
127127
dest_path
128128
);
129-
fs::copy(entry.path(), dest_path)?;
129+
fs::copy(entry.path(), &dest_path)
130+
.map_err(|e| Error::FileCopy(entry.path().to_path_buf(), dest_path, e))?;
130131
} else {
131132
// filter text files
132133
debug!(

tmc-langs-framework/src/io/zip.rs

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ pub fn zip(policy: Box<dyn StudentFilePolicy>, root_directory: &Path) -> Result<
3131
log::trace!("adding directory {}", path.display());
3232
writer.add_directory_from_path(path, FileOptions::default())?;
3333
} else {
34-
let file = File::open(entry.path())?;
35-
let bytes = file
36-
.bytes()
37-
.collect::<std::result::Result<Vec<_>, std::io::Error>>()?;
34+
let mut file = File::open(entry.path())
35+
.map_err(|e| Error::FileOpen(entry.path().to_path_buf(), e))?;
36+
let mut bytes = vec![];
37+
file.read_to_end(&mut bytes)
38+
.map_err(|e| Error::FileRead(entry.path().to_path_buf(), e))?;
3839
log::trace!("writing file {}", path.display());
3940
writer.start_file_from_path(path, FileOptions::default())?;
4041
writer
@@ -62,7 +63,7 @@ pub fn unzip<P: StudentFilePolicy>(policy: P, zip: &Path, target: &Path) -> Resu
6263
let mut unzipped_paths = HashSet::new();
6364

6465
for i in 0..zip_archive.len() {
65-
let file = zip_archive.by_index(i)?;
66+
let mut file = zip_archive.by_index(i)?;
6667
let file_path = file.sanitized_name();
6768
if !file_path.starts_with(&project_dir) {
6869
log::trace!("skip {}, not in project dir", file.name());
@@ -76,22 +77,29 @@ pub fn unzip<P: StudentFilePolicy>(policy: P, zip: &Path, target: &Path) -> Resu
7677
log::trace!("creating {:?}", path_in_target);
7778
fs::create_dir_all(&path_in_target)
7879
.map_err(|e| Error::CreateDir(path_in_target.clone(), e))?;
79-
unzipped_paths.insert(path_in_target.canonicalize()?);
80+
unzipped_paths.insert(
81+
path_in_target
82+
.canonicalize()
83+
.map_err(|e| Error::Canonicalize(path_in_target, e))?,
84+
);
8085
} else {
8186
let mut write = true;
82-
let file_contents = file.bytes().collect::<std::result::Result<Vec<_>, _>>()?;
87+
let mut file_contents = vec![];
88+
file.read_to_end(&mut file_contents)
89+
.map_err(|e| Error::FileRead(file_path, e))?;
8390
// always overwrite .tmcproject.yml
8491
if path_in_target.exists()
8592
&& !path_in_target
8693
.file_name()
8794
.map(|o| o == ".tmcproject.yml")
8895
.unwrap_or_default()
8996
{
90-
let target_file = File::open(&path_in_target)
97+
let mut target_file = File::open(&path_in_target)
9198
.map_err(|e| Error::OpenFile(path_in_target.clone(), e))?;
92-
let target_file_contents = target_file
93-
.bytes()
94-
.collect::<std::result::Result<Vec<_>, _>>()?;
99+
let mut target_file_contents = vec![];
100+
target_file
101+
.read_to_end(&mut target_file_contents)
102+
.map_err(|e| Error::FileRead(path_in_target.clone(), e))?;
95103
if file_contents == target_file_contents
96104
|| (policy.is_student_file(&path_in_target, &target, &tmc_project_yml)?
97105
&& !policy.is_updating_forced(&path_in_target, &tmc_project_yml)?)
@@ -101,35 +109,45 @@ pub fn unzip<P: StudentFilePolicy>(policy: P, zip: &Path, target: &Path) -> Resu
101109
}
102110
if write {
103111
log::trace!("writing to {}", path_in_target.display());
104-
if let Some(res) = path_in_target.parent().map(fs::create_dir_all) {
105-
res?;
112+
if let Some(res) = path_in_target.parent() {
113+
fs::create_dir_all(res).map_err(|e| Error::CreateDir(res.to_path_buf(), e))?;
106114
}
107115
let mut overwrite_target = File::create(&path_in_target)
108116
.map_err(|e| Error::CreateFile(path_in_target.clone(), e))?;
109117
overwrite_target
110118
.write_all(&file_contents)
111119
.map_err(|e| Error::Write(path_in_target.clone(), e))?;
112-
unzipped_paths.insert(path_in_target.canonicalize()?);
120+
unzipped_paths.insert(
121+
path_in_target
122+
.canonicalize()
123+
.map_err(|e| Error::Canonicalize(path_in_target, e))?,
124+
);
113125
}
114126
}
115127
}
116128

117129
// delete non-student files that were not in zip
118130
log::debug!("deleting non-student files not in zip");
119131
for entry in WalkDir::new(target).into_iter().filter_map(|e| e.ok()) {
120-
if !unzipped_paths.contains(&entry.path().canonicalize()?)
121-
&& (policy.is_updating_forced(entry.path(), &tmc_project_yml)?
122-
|| !policy.is_student_file(entry.path(), &project_dir, &tmc_project_yml)?)
132+
if !unzipped_paths.contains(
133+
&entry
134+
.path()
135+
.canonicalize()
136+
.map_err(|e| Error::Canonicalize(entry.path().to_path_buf(), e))?,
137+
) && (policy.is_updating_forced(entry.path(), &tmc_project_yml)?
138+
|| !policy.is_student_file(entry.path(), &project_dir, &tmc_project_yml)?)
123139
{
124140
if entry.path().is_dir() {
125141
// delete if empty
126142
if WalkDir::new(entry.path()).max_depth(1).into_iter().count() == 1 {
127143
log::debug!("deleting empty directory {}", entry.path().display());
128-
fs::remove_dir(entry.path())?;
144+
fs::remove_dir(entry.path())
145+
.map_err(|e| Error::RemoveDir(entry.path().to_path_buf(), e))?;
129146
}
130147
} else {
131148
log::debug!("removing file {}", entry.path().display());
132-
fs::remove_file(entry.path())?;
149+
fs::remove_file(entry.path())
150+
.map_err(|e| Error::RemoveFile(entry.path().to_path_buf(), e))?;
133151
}
134152
}
135153
}

0 commit comments

Comments
 (0)