diff --git a/src/commands/test.rs b/src/commands/test.rs index c14292a..e9df52b 100644 --- a/src/commands/test.rs +++ b/src/commands/test.rs @@ -7,13 +7,12 @@ use crate::{ }, error::{self, Result}, evaluate::evaluate, - file_utils::with_locked_file, ipynb::{convert_submission_notebooks, convert_use_case_notebooks}, print::wrap_python_output, python::LastRunResult, - readme::get_readme_path, + readme::{read_readme, write_readme}, }; -use aqora_config::{AqoraUseCaseConfig, PyProject}; +use aqora_config::{AqoraUseCaseConfig, PyProject, ReadMe}; use aqora_runner::{ pipeline::{EvaluateAllInfo, EvaluateInputInfo, EvaluationError, Pipeline, PipelineConfig}, python::PyEnv, @@ -28,12 +27,11 @@ use pyo3::{exceptions::PyException, Python}; use serde::Serialize; use std::{ collections::HashMap, - io::SeekFrom, path::{Path, PathBuf}, pin::Pin, sync::{atomic::AtomicU32, Arc}, }; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; + use url::Url; #[derive(Args, Debug, Clone, Serialize)] @@ -230,41 +228,49 @@ fn run_pipeline( ) } -async fn update_score_badge_in_file( - path: PathBuf, +async fn update_score_badge_in_readme( competition: Option, + project_dir: impl AsRef, + readme: Option<&ReadMe>, score: &Py, url: Url, ) -> Result<()> { let badge_url = build_shield_score_badge( score, - url.join(&competition.map_or_else( - || "competitons".to_string(), - |name| format!("competitions/{}", name), - ))?, + url.join(&competition.unwrap_or_else(|| "competitions".to_string()))?, )?; - with_locked_file( - |file| { - async move { - let mut contents = String::new(); - file.read_to_string(&mut contents).await?; - let updated_content = regex_replace_all!( - r"(?:.*?)|(!\[Aqora Score Badge\]\([^\)]+\))", - &contents, - badge_url.clone() - ); - if updated_content != contents { - file.seek(SeekFrom::Start(0)).await?; - file.write_all(updated_content.as_bytes()).await?; - file.set_len(updated_content.len() as u64).await?; - } - Ok(()) - } - .boxed() - }, - path, - ) - .await + + if let Some(content) = read_readme(&project_dir, readme) + .await + .inspect_err(|e| { + tracing::warn!( + "Failed to read the README file: {}. Skipping badge update.", + e + ) + }) + .ok() + .flatten() + { + let updated_content = regex_replace_all!( + r"(?:.*?)|(!\[Aqora Score Badge\]\([^\)]+\))", + &content, + badge_url + ); + + if updated_content != content { + write_readme(&project_dir, &updated_content) + .await + .inspect_err(|e| { + tracing::warn!( + "Failed to write to the README file: {}. Skipping badge update.", + e + ) + }) + .ok(); + } + } + + Ok(()) } pub async fn run_submission_tests( @@ -278,17 +284,6 @@ pub async fn run_submission_tests( .and_then(|aqora| aqora.as_submission()) .ok_or_else(|| error::user("Submission config is not valid", ""))?; let project_config = read_project_config(&global.project).await?; - let project_readme_path = get_readme_path( - &global.project, - project.project.as_ref().and_then(|p| p.readme.as_ref()), - ) - .await - .map_err(|err| { - error::user( - &format!("Could not read readme: {}", err), - "Please make sure the readme is valid", - ) - })?; let use_case_toml_path = project_use_case_toml_path(&global.project); let data_path = project_data_dir(&global.project); if !use_case_toml_path.exists() || !data_path.exists() { @@ -423,15 +418,18 @@ pub async fn run_submission_tests( "Score".if_supports_color(OwoStream::Stdout, |text| { text.bold() }), score )); - if let Some(path) = project_readme_path { - update_score_badge_in_file( - global.project.join(path), - modified_use_case.competition, - &score, - global.aqora_url()?, - ) - .await?; - }; + + update_score_badge_in_readme( + modified_use_case.competition, + &global.project, + project.project.as_ref().and_then(|p| p.readme.as_ref()), + &score, + global.aqora_url()?, + ) + .await + .inspect_err(|_| tracing::warn!("Failed to add Aqora badge to README.")) + .ok(); + pipeline_pb.finish_and_clear(); Ok(score) } diff --git a/src/readme.rs b/src/readme.rs index cc24e08..ddea681 100644 --- a/src/readme.rs +++ b/src/readme.rs @@ -2,56 +2,79 @@ use aqora_config::ReadMe; use mime::Mime; use std::path::{Path, PathBuf}; use thiserror::Error; +use tokio::fs; #[derive(Debug, Error)] -pub enum ReadmeError { +pub enum ReadMeError { #[error(transparent)] Io(#[from] std::io::Error), - #[error("README not found")] + #[error("Readme not found")] NotFound, - #[error("README content type not supported. Only markdown and plaintext supported")] + #[error("Readme content type not supported. Only markdown and plaintext supported")] ContentTypeNotSupported, } -pub async fn get_readme_path( - project_dir: impl AsRef, +fn is_supported_mime(content_type: &str) -> Result<(), ReadMeError> { + let mime: Mime = content_type + .parse() + .map_err(|_| ReadMeError::ContentTypeNotSupported)?; + if mime.type_() == mime::TEXT && (mime.subtype() == mime::PLAIN || mime.subtype() == "markdown") + { + Ok(()) + } else { + Err(ReadMeError::ContentTypeNotSupported) + } +} + +fn is_supported_extension(extension: Option<&str>) -> bool { + match extension { + Some(ext) => { + let ext = ext.to_lowercase(); + ext == "md" || ext == "txt" + } + None => true, + } +} + +async fn find_readme_path( + project_dir: &Path, readme: Option<&ReadMe>, -) -> Result, ReadmeError> { - let path = match readme { - Some(ReadMe::Table { - ref file, - text: _, - content_type, - }) => { - let path: Option<&Path> = file.as_deref().map(str::as_ref); - if let Some(content_type) = content_type { - let mime: Mime = content_type - .parse() - .map_err(|_| ReadmeError::ContentTypeNotSupported)?; - if !(mime.type_() == mime::TEXT - && (mime.subtype() == mime::PLAIN || mime.subtype() == "markdown")) - { - return Err(ReadmeError::ContentTypeNotSupported); +) -> Result, ReadMeError> { + if let Some(readme) = readme { + match readme { + ReadMe::Table { + file, + text: _, + content_type, + } => { + if let Some(content_type) = content_type { + is_supported_mime(content_type)?; + } + if let Some(file) = file { + let path = project_dir.join(file); + if is_supported_extension(path.extension().and_then(|s| s.to_str())) { + return Ok(Some(path)); + } else { + return Err(ReadMeError::ContentTypeNotSupported); + } + } + } + ReadMe::RelativePath(path) => { + let path = project_dir.join(path); + if is_supported_extension(path.extension().and_then(|s| s.to_str())) { + return Ok(Some(path)); + } else { + return Err(ReadMeError::ContentTypeNotSupported); } } - path.map(|p| p.to_path_buf()) } - Some(ReadMe::RelativePath(ref path)) => Some(PathBuf::from(path)), - None => None, - }; - - if let Some(path) = path { - return Ok(Some(path)); } - let mut dir = tokio::fs::read_dir(&project_dir).await?; + let mut dir = fs::read_dir(project_dir).await?; while let Some(entry) = dir.next_entry().await? { - match entry.file_name().to_string_lossy().to_lowercase().as_str() { - "readme.md" | "readme.txt" => {} - _ => continue, - } - let metadata = entry.metadata().await?; - if metadata.is_file() { + let file_name = entry.file_name().to_string_lossy().to_lowercase(); + let readme_files = ["readme.md", "readme.txt", "readme"]; + if readme_files.contains(&file_name.as_str()) && entry.metadata().await?.is_file() { return Ok(Some(entry.path())); } } @@ -62,24 +85,36 @@ pub async fn get_readme_path( pub async fn read_readme( project_dir: impl AsRef, readme: Option<&ReadMe>, -) -> Result, ReadmeError> { - let path = match get_readme_path(project_dir, readme).await? { - Some(path) => path, - None => return Ok(None), - }; +) -> Result, ReadMeError> { + let project_dir = project_dir.as_ref(); - match path - .extension() - .map(|ext| ext.to_string_lossy().to_lowercase()) - .as_deref() + if let Some(ReadMe::Table { + text: Some(text), .. + }) = readme { - Some("md") | Some("txt") | None => {} - _ => return Err(ReadmeError::ContentTypeNotSupported), + return Ok(Some(text.clone())); } - if !tokio::fs::try_exists(&path).await? { - return Err(ReadmeError::NotFound); + let path = find_readme_path(project_dir, readme).await?; + + if let Some(path) = path { + if !fs::try_exists(&path).await? { + return Err(ReadMeError::NotFound); + } + let content = fs::read_to_string(path).await?; + Ok(Some(content)) + } else { + Ok(None) } +} - Ok(Some(tokio::fs::read_to_string(path).await?)) +pub async fn write_readme(project_dir: impl AsRef, content: &str) -> Result<(), ReadMeError> { + let project_dir = project_dir.as_ref(); + let existing_readme_path = find_readme_path(project_dir, None).await?; + if let Some(existing_path) = existing_readme_path { + fs::write(existing_path, content).await?; + } else { + return Err(ReadMeError::NotFound); + } + Ok(()) }