mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-01-04 12:22:55 +00:00
* 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>
338 lines
12 KiB
Rust
338 lines
12 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::sync::Arc;
|
|
|
|
use arbitrary::{Arbitrary, Unstructured};
|
|
use common_telemetry::info;
|
|
use datatypes::prelude::{ConcreteDataType, DataType};
|
|
use libfuzzer_sys::fuzz_target;
|
|
use rand::{Rng, SeedableRng};
|
|
use rand_chacha::ChaChaRng;
|
|
use snafu::ResultExt;
|
|
use sqlx::{MySql, Pool};
|
|
use strum::{EnumIter, IntoEnumIterator};
|
|
use tests_fuzz::context::{TableContext, TableContextRef};
|
|
use tests_fuzz::error::{self, Result, UnexpectedSnafu};
|
|
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::alter_expr::{
|
|
AlterExprAddColumnGeneratorBuilder, AlterExprDropColumnGeneratorBuilder,
|
|
AlterExprModifyDataTypeGeneratorBuilder, AlterExprRenameGeneratorBuilder,
|
|
AlterExprSetTableOptionsGeneratorBuilder, AlterExprUnsetTableOptionsGeneratorBuilder,
|
|
};
|
|
use tests_fuzz::generator::create_expr::CreateTableExprGeneratorBuilder;
|
|
use tests_fuzz::ir::{
|
|
AlterTableExpr, AlterTableOption, CreateTableExpr, droppable_columns, modifiable_columns,
|
|
};
|
|
use tests_fuzz::translator::DslTranslator;
|
|
use tests_fuzz::translator::mysql::alter_expr::AlterTableExprTranslator;
|
|
use tests_fuzz::translator::mysql::create_expr::CreateTableExprTranslator;
|
|
use tests_fuzz::utils::{
|
|
Connections, get_fuzz_override, get_gt_fuzz_input_max_alter_actions,
|
|
get_gt_fuzz_input_max_columns, init_greptime_connections_via_env,
|
|
};
|
|
use tests_fuzz::validator;
|
|
struct FuzzContext {
|
|
greptime: Pool<MySql>,
|
|
}
|
|
|
|
impl FuzzContext {
|
|
async fn close(self) {
|
|
self.greptime.close().await;
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct FuzzInput {
|
|
seed: u64,
|
|
actions: usize,
|
|
}
|
|
|
|
#[derive(Debug, EnumIter)]
|
|
enum AlterTableKind {
|
|
AddColumn,
|
|
DropColumn,
|
|
RenameTable,
|
|
ModifyDataType,
|
|
SetTableOptions,
|
|
UnsetTableOptions,
|
|
}
|
|
|
|
fn generate_create_table_expr<R: Rng + 'static>(rng: &mut R) -> Result<CreateTableExpr> {
|
|
let max_columns = get_gt_fuzz_input_max_columns();
|
|
let columns = rng.random_range(2..max_columns);
|
|
let mut with_clause = HashMap::new();
|
|
if rng.random_bool(0.5) {
|
|
with_clause.insert("append_mode".to_string(), "true".to_string());
|
|
}
|
|
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")
|
|
.with_clause(with_clause)
|
|
.build()
|
|
.unwrap();
|
|
create_table_generator.generate(rng)
|
|
}
|
|
|
|
/// Returns the insert sql that insert one row of non-default value in table
|
|
#[allow(unused)]
|
|
fn generate_insert_table_sql<R: Rng + 'static>(
|
|
rng: &mut R,
|
|
create_table_expr: &CreateTableExpr,
|
|
) -> Result<String> {
|
|
use datatypes::value::Value;
|
|
let mut column_names = vec![];
|
|
let mut column_values = vec![];
|
|
for column in &create_table_expr.columns {
|
|
if column.is_time_index() {
|
|
continue;
|
|
}
|
|
|
|
let marker_name = &column.name;
|
|
let marker_value = match column.column_type {
|
|
ConcreteDataType::Boolean(_) => Value::Boolean(true),
|
|
ConcreteDataType::Int16(_) => Value::Int16(42),
|
|
ConcreteDataType::Int32(_) => Value::Int32(42),
|
|
ConcreteDataType::Int64(_) => Value::Int64(42),
|
|
ConcreteDataType::Float32(_) => Value::from(42.0f32),
|
|
ConcreteDataType::Float64(_) => Value::from(42.0f64),
|
|
ConcreteDataType::String(_) => Value::from("How many roads must a man walk down?"),
|
|
_ => UnexpectedSnafu {
|
|
violated: format!(
|
|
"Unexpected datatype, found {} in create-table={:?}",
|
|
column.column_type, create_table_expr
|
|
),
|
|
}
|
|
.fail()?,
|
|
};
|
|
column_names.push(marker_name.to_string());
|
|
column_values.push(marker_value.to_string());
|
|
}
|
|
|
|
let table_name = &create_table_expr.table_name;
|
|
|
|
let ts_column = create_table_expr
|
|
.columns
|
|
.iter()
|
|
.find(|c| c.is_time_index())
|
|
.unwrap();
|
|
|
|
let ts_column_name = &ts_column.name;
|
|
let ts_column_value = ts_column.column_type.default_value();
|
|
|
|
let concat_column_names = column_names.join(",");
|
|
|
|
let concat_column_values = column_values.join(",");
|
|
|
|
let sql = format!(
|
|
"INSERT INTO {table_name} ({concat_column_names}, {ts_column_name}) VALUES ({concat_column_values}, \"{ts_column_value}\");"
|
|
);
|
|
|
|
Ok(sql)
|
|
}
|
|
|
|
fn generate_alter_table_expr<R: Rng + 'static>(
|
|
table_ctx: TableContextRef,
|
|
rng: &mut R,
|
|
) -> Result<AlterTableExpr> {
|
|
let kinds = AlterTableKind::iter().collect::<Vec<_>>();
|
|
match kinds[rng.random_range(0..kinds.len())] {
|
|
AlterTableKind::DropColumn if !droppable_columns(&table_ctx.columns).is_empty() => {
|
|
AlterExprDropColumnGeneratorBuilder::default()
|
|
.table_ctx(table_ctx)
|
|
.build()
|
|
.unwrap()
|
|
.generate(rng)
|
|
}
|
|
AlterTableKind::ModifyDataType if !modifiable_columns(&table_ctx.columns).is_empty() => {
|
|
AlterExprModifyDataTypeGeneratorBuilder::default()
|
|
.table_ctx(table_ctx)
|
|
.build()
|
|
.unwrap()
|
|
.generate(rng)
|
|
}
|
|
AlterTableKind::RenameTable => AlterExprRenameGeneratorBuilder::default()
|
|
.table_ctx(table_ctx)
|
|
.name_generator(Box::new(MappedGenerator::new(
|
|
WordGenerator,
|
|
merge_two_word_map_fn(random_capitalize_map, uppercase_and_keyword_backtick_map),
|
|
)))
|
|
.build()
|
|
.unwrap()
|
|
.generate(rng),
|
|
AlterTableKind::SetTableOptions => {
|
|
let expr_generator = AlterExprSetTableOptionsGeneratorBuilder::default()
|
|
.table_ctx(table_ctx)
|
|
.build()
|
|
.unwrap();
|
|
expr_generator.generate(rng)
|
|
}
|
|
AlterTableKind::UnsetTableOptions => {
|
|
let expr_generator = AlterExprUnsetTableOptionsGeneratorBuilder::default()
|
|
.table_ctx(table_ctx)
|
|
.build()
|
|
.unwrap();
|
|
expr_generator.generate(rng)
|
|
}
|
|
_ => {
|
|
let location = rng.random_bool(0.5);
|
|
let expr_generator = AlterExprAddColumnGeneratorBuilder::default()
|
|
.table_ctx(table_ctx)
|
|
.location(location)
|
|
.build()
|
|
.unwrap();
|
|
expr_generator.generate(rng)
|
|
}
|
|
}
|
|
}
|
|
|
|
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_actions = get_gt_fuzz_input_max_alter_actions();
|
|
let actions = get_fuzz_override::<usize>("ACTIONS")
|
|
.unwrap_or_else(|| rng.random_range(1..max_actions));
|
|
|
|
Ok(FuzzInput { seed, actions })
|
|
}
|
|
}
|
|
|
|
async fn execute_alter_table(ctx: FuzzContext, input: FuzzInput) -> Result<()> {
|
|
info!("input: {input:?}");
|
|
let mut rng = ChaChaRng::seed_from_u64(input.seed);
|
|
|
|
// Create table
|
|
let expr = generate_create_table_expr(&mut rng).unwrap();
|
|
let translator = CreateTableExprTranslator;
|
|
let sql = translator.translate(&expr)?;
|
|
let result = sqlx::query(&sql)
|
|
.execute(&ctx.greptime)
|
|
.await
|
|
.context(error::ExecuteQuerySnafu { sql: &sql })?;
|
|
info!("Create table: {sql}, result: {result:?}");
|
|
|
|
let insert = generate_insert_table_sql(&mut rng, &expr)?;
|
|
let result = sqlx::query(&insert)
|
|
.execute(&ctx.greptime)
|
|
.await
|
|
.context(error::ExecuteQuerySnafu { sql: &insert })?;
|
|
info!("Insert Into table: {insert}, result: {result:?}");
|
|
|
|
// Alter table actions
|
|
let mut table_ctx = Arc::new(TableContext::from(&expr));
|
|
for _ in 0..input.actions {
|
|
let expr = generate_alter_table_expr(table_ctx.clone(), &mut rng).unwrap();
|
|
let translator = AlterTableExprTranslator;
|
|
let sql = translator.translate(&expr)?;
|
|
let result = sqlx::query(&sql)
|
|
.execute(&ctx.greptime)
|
|
.await
|
|
.context(error::ExecuteQuerySnafu { sql: &sql })?;
|
|
info!("Alter table: {sql}, result: {result:?}");
|
|
// Applies changes
|
|
table_ctx = Arc::new(Arc::unwrap_or_clone(table_ctx).alter(expr).unwrap());
|
|
|
|
// validate value
|
|
validator::column::valid_marker_value(
|
|
&ctx.greptime,
|
|
"public".into(),
|
|
table_ctx.name.clone(),
|
|
)
|
|
.await?;
|
|
|
|
// Validates columns
|
|
let mut column_entries = validator::column::fetch_columns(
|
|
&ctx.greptime,
|
|
"public".into(),
|
|
table_ctx.name.clone(),
|
|
)
|
|
.await?;
|
|
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)?;
|
|
|
|
// Validates table options
|
|
let sql = format!("SHOW CREATE TABLE {}", table_ctx.name);
|
|
let mut table_options = validator::table::fetch_table_options(&ctx.greptime, &sql).await?;
|
|
table_options.sort_by(|a, b| a.key().cmp(b.key()));
|
|
let mut expected_table_options = table_ctx.table_options.clone();
|
|
expected_table_options.sort_by(|a, b| a.key().cmp(b.key()));
|
|
table_options
|
|
.iter()
|
|
.zip(expected_table_options.iter())
|
|
.for_each(|(a, b)| {
|
|
if let (
|
|
AlterTableOption::TwcsMaxOutputFileSize(a),
|
|
AlterTableOption::TwcsMaxOutputFileSize(b),
|
|
) = (a, b)
|
|
{
|
|
// to_string loses precision for ReadableSize, so the size in generated SQL is not the same as the size in the table context,
|
|
// but the string representation should be the same. For example:
|
|
// to_string() from_str()
|
|
// ReadableSize(13001360408898724524) ------------> "11547.5PiB" -----------> ReadableSize(13001329174265200640)
|
|
assert_eq!(a.to_string(), b.to_string());
|
|
} else {
|
|
assert_eq!(a, b);
|
|
}
|
|
});
|
|
|
|
// select from table to make sure the table is still ok
|
|
let sql = format!("SELECT * FROM {}", table_ctx.name);
|
|
let result = sqlx::query(&sql)
|
|
.persistent(false)
|
|
.execute(&ctx.greptime)
|
|
.await
|
|
.context(error::ExecuteQuerySnafu { sql: &sql })?;
|
|
info!("Select from table: {sql}, result: {result:?}");
|
|
}
|
|
|
|
// Cleans up
|
|
let table_name = table_ctx.name.clone();
|
|
let sql = format!("DROP TABLE {}", table_name);
|
|
let result = sqlx::query(&sql)
|
|
.execute(&ctx.greptime)
|
|
.await
|
|
.context(error::ExecuteQuerySnafu { sql })?;
|
|
info!("Drop table: {}, result: {result:?}", table_name);
|
|
ctx.close().await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fuzz_target!(|input: FuzzInput| {
|
|
common_telemetry::init_default_ut_logging();
|
|
common_runtime::block_on_global(async {
|
|
let Connections { mysql } = init_greptime_connections_via_env().await;
|
|
let ctx = FuzzContext {
|
|
greptime: mysql.expect("mysql connection init must be succeed"),
|
|
};
|
|
execute_alter_table(ctx, input)
|
|
.await
|
|
.unwrap_or_else(|err| panic!("fuzz test must be succeed: {err:?}"));
|
|
})
|
|
});
|