Files
greptimedb/tests-fuzz/targets/unstable/fuzz_create_table_standalone.rs
Lei, HUANG f567dcef86 feat: allow fuzz input override through env var (#7208)
* feat/allow-fuzz-input-override:
 Add environment override for fuzzing parameters and seed values

 - Implement `get_fuzz_override` function to read override values from environment variables for fuzzing parameters.
 - Allow overriding `SEED`, `ACTIONS`, `ROWS`, `TABLES`, `COLUMNS`, `INSERTS`, and `PARTITIONS` in various fuzzing targets.
 - Introduce new constants `GT_FUZZ_INPUT_MAX_PARTITIONS` and `FUZZ_OVERRIDE_PREFIX`.

Signed-off-by: Lei, HUANG <mrsatangel@gmail.com>

* feat/allow-fuzz-input-override: Remove GT_FUZZ_INPUT_MAX_PARTITIONS constant and usage from fuzzing utils and tests

 • Deleted the GT_FUZZ_INPUT_MAX_PARTITIONS constant from fuzzing utility functions.
 • Updated FuzzInput struct in fuzz_migrate_mito_regions.rs to use a hardcoded range instead of get_gt_fuzz_input_max_partitions for determining the number of partitions.

Signed-off-by: Lei, HUANG <mrsatangel@gmail.com>

* feat/allow-fuzz-input-override:
 Improve fuzzing documentation with environment variable overrides

 Enhanced the fuzzing instructions in the README to include guidance on how to override fuzz input using environment variables, providing an example for better clarity.

Signed-off-by: Lei, HUANG <mrsatangel@gmail.com>

---------

Signed-off-by: Lei, HUANG <mrsatangel@gmail.com>
2025-11-10 14:02:23 +00:00

265 lines
9.4 KiB
Rust

// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![no_main]
use std::collections::HashMap;
use std::fs::create_dir_all;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use common_telemetry::info;
use common_telemetry::tracing::warn;
use libfuzzer_sys::arbitrary::{Arbitrary, Unstructured};
use libfuzzer_sys::fuzz_target;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaChaRng;
use serde::Serialize;
use snafu::ensure;
use sqlx::mysql::MySqlPoolOptions;
use sqlx::{MySql, Pool};
use tests_fuzz::context::TableContext;
use tests_fuzz::error::Result;
use tests_fuzz::fake::{
MappedGenerator, WordGenerator, merge_two_word_map_fn, random_capitalize_map,
uppercase_and_keyword_backtick_map,
};
use tests_fuzz::generator::Generator;
use tests_fuzz::generator::create_expr::CreateTableExprGeneratorBuilder;
use tests_fuzz::ir::CreateTableExpr;
use tests_fuzz::translator::DslTranslator;
use tests_fuzz::translator::mysql::create_expr::CreateTableExprTranslator;
use tests_fuzz::utils::config::{get_conf_path, write_config_file};
use tests_fuzz::utils::health::HttpHealthChecker;
use tests_fuzz::utils::process::{ProcessManager, ProcessState, UnstableProcessController};
use tests_fuzz::utils::{
get_fuzz_override, get_gt_fuzz_input_max_tables, load_unstable_test_env_variables,
};
use tests_fuzz::{error, validator};
use tokio::sync::watch;
struct FuzzContext {
greptime: Pool<MySql>,
}
impl FuzzContext {
async fn close(self) {
self.greptime.close().await;
}
}
#[derive(Clone, Debug)]
struct FuzzInput {
seed: u64,
tables: usize,
}
impl Arbitrary<'_> for FuzzInput {
fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
let seed = get_fuzz_override::<u64>("SEED").unwrap_or(u.int_in_range(u64::MIN..=u64::MAX)?);
let mut rng = ChaChaRng::seed_from_u64(seed);
let max_tables = get_gt_fuzz_input_max_tables();
let tables =
get_fuzz_override::<usize>("TABLES").unwrap_or_else(|| rng.random_range(1..max_tables));
Ok(FuzzInput { seed, tables })
}
}
const DEFAULT_TEMPLATE: &str = "standalone.template.toml";
const DEFAULT_CONFIG_NAME: &str = "standalone.template.toml";
const DEFAULT_ROOT_DIR: &str = "/tmp/unstable_greptime/";
const DEFAULT_MYSQL_URL: &str = "127.0.0.1:4002";
const DEFAULT_HTTP_HEALTH_URL: &str = "http://127.0.0.1:4000/health";
fn generate_create_table_expr<R: Rng + 'static>(rng: &mut R) -> CreateTableExpr {
let columns = rng.random_range(2..30);
let create_table_generator = CreateTableExprGeneratorBuilder::default()
.name_generator(Box::new(MappedGenerator::new(
WordGenerator,
merge_two_word_map_fn(random_capitalize_map, uppercase_and_keyword_backtick_map),
)))
.columns(columns)
.engine("mito")
.build()
.unwrap();
create_table_generator.generate(rng).unwrap()
}
async fn connect_mysql(addr: &str, database: &str) -> Pool<MySql> {
loop {
match MySqlPoolOptions::new()
.acquire_timeout(Duration::from_secs(30))
.connect(&format!("mysql://{addr}/{database}"))
.await
{
Ok(mysql) => return mysql,
Err(err) => {
warn!("Reconnecting to {addr}, error: {err}")
}
}
}
}
const FUZZ_TESTS_DATABASE: &str = "fuzz_tests";
async fn execute_unstable_create_table(
unstable_process_controller: Arc<UnstableProcessController>,
rx: watch::Receiver<ProcessState>,
input: FuzzInput,
) -> Result<()> {
// Starts the unstable process.
let moved_unstable_process_controller = unstable_process_controller.clone();
let handler = tokio::spawn(async move { moved_unstable_process_controller.start().await });
let mysql_public = connect_mysql(DEFAULT_MYSQL_URL, "public").await;
loop {
let sql = format!("CREATE DATABASE IF NOT EXISTS {FUZZ_TESTS_DATABASE}");
match sqlx::query(&sql).execute(&mysql_public).await {
Ok(result) => {
info!("Create database: {}, result: {result:?}", sql);
break;
}
Err(err) => warn!("Failed to create database: {}, error: {err}", sql),
}
}
let mysql = connect_mysql(DEFAULT_MYSQL_URL, FUZZ_TESTS_DATABASE).await;
let mut rng = ChaChaRng::seed_from_u64(input.seed);
let ctx = FuzzContext { greptime: mysql };
let mut table_states = HashMap::new();
for _ in 0..input.tables {
let expr = generate_create_table_expr(&mut rng);
let table_ctx = Arc::new(TableContext::from(&expr));
let table_name = expr.table_name.to_string();
if table_states.contains_key(&table_name) {
warn!("ignores same name table: {table_name}");
// ignores.
continue;
}
let translator = CreateTableExprTranslator;
let sql = translator.translate(&expr).unwrap();
let result = sqlx::query(&sql).execute(&ctx.greptime).await;
match result {
Ok(result) => {
let state = *rx.borrow();
table_states.insert(table_name, state);
validate_columns(&ctx.greptime, FUZZ_TESTS_DATABASE, &table_ctx).await;
info!("Create table: {sql}, result: {result:?}");
}
Err(err) => {
// FIXME(weny): support to retry it later.
if matches!(err, sqlx::Error::PoolTimedOut) {
warn!("ignore pool timeout, sql: {sql}");
continue;
}
let state = *rx.borrow();
ensure!(
!state.health(),
error::UnexpectedSnafu {
violated: format!("Failed to create table: {sql}, error: {err}")
}
);
table_states.insert(table_name, state);
continue;
}
}
}
loop {
let sql = format!("DROP DATABASE IF EXISTS {FUZZ_TESTS_DATABASE}");
match sqlx::query(&sql).execute(&mysql_public).await {
Ok(result) => {
info!("Drop database: {}, result: {result:?}", sql);
break;
}
Err(err) => warn!("Failed to drop database: {}, error: {err}", sql),
}
}
// Cleans up
ctx.close().await;
unstable_process_controller.stop();
let _ = handler.await;
info!("Finishing test for input: {:?}", input);
Ok(())
}
async fn validate_columns(client: &Pool<MySql>, schema_name: &str, table_ctx: &TableContext) {
loop {
match validator::column::fetch_columns(client, schema_name.into(), table_ctx.name.clone())
.await
{
Ok(mut column_entries) => {
column_entries.sort_by(|a, b| a.column_name.cmp(&b.column_name));
let mut columns = table_ctx.columns.clone();
columns.sort_by(|a, b| a.name.value.cmp(&b.name.value));
validator::column::assert_eq(&column_entries, &columns).unwrap();
return;
}
Err(err) => warn!(
"Failed to fetch table '{}' columns, error: {}",
table_ctx.name, err
),
}
}
}
fuzz_target!(|input: FuzzInput| {
common_telemetry::init_default_ut_logging();
common_runtime::block_on_global(async {
let variables = load_unstable_test_env_variables();
let root_dir = variables.root_dir.unwrap_or(DEFAULT_ROOT_DIR.to_string());
create_dir_all(&root_dir).unwrap();
let output_config_path = format!("{root_dir}{DEFAULT_CONFIG_NAME}");
let data_home = format!("{root_dir}datahome");
let mut conf_path = get_conf_path();
conf_path.push(DEFAULT_TEMPLATE);
let template_path = conf_path.to_str().unwrap().to_string();
// Writes config file.
#[derive(Serialize)]
struct Context {
data_home: String,
}
write_config_file(&template_path, &Context { data_home }, &output_config_path)
.await
.unwrap();
let args = vec![
"standalone".to_string(),
"start".to_string(),
format!("--config-file={output_config_path}"),
];
let process_manager = ProcessManager::new();
let (tx, rx) = watch::channel(ProcessState::NotSpawn);
let unstable_process_controller = Arc::new(UnstableProcessController {
binary_path: variables.binary_path,
args,
root_dir,
seed: input.seed,
process_manager,
health_check: Box::new(HttpHealthChecker {
url: DEFAULT_HTTP_HEALTH_URL.to_string(),
}),
sender: tx,
running: Arc::new(AtomicBool::new(false)),
});
execute_unstable_create_table(unstable_process_controller, rx, input)
.await
.unwrap_or_else(|err| panic!("fuzz test must be succeed: {err:?}"));
})
});