(rust) sqlx tests for armasuisse
This commit is contained in:
@@ -1,2 +1,4 @@
|
|||||||
target
|
target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
test*
|
||||||
|
log.db*
|
||||||
|
|||||||
+6
-1
@@ -4,7 +4,12 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
chrono = "0.4.41"
|
||||||
clap = { version = "4.5.0", features = ["derive"] }
|
clap = { version = "4.5.0", features = ["derive"] }
|
||||||
dirs = "5.0.1"
|
|
||||||
ctrlc = "3.4"
|
ctrlc = "3.4"
|
||||||
|
dirs = "5.0.1"
|
||||||
|
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite", "chrono" ] }
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
tokio = { version = "1.42.0", features = ["full", "rt"] }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use sqlx::{
|
||||||
|
migrate::MigrateDatabase,
|
||||||
|
query,
|
||||||
|
query_as,
|
||||||
|
Row,
|
||||||
|
sqlite::{
|
||||||
|
SqlitePool,
|
||||||
|
SqlitePoolOptions,
|
||||||
|
},
|
||||||
|
Sqlite,
|
||||||
|
};
|
||||||
|
|
||||||
|
const URL_DATABASE: &str = "log.db";
|
||||||
|
const TABLE_TESTS: &str = "tests";
|
||||||
|
const TABLE_LOGS: &str = "logs";
|
||||||
|
const TEST_NONE: &str = "none";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Error in crate `sqlx`: {0}")]
|
||||||
|
Internal(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, sqlx::FromRow)]
|
||||||
|
pub struct LogRecord {
|
||||||
|
log_id: i32,
|
||||||
|
/// timestamp of the logs creation
|
||||||
|
log_time: DateTime<Local>,
|
||||||
|
/// logs affiliated test
|
||||||
|
test_name: String,
|
||||||
|
/// log message
|
||||||
|
log_message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, sqlx::FromRow)]
|
||||||
|
pub struct TestRecord {
|
||||||
|
/// name of test
|
||||||
|
test_name: String,
|
||||||
|
/// time test is scheduled to start
|
||||||
|
test_time: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Logger {
|
||||||
|
database: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
pub async fn setup() -> Result<Self, Error> {
|
||||||
|
match Sqlite::database_exists(URL_DATABASE).await {
|
||||||
|
Ok(true) => {}
|
||||||
|
Ok(false) => {
|
||||||
|
println!("created `{}`", URL_DATABASE);
|
||||||
|
_ = Sqlite::create_database(URL_DATABASE).await;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
return Err(Error::Internal(error.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let db = match SqlitePool::connect(URL_DATABASE).await {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(Error::Internal(error.to_string())),
|
||||||
|
};
|
||||||
|
match query(&format!(
|
||||||
|
"CREATE TABLE IF NOT EXISTS {TABLE_TESTS} (
|
||||||
|
test_name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
test_time INTEGER DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (test_name)
|
||||||
|
)"
|
||||||
|
)).execute(&db).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => { return Err(Error::Internal(error.to_string())); }
|
||||||
|
};
|
||||||
|
match query(&format!(
|
||||||
|
"CREATE TABLE IF NOT EXISTS {TABLE_LOGS} (
|
||||||
|
log_id INTEGER NOT NULL UNIQUE,
|
||||||
|
log_time INTEGER DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
test_name VARCHAR(255) NOT NULL,
|
||||||
|
log_message VARCHAR(2047),
|
||||||
|
PRIMARY KEY (log_id),
|
||||||
|
FOREIGN KEY (test_name) REFERENCES {TABLE_TESTS}(test_name)
|
||||||
|
);"
|
||||||
|
)).execute(&db).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => { return Err(Error::Internal(error.to_string())); }
|
||||||
|
};
|
||||||
|
match query(&format!(
|
||||||
|
"INSERT OR IGNORE INTO {TABLE_TESTS} (test_name) VALUES('{TEST_NONE}');"
|
||||||
|
)).execute(&db).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => { return Err(Error::Internal(error.to_string())); }
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self{database: db})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query(&self, _query_: &str) -> Result<(), Error> {
|
||||||
|
match query(_query_).execute(&self.database).await {
|
||||||
|
Err(error) => return Err(Error::Internal(error.to_string())),
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_for_test(&self, _query_: &str) -> Result<Vec<TestRecord>, Error> {
|
||||||
|
match query_as(_query_).fetch_all(&self.database).await {
|
||||||
|
Ok(records) => Ok(records),
|
||||||
|
Err(error) => return Err(Error::Internal(error.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_for_log(&self, query: &str) -> Result<Vec<LogRecord>, Error> {
|
||||||
|
match query_as(query).fetch_all(&self.database).await {
|
||||||
|
Ok(records) => Ok(records),
|
||||||
|
Err(error) => return Err(Error::Internal(error.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_test(&self, name: Option<&str>) -> Result<Vec<TestRecord>, Error> {
|
||||||
|
match name {
|
||||||
|
Some(value) => {
|
||||||
|
self.query_for_test(&format!("SELECT * FROM {TABLE_TESTS} WHERE test_name = '{value}';")).await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.query_for_test(&format!("SELECT * FROM {TABLE_TESTS};")).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: figure out time datatype
|
||||||
|
pub async fn add_test(&self, name: &str, time: &str) -> Result<(), Error> {
|
||||||
|
self.query(&format!("INSERT INTO {TABLE_TESTS} (test_name, test_time) VALUES('{name}', '{time}');")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_test(&self, name: &str) -> Result<(), Error> {
|
||||||
|
self.query(&format!("DELETE FROM {TABLE_TESTS} WHERE test_name = '{name}';")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_logs_by_testname(&self, name: String) -> Result<Vec<LogRecord>, Error> {
|
||||||
|
self.query_for_log(&format!("SELECT * FROM {TABLE_LOGS} WHERE test_name = '{name}';")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_logs_by_id(&self, start: usize, stop: Option<usize>) -> Result<Vec<LogRecord>, Error> {
|
||||||
|
let end = match stop {
|
||||||
|
Some(value) => value,
|
||||||
|
None => start,
|
||||||
|
};
|
||||||
|
self.query_for_log(&format!("SELECT * FROM {TABLE_LOGS} WHERE log_id BETWEEN {start} AND {end};")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_log(&self, test: &str, message: &str) -> Result<(), Error> {
|
||||||
|
println!("parameters: {test}, {message}");
|
||||||
|
self.query(&format!("INSERT INTO {TABLE_LOGS} (test_name, log_message) VALUES('{test}', '{message}');")).await
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
-58
@@ -1,70 +1,102 @@
|
|||||||
#![allow(dead_code, unused)]
|
#![allow(dead_code, unused)]
|
||||||
|
|
||||||
use std::{
|
mod log_handler;
|
||||||
io::{stdin, stdout, Error, Result, Write},
|
mod log_structures;
|
||||||
sync::{
|
|
||||||
atomic::{AtomicBool, Ordering}, Arc
|
use chrono::{
|
||||||
},
|
DateTime, Local,
|
||||||
|
};
|
||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use sqlx::{
|
||||||
|
database, migrate::MigrateDatabase, query, query_as, sqlite::{
|
||||||
|
SqlitePool,
|
||||||
|
SqlitePoolOptions,
|
||||||
|
}, Row, Sqlite
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
use log_handler::Logger;
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
|
||||||
let signal_running = running.clone();
|
|
||||||
let mut input: String = String::new();
|
|
||||||
let mut length: usize;
|
|
||||||
let mut tokens: Vec<&str>;
|
|
||||||
|
|
||||||
ctrlc::set_handler(move || {
|
const DATABASE_URL: &str = "./test.db";
|
||||||
signal_running.swap(false, Ordering::SeqCst);
|
const NAME_LOG_TABLE: &str = "logs";
|
||||||
});
|
const NAME_TEST_TABLE: &str = "tests";
|
||||||
|
|
||||||
while running.load(Ordering::SeqCst) {
|
#[derive(Parser, Debug)]
|
||||||
print!("> ");
|
#[clap(author, version, about, long_about=None)]
|
||||||
_ = stdout().flush();
|
struct Arguments {
|
||||||
length = match stdin().read_line(&mut input) {
|
/// subcommand
|
||||||
Ok(value) => value,
|
#[command(subcommand)]
|
||||||
Err(error) => {
|
pub action: Action,
|
||||||
println!("\nError: {:#?}\n", error);
|
}
|
||||||
continue;
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Debug, Clone, Subcommand)]
|
||||||
|
pub enum Action {
|
||||||
|
/// perform action on logs
|
||||||
|
#[command(subcommand)]
|
||||||
|
log(SubAction),
|
||||||
|
|
||||||
|
// /// perform action on tests
|
||||||
|
// test(SubAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Debug, Clone, Subcommand)]
|
||||||
|
pub enum SubAction {
|
||||||
|
add(AddArgs),
|
||||||
|
get(GetArgs),
|
||||||
|
remove(RemoveArgs),
|
||||||
|
show(ShowArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct AddArgs {
|
||||||
|
test: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct GetArgs {
|
||||||
|
id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct RemoveArgs {
|
||||||
|
range_start: usize,
|
||||||
|
range_stop: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct ShowArgs {
|
||||||
|
range_start: Option<usize>,
|
||||||
|
range_stop: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let args = Arguments::try_parse()?;
|
||||||
|
let logger = Logger::setup().await.unwrap();
|
||||||
|
let read_logger = logger.clone();
|
||||||
|
|
||||||
|
match args.action {
|
||||||
|
Action::log(subaction) => {
|
||||||
|
match subaction {
|
||||||
|
SubAction::add(subargs) => {
|
||||||
|
logger.add_log(&subargs.test, &subargs.message).await.unwrap();
|
||||||
|
}
|
||||||
|
SubAction::get(subargs) => {
|
||||||
|
let record = logger.query_for_log(&format!("DELETE FROM logs WHERE log_id = {} RETURNING log_id, log_time, test_name, log_message;", subargs.id)).await.unwrap();
|
||||||
|
println!("record: {:#?}", record);
|
||||||
|
}
|
||||||
|
SubAction::remove(subargs) => {
|
||||||
|
// remove_log(&db, subargs.range_start, subargs.range_stop).await?;
|
||||||
|
}
|
||||||
|
SubAction::show(subargs) => {
|
||||||
|
let records = read_logger.query_for_log(&format!("SELECT * FROM logs;")).await.unwrap();
|
||||||
|
println!("records: {:#?}", records);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if 0 == length {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input = input.trim_matches('\n').to_owned();
|
|
||||||
tokens = input.splitn(2,[' ']).collect();
|
|
||||||
if tokens.len() != 2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cmd = tokens.remove(0);
|
|
||||||
let data = tokens.remove(0);
|
|
||||||
|
|
||||||
if cmd == "raw" {
|
|
||||||
println!("{:02x?}", data.as_bytes());
|
|
||||||
} else if cmd == "str" {
|
|
||||||
println!("{}", data);
|
|
||||||
}
|
|
||||||
|
|
||||||
input.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: not required, but might be fun to implement at some point
|
|
||||||
// fn tokenise(input: &str, separator: Vec<&str>) -> Vec<String> {
|
|
||||||
// let separator = separator.join("");
|
|
||||||
// let mut tokens: Vec<String> = Vec::new();
|
|
||||||
// let mut quoted = false;
|
|
||||||
//
|
|
||||||
// for char in input.chars() {
|
|
||||||
// input.len
|
|
||||||
// if separator.contains(char) {
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// tokens
|
|
||||||
// }
|
|
||||||
|
|||||||
-192
@@ -1,192 +0,0 @@
|
|||||||
DeriveInput {
|
|
||||||
attrs: [],
|
|
||||||
vis: Visibility::Public(
|
|
||||||
Pub,
|
|
||||||
),
|
|
||||||
ident: Ident {
|
|
||||||
ident: "Settings",
|
|
||||||
span: #0 bytes(1363..1371),
|
|
||||||
},
|
|
||||||
generics: Generics {
|
|
||||||
lt_token: None,
|
|
||||||
params: [],
|
|
||||||
gt_token: None,
|
|
||||||
where_clause: None,
|
|
||||||
},
|
|
||||||
data: Data::Struct {
|
|
||||||
struct_token: Struct,
|
|
||||||
fields: Fields::Named {
|
|
||||||
brace_token: Brace,
|
|
||||||
named: [
|
|
||||||
Field {
|
|
||||||
attrs: [
|
|
||||||
Attribute {
|
|
||||||
pound_token: Pound,
|
|
||||||
style: AttrStyle::Outer,
|
|
||||||
bracket_token: Bracket,
|
|
||||||
meta: Meta::Path {
|
|
||||||
leading_colon: None,
|
|
||||||
segments: [
|
|
||||||
PathSegment {
|
|
||||||
ident: Ident {
|
|
||||||
ident: "nested_config",
|
|
||||||
span: #0 bytes(1380..1393),
|
|
||||||
},
|
|
||||||
arguments: PathArguments::None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vis: Visibility::Public(
|
|
||||||
Pub,
|
|
||||||
),
|
|
||||||
mutability: FieldMutability::None,
|
|
||||||
ident: Some(
|
|
||||||
Ident {
|
|
||||||
ident: "general",
|
|
||||||
span: #0 bytes(1403..1410),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
colon_token: Some(
|
|
||||||
Colon,
|
|
||||||
),
|
|
||||||
ty: Type::Path {
|
|
||||||
qself: None,
|
|
||||||
path: Path {
|
|
||||||
leading_colon: None,
|
|
||||||
segments: [
|
|
||||||
PathSegment {
|
|
||||||
ident: Ident {
|
|
||||||
ident: "GeneralSettings",
|
|
||||||
span: #0 bytes(1412..1427),
|
|
||||||
},
|
|
||||||
arguments: PathArguments::None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Comma,
|
|
||||||
Field {
|
|
||||||
attrs: [
|
|
||||||
Attribute {
|
|
||||||
pound_token: Pound,
|
|
||||||
style: AttrStyle::Outer,
|
|
||||||
bracket_token: Bracket,
|
|
||||||
meta: Meta::Path {
|
|
||||||
leading_colon: None,
|
|
||||||
segments: [
|
|
||||||
PathSegment {
|
|
||||||
ident: Ident {
|
|
||||||
ident: "nested_config",
|
|
||||||
span: #0 bytes(1435..1448),
|
|
||||||
},
|
|
||||||
arguments: PathArguments::None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vis: Visibility::Public(
|
|
||||||
Pub,
|
|
||||||
),
|
|
||||||
mutability: FieldMutability::None,
|
|
||||||
ident: Some(
|
|
||||||
Ident {
|
|
||||||
ident: "format",
|
|
||||||
span: #0 bytes(1458..1464),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
colon_token: Some(
|
|
||||||
Colon,
|
|
||||||
),
|
|
||||||
ty: Type::Path {
|
|
||||||
qself: None,
|
|
||||||
path: Path {
|
|
||||||
leading_colon: None,
|
|
||||||
segments: [
|
|
||||||
PathSegment {
|
|
||||||
ident: Ident {
|
|
||||||
ident: "FormatSettings",
|
|
||||||
span: #0 bytes(1466..1480),
|
|
||||||
},
|
|
||||||
arguments: PathArguments::None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Comma,
|
|
||||||
Field {
|
|
||||||
attrs: [
|
|
||||||
Attribute {
|
|
||||||
pound_token: Pound,
|
|
||||||
style: AttrStyle::Outer,
|
|
||||||
bracket_token: Bracket,
|
|
||||||
meta: Meta::Path {
|
|
||||||
leading_colon: None,
|
|
||||||
segments: [
|
|
||||||
PathSegment {
|
|
||||||
ident: Ident {
|
|
||||||
ident: "nested_config",
|
|
||||||
span: #0 bytes(1488..1501),
|
|
||||||
},
|
|
||||||
arguments: PathArguments::None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Attribute {
|
|
||||||
pound_token: Pound,
|
|
||||||
style: AttrStyle::Outer,
|
|
||||||
bracket_token: Bracket,
|
|
||||||
meta: Meta::Path {
|
|
||||||
leading_colon: None,
|
|
||||||
segments: [
|
|
||||||
PathSegment {
|
|
||||||
ident: Ident {
|
|
||||||
ident: "no_config",
|
|
||||||
span: #0 bytes(1509..1518),
|
|
||||||
},
|
|
||||||
arguments: PathArguments::None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vis: Visibility::Public(
|
|
||||||
Pub,
|
|
||||||
),
|
|
||||||
mutability: FieldMutability::None,
|
|
||||||
ident: Some(
|
|
||||||
Ident {
|
|
||||||
ident: "styles",
|
|
||||||
span: #0 bytes(1528..1534),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
colon_token: Some(
|
|
||||||
Colon,
|
|
||||||
),
|
|
||||||
ty: Type::Path {
|
|
||||||
qself: None,
|
|
||||||
path: Path {
|
|
||||||
leading_colon: None,
|
|
||||||
segments: [
|
|
||||||
PathSegment {
|
|
||||||
ident: Ident {
|
|
||||||
ident: "StyleSettings",
|
|
||||||
span: #0 bytes(1536..1549),
|
|
||||||
},
|
|
||||||
arguments: PathArguments::None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Comma,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
semi_token: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user