├── tests ├── lib.rs ├── commands │ ├── mod.rs │ ├── config.rs │ ├── clear.rs │ ├── refresh.rs │ ├── tasks.rs │ ├── details.rs │ ├── verbose.rs │ ├── last.rs │ ├── init.rs │ └── log.rs ├── util │ └── mod.rs └── update_timestamps │ └── mod.rs ├── src ├── update │ ├── update_status.rs │ ├── release_service.rs │ ├── mod.rs │ ├── baca_release.rs │ ├── update_checker.rs │ ├── github_releases.rs │ └── update_check_timestamp.rs ├── parse │ ├── from_baca_output.rs │ ├── allowed_language.rs │ ├── mod.rs │ ├── tasks.rs │ └── results.rs ├── api │ ├── mod.rs │ ├── details.rs │ ├── baca_api.rs │ ├── request_type.rs │ ├── request.rs │ └── baca_service.rs ├── model │ ├── test_results.rs │ ├── mod.rs │ ├── task.rs │ ├── tasks.rs │ ├── results.rs │ ├── submit.rs │ ├── language.rs │ └── submit_status.rs ├── log.rs ├── workspace │ ├── config_object.rs │ ├── baca_release.rs │ ├── workspace_paths.rs │ ├── zip.rs │ ├── mod.rs │ ├── header_check.rs │ ├── no_polish.rs │ ├── connection_config.rs │ ├── workspace_dir.rs │ ├── config_editor.rs │ ├── no_main.rs │ └── submit_config.rs ├── command │ ├── prompt.rs │ ├── details.rs │ ├── refresh.rs │ ├── log.rs │ ├── tasks.rs │ ├── mod.rs │ ├── last.rs │ └── init.rs ├── main.rs ├── cli.rs └── error.rs ├── .gitignore ├── .codecov.yml ├── .github └── workflows │ ├── release.yml │ ├── build.yml │ └── grcov.yml ├── Cargo.toml ├── LICENSE └── README.md /tests/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod commands; 2 | mod update_timestamps; 3 | mod util; 4 | -------------------------------------------------------------------------------- /src/update/update_status.rs: -------------------------------------------------------------------------------- 1 | use crate::update::BacaRelease; 2 | 3 | #[derive(Debug, PartialEq, Eq)] 4 | pub enum UpdateStatus { 5 | NoUpdates, 6 | Update(BacaRelease), 7 | } 8 | -------------------------------------------------------------------------------- /src/parse/from_baca_output.rs: -------------------------------------------------------------------------------- 1 | use crate::workspace::ConnectionConfig; 2 | 3 | pub trait FromBacaOutput { 4 | fn from_baca_output(connection_config: &ConnectionConfig, data: &str) -> Self; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # JetBrains 9 | .idea 10 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::request::Request; 2 | pub use self::request_type::RequestType; 3 | 4 | pub mod baca_api; 5 | pub mod baca_service; 6 | pub mod details; 7 | mod request; 8 | mod request_type; 9 | -------------------------------------------------------------------------------- /src/model/test_results.rs: -------------------------------------------------------------------------------- 1 | use crate::model::SubmitStatus; 2 | 3 | #[derive(Debug, PartialEq, Eq, Clone, Default)] 4 | pub struct TestResults { 5 | pub name: String, 6 | pub status: SubmitStatus, 7 | } 8 | -------------------------------------------------------------------------------- /tests/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod clear; 2 | pub mod config; 3 | pub mod details; 4 | pub mod init; 5 | pub mod last; 6 | pub mod log; 7 | pub mod refresh; 8 | pub mod submit; 9 | pub mod tasks; 10 | pub mod verbose; 11 | -------------------------------------------------------------------------------- /src/update/release_service.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::update::BacaRelease; 3 | #[cfg(test)] 4 | use mockall::{automock, predicate::*}; 5 | 6 | #[cfg_attr(test, automock)] 7 | pub trait ReleaseService { 8 | fn get_last_release(&self) -> Result; 9 | } 10 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 80..100 3 | round: down 4 | precision: 2 5 | 6 | status: 7 | project: 8 | default: 9 | enabled: yes 10 | target: 80 11 | threshold: 5 12 | if_not_found: success 13 | if_ci_failed: error 14 | patch: 15 | default: 16 | enabled: yes 17 | target: 70 18 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | mod language; 2 | mod results; 3 | mod submit; 4 | mod submit_status; 5 | mod task; 6 | mod tasks; 7 | mod test_results; 8 | pub use self::language::Language; 9 | pub use self::results::Results; 10 | pub use self::submit::Submit; 11 | pub use self::submit_status::SubmitStatus; 12 | pub use self::task::Task; 13 | pub use self::tasks::Tasks; 14 | pub use self::test_results::TestResults; 15 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | use tracing::Level; 2 | use tracing_subscriber::FmtSubscriber; 3 | 4 | pub fn init_logging(level: Level) { 5 | let subscriber = FmtSubscriber::builder() 6 | .without_time() 7 | .with_max_level(level) 8 | .finish(); 9 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 10 | tracing::debug!("Log level: {}", level); 11 | } 12 | -------------------------------------------------------------------------------- /tests/commands/config.rs: -------------------------------------------------------------------------------- 1 | use crate::util::set_up_with_dir; 2 | use predicates::prelude::*; 3 | 4 | #[test] 5 | fn given_connection_config_edit_when_not_init_then_print_error() { 6 | let (dir, mut cmd) = set_up_with_dir().unwrap(); 7 | cmd.arg("-v"); 8 | cmd.arg("config"); 9 | cmd.assert() 10 | .stdout(predicate::str::contains("not initialized")); 11 | 12 | dir.close().unwrap(); 13 | } 14 | -------------------------------------------------------------------------------- /src/api/details.rs: -------------------------------------------------------------------------------- 1 | pub const SERVER_URL: &str = "baca.ii.uj.edu.pl"; 2 | pub const PERMUTATION: &str = "5A4AE95C27260DF45F17F9BF027335F6"; 3 | pub const EMPTY_RESPONSE: &str = "//OK[0,[],0,7]"; 4 | 5 | pub fn permutation() -> String { 6 | PERMUTATION.to_string() 7 | } 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use super::*; 12 | 13 | #[test] 14 | fn permutation_function_should_return_same_string() { 15 | assert_eq!(permutation(), PERMUTATION); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/workspace/config_object.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::workspace::Workspace; 3 | use serde::de::DeserializeOwned; 4 | use serde::Serialize; 5 | use std::fmt::Debug; 6 | 7 | pub trait ConfigObject: Serialize + DeserializeOwned + Debug + Sized { 8 | fn save_config(&self, workspace: &W) -> Result<()>; 9 | fn read_config(workspace: &W) -> Result; 10 | fn remove_config(workspace: &W) -> Result<()>; 11 | fn config_filename() -> String; 12 | } 13 | -------------------------------------------------------------------------------- /src/update/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::baca_release::BacaRelease; 2 | pub use self::github_releases::GithubReleases; 3 | pub use self::release_service::ReleaseService; 4 | pub use self::update_check_timestamp::UpdateCheckTimestamp; 5 | pub use self::update_checker::UpdateChecker; 6 | pub use self::update_status::UpdateStatus; 7 | 8 | mod baca_release; 9 | mod github_releases; 10 | mod release_service; 11 | pub mod update_check_timestamp; 12 | pub mod update_checker; 13 | mod update_status; 14 | 15 | pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); 16 | -------------------------------------------------------------------------------- /src/workspace/baca_release.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::update::BacaRelease; 3 | use crate::workspace::{ConfigObject, Workspace}; 4 | 5 | impl ConfigObject for BacaRelease { 6 | fn save_config(&self, workspace: &W) -> Result<()> { 7 | workspace.save_config_object(self) 8 | } 9 | 10 | fn read_config(workspace: &W) -> Result { 11 | workspace.read_config_object::() 12 | } 13 | 14 | fn remove_config(workspace: &W) -> Result<()> { 15 | workspace.remove_config_object::() 16 | } 17 | 18 | fn config_filename() -> String { 19 | "version".to_string() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/model/task.rs: -------------------------------------------------------------------------------- 1 | use crate::model::Language; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq)] 5 | pub struct Task { 6 | pub id: String, 7 | pub language: Language, 8 | pub problem_name: String, 9 | pub overall_oks: i32, 10 | } 11 | 12 | impl Task { 13 | #[cfg(test)] 14 | pub fn new(id: &str, language: Language, problem_name: &str, overall_oks: i32) -> Self { 15 | Self { 16 | id: id.to_string(), 17 | language, 18 | problem_name: problem_name.to_string(), 19 | overall_oks, 20 | } 21 | } 22 | } 23 | 24 | impl Display for Task { 25 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 26 | f.write_str(&self.problem_name) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/workspace/workspace_paths.rs: -------------------------------------------------------------------------------- 1 | use crate::workspace::ConfigObject; 2 | use std::path::{Path, PathBuf}; 3 | 4 | // todo: walk up dir tree until found 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct WorkspacePaths { 7 | root_path: PathBuf, 8 | } 9 | 10 | impl WorkspacePaths { 11 | pub fn new() -> Self { 12 | Self { 13 | root_path: Path::new(".").to_path_buf(), 14 | } 15 | } 16 | 17 | pub(crate) fn _with_root(root_path: &Path) -> Self { 18 | Self { 19 | root_path: root_path.to_path_buf(), 20 | } 21 | } 22 | 23 | pub fn baca_dir(&self) -> PathBuf { 24 | self.root_path.join(".baca") 25 | } 26 | 27 | pub fn config_path(&self) -> PathBuf 28 | where 29 | T: ConfigObject, 30 | { 31 | self.baca_dir().join(T::config_filename()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/model/tasks.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::model::Task; 3 | use colored::Colorize; 4 | 5 | #[derive(Debug, PartialEq, Eq)] 6 | pub struct Tasks { 7 | pub tasks: Vec, 8 | } 9 | 10 | impl Tasks { 11 | pub fn new(tasks: Vec) -> Self { 12 | Tasks { tasks } 13 | } 14 | 15 | pub fn print(&self) { 16 | for task in &self.tasks { 17 | let s = format!( 18 | "● Id: {} - {} - {} OK", 19 | task.id, task.problem_name, task.overall_oks 20 | ) 21 | .bold(); 22 | println!("{}", s); 23 | } 24 | } 25 | 26 | pub fn get_by_id(&self, task_id: &str) -> Result<&Task, Error> { 27 | self.tasks 28 | .iter() 29 | .find(|x| x.id == task_id) 30 | .ok_or_else(|| Error::InvalidTaskId(task_id.to_string())) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/commands/clear.rs: -------------------------------------------------------------------------------- 1 | use crate::util::{initialize_correct_workspace, set_up_command, set_up_with_dir}; 2 | use predicates::prelude::*; 3 | 4 | #[test] 5 | #[ignore] 6 | fn clear_when_init_then_remove_directory() { 7 | let dir = initialize_correct_workspace().unwrap(); 8 | let mut cmd = set_up_command(&dir).unwrap(); 9 | 10 | assert!(predicate::path::exists().eval(&dir.path().join(".baca"))); 11 | cmd.arg("-v"); 12 | cmd.arg("clear"); 13 | cmd.assert(); 14 | 15 | assert!(predicate::path::missing().eval(&dir.path().join(".baca"))); 16 | dir.close().unwrap(); 17 | } 18 | 19 | #[test] 20 | fn clear_when_not_init_then_print_error() { 21 | let (dir, mut cmd) = set_up_with_dir().unwrap(); 22 | 23 | cmd.arg("-v"); 24 | cmd.arg("clear"); 25 | cmd.assert() 26 | .stdout(predicate::str::contains("not initialized")); 27 | 28 | assert!(predicate::path::missing().eval(&dir.path().join(".baca"))); 29 | dir.close().unwrap(); 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ ubuntu-latest, windows-latest, macos-latest ] 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | - name: Cache cargo 24 | uses: actions/cache@v3 25 | with: 26 | path: | 27 | ~/.cargo/registry 28 | ~/.cargo/git 29 | target 30 | key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} 31 | - name: Build project 32 | run: cargo build --all --all-targets --release 33 | - name: Upload artifact 34 | uses: actions/upload-artifact@v3 35 | with: 36 | name: baca-${{ runner.os }} 37 | path: ./target/release/baca* 38 | -------------------------------------------------------------------------------- /src/workspace/zip.rs: -------------------------------------------------------------------------------- 1 | use crate::error; 2 | use std::fs::{read, File}; 3 | use std::io::{Error, ErrorKind, Write}; 4 | use std::path::Path; 5 | 6 | pub fn zip_file(path: &Path) -> Result<&Path, error::Error> { 7 | zip_file_impl(path).map_err(|e| error::Error::Zipping(e.into())) 8 | } 9 | 10 | fn zip_file_impl(path: &Path) -> Result<&Path, Error> { 11 | let filename = path.file_name().unwrap().to_str().ok_or(ErrorKind::Other)?; 12 | let path = path.to_str().ok_or(ErrorKind::Other)?; 13 | 14 | println!("Zipping {}.", filename); 15 | tracing::debug!("Relative path: {}.", path); 16 | 17 | let source = read(path)?; 18 | let buf = File::create("source.zip")?; 19 | let mut zip = zip::ZipWriter::new(buf); 20 | 21 | let options = 22 | zip::write::FileOptions::default().compression_method(zip::CompressionMethod::DEFLATE); 23 | zip.start_file(filename, options)?; 24 | zip.write_all(source.as_ref())?; 25 | zip.finish()?; 26 | 27 | Ok(Path::new("source.zip")) 28 | } 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "baca" 3 | description = "CLI client for the Jagiellonian University's BaCa online judge" 4 | authors = ["Hubert Jaremko "] 5 | version = "0.6.0" 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | clap = { version = "4", features = ["derive"] } 12 | tracing = "0" 13 | tracing-subscriber = "0" 14 | reqwest = { version = "0", features = ["blocking", "json", "cookies", "multipart"] } 15 | dyn-fmt = "0" 16 | colored = "2" 17 | serde = { version = "1", features = ["derive"] } 18 | serde_json = "1" 19 | serde_yaml = "0" 20 | zip = "0" 21 | dialoguer = "0" 22 | time = { version = "0", features = ["serde"] } 23 | merge = "0" 24 | regex = "1" 25 | deunicode = "1" 26 | 27 | [dev-dependencies] 28 | mockall = "0" 29 | assert_cmd = "2" 30 | predicates = "2" 31 | assert_fs = "1" 32 | tempfile = "3" 33 | 34 | [profile.release] 35 | strip = true 36 | lto = true 37 | codegen-units = 1 38 | -------------------------------------------------------------------------------- /tests/commands/refresh.rs: -------------------------------------------------------------------------------- 1 | use crate::util::*; 2 | use predicates::prelude::*; 3 | use std::fs; 4 | 5 | #[test] 6 | fn on_not_initialized_should_report_error() -> Result<(), Box> { 7 | assert_fails_if_not_initialized(&["refresh"]) 8 | } 9 | 10 | #[test] 11 | #[ignore] 12 | fn on_correct_repo_should_refresh_cookie() -> Result<(), Box> { 13 | let dir = initialize_correct_workspace()?; 14 | let mut cmd = set_up_command(&dir)?; 15 | 16 | cmd.arg("refresh"); 17 | cmd.assert() 18 | .stdout(predicate::str::contains("New session obtained")); 19 | dir.close()?; 20 | Ok(()) 21 | } 22 | 23 | #[test] 24 | #[ignore] 25 | fn on_corrupted_repo_should_report_error() -> Result<(), Box> { 26 | let dir = initialize_correct_workspace()?; 27 | let mut cmd = set_up_command(&dir)?; 28 | fs::remove_file(dir.baca_config_file_path())?; 29 | 30 | cmd.arg("refresh"); 31 | cmd.assert().stdout(predicate::str::contains("corrupted")); 32 | dir.close()?; 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hubert Jaremko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/api/baca_api.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::model::{Language, Results, Submit, Task, Tasks}; 3 | use crate::workspace::ConnectionConfig; 4 | 5 | #[cfg(test)] 6 | use mockall::{automock, predicate::*}; 7 | 8 | #[cfg_attr(test, automock)] 9 | pub trait BacaApi { 10 | fn get_cookie(&self, connection_config: &ConnectionConfig) -> Result; 11 | fn get_submit_details( 12 | &self, 13 | connection_config: &ConnectionConfig, 14 | submit_id: &str, 15 | ) -> Result; 16 | fn get_results(&self, connection_config: &ConnectionConfig) -> Result; 17 | fn get_results_by_task( 18 | &self, 19 | connection_config: &ConnectionConfig, 20 | task_id: &str, 21 | ) -> Result; 22 | fn get_tasks(&self, connection_config: &ConnectionConfig) -> Result; 23 | fn submit( 24 | &self, 25 | connection_config: &ConnectionConfig, 26 | task: &Task, 27 | file_path: &str, 28 | ) -> Result<()>; 29 | fn get_allowed_language( 30 | &self, 31 | connection_config: &ConnectionConfig, 32 | task_id: &str, 33 | ) -> Result>; 34 | } 35 | -------------------------------------------------------------------------------- /src/workspace/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | #[cfg(test)] 3 | use mockall::{automock, predicate::*}; 4 | 5 | pub use self::config_object::ConfigObject; 6 | pub use self::connection_config::ConnectionConfig; 7 | pub use self::no_main::remove_main; 8 | pub use self::no_polish::make_polishless_file; 9 | pub use self::submit_config::SubmitConfig; 10 | pub use self::workspace_dir::WorkspaceDir; 11 | pub use self::workspace_paths::WorkspacePaths; 12 | pub use self::zip::zip_file; 13 | 14 | pub mod baca_release; 15 | pub mod config_editor; 16 | pub mod config_object; 17 | mod connection_config; 18 | pub mod header_check; 19 | mod no_main; 20 | mod no_polish; 21 | mod submit_config; 22 | pub mod workspace_dir; 23 | pub mod workspace_paths; 24 | mod zip; 25 | 26 | #[cfg_attr(test, automock)] 27 | pub trait Workspace { 28 | fn initialize(&self) -> Result<()>; 29 | fn check_if_initialized(&self) -> Result<()>; 30 | fn remove_workspace(&self) -> Result<()>; 31 | fn save_config_object(&self, object: &T) -> Result<()>; 32 | fn read_config_object(&self) -> Result; 33 | fn remove_config_object(&self) -> Result<()>; 34 | fn get_paths(&self) -> WorkspacePaths; 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [ ubuntu-latest, windows-latest ] 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | - name: Cache cargo 18 | id: cache-cargo 19 | uses: actions/cache@v3 20 | with: 21 | path: | 22 | ~/.cargo/registry 23 | ~/.cargo/git 24 | target 25 | key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} 26 | - name: Run linter 27 | run: cargo clippy --all-features -- -D warnings 28 | - name: Run tests 29 | env: 30 | TEST_BACA_PASSWORD: ${{ secrets.TEST_BACA_PASSWORD }} 31 | TEST_BACA_LOGIN: ${{ secrets.TEST_BACA_LOGIN }} 32 | TEST_BACA_HOST: ${{ secrets.TEST_BACA_HOST }} 33 | AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} 34 | run: cargo test -- --include-ignored 35 | 36 | 37 | formatting: 38 | name: Formatting 39 | runs-on: macos-latest 40 | 41 | steps: 42 | - name: Checkout code 43 | uses: actions/checkout@v3 44 | - name: Check formatting 45 | run: cargo fmt --all -- --check 46 | -------------------------------------------------------------------------------- /tests/commands/tasks.rs: -------------------------------------------------------------------------------- 1 | use crate::util::*; 2 | use predicates::prelude::predicate; 3 | use std::fs; 4 | 5 | #[test] 6 | fn tasks_not_initialized() -> Result<(), Box> { 7 | assert_fails_if_not_initialized(&["tasks"]) 8 | } 9 | 10 | #[test] 11 | #[ignore] 12 | fn on_correct_repo_should_print_tasks() -> Result<(), Box> { 13 | let dir = initialize_correct_workspace()?; 14 | let mut cmd = set_up_command(&dir)?; 15 | 16 | cmd.arg("tasks"); 17 | cmd.assert() 18 | .stdout(predicate::str::contains("[A] Zera funkcji")) 19 | .stdout(predicate::str::contains("[B] Metoda Newtona")) 20 | .stdout(predicate::str::contains( 21 | r#"[C] FAD\x3Csup\x3E2\x3C/sup\x3E - Pochodne mieszane"#, 22 | )) 23 | .stdout(predicate::str::contains("[D] Skalowany Gauss")) 24 | .stdout(predicate::str::contains("[E] Metoda SOR")) 25 | .stdout(predicate::str::contains("[F] Interpolacja")) 26 | .stdout(predicate::str::contains("[G] Funkcje sklejane")); 27 | dir.close()?; 28 | Ok(()) 29 | } 30 | 31 | #[test] 32 | #[ignore] 33 | fn on_corrupted_repo_should_report_error() -> Result<(), Box> { 34 | let dir = initialize_correct_workspace()?; 35 | let mut cmd = set_up_command(&dir)?; 36 | fs::remove_file(dir.baca_config_file_path())?; 37 | 38 | cmd.arg("tasks"); 39 | cmd.assert().stdout(predicate::str::contains("corrupted")); 40 | dir.close()?; 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/command/prompt.rs: -------------------------------------------------------------------------------- 1 | use crate::error; 2 | use crate::model::Tasks; 3 | use dialoguer::theme::ColorfulTheme; 4 | use dialoguer::Select; 5 | #[cfg(test)] 6 | use mockall::{automock, predicate::*}; 7 | use tracing::info; 8 | 9 | #[cfg_attr(test, automock)] 10 | pub trait Prompt { 11 | fn interact(&self) -> error::Result; 12 | } 13 | 14 | pub struct Input(pub &'static str); 15 | 16 | impl Prompt for Input { 17 | fn interact(&self) -> error::Result { 18 | Ok(dialoguer::Input::::new() 19 | .with_prompt(self.0) 20 | .interact()?) 21 | } 22 | } 23 | 24 | pub struct Password; 25 | 26 | impl Prompt for Password { 27 | fn interact(&self) -> error::Result { 28 | Ok(dialoguer::Password::new() 29 | .with_prompt("Password") 30 | .interact()?) 31 | } 32 | } 33 | 34 | pub struct TaskChoice { 35 | available_tasks: Tasks, 36 | } 37 | 38 | impl TaskChoice { 39 | pub fn new(available_tasks: Tasks) -> Self { 40 | Self { available_tasks } 41 | } 42 | } 43 | 44 | impl Prompt for TaskChoice { 45 | fn interact(&self) -> error::Result { 46 | let items = &self.available_tasks.tasks; 47 | 48 | let selection = Select::with_theme(&ColorfulTheme::default()) 49 | .items(items) 50 | .with_prompt("Choose task:") 51 | .default(0) 52 | .interact()?; 53 | 54 | info!("Selection index: {}", selection); 55 | Ok(items[selection].id.clone()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/parse/allowed_language.rs: -------------------------------------------------------------------------------- 1 | use crate::model::Language; 2 | use crate::parse::deserialize; 3 | use crate::parse::from_baca_output::FromBacaOutput; 4 | use crate::workspace::ConnectionConfig; 5 | use tracing::debug; 6 | 7 | impl FromBacaOutput for Option { 8 | fn from_baca_output(_: &ConnectionConfig, data: &str) -> Self { 9 | let data = deserialize(data); 10 | debug!("Deserialized: {:?}", data); 11 | 12 | if let Ok(language) = data[4].replace('\\', "").parse::() { 13 | Some(language) 14 | } else { 15 | None 16 | } 17 | } 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use super::*; 23 | 24 | #[test] 25 | fn no_allowed_languages() { 26 | let mock_connection = ConnectionConfig::default(); 27 | let data = r#"//OK[0,5,4,2,3,0,2,1,["testerka.gwt.client.tools.DataSource/1474249525","[[Ljava.lang.String;/4182515373","[Ljava.lang.String;/2600011424","id","nazwa"],0,7]"#; 28 | let expected = None; 29 | let actual = Option::::from_baca_output(&mock_connection, data); 30 | 31 | assert_eq!(actual, expected); 32 | } 33 | 34 | #[test] 35 | fn one_allowed_language() { 36 | let mock_connection = ConnectionConfig::default(); 37 | let data = r#"//OK[0,7,6,2,3,5,4,2,3,1,2,1,[\"testerka.gwt.client.tools.DataSource/1474249525\",\"[[Ljava.lang.String;/4182515373\",\"[Ljava.lang.String;/2600011424\",\"1\",\"C++\",\"id\",\"nazwa\"],0,7]"#; 38 | let expected = Some(Language::Cpp); 39 | let actual = Option::::from_baca_output(&mock_connection, data); 40 | 41 | assert_eq!(actual, expected); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/api/request_type.rs: -------------------------------------------------------------------------------- 1 | pub enum RequestType { 2 | Results, 3 | SubmitDetails(String), 4 | Login(String, String), 5 | Tasks, 6 | AllowedLanguages(String), 7 | } 8 | 9 | impl RequestType { 10 | pub fn payload_template(&self) -> String { 11 | match self { 12 | RequestType::Results => 13 | "7|0|5|{}|03D93DB883748ED9135F6A4744CFFA07|testerka.gwt.client.submits.SubmitsService|getAllSubmits|Z|1|2|3|4|1|5|1|".to_string(), 14 | RequestType::SubmitDetails(id) => 15 | "7|0|5|{}|03D93DB883748ED9135F6A4744CFFA07|testerka.gwt.client.submits.SubmitsService|getSubmitDetails|I|1|2|3|4|1|5|".to_string() + id + "|", 16 | RequestType::Login(_, _) => 17 | "7|0|7|{}|620F3CE7784C04B839FC8E10C6C4A753|testerka.gwt.client.acess.PrivilegesService|login|java.lang.String/2004016611|{}|{}|1|2|3|4|2|5|5|6|7|".to_string(), 18 | RequestType::Tasks => "7|0|4|{}|548F7E6329FFDEC9688CE48426651141|testerka.gwt.client.problems.ProblemsService|getAllProblems|1|2|3|4|0|".to_string(), 19 | RequestType::AllowedLanguages(id) => 20 | "7|0|5|{}|03D93DB883748ED9135F6A4744CFFA07|testerka.gwt.client.problems.ProblemsService|getAllowedLanguage|I|1|2|3|4|1|5|".to_string() + id + "|", 21 | } 22 | } 23 | 24 | pub fn mapping(&self) -> String { 25 | match *self { 26 | RequestType::Results => "submits".to_string(), 27 | RequestType::SubmitDetails(_) => "submits".to_string(), 28 | RequestType::Login(_, _) => "privileges".to_string(), 29 | RequestType::Tasks => "problems".to_string(), 30 | RequestType::AllowedLanguages(_) => "problems".to_string(), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/update/baca_release.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 5 | pub struct BacaRelease { 6 | pub version: String, 7 | #[serde(skip_serializing)] 8 | pub link: String, 9 | } 10 | 11 | impl BacaRelease { 12 | pub fn is_newer_than(&self, other: &str) -> bool { 13 | self.version.as_str() > other 14 | } 15 | 16 | pub fn new(version: &str, link: &str) -> Self { 17 | BacaRelease { 18 | version: version.to_string(), 19 | link: link.to_string(), 20 | } 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use super::*; 27 | 28 | #[test] 29 | fn same_should_return_false() { 30 | assert!(!BacaRelease::new("0.1.0", "link").is_newer_than("0.1.0")); 31 | } 32 | 33 | #[test] 34 | fn older_should_return_false() { 35 | assert!(!BacaRelease::new("0.0.1", "link").is_newer_than("0.0.2")); 36 | assert!(!BacaRelease::new("0.1.1", "link").is_newer_than("0.2.0")); 37 | assert!(!BacaRelease::new("0.1.0", "link").is_newer_than("0.2.0")); 38 | assert!(!BacaRelease::new("0.1.0", "link").is_newer_than("1.0.0")); 39 | assert!(!BacaRelease::new("0.0.1", "link").is_newer_than("1.0.0")); 40 | } 41 | 42 | #[test] 43 | fn newer_should_return_true() { 44 | assert!(BacaRelease::new("0.0.1", "link").is_newer_than("0.0.0")); 45 | assert!(BacaRelease::new("0.1.1", "link").is_newer_than("0.1.0")); 46 | assert!(BacaRelease::new("0.1.0", "link").is_newer_than("0.0.9")); 47 | assert!(BacaRelease::new("1.0.0", "link").is_newer_than("0.1.0")); 48 | assert!(BacaRelease::new("1.0.0", "link").is_newer_than("0.0.1")); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/commands/details.rs: -------------------------------------------------------------------------------- 1 | use crate::util::*; 2 | use predicates::prelude::predicate; 3 | use std::fs; 4 | 5 | #[test] 6 | fn tasks_not_initialized() -> Result<(), Box> { 7 | assert_fails_if_not_initialized(&["details", "123"]) 8 | } 9 | 10 | #[test] 11 | #[ignore] 12 | fn no_argument_should_report_error() -> Result<(), Box> { 13 | let dir = initialize_correct_workspace()?; 14 | let mut cmd = set_up_command(&dir)?; 15 | 16 | cmd.arg("details"); 17 | cmd.assert().stderr(predicate::str::contains( 18 | "required arguments were not provided", 19 | )); 20 | dir.close()?; 21 | Ok(()) 22 | } 23 | 24 | #[test] 25 | #[ignore] 26 | fn on_correct_argument_should_print_task() -> Result<(), Box> { 27 | let dir = initialize_correct_workspace()?; 28 | let mut cmd = set_up_command(&dir)?; 29 | 30 | cmd.arg("details").arg("2796"); 31 | cmd.assert() 32 | .stdout(predicate::str::contains("[D] Skalowany Gauss")) 33 | .stdout(predicate::str::contains("C++")) 34 | .stdout(predicate::str::contains("2020-04-20 15:39:42")) 35 | .stdout(predicate::str::contains("2796")) 36 | .stdout(predicate::str::contains("74")) 37 | .stdout(predicate::str::contains("2.95")) 38 | .stdout(predicate::str::contains("WrongAnswer")); 39 | dir.close()?; 40 | Ok(()) 41 | } 42 | 43 | #[test] 44 | #[ignore] 45 | fn on_corrupted_repo_should_report_error() -> Result<(), Box> { 46 | let dir = initialize_correct_workspace()?; 47 | let mut cmd = set_up_command(&dir)?; 48 | fs::remove_file(dir.baca_config_file_path())?; 49 | 50 | cmd.arg("details").arg("123"); 51 | cmd.assert().stdout(predicate::str::contains("corrupted")); 52 | dir.close()?; 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /tests/commands/verbose.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | use predicates::prelude::*; 3 | 4 | #[test] 5 | fn no_verbose_should_not_enable_logs() -> Result<(), Box> { 6 | let mut cmd = Command::cargo_bin("baca")?; 7 | 8 | cmd.arg("-u"); 9 | cmd.arg("tasks"); 10 | cmd.assert() 11 | .stdout(predicate::str::contains("INFO").not()) 12 | .stdout(predicate::str::contains("DEBUG").not()) 13 | .stdout(predicate::str::contains("TRACE").not()) 14 | .stdout(predicate::str::contains("ERROR").not()); 15 | 16 | Ok(()) 17 | } 18 | 19 | #[test] 20 | fn one_verbose_should_enable_info() -> Result<(), Box> { 21 | let mut cmd = Command::cargo_bin("baca")?; 22 | 23 | cmd.arg("-u"); 24 | cmd.arg("-v").arg("tasks"); 25 | cmd.assert() 26 | .stdout(predicate::str::contains("INFO")) 27 | .stdout(predicate::str::contains("DEBUG").not()) 28 | .stdout(predicate::str::contains("TRACE").not()); 29 | 30 | Ok(()) 31 | } 32 | 33 | #[test] 34 | fn two_verbose_should_enable_debug() -> Result<(), Box> { 35 | let mut cmd = Command::cargo_bin("baca")?; 36 | 37 | cmd.arg("-u"); 38 | cmd.arg("-vv").arg("tasks"); 39 | cmd.assert() 40 | .stdout(predicate::str::contains("INFO")) 41 | .stdout(predicate::str::contains("DEBUG")) 42 | .stdout(predicate::str::contains("TRACE").not()); 43 | 44 | Ok(()) 45 | } 46 | 47 | #[test] 48 | fn three_verbose_should_enable_trace() -> Result<(), Box> { 49 | let mut cmd = Command::cargo_bin("baca")?; 50 | 51 | cmd.arg("-u"); 52 | cmd.arg("-vvv").arg("tasks"); 53 | cmd.assert() 54 | .stdout(predicate::str::contains("INFO")) 55 | .stdout(predicate::str::contains("DEBUG")) 56 | .stdout(predicate::str::contains("TRACE")); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/parse/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod allowed_language; 2 | pub mod from_baca_output; 3 | pub mod results; 4 | pub mod submit; 5 | pub mod tasks; 6 | 7 | // todo: deserializable trait 8 | // todo: document this 9 | 10 | fn deserialize(data: &str) -> Vec { 11 | if data.len() < 18 { 12 | return Vec::new(); 13 | } 14 | 15 | let data = remove_outer_layer(data); 16 | let data = split_raw(data); 17 | let keys = get_keys(&data); 18 | let values = get_values(&data, keys.len()); 19 | map_serialized(&keys, &values) 20 | } 21 | 22 | fn map_serialized(keys: &[String], values: &[String]) -> Vec { 23 | let to_usize = |x: &String| x.to_string().parse::().unwrap(); 24 | let not_zero = |x: &usize| *x != 0usize; 25 | let to_value = |x: usize| (*values[x - 1]).to_string(); 26 | 27 | keys.iter() 28 | .map(to_usize) 29 | .filter(not_zero) 30 | .map(to_value) 31 | .map(|x| x.replace('\"', "")) 32 | .collect() 33 | } 34 | 35 | fn get_values(data: &[String], keys_len: usize) -> Vec { 36 | data.iter() 37 | .skip(keys_len) 38 | .map(|x| x.to_string()) 39 | .collect::>() 40 | } 41 | 42 | fn remove_outer_layer(data: &str) -> String { 43 | data.chars().skip(5).take(data.len() - 13).collect() 44 | } 45 | 46 | fn split_raw(data: String) -> Vec { 47 | data.split(',').map(|x| x.to_owned()).collect() 48 | } 49 | 50 | fn get_keys(data: &[String]) -> Vec { 51 | let is_number = |x: &&String| (**x).chars().all(|c| c.is_ascii_digit()); 52 | data.iter() 53 | .take_while(is_number) 54 | .map(|x| x.to_owned()) 55 | .collect() 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use crate::parse::deserialize; 61 | 62 | #[test] 63 | fn deserialize_empty_string() { 64 | assert_eq!(deserialize(""), Vec::::new()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/command/details.rs: -------------------------------------------------------------------------------- 1 | use crate::api::baca_api::BacaApi; 2 | use crate::command::Command; 3 | use crate::error::Result; 4 | use crate::workspace::{ConfigObject, ConnectionConfig, Workspace}; 5 | use tracing::info; 6 | 7 | pub struct Details { 8 | submit_id: String, 9 | } 10 | 11 | impl Details { 12 | pub fn new(submit_id: &str) -> Self { 13 | Details { 14 | submit_id: submit_id.to_string(), 15 | } 16 | } 17 | } 18 | 19 | impl Command for Details { 20 | fn execute(self, workspace: &W, api: &A) -> Result<()> 21 | where 22 | W: Workspace, 23 | A: BacaApi, 24 | { 25 | info!("Printing details for submit: {}", self.submit_id); 26 | 27 | let connection_config = ConnectionConfig::read_config(workspace)?; 28 | let submit = api.get_submit_details(&connection_config, &self.submit_id)?; 29 | 30 | submit.print_with_tests(); 31 | Ok(()) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | use crate::api::baca_api::MockBacaApi; 39 | use crate::workspace::{ConnectionConfig, MockWorkspace}; 40 | 41 | #[test] 42 | fn no_tasks_yet_should_return_error() { 43 | let mut mock_workspace = MockWorkspace::new(); 44 | mock_workspace 45 | .expect_read_config_object() 46 | .returning(|| Ok(ConnectionConfig::default())); 47 | 48 | let mut mock_api = MockBacaApi::new(); 49 | mock_api 50 | .expect_get_submit_details() 51 | .once() 52 | .withf(|x, id| *x == ConnectionConfig::default() && id == "2888") 53 | .returning(|_, _| Err(crate::error::Error::InvalidSubmitId)); 54 | 55 | let details = Details { 56 | submit_id: "2888".to_string(), 57 | }; 58 | let result = details.execute(&mock_workspace, &mock_api); 59 | assert!(result.is_err(), "result = {:?}", result); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/command/refresh.rs: -------------------------------------------------------------------------------- 1 | use crate::api::baca_api::BacaApi; 2 | use crate::command::Command; 3 | use crate::error; 4 | use crate::workspace::{ConfigObject, ConnectionConfig, Workspace}; 5 | use tracing::info; 6 | 7 | pub struct Refresh {} 8 | 9 | impl Refresh { 10 | pub fn new() -> Self { 11 | Self {} 12 | } 13 | } 14 | 15 | impl Command for Refresh { 16 | fn execute(self, workspace: &W, api: &A) -> error::Result<()> 17 | where 18 | W: Workspace, 19 | A: BacaApi, 20 | { 21 | info!("Refreshing Baca session."); 22 | let mut connection_config = ConnectionConfig::read_config(workspace)?; 23 | connection_config.cookie = api.get_cookie(&connection_config)?; 24 | connection_config.save_config(workspace)?; 25 | 26 | println!("New session obtained."); 27 | Ok(()) 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use super::*; 34 | use crate::api::baca_api::MockBacaApi; 35 | use crate::workspace::{ConnectionConfig, MockWorkspace}; 36 | 37 | #[test] 38 | fn refresh_success_test() { 39 | let mut mock_workspace = MockWorkspace::new(); 40 | mock_workspace 41 | .expect_read_config_object() 42 | .returning(|| Ok(ConnectionConfig::default())); 43 | mock_workspace 44 | .expect_save_config_object() 45 | .once() 46 | .withf(|x: &ConnectionConfig| { 47 | let mut expected = ConnectionConfig::default(); 48 | expected.cookie = "ok_cookie".to_string(); 49 | 50 | *x == expected 51 | }) 52 | .returning(|_| Ok(())); 53 | 54 | let mut mock_api = MockBacaApi::new(); 55 | mock_api 56 | .expect_get_cookie() 57 | .withf(|x| *x == ConnectionConfig::default()) 58 | .returning(|_| Ok("ok_cookie".to_string())); 59 | 60 | let refresh = Refresh::new(); 61 | let result = refresh.execute(&mock_workspace, &mock_api); 62 | assert!(result.is_ok()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/workspace/header_check.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::model::Language; 3 | use std::fmt::Debug; 4 | use std::fs::File; 5 | use std::io::{BufRead, BufReader}; 6 | use std::path::Path; 7 | use tracing::{debug, info}; 8 | 9 | pub fn is_header_present

