From a9b67e725ac3bd69bc0847c7e5f22e645f6b3784 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Mon, 8 Sep 2025 12:43:22 -0400 Subject: [PATCH 1/7] Create the database file if it does not exist --- .gitignore | 1 + src/commands/client.rs | 4 +--- src/config.rs | 48 +++++++++++++++++++++++++++++------------- src/database.rs | 16 ++++++++++++-- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index e197867..9db19c4 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ target # Coverage artifacts cobertura.xml lcov.info +prserv.db* diff --git a/src/commands/client.rs b/src/commands/client.rs index deae138..b4b711f 100644 --- a/src/commands/client.rs +++ b/src/commands/client.rs @@ -148,9 +148,7 @@ pub async fn execute(args: Args) -> Result<()> { let value = client .max_package_pr(&version, &pkgarch) .await - .map_err(|e| { - crate::error::Error::Generic(format!("max-package-pr failed: {e}")) - })?; + .map_err(|e| crate::error::Error::Generic(format!("max-package-pr failed: {e}")))?; match value { Some(v) => println!("Max PR value: {v}"), None => println!("No PR values found"), diff --git a/src/config.rs b/src/config.rs index 02153f3..4d57ed0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use std::fs; -use std::path::Path; use std::net::SocketAddr; +use std::path::Path; use std::str::FromStr; use std::time::Duration; @@ -24,7 +24,6 @@ pub enum SyncMode { OnShutdown, } - impl SyncMode { /// Convert to Duration for periodic mode, None for other modes pub fn as_duration(&self) -> Option { @@ -62,8 +61,9 @@ impl FromStr for SyncMode { "on_shutdown" | "on-shutdown" | "shutdown" => Ok(SyncMode::OnShutdown), s if s.starts_with("periodic:") => { let interval_str = s.strip_prefix("periodic:").unwrap(); - let interval_secs = interval_str.parse::() - .map_err(|_| Error::Other(format!("Invalid periodic interval: {interval_str}")))?; + let interval_secs = interval_str.parse::().map_err(|_| { + Error::Other(format!("Invalid periodic interval: {interval_str}")) + })?; Ok(SyncMode::Periodic { interval_secs }) } _ => Err(Error::Other(format!( @@ -132,7 +132,6 @@ impl Default for ClientConfig { } } - impl Config { /// Loads configuration from the specified file. /// @@ -231,9 +230,7 @@ impl Config { // Validate database path is not empty if self.server.database.trim().is_empty() { - return Err(Error::Other( - "Database path cannot be empty".to_string() - )); + return Err(Error::Other("Database path cannot be empty".to_string())); } Ok(()) @@ -305,7 +302,10 @@ mod tests { assert_eq!(config.server.bind_addr, "0.0.0.0:9999"); assert!(config.server.read_only); assert!(config.server.nohist); - assert_eq!(config.server.sync_mode, SyncMode::Periodic { interval_secs: 30 }); + assert_eq!( + config.server.sync_mode, + SyncMode::Periodic { interval_secs: 30 } + ); assert_eq!(config.client.server_addr, "192.168.1.100:8585"); // Clean up @@ -319,12 +319,30 @@ mod tests { #[test] fn test_sync_mode_parsing() { - assert_eq!(SyncMode::from_str("immediate").unwrap(), SyncMode::Immediate); - assert_eq!(SyncMode::from_str("on_shutdown").unwrap(), SyncMode::OnShutdown); - assert_eq!(SyncMode::from_str("on-shutdown").unwrap(), SyncMode::OnShutdown); - assert_eq!(SyncMode::from_str("shutdown").unwrap(), SyncMode::OnShutdown); - assert_eq!(SyncMode::from_str("periodic:5").unwrap(), SyncMode::Periodic { interval_secs: 5 }); - assert_eq!(SyncMode::from_str("periodic:300").unwrap(), SyncMode::Periodic { interval_secs: 300 }); + assert_eq!( + SyncMode::from_str("immediate").unwrap(), + SyncMode::Immediate + ); + assert_eq!( + SyncMode::from_str("on_shutdown").unwrap(), + SyncMode::OnShutdown + ); + assert_eq!( + SyncMode::from_str("on-shutdown").unwrap(), + SyncMode::OnShutdown + ); + assert_eq!( + SyncMode::from_str("shutdown").unwrap(), + SyncMode::OnShutdown + ); + assert_eq!( + SyncMode::from_str("periodic:5").unwrap(), + SyncMode::Periodic { interval_secs: 5 } + ); + assert_eq!( + SyncMode::from_str("periodic:300").unwrap(), + SyncMode::Periodic { interval_secs: 300 } + ); // Test invalid inputs assert!(SyncMode::from_str("invalid").is_err()); diff --git a/src/database.rs b/src/database.rs index c50923a..8312401 100644 --- a/src/database.rs +++ b/src/database.rs @@ -7,8 +7,12 @@ use crate::buffered_writes::{BufferedWriteManager, PendingWrite}; use crate::config::SyncMode; use serde::{Deserialize, Serialize}; -use sqlx::{sqlite::SqlitePool, Row}; +use sqlx::{ + sqlite::{SqliteConnectOptions, SqlitePool}, + Row, +}; use std::path::Path; +use std::str::FromStr; use std::sync::Arc; use thiserror::Error; use tracing::{debug, error, info}; @@ -610,7 +614,15 @@ impl PrDatabase { info!("Opening PR database: {}", database_url); - let pool = SqlitePool::connect(&database_url).await?; + // Use SqliteConnectOptions to ensure database file is created if it doesn't exist + let pool = if read_only { + // For read-only mode, don't create the file if it doesn't exist + SqlitePool::connect(&database_url).await? + } else { + // For read-write mode, create the file if it doesn't exist + let options = SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true); + SqlitePool::connect_with(options).await? + }; // Configure SQLite for immediate persistence and performance if !read_only { From 3209e83824b0094eac0ea78f76b9f53e44a176c7 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Mon, 8 Sep 2025 13:11:31 -0400 Subject: [PATCH 2/7] update yocto client protocol --- prserv.log | 24 +++ src/client.rs | 60 ++++--- src/server.rs | 460 +++++++++++++++++++++++++++---------------------- test.db | Bin 0 -> 4096 bytes test.db-shm | Bin 0 -> 32768 bytes test.db-wal | 0 test_python.db | Bin 0 -> 12288 bytes vendor/bitbake | 1 + 8 files changed, 312 insertions(+), 233 deletions(-) create mode 100644 prserv.log create mode 100644 test.db create mode 100644 test.db-shm create mode 100644 test.db-wal create mode 100644 test_python.db create mode 160000 vendor/bitbake diff --git a/prserv.log b/prserv.log new file mode 100644 index 0000000..6ed07a6 --- /dev/null +++ b/prserv.log @@ -0,0 +1,24 @@ +DEBUG: Listening on ('127.0.0.1', 8586) +2025-09-08 19:46:03,160 Listening on ('127.0.0.1', 8586) +DEBUG: Opening PRServ database 'file:/mnt/src/jschneck/avocado-linux/prserv/test_python.db' +2025-09-08 19:46:03,160 Opening PRServ database 'file:/mnt/src/jschneck/avocado-linux/prserv/test_python.db' +NOTE: Started PRServer with DBfile: /mnt/src/jschneck/avocado-linux/prserv/test_python.db, Address: 127.0.0.1:8586, PID: 4173579 +2025-09-08 19:46:03,161 Started PRServer with DBfile: /mnt/src/jschneck/avocado-linux/prserv/test_python.db, Address: 127.0.0.1:8586, PID: 4173579 +NOTE: [Client ('127.0.0.1', 54954)] Client ('127.0.0.1', 54954) connected +2025-09-08 19:46:07,405 [Client ('127.0.0.1', 54954)] Client ('127.0.0.1', 54954) connected +NOTE: [Client ('127.0.0.1', 54954)] Connection closed +2025-09-08 19:46:08,399 [Client ('127.0.0.1', 54954)] Connection closed +DEBUG: Client ('127.0.0.1', 54954) disconnected +2025-09-08 19:46:08,399 Client ('127.0.0.1', 54954) disconnected +NOTE: [Client ('127.0.0.1', 45652)] Client ('127.0.0.1', 45652) connected +2025-09-08 19:46:14,519 [Client ('127.0.0.1', 45652)] Client ('127.0.0.1', 45652) connected +NOTE: [Client ('127.0.0.1', 45652)] Connection closed +2025-09-08 19:46:14,520 [Client ('127.0.0.1', 45652)] Connection closed +DEBUG: Client ('127.0.0.1', 45652) disconnected +2025-09-08 19:46:14,520 Client ('127.0.0.1', 45652) disconnected +DEBUG: Got exit signal +2025-09-08 19:46:18,530 Got exit signal +DEBUG: Stopping server +2025-09-08 19:46:18,530 Stopping server +DEBUG: Server shutting down +2025-09-08 19:46:18,530 Server shutting down diff --git a/src/client.rs b/src/client.rs index 10b6621..31d5d8f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,14 +2,12 @@ //! //! This module provides the client for communicating with the PR server -use crate::server::{Request, Response}; use serde_json::Value; use std::net::SocketAddr; use thiserror::Error; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpStream; use tracing::{debug, error}; -use uuid::Uuid; #[derive(Error, Debug)] pub enum ClientError { @@ -48,26 +46,44 @@ impl PrClient { let stream = TcpStream::connect(addr).await?; debug!("Connected to PR server at {}", addr); - let (read_half, write_half) = stream.into_split(); + let (read_half, mut write_half) = stream.into_split(); let reader = BufReader::new(read_half); + // Send handshake + write_half.write_all(b"PRSERVICE 1.0\n").await?; + write_half.flush().await?; + self.reader = Some(reader); self.writer = Some(write_half); - // Read the initial handshake response - let response = self.read_response().await?; - debug!("Handshake response: {:?}", response); + // Read the handshake response + let response = self.read_line().await?; + debug!("Handshake response: {}", response); + + if !response.starts_with("PRSERVICE") { + return Err(ClientError::Protocol(format!( + "Invalid handshake response: {response}" + ))); + } + + // Send configuration + if let Some(writer) = &mut self.writer { + writer.write_all(b"needs-headers: false\n").await?; + writer.flush().await?; + } + + // Read configuration response + let config_response = self.read_line().await?; + debug!("Config response: {}", config_response); Ok(()) } /// Send a request and get the response async fn send_request(&mut self, method: &str, params: Value) -> Result { - let request = Request { - id: Some(Uuid::new_v4().to_string()), - method: method.to_string(), - params, - }; + let request = serde_json::json!({ + method: params + }); let request_json = serde_json::to_string(&request)?; debug!("Sending request: {}", request_json); @@ -80,19 +96,19 @@ impl PrClient { return Err(ClientError::Protocol("Not connected".to_string())); } - let response = self.read_response().await?; + let response_line = self.read_line().await?; + debug!("Received response: {}", response_line); - if let Some(error) = response.error { - return Err(ClientError::Server(error)); + if response_line.starts_with("ERROR:") { + return Err(ClientError::Server(response_line)); } - response - .result - .ok_or_else(|| ClientError::Protocol("No result in response".to_string())) + let response: Value = serde_json::from_str(&response_line)?; + Ok(response) } - /// Read a response from the server - async fn read_response(&mut self) -> Result { + /// Read a line from the server + async fn read_line(&mut self) -> Result { let mut line = String::new(); if let Some(reader) = &mut self.reader { @@ -101,11 +117,7 @@ impl PrClient { return Err(ClientError::Protocol("Not connected".to_string())); } - let line = line.trim(); - debug!("Received response: {}", line); - - let response: Response = serde_json::from_str(line)?; - Ok(response) + Ok(line.trim().to_string()) } /// Get a PR value (creates if doesn't exist) diff --git a/src/server.rs b/src/server.rs index 8a33756..8f9eec5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -4,7 +4,6 @@ //! with immediate SQLite persistence for reliability. use crate::database::{DatabaseError, PrDatabase, PrTable}; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; @@ -31,23 +30,11 @@ pub enum ServerError { pub type Result = std::result::Result; -/// Protocol version -const PROTOCOL_VERSION: (u32, u32) = (1, 0); - -/// Request message structure -#[derive(Debug, Serialize, Deserialize)] -pub struct Request { - pub id: Option, - pub method: String, - pub params: serde_json::Value, -} - -/// Response message structure -#[derive(Debug, Serialize, Deserialize)] -pub struct Response { - pub id: Option, - pub result: Option, - pub error: Option, +/// Connection state for BitBake asyncrpc protocol +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConnectionState { + WaitingForHandshake, // Waiting for PRSERVICE 1.0 handshake + Ready, // Ready to handle JSON requests } /// PR Server configuration @@ -143,20 +130,8 @@ impl PrServer { let (read_half, mut write_half) = stream.into_split(); let mut reader = BufReader::new(read_half); let mut line = String::new(); - - // Protocol handshake - self.send_response( - &mut write_half, - Response { - id: None, - result: Some(serde_json::json!({ - "protocol": "PRSERVICE", - "version": PROTOCOL_VERSION - })), - error: None, - }, - ) - .await?; + // Start waiting for BitBake asyncrpc handshake + let mut connection_state = ConnectionState::WaitingForHandshake; loop { line.clear(); @@ -167,24 +142,58 @@ impl PrServer { } _ => { let line = line.trim(); - if line.is_empty() { - continue; - } - - debug!("Received from {}: {}", addr, line); - - match self.process_request(line).await { - Ok(response) => { - self.send_response(&mut write_half, response).await?; + debug!("Received from {}: '{}'", addr, line); + + match connection_state { + ConnectionState::WaitingForHandshake => { + if line.starts_with("PRSERVICE") { + debug!("Received BitBake asyncrpc handshake from {}", addr); + // BitBake asyncrpc server does NOT respond to PRSERVICE 1.0 + continue; + } else if line.starts_with("needs-headers:") { + debug!("Received needs-headers from {}", addr); + // BitBake asyncrpc protocol - no response needed for needs-headers + continue; + } else if line.is_empty() { + debug!("Handshake complete for {}, ready for JSON requests", addr); + connection_state = ConnectionState::Ready; + continue; + } else { + return Err(ServerError::InvalidRequest(format!( + "Unexpected handshake message: {line}" + ))); + } } - Err(e) => { - error!("Error processing request from {}: {}", addr, e); - let error_response = Response { - id: None, - result: None, - error: Some(e.to_string()), - }; - self.send_response(&mut write_half, error_response).await?; + ConnectionState::Ready => { + // Skip empty lines in ready state + if line.is_empty() { + continue; + } + + // Handle BitBake asyncrpc JSON requests + match self.process_bitbake_request(line).await { + Ok(response) => { + debug!( + "Sending BitBake asyncrpc response to {}: {}", + addr, response + ); + // Send response with newline as a single write to avoid packet fragmentation + let response_with_newline = format!("{response}\n"); + write_half + .write_all(response_with_newline.as_bytes()) + .await?; + write_half.flush().await?; + } + Err(e) => { + error!( + "Error processing BitBake asyncrpc request from {}: {}", + addr, e + ); + let error_response = "{\"error\":\"invalid request\"}\n"; + write_half.write_all(error_response.as_bytes()).await?; + write_half.flush().await?; + } + } } } } @@ -194,175 +203,208 @@ impl PrServer { Ok(()) } - /// Process a JSON-RPC style request - async fn process_request(&self, request_line: &str) -> Result { - let request: Request = serde_json::from_str(request_line)?; - - debug!("Processing method: {}", request.method); - - let result = match request.method.as_str() { - "get-pr" => self.handle_get_pr(request.params).await?, - "test-pr" => self.handle_test_pr(request.params).await?, - "test-package" => self.handle_test_package(request.params).await?, - "max-package-pr" => self.handle_max_package_pr(request.params).await?, - "import-one" => self.handle_import_one(request.params).await?, - "export" => self.handle_export(request.params).await?, - "is-readonly" => self.handle_is_readonly().await?, - "ping" => serde_json::json!({"pong": true}), - _ => { - return Ok(Response { - id: request.id, - result: None, - error: Some(format!("Unknown method: {}", request.method)), - }); - } - }; - - Ok(Response { - id: request.id, - result: Some(result), - error: None, - }) - } - - /// Send a response to the client - async fn send_response( - &self, - writer: &mut tokio::net::tcp::OwnedWriteHalf, - response: Response, - ) -> Result<()> { - let response_json = serde_json::to_string(&response)?; - writer.write_all(response_json.as_bytes()).await?; - writer.write_all(b"\n").await?; - writer.flush().await?; - Ok(()) - } - - /// Handle get-pr request - async fn handle_get_pr(&self, params: serde_json::Value) -> Result { - let version: String = params["version"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing version".to_string()))? - .to_string(); - let pkgarch: String = params["pkgarch"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing pkgarch".to_string()))? - .to_string(); - let checksum: String = params["checksum"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing checksum".to_string()))? - .to_string(); - - let table = self.get_table("PRMAIN").await?; - let value = table.get_value(&version, &pkgarch, &checksum).await?; - - Ok(serde_json::json!({"value": value})) - } + /// Process a BitBake asyncrpc JSON request + async fn process_bitbake_request(&self, request_line: &str) -> Result { + // Try to parse as JSON + if let Ok(json_value) = serde_json::from_str::(request_line) { + if let Some(obj) = json_value.as_object() { + // Handle ping request + if obj.contains_key("ping") { + return Ok(serde_json::to_string(&serde_json::json!({"alive": true}))?); + } - /// Handle test-pr request - async fn handle_test_pr(&self, params: serde_json::Value) -> Result { - let version: String = params["version"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing version".to_string()))? - .to_string(); - let pkgarch: String = params["pkgarch"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing pkgarch".to_string()))? - .to_string(); - let checksum: String = params["checksum"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing checksum".to_string()))? - .to_string(); - - let table = self.get_table("PRMAIN").await?; - let value = table.find_value(&version, &pkgarch, &checksum).await?; - - Ok(serde_json::json!({"value": value})) - } + // Handle get-pr request + if let Some(get_pr) = obj.get("get-pr") { + if let Some(params) = get_pr.as_object() { + let version = + params + .get("version") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing version".to_string()) + })?; + let pkgarch = + params + .get("pkgarch") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing pkgarch".to_string()) + })?; + let checksum = + params + .get("checksum") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing checksum".to_string()) + })?; + + let table = self.get_table("PRMAIN").await?; + let value = table.get_value(version, pkgarch, checksum).await?; + return Ok(serde_json::to_string(&serde_json::json!({"value": value}))?); + } + } - /// Handle test-package request - async fn handle_test_package(&self, params: serde_json::Value) -> Result { - let version: String = params["version"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing version".to_string()))? - .to_string(); - let pkgarch: String = params["pkgarch"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing pkgarch".to_string()))? - .to_string(); - - let table = self.get_table("PRMAIN").await?; - let exists = table.test_package(&version, &pkgarch).await?; - - Ok(serde_json::json!({"value": exists})) - } + // Handle test-pr request + if let Some(test_pr) = obj.get("test-pr") { + if let Some(params) = test_pr.as_object() { + let version = + params + .get("version") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing version".to_string()) + })?; + let pkgarch = + params + .get("pkgarch") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing pkgarch".to_string()) + })?; + let checksum = + params + .get("checksum") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing checksum".to_string()) + })?; + + let table = self.get_table("PRMAIN").await?; + let value = table.find_value(version, pkgarch, checksum).await?; + return Ok(serde_json::to_string(&serde_json::json!({"value": value}))?); + } + } - /// Handle max-package-pr request - async fn handle_max_package_pr(&self, params: serde_json::Value) -> Result { - let version: String = params["version"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing version".to_string()))? - .to_string(); - let pkgarch: String = params["pkgarch"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing pkgarch".to_string()))? - .to_string(); - - let table = self.get_table("PRMAIN").await?; - let value = table.find_max_value(&version, &pkgarch).await?; - - Ok(serde_json::json!({"value": value})) - } + // Handle test-package request + if let Some(test_package) = obj.get("test-package") { + if let Some(params) = test_package.as_object() { + let version = + params + .get("version") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing version".to_string()) + })?; + let pkgarch = + params + .get("pkgarch") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing pkgarch".to_string()) + })?; + + let table = self.get_table("PRMAIN").await?; + let exists = table.test_package(version, pkgarch).await?; + return Ok(serde_json::to_string( + &serde_json::json!({"value": exists}), + )?); + } + } - /// Handle import-one request - async fn handle_import_one(&self, params: serde_json::Value) -> Result { - if self.config.read_only { - return Ok(serde_json::json!({"value": null})); - } + // Handle max-package-pr request + if let Some(max_package_pr) = obj.get("max-package-pr") { + if let Some(params) = max_package_pr.as_object() { + let version = + params + .get("version") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing version".to_string()) + })?; + let pkgarch = + params + .get("pkgarch") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing pkgarch".to_string()) + })?; + + let table = self.get_table("PRMAIN").await?; + let value = table.find_max_value(version, pkgarch).await?; + return Ok(serde_json::to_string(&serde_json::json!({"value": value}))?); + } + } - let version: String = params["version"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing version".to_string()))? - .to_string(); - let pkgarch: String = params["pkgarch"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing pkgarch".to_string()))? - .to_string(); - let checksum: String = params["checksum"] - .as_str() - .ok_or_else(|| ServerError::InvalidRequest("Missing checksum".to_string()))? - .to_string(); - let value: i64 = params["value"] - .as_i64() - .ok_or_else(|| ServerError::InvalidRequest("Missing or invalid value".to_string()))?; - - let table = self.get_table("PRMAIN").await?; - let result = table - .import_one(&version, &pkgarch, &checksum, value) - .await?; - - Ok(serde_json::json!({"value": result})) - } + // Handle import-one request + if let Some(import_one) = obj.get("import-one") { + if let Some(params) = import_one.as_object() { + if self.config.read_only { + return Ok(serde_json::to_string(&serde_json::json!({"value": null}))?); + } - /// Handle export request - async fn handle_export(&self, params: serde_json::Value) -> Result { - let version = params["version"].as_str(); - let pkgarch = params["pkgarch"].as_str(); - let checksum = params["checksum"].as_str(); - let colinfo = params["colinfo"].as_bool().unwrap_or(false); + let version = + params + .get("version") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing version".to_string()) + })?; + let pkgarch = + params + .get("pkgarch") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing pkgarch".to_string()) + })?; + let checksum = + params + .get("checksum") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ServerError::InvalidRequest("Missing checksum".to_string()) + })?; + let value = + params + .get("value") + .and_then(|v| v.as_i64()) + .ok_or_else(|| { + ServerError::InvalidRequest( + "Missing or invalid value".to_string(), + ) + })?; + + let table = self.get_table("PRMAIN").await?; + let result = table.import_one(version, pkgarch, checksum, value).await?; + return Ok(serde_json::to_string( + &serde_json::json!({"value": result}), + )?); + } + } - let table = self.get_table("PRMAIN").await?; - let (metainfo, datainfo) = table.export(version, pkgarch, checksum, colinfo).await?; + // Handle export request + if let Some(export) = obj.get("export") { + if let Some(params) = export.as_object() { + let version = params.get("version").and_then(|v| v.as_str()); + let pkgarch = params.get("pkgarch").and_then(|v| v.as_str()); + let checksum = params.get("checksum").and_then(|v| v.as_str()); + let colinfo = params + .get("colinfo") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let table = self.get_table("PRMAIN").await?; + let (metainfo, datainfo) = + table.export(version, pkgarch, checksum, colinfo).await?; + + return Ok(serde_json::to_string(&serde_json::json!({ + "metainfo": metainfo, + "datainfo": datainfo + }))?); + } + } - Ok(serde_json::json!({ - "metainfo": metainfo, - "datainfo": datainfo - })) - } + // Handle is-readonly request + if obj.contains_key("is-readonly") { + return Ok(serde_json::to_string( + &serde_json::json!({"readonly": self.config.read_only}), + )?); + } + } + } - /// Handle is-readonly request - async fn handle_is_readonly(&self) -> Result { - Ok(serde_json::json!({"readonly": self.config.read_only})) + Err(ServerError::InvalidRequest(format!( + "Unknown request: {request_line}" + ))) } } diff --git a/test.db b/test.db new file mode 100644 index 0000000000000000000000000000000000000000..4ebf78cf96088c7148cb47caf804cc036de75a34 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WY8;GzzfnYK(-m98b?E5 nGz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nC=3ArfDQ*E literal 0 HcmV?d00001 diff --git a/test.db-shm b/test.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10 GIT binary patch literal 32768 zcmeIuAr62r3Wjaog*=*@6VIr#fXZgrv5aLEA;lEM;9Sb(c=DME_R*SI3+M z0}rx$mhXc%@|YQ%`RL`{-5OC5&`X-ly@JMUz&K~;M2sf zkKO(+>-1Kvx9b0F+JJcoKmY;|fB*y_009U<00Izbr@+@4-ybRp{~j0KQyffeQ`0Rs zNy8{FYDMSL7Mdf-(Jl-@wJBIcmKC literal 0 HcmV?d00001 diff --git a/vendor/bitbake b/vendor/bitbake new file mode 160000 index 0000000..9826451 --- /dev/null +++ b/vendor/bitbake @@ -0,0 +1 @@ +Subproject commit 982645110a19ebb94d519926a4e14c8a2a205cfd From bb920e25d69c723f70329857c02d0bdae4c8666d Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Mon, 8 Sep 2025 20:17:52 -0400 Subject: [PATCH 3/7] update checks --- .github/workflows/checks.yml | 12 +- .github/workflows/release.yml | 110 ++--------- Cargo.lock | 333 ++++++++++++++++------------------ Cargo.toml | 2 +- scripts/checks.sh | 2 +- tests/commands/mod.rs | 184 ------------------- tests/commands/run/mod.rs | 278 ---------------------------- tests/examples/prserv.json | 11 -- tests/integration_test.rs | 2 - tests/support/mod.rs | 6 - 10 files changed, 179 insertions(+), 761 deletions(-) delete mode 100644 tests/commands/mod.rs delete mode 100644 tests/commands/run/mod.rs delete mode 100644 tests/examples/prserv.json delete mode 100644 tests/integration_test.rs delete mode 100644 tests/support/mod.rs diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9d0e7e2..6685f21 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -10,15 +10,17 @@ env: jobs: checks: name: Run Checks - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsqlite3-dev sqlite3 + - name: Install Rust uses: dtolnay/rust-toolchain@stable with: @@ -42,7 +44,7 @@ jobs: run: ./scripts/checks.sh - name: Upload coverage to Codecov - if: matrix.os == 'ubuntu-latest' && success() + if: success() uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6a8df3..5dff0ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,20 +12,20 @@ env: jobs: test: name: Run Tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - rust: [stable] + runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsqlite3-dev sqlite3 + - name: Install Rust - uses: dtolnay/rust-toolchain@master + uses: dtolnay/rust-toolchain@stable with: - toolchain: ${{ matrix.rust }} components: rustfmt, clippy - name: Cache dependencies @@ -73,12 +73,6 @@ jobs: fail-fast: false matrix: include: - - os: macos-latest - target: x86_64-apple-darwin - suffix: "" - - os: macos-latest - target: aarch64-apple-darwin - suffix: "" - os: ubuntu-latest target: x86_64-unknown-linux-gnu suffix: "" @@ -88,14 +82,16 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-musl suffix: "" - - os: windows-latest - target: x86_64-pc-windows-msvc - suffix: .exe steps: - uses: actions/checkout@v4 name: Checkout for cargo metadata + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsqlite3-dev sqlite3 + - name: Extract binary name from Cargo.toml id: get_binary_name shell: bash @@ -105,12 +101,6 @@ jobs: echo "binary_name=$BINARY_NAME" >> $GITHUB_OUTPUT echo "Binary name: $BINARY_NAME" - - name: Install aws-lc-rs build pre-reqs for Windows - if: ${{ matrix.target == 'x86_64-pc-windows-msvc' }} - run: | - choco install nasm -y - echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - uses: dtolnay/rust-toolchain@stable with: target: ${{ matrix.target }} @@ -123,75 +113,13 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build release (native) - if: ${{ !contains(fromJSON('["aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl", "aarch64-apple-darwin"]'), matrix.target) }} + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} run: cargo build --release --target ${{ matrix.target }} - name: Build release (cross) if: ${{ contains(fromJSON('["aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"]'), matrix.target) }} run: cross build --release --target ${{ matrix.target }} - - name: Build release (macOS ARM64) - if: ${{ matrix.target == 'aarch64-apple-darwin' }} - run: | - rustup target add aarch64-apple-darwin - cargo build --release --target aarch64-apple-darwin - - - name: Codesign executable (macOS) - if: ${{ contains(fromJSON('["x86_64-apple-darwin", "aarch64-apple-darwin"]'), matrix.target) }} - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} - APPLE_CODESIGNING_IDENTITY: ${{ secrets.APPLE_CODESIGNING_IDENTITY }} - run: | - # Require certificate for macOS builds - if [ -z "$MACOS_CERTIFICATE" ]; then - echo "ERROR: macOS code signing certificate not configured" - echo "Please configure MACOS_CERTIFICATE secret in GitHub Actions" - exit 1 - fi - - # Create variables - CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db - BINARY_PATH="./target/${{ matrix.target }}/release/${{ steps.get_binary_name.outputs.binary_name }}" - - # Import certificate - echo -n "$MACOS_CERTIFICATE" | base64 --decode --output $CERTIFICATE_PATH - - # Create temporary keychain - security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security default-keychain -s $KEYCHAIN_PATH - security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - - # Make cert accessible by codesign - security import $CERTIFICATE_PATH -P "$MACOS_CERTIFICATE_PWD" -k $KEYCHAIN_PATH -T /usr/bin/codesign - - # Avoid password prompt - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - - # Give permissions - chmod +x $BINARY_PATH - - # Code sign - /usr/bin/codesign -s $APPLE_CODESIGNING_IDENTITY --deep -f --timestamp -o runtime $BINARY_PATH - - # Verify signing - /usr/bin/codesign --verify --strict $BINARY_PATH - - # Zip file for notarization - ditto -c -k --sequesterRsrc --keepParent "$BINARY_PATH" "$BINARY_PATH.zip" - - # Notarize app - xcrun notarytool submit \ - --wait \ - --apple-id "$APPLE_ID" \ - --team-id "$APPLE_TEAM_ID" \ - --password "$APP_PASSWORD" \ - "$BINARY_PATH.zip" - name: Package release shell: bash @@ -199,15 +127,9 @@ jobs: BINARY_NAME="${{ steps.get_binary_name.outputs.binary_name }}${{ matrix.suffix }}" mv "./target/${{ matrix.target }}/release/${{ steps.get_binary_name.outputs.binary_name }}${{ matrix.suffix }}" "./$BINARY_NAME" - # Create tarball for Unix systems - if [ "${{ matrix.os }}" != "windows-latest" ]; then - tar -czf "${{ steps.get_binary_name.outputs.binary_name }}-${{ github.ref_name }}_${{ matrix.target }}.tar.gz" "./$BINARY_NAME" - echo "ASSET=${{ steps.get_binary_name.outputs.binary_name }}-${{ github.ref_name }}_${{ matrix.target }}.tar.gz" >> $GITHUB_ENV - else - # Create zip for Windows - 7z a -tzip "${{ steps.get_binary_name.outputs.binary_name }}-${{ github.ref_name }}_${{ matrix.target }}.zip" "./$BINARY_NAME" - echo "ASSET=${{ steps.get_binary_name.outputs.binary_name }}-${{ github.ref_name }}_${{ matrix.target }}.zip" >> $GITHUB_ENV - fi + # Create tarball for Linux + tar -czf "${{ steps.get_binary_name.outputs.binary_name }}-${{ github.ref_name }}_${{ matrix.target }}.tar.gz" "./$BINARY_NAME" + echo "ASSET=${{ steps.get_binary_name.outputs.binary_name }}-${{ github.ref_name }}_${{ matrix.target }}.tar.gz" >> $GITHUB_ENV - name: Upload Release Asset uses: softprops/action-gh-release@v2 diff --git a/Cargo.lock b/Cargo.lock index 9af25ed..3991d8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,19 +17,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -156,6 +143,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.8.0" @@ -273,10 +266,10 @@ version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -291,6 +284,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.11" @@ -442,7 +444,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -510,9 +512,14 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.3" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] name = "fastrand" @@ -574,6 +581,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -650,7 +663,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -712,7 +725,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.3+wasi-0.2.4", + "wasi 0.14.4+wasi-0.2.4", ] [[package]] @@ -740,38 +753,24 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.5", + "allocator-api2", + "equivalent", + "foldhash", ] [[package]] -name = "heck" -version = "0.4.1" +name = "hashlink" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "unicode-segmentation", + "hashbrown", ] [[package]] @@ -1022,7 +1021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown", ] [[package]] @@ -1111,9 +1110,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -1179,12 +1178,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1222,16 +1215,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1344,7 +1327,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1371,6 +1354,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.4" @@ -1394,12 +1383,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1537,7 +1520,7 @@ dependencies = [ "sqlx", "tar", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", "tracing", @@ -1607,7 +1590,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1645,7 +1628,7 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1736,13 +1719,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.12" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ + "once_cell", "ring", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] [[package]] @@ -1751,16 +1737,26 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] @@ -1791,16 +1787,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "security-framework" version = "2.11.1" @@ -1816,9 +1802,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -1841,7 +1827,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1948,6 +1934,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1988,21 +1977,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2013,70 +1992,64 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "ahash", - "atoi", - "byteorder", + "base64 0.22.1", "bytes", "chrono", "crc", "crossbeam-queue", "either", "event-listener", - "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", + "hashbrown", "hashlink", - "hex", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", "rustls", - "rustls-pemfile", "serde", "serde_json", "sha2", "smallvec", - "sqlformat", - "thiserror", + "thiserror 2.0.16", "tokio", "tokio-stream", "tracing", "url", "uuid", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", - "heck 0.4.1", + "heck", "hex", "once_cell", "proc-macro2", @@ -2088,20 +2061,19 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", - "tempfile", + "syn", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags 2.9.4", "byteorder", "bytes", @@ -2132,7 +2104,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.16", "tracing", "uuid", "whoami", @@ -2140,12 +2112,12 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags 2.9.4", "byteorder", "chrono", @@ -2154,7 +2126,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -2172,7 +2143,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.16", "tracing", "uuid", "whoami", @@ -2180,9 +2151,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -2196,10 +2167,11 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", + "thiserror 2.0.16", "tracing", "url", - "urlencoding", "uuid", ] @@ -2232,17 +2204,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.106" @@ -2268,7 +2229,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -2328,7 +2289,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", ] [[package]] @@ -2339,7 +2309,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2404,7 +2385,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -2467,7 +2448,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -2548,24 +2529,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-width" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2590,12 +2559,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -2663,9 +2626,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.3+wasi-0.2.4" +version = "0.14.4+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" dependencies = [ "wit-bindgen", ] @@ -2699,7 +2662,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn", "wasm-bindgen-shared", ] @@ -2734,7 +2697,7 @@ checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2783,9 +2746,21 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "whoami" @@ -2818,7 +2793,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -2829,7 +2804,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -3136,7 +3111,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", "synstructure", ] @@ -3157,7 +3132,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -3177,7 +3152,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", "synstructure", ] @@ -3217,5 +3192,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index b9dbb4e..86e1b5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ directories = "5.0" flate2 = "1.0" tar = "0.4" # Async SQLite support -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] } +sqlx = { version = "0.8.1", default-features = false, features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] } # UUID generation for request IDs uuid = { version = "1.0", features = ["v4"] } # Chrono for timestamps diff --git a/scripts/checks.sh b/scripts/checks.sh index 185f176..cd946a5 100755 --- a/scripts/checks.sh +++ b/scripts/checks.sh @@ -13,7 +13,7 @@ if [ -z "$CI" ]; then echo "cargo install cargo-audit" exit 1 fi - cargo audit + cargo audit --ignore RUSTSEC-2023-0071 fi # Check for cargo-llvm-cov (skip check in CI as it's installed by workflow) diff --git a/tests/commands/mod.rs b/tests/commands/mod.rs deleted file mode 100644 index f1820eb..0000000 --- a/tests/commands/mod.rs +++ /dev/null @@ -1,184 +0,0 @@ -use predicates::prelude::*; -use tempfile::TempDir; - -use crate::support::cli; - -pub mod run; - -#[test] -fn test_cli_version() { - cli() - .arg("--version") - .assert() - .success() - .stdout(predicate::str::contains(env!("CARGO_PKG_NAME"))); -} - -#[test] -fn test_cli_help() { - cli() - .arg("--help") - .assert() - .success() - .stdout(predicate::str::contains( - "Package Revision (PR) server for BitBake builds", - )) - .stdout(predicate::str::contains("Commands:")) - .stdout(predicate::str::contains("run")); -} - -#[test] -fn test_invalid_command() { - cli() - .arg("invalid-command") - .assert() - .failure() - .stderr(predicate::str::contains("unrecognized subcommand")); -} - -#[test] -fn test_global_verbose_flag() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - std::fs::write(&test_file, "test content").unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("-vv") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stderr(predicate::str::contains("INFO")); -} - -#[test] -fn test_global_log_level() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - std::fs::write(&test_file, "test content").unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("-L") - .arg("debug") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stderr(predicate::str::contains("DEBUG")); -} - -#[test] -fn test_syslog_numeric_levels() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - std::fs::write(&test_file, "test content").unwrap(); - - // Test numeric level 6 (info) - cli() - .current_dir(temp_dir.path()) - .arg("-L") - .arg("6") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stderr(predicate::str::contains("INFO")); -} - -#[test] -fn test_case_insensitive_log_levels() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - std::fs::write(&test_file, "test content").unwrap(); - - // Test uppercase - cli() - .current_dir(temp_dir.path()) - .arg("-L") - .arg("INFO") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stderr(predicate::str::contains("INFO")); - - // Test mixed case - cli() - .current_dir(temp_dir.path()) - .arg("-L") - .arg("Info") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stderr(predicate::str::contains("INFO")); -} - -#[test] -fn test_config_flag() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - std::fs::write(&test_file, "test content").unwrap(); - - let config_file = temp_dir.path().join("config.json"); - std::fs::write(&config_file, r#"{"test": true}"#).unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("-C") - .arg(config_file.to_str().unwrap()) - .arg("-vv") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stderr(predicate::str::contains("Using configuration file")); -} - -#[test] -fn test_verbose_increment() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - std::fs::write(&test_file, "test content").unwrap(); - - // Test multiple -v flags - cli() - .current_dir(temp_dir.path()) - .arg("-v") - .arg("-v") - .arg("-v") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stderr(predicate::str::contains("INFO")); -} - -#[test] -fn test_log_level_with_verbose() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - std::fs::write(&test_file, "test content").unwrap(); - - // Test log level with verbose increment - cli() - .current_dir(temp_dir.path()) - .arg("-L") - .arg("warn") - .arg("-vv") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stderr(predicate::str::contains("INFO")); -} diff --git a/tests/commands/run/mod.rs b/tests/commands/run/mod.rs deleted file mode 100644 index f97f6bf..0000000 --- a/tests/commands/run/mod.rs +++ /dev/null @@ -1,278 +0,0 @@ -use predicates::prelude::*; -use std::fs; -use tempfile::TempDir; - -use crate::support::cli; - -#[test] -fn test_run_with_valid_input() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("input.txt"); - fs::write( - &test_file, - "Hello, world!\nThis is a test file.\nIt has lines.\n", - ) - .unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stdout(predicate::str::contains("Lines:")) - .stdout(predicate::str::contains("Words:")) - .stdout(predicate::str::contains("Bytes:")) - .stdout(predicate::str::contains("[SUCCESS]")); -} - -#[test] -fn test_run_with_nonexistent_file() { - cli() - .arg("run") - .arg("--input") - .arg("nonexistent.txt") - .assert() - .failure() - .stderr(predicate::str::contains("File not found: nonexistent.txt")); -} - -#[test] -fn test_run_stats_only() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("data.txt"); - fs::write( - &test_file, - "Hello, world!\nThis is a test file.\nIt has lines.\n", - ) - .unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .arg("--stats-only") - .assert() - .success() - .stdout(predicate::str::contains("File statistics for")) - .stdout(predicate::str::contains("Lines: 3")) - .stdout(predicate::str::contains("Words: 10")) - .stdout(predicate::str::contains("Bytes: 49")); -} - -#[test] -fn test_run_with_output_file() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("input.txt"); - fs::write(&test_file, "hello world").unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .arg("--output") - .arg("output.txt") - .assert() - .success() - .stdout(predicate::str::contains("Output written to: output.txt")) - .stdout(predicate::str::contains("[SUCCESS]")); - - // Verify output file was created with uppercase content - let output_content = fs::read_to_string(temp_dir.path().join("output.txt")).unwrap(); - assert_eq!(output_content, "HELLO WORLD"); -} - -#[test] -fn test_run_with_verbose_logging() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - fs::write(&test_file, "test content").unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("-vv") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .arg("--stats-only") - .assert() - .success() - .stderr(predicate::str::contains("INFO")) - .stderr(predicate::str::contains("Processing file")); -} - -#[test] -fn test_run_with_debug_logging() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - fs::write(&test_file, "test content").unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("-L") - .arg("debug") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .arg("--stats-only") - .assert() - .success() - .stderr(predicate::str::contains("DEBUG")) - .stderr(predicate::str::contains("Reading file contents")); -} - -#[test] -fn test_run_with_large_file() { - let temp_dir = TempDir::new().unwrap(); - let large_content = (0..1000) - .map(|i| { - format!( - "Line {}: This is test content for line number {}.", - i + 1, - i + 1 - ) - }) - .collect::>() - .join("\n"); - let test_file = temp_dir.path().join("large.txt"); - fs::write(&test_file, &large_content).unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .arg("--stats-only") - .assert() - .success() - .stdout(predicate::str::contains("Lines: 1000")) - .stdout(predicate::str::contains("Words: 10000")); -} - -#[test] -fn test_run_with_json_file() { - let temp_dir = TempDir::new().unwrap(); - let json_content = r#"{ - "test": true, - "value": 42, - "items": ["one", "two", "three"] -}"#; - let test_file = temp_dir.path().join("data.json"); - fs::write(&test_file, json_content).unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stdout(predicate::str::contains("Lines:")) - .stdout(predicate::str::contains("Words:")) - .stdout(predicate::str::contains("Bytes:")) - .stdout(predicate::str::contains("Lines: 5")); -} - -#[test] -fn test_run_output_overwrites_existing() { - let temp_dir = TempDir::new().unwrap(); - - // Create initial output file - let output_file = temp_dir.path().join("output.txt"); - fs::write(&output_file, "old content").unwrap(); - - // Create input file - let input = temp_dir.path().join("input.txt"); - fs::write(&input, "new content").unwrap(); - - // Run command to overwrite - cli() - .current_dir(temp_dir.path()) - .arg("run") - .arg("--input") - .arg(input.to_str().unwrap()) - .arg("--output") - .arg("output.txt") - .assert() - .success(); - - // Verify file was overwritten - let content = fs::read_to_string(&output_file).unwrap(); - assert_eq!(content, "NEW CONTENT"); -} - -#[test] -fn test_run_with_config_option() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("input.txt"); - fs::write(&test_file, "test").unwrap(); - - let config_file = temp_dir.path().join("custom.json"); - fs::write(&config_file, r#"{"test": true}"#).unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("-C") - .arg(config_file.to_str().unwrap()) - .arg("-vv") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .arg("--stats-only") - .assert() - .success() - .stderr(predicate::str::contains("Using configuration file")); -} - -#[test] -fn test_run_empty_file() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("empty.txt"); - fs::write(&test_file, "").unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stdout(predicate::str::contains("Lines: 0")) - .stdout(predicate::str::contains("Words: 0")) - .stdout(predicate::str::contains("Bytes: 0")); -} - -#[test] -fn test_run_with_multiple_verbose_flags() { - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - fs::write(&test_file, "content").unwrap(); - - cli() - .current_dir(temp_dir.path()) - .arg("-v") - .arg("-v") - .arg("-v") - .arg("run") - .arg("--input") - .arg(test_file.to_str().unwrap()) - .assert() - .success() - .stderr(predicate::str::contains("INFO")); -} - -#[test] -fn test_run_help() { - cli() - .arg("run") - .arg("--help") - .assert() - .success() - .stdout(predicate::str::contains("Run the main functionality")) - .stdout(predicate::str::contains("--input")) - .stdout(predicate::str::contains("--output")) - .stdout(predicate::str::contains("--stats-only")); -} diff --git a/tests/examples/prserv.json b/tests/examples/prserv.json deleted file mode 100644 index b0bb09c..0000000 --- a/tests/examples/prserv.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "server": { - "database": "prserv.db", - "bind_addr": "127.0.0.1:8585", - "read_only": false, - "nohist": false - }, - "client": { - "server_addr": "127.0.0.1:8585" - } -} diff --git a/tests/integration_test.rs b/tests/integration_test.rs deleted file mode 100644 index de78c4a..0000000 --- a/tests/integration_test.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod commands; -mod support; diff --git a/tests/support/mod.rs b/tests/support/mod.rs deleted file mode 100644 index e40681c..0000000 --- a/tests/support/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -use assert_cmd::Command; - -/// Create a new Command instance for the CLI binary -pub fn cli() -> Command { - Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap() -} From b8423fc18bf303ddf2d4773b6b4433300867477a Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Mon, 8 Sep 2025 20:35:49 -0400 Subject: [PATCH 4/7] Handle graceful shutdown --- src/database.rs | 9 +++++ src/server.rs | 90 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/database.rs b/src/database.rs index 8312401..87ca2a1 100644 --- a/src/database.rs +++ b/src/database.rs @@ -662,6 +662,15 @@ impl PrDatabase { .await } + /// Flush any buffered writes to ensure data persistence + #[allow(dead_code)] + pub async fn flush(&self) -> Result<()> { + // For now, this is a no-op since the PrDatabase doesn't directly manage + // buffered writes. The buffering is handled at the PrTable level. + // In a more complete implementation, we might track all tables and flush them. + Ok(()) + } + /// Close the database connection #[allow(dead_code)] pub async fn close(self) { diff --git a/src/server.rs b/src/server.rs index 8f9eec5..17fbba1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -10,8 +10,8 @@ use std::sync::Arc; use thiserror::Error; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::RwLock; -use tracing::{debug, error, info}; +use tokio::sync::{broadcast, RwLock}; +use tracing::{debug, error, info, warn}; #[derive(Error, Debug)] pub enum ServerError { @@ -85,22 +85,88 @@ impl PrServer { self.config.db_path, self.config.read_only, self.config.nohist, self.config.sync_mode ); + // Create shutdown channel + let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1); + + // Spawn signal handler task + let shutdown_tx_clone = shutdown_tx.clone(); + tokio::spawn(async move { + let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("Failed to register SIGTERM handler"); + let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) + .expect("Failed to register SIGINT handler"); + + tokio::select! { + _ = sigterm.recv() => { + info!("Received SIGTERM, initiating graceful shutdown..."); + } + _ = sigint.recv() => { + info!("Received SIGINT (Ctrl-C), initiating graceful shutdown..."); + } + } + + let _ = shutdown_tx_clone.send(()); + }); + loop { - match listener.accept().await { - Ok((stream, addr)) => { - debug!("New client connection from {}", addr); - let server = self.clone(); - tokio::spawn(async move { - if let Err(e) = server.handle_client(stream, addr).await { - error!("Error handling client {}: {}", addr, e); + tokio::select! { + // Handle new connections + result = listener.accept() => { + match result { + Ok((stream, addr)) => { + debug!("New client connection from {}", addr); + let server = self.clone(); + let mut client_shutdown_rx = shutdown_tx.subscribe(); + tokio::spawn(async move { + tokio::select! { + result = server.handle_client(stream, addr) => { + if let Err(e) = result { + error!("Error handling client {}: {}", addr, e); + } + } + _ = client_shutdown_rx.recv() => { + debug!("Client {} connection terminated due to shutdown", addr); + } + } + }); + } + Err(e) => { + error!("Failed to accept connection: {}", e); } - }); + } } - Err(e) => { - error!("Failed to accept connection: {}", e); + // Handle shutdown signal + _ = shutdown_rx.recv() => { + info!("Shutdown signal received, stopping server..."); + break; } } } + + // Perform cleanup + self.shutdown().await?; + info!("Server shutdown complete"); + Ok(()) + } + + /// Perform graceful shutdown cleanup + async fn shutdown(&self) -> Result<()> { + info!("Performing graceful shutdown cleanup..."); + + // Flush any buffered writes from all cached tables + { + let tables = self.tables.read().await; + for (table_name, table) in tables.iter() { + if let Err(e) = table.flush().await { + warn!("Error flushing table '{}' during shutdown: {}", table_name, e); + } else { + debug!("Successfully flushed table '{}'", table_name); + } + } + } + + info!("Graceful shutdown cleanup complete"); + Ok(()) } /// Get or create a table instance From b1ee2e82afa84a35a7096c3445d236fabdf08101 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Mon, 8 Sep 2025 21:12:44 -0400 Subject: [PATCH 5/7] add tests for incrementing logic --- src/database.rs | 163 ++++++++++++++++++++++++++++++++++++++++++------ src/server.rs | 15 +++-- 2 files changed, 154 insertions(+), 24 deletions(-) diff --git a/src/database.rs b/src/database.rs index 87ca2a1..c93d3c5 100644 --- a/src/database.rs +++ b/src/database.rs @@ -211,10 +211,10 @@ impl PrTable { self.table_name ); - let result = sqlx::query_scalar::<_, i64>(&sql) + let result = sqlx::query_scalar::<_, Option>(&sql) .bind(version) .bind(pkgarch) - .fetch_optional(&self.pool) + .fetch_one(&self.pool) .await?; Ok(result) @@ -289,23 +289,8 @@ impl PrTable { /// Get value in no-history mode (no decrements allowed) async fn get_value_no_hist(&self, version: &str, pkgarch: &str, checksum: &str) -> Result { - // Find existing value that is >= max value for this version/pkgarch - let sql = format!( - "SELECT value FROM {} - WHERE version=? AND pkgarch=? AND checksum=? AND - value >= COALESCE((SELECT max(value) FROM {} WHERE version=? AND pkgarch=?), 0)", - self.table_name, self.table_name - ); - - if let Some(value) = sqlx::query_scalar::<_, i64>(&sql) - .bind(version) - .bind(pkgarch) - .bind(checksum) - .bind(version) - .bind(pkgarch) - .fetch_optional(&self.pool) - .await? - { + // First try to find existing value for exact match + if let Some(value) = self.find_value(version, pkgarch, checksum).await? { return Ok(value); } @@ -677,3 +662,143 @@ impl PrDatabase { self.pool.close().await; } } + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_version_increment_different_checksums() { + let temp_dir = tempdir().unwrap(); + let db_path = temp_dir.path().join("test_increment.db"); + + // Create database with history mode (allows increments) + let database = PrDatabase::new(&db_path, false, false, SyncMode::Immediate) + .await + .unwrap(); + let table = database.get_table("TEST_INCREMENT_TABLE").await.unwrap(); + + let version = "1.0.0"; + let pkgarch = "x86_64"; + + // First request: new package with checksum1 should get PR value 0 + let checksum1 = "abc123def456"; + let pr_value1 = table.get_value(version, pkgarch, checksum1).await.unwrap(); + assert_eq!(pr_value1, 0, "First checksum should get PR value 0"); + + // Second request: same package/arch but different checksum should get PR value 1 + let checksum2 = "789xyz012abc"; + let pr_value2 = table.get_value(version, pkgarch, checksum2).await.unwrap(); + assert_eq!( + pr_value2, 1, + "Different checksum should get incremented PR value" + ); + + // Third request: another different checksum should get PR value 2 + let checksum3 = "fedcba987654"; + let pr_value3 = table.get_value(version, pkgarch, checksum3).await.unwrap(); + assert_eq!(pr_value3, 2, "Third checksum should get PR value 2"); + + // Requesting the same checksum again should return the same PR value + let pr_value1_again = table.get_value(version, pkgarch, checksum1).await.unwrap(); + assert_eq!( + pr_value1_again, 0, + "Same checksum should return same PR value" + ); + + let pr_value2_again = table.get_value(version, pkgarch, checksum2).await.unwrap(); + assert_eq!( + pr_value2_again, 1, + "Same checksum should return same PR value" + ); + + // Test with different architecture - should start from 0 again + let different_arch = "aarch64"; + let pr_value_diff_arch = table + .get_value(version, different_arch, checksum1) + .await + .unwrap(); + assert_eq!( + pr_value_diff_arch, 0, + "Different architecture should start from 0" + ); + + // Test with different version - should start from 0 again + let different_version = "2.0.0"; + let pr_value_diff_version = table + .get_value(different_version, pkgarch, checksum1) + .await + .unwrap(); + assert_eq!( + pr_value_diff_version, 0, + "Different version should start from 0" + ); + } + + #[tokio::test] + async fn test_find_value_existing_entries() { + let temp_dir = tempdir().unwrap(); + let db_path = temp_dir.path().join("test_find.db"); + + let database = PrDatabase::new(&db_path, false, false, SyncMode::Immediate) + .await + .unwrap(); + let table = database.get_table("TEST_FIND_TABLE").await.unwrap(); + + let version = "1.0.0"; + let pkgarch = "x86_64"; + let checksum = "test_checksum"; + + // Initially should find nothing + let initial_find = table.find_value(version, pkgarch, checksum).await.unwrap(); + assert_eq!(initial_find, None, "Should find no value initially"); + + // Create an entry + let pr_value = table.get_value(version, pkgarch, checksum).await.unwrap(); + assert_eq!(pr_value, 0, "First entry should get PR value 0"); + + // Now find_value should return the created value + let found_value = table.find_value(version, pkgarch, checksum).await.unwrap(); + assert_eq!(found_value, Some(0), "Should find the created value"); + + // find_max_value should also work + let max_value = table.find_max_value(version, pkgarch).await.unwrap(); + assert_eq!(max_value, Some(0), "Max value should be 0"); + } + + #[tokio::test] + async fn test_nohist_mode_behavior() { + let temp_dir = tempdir().unwrap(); + let db_path = temp_dir.path().join("test_nohist.db"); + + // Create database with nohist=true (no decrements allowed) + let database = PrDatabase::new(&db_path, true, false, SyncMode::Immediate) + .await + .unwrap(); + let table = database.get_table("TEST_NOHIST_TABLE").await.unwrap(); + + let version = "1.0.0"; + let pkgarch = "x86_64"; + + // First checksum should get PR value 0 + let checksum1 = "checksum1"; + let pr_value1 = table.get_value(version, pkgarch, checksum1).await.unwrap(); + assert_eq!(pr_value1, 0, "First checksum in nohist mode should get 0"); + + // Different checksum should get incremented value + let checksum2 = "checksum2"; + let pr_value2 = table.get_value(version, pkgarch, checksum2).await.unwrap(); + assert_eq!( + pr_value2, 1, + "Different checksum in nohist mode should increment" + ); + + // Same checksum should return same value + let pr_value1_again = table.get_value(version, pkgarch, checksum1).await.unwrap(); + assert_eq!( + pr_value1_again, 0, + "Same checksum should return same value in nohist mode" + ); + } +} diff --git a/src/server.rs b/src/server.rs index 17fbba1..edde46e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -91,10 +91,12 @@ impl PrServer { // Spawn signal handler task let shutdown_tx_clone = shutdown_tx.clone(); tokio::spawn(async move { - let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) - .expect("Failed to register SIGTERM handler"); - let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) - .expect("Failed to register SIGINT handler"); + let mut sigterm = + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("Failed to register SIGTERM handler"); + let mut sigint = + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) + .expect("Failed to register SIGINT handler"); tokio::select! { _ = sigterm.recv() => { @@ -158,7 +160,10 @@ impl PrServer { let tables = self.tables.read().await; for (table_name, table) in tables.iter() { if let Err(e) = table.flush().await { - warn!("Error flushing table '{}' during shutdown: {}", table_name, e); + warn!( + "Error flushing table '{}' during shutdown: {}", + table_name, e + ); } else { debug!("Successfully flushed table '{}'", table_name); } From 483c8c720ec597bb618493cff6f2f751543d7a85 Mon Sep 17 00:00:00 2001 From: Daniel Spofford <868014+danielspofford@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:52:37 -0600 Subject: [PATCH 6/7] ignore RUSTSEC-2023-0071 --- .cargo/audit.toml | 10 ++++++++++ scripts/checks.sh | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .cargo/audit.toml diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 0000000..d04f7e7 --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,10 @@ +[advisories] +ignore = [ + # RUSTSEC-2023-0071: Marvin attack in rsa crate False positive - we use sqlx with sqlite only, + # not mysql. The rsa crate is a transitive dependency of sqlx-mysql which isn't compiled or + # linked in our build. This is due to a long-standing Cargo bug where lockfiles include all + # workspace dependencies regardless of enabled features + # (https://github.com/rust-lang/cargo/issues/10801). The vulnerability does not affect our + # binary since the code path is never compiled. + "RUSTSEC-2023-0071", +] diff --git a/scripts/checks.sh b/scripts/checks.sh index cd946a5..185f176 100755 --- a/scripts/checks.sh +++ b/scripts/checks.sh @@ -13,7 +13,7 @@ if [ -z "$CI" ]; then echo "cargo install cargo-audit" exit 1 fi - cargo audit --ignore RUSTSEC-2023-0071 + cargo audit fi # Check for cargo-llvm-cov (skip check in CI as it's installed by workflow) From 6d68521c725282e0f7a84db171907994cf7a57bf Mon Sep 17 00:00:00 2001 From: Daniel Spofford <868014+danielspofford@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:52:44 -0600 Subject: [PATCH 7/7] ci: fix codecov --- .github/workflows/checks.yml | 3 ++- .github/workflows/pr.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6685f21..3f22b98 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -45,8 +45,9 @@ jobs: - name: Upload coverage to Codecov if: success() - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./cobertura.xml fail_ci_if_error: true + slug: avocado-linux/prserv diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ac6a2d5..2fc2466 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,3 +8,4 @@ jobs: checks: name: Run Checks uses: ./.github/workflows/checks.yml + secrets: inherit