From c521f50065d2ef1652a14320cfa2707e35d9ba48 Mon Sep 17 00:00:00 2001 From: peefy Date: Wed, 22 Nov 2023 09:57:27 +0800 Subject: [PATCH] feat: impl basic test suite loader and runner Signed-off-by: peefy --- kclvm/compiler/src/codegen/llvm/node.rs | 2 +- kclvm/driver/src/lib.rs | 84 +++++++- .../driver/src/test_data/pkg_list/pkg1/pkg.k | 1 + .../test_data/pkg_list/pkg1/sub_pkg1/pkg.k | 1 + .../driver/src/test_data/pkg_list/pkg2/pkg.k | 1 + kclvm/driver/src/tests.rs | 11 +- kclvm/runner/src/lib.rs | 89 +++++--- kclvm/runner/src/runner.rs | 43 +++- kclvm/sema/src/resolver/node.rs | 5 +- kclvm/tools/src/lib.rs | 1 + kclvm/tools/src/testing/mod.rs | 58 ++++++ kclvm/tools/src/testing/suite.rs | 195 ++++++++++++++++++ .../src/testing/test_data/module/kcl.mod | 3 + .../src/testing/test_data/module/pkg/func.k | 3 + .../testing/test_data/module/pkg/func_test.k | 7 + kclvm/tools/src/testing/tests.rs | 32 +++ 16 files changed, 507 insertions(+), 29 deletions(-) create mode 100644 kclvm/driver/src/test_data/pkg_list/pkg1/pkg.k create mode 100644 kclvm/driver/src/test_data/pkg_list/pkg1/sub_pkg1/pkg.k create mode 100644 kclvm/driver/src/test_data/pkg_list/pkg2/pkg.k create mode 100644 kclvm/tools/src/testing/mod.rs create mode 100644 kclvm/tools/src/testing/suite.rs create mode 100644 kclvm/tools/src/testing/test_data/module/kcl.mod create mode 100644 kclvm/tools/src/testing/test_data/module/pkg/func.k create mode 100644 kclvm/tools/src/testing/test_data/module/pkg/func_test.k create mode 100644 kclvm/tools/src/testing/tests.rs diff --git a/kclvm/compiler/src/codegen/llvm/node.rs b/kclvm/compiler/src/codegen/llvm/node.rs index e5869e169..5678d05f1 100644 --- a/kclvm/compiler/src/codegen/llvm/node.rs +++ b/kclvm/compiler/src/codegen/llvm/node.rs @@ -261,7 +261,7 @@ impl<'ctx> TypedResultWalker<'ctx> for LLVMCodeGenContext<'ctx> { ); self.br(end_block); self.builder.position_at_end(end_block); - self.ok_result() + Ok(self.undefined_value()) } fn walk_if_stmt(&self, if_stmt: &'ctx ast::IfStmt) -> Self::Result { diff --git a/kclvm/driver/src/lib.rs b/kclvm/driver/src/lib.rs index d9595b6ae..d090809a5 100644 --- a/kclvm/driver/src/lib.rs +++ b/kclvm/driver/src/lib.rs @@ -9,7 +9,7 @@ mod tests; use glob::glob; use kclvm_ast::ast; use kclvm_config::{ - modfile::{KCL_FILE_EXTENSION, KCL_FILE_SUFFIX, KCL_MOD_PATH_ENV}, + modfile::{get_pkg_root, KCL_FILE_EXTENSION, KCL_FILE_SUFFIX, KCL_MOD_PATH_ENV}, path::ModRelativePath, settings::{build_settings_pathbuf, DEFAULT_SETTING_FILE}, }; @@ -17,6 +17,7 @@ use kclvm_parser::LoadProgramOptions; use kclvm_utils::path::PathPrefix; use kpm_metadata::fill_pkg_maps_for_k_file; use std::{ + collections::HashSet, fs::read_dir, io::{self, ErrorKind}, path::{Path, PathBuf}, @@ -286,3 +287,84 @@ pub fn get_kcl_files>(path: P, recursively: bool) -> Result Result> { + let mut dir_list: Vec = Vec::new(); + let mut dir_map: HashSet = HashSet::new(); + let cwd = std::env::current_dir()?; + + let pkgpath = if pkgpath.is_empty() { + cwd.to_string_lossy().to_string() + } else { + pkgpath.to_string() + }; + + let include_sub_pkg = pkgpath.ends_with("/..."); + let pkgpath = if include_sub_pkg { + pkgpath.trim_end_matches("/...").to_string() + } else { + pkgpath + }; + + if pkgpath != "." && pkgpath.ends_with(".") { + return Ok(Vec::new()); + } + + if pkgpath.is_empty() { + return Ok(Vec::new()); + } + + match pkgpath.chars().next() { + Some('.') => { + let pkgpath = Path::new(&cwd).join(&pkgpath); + pkgpath.to_string_lossy().to_string() + } + _ => { + if Path::new(&pkgpath).is_absolute() { + pkgpath.clone() + } else { + if !pkgpath.contains('/') && !pkgpath.contains('\\') { + pkgpath.replace(".", "/") + } else { + let pkgroot = + get_pkg_root(&cwd.to_str().ok_or(anyhow::anyhow!("cwd path not found"))?) + .unwrap_or_default(); + if !pkgroot.is_empty() { + PathBuf::from(pkgroot) + .join(&pkgpath) + .to_string_lossy() + .to_string() + } else { + Path::new(&cwd).join(&pkgpath).to_string_lossy().to_string() + } + } + } + } + }; + + if !include_sub_pkg { + return Ok(vec![pkgpath]); + } + + for entry in WalkDir::new(&pkgpath).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + if !path.is_dir() { + if path.extension().and_then(|ext| ext.to_str()) == Some(KCL_FILE_EXTENSION) + && !path + .file_name() + .map(|name| name.to_string_lossy().starts_with("_")) + .unwrap_or(false) + { + if let Some(dir) = path.parent().map(|p| p.to_string_lossy().to_string()) { + if !dir_map.contains(&dir) { + dir_list.push(dir.clone()); + dir_map.insert(dir); + } + } + } + } + } + + Ok(dir_list) +} diff --git a/kclvm/driver/src/test_data/pkg_list/pkg1/pkg.k b/kclvm/driver/src/test_data/pkg_list/pkg1/pkg.k new file mode 100644 index 000000000..d25d49e0f --- /dev/null +++ b/kclvm/driver/src/test_data/pkg_list/pkg1/pkg.k @@ -0,0 +1 @@ +a = 1 \ No newline at end of file diff --git a/kclvm/driver/src/test_data/pkg_list/pkg1/sub_pkg1/pkg.k b/kclvm/driver/src/test_data/pkg_list/pkg1/sub_pkg1/pkg.k new file mode 100644 index 000000000..d25d49e0f --- /dev/null +++ b/kclvm/driver/src/test_data/pkg_list/pkg1/sub_pkg1/pkg.k @@ -0,0 +1 @@ +a = 1 \ No newline at end of file diff --git a/kclvm/driver/src/test_data/pkg_list/pkg2/pkg.k b/kclvm/driver/src/test_data/pkg_list/pkg2/pkg.k new file mode 100644 index 000000000..a668ca34b --- /dev/null +++ b/kclvm/driver/src/test_data/pkg_list/pkg2/pkg.k @@ -0,0 +1 @@ +a = 2 \ No newline at end of file diff --git a/kclvm/driver/src/tests.rs b/kclvm/driver/src/tests.rs index 3849582ae..c772e3524 100644 --- a/kclvm/driver/src/tests.rs +++ b/kclvm/driver/src/tests.rs @@ -8,7 +8,7 @@ use walkdir::WalkDir; use crate::arguments::parse_key_value_pair; use crate::kpm_metadata::{fetch_metadata, fill_pkg_maps_for_k_file, lookup_the_nearest_file_dir}; -use crate::{canonicalize_input_files, expand_input_files}; +use crate::{canonicalize_input_files, expand_input_files, get_pkg_list}; #[test] fn test_canonicalize_input_files() { @@ -324,3 +324,12 @@ fn test_fetch_metadata_invalid() { Err(e) => panic!("The method should not panic forever.: {:?}", e), } } + +#[test] +fn test_get_pkg_list() { + assert_eq!(get_pkg_list("./src/test_data/pkg_list/").unwrap().len(), 1); + assert_eq!( + get_pkg_list("./src/test_data/pkg_list/...").unwrap().len(), + 3 + ); +} diff --git a/kclvm/runner/src/lib.rs b/kclvm/runner/src/lib.rs index b60753e99..73c698ddc 100644 --- a/kclvm/runner/src/lib.rs +++ b/kclvm/runner/src/lib.rs @@ -7,15 +7,14 @@ use kclvm_ast::{ MAIN_PKG, }; use kclvm_driver::{canonicalize_input_files, expand_input_files}; -use kclvm_error::{Diagnostic, Handler}; use kclvm_parser::{load_program, ParseSession}; use kclvm_query::apply_overrides; -use kclvm_runtime::{Context, PanicInfo, PlanOptions, ValueRef}; +use kclvm_runtime::{Context, PlanOptions, ValueRef}; use kclvm_sema::resolver::{ resolve_program, resolve_program_with_opts, scope::ProgramScope, Options, }; use linker::Command; -pub use runner::{ExecProgramArgs, ExecProgramResult, MapErrorResult}; +pub use runner::{Artifact, ExecProgramArgs, ExecProgramResult, MapErrorResult}; use runner::{KclLibRunner, KclLibRunnerOptions}; use tempfile::tempdir; @@ -27,7 +26,7 @@ pub mod runner; pub mod tests; /// After the kcl program passed through kclvm-parser in the compiler frontend, -/// KCLVM needs to resolve ast, generate corresponding LLVM IR, dynamic link library or +/// KCL needs to resolve ast, generate corresponding LLVM IR, dynamic link library or /// executable file for kcl program in the compiler backend. /// /// Method “execute” is the entry point for the compiler backend. @@ -74,14 +73,8 @@ pub mod tests; pub fn exec_program(sess: Arc, args: &ExecProgramArgs) -> Result { // parse args from json string let opts = args.get_load_program_options(); - let k_files = &args.k_filename_list; - let work_dir = args.work_dir.clone().unwrap_or_default(); - let k_files = expand_input_files(k_files); - let kcl_paths = - canonicalize_input_files(&k_files, work_dir, false).map_err(|err| anyhow!(err))?; - + let kcl_paths = expand_files(args)?; let kcl_paths_str = kcl_paths.iter().map(|s| s.as_str()).collect::>(); - let mut program = load_program(sess.clone(), kcl_paths_str.as_slice(), Some(opts), None) .map_err(|err| anyhow!(err))?; @@ -212,22 +205,10 @@ pub fn execute( let runner = KclLibRunner::new(Some(KclLibRunnerOptions { plugin_agent_ptr: args.plugin_agent, })); - let mut result = runner.run(&lib_path, args)?; + let result = runner.run(&lib_path, args)?; remove_file(&lib_path)?; clean_tmp_files(&temp_entry_file, &lib_suffix)?; - // Wrap runtime error into diagnostic style string. - if !result.err_message.is_empty() { - result.err_message = match Handler::default() - .add_diagnostic(>::into(PanicInfo::from( - result.err_message.as_str(), - ))) - .emit_to_string() - { - Ok(msg) => msg, - Err(err) => err.to_string(), - }; - } Ok(result) } @@ -256,6 +237,66 @@ pub fn execute_module(mut m: Module) -> Result { ) } +/// Build a KCL program and generate a library artifact. +pub fn build_program>( + sess: Arc, + args: &ExecProgramArgs, + output: Option

, +) -> Result { + // Parse program. + let opts = args.get_load_program_options(); + let kcl_paths = expand_files(args)?; + let kcl_paths_str = kcl_paths.iter().map(|s| s.as_str()).collect::>(); + let mut program = load_program(sess.clone(), kcl_paths_str.as_slice(), Some(opts), None) + .map_err(|err| anyhow!(err))?; + // Resolve program. + let scope = resolve_program(&mut program); + emit_compile_diag_to_string(sess, &scope, false)?; + // Create a temp entry file and the temp dir will be delete automatically. + let temp_dir = tempdir()?; + let temp_dir_path = temp_dir.path().to_str().ok_or(anyhow!( + "Internal error: {}: No such file or directory", + temp_dir.path().display() + ))?; + let temp_entry_file = temp_file(temp_dir_path)?; + // Generate native libs. + let lib_paths = assembler::KclvmAssembler::new( + program, + scope, + temp_entry_file.clone(), + KclvmLibAssembler::LLVM, + args.get_package_maps_from_external_pkg(), + ) + .gen_libs()?; + + // Link libs into one library. + let lib_suffix = Command::get_lib_suffix(); + let temp_out_lib_file = if let Some(output) = output { + let path = output + .as_ref() + .to_str() + .ok_or(anyhow!("build output path is not found"))? + .to_string(); + path + } else { + format!("{}{}", temp_entry_file, lib_suffix) + }; + let lib_path = linker::KclvmLinker::link_all_libs(lib_paths, temp_out_lib_file)?; + + // Return the library artifact. + Artifact::from_path(lib_path) +} + +/// Expand and return the normalized file paths for the input file list. +pub fn expand_files(args: &ExecProgramArgs) -> Result> { + let k_files = &args.k_filename_list; + let work_dir = args.work_dir.clone().unwrap_or_default(); + let k_files = expand_input_files(k_files); + let kcl_paths = + canonicalize_input_files(&k_files, work_dir, false).map_err(|err| anyhow!(err))?; + Ok(kcl_paths) +} + /// Clean all the tmp files generated during lib generating and linking. #[inline] fn clean_tmp_files(temp_entry_file: &String, lib_suffix: &String) -> Result<()> { diff --git a/kclvm/runner/src/runner.rs b/kclvm/runner/src/runner.rs index ed8bdcd8d..49fb2a85b 100644 --- a/kclvm/runner/src/runner.rs +++ b/kclvm/runner/src/runner.rs @@ -6,9 +6,11 @@ use kclvm_config::{ modfile::get_vendor_home, settings::{SettingsFile, SettingsPathBuf}, }; +use kclvm_error::{Diagnostic, Handler}; use kclvm_query::r#override::parse_override_spec; -use kclvm_runtime::{Context, ValueRef}; +use kclvm_runtime::{Context, PanicInfo, ValueRef}; use serde::{Deserialize, Serialize}; +use std::ffi::OsStr; const RESULT_SIZE: usize = 2048 * 2048; @@ -206,6 +208,31 @@ impl TryFrom for ExecProgramArgs { } } +/// A public struct named [Artifact] which wraps around the native library [libloading::Library]. +pub struct Artifact(libloading::Library); + +pub trait ProgramRunner { + /// Run with the arguments [ExecProgramArgs] and return the program execute result that + /// contains the planning result and the evaluation errors if any. + fn run(&self, args: &ExecProgramArgs) -> Result; +} + +impl ProgramRunner for Artifact { + fn run(&self, args: &ExecProgramArgs) -> Result { + unsafe { + KclLibRunner::lib_kclvm_plugin_init(&self.0, args.plugin_agent)?; + KclLibRunner::lib_kcl_run(&self.0, args) + } + } +} + +impl Artifact { + pub fn from_path>(path: P) -> Result { + let lib = unsafe { libloading::Library::new(path)? }; + Ok(Self(lib)) + } +} + #[derive(Debug, Default)] pub struct KclLibRunnerOptions { pub plugin_agent_ptr: u64, @@ -379,6 +406,20 @@ impl KclLibRunner { let return_len = 0 - n; result.err_message = String::from_utf8(warn_data[0..return_len as usize].to_vec())?; } + + // Wrap runtime error into diagnostic style string. + if !result.err_message.is_empty() { + result.err_message = match Handler::default() + .add_diagnostic(>::into(PanicInfo::from( + result.err_message.as_str(), + ))) + .emit_to_string() + { + Ok(msg) => msg, + Err(err) => err.to_string(), + }; + } + Ok(result) } } diff --git a/kclvm/sema/src/resolver/node.rs b/kclvm/sema/src/resolver/node.rs index ea2293585..479aafee8 100644 --- a/kclvm/sema/src/resolver/node.rs +++ b/kclvm/sema/src/resolver/node.rs @@ -978,7 +978,10 @@ impl<'ctx> MutSelfTypedResultWalker<'ctx> for Resolver<'ctx> { if let Some(stmt) = lambda_expr.body.last() { if !matches!( stmt.node, - ast::Stmt::Expr(_) | ast::Stmt::Assign(_) | ast::Stmt::AugAssign(_) + ast::Stmt::Expr(_) + | ast::Stmt::Assign(_) + | ast::Stmt::AugAssign(_) + | ast::Stmt::Assert(_) ) { self.handler.add_compile_error( "The last statement of the lambda body must be a expression e.g., x, 1, etc.", diff --git a/kclvm/tools/src/lib.rs b/kclvm/tools/src/lib.rs index fc86da6cc..703c3b406 100644 --- a/kclvm/tools/src/lib.rs +++ b/kclvm/tools/src/lib.rs @@ -1,5 +1,6 @@ pub mod fix; pub mod format; pub mod lint; +pub mod testing; pub mod util; pub mod vet; diff --git a/kclvm/tools/src/testing/mod.rs b/kclvm/tools/src/testing/mod.rs new file mode 100644 index 000000000..82e5a329e --- /dev/null +++ b/kclvm/tools/src/testing/mod.rs @@ -0,0 +1,58 @@ +//! [kclvm_tools::testing] module mainly contains some functions of language testing tool. +//! +//! The basic principle of the testing tool is to search for test files in the KCL package +//! that have the suffix "_test.k" and do not start with "_". These test files will be regard +//! as test suites. Within these files, any lambda literals starting with "test_" will be +//! considered as test cases, but these lambda functions should not have any parameters. +//! To perform the testing, the tool compiles the test suite file and its dependencies into an +//! [kclvm_runner::Artifact], which is regard as a new compilation entry point. Then, +//! it executes each test case separately and collects information about the test cases, +//! such as the execution time and whether the test passes or fails. +pub use crate::testing::suite::{load_test_suites, TestSuite}; +use anyhow::{Error, Result}; +use indexmap::IndexMap; +use kclvm_runner::ExecProgramArgs; +use std::time::Duration; + +mod suite; + +#[cfg(test)] +mod tests; + +/// Trait for running tests. +pub trait TestRun { + type Options; + type Result; + + /// Run the test with the given options and return the result. + fn run(&self, opts: &Self::Options) -> Result; +} + +/// Represents the result of a test. +#[derive(Debug, Default)] +pub struct TestResult { + /// This field stores the log message of the test. + pub log_message: String, + /// This field stores test case information in an [IndexMap], where the key is a [String] and the value is a [TestCaseInfo] struct. + pub info: IndexMap, +} + +/// Represents information about a test case. +#[derive(Debug, Default)] +pub struct TestCaseInfo { + /// This field stores the error associated with the test case, if any. + pub error: Option, + /// This field stores the duration of the test case. + pub duration: Duration, +} + +/// Represents options for running tests. +#[derive(Debug, Default, Clone)] +pub struct TestOptions { + /// This field stores the execution program arguments. + pub exec_args: ExecProgramArgs, + /// This field stores a regular expression for filtering tests to run. + pub run_regexp: String, + /// This field determines whether the test run should stop on the first failure. + pub fail_fast: bool, +} diff --git a/kclvm/tools/src/testing/suite.rs b/kclvm/tools/src/testing/suite.rs new file mode 100644 index 000000000..e763b1309 --- /dev/null +++ b/kclvm/tools/src/testing/suite.rs @@ -0,0 +1,195 @@ +use std::{fs::remove_file, path::Path}; + +use crate::testing::{TestCaseInfo, TestOptions, TestResult, TestRun}; +use anyhow::{anyhow, Result}; +use indexmap::IndexMap; +use kclvm_ast::ast; +use kclvm_driver::{get_kcl_files, get_pkg_list}; +use kclvm_parser::{parse_file, ParseSession}; +use kclvm_runner::runner::ProgramRunner; +use kclvm_runner::{build_program, ExecProgramArgs}; +use std::sync::Arc; +use std::time::Instant; + +/// File suffix for test files. +pub const TEST_FILE_SUFFIX: &str = "_test.k"; +/// Prefix for test suite names. +pub const TEST_SUITE_PREFIX: &str = "test_"; + +const TEST_MAIN_FILE: &str = "_kcl_test.k"; +const TEST_CASE_RUN_OPTION: &str = "_kcl_test_case_run"; +const TEST_MAIN_FILE_PREFIX: &str = r#" +# Auto generated by the kcl test tool; DO NOT EDIT! + +_kcl_test_case_run = option("_kcl_test_case_run", type="str", default="") + +"#; + +pub struct TestSuite { + /// Package path of the test suite. e.g. ./path/to/pkg + pub pkg: String, + /// List of normal files in the package. + pub normal_files: Vec, + /// List of normal files without the `_test.k` suffix in the package. + pub test_files: Vec, + // Map of test cases in the test suite. + pub cases: IndexMap, + // Flag indicating whether the test suite should be skipped. + pub skip: bool, +} + +impl TestRun for TestSuite { + type Options = TestOptions; + type Result = TestResult; + + /// Run the test suite with the given options and return the result. + fn run(&self, opts: &Self::Options) -> Result { + let mut result = TestResult::default(); + // Skip test suite if marked as skipped or if there are no test cases. + if self.skip || self.cases.is_empty() { + return Ok(result); + } + // Generate the test main entry file. + let main_file = self.gen_test_main_file()?; + // Set up execution arguments. + let mut args = ExecProgramArgs { + k_filename_list: self.get_input_files(&main_file), + overrides: vec![], + disable_yaml_result: true, + ..opts.exec_args.clone() + }; + // Build the program. + let artifact = build_program::(Arc::new(ParseSession::default()), &args, None)?; + // Test every case in the suite. + for (name, _) in &self.cases { + args.args = vec![ast::CmdArgSpec { + name: TEST_CASE_RUN_OPTION.into(), + value: format!("{:?}", name), + }]; + let start = Instant::now(); + let exec_result = artifact.run(&args)?; + // Check if there was an error. + let error = if exec_result.err_message.is_empty() { + None + } else { + Some(anyhow!("{}", exec_result.err_message)) + }; + // Check if the fail_fast option is enabled and there was an error. + let fail_fast = error.is_some() && opts.fail_fast; + // Add test case information to the result. + result.info.insert( + name.clone(), + TestCaseInfo { + duration: Instant::now() - start, + error, + }, + ); + // Add the log message to the result. + result.log_message += &exec_result.log_message; + if fail_fast { + break; + } + } + // Remove the temp test main file + if opts.exec_args.debug == 0 { + remove_file(main_file)?; + } + Ok(result) + } +} + +impl TestSuite { + fn gen_test_main_file(&self) -> Result { + let test_codes = self + .cases + .keys() + .map(|c| format!("if {} == '{}': {}()", TEST_CASE_RUN_OPTION, c, c)) + .collect::>(); + let code = format!("{}{}", TEST_MAIN_FILE_PREFIX, test_codes.join("\n")); + let path = Path::new(&self.pkg).join(TEST_MAIN_FILE); + let test_main_file = path + .to_str() + .ok_or(anyhow!("{} is not found", TEST_MAIN_FILE))?; + std::fs::write(test_main_file, code)?; + Ok(test_main_file.into()) + } + + fn get_input_files(&self, main_file: &str) -> Vec { + // Construct test package files. + let mut files = vec![]; + let mut normal_files = self.normal_files.clone(); + let mut test_files = self.test_files.clone(); + files.append(&mut normal_files); + files.append(&mut test_files); + files.push(main_file.into()); + files + } +} + +pub struct TestCase; + +/// Load test suite from path +pub fn load_test_suites>(path: P, opts: &TestOptions) -> Result> { + let pkg_list = get_pkg_list(&path.as_ref())?; + let mut suites = vec![]; + for pkg in &pkg_list { + let (normal_files, test_files) = get_test_files(pkg)?; + let mut cases = IndexMap::new(); + for file in &test_files { + let module = parse_file(file, None).map_err(|e| anyhow!(e))?; + for stmt in &module.body { + if let ast::Stmt::Assign(assign_stmt) = &stmt.node { + if let ast::Expr::Lambda(_lambda_expr) = &assign_stmt.value.node { + for target in &assign_stmt.targets { + let func_name = target.node.get_name(); + if is_test_suite(&func_name) && should_run(&opts.run_regexp, &func_name) + { + cases.insert(func_name.clone(), TestCase {}); + } + } + } + } + } + } + suites.push(TestSuite { + pkg: pkg.clone(), + cases, + normal_files, + test_files, + skip: false, + }); + } + Ok(suites) +} + +#[inline] +fn get_test_files>(pkg: P) -> Result<(Vec, Vec)> { + let files = get_kcl_files(pkg, false)?; + let normal_files = files + .iter() + .filter(|x| !x.starts_with("_") && !x.ends_with(TEST_FILE_SUFFIX)) + .cloned() + .collect::>(); + let test_files = files + .iter() + .filter(|x| !x.starts_with("_") && x.ends_with(TEST_FILE_SUFFIX)) + .cloned() + .collect::>(); + Ok((normal_files, test_files)) +} + +#[inline] +fn is_test_suite(name: &str) -> bool { + name.starts_with(TEST_SUITE_PREFIX) +} + +#[inline] +fn should_run(run_regexp: &str, name: &str) -> bool { + if !run_regexp.is_empty() { + regex::Regex::new(run_regexp) + .map(|re| re.is_match(name)) + .unwrap_or_default() + } else { + true + } +} diff --git a/kclvm/tools/src/testing/test_data/module/kcl.mod b/kclvm/tools/src/testing/test_data/module/kcl.mod new file mode 100644 index 000000000..35d888aa7 --- /dev/null +++ b/kclvm/tools/src/testing/test_data/module/kcl.mod @@ -0,0 +1,3 @@ +[package] +name = "test_data" + diff --git a/kclvm/tools/src/testing/test_data/module/pkg/func.k b/kclvm/tools/src/testing/test_data/module/pkg/func.k new file mode 100644 index 000000000..26df9cf5a --- /dev/null +++ b/kclvm/tools/src/testing/test_data/module/pkg/func.k @@ -0,0 +1,3 @@ +func = lambda x { + x +} diff --git a/kclvm/tools/src/testing/test_data/module/pkg/func_test.k b/kclvm/tools/src/testing/test_data/module/pkg/func_test.k new file mode 100644 index 000000000..2aadb5a3f --- /dev/null +++ b/kclvm/tools/src/testing/test_data/module/pkg/func_test.k @@ -0,0 +1,7 @@ +test_func_0 = lambda { + assert func("a") == "a" +} + +test_func_1 = lambda { + assert func("a") == "d" +} diff --git a/kclvm/tools/src/testing/tests.rs b/kclvm/tools/src/testing/tests.rs new file mode 100644 index 000000000..54d545c4b --- /dev/null +++ b/kclvm/tools/src/testing/tests.rs @@ -0,0 +1,32 @@ +use crate::testing::TestRun; + +use super::{load_test_suites, TestOptions}; +use std::path::Path; + +#[test] +fn test_load_test_suites_and_run() { + let opts = TestOptions::default(); + let suites = load_test_suites( + Path::new(".") + .join("src") + .join("testing") + .join("test_data") + .join("module") + .join("pkg") + .to_str() + .unwrap(), + &opts, + ) + .unwrap(); + assert_eq!(suites.len(), 1); + assert_eq!(suites[0].cases.len(), 2); + let test_result = suites[0].run(&opts).unwrap(); + assert_eq!(test_result.info.len(), 2); + assert!(test_result.info[0].error.is_none()); + assert!(test_result.info[1] + .error + .as_ref() + .unwrap() + .to_string() + .contains("Error"),); +}