(input_file: P, lang: &Language) -> Result 10 | where 11 | P: AsRef + Debug, 12 | { 13 | info!("Checking for header..."); 14 | debug!("Checking for {:?} header in file {:?}", lang, input_file); 15 | 16 | let input_file = File::open(input_file)?; 17 | let first_line = BufReader::new(input_file) 18 | .lines() 19 | .take(1) 20 | .map(|x| x.unwrap_or_else(|_| "no_header".to_string())) 21 | .collect::(); 22 | 23 | debug!("First line: {first_line}"); 24 | let r = lang.is_comment(&first_line); 25 | info!("Header found: {r}"); 26 | Ok(r) 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | use std::io::Write; 33 | use tempfile::NamedTempFile; 34 | 35 | fn make_input_file(content: &str) -> NamedTempFile { 36 | let mut file = NamedTempFile::new().unwrap(); 37 | file.write_all(content.as_ref()).unwrap(); 38 | file 39 | } 40 | 41 | #[test] 42 | fn empty_file() { 43 | let input = make_input_file(""); 44 | 45 | assert!(!(is_header_present(input.path(), &Language::Cpp).unwrap())); 46 | } 47 | 48 | #[test] 49 | fn invalid_file() { 50 | assert!(is_header_present("/i/hope/invalid_path", &Language::Cpp).is_err()); 51 | } 52 | 53 | #[test] 54 | fn no_header() { 55 | let input = make_input_file( 56 | r#"#include 57 | 58 | void Add(int** arr, int*[] arr2) 59 | { 60 | std::cout << "Hello\n"; 61 | return; 62 | } 63 | 64 | 65 | 66 | int moin(int argc, char** argv) 67 | { 68 | return 5; 69 | } 70 | "#, 71 | ); 72 | 73 | assert!(!(is_header_present(input.path(), &Language::Cpp).unwrap())); 74 | } 75 | 76 | #[test] 77 | fn header_present() { 78 | let input = make_input_file( 79 | r#"// Hubert Jaremko 80 | #include 81 | 82 | int main(int argc, char** argv) 83 | { 84 | return 5; 85 | } 86 | "#, 87 | ); 88 | 89 | assert!(is_header_present(input.path(), &Language::Cpp).unwrap()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/parse/tasks.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{Language, Task, Tasks}; 2 | use crate::parse::deserialize; 3 | use std::str::FromStr; 4 | use tracing::debug; 5 | 6 | impl FromStr for Tasks { 7 | type Err = crate::error::Error; 8 | 9 | fn from_str(data: &str) -> Result { 10 | let data = deserialize(data); 11 | debug!("Deserialized: {:?}", data); 12 | 13 | let st: Vec = data.iter().skip(3).map(|x| x.to_owned()).collect(); 14 | 15 | let tasks: Vec<_> = st 16 | .chunks(5) 17 | .rev() 18 | .skip(1) 19 | .map(|raw| Task { 20 | id: raw[4].to_string(), 21 | language: Language::Unsupported, 22 | problem_name: raw[3].to_string(), 23 | overall_oks: raw[2].parse().unwrap(), 24 | }) 25 | .collect(); 26 | 27 | debug!("Parsed tasks: {:?}", tasks); 28 | Ok(Tasks::new(tasks)) 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | use crate::model::Language::Unsupported; 36 | 37 | #[test] 38 | fn real_data() { 39 | let raw_data = r#"//OK[0,12,11,10,3,3,9,8,7,3,3,6,5,4,3,3,2,2,1,["testerka.gwt.client.tools.DataSource/1474249525","[[Ljava.lang.String;/4182515373","[Ljava.lang.String;/2600011424","1","Metoda parametryzacji","12","2","Metoda parametryzacji torusów","4","id","nazwa","liczba OK"],0,7]"#; 40 | let actual = raw_data.parse::().unwrap(); 41 | let expected = Tasks::new(vec![ 42 | Task::new("1", Unsupported, "Metoda parametryzacji", 12), 43 | Task::new("2", Unsupported, "Metoda parametryzacji torusów", 4), 44 | ]); 45 | 46 | assert_eq!(actual, expected); 47 | } 48 | 49 | #[test] 50 | fn empty_data() { 51 | let raw_data = crate::api::details::EMPTY_RESPONSE; 52 | let actual = raw_data.parse::().unwrap(); 53 | let expected = Tasks::new(Vec::new()); 54 | 55 | assert_eq!(actual, expected); 56 | } 57 | 58 | #[test] 59 | fn invalid_response() { 60 | let raw_data = "//OK[0,[3,3,6,5,4,3, invalid ababa],0,7]"; 61 | let actual = raw_data.parse::().unwrap(); 62 | let expected = Tasks::new(Vec::new()); 63 | 64 | assert_eq!(actual, expected); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/command/log.rs: -------------------------------------------------------------------------------- 1 | use crate::api::baca_api::BacaApi; 2 | use crate::command::Command; 3 | use crate::error::{Error, Result}; 4 | use crate::model::Results; 5 | use crate::workspace::{ConfigObject, ConnectionConfig, Workspace}; 6 | use tracing::info; 7 | 8 | pub struct Log { 9 | pub last_n: String, 10 | pub task_id: Option, 11 | } 12 | 13 | impl Log { 14 | pub fn new(last_n: &str, task_id: &Option) -> Self { 15 | Log { 16 | last_n: last_n.to_string(), 17 | task_id: task_id.map(|x| x.to_string()), 18 | } 19 | } 20 | 21 | fn fetch_logs(&self, api: &A, connection_config: &ConnectionConfig) -> Result 22 | where 23 | A: BacaApi, 24 | { 25 | Ok(if let Some(task_id) = &self.task_id { 26 | api.get_results_by_task(connection_config, task_id)? 27 | } else { 28 | api.get_results(connection_config)? 29 | }) 30 | } 31 | } 32 | 33 | impl Command for Log { 34 | fn execute(self, workspace: &W, api: &A) -> Result<()> 35 | where 36 | W: Workspace, 37 | A: BacaApi, 38 | { 39 | let n = to_int(&self.last_n)?; 40 | info!("Fetching {} logs.", n); 41 | let connection_config = ConnectionConfig::read_config(workspace)?; 42 | let results = self.fetch_logs(api, &connection_config)?; 43 | 44 | results.print(n); 45 | Ok(()) 46 | } 47 | } 48 | 49 | fn to_int(n: &str) -> Result { 50 | n.parse().map_err(|_| Error::InvalidArgument) 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | use crate::api::baca_api::MockBacaApi; 57 | use crate::model::Results; 58 | use crate::workspace::{ConnectionConfig, MockWorkspace}; 59 | 60 | #[test] 61 | fn no_submits() { 62 | let mut mock_workspace = MockWorkspace::new(); 63 | mock_workspace 64 | .expect_read_config_object() 65 | .returning(|| Ok(ConnectionConfig::default())); 66 | 67 | let mut mock_api = MockBacaApi::new(); 68 | mock_api 69 | .expect_get_results() 70 | .withf(|x| *x == ConnectionConfig::default()) 71 | .returning(|_| Ok(Results::default())); 72 | 73 | let log = Log::new("10", &None); 74 | let result = log.execute(&mock_workspace, &mock_api); 75 | assert!(result.is_ok()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/grcov.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | grcov: 13 | name: Coverage 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: 18 | - macos-latest 19 | toolchain: 20 | - nightly 21 | cargo_flags: 22 | - "--all-features" 23 | steps: 24 | - name: Checkout source code 25 | uses: actions/checkout@v2 26 | 27 | - name: Install Rust 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | profile: minimal 31 | toolchain: ${{ matrix.toolchain }} 32 | override: true 33 | 34 | - name: Cache cargo 35 | id: cache-cargo 36 | uses: actions/cache@v3 37 | with: 38 | path: | 39 | ~/.cargo/bin/grcov 40 | key: ${{ runner.os }}-grcov 41 | restore-keys: ${{ runner.os }}-grcov 42 | 43 | - name: Install grcov 44 | if: steps.cache-cargo.outputs.cache-hit != 'true' 45 | uses: actions-rs/install@v0.1 46 | with: 47 | crate: grcov 48 | version: latest 49 | use-tool-cache: true 50 | 51 | - name: Run tests 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: test 55 | args: --all --no-fail-fast ${{ matrix.cargo_flags }} -- --include-ignored 56 | env: 57 | TEST_BACA_PASSWORD: ${{ secrets.TEST_BACA_PASSWORD }} 58 | TEST_BACA_LOGIN: ${{ secrets.TEST_BACA_LOGIN }} 59 | TEST_BACA_HOST: ${{ secrets.TEST_BACA_HOST }} 60 | AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} 61 | CARGO_INCREMENTAL: "0" 62 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebug-assertions=off' 63 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebug-assertions=off' 64 | 65 | - name: Generate coverage data 66 | id: grcov 67 | uses: actions-rs/grcov@v0.1 68 | - name: Upload coverage as artifact 69 | uses: actions/upload-artifact@v2 70 | with: 71 | name: lcov.info 72 | path: ${{ steps.grcov.outputs.report }} 73 | 74 | - name: Upload coverage to codecov.io 75 | uses: codecov/codecov-action@v3 76 | with: 77 | file: ${{ steps.grcov.outputs.report }} 78 | fail_ci_if_error: true 79 | -------------------------------------------------------------------------------- /tests/commands/last.rs: -------------------------------------------------------------------------------- 1 | use crate::util::{ 2 | assert_fails_if_not_initialized, initialize_correct_workspace, set_up_command, 3 | BacaDirectoryPaths, 4 | }; 5 | use predicates::prelude::*; 6 | use std::fs; 7 | 8 | #[test] 9 | fn tasks_not_initialized() -> Result<(), Box> { 10 | assert_fails_if_not_initialized(&["last"]) 11 | } 12 | 13 | #[test] 14 | #[ignore] 15 | fn on_correct_repo_should_print_last_submit() -> Result<(), Box> { 16 | let dir = initialize_correct_workspace()?; 17 | let mut cmd = set_up_command(&dir)?; 18 | 19 | cmd.arg("last"); 20 | cmd.assert() 21 | .stdout(predicate::str::contains("[G] Funkcje sklejane")) 22 | .stdout(predicate::str::contains("C++")) 23 | .stdout(predicate::str::contains("2020-05-17 18:53:09")) 24 | .stdout(predicate::str::contains("4334")) 25 | .stdout(predicate::str::contains("100%")) 26 | .stdout(predicate::str::contains("4/4")) 27 | .stdout(predicate::str::contains("Ok")) 28 | .stdout(predicate::str::contains("test0/0")) 29 | .stdout(predicate::str::contains("test1/0")) 30 | .stdout(predicate::str::contains("test2/0")) 31 | .stdout(predicate::str::contains("test3/0")); 32 | dir.close()?; 33 | Ok(()) 34 | } 35 | 36 | #[test] 37 | #[ignore] 38 | fn on_corrupted_repo_should_report_error() -> Result<(), Box> { 39 | let dir = initialize_correct_workspace()?; 40 | let mut cmd = set_up_command(&dir)?; 41 | fs::remove_file(dir.baca_config_file_path())?; 42 | 43 | cmd.arg("last"); 44 | cmd.assert().stdout(predicate::str::contains("corrupted")); 45 | dir.close()?; 46 | Ok(()) 47 | } 48 | 49 | #[test] 50 | #[ignore] 51 | fn filter() -> Result<(), Box> { 52 | let dir = initialize_correct_workspace()?; 53 | let mut cmd = set_up_command(&dir)?; 54 | 55 | cmd.arg("last").arg("-t").arg("1"); 56 | cmd.assert() 57 | .stdout(predicate::str::contains("[A] Zera funkcji")); 58 | dir.close()?; 59 | Ok(()) 60 | } 61 | 62 | #[test] 63 | #[ignore] 64 | fn filter_given_invalid_task_id_should_print_error() -> Result<(), Box> { 65 | let dir = initialize_correct_workspace()?; 66 | let mut cmd = set_up_command(&dir)?; 67 | 68 | cmd.arg("last").arg("-t").arg("1123"); 69 | cmd.assert() 70 | .stdout(predicate::str::contains("1123 does not exist")); 71 | dir.close()?; 72 | Ok(()) 73 | } 74 | 75 | #[test] 76 | #[ignore] 77 | fn filter_given_invalid_argument_should_print_error() -> Result<(), Box> { 78 | let dir = initialize_correct_workspace()?; 79 | let mut cmd = set_up_command(&dir)?; 80 | 81 | cmd.arg("last").arg("-t").arg("asd"); 82 | cmd.assert() 83 | .stderr(predicate::str::contains("invalid value")); 84 | dir.close()?; 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /src/workspace/no_polish.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use deunicode::deunicode; 3 | use std::fs; 4 | use std::fs::File; 5 | use std::io::Write; 6 | use std::path::{Path, PathBuf}; 7 | use tracing::{debug, info}; 8 | 9 | pub fn make_polishless_file

(input_file: P) -> Result 10 | where 11 | P: AsRef, 12 | { 13 | let input_file: &Path = input_file.as_ref(); 14 | info!("Removing Polish diacritics from {:?}", input_file); 15 | 16 | let content = fs::read_to_string(input_file)?; 17 | let content = deunicode(&content); 18 | 19 | let filepath = 20 | std::env::temp_dir().join(input_file.file_name().ok_or(Error::InputFileDoesNotExist)?); 21 | let mut file = File::create(filepath.clone())?; 22 | file.write_all(content.as_ref())?; 23 | 24 | debug!("New input file path: {:?}", filepath); 25 | debug!("New input file content:\n{}", content); 26 | 27 | Ok(filepath) 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | use mockall::{predicate, Predicate}; 34 | 35 | fn make_input_file(content: &str, name: &str) -> PathBuf { 36 | let filepath = std::env::temp_dir().join(format!("test-input-{}.cpp", name)); 37 | let mut file = File::create(filepath.clone()).unwrap(); 38 | file.write_all(content.as_ref()).unwrap(); 39 | filepath 40 | } 41 | 42 | #[test] 43 | fn all_polish() { 44 | let input = "ążźćłóć"; 45 | let expected = "azzcloc"; 46 | 47 | let input_file = make_input_file(input, "all"); 48 | let actual_filepath = make_polishless_file(input_file).unwrap(); 49 | 50 | assert!(predicate::path::exists().eval(&actual_filepath)); 51 | assert!(predicate::path::eq_file(&actual_filepath) 52 | .utf8() 53 | .unwrap() 54 | .eval(expected)); 55 | } 56 | 57 | #[test] 58 | fn mixed() { 59 | let input = " ążźasdghjkescćłósda 3423ć "; 60 | let expected = " azzasdghjkescclosda 3423c "; 61 | 62 | let input_file = make_input_file(input, "mixed"); 63 | let actual_filepath = make_polishless_file(input_file).unwrap(); 64 | 65 | assert!(predicate::path::exists().eval(&actual_filepath)); 66 | assert!(predicate::path::eq_file(&actual_filepath) 67 | .utf8() 68 | .unwrap() 69 | .eval(expected)); 70 | } 71 | 72 | #[test] 73 | fn no_polish() { 74 | let input = " axxasdghjkescclosda 3423c \n "; 75 | let expected = " axxasdghjkescclosda 3423c \n "; 76 | 77 | let input_file = make_input_file(input, "no"); 78 | let actual_filepath = make_polishless_file(input_file).unwrap(); 79 | 80 | assert!(predicate::path::exists().eval(&actual_filepath)); 81 | assert!(predicate::path::eq_file(&actual_filepath) 82 | .utf8() 83 | .unwrap() 84 | .eval(expected)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/commands/init.rs: -------------------------------------------------------------------------------- 1 | use crate::util::get_baca_credentials; 2 | use assert_cmd::Command; 3 | use assert_fs::TempDir; 4 | use predicates::prelude::*; 5 | use std::fs::read_to_string; 6 | 7 | fn baca_dir_exists(temp: &TempDir) -> bool { 8 | predicate::path::exists().eval(&*temp.path().join(".baca")) 9 | } 10 | 11 | fn config_exists(temp: &TempDir) -> bool { 12 | predicate::path::exists().eval(&*temp.path().join(".baca/connection")) 13 | } 14 | 15 | #[test] 16 | #[ignore] 17 | fn invalid_password() -> Result<(), Box> { 18 | let temp = assert_fs::TempDir::new()?; 19 | 20 | let mut cmd = Command::cargo_bin("baca")?; 21 | 22 | cmd.current_dir(&temp); 23 | cmd.arg("-u"); 24 | cmd.arg("init") 25 | .args(["--host", "mn2020", "--login", "jaremko", "-p", "invalid"]); 26 | cmd.assert() 27 | .stdout(predicate::str::contains("Invalid login or password")); 28 | 29 | assert!(!baca_dir_exists(&temp)); 30 | temp.close()?; 31 | Ok(()) 32 | } 33 | 34 | #[test] 35 | #[ignore] 36 | fn invalid_host() -> Result<(), Box> { 37 | let (login, pass, _) = get_baca_credentials(); 38 | let temp = assert_fs::TempDir::new()?; 39 | 40 | let mut cmd = Command::cargo_bin("baca")?; 41 | 42 | cmd.current_dir(&temp); 43 | cmd.arg("-u"); 44 | cmd.arg("init") 45 | .args(["--host", "invalid", "--login", &login, "-p", &pass]); 46 | cmd.assert() 47 | .stdout(predicate::str::contains("Invalid host")); 48 | 49 | assert!(!baca_dir_exists(&temp)); 50 | temp.close()?; 51 | Ok(()) 52 | } 53 | 54 | #[test] 55 | #[ignore] 56 | fn success() -> Result<(), Box> { 57 | let (login, pass, host) = get_baca_credentials(); 58 | let temp = assert_fs::TempDir::new()?; 59 | 60 | let mut cmd = Command::cargo_bin("baca")?; 61 | 62 | cmd.current_dir(&temp); 63 | cmd.arg("-u"); 64 | cmd.arg("init") 65 | .args(["--host", &host, "-p", &pass, "-l", &login]); 66 | cmd.assert().code(0); 67 | 68 | assert!(baca_dir_exists(&temp)); 69 | assert!(config_exists(&temp)); 70 | temp.close()?; 71 | Ok(()) 72 | } 73 | 74 | #[test] 75 | #[ignore] 76 | fn should_save_version() -> Result<(), Box> { 77 | let (login, pass, host) = get_baca_credentials(); 78 | let temp = assert_fs::TempDir::new()?; 79 | 80 | let mut cmd = Command::cargo_bin("baca")?; 81 | 82 | cmd.current_dir(&temp); 83 | cmd.arg("-u"); 84 | cmd.arg("init") 85 | .args(["--host", &host, "-p", &pass, "-l", &login]); 86 | cmd.assert().code(0); 87 | 88 | let version_path = temp.path().join(".baca/version"); 89 | assert!(predicate::path::exists().eval(&version_path)); 90 | let saved_version = read_to_string(version_path).unwrap(); 91 | assert!(predicate::str::contains(env!("CARGO_PKG_VERSION")).eval(&saved_version)); 92 | temp.close()?; 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::Cli; 2 | use crate::update::{GithubReleases, UpdateCheckTimestamp, UpdateChecker, UpdateStatus}; 3 | use crate::workspace::{ConfigObject, WorkspaceDir}; 4 | use api::baca_service::BacaService; 5 | use clap::Parser; 6 | use colored::Colorize; 7 | use std::env; 8 | use tracing::{error, info, Level}; 9 | 10 | mod api; 11 | mod cli; 12 | mod command; 13 | mod error; 14 | mod log; 15 | mod model; 16 | mod parse; 17 | mod update; 18 | mod workspace; 19 | 20 | fn main() { 21 | let cli = Cli::parse(); 22 | let workspace = WorkspaceDir::new(); 23 | let baca_api = BacaService::default(); 24 | 25 | set_logging_level(&cli); 26 | check_for_updates(&workspace, cli.no_update, cli.force_update); 27 | 28 | let result = match &cli.command { 29 | Some(commands) => command::execute(&workspace, &baca_api, commands), 30 | None => Ok(()), 31 | }; 32 | 33 | if let Err(e) = result { 34 | error!("{:?}", e); 35 | println!("{}", format!("{}", e).bright_red()); 36 | } 37 | } 38 | 39 | fn set_logging_level(cli: &Cli) { 40 | let log_level = match cli.verbose { 41 | 0 => return, 42 | 1 => Level::INFO, 43 | 2 => Level::DEBUG, 44 | _ => Level::TRACE, 45 | }; 46 | 47 | log::init_logging(log_level); 48 | } 49 | 50 | fn check_for_updates(workspace: &WorkspaceDir, no_update: bool, force_update: bool) { 51 | if no_update { 52 | info!("Update check disabled."); 53 | return; 54 | } 55 | 56 | let now = UpdateCheckTimestamp::now(); 57 | let last_check = UpdateCheckTimestamp::read_config(workspace).unwrap(); 58 | 59 | if force_update || last_check.is_expired(&now) { 60 | let updates = fetch_updates(); 61 | 62 | if let Err(e) = updates { 63 | error!("Error checking for updates: {}", e); 64 | return; 65 | } 66 | 67 | match updates.unwrap() { 68 | UpdateStatus::NoUpdates => { 69 | info!("No updates available."); 70 | 71 | now.save_config(workspace).unwrap_or_else(|e| { 72 | error!("Error saving last update check timestamp: {:?}", e) 73 | }); 74 | } 75 | UpdateStatus::Update(new_rel) => { 76 | println!( 77 | "{}", 78 | format!( 79 | "New version {} is available!!\nDownload at {}", 80 | new_rel.version, new_rel.link 81 | ) 82 | .bright_yellow() 83 | ) 84 | } 85 | } 86 | } 87 | } 88 | 89 | fn fetch_updates() -> error::Result { 90 | let owner = env::var("GITHUB_USER").unwrap_or_else(|_| "hjaremko".to_string()); 91 | let repo = env::var("GITHUB_REPO").unwrap_or_else(|_| "baca-cli".to_string()); 92 | 93 | let gh_service = GithubReleases::new(&owner, &repo); 94 | let checker = UpdateChecker::new(gh_service, update::CURRENT_VERSION); 95 | checker.check_for_updates() 96 | } 97 | -------------------------------------------------------------------------------- /src/model/results.rs: -------------------------------------------------------------------------------- 1 | use crate::model::Submit; 2 | 3 | #[derive(Debug, PartialEq, Clone, Default)] 4 | pub struct Results { 5 | pub submits: Vec, 6 | } 7 | 8 | impl Results { 9 | pub fn new(submits: Vec) -> Self { 10 | Self { submits } 11 | } 12 | 13 | pub fn print(&self, amount: usize) { 14 | self.submits.iter().take(amount).for_each(|s| s.print()); 15 | } 16 | 17 | pub fn filter_by_task(&self, task_name: &str) -> Results { 18 | Results { 19 | submits: self 20 | .submits 21 | .iter() 22 | .filter(|submit| submit.problem_name == task_name) 23 | .cloned() 24 | .collect(), 25 | } 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | use crate::model::SubmitStatus; 33 | 34 | fn make_mock_submit(name: &str) -> Submit { 35 | Submit { 36 | status: SubmitStatus::Processing, 37 | points: 0.0, 38 | lateness: None, 39 | accepted: 0, 40 | size: 123, 41 | timestamp: "2002".to_string(), 42 | language: "Bash".to_string(), 43 | id: "".to_string(), 44 | max_points: None, 45 | problem_name: name.to_string(), 46 | link: "www.baca.pl".to_string(), 47 | test_results: None, 48 | } 49 | } 50 | 51 | #[test] 52 | fn filter_empty_results() { 53 | let expected = Results::default(); 54 | let actual = Results::default().filter_by_task("1"); 55 | 56 | assert_eq!(actual, expected); 57 | } 58 | 59 | #[test] 60 | fn filter_single_log_no_matches() { 61 | let data = Results::new(vec![make_mock_submit("1")]); 62 | let expected = Results::default(); 63 | let actual = data.filter_by_task("2"); 64 | 65 | assert_eq!(actual, expected); 66 | } 67 | 68 | #[test] 69 | fn filter_single_log_match() { 70 | let expected = Results::new(vec![make_mock_submit("1")]); 71 | let actual = expected.filter_by_task("1"); 72 | 73 | assert_eq!(actual, expected); 74 | } 75 | 76 | #[test] 77 | fn filter() { 78 | let data = Results::new(vec![ 79 | make_mock_submit("1"), 80 | make_mock_submit("2"), 81 | make_mock_submit("3"), 82 | make_mock_submit("1"), 83 | make_mock_submit("1"), 84 | make_mock_submit("1"), 85 | make_mock_submit("2"), 86 | make_mock_submit("2"), 87 | make_mock_submit("4"), 88 | make_mock_submit("1"), 89 | ]); 90 | let expected = Results::new(vec![ 91 | make_mock_submit("1"), 92 | make_mock_submit("1"), 93 | make_mock_submit("1"), 94 | make_mock_submit("1"), 95 | make_mock_submit("1"), 96 | ]); 97 | let actual = data.filter_by_task("1"); 98 | 99 | assert_eq!(actual, expected); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/command/tasks.rs: -------------------------------------------------------------------------------- 1 | use crate::api::baca_api::BacaApi; 2 | use crate::command::Command; 3 | use crate::error::Result; 4 | use crate::workspace::{ConfigObject, ConnectionConfig, Workspace}; 5 | use tracing::info; 6 | 7 | pub struct Tasks {} 8 | 9 | impl Tasks { 10 | pub fn new() -> Self { 11 | Tasks {} 12 | } 13 | } 14 | 15 | impl Command for Tasks { 16 | fn execute(self, workspace: &W, api: &A) -> Result<()> 17 | where 18 | W: Workspace, 19 | A: BacaApi, 20 | { 21 | info!("Getting all tasks."); 22 | let connection_config = ConnectionConfig::read_config(workspace)?; 23 | let tasks = api.get_tasks(&connection_config)?; 24 | 25 | tasks.print(); 26 | Ok(()) 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | use crate::api::baca_api::MockBacaApi; 34 | use crate::model; 35 | use crate::model::{Language, Task}; 36 | use crate::workspace::{ConnectionConfig, MockWorkspace}; 37 | 38 | #[test] 39 | fn success_test() { 40 | let mut mock_workspace = MockWorkspace::new(); 41 | mock_workspace 42 | .expect_read_config_object() 43 | .returning(|| Ok(ConnectionConfig::default())); 44 | 45 | let mut mock_api = MockBacaApi::new(); 46 | mock_api 47 | .expect_get_tasks() 48 | .once() 49 | .withf(|x| *x == ConnectionConfig::default()) 50 | .returning(|_| { 51 | Ok(model::Tasks { 52 | tasks: vec![ 53 | Task { 54 | id: "1".to_string(), 55 | language: Language::Unsupported, 56 | problem_name: "Test 1".to_string(), 57 | overall_oks: 5, 58 | }, 59 | Task { 60 | id: "2".to_string(), 61 | language: Language::CppWithFileSupport, 62 | problem_name: "Test 2".to_string(), 63 | overall_oks: 4, 64 | }, 65 | Task { 66 | id: "3".to_string(), 67 | language: Language::Cpp, 68 | problem_name: "Test 3".to_string(), 69 | overall_oks: 3, 70 | }, 71 | Task { 72 | id: "4".to_string(), 73 | language: Language::Ada, 74 | problem_name: "Test 4".to_string(), 75 | overall_oks: 2, 76 | }, 77 | Task { 78 | id: "5".to_string(), 79 | language: Language::Bash, 80 | problem_name: "Test 5".to_string(), 81 | overall_oks: 1, 82 | }, 83 | ], 84 | }) 85 | }); 86 | 87 | let tasks = Tasks::new(); 88 | let result = tasks.execute(&mock_workspace, &mock_api); 89 | assert!(result.is_ok()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/command/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::api::baca_api::BacaApi; 2 | use crate::cli::Commands; 3 | use crate::command::details::Details; 4 | use crate::command::init::Init; 5 | use crate::command::last::Last; 6 | use crate::command::log::Log; 7 | use crate::command::refresh::Refresh; 8 | use crate::command::submit::{SaveSwitch, Submit, SubmitSubcommand}; 9 | use crate::command::tasks::Tasks; 10 | use crate::error; 11 | use crate::workspace::config_editor::ConfigEditor; 12 | use crate::workspace::{ConnectionConfig, SubmitConfig, Workspace}; 13 | 14 | mod details; 15 | mod init; 16 | mod last; 17 | mod log; 18 | mod prompt; 19 | mod refresh; 20 | mod submit; 21 | mod tasks; 22 | 23 | pub trait Command { 24 | fn execute(self, workspace: &W, api: &A) -> error::Result<()>; 25 | } 26 | 27 | pub(crate) fn execute(workspace: &W, api: &Api, commands: &Commands) -> error::Result<()> 28 | where 29 | W: Workspace, 30 | Api: BacaApi, 31 | { 32 | match commands { 33 | Commands::Init { 34 | host, 35 | login, 36 | password, 37 | } => Init::new(host.clone(), login.clone(), password.clone()).execute(workspace, api), 38 | Commands::Details { submit_id } => { 39 | Details::new(&submit_id.to_string()).execute(workspace, api) 40 | } 41 | Commands::Refresh {} => Refresh::new().execute(workspace, api), 42 | Commands::Log { amount, task } => { 43 | let log = Log::new(&amount.to_string(), task); 44 | log.execute(workspace, api) 45 | } 46 | Commands::Tasks {} => Tasks::new().execute(workspace, api), 47 | Commands::Submit { 48 | task, 49 | file, 50 | language, 51 | rename, 52 | save, 53 | zip, 54 | no_save, 55 | no_main, 56 | no_polish, 57 | skip_header, 58 | command, 59 | } => { 60 | let subcommand = SubmitSubcommand::from(command); 61 | let save_switch = SaveSwitch::new(*save, *no_save); 62 | let mut provided_config = SubmitConfig { 63 | file: None, 64 | language: match language { 65 | None => None, 66 | Some(lang_str) => Some(lang_str.parse()?), 67 | }, 68 | id: task.map(|x| x.to_string()), 69 | rename_as: rename.clone(), 70 | to_zip: *zip, 71 | no_main: *no_main, 72 | no_polish: *no_polish, 73 | skip_header: *skip_header, 74 | }; 75 | provided_config.try_set_file(file.as_ref())?; 76 | 77 | Submit { 78 | subcommand, 79 | save_switch, 80 | provided_config, 81 | } 82 | .execute(workspace, api) 83 | } 84 | Commands::Last { task } => { 85 | let task = if let Some(task_id) = task { 86 | Last::with_filter(task_id.to_string()) 87 | } else { 88 | Last::new() 89 | }; 90 | task.execute(workspace, api) 91 | } 92 | Commands::Config {} => { 93 | ConfigEditor::new().edit::(workspace)?; 94 | Ok(()) 95 | } 96 | Commands::Clear {} => workspace.remove_workspace(), 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/util/mod.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | use assert_fs::fixture::ChildPath; 3 | use assert_fs::prelude::{FileTouch, FileWriteStr, PathChild}; 4 | use assert_fs::TempDir; 5 | use predicates::prelude::predicate; 6 | use std::env; 7 | use std::error::Error; 8 | use std::path::Path; 9 | 10 | pub trait BacaDirectoryPaths { 11 | fn baca_config_file_path(&self) -> Box; 12 | fn baca_submit_config_file_path(&self) -> Box; 13 | } 14 | 15 | impl BacaDirectoryPaths for TempDir { 16 | fn baca_config_file_path(&self) -> Box { 17 | self.path().join(".baca/connection").into_boxed_path() 18 | } 19 | 20 | fn baca_submit_config_file_path(&self) -> Box { 21 | self.path().join(".baca/submit").into_boxed_path() 22 | } 23 | } 24 | 25 | pub fn set_up_command(dir: &TempDir) -> Result> { 26 | let mut cmd = Command::cargo_bin("baca")?; 27 | cmd.current_dir(dir); 28 | cmd.arg("-uv"); 29 | Ok(cmd) 30 | } 31 | 32 | pub fn set_up_with_dir() -> Result<(TempDir, Command), Box> { 33 | let dir = assert_fs::TempDir::new()?; 34 | let cmd = set_up_command(&dir)?; 35 | Ok((dir, cmd)) 36 | } 37 | 38 | pub fn assert_contains_pattern(command: &[&str], pattern: &str) -> Result<(), Box> { 39 | let (dir, mut cmd) = set_up_with_dir()?; 40 | 41 | cmd.args(command); 42 | cmd.assert() 43 | // .failure() // todo: exit codes 44 | .stdout(predicate::str::contains(pattern)); 45 | 46 | dir.close()?; 47 | Ok(()) 48 | } 49 | 50 | pub fn assert_fails_if_not_initialized(command: &[&str]) -> Result<(), Box> { 51 | let pattern = "not initialized"; 52 | assert_contains_pattern(command, pattern) 53 | } 54 | 55 | pub fn initialize_correct_workspace() -> Result> { 56 | let (login, pass, host) = get_baca_credentials(); 57 | let (dir, mut cmd) = set_up_with_dir()?; 58 | 59 | cmd.arg("init") 60 | .args(["--host", &host, "-p", &pass, "-l", &login]); 61 | cmd.assert(); 62 | Ok(dir) 63 | } 64 | 65 | pub fn make_input_file_cpp(dir: &TempDir) -> Result> { 66 | let input_file = dir.child("source.cpp"); 67 | input_file.touch()?; 68 | input_file.write_str( 69 | r#"// Hubert Jaremko 70 | #include 71 | int main() { 72 | std::cout << "Hello world" << std::endl; 73 | return 0; 74 | } 75 | "#, 76 | )?; 77 | Ok(input_file) 78 | } 79 | 80 | pub fn make_input_file_dummy(dir: &TempDir) -> Result> { 81 | let input_file = dir.child("dummy.txt"); 82 | input_file.touch()?; 83 | input_file.write_str( 84 | r#"// Hubert Jaremko 85 | Dummy text file 86 | "#, 87 | )?; 88 | Ok(input_file) 89 | } 90 | 91 | pub fn make_input_file_dummy_no_header( 92 | dir: &TempDir, 93 | ) -> Result> { 94 | let input_file = dir.child("dummy.txt"); 95 | input_file.touch()?; 96 | input_file.write_str( 97 | r#"Dummy text file 98 | "#, 99 | )?; 100 | Ok(input_file) 101 | } 102 | 103 | pub fn get_baca_credentials() -> (String, String, String) { 104 | let login = env::var("TEST_BACA_LOGIN").expect("No TEST_BACA_LOGIN provided"); 105 | let pass = env::var("TEST_BACA_PASSWORD").expect("No TEST_BACA_PASSWORD provided"); 106 | let host = env::var("TEST_BACA_HOST").expect("No TEST_BACA_HOST provided"); 107 | (login, pass, host) 108 | } 109 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | // todo: AppSettings::ArgRequiredElseHel 4 | #[derive(Parser)] 5 | #[command(author, version, about, long_about = None)] 6 | pub(crate) struct Cli { 7 | /// Sets the level of log verbosity 8 | #[arg(short, long, action = clap::ArgAction::Count)] 9 | pub verbose: u8, 10 | 11 | /// Disable update check 12 | #[arg(short = 'u', long)] 13 | pub no_update: bool, 14 | 15 | /// Force update check 16 | #[arg(short = 'U', long)] 17 | pub force_update: bool, 18 | 19 | #[command(subcommand)] 20 | pub command: Option, 21 | } 22 | 23 | #[derive(Subcommand)] 24 | pub(crate) enum Commands { 25 | /// Initialise the current directory as a BaCa workspace 26 | Init { 27 | /// BaCa hostname, ex. mn2020 28 | #[arg(long)] 29 | host: Option, 30 | 31 | /// BaCa login 32 | #[arg(long, short)] 33 | login: Option, 34 | 35 | /// BaCa password 36 | #[arg(long, short)] 37 | password: Option, 38 | }, 39 | 40 | /// Get submit details 41 | Details { submit_id: u32 }, 42 | 43 | /// Refresh session, use in case of a cookie expiration 44 | Refresh {}, 45 | 46 | /// Print the last N (default 3) submits 47 | Log { 48 | #[arg(default_value_t = 3)] 49 | amount: u16, 50 | 51 | /// Print only the specified task's logs, use 'baca tasks' to see what ids are available 52 | #[arg(long, short, value_name = "TASK_ID")] 53 | task: Option, 54 | }, 55 | 56 | /// Print available tasks 57 | Tasks {}, 58 | 59 | /// Make a submit 60 | Submit { 61 | /// Task id, use 'baca tasks' to see what ids are available, overrides saved task id 62 | #[arg(long, short, value_name = "TASK_ID")] 63 | task: Option, 64 | 65 | /// A file to submit, overrides saved path 66 | #[arg(short, long, value_name = "FILE")] 67 | file: Option, 68 | 69 | /// Task language. Please provide it exactly as is displayed on BaCa 70 | #[arg(short, long)] 71 | language: Option, 72 | 73 | /// Submit input file under different name 74 | #[arg(short, long, value_name = "NEW_NAME")] 75 | rename: Option, 76 | 77 | /// Save task config. If provided, future 'submit' calls won't require providing task config 78 | #[arg(short, long)] 79 | save: bool, 80 | 81 | /// Zip files to 'source.zip' before submitting, overrides saved config 82 | #[arg(short, long)] 83 | zip: bool, 84 | 85 | /// Do not ask for save 86 | #[arg(long)] 87 | no_save: bool, 88 | 89 | /// Remove main function before submitting. Takes effect only on C/C++ files 90 | #[arg(long)] 91 | no_main: bool, 92 | 93 | /// Transliterate Unicode strings in the input file into pure ASCII, effectively removing Polish diacritics 94 | #[arg(long)] 95 | no_polish: bool, 96 | 97 | /// Skip header verification 98 | #[arg(long)] 99 | skip_header: bool, 100 | 101 | #[command(subcommand)] 102 | command: Option, 103 | }, 104 | 105 | /// Print details of the last submit 106 | Last { 107 | /// Print only the specified task's logs, use 'baca tasks' to see what ids are available 108 | #[arg(long, short, value_name = "TASK_ID")] 109 | task: Option, 110 | }, 111 | 112 | /// Open a editor to edit BaCa configuration 113 | Config {}, 114 | 115 | /// Remove the whole `.baca` directory 116 | Clear {}, 117 | } 118 | 119 | #[derive(Subcommand)] 120 | pub(crate) enum SubmitCommands { 121 | /// Open a editor to edit submit config 122 | Config {}, 123 | 124 | /// Clear saved submit config 125 | Clear {}, 126 | } 127 | -------------------------------------------------------------------------------- /tests/update_timestamps/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::util::get_baca_credentials; 2 | use assert_cmd::Command; 3 | use assert_fs::TempDir; 4 | use predicates::prelude::*; 5 | 6 | #[test] 7 | #[ignore] 8 | fn update_check_timestamp_should_be_saved_if_no_update() -> Result<(), Box> { 9 | let (login, pass, host) = get_baca_credentials(); 10 | let temp = assert_fs::TempDir::new()?; 11 | 12 | let mut cmd = baca_verbose(&temp)?; 13 | cmd.arg("init") 14 | .args(["--host", &host, "-p", &pass, "-l", &login]); 15 | cmd.assert() 16 | .stdout(predicate::str::contains("Checking for updates")) 17 | .success(); 18 | 19 | let mut cmd = baca_verbose(&temp)?; 20 | cmd.assert() 21 | .stdout(predicate::str::contains("Checking for updates")) 22 | .success(); 23 | 24 | let mut cmd = baca_verbose(&temp)?; 25 | cmd.assert() 26 | .stdout(predicate::str::contains("Checking for updates").not()) 27 | .success(); 28 | 29 | temp.close()?; 30 | Ok(()) 31 | } 32 | 33 | #[test] 34 | #[ignore] 35 | fn update_check_timestamp_should_not_be_saved_if_update() -> Result<(), Box> 36 | { 37 | let (login, pass, host) = get_baca_credentials(); 38 | let temp = assert_fs::TempDir::new()?; 39 | 40 | let mut cmd = baca_verbose_dummy_repo(&temp)?; 41 | cmd.arg("init") 42 | .args(["--host", &host, "-p", &pass, "-l", &login]); 43 | cmd.assert() 44 | .stdout(predicate::str::contains("New version")) 45 | .success(); 46 | 47 | let mut cmd = baca_verbose_dummy_repo(&temp)?; 48 | cmd.assert() 49 | .stdout(predicate::str::contains("New version")) 50 | .success(); 51 | 52 | let mut cmd = baca_verbose_dummy_repo(&temp)?; 53 | cmd.assert() 54 | .stdout(predicate::str::contains("New version")) 55 | .success(); 56 | 57 | temp.close()?; 58 | Ok(()) 59 | } 60 | 61 | #[test] 62 | fn update_check_error_if_invalid_repo() -> Result<(), Box> { 63 | let temp = assert_fs::TempDir::new()?; 64 | let mut cmd = baca_verbose(&temp)?; 65 | cmd.env("GITHUB_REPO", "does_not_exists"); 66 | cmd.assert() 67 | .stdout(predicate::str::contains("Error checking for updates")) 68 | .success(); 69 | 70 | temp.close()?; 71 | Ok(()) 72 | } 73 | 74 | #[test] 75 | #[ignore] 76 | fn forced_update_check_should_be_triggered_despite_timestamp( 77 | ) -> Result<(), Box> { 78 | let (login, pass, host) = get_baca_credentials(); 79 | let temp = assert_fs::TempDir::new()?; 80 | 81 | // Save timestamp 82 | let mut cmd = baca_verbose(&temp)?; 83 | cmd.arg("init") 84 | .args(["--host", &host, "-p", &pass, "-l", &login]); 85 | cmd.assert() 86 | .stdout(predicate::str::contains("Checking for updates")) 87 | .success(); 88 | 89 | let mut cmd = baca_verbose(&temp)?; 90 | cmd.assert() 91 | .stdout(predicate::str::contains("Checking for updates")) 92 | .success(); 93 | 94 | let mut cmd = baca_verbose(&temp)?; 95 | cmd.assert() 96 | .stdout(predicate::str::contains("Checking for updates").not()) 97 | .success(); 98 | 99 | // Force update 100 | let mut cmd = baca_verbose(&temp)?; 101 | cmd.arg("-U"); 102 | cmd.assert() 103 | .stdout(predicate::str::contains("Checking for updates")) 104 | .success(); 105 | 106 | // Should not check again 107 | let mut cmd = baca_verbose(&temp)?; 108 | cmd.assert() 109 | .stdout(predicate::str::contains("Checking for updates").not()) 110 | .success(); 111 | 112 | temp.close()?; 113 | Ok(()) 114 | } 115 | 116 | fn baca_verbose(temp: &TempDir) -> Result> { 117 | let mut cmd = Command::cargo_bin("baca")?; 118 | cmd.current_dir(temp); 119 | cmd.arg("-v"); 120 | Ok(cmd) 121 | } 122 | 123 | fn baca_verbose_dummy_repo(temp: &TempDir) -> Result> { 124 | let mut cmd = baca_verbose(temp)?; 125 | cmd.env("GITHUB_REPO", "dummy"); 126 | Ok(cmd) 127 | } 128 | -------------------------------------------------------------------------------- /src/update/update_checker.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | use crate::update::update_status::UpdateStatus::{NoUpdates, Update}; 3 | use crate::update::{ReleaseService, UpdateStatus, CURRENT_VERSION}; 4 | 5 | use tracing::{debug, error, info}; 6 | 7 | pub struct UpdateChecker { 8 | release_service: T, 9 | current_version: String, 10 | } 11 | 12 | impl UpdateChecker { 13 | pub fn new(release_service: T, current_version: &str) -> Self { 14 | Self { 15 | release_service, 16 | current_version: current_version.to_string(), 17 | } 18 | } 19 | 20 | pub fn check_for_updates(&self) -> Result { 21 | info!("Checking for updates."); 22 | let last = self.release_service.get_last_release(); 23 | debug!("Update check result: {:?}", last); 24 | 25 | if let Err(e) = last { 26 | return match e { 27 | Error::NoRelease => Ok(NoUpdates), 28 | _ => { 29 | error!("{}", e); 30 | Err(e) 31 | } 32 | }; 33 | } 34 | 35 | let last = last.unwrap(); 36 | 37 | debug!("Current version: {}", CURRENT_VERSION); 38 | let res = if last.is_newer_than(&self.current_version) { 39 | info!("New version: {}", last.version); 40 | Update(last) 41 | } else { 42 | info!("No updates available."); 43 | NoUpdates 44 | }; 45 | Ok(res) 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | use crate::update::release_service::MockReleaseService; 53 | use crate::update::{BacaRelease, CURRENT_VERSION}; 54 | 55 | #[test] 56 | fn connection_error_should_return_error() { 57 | let mut mock = MockReleaseService::new(); 58 | mock.expect_get_last_release() 59 | .returning(|| Err(Error::FetchingRelease)); 60 | let checker = UpdateChecker::new(mock, CURRENT_VERSION); 61 | 62 | let actual = checker.check_for_updates(); 63 | 64 | if let Some(Error::FetchingRelease) = actual.err() { 65 | return; 66 | } 67 | panic!(); 68 | } 69 | 70 | #[test] 71 | fn no_releases_should_not_report_update() { 72 | let mut mock = MockReleaseService::new(); 73 | mock.expect_get_last_release() 74 | .returning(|| Err(Error::NoRelease)); 75 | let checker = UpdateChecker::new(mock, "v0.0.1"); 76 | let actual = checker.check_for_updates().unwrap(); 77 | 78 | assert_eq!(actual, UpdateStatus::NoUpdates); 79 | } 80 | 81 | #[test] 82 | fn release_up_to_date_should_not_report_update() { 83 | let mut mock = MockReleaseService::new(); 84 | mock.expect_get_last_release() 85 | .returning(|| Ok(BacaRelease::new("v0.0.1", "link"))); 86 | let checker = UpdateChecker::new(mock, "v0.0.1"); 87 | let actual = checker.check_for_updates().unwrap(); 88 | 89 | assert_eq!(actual, UpdateStatus::NoUpdates); 90 | } 91 | 92 | #[test] 93 | fn release_older_should_not_report_update() { 94 | let mut mock = MockReleaseService::new(); 95 | mock.expect_get_last_release() 96 | .returning(|| Ok(BacaRelease::new("v0.0.1", "link"))); 97 | let checker = UpdateChecker::new(mock, "v0.0.2"); 98 | let actual = checker.check_for_updates().unwrap(); 99 | 100 | assert_eq!(actual, UpdateStatus::NoUpdates); 101 | } 102 | 103 | #[test] 104 | fn release_newer_should_report_update() { 105 | let mut mock = MockReleaseService::new(); 106 | mock.expect_get_last_release() 107 | .returning(|| Ok(BacaRelease::new("v0.0.2", "link"))); 108 | let checker = UpdateChecker::new(mock, "v0.0.1"); 109 | let actual = checker.check_for_updates().unwrap(); 110 | 111 | if let UpdateStatus::Update(new_release) = actual { 112 | assert_eq!(new_release.version, "v0.0.2"); 113 | return; 114 | } 115 | panic!(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/model/submit.rs: -------------------------------------------------------------------------------- 1 | use crate::model::submit_status::SubmitStatus; 2 | use crate::model::TestResults; 3 | use colored::*; 4 | 5 | #[derive(Debug, PartialEq, Clone, Default)] 6 | pub struct Submit { 7 | // problem: Problem, // todo: Task here 8 | pub status: SubmitStatus, 9 | pub points: f32, 10 | pub lateness: Option, 11 | pub accepted: i32, 12 | pub size: i32, 13 | pub timestamp: String, 14 | pub language: String, 15 | pub id: String, 16 | pub max_points: Option, 17 | pub problem_name: String, 18 | pub link: String, 19 | pub test_results: Option>, 20 | } 21 | 22 | impl Submit { 23 | // todo: ctor 24 | pub fn print_with_tests(&self) { 25 | self.print(); 26 | 27 | if self.test_results.is_none() { 28 | return; 29 | } 30 | 31 | let test_results = self.test_results.as_ref().unwrap(); 32 | 33 | let first = test_results.first().unwrap(); 34 | let first_str = format!(" ── {} - {:?}", first.name, first.status); 35 | let first_str = add_emoji(&first_str, &first.status); 36 | let first_str = apply_color_according_to_status(&first_str, &first.status); 37 | println!("{}", first_str); 38 | 39 | if test_results.len() > 2 { 40 | let mid = test_results; 41 | let mid = &mid[1..mid.len() - 1]; 42 | for test in mid { 43 | let test_str = format!(" ── {} - {:?}", test.name, test.status); 44 | let test_str = add_emoji(&test_str, &test.status); 45 | let test_str = apply_color_according_to_status(&test_str, &test.status); 46 | println!("{}", test_str); 47 | } 48 | } 49 | 50 | if test_results.len() > 3 { 51 | let last = test_results.last().unwrap(); 52 | let last_str = format!(" ── {} - {:?}", last.name, last.status); 53 | let last_str = add_emoji(&last_str, &last.status); 54 | let last_str = apply_color_according_to_status(&last_str, &last.status); 55 | println!("{}", last_str); 56 | } 57 | } 58 | 59 | pub fn print(&self) { 60 | let header_line = self.make_header_line(); 61 | let status_line = self.make_status_line(); 62 | let link_line = self.make_link_line(); 63 | 64 | let submit_info = format!("{}\n{}\n{}", header_line, status_line, link_line); 65 | let submit_info = apply_color_according_to_status(&submit_info, &self.status); 66 | 67 | println!("\n{}", submit_info); 68 | } 69 | 70 | fn make_link_line(&self) -> String { 71 | format!("└─── {}", self.link) 72 | } 73 | 74 | fn make_status_line(&self) -> String { 75 | match self.max_points { 76 | None => format!( 77 | "├─── {}% - {} pts - {:?}", 78 | self.accepted, self.points, self.status 79 | ), 80 | Some(max) => format!( 81 | "├─── {}% - {}/{} pts - {:?}", 82 | self.accepted, self.points, max, self.status 83 | ), 84 | } 85 | } 86 | 87 | fn make_header_line(&self) -> String { 88 | format!( 89 | "● {} - {} - {} - submit {}", 90 | self.problem_name, self.language, self.timestamp, self.id 91 | ) 92 | } 93 | } 94 | 95 | fn add_emoji(str: &str, status: &SubmitStatus) -> String { 96 | match status { 97 | SubmitStatus::Ok => format!(" ✅{}", str), 98 | _ => format!(" ❌{}", str), 99 | } 100 | } 101 | 102 | fn apply_color_according_to_status(str: &str, status: &SubmitStatus) -> ColoredString { 103 | match status { 104 | SubmitStatus::Ok => str.green().bold(), 105 | SubmitStatus::Processing => str.bright_yellow().bold(), 106 | SubmitStatus::InQueue => str.bright_yellow().bold(), 107 | SubmitStatus::WrongAnswer => str.yellow().bold(), 108 | SubmitStatus::TimeExceeded => str.yellow().bold(), 109 | SubmitStatus::CompileError => str.yellow().bold(), 110 | SubmitStatus::NoHeader => str.blue().bold(), 111 | SubmitStatus::RealTimeExceeded => str.yellow().bold(), 112 | SubmitStatus::ManuallyRejected => str.magenta().bold(), 113 | SubmitStatus::RuntimeError => str.yellow().bold(), 114 | SubmitStatus::InternalError => str.red().bold(), 115 | SubmitStatus::OutputSizeExceeded => str.yellow().bold(), 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/update/github_releases.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | use crate::update::{BacaRelease, ReleaseService}; 3 | use serde_json::Value; 4 | use std::env; 5 | use tracing::{debug, info}; 6 | 7 | pub struct GithubReleases { 8 | owner: String, 9 | repo: String, 10 | } 11 | 12 | impl GithubReleases { 13 | pub fn new(owner: &str, repo: &str) -> Self { 14 | info!("Creating GitHub API service to repo {}/{}", owner, repo); 15 | GithubReleases { 16 | owner: owner.to_string(), 17 | repo: repo.to_string(), 18 | } 19 | } 20 | } 21 | 22 | impl ReleaseService for GithubReleases { 23 | fn get_last_release(&self) -> Result { 24 | let client = reqwest::blocking::ClientBuilder::new() 25 | .user_agent(make_user_agent()) 26 | .build()?; 27 | let mut request_builder = client.get(format!( 28 | "https://api.github.com/repos/{}/{}/releases", 29 | self.owner, self.repo 30 | )); 31 | 32 | if let Ok(auth_token) = env::var("AUTH_TOKEN") { 33 | info!("Auth token present, setting auth header."); 34 | request_builder = request_builder.header( 35 | reqwest::header::AUTHORIZATION, 36 | format!("token {}", auth_token), 37 | ); 38 | } 39 | 40 | let response = request_builder.send(); 41 | 42 | debug!("{:?}", response); 43 | let response = response?.text()?; 44 | debug!("{:?}", response); 45 | 46 | if response.contains("API rate limit exceeded") { 47 | return Err(Error::ApiRateLimitExceeded); 48 | } 49 | 50 | if response.contains("Not Found") { 51 | return Err(Error::FetchingRelease); 52 | } 53 | 54 | let v: Value = serde_json::from_str(&response)?; 55 | let ver = &v[0]["tag_name"]; 56 | let link = &v[0]["html_url"]; 57 | 58 | if ver.is_null() || link.is_null() { 59 | return Err(Error::NoRelease); 60 | } 61 | 62 | let ver = ver.as_str().unwrap(); 63 | let ver = &ver[1..]; 64 | Ok(BacaRelease::new(ver, link.as_str().unwrap())) 65 | } 66 | } 67 | 68 | fn make_user_agent() -> String { 69 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 70 | format!("baca_cli/{}", VERSION) 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::*; 76 | 77 | fn assert_fetching_release_error(actual: Result) { 78 | let assert_err = |e| match e { 79 | Error::ApiRateLimitExceeded => println!("API limit exceeded!"), 80 | Error::FetchingRelease => (), 81 | _ => panic!("Unexpected error: {:?}", e), 82 | }; 83 | 84 | assert_error(actual, assert_err); 85 | } 86 | 87 | fn assert_no_release_error(actual: Result) { 88 | let assert_err = |e| match e { 89 | Error::ApiRateLimitExceeded => println!("API limit exceeded!"), 90 | Error::NoRelease => (), 91 | _ => panic!("Unexpected error: {:?}", e), 92 | }; 93 | 94 | assert_error(actual, assert_err); 95 | } 96 | 97 | fn assert_error(actual: Result, assert_err: fn(Error)) { 98 | match actual { 99 | Ok(r) => { 100 | panic!("Unexpected success: {:?}", r) 101 | } 102 | Err(e) => assert_err(e), 103 | } 104 | } 105 | 106 | #[test] 107 | fn invalid_repo_should_return_error() { 108 | let gh = GithubReleases::new("hjaremko", "invalid"); 109 | let actual = gh.get_last_release(); 110 | 111 | assert_fetching_release_error(actual) 112 | } 113 | 114 | #[test] 115 | fn invalid_owner_should_return_error() { 116 | let gh = GithubReleases::new("invalid", "baca-cli"); 117 | let actual = gh.get_last_release(); 118 | 119 | assert_fetching_release_error(actual); 120 | } 121 | 122 | #[test] 123 | fn correct_repo_should_return_latest_release() { 124 | let gh = GithubReleases::new("hjaremko", "baca-cli"); 125 | let actual = gh.get_last_release(); 126 | 127 | if let Err(e) = actual { 128 | match e { 129 | Error::ApiRateLimitExceeded => (), 130 | _ => panic!("Unexpected error: {:?}", e), 131 | } 132 | } 133 | } 134 | 135 | #[test] 136 | fn correct_repo_with_no_releases_should_return_error() { 137 | let gh = GithubReleases::new("hjaremko", "fi"); 138 | let actual = gh.get_last_release(); 139 | 140 | assert_no_release_error(actual) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/workspace/connection_config.rs: -------------------------------------------------------------------------------- 1 | use crate::api; 2 | use crate::error::Error; 3 | use crate::error::Result; 4 | use crate::workspace::{ConfigObject, Workspace}; 5 | use serde::{Deserialize, Serialize}; 6 | use tracing::error; 7 | 8 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] 9 | pub struct ConnectionConfig { 10 | pub host: String, 11 | pub login: String, 12 | pub password: String, 13 | pub permutation: String, 14 | pub cookie: String, 15 | } 16 | 17 | impl ConnectionConfig { 18 | pub fn credentials(&self) -> (String, String) { 19 | (self.login.clone(), self.password.clone()) 20 | } 21 | 22 | pub fn make_url(&self) -> String { 23 | format!("https://{}/{}", api::details::SERVER_URL, self.host) 24 | } 25 | 26 | pub fn make_module_base(&self) -> String { 27 | format!("{}/testerka_gwt/", self.make_url()) 28 | } 29 | 30 | pub fn make_payload(&self, req_type: &api::RequestType) -> String { 31 | use dyn_fmt::AsStrFormatExt; 32 | 33 | req_type.payload_template().format(&[ 34 | self.make_module_base(), 35 | self.login.clone(), 36 | self.password.clone(), 37 | ]) 38 | } 39 | 40 | pub fn make_cookie(&self) -> String { 41 | format!("JSESSIONID={};", self.cookie) 42 | } 43 | } 44 | 45 | impl ConfigObject for ConnectionConfig { 46 | fn save_config(&self, workspace: &W) -> Result<()> { 47 | workspace.save_config_object(self).map_err(|e| { 48 | error!("{:?}", e); 49 | match e { 50 | Error::WorkspaceNotInitialized => e, 51 | _ => Error::WorkspaceCorrupted, 52 | } 53 | }) 54 | } 55 | 56 | fn read_config(workspace: &W) -> Result { 57 | workspace.read_config_object::().map_err(|e| { 58 | error!("{:?}", e); 59 | match e { 60 | Error::WorkspaceNotInitialized => e, 61 | _ => Error::WorkspaceCorrupted, 62 | } 63 | }) 64 | } 65 | 66 | fn remove_config(workspace: &W) -> Result<()> { 67 | workspace.remove_config_object::().map_err(|e| { 68 | error!("{:?}", e); 69 | match e { 70 | Error::WorkspaceNotInitialized => e, 71 | _ => Error::WorkspaceCorrupted, 72 | } 73 | }) 74 | } 75 | 76 | fn config_filename() -> String { 77 | "connection".to_string() 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | use crate::workspace::workspace_dir::tests::{make_baca, make_temp_workspace}; 85 | use crate::workspace::{ConfigObject, ConnectionConfig}; 86 | use predicates::prelude::*; 87 | 88 | #[test] 89 | fn save_read_success() { 90 | let (temp_dir, _, workspace) = make_temp_workspace().unwrap(); 91 | let baca = make_baca(); 92 | 93 | workspace.initialize().unwrap(); 94 | baca.save_config(&workspace).unwrap(); 95 | 96 | let result = ConnectionConfig::read_config(&workspace).unwrap(); 97 | assert_eq!(result, baca); 98 | temp_dir.close().unwrap(); 99 | } 100 | 101 | #[test] 102 | fn save_not_initialized() { 103 | let (temp_dir, mock_paths, workspace) = make_temp_workspace().unwrap(); 104 | 105 | let baca = make_baca(); 106 | let result = baca.save_config(&workspace); 107 | 108 | assert!(result.is_err()); 109 | if let Err(e) = result { 110 | assert!(matches!(e, Error::WorkspaceNotInitialized)); 111 | } 112 | assert!( 113 | predicate::path::missing().eval(mock_paths.config_path::().as_path()) 114 | ); 115 | temp_dir.close().unwrap(); 116 | } 117 | 118 | #[test] 119 | fn save_should_override() { 120 | let (temp_dir, _, workspace) = make_temp_workspace().unwrap(); 121 | let baca_first = make_baca(); 122 | let mut baca_second = make_baca(); 123 | baca_second.host = "other_host".to_string(); 124 | 125 | workspace.initialize().unwrap(); 126 | baca_first.save_config(&workspace).unwrap(); 127 | assert_eq!( 128 | ConnectionConfig::read_config(&workspace).unwrap(), 129 | baca_first 130 | ); 131 | baca_second.save_config(&workspace).unwrap(); 132 | assert_eq!( 133 | ConnectionConfig::read_config(&workspace).unwrap(), 134 | baca_second 135 | ); 136 | 137 | temp_dir.close().unwrap(); 138 | } 139 | // todo: tests for removing and saving objects 140 | } 141 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error::{Network, Other, WorkspaceCorrupted}; 2 | use std::{fmt, io}; 3 | use tracing::error; 4 | 5 | pub type Result = std::result::Result; 6 | 7 | #[derive(Debug)] 8 | pub enum Error { 9 | Other(Box), 10 | Network(Box), 11 | CreatingWorkspace(Box), 12 | RemovingWorkspace(Box), 13 | RemovingConfig(Box), 14 | ReadingConfig(Box), 15 | SavingConfig(Box), 16 | ReadingSource(Box), 17 | Zipping(Box), 18 | WorkspaceNotInitialized, 19 | WorkspaceCorrupted, 20 | WorkspaceAlreadyInitialized, 21 | InvalidSubmitId, 22 | LoggedOut, 23 | TaskNotActive, 24 | InvalidTaskId(String), 25 | InvalidHost, 26 | InvalidLoginOrPassword, 27 | FetchingRelease, 28 | NoRelease, 29 | ApiRateLimitExceeded, 30 | InvalidArgument, 31 | UnsupportedLanguage(String), 32 | NoSubmitsYet, 33 | EditorFail(i32), 34 | SubmitArgumentNotProvided(String), 35 | InputFileDoesNotExist, 36 | NoHeader, 37 | } 38 | 39 | impl std::error::Error for Error {} 40 | 41 | impl fmt::Display for Error { 42 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 43 | let msg = match self { 44 | Error::Other(e) => format!("Error: {}", e), 45 | Error::Network(e) => format!("Network error: {}", e), 46 | Error::CreatingWorkspace(e) => format!("Error creating workspace directory: {}", e), 47 | Error::RemovingWorkspace(e) => format!("Error removing workspace directory: {}", e), 48 | Error::RemovingConfig(e) => format!("Error removing config: {}", e), 49 | Error::ReadingConfig(e) => format!("Error reading config: {}", e), 50 | Error::SavingConfig(e) => format!("Error saving config: {}", e), 51 | Error::ReadingSource(e) => format!("Error reading source file: {}", e), 52 | Error::Zipping(e) => format!("Error zipping! Error: {}", e), 53 | Error::WorkspaceNotInitialized => "Baca is not initialized! Type 'baca init --help' for more information.".to_owned(), 54 | Error::WorkspaceCorrupted => "Workspace corrupted, please delete .baca directory and initialize again.".to_owned(), 55 | Error::WorkspaceAlreadyInitialized => "Baca already initialized. Remove '.baca' directory if you want to change config or edit it manually.".to_owned(), 56 | Error::InvalidSubmitId => "Invalid submit id.".to_owned(), 57 | Error::LoggedOut => "The session cookie has expired, type 'baca refresh' to re-log and try again.".to_owned(), 58 | Error::TaskNotActive => "Error sending submit. Is the task still active?".to_owned(), 59 | Error::InvalidTaskId(id) => format!("Task no. {} does not exist.", id), 60 | Error::InvalidHost => "Invalid host provided. Example: for baca url 'https://baca.ii.uj.edu.pl/mn2021/', the host is 'mn2021'.".to_owned(), 61 | Error::InvalidLoginOrPassword => "Invalid login or password!".to_owned(), 62 | Error::FetchingRelease => "Error fetching releases.".to_owned(), 63 | Error::NoRelease => "No releases available.".to_owned(), 64 | Error::ApiRateLimitExceeded => "GitHub API rate limit exceeded. Try again later.".to_owned(), 65 | Error::InvalidArgument => "Invalid argument.".to_owned(), 66 | Error::UnsupportedLanguage(lang) => format!("{} is not yet supported!! Please create an issue at https://github.com/hjaremko/baca-cli/issues", lang), 67 | Error::NoSubmitsYet => "No submits yet!".to_owned(), 68 | Error::InputFileDoesNotExist => "Provided input file does not exist!".to_owned(), 69 | Error::EditorFail(code) => format!("Config editor failed with exit code: {}", code), 70 | Error::SubmitArgumentNotProvided(argument) => format!("Please provide a {}. Type 'baca submit -h' for more info.", argument), 71 | Error::NoHeader => "No header!".to_owned(), 72 | }; 73 | 74 | write!(f, "{}", msg) 75 | } 76 | } 77 | 78 | impl From for Error { 79 | fn from(e: serde_json::Error) -> Self { 80 | error!("{}", e); 81 | Other(e.into()) 82 | } 83 | } 84 | 85 | impl From for Error { 86 | fn from(e: serde_yaml::Error) -> Self { 87 | error!("{}", e); 88 | WorkspaceCorrupted 89 | } 90 | } 91 | 92 | impl From for Error { 93 | fn from(e: reqwest::Error) -> Self { 94 | error!("{}", e); 95 | Network(e.into()) 96 | } 97 | } 98 | 99 | impl From for Error { 100 | fn from(e: io::Error) -> Self { 101 | error!("{}", e); 102 | Other(e.into()) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/update/update_check_timestamp.rs: -------------------------------------------------------------------------------- 1 | use crate::workspace::{ConfigObject, Workspace}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt::Debug; 4 | use time::OffsetDateTime; 5 | use tracing::debug; 6 | 7 | const TIMESTAMP_FILENAME: &str = "update_timestamp"; 8 | 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct UpdateCheckTimestamp { 11 | timestamp: OffsetDateTime, 12 | } 13 | 14 | impl Default for UpdateCheckTimestamp { 15 | fn default() -> Self { 16 | Self { 17 | timestamp: OffsetDateTime::from_unix_timestamp(0).unwrap(), 18 | } 19 | } 20 | } 21 | 22 | impl From for UpdateCheckTimestamp { 23 | fn from(unix_timestamp: i64) -> Self { 24 | Self { 25 | timestamp: OffsetDateTime::from_unix_timestamp(unix_timestamp).unwrap(), 26 | } 27 | } 28 | } 29 | 30 | impl UpdateCheckTimestamp { 31 | pub fn now() -> Self { 32 | let timestamp = OffsetDateTime::now_utc(); 33 | Self { timestamp } 34 | } 35 | 36 | pub fn is_expired(&self, other: &Self) -> bool { 37 | let saved = self.get_timestamp(); 38 | let now = other.get_timestamp(); 39 | let diff = now - saved; 40 | debug!("Expired for {} days", diff.whole_days()); 41 | diff.whole_days() >= 1 42 | } 43 | 44 | pub fn get_timestamp(&self) -> OffsetDateTime { 45 | self.timestamp 46 | } 47 | } 48 | 49 | impl ConfigObject for UpdateCheckTimestamp { 50 | fn save_config(&self, workspace: &W) -> crate::error::Result<()> { 51 | workspace.save_config_object(self) 52 | } 53 | 54 | fn read_config(workspace: &W) -> crate::error::Result { 55 | Ok(workspace.read_config_object::().unwrap_or_default()) 56 | } 57 | 58 | fn remove_config(_workspace: &W) -> crate::error::Result<()> { 59 | unimplemented!() 60 | } 61 | 62 | fn config_filename() -> String { 63 | TIMESTAMP_FILENAME.to_string() 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use super::*; 70 | use crate::error::Error; 71 | 72 | use crate::workspace::MockWorkspace; 73 | 74 | type Timestamp = UpdateCheckTimestamp; 75 | 76 | #[test] 77 | fn no_saved_info_should_return_true() { 78 | let mut mock_workspace = MockWorkspace::new(); 79 | mock_workspace 80 | .expect_read_config_object::() 81 | .returning(|| Err(Error::WorkspaceCorrupted)); 82 | 83 | let now = UpdateCheckTimestamp::now(); 84 | let saved = UpdateCheckTimestamp::read_config(&mock_workspace).unwrap(); 85 | 86 | assert!(saved.is_expired(&now)) 87 | } 88 | 89 | #[test] 90 | fn no_saved_info_get_timestamp_should_return_default() { 91 | let mut mock_workspace = MockWorkspace::new(); 92 | mock_workspace 93 | .expect_read_config_object::() 94 | .returning(|| Err(Error::WorkspaceCorrupted)); //todo 95 | 96 | let saved = UpdateCheckTimestamp::read_config(&mock_workspace).unwrap(); 97 | assert_eq!( 98 | saved.get_timestamp().unix_timestamp(), 99 | UpdateCheckTimestamp::default() 100 | .get_timestamp() 101 | .unix_timestamp() 102 | ) 103 | } 104 | 105 | #[test] 106 | fn save_timestamp_should_save() { 107 | let mut mock_workspace = MockWorkspace::new(); 108 | mock_workspace 109 | .expect_save_config_object::() 110 | .withf(|timestamp: &Timestamp| timestamp.get_timestamp().unix_timestamp() == 1625126400) 111 | .returning(|_| Ok(())); 112 | 113 | let udt = UpdateCheckTimestamp::from(1625126400); 114 | assert!(udt.save_config(&mock_workspace).is_ok()); 115 | } 116 | 117 | #[test] 118 | fn read_timestamp_test() { 119 | let mut mock_workspace = MockWorkspace::new(); 120 | mock_workspace 121 | .expect_read_config_object::() 122 | .returning(|| Ok(1627068997.into())); 123 | 124 | let udt = UpdateCheckTimestamp::read_config(&mock_workspace).unwrap(); 125 | assert_eq!(udt.get_timestamp().unix_timestamp(), 1627068997); 126 | } 127 | 128 | #[test] 129 | fn timestamp_newer_than_one_day_should_not_expire() { 130 | const TIME_2021_07_01_10_00_00: i64 = 1625126400; 131 | const TIME_2021_07_01_02_00_00: i64 = 1625097600; 132 | let newer = Timestamp::from(TIME_2021_07_01_02_00_00); 133 | let older = Timestamp::from(TIME_2021_07_01_10_00_00); 134 | 135 | assert!(!newer.is_expired(&older)); 136 | } 137 | 138 | #[test] 139 | fn timestamp_older_than_one_day_should_expire() { 140 | const TIME_2021_07_01_10_00_00: i64 = 1625126400; 141 | const TIME_2021_06_01_02_00_00: i64 = 1622505600; 142 | let newer = Timestamp::from(TIME_2021_07_01_10_00_00); 143 | let older = Timestamp::from(TIME_2021_06_01_02_00_00); 144 | 145 | assert!(older.is_expired(&newer)); 146 | } 147 | 148 | #[test] 149 | fn timestamp_equal_one_day_should_expire() { 150 | const TIME_2021_07_01_10_00_00: i64 = 1625126400; 151 | const TIME_2021_07_02_10_00_00: i64 = 1625212800; 152 | let newer = Timestamp::from(TIME_2021_07_01_10_00_00); 153 | let older = Timestamp::from(TIME_2021_07_02_10_00_00); 154 | 155 | assert!(newer.is_expired(&older)); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/model/language.rs: -------------------------------------------------------------------------------- 1 | use crate::error; 2 | use serde::{Deserialize, Serialize}; 3 | use std::str::FromStr; 4 | 5 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 6 | pub enum Language { 7 | Unsupported, 8 | Cpp = 1, 9 | Java = 4, 10 | Bash = 10, 11 | CppWithFileSupport = 12, 12 | // C = ?, 13 | Ada = 9, 14 | } 15 | 16 | impl Language { 17 | pub fn code(&self) -> String { 18 | let val = *self; 19 | let val = val as i32; 20 | val.to_string() 21 | } 22 | 23 | pub fn comment_styles(&self) -> Option> { 24 | match self { 25 | Language::Cpp => Some(vec!["//", ";"]), 26 | Language::Java | Language::CppWithFileSupport => Some(vec!["//"]), 27 | Language::Bash => Some(vec!["#"]), 28 | Language::Ada => Some(vec!["--"]), // Language::C => {"/*"} 29 | _ => None, 30 | } 31 | } 32 | 33 | pub fn is_comment(&self, line: &str) -> bool { 34 | match self.comment_styles() { 35 | None => false, 36 | Some(comment_styles) => comment_styles.iter().any(|style| line.starts_with(style)), 37 | } 38 | } 39 | } 40 | 41 | impl ToString for Language { 42 | fn to_string(&self) -> String { 43 | match self { 44 | Language::Cpp => "C++", 45 | Language::Java => "Java", 46 | Language::Bash => "Bash", 47 | Language::CppWithFileSupport => "C++ with file support", 48 | Language::Ada => "Ada", 49 | Language::Unsupported => "Unsupported language", 50 | } 51 | .to_string() 52 | } 53 | } 54 | 55 | impl FromStr for Language { 56 | type Err = error::Error; 57 | 58 | fn from_str(s: &str) -> Result { 59 | let l = match s.to_lowercase().as_str() { 60 | "c++" => Language::Cpp, 61 | "java" => Language::Java, 62 | "bash" => Language::Bash, 63 | "c++ z obsluga plikow" => Language::CppWithFileSupport, 64 | "ada" => Language::Ada, 65 | lang => return Err(Self::Err::UnsupportedLanguage(lang.to_string())), 66 | }; 67 | 68 | Ok(l) 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | use crate::error::Error; 76 | 77 | #[test] 78 | fn from_string() { 79 | assert_eq!(Language::from_str("C++").unwrap(), Language::Cpp); 80 | assert_eq!(Language::from_str("Java").unwrap(), Language::Java); 81 | assert_eq!(Language::from_str("Bash").unwrap(), Language::Bash); 82 | assert_eq!( 83 | Language::from_str("C++ z obsluga plikow").unwrap(), 84 | Language::CppWithFileSupport 85 | ); 86 | assert_eq!(Language::from_str("Ada").unwrap(), Language::Ada); 87 | assert!(Language::from_str("C").is_err()); 88 | } 89 | 90 | #[test] 91 | fn to_string() { 92 | assert_eq!(Language::Unsupported.to_string(), "Unsupported language"); 93 | assert_eq!(Language::Cpp.to_string(), "C++"); 94 | assert_eq!(Language::Java.to_string(), "Java"); 95 | assert_eq!(Language::Bash.to_string(), "Bash"); 96 | assert_eq!( 97 | Language::CppWithFileSupport.to_string(), 98 | "C++ with file support" 99 | ); 100 | assert_eq!(Language::Ada.to_string(), "Ada"); 101 | } 102 | 103 | #[test] 104 | fn from_string_polish() { 105 | let result = Language::from_str("C++ z obsługą plików"); 106 | assert!(matches!(result, Err(Error::UnsupportedLanguage { .. }))); 107 | } 108 | 109 | #[test] 110 | fn from_invalid_string() { 111 | let result = Language::from_str("sada224214@dasdas"); 112 | assert!(matches!(result, Err(Error::UnsupportedLanguage { .. }))); 113 | } 114 | 115 | #[test] 116 | fn from_different_case_string() { 117 | assert_eq!( 118 | Language::from_str("c++ z Obsluga pliKOW").unwrap(), 119 | Language::CppWithFileSupport 120 | ); 121 | } 122 | 123 | #[test] 124 | fn codes() { 125 | assert_eq!(Language::Unsupported.code(), "0"); 126 | assert_eq!(Language::Cpp.code(), "1"); 127 | assert_eq!(Language::Java.code(), "4"); 128 | assert_eq!(Language::Bash.code(), "10"); 129 | assert_eq!(Language::CppWithFileSupport.code(), "12"); 130 | assert_eq!(Language::Ada.code(), "9"); 131 | } 132 | 133 | // #[test] 134 | // fn no_comment() { 135 | // assert!(!contains_comment("no comment sorry")); 136 | // } 137 | 138 | #[test] 139 | fn cpp_comment() { 140 | assert!(Language::Cpp.is_comment("// Hubert Jaremko")); 141 | } 142 | 143 | #[test] 144 | fn asm_comment() { 145 | assert!(Language::Cpp.is_comment("; Hubert Jaremko")); 146 | } 147 | 148 | #[test] 149 | fn cpp_comment_should_not_confuse_preprocessor() { 150 | assert!(!Language::Cpp.is_comment("#include ")); 151 | } 152 | 153 | #[test] 154 | fn bash_comment() { 155 | assert!(Language::Bash.is_comment("# Hubert Jaremko")); 156 | } 157 | 158 | // #[test] 159 | // fn c_comment() { 160 | // assert!(Language::C.is_comment("/* Hubert Jaremko */")); 161 | // } 162 | 163 | #[test] 164 | fn ada_comment() { 165 | assert!(Language::Ada.is_comment("-- Hubert Jaremko")); 166 | } 167 | 168 | #[test] 169 | fn unsupported_comment() { 170 | assert!(!Language::Unsupported.is_comment("% Hubert Jaremko")); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/workspace/workspace_dir.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::error::Result; 3 | use crate::workspace::{ConfigObject, Workspace, WorkspacePaths}; 4 | use std::fs::DirBuilder; 5 | use std::{fs, io}; 6 | use tracing::{debug, info}; 7 | 8 | pub struct WorkspaceDir { 9 | paths: WorkspacePaths, 10 | } 11 | 12 | impl WorkspaceDir { 13 | pub fn new() -> Self { 14 | Self { 15 | paths: WorkspacePaths::new(), 16 | } 17 | } 18 | 19 | pub(crate) fn _with_paths(paths: WorkspacePaths) -> Self { 20 | Self { paths } 21 | } 22 | 23 | fn check_if_already_initialized(&self) -> Result<()> { 24 | let path = self.paths.baca_dir(); 25 | info!("Checking if {} exists.", path.to_str().unwrap()); 26 | 27 | if path.is_dir() { 28 | return Err(Error::WorkspaceAlreadyInitialized); 29 | } 30 | 31 | Ok(()) 32 | } 33 | } 34 | 35 | impl Workspace for WorkspaceDir { 36 | fn initialize(&self) -> Result<()> { 37 | self.check_if_already_initialized()?; 38 | 39 | let path = self.paths.baca_dir(); 40 | info!("Creating new {}", path.to_str().unwrap()); 41 | 42 | DirBuilder::new() 43 | .create(path) 44 | .map_err(as_config_create_error)?; 45 | 46 | info!("Baca directory created successfully."); 47 | Ok(()) 48 | } 49 | 50 | fn check_if_initialized(&self) -> Result<()> { 51 | let path = self.paths.baca_dir(); 52 | debug!("Checking if {} exists.", path.to_str().unwrap()); 53 | 54 | if !path.exists() { 55 | return Err(Error::WorkspaceNotInitialized); 56 | } 57 | 58 | Ok(()) 59 | } 60 | 61 | fn remove_workspace(&self) -> Result<()> { 62 | self.check_if_initialized()?; 63 | info!("Removing Baca workspace."); 64 | fs::remove_dir_all(self.paths.baca_dir()).map_err(as_config_remove_error) 65 | } 66 | 67 | fn save_config_object(&self, object: &T) -> Result<()> 68 | where 69 | T: ConfigObject + 'static, 70 | { 71 | self.check_if_initialized()?; 72 | let path = self.paths.config_path::(); 73 | 74 | info!("Saving object {}", path.to_str().unwrap()); 75 | debug!("Saving object {:?}", &object); 76 | 77 | let serialized = serde_yaml::to_string(object)?; 78 | debug!("Serialized: {}", serialized); 79 | fs::write(path, serialized).map_err(|e| Error::SavingConfig(e.into())) 80 | } 81 | 82 | fn read_config_object(&self) -> Result 83 | where 84 | T: ConfigObject + 'static, 85 | { 86 | self.check_if_initialized()?; 87 | let path = self.paths.config_path::(); 88 | 89 | info!("Reading {}", path.to_str().unwrap()); 90 | let serialized = fs::read_to_string(path).map_err(|e| Error::ReadingConfig(e.into()))?; 91 | debug!("Serialized: {}", serialized); 92 | 93 | let deserialized = serde_yaml::from_str::(&serialized)?; 94 | debug!("Deserialized: {:?}", deserialized); 95 | 96 | Ok(deserialized) 97 | } 98 | 99 | fn remove_config_object(&self) -> Result<()> 100 | where 101 | T: ConfigObject + 'static, 102 | { 103 | let path = self.paths.config_path::(); 104 | 105 | info!("Removing config file {}", path.to_str().unwrap()); 106 | fs::remove_file(path).map_err(|e| Error::RemovingConfig(e.into())) 107 | } 108 | 109 | fn get_paths(&self) -> WorkspacePaths { 110 | self.paths.clone() 111 | } 112 | } 113 | 114 | fn as_config_create_error(e: io::Error) -> Error { 115 | Error::CreatingWorkspace(e.into()) 116 | } 117 | 118 | fn as_config_remove_error(e: io::Error) -> Error { 119 | Error::RemovingWorkspace(e.into()) 120 | } 121 | 122 | #[cfg(test)] 123 | pub(crate) mod tests { 124 | use super::*; 125 | use crate::workspace::ConnectionConfig; 126 | use assert_fs::TempDir; 127 | 128 | pub fn make_temp_workspace( 129 | ) -> std::result::Result<(TempDir, WorkspacePaths, WorkspaceDir), Box> 130 | { 131 | let temp_dir = assert_fs::TempDir::new()?; 132 | let mock_paths = WorkspacePaths::_with_root(temp_dir.path()); 133 | let workspace = WorkspaceDir::_with_paths(mock_paths.clone()); 134 | Ok((temp_dir, mock_paths, workspace)) 135 | } 136 | 137 | pub fn make_baca() -> ConnectionConfig { 138 | ConnectionConfig { 139 | host: "test_host".to_string(), 140 | login: "test_login".to_string(), 141 | password: "test_pass".to_string(), 142 | permutation: "test_perm".to_string(), 143 | cookie: "test_cookie".to_string(), 144 | } 145 | } 146 | 147 | #[test] 148 | fn init_success() { 149 | let (temp_dir, mock_paths, workspace) = make_temp_workspace().unwrap(); 150 | 151 | workspace.initialize().unwrap(); 152 | 153 | assert!(fs::read_dir(mock_paths.baca_dir()).is_ok()); 154 | temp_dir.close().unwrap(); 155 | } 156 | 157 | #[test] 158 | fn init_already_initialized() { 159 | let (temp_dir, mock_paths, workspace) = make_temp_workspace().unwrap(); 160 | 161 | let result = workspace.initialize(); 162 | assert!(result.is_ok()); 163 | 164 | let result = workspace.initialize(); 165 | assert!(result.is_err()); 166 | 167 | if let Err(e) = result { 168 | assert!(matches!(e, Error::WorkspaceAlreadyInitialized)); 169 | } 170 | 171 | assert!(fs::read_dir(mock_paths.baca_dir()).is_ok()); 172 | temp_dir.close().unwrap(); 173 | } 174 | // todo: tests for removing and saving objects 175 | 176 | #[test] 177 | fn get_paths_test() { 178 | let actual = WorkspacePaths::new(); 179 | let workspace = WorkspaceDir::new(); 180 | assert_eq!(actual, workspace.get_paths()); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/commands/log.rs: -------------------------------------------------------------------------------- 1 | use crate::util::*; 2 | use predicates::prelude::{predicate, PredicateBooleanExt}; 3 | use std::fs; 4 | 5 | #[test] 6 | fn tasks_not_initialized() -> Result<(), Box> { 7 | assert_fails_if_not_initialized(&["log"]) 8 | } 9 | 10 | #[test] 11 | #[ignore] 12 | fn no_argument_should_print_last_three() -> Result<(), Box> { 13 | let dir = initialize_correct_workspace()?; 14 | let mut cmd = set_up_command(&dir)?; 15 | 16 | cmd.arg("log"); 17 | cmd.assert() 18 | .stdout(predicate::str::contains("[G] Funkcje sklejane -")) 19 | .stdout(predicate::str::contains("[A] Zera funkcji -").not()) 20 | .stdout(predicate::str::contains("[B] Metoda Newtona -").not()) 21 | .stdout( 22 | predicate::str::contains(r#"[C] FAD\x3Csup\x3E2\x3C/sup\x3E - Pochodne mieszane -"#) 23 | .not(), 24 | ) 25 | .stdout(predicate::str::contains("[D] Skalowany Gauss -").not()) 26 | .stdout(predicate::str::contains("[E] Metoda SOR -").not()) 27 | .stdout(predicate::str::contains("submit 4334")) 28 | .stdout(predicate::str::contains("submit 4328")) 29 | .stdout(predicate::str::contains("submit 4326")) 30 | .stdout(predicate::str::contains("submit 4325").not()); 31 | dir.close()?; 32 | Ok(()) 33 | } 34 | 35 | #[test] 36 | #[ignore] 37 | fn with_given_1_should_print_last_1() -> Result<(), Box> { 38 | let dir = initialize_correct_workspace()?; 39 | let mut cmd = set_up_command(&dir)?; 40 | 41 | cmd.arg("log").arg("1"); 42 | cmd.assert() 43 | .stdout(predicate::str::contains("[G] Funkcje sklejane -")) 44 | .stdout(predicate::str::contains("[A] Zera funkcji -").not()) 45 | .stdout(predicate::str::contains("[B] Metoda Newtona -").not()) 46 | .stdout( 47 | predicate::str::contains(r#"[C] FAD\x3Csup\x3E2\x3C/sup\x3E - Pochodne mieszane -"#) 48 | .not(), 49 | ) 50 | .stdout(predicate::str::contains("[D] Skalowany Gauss -").not()) 51 | .stdout(predicate::str::contains("[E] Metoda SOR -").not()) 52 | .stdout(predicate::str::contains("submit 4334")) 53 | .stdout(predicate::str::contains("submit 4328").not()) 54 | .stdout(predicate::str::contains("submit 4326").not()) 55 | .stdout(predicate::str::contains("submit 4325").not()); 56 | dir.close()?; 57 | Ok(()) 58 | } 59 | 60 | #[test] 61 | #[ignore] 62 | fn with_given_more_than_available_should_print_all() -> Result<(), Box> { 63 | let dir = initialize_correct_workspace()?; 64 | let mut cmd = set_up_command(&dir)?; 65 | 66 | cmd.arg("log").arg("60000"); 67 | cmd.assert() 68 | .stdout(predicate::str::contains("[G] Funkcje sklejane")) 69 | .stdout(predicate::str::contains("[A] Zera funkcji")) 70 | .stdout(predicate::str::contains("[B] Metoda Newtona")) 71 | .stdout(predicate::str::contains( 72 | r#"[C] FAD\x3Csup\x3E2\x3C/sup\x3E - Pochodne mieszane"#, 73 | )) 74 | .stdout(predicate::str::contains("[D] Skalowany Gauss")) 75 | .stdout(predicate::str::contains("[E] Metoda SOR")) 76 | .stdout(predicate::str::contains("4334")) 77 | .stdout(predicate::str::contains("4328")) 78 | .stdout(predicate::str::contains("4326")) 79 | .stdout(predicate::str::contains("532")); 80 | dir.close()?; 81 | Ok(()) 82 | } 83 | 84 | #[test] 85 | #[ignore] 86 | fn with_invalid_argument_should_report_error() -> Result<(), Box> { 87 | let dir = initialize_correct_workspace()?; 88 | let mut cmd = set_up_command(&dir)?; 89 | 90 | cmd.arg("log").arg("nan"); 91 | cmd.assert() 92 | .stderr(predicate::str::contains("invalid value")); 93 | dir.close()?; 94 | Ok(()) 95 | } 96 | 97 | #[test] 98 | #[ignore] 99 | fn on_corrupted_repo_should_report_error() -> Result<(), Box> { 100 | let dir = initialize_correct_workspace()?; 101 | let mut cmd = set_up_command(&dir)?; 102 | fs::remove_file(dir.baca_config_file_path())?; 103 | 104 | cmd.arg("log"); 105 | cmd.assert().stdout(predicate::str::contains("corrupted")); 106 | dir.close()?; 107 | Ok(()) 108 | } 109 | 110 | #[test] 111 | #[ignore] 112 | fn filter() -> Result<(), Box> { 113 | let dir = initialize_correct_workspace()?; 114 | let mut cmd = set_up_command(&dir)?; 115 | 116 | cmd.arg("log").arg("100").arg("-t").arg("2"); 117 | cmd.assert() 118 | .stdout(predicate::str::contains("[A] Zera funkcji - C++").not()) 119 | .stdout(predicate::str::contains("[B] Metoda Newtona")) 120 | .stdout( 121 | predicate::str::contains(r#"[C] FAD\x3Csup\x3E2\x3C/sup\x3E - Pochodne mieszane -"#) 122 | .not(), 123 | ) 124 | .stdout(predicate::str::contains("[D] Skalowany Gauss -").not()) 125 | .stdout(predicate::str::contains("[E] Metoda SOR -").not()) 126 | .stdout(predicate::str::contains("[G] Funkcje sklejane -").not()); 127 | dir.close()?; 128 | Ok(()) 129 | } 130 | 131 | #[test] 132 | #[ignore] 133 | fn filter_given_invalid_task_id_should_print_error() -> Result<(), Box> { 134 | let dir = initialize_correct_workspace()?; 135 | let mut cmd = set_up_command(&dir)?; 136 | 137 | cmd.arg("log").arg("-t").arg("1123"); 138 | cmd.assert() 139 | .stdout(predicate::str::contains("1123 does not exist")); 140 | dir.close()?; 141 | Ok(()) 142 | } 143 | 144 | #[test] 145 | #[ignore] 146 | fn filter_given_invalid_argument_should_print_error() -> Result<(), Box> { 147 | let dir = initialize_correct_workspace()?; 148 | let mut cmd = set_up_command(&dir)?; 149 | 150 | cmd.arg("log").arg("-t").arg("asd"); 151 | cmd.assert() 152 | .stderr(predicate::str::contains("invalid value")); 153 | dir.close()?; 154 | Ok(()) 155 | } 156 | -------------------------------------------------------------------------------- /src/api/request.rs: -------------------------------------------------------------------------------- 1 | use reqwest::blocking::{multipart, RequestBuilder, Response}; 2 | use reqwest::header::{CONTENT_TYPE, COOKIE}; 3 | use tracing::{debug, info}; 4 | 5 | use crate::api::RequestType; 6 | use crate::error; 7 | use crate::error::Error; 8 | use crate::model::Task; 9 | use crate::workspace::ConnectionConfig; 10 | 11 | pub struct Request<'a> { 12 | connection_config: &'a ConnectionConfig, 13 | client: reqwest::blocking::Client, 14 | } 15 | 16 | impl<'a> Request<'a> { 17 | pub fn new(connection_config: &'a ConnectionConfig) -> Self { 18 | Request { 19 | connection_config, 20 | client: reqwest::blocking::ClientBuilder::new() 21 | .danger_accept_invalid_certs(true) 22 | .build() 23 | .unwrap(), 24 | } 25 | } 26 | 27 | pub fn login(self) -> reqwest::Result { 28 | let (login, pass) = self.connection_config.credentials(); 29 | let req = self.make_request(RequestType::Login(login, pass)); 30 | req.send() 31 | } 32 | 33 | pub fn details(self, id: &str) -> reqwest::Result { 34 | let req = self.make_request(RequestType::SubmitDetails(id.to_string())); 35 | req.send() 36 | } 37 | 38 | pub fn results(self) -> reqwest::Result { 39 | let req = self.make_request(RequestType::Results); 40 | req.send() 41 | } 42 | 43 | pub fn tasks(&self) -> reqwest::Result { 44 | let req = self.make_request(RequestType::Tasks); 45 | req.send() 46 | } 47 | 48 | pub fn submit(&self, task: &Task, file_path: &str) -> error::Result { 49 | let req = self.make_submit_request(task, file_path)?; 50 | req.send().map_err(|e| e.into()) 51 | } 52 | 53 | pub fn allowed_languages(&self, task_id: &str) -> reqwest::Result { 54 | let req = self.make_request(RequestType::AllowedLanguages(task_id.to_string())); 55 | req.send() 56 | } 57 | 58 | fn make_request(&self, req_type: RequestType) -> RequestBuilder { 59 | let post_url = format!( 60 | "{}{}", 61 | self.connection_config.make_module_base(), 62 | req_type.mapping() 63 | ); 64 | let payload = self.connection_config.make_payload(&req_type); 65 | 66 | info!("Making request to: {}", post_url); 67 | debug!("Request payload: {}", payload); 68 | 69 | let req = self.make_base_request(&post_url).body(payload); 70 | let req = match req_type { 71 | RequestType::Login(_, _) => req, 72 | _ => req.header(COOKIE, self.connection_config.make_cookie()), 73 | }; 74 | 75 | debug!("{:?}", req); 76 | req 77 | } 78 | 79 | fn make_base_request(&self, url: &str) -> RequestBuilder { 80 | self.client 81 | .post(url) 82 | .header(CONTENT_TYPE, "text/x-gwt-rpc; charset=UTF-8") 83 | .header("DNT", "1") 84 | .header( 85 | "X-GWT-Module-Base", 86 | self.connection_config.make_module_base(), 87 | ) 88 | .header("X-GWT-Permutation", &self.connection_config.permutation) 89 | } 90 | 91 | fn make_submit_request(&self, task: &Task, file_path: &str) -> error::Result { 92 | let form = multipart::Form::new() 93 | .text("zadanie", task.id.clone()) 94 | .text("jezyk", task.language.code()) 95 | .file("zrodla", file_path) 96 | .map_err(|e| Error::ReadingSource(e.into()))?; 97 | 98 | let url = format!( 99 | "https://baca.ii.uj.edu.pl/{}/sendSubmit", 100 | self.connection_config.host 101 | ); 102 | 103 | info!("Making submit request to: {}", url); 104 | debug!("Form: {:?}", form); 105 | 106 | let req = self 107 | .client 108 | .post(url) 109 | .multipart(form) 110 | .header(COOKIE, self.connection_config.make_cookie()); 111 | debug!("{:?}", req); 112 | Ok(req) 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | use reqwest::StatusCode; 119 | 120 | use super::*; 121 | 122 | fn make_connection_config() -> ConnectionConfig { 123 | ConnectionConfig { 124 | host: "mn2020".to_string(), 125 | login: "login".to_string(), 126 | password: "password".to_string(), 127 | permutation: "5A4AE95C27260DF45F17F9BF027335F6".to_string(), 128 | cookie: "cookie".to_string(), 129 | } 130 | } 131 | 132 | fn check_response(response: reqwest::Result) { 133 | if let Ok(response) = response { 134 | assert_eq!(response.status(), StatusCode::OK); 135 | assert_eq!(response.text().unwrap(), "//OK[0,[],0,7]"); 136 | }; 137 | } 138 | 139 | #[test] 140 | #[ignore] 141 | fn login_should_connect() { 142 | let baca = make_connection_config(); 143 | let req = Request::new(&baca); 144 | let response = req.login(); 145 | 146 | check_response(response); 147 | } 148 | 149 | #[test] 150 | #[ignore] 151 | fn details_should_connect() { 152 | let baca = make_connection_config(); 153 | let req = Request::new(&baca); 154 | let response = req.details("1"); 155 | 156 | check_response(response); 157 | } 158 | 159 | #[test] 160 | #[ignore] 161 | fn results_should_connect() { 162 | let baca = make_connection_config(); 163 | let req = Request::new(&baca); 164 | let response = req.results(); 165 | 166 | check_response(response); 167 | } 168 | 169 | #[test] 170 | #[ignore] 171 | fn tasks_should_connect() { 172 | let baca = make_connection_config(); 173 | let req = Request::new(&baca); 174 | let response = req.tasks(); 175 | 176 | if let Ok(response) = response { 177 | assert_eq!(response.status(), StatusCode::OK); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/command/last.rs: -------------------------------------------------------------------------------- 1 | use crate::api::baca_api::BacaApi; 2 | use crate::command::details::Details; 3 | use crate::command::Command; 4 | use crate::error::{Error, Result}; 5 | use crate::model::Submit; 6 | use crate::workspace::{ConfigObject, ConnectionConfig, Workspace}; 7 | 8 | pub struct Last { 9 | task_id: Option, 10 | } 11 | 12 | impl Last { 13 | pub fn new() -> Self { 14 | Self { task_id: None } 15 | } 16 | 17 | pub fn with_filter(task_id: String) -> Self { 18 | Self { 19 | task_id: Some(task_id), 20 | } 21 | } 22 | 23 | fn get_last_submit(&self, connection_config: &ConnectionConfig, api: &A) -> Result 24 | where 25 | A: BacaApi, 26 | { 27 | let results = if let Some(task_id) = &self.task_id { 28 | api.get_results_by_task(connection_config, task_id)? 29 | } else { 30 | api.get_results(connection_config)? 31 | }; 32 | 33 | Ok(results.submits.first().ok_or(Error::NoSubmitsYet)?.clone()) 34 | } 35 | } 36 | 37 | impl Command for Last { 38 | fn execute(self, workspace: &W, api: &A) -> Result<()> 39 | where 40 | W: Workspace, 41 | A: BacaApi, 42 | { 43 | let connection_config = ConnectionConfig::read_config(workspace)?; 44 | let last = self.get_last_submit(&connection_config, api)?; 45 | 46 | Details::new(&last.id).execute(workspace, api) 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | use crate::api::baca_api::MockBacaApi; 54 | use crate::model::SubmitStatus; 55 | use crate::model::{Results, Submit}; 56 | use crate::workspace::{ConnectionConfig, MockWorkspace}; 57 | 58 | #[test] 59 | fn no_submits() { 60 | let mut mock_workspace = MockWorkspace::new(); 61 | mock_workspace 62 | .expect_read_config_object() 63 | .returning(|| Ok(ConnectionConfig::default())); 64 | 65 | let mut mock_api = MockBacaApi::new(); 66 | mock_api 67 | .expect_get_results() 68 | .withf(|x| *x == ConnectionConfig::default()) 69 | .returning(|_| Ok(Results { submits: vec![] })); 70 | 71 | let last = Last::new(); 72 | let result = last.execute(&mock_workspace, &mock_api); 73 | assert!(result.is_err()); 74 | assert!(matches!(result.err().unwrap(), Error::NoSubmitsYet)); 75 | } 76 | 77 | #[test] 78 | fn one_submit() { 79 | let expected = Submit { 80 | status: SubmitStatus::InternalError, 81 | points: 0.0, 82 | lateness: None, 83 | accepted: 0, 84 | size: 123, 85 | timestamp: "2002".to_string(), 86 | language: "Java".to_string(), 87 | id: "3".to_string(), 88 | max_points: None, 89 | problem_name: "Test Problem".to_string(), 90 | link: "www.baca.pl".to_string(), 91 | test_results: None, 92 | }; 93 | 94 | let mut mock_workspace = MockWorkspace::new(); 95 | mock_workspace 96 | .expect_read_config_object() 97 | .returning(|| Ok(ConnectionConfig::default())); 98 | 99 | let mut mock_api = MockBacaApi::new(); 100 | let results = Results { 101 | submits: vec![expected.clone()], 102 | }; 103 | mock_api 104 | .expect_get_results() 105 | .withf(|x| *x == ConnectionConfig::default()) 106 | .returning(move |_| Ok(results.clone())); 107 | 108 | let submit = expected; 109 | mock_api 110 | .expect_get_submit_details() 111 | .withf(|x, id| *x == ConnectionConfig::default() && id == "3") 112 | .returning(move |_, _| Ok(submit.clone())); 113 | 114 | let last = Last::new(); 115 | let result = last.execute(&mock_workspace, &mock_api); 116 | assert!(result.is_ok()); 117 | } 118 | 119 | #[test] 120 | fn three_submits() { 121 | let submit1 = Submit { 122 | status: SubmitStatus::InternalError, 123 | points: 0.0, 124 | lateness: None, 125 | accepted: 0, 126 | size: 123, 127 | timestamp: "2002".to_string(), 128 | language: "Java".to_string(), 129 | id: "1".to_string(), 130 | max_points: None, 131 | problem_name: "Test Problem 1".to_string(), 132 | link: "www.baca.pl".to_string(), 133 | test_results: None, 134 | }; 135 | 136 | let submit2 = Submit { 137 | status: SubmitStatus::InternalError, 138 | points: 0.0, 139 | lateness: None, 140 | accepted: 0, 141 | size: 123, 142 | timestamp: "2002".to_string(), 143 | language: "Java".to_string(), 144 | id: "2".to_string(), 145 | max_points: None, 146 | problem_name: "Test Problem 2".to_string(), 147 | link: "www.baca.pl".to_string(), 148 | test_results: None, 149 | }; 150 | 151 | let submit3 = Submit { 152 | status: SubmitStatus::InternalError, 153 | points: 0.0, 154 | lateness: None, 155 | accepted: 0, 156 | size: 123, 157 | timestamp: "2002".to_string(), 158 | language: "Java".to_string(), 159 | id: "3".to_string(), 160 | max_points: None, 161 | problem_name: "Test Problem 3".to_string(), 162 | link: "www.baca.pl".to_string(), 163 | test_results: None, 164 | }; 165 | 166 | let all_submits = vec![submit1.clone(), submit2, submit3]; 167 | 168 | let mut mock_workspace = MockWorkspace::new(); 169 | mock_workspace 170 | .expect_read_config_object() 171 | .returning(|| Ok(ConnectionConfig::default())); 172 | 173 | let mut mock_api = MockBacaApi::new(); 174 | let results = Results { 175 | submits: all_submits, 176 | }; 177 | mock_api 178 | .expect_get_results() 179 | .withf(|x| *x == ConnectionConfig::default()) 180 | .returning(move |_| Ok(results.clone())); 181 | 182 | let submit = submit1; 183 | mock_api 184 | .expect_get_submit_details() 185 | .withf(|x, id| *x == ConnectionConfig::default() && id == "1") 186 | .returning(move |_, _| Ok(submit.clone())); 187 | 188 | let last = Last::new(); 189 | let result = last.execute(&mock_workspace, &mock_api); 190 | assert!(result.is_ok()); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/parse/results.rs: -------------------------------------------------------------------------------- 1 | use crate::model::SubmitStatus; 2 | use crate::model::{Results, Submit}; 3 | use crate::parse::deserialize; 4 | use crate::parse::from_baca_output::FromBacaOutput; 5 | use crate::workspace::ConnectionConfig; 6 | use std::str::FromStr; 7 | use tracing::debug; 8 | 9 | impl FromBacaOutput for Results { 10 | fn from_baca_output(connection_config: &ConnectionConfig, data: &str) -> Results { 11 | let data = deserialize(data); 12 | debug!("Deserialized: {:?}", data); 13 | 14 | let st: Vec = data 15 | .iter() 16 | .skip_while(|x| !x.contains("Ljava.lang.String;/2600011424")) 17 | .skip(10) 18 | .map(|x| x.to_owned()) 19 | .collect(); 20 | 21 | let submits: Vec = st 22 | .chunks(10) 23 | .rev() 24 | .skip(1) 25 | .map(|raw| Submit { 26 | status: SubmitStatus::from_str(raw[1].as_str()).unwrap(), 27 | points: raw[2].parse().unwrap(), 28 | lateness: None, 29 | accepted: raw[3].parse().unwrap(), 30 | size: raw[4].parse().unwrap(), 31 | timestamp: raw[5].to_string(), 32 | language: raw[6].to_string(), 33 | id: raw[8].to_string(), 34 | max_points: None, 35 | problem_name: raw[7].to_string(), 36 | link: connection_config.make_url() + "/#SubmitDetails/" + raw[8].as_str(), 37 | test_results: None, 38 | }) 39 | .collect(); 40 | 41 | debug!("Parsed submits: {:?}", submits); 42 | Results::new(submits) 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod results_tests { 48 | use crate::model::SubmitStatus; 49 | use crate::model::{Results, Submit}; 50 | use crate::parse::from_baca_output::FromBacaOutput; 51 | use crate::workspace::ConnectionConfig; 52 | 53 | #[test] 54 | fn parse_test() { 55 | let baca = ConnectionConfig { 56 | host: "mn".to_string(), 57 | login: "".to_string(), 58 | password: "".to_string(), 59 | permutation: "permutation".to_string(), 60 | cookie: "cookie".to_string(), 61 | }; 62 | let raw = r#"//OK[207,206,205,205,207,205,206,209,209,209,208,208,206,208,206,208,208,208,208,208,207,207,207,207,206,208,208,208,208,208,208,208,208,208,208,205,208,208,208,207,209,209,208,207,205,206,206,205,205,206,206,205,205,53,3,204,203,202,201,200,199,198,197,8,3,42,196,195,194,193,6,186,192,8,3,20,19,18,191,190,6,186,189,8,3,11,10,9,188,187,6,186,185,8,3,11,10,9,184,183,6,182,181,8,3,42,180,179,178,177,6,173,176,8,3,11,10,9,175,174,6,173,172,8,3,20,19,18,171,170,6,60,169,8,3,52,112,111,168,167,6,60,166,8,3,52,112,111,165,164,6,60,163,8,3,52,112,111,162,161,6,60,160,8,3,48,93,92,159,158,6,60,157,8,3,48,93,92,156,155,6,60,154,8,3,20,19,18,153,152,6,60,151,8,3,48,144,143,150,149,6,60,148,8,3,20,19,18,147,146,6,60,145,8,3,48,144,143,142,141,6,60,140,8,3,48,129,128,139,138,6,60,137,8,3,48,134,133,132,136,6,60,135,8,3,48,134,133,132,131,6,60,130,8,3,48,129,128,127,126,6,60,125,8,3,42,122,121,24,124,6,60,123,8,3,42,122,121,120,119,6,60,118,8,3,42,117,116,115,114,6,60,113,8,3,42,112,111,110,109,6,60,108,8,3,20,19,18,107,106,6,60,105,8,3,48,64,63,104,103,6,60,102,8,3,48,64,63,96,101,6,60,100,8,3,48,64,63,99,98,6,60,97,8,3,48,64,63,96,95,6,60,94,8,3,48,93,92,91,90,6,60,89,8,3,48,64,63,88,87,6,60,86,8,3,48,64,63,85,84,6,60,83,8,3,48,64,63,82,81,6,60,80,8,3,48,64,63,79,78,6,60,77,8,3,48,64,63,76,75,6,60,74,8,3,11,10,9,73,72,6,60,71,8,3,48,64,63,70,69,6,60,68,8,3,48,64,63,67,66,6,60,65,8,3,48,64,63,62,61,6,60,59,8,3,42,19,18,58,57,6,37,56,8,3,52,47,46,55,54,6,37,53,8,3,52,47,46,51,50,6,37,49,8,3,48,47,46,45,44,6,37,43,8,3,42,19,18,39,41,6,37,40,8,3,11,10,9,39,38,6,37,36,8,3,20,19,18,35,34,6,25,33,8,3,20,19,18,27,32,6,25,31,8,3,11,10,9,30,29,6,25,28,8,3,11,10,9,27,26,6,25,24,8,3,20,19,18,23,22,6,5,21,8,3,20,19,18,17,16,6,5,15,8,3,11,10,9,14,13,6,5,12,8,3,11,10,9,8,7,6,5,4,8,3,53,2,1,["testerka.gwt.client.tools.DataSource/1474249525","[[Ljava.lang.String;/4182515373","[Ljava.lang.String;/2600011424","4334","[G] Funkcje sklejane","C++","2020-05-17 18:53:09","1190","100","4.00","program zaakceptowany","4328","2020-05-17 16:57:22","2022","4326","2020-05-17 16:53:41","2010","0","0.00","bĹ\x82Ä\x85d kompilacji","4325","2020-05-17 16:52:45","1226","4147","[F] Interpolacja","2020-05-15 11:11:42","4381","4073","2020-05-14 13:45:22","4880","4070","2020-05-14 13:11:52","4069","2020-05-14 13:09:50","1976","3269","[E] Metoda SOR","2020-04-26 13:27:14","2004","3268","2020-04-26 13:24:45","zĹ\x82a odpowiedz","3266","2020-04-26 12:43:36","1970","17","0.67","przekroczony czas","3113","2020-04-24 20:06:32","1612","bĹ\x82Ä\x85d wykonania","3111","2020-04-24 19:41:07","1595","2919","2020-04-23 12:23:38","75","2918","[D] Skalowany Gauss","2020-04-23 12:04:20","4327","89","3.58","2917","2020-04-23 12:01:03","4281","2908","2020-04-22 20:51:41","5816","2907","2020-04-22 20:41:43","7244","2905","2020-04-22 19:22:21","5718","2904","2020-04-22 19:20:07","5709","2903","2020-04-22 18:43:42","5212","2897","2020-04-22 16:14:55","5096","2896","2020-04-22 16:13:45","5100","2895","2020-04-22 16:07:00","5116","26","1.05","2894","2020-04-22 15:46:59","5048","2888","2020-04-22 13:04:36","5752","2886","2020-04-22 12:36:04","2885","2020-04-22 12:31:25","5032","2884","2020-04-22 12:30:53","5010","2796","2020-04-20 15:39:42","4358","74","2.95","2795","2020-04-20 15:36:05","4483","63","2.53","2534","2020-04-17 11:24:47","3946","16","0.63","2533","2020-04-17 10:42:33","2532","2020-04-17 10:37:46","4191","42","1.68","2531","2020-04-17 10:03:54","4101","37","1.47","2501","2020-04-16 21:47:55","2500","2020-04-16 21:45:00","4071","2495","2020-04-16 20:41:45","4068","32","1.26","2494","2020-04-16 20:34:12","3980","2493","2020-04-16 20:18:00","3995","2492","2020-04-16 20:17:28","3984","2490","2020-04-16 19:44:56","4024","2487","2020-04-16 18:12:59","4018","2471","2020-04-16 17:13:03","6278","2458","2020-04-16 16:06:32","6340","2454","2020-04-16 15:22:25","5211","2453","2020-04-16 15:20:10","5213","1721","[C] FAD\x3Csup\x3E2\x3C/sup\x3E - Pochodne mieszane","2020-04-04 00:25:12","6251","1720","2020-04-04 00:12:59","6277","57","2.29","532","[B] Metoda Newtona","2020-03-22 22:43:32","7431","189","[A] Zera funkcji","2020-03-20 01:42:03","1993","188","2020-03-20 01:41:32","1975","160","2020-03-19 21:21:25","2112","25","1.00","id","zadanie","język","czas zgłoszenia","rozmiar (b)","zaliczone (%)","punkty","nazwa statusu","status_OK","status_CMP","status_ANS","status_TLE","status_RTE"],0,7]"#; 63 | 64 | let actual = Results::from_baca_output(&baca, raw); 65 | let last = &actual.submits[0]; 66 | 67 | let expected = Submit { 68 | status: SubmitStatus::Ok, 69 | points: 4.0, 70 | lateness: None, 71 | accepted: 100, 72 | size: 1190, 73 | timestamp: "2020-05-17 18:53:09".to_string(), 74 | language: "C++".to_string(), 75 | id: "4334".to_string(), 76 | max_points: None, 77 | problem_name: "[G] Funkcje sklejane".to_string(), 78 | link: "https://baca.ii.uj.edu.pl/mn/#SubmitDetails/4334".to_string(), 79 | test_results: None, 80 | }; 81 | 82 | actual.print(5); 83 | assert_eq!(*last, expected); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/workspace/config_editor.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::workspace::{ConfigObject, Workspace}; 3 | use std::path::Path; 4 | use std::process::ExitStatus; 5 | use std::{env, ffi::OsString, fs, io, process}; 6 | use tracing::{debug, info}; 7 | 8 | #[derive(Debug, PartialEq, Eq)] 9 | pub enum EditorStatus { 10 | Modified, 11 | NotModified, 12 | } 13 | 14 | pub struct ConfigEditor 15 | where 16 | E: EditorSpawner, 17 | { 18 | editor: E, 19 | } 20 | 21 | impl ConfigEditor { 22 | pub fn new() -> Self { 23 | ConfigEditor { 24 | editor: Spawner::new(), 25 | } 26 | } 27 | } 28 | 29 | impl ConfigEditor 30 | where 31 | E: EditorSpawner, 32 | { 33 | pub fn edit(&self, workspace: &W) -> Result 34 | where 35 | W: Workspace, 36 | T: ConfigObject, 37 | { 38 | workspace.check_if_initialized()?; 39 | 40 | let config_path = workspace.get_paths().config_path::(); 41 | let ts = fs::metadata(&config_path)?.modified()?; 42 | let rv = self.editor.spawn_and_wait(&config_path)?; 43 | 44 | if !rv.success() { 45 | return Err(crate::error::Error::EditorFail(rv.code().unwrap_or(-1))); 46 | } 47 | 48 | if rv.success() && ts >= fs::metadata(&config_path)?.modified()? { 49 | info!("{} config not modified", T::config_filename()); 50 | return Ok(EditorStatus::NotModified); 51 | } 52 | 53 | info!("{} modified and saved", T::config_filename()); 54 | Ok(EditorStatus::Modified) 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | use mockall::{automock, predicate::*}; 60 | 61 | #[cfg_attr(test, automock)] 62 | pub trait EditorSpawner { 63 | fn default_editor() -> OsString; 64 | fn name(&self) -> &String; 65 | fn spawn_and_wait(&self, path: &Path) -> io::Result; 66 | } 67 | 68 | pub struct Spawner { 69 | name: String, 70 | } 71 | 72 | impl Spawner { 73 | pub fn new() -> Self { 74 | Self { 75 | name: Self::default_editor().into_string().unwrap(), 76 | } 77 | } 78 | } 79 | 80 | impl EditorSpawner for Spawner { 81 | fn default_editor() -> OsString { 82 | if let Some(prog) = env::var_os("VISUAL") { 83 | return prog; 84 | } 85 | if let Some(prog) = env::var_os("EDITOR") { 86 | return prog; 87 | } 88 | if cfg!(windows) { 89 | "notepad.exe".into() 90 | } else { 91 | "vi".into() 92 | } 93 | } 94 | 95 | fn name(&self) -> &String { 96 | &self.name 97 | } 98 | 99 | fn spawn_and_wait(&self, path: &Path) -> io::Result { 100 | info!("Opening editor: {} for config file: {:?}", &self.name, path); 101 | 102 | let s = self.name.clone(); 103 | let mut iterator = s.split(' '); 104 | let cmd = iterator.next().unwrap(); 105 | let args: Vec<&str> = iterator.collect(); 106 | 107 | debug!("{:?} {:?} {:?}", cmd, args, &path); 108 | 109 | process::Command::new(cmd) 110 | .args(args) 111 | .arg(path) 112 | .spawn()? 113 | .wait() 114 | } 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use super::*; 120 | use crate::workspace::workspace_dir::tests::make_temp_workspace; 121 | use crate::workspace::{ConnectionConfig, MockWorkspace, WorkspacePaths}; 122 | use std::fs::File; 123 | use std::io::prelude::*; 124 | #[cfg(unix)] 125 | use std::os::unix::process::ExitStatusExt; 126 | #[cfg(windows)] 127 | use std::os::windows::process::ExitStatusExt; 128 | 129 | #[test] 130 | fn file_not_modified_test() { 131 | let (temp_dir, _mock_paths, _workspace) = make_temp_workspace().unwrap(); 132 | 133 | fs::create_dir(temp_dir.join(".baca")).unwrap(); 134 | File::create(temp_dir.join(".baca/connection")).unwrap(); 135 | 136 | let mut workspace_mock = MockWorkspace::new(); 137 | workspace_mock 138 | .expect_check_if_initialized() 139 | .returning(|| Ok(())); 140 | workspace_mock 141 | .expect_get_paths() 142 | .returning(move || WorkspacePaths::_with_root(&temp_dir)); 143 | let mut spawner_mock = MockEditorSpawner::new(); 144 | spawner_mock 145 | .expect_spawn_and_wait() 146 | .returning(|_| Ok(ExitStatus::from_raw(0))); 147 | 148 | let ce = ConfigEditor { 149 | editor: spawner_mock, 150 | }; 151 | let result = ce 152 | .edit::(&workspace_mock) 153 | .unwrap(); 154 | assert_eq!(result, EditorStatus::NotModified); 155 | } 156 | 157 | #[test] 158 | fn file_modified_test() { 159 | let (temp_dir, _mock_paths, _workspace) = make_temp_workspace().unwrap(); 160 | 161 | fs::create_dir(temp_dir.join(".baca")).unwrap(); 162 | let config_path = temp_dir.join(".baca/connection"); 163 | let mut config_mock = File::create(config_path).unwrap(); 164 | 165 | let mut workspace_mock = MockWorkspace::new(); 166 | workspace_mock 167 | .expect_check_if_initialized() 168 | .returning(|| Ok(())); 169 | workspace_mock 170 | .expect_get_paths() 171 | .returning(move || WorkspacePaths::_with_root(&temp_dir)); 172 | let mut spawner_mock = MockEditorSpawner::new(); 173 | spawner_mock.expect_spawn_and_wait().returning(move |_| { 174 | { 175 | use std::{thread, time}; 176 | thread::sleep(time::Duration::from_secs(1)); 177 | config_mock.write_all(b"Testing!").unwrap(); 178 | } 179 | 180 | Ok(ExitStatus::from_raw(0)) 181 | }); 182 | 183 | let ce = ConfigEditor { 184 | editor: spawner_mock, 185 | }; 186 | let result = ce 187 | .edit::(&workspace_mock) 188 | .unwrap(); 189 | assert_eq!(result, EditorStatus::Modified); 190 | } 191 | 192 | #[test] 193 | fn os_error_test() { 194 | let (temp_dir, _mock_paths, _workspace) = make_temp_workspace().unwrap(); 195 | 196 | fs::create_dir(temp_dir.join(".baca")).unwrap(); 197 | let mut config_mock = File::create(temp_dir.join(".baca/connection")).unwrap(); 198 | 199 | let mut workspace_mock = MockWorkspace::new(); 200 | workspace_mock 201 | .expect_check_if_initialized() 202 | .returning(|| Ok(())); 203 | workspace_mock 204 | .expect_get_paths() 205 | .returning(move || WorkspacePaths::_with_root(&temp_dir)); 206 | let mut spawner_mock = MockEditorSpawner::new(); 207 | spawner_mock.expect_spawn_and_wait().returning(move |_| { 208 | config_mock.write_all(b"Test!").unwrap(); 209 | Ok(ExitStatus::from_raw(1)) 210 | }); 211 | 212 | let ce = ConfigEditor { 213 | editor: spawner_mock, 214 | }; 215 | let result = ce.edit::(&workspace_mock); 216 | assert!(result.is_err(), "{:?}", result); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/model/submit_status.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | #[derive(Debug, PartialEq, Eq, Clone, Default)] 4 | pub enum SubmitStatus { 5 | Ok, 6 | InQueue, 7 | Processing, 8 | #[default] 9 | WrongAnswer, 10 | TimeExceeded, 11 | CompileError, 12 | NoHeader, 13 | RealTimeExceeded, 14 | ManuallyRejected, 15 | RuntimeError, 16 | InternalError, 17 | OutputSizeExceeded, 18 | } 19 | 20 | impl FromStr for SubmitStatus { 21 | type Err = (); 22 | 23 | fn from_str(input: &str) -> Result { 24 | match input { 25 | "program zaakceptowany" => Ok(SubmitStatus::Ok), 26 | "czekanie na przetworzenie" => Ok(SubmitStatus::Processing), 27 | "w kolejce" => Ok(SubmitStatus::InQueue), 28 | "zĹ\\x82a odpowiedz" => Ok(SubmitStatus::WrongAnswer), 29 | "przekroczony czas" => Ok(SubmitStatus::TimeExceeded), 30 | "brak nagĹ\\x82Ăłwka" => Ok(SubmitStatus::NoHeader), 31 | "bĹ\\x82Ä\\x85d kompilacji" => Ok(SubmitStatus::CompileError), 32 | "bĹ\\x82Ä\\x85d wykonania: przekroczony real time" => { 33 | Ok(SubmitStatus::RealTimeExceeded) 34 | } 35 | _ => Ok(from_partial_str(input)), 36 | } 37 | } 38 | } 39 | 40 | fn from_partial_str(input: &str) -> SubmitStatus { 41 | const POSSIBLE_STATUSES: [(&str, SubmitStatus); 11] = [ 42 | ("zaakceptowany", SubmitStatus::Ok), 43 | ("przetworzenie", SubmitStatus::Processing), 44 | ("odpowiedz", SubmitStatus::WrongAnswer), 45 | ("czas", SubmitStatus::TimeExceeded), 46 | ("real time", SubmitStatus::RealTimeExceeded), 47 | ("brak", SubmitStatus::NoHeader), 48 | ("kompilacji", SubmitStatus::CompileError), 49 | ("wykonania", SubmitStatus::RuntimeError), 50 | ("odrzucone", SubmitStatus::ManuallyRejected), 51 | ("testerki", SubmitStatus::InternalError), 52 | ("wyjscia", SubmitStatus::OutputSizeExceeded), 53 | ]; 54 | 55 | for (status_str, status) in POSSIBLE_STATUSES { 56 | if input.contains(status_str) { 57 | return status; 58 | } 59 | } 60 | 61 | SubmitStatus::WrongAnswer 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | 68 | mod real_strings { 69 | use super::*; 70 | 71 | #[test] 72 | fn accepted() { 73 | assert_eq!( 74 | SubmitStatus::from_str("program zaakceptowany").unwrap(), 75 | SubmitStatus::Ok 76 | ); 77 | } 78 | 79 | #[test] 80 | fn in_queue() { 81 | assert_eq!( 82 | SubmitStatus::from_str("w kolejce").unwrap(), 83 | SubmitStatus::InQueue 84 | ); 85 | } 86 | 87 | #[test] 88 | fn processing() { 89 | assert_eq!( 90 | SubmitStatus::from_str("czekanie na przetworzenie").unwrap(), 91 | SubmitStatus::Processing 92 | ); 93 | } 94 | #[test] 95 | fn wrong_answer() { 96 | assert_eq!( 97 | SubmitStatus::from_str("zĹ\\x82a odpowiedz").unwrap(), 98 | SubmitStatus::WrongAnswer 99 | ); 100 | } 101 | #[test] 102 | fn time_exceeded() { 103 | assert_eq!( 104 | SubmitStatus::from_str("przekroczony czas").unwrap(), 105 | SubmitStatus::TimeExceeded 106 | ); 107 | } 108 | #[test] 109 | fn compile_error() { 110 | assert_eq!( 111 | SubmitStatus::from_str("bĹ\\x82Ä\\x85d kompilacji").unwrap(), 112 | SubmitStatus::CompileError 113 | ); 114 | } 115 | #[test] 116 | fn no_header() { 117 | assert_eq!( 118 | SubmitStatus::from_str("brak nagĹ\\x82Ăłwka").unwrap(), 119 | SubmitStatus::NoHeader 120 | ); 121 | } 122 | 123 | #[test] 124 | fn runtime_error() { 125 | assert_eq!( 126 | SubmitStatus::from_str("bĹ\\x82Ä\\x85d wykonania").unwrap(), 127 | SubmitStatus::RuntimeError 128 | ); 129 | } 130 | 131 | // todo: find out the real string 132 | // #[test] 133 | // fn manually_rejected() { 134 | // assert_eq!( 135 | // SubmitStatus::from_str("recznie odrzucono").unwrap(), 136 | // SubmitStatus::ManuallyRejected 137 | // ); 138 | // } 139 | 140 | #[test] 141 | fn real_time_exceeded() { 142 | assert_eq!( 143 | SubmitStatus::from_str("bĹ\\x82Ä\\x85d wykonania: przekroczony real time").unwrap(), 144 | SubmitStatus::RealTimeExceeded 145 | ); 146 | } 147 | 148 | #[test] 149 | fn unknown() { 150 | assert_eq!( 151 | SubmitStatus::from_str("unknown status").unwrap(), 152 | SubmitStatus::WrongAnswer 153 | ); 154 | } 155 | } 156 | 157 | mod corrected_ascii_strings { 158 | use super::*; 159 | 160 | #[test] 161 | fn accepted() { 162 | assert_eq!( 163 | SubmitStatus::from_str("program zaakceptowany").unwrap(), 164 | SubmitStatus::Ok 165 | ); 166 | } 167 | 168 | #[test] 169 | fn in_queue() { 170 | assert_eq!( 171 | SubmitStatus::from_str("w kolejce").unwrap(), 172 | SubmitStatus::InQueue 173 | ); 174 | } 175 | 176 | #[test] 177 | fn processing() { 178 | assert_eq!( 179 | SubmitStatus::from_str("czekanie na przetworzenie").unwrap(), 180 | SubmitStatus::Processing 181 | ); 182 | } 183 | #[test] 184 | fn wrong_answer() { 185 | assert_eq!( 186 | SubmitStatus::from_str("zla odpowiedz").unwrap(), 187 | SubmitStatus::WrongAnswer 188 | ); 189 | } 190 | #[test] 191 | fn time_exceeded() { 192 | assert_eq!( 193 | SubmitStatus::from_str("przekroczony czas").unwrap(), 194 | SubmitStatus::TimeExceeded 195 | ); 196 | } 197 | #[test] 198 | fn compile_error() { 199 | assert_eq!( 200 | SubmitStatus::from_str("blad kompilacji").unwrap(), 201 | SubmitStatus::CompileError 202 | ); 203 | } 204 | #[test] 205 | fn no_header() { 206 | assert_eq!( 207 | SubmitStatus::from_str("brak naglowka").unwrap(), 208 | SubmitStatus::NoHeader 209 | ); 210 | } 211 | 212 | #[test] 213 | fn runtime_error() { 214 | assert_eq!( 215 | SubmitStatus::from_str("blad wykonania").unwrap(), 216 | SubmitStatus::RuntimeError 217 | ); 218 | } 219 | 220 | #[test] 221 | fn manually_rejected() { 222 | assert_eq!( 223 | SubmitStatus::from_str("recznie odrzucone").unwrap(), 224 | SubmitStatus::ManuallyRejected 225 | ); 226 | } 227 | 228 | #[test] 229 | fn real_time_exceeded() { 230 | assert_eq!( 231 | SubmitStatus::from_str("blad wykonania: przekroczony real time").unwrap(), 232 | SubmitStatus::RealTimeExceeded 233 | ); 234 | } 235 | 236 | #[test] 237 | fn internal_error() { 238 | assert_eq!( 239 | SubmitStatus::from_str("blad wewnetrzny testerki").unwrap(), 240 | SubmitStatus::InternalError 241 | ); 242 | } 243 | 244 | #[test] 245 | fn output_size_exceeded() { 246 | assert_eq!( 247 | SubmitStatus::from_str("przekroczony rozmiar wyjscia").unwrap(), 248 | SubmitStatus::OutputSizeExceeded 249 | ); 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/workspace/no_main.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use regex::Regex; 3 | use std::fs; 4 | use std::fs::File; 5 | use std::io::Write; 6 | use std::path::{Path, PathBuf}; 7 | use tracing::{debug, info}; 8 | 9 | pub fn remove_main

(input_file: P) -> Result 10 | where 11 | P: AsRef, 12 | { 13 | let input_file: &Path = input_file.as_ref(); 14 | info!("Removing main from {:?}", input_file); 15 | 16 | let content = fs::read_to_string(input_file)?; 17 | let content = strip_main(&content); 18 | 19 | let filepath = 20 | std::env::temp_dir().join(input_file.file_name().ok_or(Error::InputFileDoesNotExist)?); 21 | let mut file = File::create(filepath.clone())?; 22 | file.write_all(content.as_ref())?; 23 | 24 | debug!("New input file path: {:?}", filepath); 25 | debug!("New input file content:\n{}", content); 26 | 27 | Ok(filepath) 28 | } 29 | 30 | fn strip_main(content: &str) -> String { 31 | let re = Regex::new(r#"int\s+main"#).unwrap(); 32 | let f = re.find(content); 33 | 34 | if f.is_none() { 35 | return content.to_string(); 36 | } 37 | let f = f.unwrap(); 38 | 39 | let mut brackets = 0; 40 | let mut end = 0; 41 | let mut i = f.end(); 42 | let mut first_bracket = false; 43 | 44 | for c in content.chars().skip(f.end()) { 45 | i += 1; 46 | 47 | if c == '{' { 48 | brackets += 1; 49 | first_bracket = true; 50 | } else if c == '}' { 51 | brackets -= 1; 52 | } 53 | 54 | if first_bracket && brackets == 0 { 55 | end = i; 56 | break; 57 | } 58 | } 59 | 60 | let mut result = content.to_string(); 61 | result.replace_range(f.start()..end, ""); 62 | result 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use super::*; 68 | use mockall::{predicate, Predicate}; 69 | 70 | #[test] 71 | fn no_main_present() { 72 | let input = r#" 73 | // Hubert Jaremko 74 | #include 75 | 76 | void Add(int** arr, int*[] arr2) 77 | { 78 | std::cout << "Hello\n"; 79 | return; 80 | } 81 | 82 | void main() { 83 | } 84 | "#; 85 | 86 | let actual = strip_main(input); 87 | assert_eq!(actual, input); 88 | } 89 | 90 | #[test] 91 | fn minimal_main() { 92 | let input = r#"int main() { 93 | }"#; 94 | let expected = r#""#; 95 | let actual = strip_main(input); 96 | 97 | assert_eq!(actual, expected); 98 | } 99 | 100 | #[test] 101 | fn one_line() { 102 | let input = r#"int main() {} int foo() { return 5; }"#; 103 | let expected = r#" int foo() { return 5; }"#; 104 | let actual = strip_main(input); 105 | 106 | assert_eq!(actual, expected); 107 | } 108 | 109 | #[test] 110 | fn properly_formatted() { 111 | let input = r#" 112 | // Hubert Jaremko 113 | #include 114 | 115 | void Add(int** arr, int*[] arr2) 116 | { 117 | std::cout << "Hello\n"; 118 | return; 119 | } 120 | 121 | int main() 122 | { 123 | Add(..., ...); 124 | return 0; 125 | } 126 | 127 | "#; 128 | 129 | let expected = r#" 130 | // Hubert Jaremko 131 | #include 132 | 133 | void Add(int** arr, int*[] arr2) 134 | { 135 | std::cout << "Hello\n"; 136 | return; 137 | } 138 | 139 | 140 | 141 | "#; 142 | 143 | let actual = strip_main(input); 144 | assert_eq!(actual, expected); 145 | } 146 | 147 | #[test] 148 | fn with_arguments() { 149 | let input = r#" 150 | // Hubert Jaremko 151 | #include 152 | 153 | void Add(int** arr, int*[] arr2) 154 | { 155 | std::cout << "Hello\n"; 156 | return; 157 | } 158 | 159 | int main(int argc, char** argv) 160 | { 161 | Add(..., ...); 162 | return 0; 163 | } 164 | "#; 165 | 166 | let expected = r#" 167 | // Hubert Jaremko 168 | #include 169 | 170 | void Add(int** arr, int*[] arr2) 171 | { 172 | std::cout << "Hello\n"; 173 | return; 174 | } 175 | 176 | 177 | "#; 178 | 179 | let actual = strip_main(input); 180 | assert_eq!(actual, expected); 181 | } 182 | 183 | #[test] 184 | fn with_another_function_below() { 185 | let input = r#" 186 | // Hubert Jaremko 187 | #include 188 | 189 | void Add(int** arr, int*[] arr2) 190 | { 191 | std::cout << "Hello\n"; 192 | return; 193 | } 194 | 195 | int main(int argc, char** argv) 196 | { 197 | Add(..., ...); 198 | return 0; 199 | } 200 | 201 | int moin(int argc, char** argv) 202 | { 203 | return 5; 204 | } 205 | "#; 206 | 207 | let expected = r#" 208 | // Hubert Jaremko 209 | #include 210 | 211 | void Add(int** arr, int*[] arr2) 212 | { 213 | std::cout << "Hello\n"; 214 | return; 215 | } 216 | 217 | 218 | 219 | int moin(int argc, char** argv) 220 | { 221 | return 5; 222 | } 223 | "#; 224 | 225 | let actual = strip_main(input); 226 | assert_eq!(actual, expected); 227 | } 228 | 229 | #[test] 230 | fn with_nested_brackets() { 231 | let input = r#" 232 | // Hubert Jaremko 233 | #include 234 | 235 | void Add(int** arr, int*[] arr2) 236 | { 237 | std::cout << "Hello\n"; 238 | return; 239 | } 240 | 241 | int main(int argc, char** argv) 242 | { 243 | Add(..., ...); 244 | { 245 | if ( test) { 246 | {}{} 247 | } 248 | } 249 | return 0; 250 | } 251 | 252 | int moin(int argc, char** argv) 253 | { 254 | return 5; 255 | } 256 | "#; 257 | 258 | let expected = r#" 259 | // Hubert Jaremko 260 | #include 261 | 262 | void Add(int** arr, int*[] arr2) 263 | { 264 | std::cout << "Hello\n"; 265 | return; 266 | } 267 | 268 | 269 | 270 | int moin(int argc, char** argv) 271 | { 272 | return 5; 273 | } 274 | "#; 275 | 276 | let actual = strip_main(input); 277 | assert_eq!(actual, expected); 278 | } 279 | 280 | #[test] 281 | fn with_stretched_main() { 282 | let input = r#" 283 | // Hubert Jaremko 284 | #include 285 | 286 | void Add(int** arr, int*[] arr2) 287 | { 288 | std::cout << "Hello\n"; 289 | return; 290 | } 291 | 292 | int 293 | 294 | 295 | 296 | main 297 | 298 | 299 | 300 | (int argc, 301 | 302 | 303 | char** argv 304 | 305 | ) 306 | { 307 | Add(..., ...); 308 | { 309 | if ( test) { 310 | {}{} 311 | } 312 | } 313 | return 0; 314 | } 315 | 316 | int moin(int argc, char** argv) 317 | { 318 | return 5; 319 | } 320 | "#; 321 | 322 | let expected = r#" 323 | // Hubert Jaremko 324 | #include 325 | 326 | void Add(int** arr, int*[] arr2) 327 | { 328 | std::cout << "Hello\n"; 329 | return; 330 | } 331 | 332 | 333 | 334 | int moin(int argc, char** argv) 335 | { 336 | return 5; 337 | } 338 | "#; 339 | 340 | let actual = strip_main(input); 341 | assert_eq!(actual, expected); 342 | } 343 | 344 | #[test] 345 | fn saved_file_should_contain_stripped_content() { 346 | let input = r#" 347 | // Hubert Jaremko 348 | #include 349 | 350 | void Add(int** arr, int*[] arr2) 351 | { 352 | std::cout << "Hello\n"; 353 | return; 354 | } 355 | 356 | int main(int argc, char** argv) 357 | { 358 | Add(..., ...); 359 | { 360 | if ( test) { 361 | {}{} 362 | } 363 | } 364 | return 0; 365 | } 366 | 367 | int moin(int argc, char** argv) 368 | { 369 | return 5; 370 | } 371 | "#; 372 | let expected = r#" 373 | // Hubert Jaremko 374 | #include 375 | 376 | void Add(int** arr, int*[] arr2) 377 | { 378 | std::cout << "Hello\n"; 379 | return; 380 | } 381 | 382 | 383 | 384 | int moin(int argc, char** argv) 385 | { 386 | return 5; 387 | } 388 | "#; 389 | let original_filepath = std::env::temp_dir().join("input.cpp"); 390 | let mut original_file = File::create(original_filepath.clone()).unwrap(); 391 | original_file.write_all(input.as_ref()).unwrap(); 392 | 393 | let actual_filepath = remove_main(&original_filepath).unwrap(); 394 | 395 | assert!(predicate::path::exists().eval(&actual_filepath)); 396 | assert!(predicate::path::eq_file(&actual_filepath) 397 | .utf8() 398 | .unwrap() 399 | .eval(expected)); 400 | assert_eq!(actual_filepath.file_name().unwrap(), "input.cpp"); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # baca-cli [![Build](https://github.com/hjaremko/baca-cli/actions/workflows/build.yml/badge.svg)](https://github.com/hjaremko/baca-cli/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/hjaremko/baca-cli/branch/master/graph/badge.svg?token=CP9EWDCOMV)](https://codecov.io/gh/hjaremko/baca-cli) 2 | 3 | CLI client for the Jagiellonian University's BaCa online judge 4 | 5 | ![Preview](https://i.imgur.com/xOAHuXk.png) 6 | 7 | ## Installation 8 | 9 | Using `cargo` is recommended. The latest release binaries can be downloaded **[here](https://github.com/hjaremko/baca-cli/releases/latest)**. 10 | 11 | #### Cargo 12 | ```shell 13 | cargo install --git https://github.com/hjaremko/baca-cli.git 14 | ``` 15 | #### Linux 16 | ```sh 17 | $ curl -Lo baca.zip https://github.com/hjaremko/baca-cli/releases/download/v0.6.0/baca-0.6.0-linux.zip 18 | $ unzip baca.zip 19 | $ sudo install baca /usr/local/bin/ 20 | ``` 21 | #### macOS 22 | ```sh 23 | $ curl -Lo baca.zip https://github.com/hjaremko/baca-cli/releases/download/v0.6.0/baca-0.6.0-linux.zip 24 | $ unzip baca.zip 25 | $ sudo cp baca /usr/local/bin/ 26 | ``` 27 | #### Windows 28 | Download the raw binary and place it in your `PATH` ([What is `PATH`?](https://en.wikipedia.org/wiki/PATH_(variable))). 29 | 30 | #### Arch Linux (not maintained) 31 | 32 | Download the release from [AUR](https://aur.archlinux.org/packages/baca-cli) and install it using your 33 | favourite AUR helper or directly from source: 34 | 35 | ``` 36 | sudo pacman -S base-devel git 37 | git clone https://aur.archlinux.org/baca-cli.git 38 | cd baca-cli 39 | makepkg -sic 40 | ``` 41 | 42 | ### Dependencies (Linux) 43 | 44 | ``` 45 | sudo apt install pkg-config libssl-dev 46 | ``` 47 | 48 | ## Usage 49 | 50 | ``` 51 | baca [OPTIONS] [COMMAND] 52 | ``` 53 | 54 | ``` 55 | Commands: 56 | init Initialise the current directory as a BaCa workspace 57 | details Get submit details 58 | refresh Refresh session, use in case of a cookie expiration 59 | log Print the last N (default 3) submits 60 | tasks Print available tasks 61 | submit Make a submit 62 | last Print details of the last submit 63 | config Open a editor to edit BaCa configuration 64 | clear Remove the whole `.baca` directory 65 | help Print this message or the help of the given subcommand(s) 66 | 67 | Options: 68 | -v, --verbose... Sets the level of log verbosity 69 | -u, --no-update Disable update check 70 | -U, --force-update Force update check 71 | -h, --help Print help 72 | -V, --version Print version 73 | 74 | ``` 75 | 76 | ### Workspace initialization: `init` 77 | 78 | Initializes current directory as BaCa workspace, similar to `git init`. Currently, passwords are stored in **plain 79 | text.** 80 | User will be asked for credentials, if not provided. 81 | 82 | ``` 83 | baca init [OPTIONS] 84 | ``` 85 | 86 | ``` 87 | Options: 88 | --host BaCa hostname, ex. mn2020 89 | -l, --login BaCa login 90 | -p, --password BaCa password 91 | -h, --help Print help 92 | ``` 93 | 94 | Example, running on `Metody numeryczne 2019/2020` with no login prompt: 95 | 96 | ``` 97 | baca init --host mn2020 --login jaremko --password PaSsWorD 98 | ``` 99 | 100 | ### Re-login: `refresh` 101 | 102 | Refreshes session, use in case of cookie expiration. 103 | 104 | ``` 105 | baca refresh 106 | ``` 107 | 108 | ### Submit: `submit` 109 | 110 | Submits given file to specified task. Will prompt the user for task, if not provided. 111 | **Submits with no comment on the first line (header) will fail. Please include header.** 112 | 113 | - Optional parameter `--task ` explicitly sets problem to submit to. Use `baca tasks` to see what ids are available. 114 | - Optional parameter `--zip` will zip given file before submitting. The archive is saved as **`source.zip`**. 115 | - Optional parameter `--rename` will rename file before submitting and zipping. 116 | - Optional parameter `--no-main` will remove main function from C/C++ files before submitting and zipping. 117 | - Optional parameter `--no-polish` will remove non-unicode characters from files before submitting and zipping. 118 | - Optional parameter `--language ` explicitly sets input file language. 119 | - Optional parameter `--skip-header` disabled header verification. Use in case of a non-standard header. 120 | - `submit config` opens editor to edit submit config. 121 | - `submit clear` clears saved submit config. 122 | 123 | ``` 124 | Usage: baca submit [OPTIONS] [COMMAND] 125 | 126 | Commands: 127 | config Open a editor to edit submit config 128 | clear Clear saved submit config 129 | help Print this message or the help of the given subcommand(s) 130 | 131 | Options: 132 | -t, --task Task id, use 'baca tasks' to see what ids are available, overrides saved task id 133 | -f, --file A file to submit, overrides saved path 134 | -l, --language Task language. Please provide it exactly as is displayed on BaCa 135 | -r, --rename Submit input file under different name 136 | -s, --save Save task config. If provided, future 'submit' calls won't require providing task config 137 | -z, --zip Zip files to 'source.zip' before submitting, overrides saved config 138 | --no-save Do not ask for save 139 | --no-main Remove main function before submitting. Takes effect only on C/C++ files 140 | --no-polish Transliterate Unicode strings in the input file into pure ASCII, effectively removing Polish diacritics 141 | --skip-header Skip header verification 142 | -h, --help Print help 143 | ``` 144 | 145 | Example: 146 | 147 | ``` 148 | > baca submit -f hello.cpp 149 | ✔ Choose task: · [E] Metoda SOR 150 | Submitting hello.cpp to task [E] Metoda SOR (C++ with file support). 151 | ``` 152 | 153 | #### Saving task info 154 | 155 | If you don't want to type task info (id and filename) every time you submit, use `--save` flag to save it. Keep 156 | in mind that the config provided through parameters will override saved data. To completely remove saved data 157 | use `baca submit clear`. To disable automatic prompt for save, use `--no-save`. 158 | 159 | Example: 160 | 161 | ``` 162 | > baca submit -f hello.cpp -t 5 --save 163 | Submitting hello.cpp to task [E] Metoda SOR (C++ with file support). 164 | > baca submit 165 | Submitting hello.cpp to task [E] Metoda SOR (C++ with file support). 166 | ``` 167 | 168 | ### Recent submits: `log` 169 | 170 | Prints statuses of a couple of recent submits (default 3). Parameter `-t ` lets you print logs for a specific 171 | task. Task ID can be found through `baca tasks`. 172 | 173 | ``` 174 | baca log [optional: number, default 3] [optional: -t ] 175 | ``` 176 | 177 | Example: 178 | 179 | ``` 180 | > baca log 181 | 182 | ● [G] Funkcje sklejane - C++ - 2020-05-17 18:53:09 - submit 4334 183 | ├─── 100% - 4 pts - Ok 184 | └─── https://baca.ii.uj.edu.pl/mn2020/#SubmitDetails/4334 185 | 186 | ● [G] Funkcje sklejane - C++ - 2020-05-17 16:57:22 - submit 4328 187 | ├─── 100% - 4 pts - Ok 188 | └─── https://baca.ii.uj.edu.pl/mn2020/#SubmitDetails/4328 189 | 190 | ● [G] Funkcje sklejane - C++ - 2020-05-17 16:53:41 - submit 4326 191 | ├─── 0% - 0 pts - WrongAnswer 192 | └─── https://baca.ii.uj.edu.pl/mn2020/#SubmitDetails/4326 193 | ``` 194 | 195 | ### Last submit details: `last` 196 | 197 | Prints details of the last submit. Requires workspace to be initialized. Parameter `-t ` lets you print logs 198 | for a specific task. Task ID can be found through `baca tasks`. 199 | 200 | ``` 201 | baca last [optional: -t ] 202 | ``` 203 | 204 | Example: 205 | 206 | ``` 207 | > baca last 208 | 209 | ● [G] Funkcje sklejane - C++ - 2020-05-17 18:53:09 - submit 4334 210 | ├─── 100% - 4/4 pts - Ok 211 | └─── https://baca.ii.uj.edu.pl/mn2020/#SubmitDetails/4334 212 | ✅ ── test0/0 - Ok 213 | ✅ ── test1/0 - Ok 214 | ✅ ── test2/0 - Ok 215 | ✅ ── test3/0 - Ok 216 | ``` 217 | 218 | ### Any submit details: `details` 219 | 220 | Prints details of a given submit. Requires workspace to be initialized. 221 | 222 | ``` 223 | baca details 224 | ``` 225 | 226 | Example: 227 | 228 | ``` 229 | > baca details 2904 230 | 231 | ● [D] Skalowany Gauss - C++ - 2020-04-22 19:20:07 - submit 2904 232 | ├─── 89% - 3.58/4 pts - TimeExceeded 233 | └─── https://baca.ii.uj.edu.pl/mn2020/#SubmitDetails/2904 234 | ✅ ── testy_jawne/test1 - Ok 235 | ✅ ── testy_jawne/test2 - Ok 236 | ✅ ── testy_jawne/test3 - Ok 237 | ✅ ── testy_jawne/test4 - Ok 238 | ✅ ── testy_jawne/test5 - Ok 239 | ✅ ── testy_jawne/test6 - Ok 240 | ✅ ── testy_jawne/test8 - Ok 241 | ✅ ── testy/test0 - Ok 242 | ✅ ── testy/test1 - Ok 243 | ❌ ── testy/test10 - TimeExceeded 244 | ❌ ── testy/test11 - TimeExceeded 245 | ✅ ── testy/test2 - Ok 246 | ✅ ── testy/test3 - Ok 247 | ✅ ── testy/test4 - Ok 248 | ✅ ── testy/test5 - Ok 249 | ✅ ── testy/test6 - Ok 250 | ✅ ── testy/test7 - Ok 251 | ✅ ── testy/test8 - Ok 252 | ✅ ── testy/test9 - Ok 253 | ``` 254 | 255 | ### All tasks: `tasks` 256 | 257 | Prints all tasks. 258 | 259 | ``` 260 | baca tasks 261 | ``` 262 | 263 | Example: 264 | 265 | ``` 266 | > baca tasks 267 | 268 | ● Id: 1 - [A] Zera funkcji - 69 OK 269 | ● Id: 2 - [B] Metoda Newtona - 58 OK 270 | ● Id: 3 - [C] FAD\x3Csup\x3E2\x3C/sup\x3E - Pochodne mieszane - 62 OK 271 | ● Id: 4 - [D] Skalowany Gauss - 52 OK 272 | ● Id: 5 - [E] Metoda SOR - 64 OK 273 | ● Id: 6 - [F] Interpolacja - 63 OK 274 | ● Id: 7 - [G] Funkcje sklejane - 59 OK 275 | ● Id: 8 - A2 - 1 OK 276 | ● Id: 9 - B2 - 2 OK 277 | ● Id: 10 - C2 - 1 OK 278 | ● Id: 11 - D2 - 2 OK 279 | ● Id: 12 - E2 - 1 OK 280 | ● Id: 13 - F2 - 3 OK 281 | ● Id: 14 - G2 - 2 OK 282 | ``` 283 | 284 | ## Environment variables 285 | 286 | ### Settings for update check 287 | 288 | ``` 289 | GITHUB_USER=hjaremko 290 | GITHUB_REPO=baca-cli 291 | AUTH_TOKEN= # auth GitHub API requests (increases API call limit) 292 | ``` 293 | 294 | ## Compilation 295 | 296 | ``` 297 | cargo build --release 298 | ``` 299 | 300 | ## Running tests 301 | 302 | Some tests require credentials to a actual BaCa server, which can be set using environment variables. These tests are 303 | disabled by default, but you can try running them with the command `cargo test -- --ignored`. 304 | 305 | ``` 306 | TEST_BACA_LOGIN= 307 | TEST_BACA_PASSWORD= 308 | TEST_BACA_HOST= 309 | ``` 310 | 311 | ## Setting log levels 312 | 313 | Log levels are configured by a `-v` flag. 314 | 315 | - `no flag` - no additional logs 316 | - `-v` - **info** 317 | - `-vv` - **debug** 318 | - `-vvv or more` - **trace** 319 | -------------------------------------------------------------------------------- /src/workspace/submit_config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::model::Language; 3 | use crate::workspace::{ConfigObject, Workspace}; 4 | use merge::Merge; 5 | use serde::{Deserialize, Serialize}; 6 | use std::path::Path; 7 | use std::path::PathBuf; 8 | 9 | fn merge_left_option(left: &mut Option, right: Option) { 10 | if let Some(right) = right { 11 | let _ = left.insert(right); 12 | } 13 | } 14 | 15 | #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Merge, Clone)] 16 | pub struct SubmitConfig { 17 | #[merge(strategy = merge_left_option)] 18 | pub id: Option, 19 | #[merge(strategy = merge_left_option)] 20 | pub file: Option, 21 | #[merge(strategy = merge::bool::overwrite_false)] 22 | pub to_zip: bool, 23 | #[merge(strategy = merge_left_option)] 24 | pub language: Option, 25 | #[merge(strategy = merge_left_option)] 26 | pub rename_as: Option, 27 | #[merge(strategy = merge::bool::overwrite_false)] 28 | pub no_main: bool, 29 | #[merge(strategy = merge::bool::overwrite_false)] 30 | pub no_polish: bool, 31 | #[merge(strategy = merge::bool::overwrite_false)] 32 | pub skip_header: bool, 33 | } 34 | 35 | impl SubmitConfig { 36 | #[cfg(test)] 37 | pub fn new( 38 | id: &str, 39 | file: &Path, 40 | to_zip: bool, 41 | language: Language, 42 | rename_as: Option, 43 | ) -> Self { 44 | Self { 45 | id: id.to_string().into(), 46 | file: file.to_owned().into(), 47 | to_zip, 48 | language: language.into(), 49 | rename_as, 50 | no_main: false, 51 | no_polish: false, 52 | skip_header: false, 53 | } 54 | } 55 | 56 | pub fn id(&self) -> Option<&String> { 57 | self.id.as_ref() 58 | } 59 | 60 | pub fn file(&self) -> Option<&Path> { 61 | self.file.as_deref() 62 | } 63 | 64 | pub fn try_set_file

(&mut self, filepath: Option

) -> crate::error::Result<()> 65 | where 66 | P: Into, 67 | { 68 | let file = match filepath { 69 | None => None, 70 | Some(file) => { 71 | let path = file.into(); 72 | 73 | if !path.exists() { 74 | return Err(Error::InputFileDoesNotExist); 75 | } 76 | 77 | Some(path.canonicalize()?) 78 | } 79 | }; 80 | self.file = file; 81 | Ok(()) 82 | } 83 | } 84 | 85 | impl ConfigObject for SubmitConfig { 86 | fn save_config(&self, workspace: &W) -> crate::error::Result<()> { 87 | workspace.save_config_object(self) 88 | } 89 | 90 | fn read_config(workspace: &W) -> crate::error::Result { 91 | workspace.read_config_object::() 92 | } 93 | 94 | fn remove_config(workspace: &W) -> crate::error::Result<()> { 95 | workspace.remove_config_object::() 96 | } 97 | 98 | fn config_filename() -> String { 99 | "submit".to_string() 100 | } 101 | } 102 | 103 | #[cfg(test)] 104 | mod tests { 105 | use super::*; 106 | use crate::model::Language; 107 | use crate::workspace::workspace_dir::tests::make_temp_workspace; 108 | use assert_fs::fixture::ChildPath; 109 | use assert_fs::prelude::*; 110 | use predicates::prelude::*; 111 | use std::ops::Not; 112 | use std::str::FromStr; 113 | 114 | #[test] 115 | fn save_read_task_success() { 116 | let (temp_dir, mock_paths, workspace) = make_temp_workspace().unwrap(); 117 | let input_file = temp_dir.child("foo.sh"); 118 | input_file.touch().unwrap(); 119 | let expected_submit_config = 120 | SubmitConfig::new("2", input_file.path(), false, Language::Bash, None); 121 | 122 | workspace.initialize().unwrap(); 123 | expected_submit_config.save_config(&workspace).unwrap(); 124 | 125 | assert_eq!( 126 | SubmitConfig::read_config(&workspace).unwrap(), 127 | expected_submit_config 128 | ); 129 | assert!(predicate::path::exists().eval(mock_paths.config_path::().as_path())); 130 | temp_dir.close().unwrap(); 131 | } 132 | 133 | #[test] 134 | fn read_corrupted_task() { 135 | let (temp_dir, mock_paths, workspace) = make_temp_workspace().unwrap(); 136 | let corrupted_submit_config = ChildPath::new(mock_paths.config_path::()); 137 | 138 | workspace.initialize().unwrap(); 139 | corrupted_submit_config.write_str("invalid config").unwrap(); 140 | let result = SubmitConfig::read_config(&workspace); 141 | 142 | assert!(result.is_err()); 143 | if let Err(e) = result { 144 | assert!(matches!(e, Error::WorkspaceCorrupted)); 145 | } 146 | assert!(predicate::path::exists().eval(mock_paths.config_path::().as_path())); 147 | temp_dir.close().unwrap(); 148 | } 149 | 150 | #[test] 151 | fn read_no_task_exists() { 152 | let (temp_dir, mock_paths, workspace) = make_temp_workspace().unwrap(); 153 | 154 | workspace.initialize().unwrap(); 155 | let result = SubmitConfig::read_config(&workspace); 156 | 157 | assert!(result.is_err()); 158 | if let Err(e) = result { 159 | assert!(matches!(e, Error::ReadingConfig(_)), "error = {:?}", e); 160 | } 161 | assert!(predicate::path::missing().eval(mock_paths.config_path::().as_path())); 162 | temp_dir.close().unwrap(); 163 | } 164 | 165 | #[test] 166 | fn save_task_not_initialized() { 167 | let (temp_dir, mock_paths, workspace) = make_temp_workspace().unwrap(); 168 | 169 | let submit_config = 170 | SubmitConfig::new("2", Path::new("foo.txt"), true, Language::Bash, None); 171 | let result = submit_config.save_config(&workspace); 172 | 173 | assert!(result.is_err()); 174 | if let Err(e) = result { 175 | assert!(matches!(e, Error::WorkspaceNotInitialized)); 176 | } 177 | assert!(predicate::path::missing().eval(mock_paths.config_path::().as_path())); 178 | temp_dir.close().unwrap(); 179 | } 180 | 181 | #[test] 182 | fn save_task_should_override() { 183 | let (temp_dir, mock_paths, workspace) = make_temp_workspace().unwrap(); 184 | let input_file = temp_dir.child("foo.sh"); 185 | input_file.touch().unwrap(); 186 | let submit_config_first = 187 | SubmitConfig::new("2", input_file.path(), false, Language::Bash, None); 188 | let submit_config_second = 189 | SubmitConfig::new("3", Path::new("bar.cpp"), false, Language::Cpp, None); 190 | 191 | workspace.initialize().unwrap(); 192 | submit_config_first.save_config(&workspace).unwrap(); 193 | submit_config_second.save_config(&workspace).unwrap(); 194 | 195 | assert_eq!( 196 | SubmitConfig::read_config(&workspace).unwrap(), 197 | submit_config_second 198 | ); 199 | assert!(predicate::path::exists().eval(mock_paths.config_path::().as_path())); 200 | temp_dir.close().unwrap(); 201 | } 202 | 203 | #[test] 204 | fn remove_submit_config() { 205 | let (temp_dir, mock_paths, workspace) = make_temp_workspace().unwrap(); 206 | let input_file = temp_dir.child("foo.sh"); 207 | input_file.touch().unwrap(); 208 | let submit_config = SubmitConfig::new("2", input_file.path(), false, Language::Bash, None); 209 | 210 | workspace.initialize().unwrap(); 211 | submit_config.save_config(&workspace).unwrap(); 212 | SubmitConfig::remove_config(&workspace).unwrap(); 213 | assert!(predicate::path::exists() 214 | .eval(mock_paths.config_path::().as_path()) 215 | .not()); 216 | temp_dir.close().unwrap(); 217 | } 218 | 219 | #[test] 220 | fn default_should_contain_none() { 221 | let default = SubmitConfig::default(); 222 | 223 | assert!(default.file.is_none()); 224 | assert!(default.language.is_none()); 225 | assert!(default.id.is_none()); 226 | assert!(default.rename_as.is_none()); 227 | assert!(!default.to_zip); 228 | assert!(!default.no_main); 229 | assert!(!default.no_polish); 230 | } 231 | 232 | #[test] 233 | fn merge_both_none() { 234 | let mut lhs = SubmitConfig::default(); 235 | let rhs = SubmitConfig::default(); 236 | lhs.merge(rhs); 237 | let merged = lhs; 238 | 239 | assert!(merged.file.is_none()); 240 | assert!(merged.language.is_none()); 241 | assert!(merged.id.is_none()); 242 | assert!(merged.rename_as.is_none()); 243 | assert!(!merged.to_zip); 244 | assert!(!merged.no_main); 245 | assert!(!merged.no_polish); 246 | } 247 | 248 | fn make_submit_config() -> SubmitConfig { 249 | SubmitConfig { 250 | id: "3".to_string().into(), 251 | file: PathBuf::from("file.txt").into(), 252 | to_zip: true, 253 | language: Language::from_str("C++").unwrap().into(), 254 | rename_as: "source.cpp".to_string().into(), 255 | no_main: true, 256 | no_polish: true, 257 | skip_header: false, 258 | } 259 | } 260 | 261 | #[test] 262 | fn merge_left_full() { 263 | let mut lhs = make_submit_config(); 264 | let rhs = SubmitConfig::default(); 265 | 266 | lhs.merge(rhs); 267 | let merged = lhs; 268 | 269 | assert_eq!(merged.file.unwrap().to_str().unwrap(), "file.txt"); 270 | assert_eq!(merged.language.unwrap(), Language::Cpp); 271 | assert_eq!(merged.id.unwrap(), "3"); 272 | assert_eq!(merged.rename_as.unwrap(), "source.cpp"); 273 | assert!(merged.to_zip); 274 | assert!(merged.no_main); 275 | assert!(merged.no_polish); 276 | } 277 | 278 | #[test] 279 | fn merge_right_full() { 280 | let mut lhs = make_submit_config(); 281 | lhs.language = Language::Java.into(); 282 | let mut rhs = make_submit_config(); 283 | rhs.merge(lhs); 284 | let merged = rhs; 285 | 286 | assert_eq!(merged.file.unwrap().to_str().unwrap(), "file.txt"); 287 | assert_eq!(merged.language.unwrap(), Language::Java); 288 | assert_eq!(merged.id.unwrap(), "3"); 289 | assert_eq!(merged.rename_as.unwrap(), "source.cpp"); 290 | assert!(merged.to_zip); 291 | assert!(merged.no_main); 292 | assert!(merged.no_polish); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/api/baca_service.rs: -------------------------------------------------------------------------------- 1 | use crate::api::baca_api::BacaApi; 2 | use crate::api::details::EMPTY_RESPONSE; 3 | use crate::api::Request; 4 | use crate::error::{Error, Result}; 5 | use crate::model::{Language, Results, Submit, Task, Tasks}; 6 | use crate::parse::from_baca_output::FromBacaOutput; 7 | use crate::workspace::ConnectionConfig; 8 | use reqwest::blocking::Response; 9 | use std::str::FromStr; 10 | use tracing::{debug, info}; 11 | 12 | #[derive(Default)] 13 | pub struct BacaService {} 14 | 15 | impl BacaApi for BacaService { 16 | fn get_cookie(&self, connection_config: &ConnectionConfig) -> Result { 17 | let login_response = Request::new(connection_config).login()?; 18 | log_response_details(&login_response); 19 | check_response_status(&login_response)?; 20 | extract_cookie(&login_response) 21 | } 22 | 23 | fn get_submit_details( 24 | &self, 25 | connection_config: &ConnectionConfig, 26 | submit_id: &str, 27 | ) -> Result { 28 | let resp = Request::new(connection_config).details(submit_id)?; 29 | check_response_status(&resp)?; 30 | let resp = resp.text()?; 31 | debug!("Received raw submit: {}", resp); 32 | 33 | if resp.contains("failed") { 34 | return Err(Error::InvalidSubmitId); 35 | } 36 | 37 | Ok(Submit::parse( 38 | connection_config, 39 | &check_for_empty_response(resp)?, 40 | )) 41 | } 42 | 43 | fn get_results(&self, connection_config: &ConnectionConfig) -> Result { 44 | let resp = Request::new(connection_config).results()?; 45 | check_response_status(&resp)?; 46 | let resp = resp.text().expect("Invalid submit data"); 47 | debug!("Received raw results: {}", resp); 48 | 49 | Ok(Results::from_baca_output( 50 | connection_config, 51 | &check_for_empty_response(resp)?, 52 | )) 53 | } 54 | 55 | fn get_results_by_task( 56 | &self, 57 | connection_config: &ConnectionConfig, 58 | task_id: &str, 59 | ) -> Result { 60 | let tasks = self.get_tasks(connection_config)?; 61 | let task = tasks.get_by_id(task_id)?; 62 | info!("Showing logs for task {}", &task.problem_name); 63 | Ok(self 64 | .get_results(connection_config)? 65 | .filter_by_task(&task.problem_name)) 66 | } 67 | 68 | fn get_tasks(&self, connection_config: &ConnectionConfig) -> Result { 69 | let resp = Request::new(connection_config).tasks()?; 70 | check_response_status(&resp)?; 71 | 72 | let resp = resp.text().expect("Invalid submit data"); 73 | debug!("Received raw tasks: {}", resp); 74 | 75 | Tasks::from_str(&check_for_empty_response(resp)?) 76 | } 77 | 78 | fn submit( 79 | &self, 80 | connection_config: &ConnectionConfig, 81 | task: &Task, 82 | file_path: &str, 83 | ) -> Result<()> { 84 | debug!("{:?}", task); 85 | let resp = Request::new(connection_config).submit(task, file_path)?; 86 | let resp = resp.text()?; 87 | debug!("Response: {}", resp); 88 | 89 | match resp.as_str() { 90 | "Niezalogowany jesteś" => Err(Error::LoggedOut), 91 | "Błąd" => Err(Error::TaskNotActive), 92 | _ => Ok(()), 93 | } 94 | } 95 | 96 | fn get_allowed_language( 97 | &self, 98 | connection_config: &ConnectionConfig, 99 | task_id: &str, 100 | ) -> Result> { 101 | let response = Request::new(connection_config).allowed_languages(task_id)?; 102 | check_response_status(&response)?; 103 | let response = response.text()?; 104 | debug!("Received raw allowed languages: {:?}", response); 105 | Ok(Option::::from_baca_output( 106 | connection_config, 107 | &response, 108 | )) 109 | } 110 | } 111 | 112 | fn log_response_details(login_response: &Response) { 113 | for (name, val) in login_response.headers() { 114 | debug!("Response header: {} = {:?}", name, val); 115 | } 116 | 117 | debug!("Status code: {}", login_response.status()); 118 | } 119 | 120 | fn extract_cookie(response: &Response) -> Result { 121 | let cookie = response 122 | .cookies() 123 | .next() 124 | .ok_or(Error::InvalidLoginOrPassword)?; 125 | 126 | debug!("Cookie: {} = {}", cookie.name(), cookie.value()); 127 | Ok(cookie.value().to_string()) 128 | } 129 | 130 | fn check_response_status(response: &Response) -> Result<()> { 131 | if response.status().as_str() == "404" { 132 | return Err(Error::InvalidHost); 133 | }; 134 | 135 | Ok(()) 136 | } 137 | 138 | fn check_for_empty_response(resp: String) -> Result { 139 | if resp == EMPTY_RESPONSE { 140 | Err(Error::LoggedOut) 141 | } else { 142 | Ok(resp) 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | mod tests { 148 | use super::*; 149 | use crate::api; 150 | use crate::model::Language::Unsupported; 151 | use std::fmt::Debug; 152 | 153 | fn make_correct_baca_invalid_session() -> ConnectionConfig { 154 | ConnectionConfig { 155 | host: "mn2020".to_string(), 156 | login: "login".to_string(), 157 | password: "pass".to_string(), 158 | permutation: api::details::permutation(), 159 | cookie: "invalid".to_string(), 160 | } 161 | } 162 | 163 | fn make_incorrect_baca() -> ConnectionConfig { 164 | ConnectionConfig { 165 | host: "invalid".to_string(), 166 | login: "login".to_string(), 167 | password: "pass".to_string(), 168 | permutation: "invalid".to_string(), 169 | cookie: "".to_string(), 170 | } 171 | } 172 | 173 | fn check_invalid_host(result: Result) 174 | where 175 | T: Debug, 176 | { 177 | let e = result.expect_err("Should fail"); 178 | assert!(matches!(e, Error::InvalidHost)); 179 | } 180 | 181 | fn check_invalid_login(result: Result) 182 | where 183 | T: Debug, 184 | { 185 | let e = result.expect_err("Should fail"); 186 | assert!(matches!(e, Error::InvalidLoginOrPassword)); 187 | } 188 | 189 | fn check_logged_out(result: Result) 190 | where 191 | T: Debug, 192 | { 193 | let e = result.expect_err("Should fail"); 194 | assert!(matches!(e, Error::LoggedOut)); 195 | } 196 | 197 | #[test] 198 | #[ignore] 199 | fn get_cookie_on_correct_host_should_fail_login() { 200 | let baca = make_correct_baca_invalid_session(); 201 | let api = BacaService::default(); 202 | let result = api.get_cookie(&baca); 203 | 204 | check_invalid_login(result) 205 | } 206 | 207 | #[test] 208 | #[ignore] 209 | fn get_cookie_on_incorrect_host_should_fail() { 210 | let baca = make_incorrect_baca(); 211 | let api = BacaService::default(); 212 | let result = api.get_cookie(&baca); 213 | 214 | check_invalid_host(result); 215 | } 216 | 217 | #[test] 218 | #[ignore] 219 | fn get_task_on_correct_host_should_succeed() { 220 | let baca = make_correct_baca_invalid_session(); 221 | let api = BacaService::default(); 222 | let actual = api.get_tasks(&baca).unwrap(); 223 | let expected = Tasks { 224 | tasks: vec![ 225 | Task::new("1", Unsupported, "[A] Zera funkcji", 69), 226 | Task::new("2", Unsupported, "[B] Metoda Newtona", 58), 227 | Task::new( 228 | "3", 229 | Unsupported, 230 | "[C] FAD\\x3Csup\\x3E2\\x3C/sup\\x3E - Pochodne mieszane", 231 | 62, 232 | ), 233 | Task::new("4", Unsupported, "[D] Skalowany Gauss", 52), 234 | Task::new("5", Unsupported, "[E] Metoda SOR", 64), 235 | Task::new("6", Unsupported, "[F] Interpolacja", 63), 236 | Task::new("7", Unsupported, "[G] Funkcje sklejane", 59), 237 | Task::new("8", Unsupported, "A2", 1), 238 | Task::new("9", Unsupported, "B2", 2), 239 | Task::new("10", Unsupported, "C2", 1), 240 | Task::new("11", Unsupported, "D2", 2), 241 | Task::new("12", Unsupported, "E2", 1), 242 | Task::new("13", Unsupported, "F2", 3), 243 | Task::new("14", Unsupported, "G2", 2), 244 | ], 245 | }; 246 | assert_eq!(actual, expected); 247 | } 248 | 249 | #[test] 250 | #[ignore] 251 | fn get_task_on_incorrect_host_should_fail() { 252 | let baca = make_incorrect_baca(); 253 | let api = BacaService::default(); 254 | let result = api.get_tasks(&baca); 255 | 256 | check_invalid_host(result); 257 | } 258 | 259 | #[test] 260 | #[ignore] 261 | fn get_details_on_incorrect_host_should_fail() { 262 | let baca = make_incorrect_baca(); 263 | let api = BacaService::default(); 264 | let result = api.get_submit_details(&baca, "123"); 265 | 266 | check_invalid_host(result); 267 | } 268 | 269 | #[test] 270 | #[ignore] 271 | fn get_details_on_incorrect_session_should_fail() { 272 | let baca = make_correct_baca_invalid_session(); 273 | let api = BacaService::default(); 274 | let result = api.get_submit_details(&baca, "123"); 275 | 276 | check_logged_out(result); 277 | } 278 | 279 | #[test] 280 | #[ignore] 281 | fn get_results_on_incorrect_host_should_fail() { 282 | let baca = make_incorrect_baca(); 283 | let api = BacaService::default(); 284 | let result = api.get_results(&baca); 285 | 286 | check_invalid_host(result); 287 | } 288 | 289 | #[test] 290 | #[ignore] 291 | fn get_results_on_incorrect_session_should_fail() { 292 | let baca = make_correct_baca_invalid_session(); 293 | let api = BacaService::default(); 294 | let result = api.get_results(&baca); 295 | 296 | check_logged_out(result); 297 | } 298 | 299 | #[test] 300 | #[ignore] 301 | fn get_languages_expired_task_should_return_empty() { 302 | let connection = make_correct_baca_invalid_session(); 303 | let api = BacaService::default(); 304 | let result = api.get_allowed_language(&connection, "1").unwrap(); 305 | 306 | assert!(result.is_none()); 307 | } 308 | 309 | #[test] 310 | #[ignore] 311 | fn get_languages_on_incorrect_host_should_fail() { 312 | let connection = make_incorrect_baca(); 313 | let api = BacaService::default(); 314 | let result = api.get_allowed_language(&connection, "1"); 315 | 316 | check_invalid_host(result); 317 | } 318 | 319 | #[test] 320 | #[ignore] 321 | fn get_languages_on_incorrect_task_should_return_empty() { 322 | let connection = make_correct_baca_invalid_session(); 323 | let api = BacaService::default(); 324 | let result = api.get_allowed_language(&connection, "12323").unwrap(); 325 | 326 | assert!(result.is_none()); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/command/init.rs: -------------------------------------------------------------------------------- 1 | use crate::api; 2 | use crate::api::baca_api::BacaApi; 3 | use crate::command::prompt::{Input, Password, Prompt}; 4 | use crate::command::Command; 5 | use crate::update::BacaRelease; 6 | use crate::workspace::{ConfigObject, Workspace}; 7 | use crate::{error, workspace}; 8 | use tracing::{debug, info}; 9 | 10 | pub struct Init { 11 | host: Option, 12 | login: Option, 13 | password: Option, 14 | login_prompt: Box, 15 | password_prompt: Box, 16 | host_prompt: Box, 17 | } 18 | 19 | impl Init { 20 | pub fn new(host: Option, login: Option, password: Option) -> Self { 21 | Self { 22 | host, 23 | login, 24 | password, 25 | login_prompt: Box::new(Input("Login")), 26 | password_prompt: Box::new(Password {}), 27 | host_prompt: Box::new(Input("Host")), 28 | } 29 | } 30 | } 31 | 32 | impl Init { 33 | fn get_host(&self) -> error::Result { 34 | if self.host.as_ref().is_none() { 35 | return self.host_prompt.interact(); 36 | } 37 | 38 | Ok(self.host.as_ref().unwrap().clone()) 39 | } 40 | 41 | fn get_login(&self) -> error::Result { 42 | if self.login.as_ref().is_none() { 43 | return self.login_prompt.interact(); 44 | } 45 | 46 | Ok(self.login.as_ref().unwrap().clone()) 47 | } 48 | 49 | fn get_password(&self) -> error::Result { 50 | if self.password.as_ref().is_none() { 51 | return self.password_prompt.interact(); 52 | } 53 | 54 | Ok(self.password.as_ref().unwrap().clone()) 55 | } 56 | } 57 | 58 | impl Command for Init { 59 | // todo: -r to override current config 60 | fn execute(self, workspace: &W, api: &A) -> error::Result<()> 61 | where 62 | W: Workspace, 63 | A: BacaApi, 64 | { 65 | info!("Initializing Baca workspace."); 66 | 67 | let host = self.get_host()?; 68 | let login = self.get_login()?; 69 | let password = self.get_password()?; 70 | 71 | debug!("Host: {}", host); 72 | debug!("Login: {}", login); 73 | debug!("Password: {}", password); 74 | 75 | let mut config = workspace::ConnectionConfig { 76 | host, 77 | login, 78 | password, 79 | permutation: api::details::permutation(), 80 | cookie: "".to_string(), 81 | }; 82 | 83 | let cleanup_directory = |e| match e { 84 | error::Error::WorkspaceAlreadyInitialized => e, 85 | _ => { 86 | workspace 87 | .remove_workspace() 88 | .expect("Cannot cleanup baca directory"); 89 | e 90 | } 91 | }; 92 | 93 | workspace.initialize().map_err(cleanup_directory)?; 94 | config.cookie = api.get_cookie(&config).map_err(cleanup_directory)?; 95 | config.save_config(workspace).map_err(cleanup_directory)?; 96 | save_version(workspace) 97 | } 98 | } 99 | 100 | fn save_version(workspace: &W) -> error::Result<()> { 101 | let version = BacaRelease::new(env!("CARGO_PKG_VERSION"), ""); 102 | version.save_config(workspace) 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use super::*; 108 | use crate::api; 109 | use crate::api::baca_api::MockBacaApi; 110 | use crate::command::prompt::MockPrompt; 111 | use crate::workspace::{ConnectionConfig, MockWorkspace}; 112 | 113 | // todo: tests::utils 114 | fn make_mock_connection_config() -> ConnectionConfig { 115 | ConnectionConfig { 116 | host: "host".to_string(), 117 | login: "login".to_string(), 118 | password: "pass".to_string(), 119 | permutation: api::details::permutation(), 120 | cookie: "".to_string(), 121 | } 122 | } 123 | 124 | fn make_never_called_prompt_mock() -> MockPrompt { 125 | let mut mock = MockPrompt::new(); 126 | mock.expect_interact().never(); 127 | mock 128 | } 129 | 130 | fn make_prompt_mock(return_val: &'static str) -> MockPrompt { 131 | let mut mock = MockPrompt::new(); 132 | mock.expect_interact() 133 | .once() 134 | .returning(move || Ok(return_val.to_string())); 135 | mock 136 | } 137 | 138 | fn make_baca_config( 139 | host: &'static str, 140 | login: &'static str, 141 | password: &'static str, 142 | ) -> ConnectionConfig { 143 | let host = host.to_string(); 144 | let login = login.to_string(); 145 | let password = password.to_string(); 146 | ConnectionConfig { 147 | host, 148 | login, 149 | password, 150 | permutation: api::details::permutation(), 151 | cookie: "ok_cookie".to_string(), 152 | } 153 | } 154 | 155 | #[test] 156 | fn success_test() { 157 | let mut mock_workspace = MockWorkspace::new(); 158 | mock_workspace 159 | .expect_initialize() 160 | .once() 161 | .returning(|| Ok(())); 162 | mock_workspace 163 | .expect_save_config_object::() 164 | .returning(|_| Ok(())); 165 | 166 | mock_workspace 167 | .expect_save_config_object() 168 | .withf(|x: &ConnectionConfig| { 169 | let mut expected = make_mock_connection_config(); 170 | expected.cookie = "ok_cookie".to_string(); 171 | 172 | *x == expected 173 | }) 174 | .returning(|_| Ok(())); 175 | 176 | let mut mock_api = MockBacaApi::new(); 177 | mock_api 178 | .expect_get_cookie() 179 | .withf(|x| *x == make_mock_connection_config()) 180 | .returning(|_| Ok("ok_cookie".to_string())); 181 | 182 | let init = Init { 183 | host: Some("host".to_string()), 184 | login: Some("login".to_string()), 185 | password: Some("pass".to_string()), 186 | login_prompt: Box::new(Input("Login")), 187 | password_prompt: Box::new(Password {}), 188 | host_prompt: Box::new(Input("Host")), 189 | }; 190 | let result = init.execute(&mock_workspace, &mock_api); 191 | assert!(result.is_ok()) 192 | } 193 | 194 | #[test] 195 | fn no_provided_login_should_invoke_prompt() { 196 | let login_prompt_mock = make_prompt_mock("prompt_login"); 197 | let password_prompt_mock = make_never_called_prompt_mock(); 198 | let host_prompt_mock = make_never_called_prompt_mock(); 199 | 200 | let mut mock_workspace = MockWorkspace::new(); 201 | mock_workspace 202 | .expect_initialize() 203 | .once() 204 | .returning(|| Ok(())); 205 | mock_workspace 206 | .expect_save_config_object::() 207 | .returning(|_| Ok(())); 208 | 209 | let expected_config = make_baca_config("host", "prompt_login", "pass"); 210 | let expected_cookie = expected_config.cookie.clone(); 211 | 212 | mock_workspace 213 | .expect_save_config_object() 214 | .withf(move |x: &ConnectionConfig| *x == expected_config) 215 | .returning(|_| Ok(())); 216 | 217 | let mut mock_api = MockBacaApi::new(); 218 | mock_api 219 | .expect_get_cookie() 220 | .returning(move |_| Ok(expected_cookie.clone())); 221 | 222 | let init = Init { 223 | host: Some("host".to_string()), 224 | login: None, 225 | password: Some("pass".to_string()), 226 | login_prompt: Box::new(login_prompt_mock), 227 | password_prompt: Box::new(password_prompt_mock), 228 | host_prompt: Box::new(host_prompt_mock), 229 | }; 230 | 231 | let result = init.execute(&mock_workspace, &mock_api); 232 | assert!(result.is_ok()) 233 | } 234 | 235 | #[test] 236 | fn no_provided_password_should_invoke_prompt() { 237 | let login_prompt_mock = make_never_called_prompt_mock(); 238 | let password_prompt_mock = make_prompt_mock("prompt_password"); 239 | let host_prompt_mock = make_never_called_prompt_mock(); 240 | 241 | let mut mock_workspace = MockWorkspace::new(); 242 | mock_workspace 243 | .expect_initialize() 244 | .once() 245 | .returning(|| Ok(())); 246 | mock_workspace 247 | .expect_save_config_object::() 248 | .returning(|_| Ok(())); 249 | 250 | let expected_config = make_baca_config("host", "login", "prompt_password"); 251 | let expected_cookie = expected_config.cookie.clone(); 252 | mock_workspace 253 | .expect_save_config_object() 254 | .withf(move |x: &ConnectionConfig| *x == expected_config) 255 | .returning(|_| Ok(())); 256 | 257 | let mut mock_api = MockBacaApi::new(); 258 | mock_api 259 | .expect_get_cookie() 260 | .returning(move |_| Ok(expected_cookie.clone())); 261 | 262 | let init = Init { 263 | host: Some("host".to_string()), 264 | login: Some("login".to_string()), 265 | password: None, 266 | login_prompt: Box::new(login_prompt_mock), 267 | password_prompt: Box::new(password_prompt_mock), 268 | host_prompt: Box::new(host_prompt_mock), 269 | }; 270 | 271 | let result = init.execute(&mock_workspace, &mock_api); 272 | assert!(result.is_ok()) 273 | } 274 | 275 | #[test] 276 | fn no_provided_login_and_password_should_invoke_prompt() { 277 | let login_prompt_mock = make_prompt_mock("prompt_login"); 278 | let password_prompt_mock = make_prompt_mock("prompt_password"); 279 | let host_prompt_mock = make_never_called_prompt_mock(); 280 | 281 | let mut mock_workspace = MockWorkspace::new(); 282 | mock_workspace 283 | .expect_initialize() 284 | .once() 285 | .returning(|| Ok(())); 286 | mock_workspace 287 | .expect_save_config_object::() 288 | .returning(|_| Ok(())); 289 | 290 | let expected_config = make_baca_config("host", "prompt_login", "prompt_password"); 291 | let expected_cookie = expected_config.cookie.clone(); 292 | mock_workspace 293 | .expect_save_config_object() 294 | .withf(move |x: &ConnectionConfig| *x == expected_config) 295 | .returning(|_| Ok(())); 296 | 297 | let mut mock_api = MockBacaApi::new(); 298 | mock_api 299 | .expect_get_cookie() 300 | .returning(move |_| Ok(expected_cookie.clone())); 301 | 302 | let init = Init { 303 | host: Some("host".to_string()), 304 | login: None, 305 | password: None, 306 | login_prompt: Box::new(login_prompt_mock), 307 | password_prompt: Box::new(password_prompt_mock), 308 | host_prompt: Box::new(host_prompt_mock), 309 | }; 310 | 311 | let result = init.execute(&mock_workspace, &mock_api); 312 | assert!(result.is_ok()) 313 | } 314 | 315 | #[test] 316 | fn no_provided_host_should_invoke_prompt() { 317 | let input_prompt_mock = make_never_called_prompt_mock(); 318 | let password_prompt_mock = make_never_called_prompt_mock(); 319 | let host_prompt_mock = make_prompt_mock("prompt_host"); 320 | 321 | let mut mock_workspace = MockWorkspace::new(); 322 | mock_workspace 323 | .expect_initialize() 324 | .once() 325 | .returning(|| Ok(())); 326 | mock_workspace 327 | .expect_save_config_object::() 328 | .returning(|_| Ok(())); 329 | 330 | let expected_config = make_baca_config("prompt_host", "login", "pass"); 331 | let expected_cookie = expected_config.cookie.clone(); 332 | mock_workspace 333 | .expect_save_config_object() 334 | .withf(move |x: &ConnectionConfig| *x == expected_config) 335 | .returning(|_| Ok(())); 336 | 337 | let mut mock_api = MockBacaApi::new(); 338 | mock_api 339 | .expect_get_cookie() 340 | .returning(move |_| Ok(expected_cookie.clone())); 341 | 342 | let init = Init { 343 | host: None, 344 | login: Some("login".to_string()), 345 | password: Some("pass".to_string()), 346 | login_prompt: Box::new(input_prompt_mock), 347 | password_prompt: Box::new(password_prompt_mock), 348 | host_prompt: Box::new(host_prompt_mock), 349 | }; 350 | 351 | let result = init.execute(&mock_workspace, &mock_api); 352 | assert!(result.is_ok()) 353 | } 354 | } 355 | --------------------------------------------------------------------------------