From 9d3d5fb105e53eef4e6c5524cc74e9a75e58a4da Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Mon, 24 Nov 2025 19:52:52 -0300 Subject: [PATCH] Refactor on duplicate functions and new tests :3 --- builder/builder.v | 32 +--- config/config.v | 28 +-- lana.v | 29 +-- runner/runner.v | 31 +--- tests/build_graph_advanced_test.v | 284 ++++++++++++++++++++++++++++++ tests/config_parsing_test.v | 241 +++++++++++++++++++++++++ tests/default_config_test.v | 144 +++++++++++++++ tests/source_discovery_test.v | 199 +++++++++++++++++++++ tests/toolchain_test.v | 221 +++++++++++++++++++++++ util/util.v | 56 ++++++ 10 files changed, 1168 insertions(+), 97 deletions(-) create mode 100644 tests/build_graph_advanced_test.v create mode 100644 tests/config_parsing_test.v create mode 100644 tests/default_config_test.v create mode 100644 tests/source_discovery_test.v create mode 100644 tests/toolchain_test.v create mode 100644 util/util.v diff --git a/builder/builder.v b/builder/builder.v index 3a6482a..e2c67f6 100644 --- a/builder/builder.v +++ b/builder/builder.v @@ -5,6 +5,7 @@ import config import deps import runtime import time +import util // BuildTarget represents a build target (shared lib or tool) pub enum BuildTarget { @@ -653,7 +654,7 @@ fn auto_discover_sources(mut build_config config.BuildConfig) { // Look for sources in src/lib// lib_src_dir := os.join_path('src', 'lib', lib_config.name) if os.is_dir(lib_src_dir) { - lib_sources := find_source_files(lib_src_dir) or { []string{} } + lib_sources := util.find_source_files(lib_src_dir) or { []string{} } lib_config.sources = lib_sources if build_config.verbose && lib_sources.len > 0 { println('Auto-discovered ${lib_sources.len} source files for shared lib ${lib_config.name}') @@ -668,7 +669,7 @@ fn auto_discover_sources(mut build_config config.BuildConfig) { // Look for sources in src/tools// tool_src_dir := os.join_path('src', 'tools', tool_config.name) if os.is_dir(tool_src_dir) { - tool_sources := find_source_files(tool_src_dir) or { []string{} } + tool_sources := util.find_source_files(tool_src_dir) or { []string{} } if tool_sources.len > 0 { tool_config.sources = tool_sources } else { @@ -695,7 +696,7 @@ fn auto_discover_sources(mut build_config config.BuildConfig) { if build_config.tools.len > 0 && build_config.tools[0].sources.len == 0 { mut default_tool := &build_config.tools[0] if default_tool.name == build_config.project_name { - all_sources := find_source_files(build_config.src_dir) or { []string{} } + all_sources := util.find_source_files(build_config.src_dir) or { []string{} } if all_sources.len > 0 { default_tool.sources = all_sources if build_config.verbose { @@ -1015,31 +1016,6 @@ fn get_object_file(source_file string, object_dir string) string { return obj_file } -fn find_source_files(dir string) ![]string { - mut files := []string{} - - if !os.is_dir(dir) { - return error('Source directory does not exist: ${dir}') - } - - items := os.ls(dir) or { return error('Failed to list directory: ${dir}') } - - for item in items { - full_path := os.join_path(dir, item) - if os.is_file(full_path) { - if item.ends_with('.cpp') || item.ends_with('.cc') || item.ends_with('.cxx') { - files << full_path - } - } else if os.is_dir(full_path) { - // Recursively search subdirectories - sub_files := find_source_files(full_path)! - files << sub_files - } - } - - return files -} - fn needs_recompile(source_file string, object_file string) bool { if !os.is_file(source_file) { // source missing, signal recompile to allow upstream code to handle error diff --git a/config/config.v b/config/config.v index 407e85f..1a7a220 100644 --- a/config/config.v +++ b/config/config.v @@ -1,6 +1,7 @@ module config import os +import util // BuildDirective represents a single build directive found in source files pub struct BuildDirective { @@ -351,7 +352,7 @@ pub fn (mut build_config BuildConfig) parse_build_directives() ! { mut directives := []BuildDirective{} // Find all source files in src directory - src_files := find_source_files(build_config.src_dir) or { + src_files := util.find_source_files(build_config.src_dir) or { if build_config.verbose { println('No source files found in ${build_config.src_dir}') } @@ -1030,28 +1031,7 @@ pub fn build_compiler_command(source_file string, object_file string, build_conf return cmd } -// Utility function to find source files +// Utility function to find source files (delegates to util module for backward compatibility) pub fn find_source_files(dir string) ![]string { - mut files := []string{} - - if !os.is_dir(dir) { - return error('Source directory does not exist: ${dir}') - } - - items := os.ls(dir) or { return error('Failed to list directory: ${dir}') } - - for item in items { - full_path := os.join_path(dir, item) - if os.is_file(full_path) { - if item.ends_with('.cpp') || item.ends_with('.cc') || item.ends_with('.cxx') { - files << full_path - } - } else if os.is_dir(full_path) { - // Recursively search subdirectories - sub_files := find_source_files(full_path)! - files << sub_files - } - } - - return files + return util.find_source_files(dir) } \ No newline at end of file diff --git a/lana.v b/lana.v index 46102e3..2b051a6 100644 --- a/lana.v +++ b/lana.v @@ -7,10 +7,7 @@ import runner import initializer import deps import help - -// For runner compatibility -const bin_dir = 'bin' -const tools_dir = 'bin/tools' +import util fn main() { mut config_data := config.parse_args() or { config.default_config } @@ -38,7 +35,11 @@ fn main() { } // Find and run the main executable (first tool or project_name) - main_executable := get_main_executable(config_data) + tools := config_data.tools.map(util.ToolInfo{ + name: it.name + output_dir: it.output_dir + }) + main_executable := util.get_main_executable(config_data.project_name, config_data.bin_dir, tools) if main_executable != '' && os.is_file(main_executable) { runner.run_executable(config_data) } else { @@ -59,21 +60,3 @@ fn main() { } } } - -fn get_main_executable(build_config config.BuildConfig) string { - // First try to find a tool with the project name - for tool_config in build_config.tools { - if tool_config.name == build_config.project_name { - return os.join_path(tool_config.output_dir, tool_config.name) - } - } - - // Then try the first tool - if build_config.tools.len > 0 { - tool_config := build_config.tools[0] - return os.join_path(tool_config.output_dir, tool_config.name) - } - - // Fallback to old behavior - return os.join_path(build_config.bin_dir, build_config.project_name) -} diff --git a/runner/runner.v b/runner/runner.v index 6c443fb..f27f5e6 100644 --- a/runner/runner.v +++ b/runner/runner.v @@ -2,19 +2,24 @@ module runner import os import config +import util pub fn run_executable(build_config config.BuildConfig) { - main_executable := get_main_executable(build_config) - + tools := build_config.tools.map(util.ToolInfo{ + name: it.name + output_dir: it.output_dir + }) + main_executable := util.get_main_executable(build_config.project_name, build_config.bin_dir, tools) + if !os.is_file(main_executable) { println('Main executable not found: ${main_executable}') println('Please run "lana build" first') return } - + println('Running ${main_executable}...') res := os.execute('${main_executable}') - + if res.exit_code == 0 { println('Execution completed successfully!') if res.output.len > 0 { @@ -26,22 +31,4 @@ pub fn run_executable(build_config config.BuildConfig) { println('Output:\n${res.output}') } } -} - -fn get_main_executable(build_config config.BuildConfig) string { - // First try to find a tool with the project name - for tool_config in build_config.tools { - if tool_config.name == build_config.project_name { - return os.join_path(tool_config.output_dir, tool_config.name) - } - } - - // Then try the first tool - if build_config.tools.len > 0 { - tool_config := build_config.tools[0] - return os.join_path(tool_config.output_dir, tool_config.name) - } - - // Fallback to old behavior - return os.join_path(build_config.bin_dir, build_config.project_name) } \ No newline at end of file diff --git a/tests/build_graph_advanced_test.v b/tests/build_graph_advanced_test.v new file mode 100644 index 0000000..30d2c24 --- /dev/null +++ b/tests/build_graph_advanced_test.v @@ -0,0 +1,284 @@ +module tests + +import os +import builder +import config + +fn test_build_graph_handles_empty_config() { + cfg := config.BuildConfig{ + project_name: 'empty' + shared_libs: [] + tools: [] + } + + summary := builder.preview_build_graph(&cfg) or { panic(err) } + + assert summary.nodes.len == 0 + assert summary.order.len == 0 +} + +fn test_build_graph_handles_multiple_dependencies() { + tmp := new_temp_dir('lana_multi_deps') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + lib_dir := os.join_path(src_dir, 'lib') + tool_dir := os.join_path(src_dir, 'tools') + os.mkdir_all(lib_dir) or { panic(err) } + os.mkdir_all(tool_dir) or { panic(err) } + + // Create source files + os.write_file(os.join_path(lib_dir, 'base.cpp'), '// base') or { panic(err) } + os.write_file(os.join_path(lib_dir, 'utils.cpp'), '// utils') or { panic(err) } + os.write_file(os.join_path(lib_dir, 'core.cpp'), '// core') or { panic(err) } + os.write_file(os.join_path(tool_dir, 'app.cpp'), '// app') or { panic(err) } + + // core depends on base and utils + // app depends on core + cfg := config.BuildConfig{ + project_name: 'multi_deps' + src_dir: src_dir + shared_libs: [ + config.SharedLibConfig{ + name: 'base' + sources: [os.join_path(lib_dir, 'base.cpp')] + }, + config.SharedLibConfig{ + name: 'utils' + sources: [os.join_path(lib_dir, 'utils.cpp')] + }, + config.SharedLibConfig{ + name: 'core' + sources: [os.join_path(lib_dir, 'core.cpp')] + libraries: ['base', 'utils'] + }, + ] + tools: [ + config.ToolConfig{ + name: 'app' + sources: [os.join_path(tool_dir, 'app.cpp')] + libraries: ['core'] + }, + ] + } + + summary := builder.preview_build_graph(&cfg) or { panic(err) } + + // Should have 4 nodes + assert summary.nodes.len == 4 + + // Find app in order - it should come after core + mut core_idx := -1 + mut app_idx := -1 + for idx, id in summary.order { + if id == 'shared:core' { + core_idx = idx + } + if id == 'tool:app' { + app_idx = idx + } + } + + assert core_idx >= 0 + assert app_idx >= 0 + assert app_idx > core_idx + + // base and utils should come before core + mut base_idx := -1 + mut utils_idx := -1 + for idx, id in summary.order { + if id == 'shared:base' { + base_idx = idx + } + if id == 'shared:utils' { + utils_idx = idx + } + } + + assert base_idx < core_idx + assert utils_idx < core_idx +} + +fn test_build_graph_detects_unresolved_dependencies() { + tmp := new_temp_dir('lana_unresolved') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + os.mkdir_all(src_dir) or { panic(err) } + os.write_file(os.join_path(src_dir, 'main.cpp'), '// main') or { panic(err) } + + cfg := config.BuildConfig{ + project_name: 'unresolved' + src_dir: src_dir + tools: [ + config.ToolConfig{ + name: 'app' + sources: [os.join_path(src_dir, 'main.cpp')] + libraries: ['nonexistent_lib'] + }, + ] + } + + summary := builder.preview_build_graph(&cfg) or { panic(err) } + + // Should have 1 node with unresolved dependency + assert summary.nodes.len == 1 + assert 'tool:app' in summary.unresolved + assert summary.unresolved['tool:app'].contains('nonexistent_lib') +} + +fn test_build_graph_skips_empty_sources() { + cfg := config.BuildConfig{ + project_name: 'empty_sources' + debug: true + verbose: true + shared_libs: [ + config.SharedLibConfig{ + name: 'empty_lib' + sources: [] + debug: true + }, + ] + tools: [ + config.ToolConfig{ + name: 'empty_tool' + sources: [] + debug: true + }, + ] + } + + summary := builder.preview_build_graph(&cfg) or { panic(err) } + + // Empty sources should be skipped + assert summary.nodes.len == 0 +} + +fn test_build_graph_resolves_lib_prefix_aliases() { + tmp := new_temp_dir('lana_aliases') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + lib_dir := os.join_path(src_dir, 'lib') + tool_dir := os.join_path(src_dir, 'tools') + os.mkdir_all(lib_dir) or { panic(err) } + os.mkdir_all(tool_dir) or { panic(err) } + + os.write_file(os.join_path(lib_dir, 'mylib.cpp'), '// lib') or { panic(err) } + os.write_file(os.join_path(tool_dir, 'app.cpp'), '// app') or { panic(err) } + + // Reference library with lib/ prefix + cfg := config.BuildConfig{ + project_name: 'aliases' + src_dir: src_dir + shared_libs: [ + config.SharedLibConfig{ + name: 'mylib' + sources: [os.join_path(lib_dir, 'mylib.cpp')] + }, + ] + tools: [ + config.ToolConfig{ + name: 'app' + sources: [os.join_path(tool_dir, 'app.cpp')] + libraries: ['lib/mylib'] + }, + ] + } + + summary := builder.preview_build_graph(&cfg) or { panic(err) } + + // Dependency should be resolved + assert summary.nodes.len == 2 + assert summary.unresolved.len == 0 + + // Find app node and check its dependencies + for node in summary.nodes { + if node.id == 'tool:app' { + assert node.dependencies.contains('shared:mylib') + } + } +} + +fn test_build_graph_resolves_so_extension_aliases() { + tmp := new_temp_dir('lana_so_aliases') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + lib_dir := os.join_path(src_dir, 'lib') + tool_dir := os.join_path(src_dir, 'tools') + os.mkdir_all(lib_dir) or { panic(err) } + os.mkdir_all(tool_dir) or { panic(err) } + + os.write_file(os.join_path(lib_dir, 'core.cpp'), '// lib') or { panic(err) } + os.write_file(os.join_path(tool_dir, 'app.cpp'), '// app') or { panic(err) } + + // Reference library with .so extension + cfg := config.BuildConfig{ + project_name: 'so_aliases' + src_dir: src_dir + shared_libs: [ + config.SharedLibConfig{ + name: 'core' + sources: [os.join_path(lib_dir, 'core.cpp')] + }, + ] + tools: [ + config.ToolConfig{ + name: 'app' + sources: [os.join_path(tool_dir, 'app.cpp')] + libraries: ['core.so'] + }, + ] + } + + summary := builder.preview_build_graph(&cfg) or { panic(err) } + + // Dependency should be resolved + assert summary.unresolved.len == 0 + + for node in summary.nodes { + if node.id == 'tool:app' { + assert node.dependencies.contains('shared:core') + } + } +} + +fn test_build_graph_includes_directives() { + tmp := new_temp_dir('lana_directives_graph') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + tool_dir := os.join_path(src_dir, 'tools') + os.mkdir_all(tool_dir) or { panic(err) } + + os.write_file(os.join_path(tool_dir, 'custom.cpp'), '// custom') or { panic(err) } + + cfg := config.BuildConfig{ + project_name: 'directives' + src_dir: src_dir + build_directives: [ + config.BuildDirective{ + unit_name: 'tools/custom' + output_path: 'tools/custom' + is_shared: false + }, + ] + } + + summary := builder.preview_build_graph(&cfg) or { panic(err) } + + assert summary.nodes.len == 1 + assert summary.nodes[0].id == 'directive:tools/custom' + assert summary.nodes[0].is_directive == true +} diff --git a/tests/config_parsing_test.v b/tests/config_parsing_test.v new file mode 100644 index 0000000..a104d72 --- /dev/null +++ b/tests/config_parsing_test.v @@ -0,0 +1,241 @@ +module tests + +import os +import config + +fn test_parse_bool_values_true() { + tmp := new_temp_dir('lana_bool') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = test\ndebug = true\noptimize = yes\nverbose = 1\nparallel_compilation = on\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.debug == true + assert cfg.optimize == true + assert cfg.verbose == true + assert cfg.parallel_compilation == true +} + +fn test_parse_bool_values_false() { + tmp := new_temp_dir('lana_bool_false') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = test\ndebug = false\noptimize = no\nverbose = 0\nparallel_compilation = off\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.debug == false + assert cfg.optimize == false + assert cfg.verbose == false + assert cfg.parallel_compilation == false +} + +fn test_parse_comma_separated_lists() { + tmp := new_temp_dir('lana_lists') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = test\ninclude_dirs = include, src/include, deps/include\nlibraries = pthread, m, dl\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.include_dirs.len == 3 + assert cfg.include_dirs.contains('include') + assert cfg.include_dirs.contains('src/include') + assert cfg.include_dirs.contains('deps/include') + + assert cfg.libraries.len == 3 + assert cfg.libraries.contains('pthread') + assert cfg.libraries.contains('m') + assert cfg.libraries.contains('dl') +} + +fn test_parse_space_separated_cflags() { + tmp := new_temp_dir('lana_cflags') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = test\ncflags = -Wall -Wextra -Werror -pedantic\nldflags = -pthread -lm\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.cflags.contains('-Wall') + assert cfg.cflags.contains('-Wextra') + assert cfg.cflags.contains('-Werror') + assert cfg.cflags.contains('-pedantic') + + assert cfg.ldflags.contains('-pthread') + assert cfg.ldflags.contains('-lm') +} + +fn test_parse_multiple_shared_libs() { + tmp := new_temp_dir('lana_multi_libs') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = test\n\n[shared_libs]\nname = core\nsources = src/lib/core.cpp\n\n[shared_libs]\nname = utils\nsources = src/lib/utils.cpp\nlibraries = core\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.shared_libs.len == 2 + assert cfg.shared_libs[0].name == 'core' + assert cfg.shared_libs[1].name == 'utils' + assert cfg.shared_libs[1].libraries.contains('core') +} + +fn test_parse_multiple_tools() { + tmp := new_temp_dir('lana_multi_tools') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = test\n\n[tools]\nname = cli\nsources = src/tools/cli.cpp\n\n[tools]\nname = server\nsources = src/tools/server.cpp\nlibraries = core\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.tools.len == 2 + assert cfg.tools[0].name == 'cli' + assert cfg.tools[1].name == 'server' + assert cfg.tools[1].libraries.contains('core') +} + +fn test_parse_dependencies_section() { + tmp := new_temp_dir('lana_deps_parse') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = test\ndependencies_dir = external\n\n[dependencies]\nname = json\nurl = https://example.com/json.tar.gz\narchive = json.tar.gz\nextract_to = json\nchecksum = abc123\nbuild_cmds = mkdir build; cd build; cmake ..\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.dependencies_dir == 'external' + assert cfg.dependencies.len == 1 + assert cfg.dependencies[0].name == 'json' + assert cfg.dependencies[0].url == 'https://example.com/json.tar.gz' + assert cfg.dependencies[0].archive == 'json.tar.gz' + assert cfg.dependencies[0].extract_to == 'json' + assert cfg.dependencies[0].checksum == 'abc123' + // "mkdir build; cd build; cmake .." splits into 3 commands + assert cfg.dependencies[0].build_cmds.len == 3 + assert cfg.dependencies[0].build_cmds[0] == 'mkdir build' + assert cfg.dependencies[0].build_cmds[1] == 'cd build' + assert cfg.dependencies[0].build_cmds[2] == 'cmake ..' +} + +fn test_config_inherits_global_values() { + tmp := new_temp_dir('lana_inherit') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = test\ndebug = true\noptimize = false\ninclude_dirs = global/include\ncflags = -DGLOBAL\n\n[shared_libs]\nname = mylib\nsources = src/lib.cpp\n\n[tools]\nname = mytool\nsources = src/main.cpp\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + // Shared lib inherits global values + assert cfg.shared_libs[0].debug == true + assert cfg.shared_libs[0].optimize == false + assert cfg.shared_libs[0].include_dirs.contains('global/include') + assert cfg.shared_libs[0].cflags.contains('-DGLOBAL') + + // Tool inherits global values + assert cfg.tools[0].debug == true + assert cfg.tools[0].optimize == false + assert cfg.tools[0].include_dirs.contains('global/include') + assert cfg.tools[0].cflags.contains('-DGLOBAL') +} + +fn test_target_can_override_global_values() { + tmp := new_temp_dir('lana_override') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = test\ndebug = true\noptimize = false\n\n[tools]\nname = release_tool\nsources = src/main.cpp\ndebug = false\noptimize = true\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + // Tool overrides global debug/optimize + assert cfg.tools[0].debug == false + assert cfg.tools[0].optimize == true +} + +fn test_parse_config_file_missing_file_returns_error() { + if _ := config.parse_config_file('/nonexistent/config.ini') { + assert false, 'Expected error for nonexistent file' + } + // If we get here without the or block catching an error, the test passes +} + +fn test_comments_are_ignored() { + tmp := new_temp_dir('lana_comments') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '# This is a comment\n[global]\n# Another comment\nproject_name = test\n# Comment at end\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.project_name == 'test' +} + +fn test_empty_lines_are_ignored() { + tmp := new_temp_dir('lana_empty') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '\n\n[global]\n\nproject_name = test\n\n\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.project_name == 'test' +} + +fn test_quoted_values_are_trimmed() { + tmp := new_temp_dir('lana_quoted') + defer { + os.rmdir_all(tmp) or {} + } + + config_path := os.join_path(tmp, 'config.ini') + content := '[global]\nproject_name = "my_project"\ncompiler = \'clang++\'\n' + os.write_file(config_path, content) or { panic(err) } + + cfg := config.parse_config_file(config_path) or { panic(err) } + + assert cfg.project_name == 'my_project' + assert cfg.compiler == 'clang++' +} diff --git a/tests/default_config_test.v b/tests/default_config_test.v new file mode 100644 index 0000000..f9abd48 --- /dev/null +++ b/tests/default_config_test.v @@ -0,0 +1,144 @@ +module tests + +import config + +fn test_default_config_has_expected_values() { + cfg := config.default_config + + assert cfg.src_dir == 'src' + assert cfg.build_dir == 'build' + assert cfg.bin_dir == 'bin' + assert cfg.toolchain == 'gcc' + assert cfg.debug == true + assert cfg.optimize == false + assert cfg.verbose == false + assert cfg.shared_libs.len == 0 + assert cfg.tools.len == 0 + assert cfg.dependencies.len == 0 +} + +fn test_shared_lib_config_has_default_output_dir() { + lib := config.SharedLibConfig{ + name: 'test' + } + + assert lib.output_dir == 'bin/lib' +} + +fn test_tool_config_has_default_output_dir() { + tool := config.ToolConfig{ + name: 'test' + } + + assert tool.output_dir == 'bin/tools' +} + +fn test_build_config_has_default_dependencies_dir() { + cfg := config.BuildConfig{} + + assert cfg.dependencies_dir == 'dependencies' +} + +fn test_build_config_has_parallel_compilation_enabled_by_default() { + cfg := config.BuildConfig{} + + assert cfg.parallel_compilation == true +} + +fn test_build_config_has_default_compiler() { + cfg := config.BuildConfig{} + + assert cfg.compiler == 'g++' +} + +fn test_get_target_config_values_for_shared_lib() { + lib := config.SharedLibConfig{ + name: 'test' + debug: true + optimize: false + verbose: true + include_dirs: ['include'] + cflags: ['-DTEST'] + } + + target := config.TargetConfig(lib) + is_shared, use_debug, use_optimize, use_verbose, includes, cflags := config.get_target_config_values(target) + + assert is_shared == true + assert use_debug == true + assert use_optimize == false + assert use_verbose == true + assert includes.contains('include') + assert cflags.contains('-DTEST') +} + +fn test_get_target_config_values_for_tool() { + tool := config.ToolConfig{ + name: 'test' + debug: false + optimize: true + verbose: false + include_dirs: ['src'] + cflags: ['-O3'] + } + + target := config.TargetConfig(tool) + is_shared, use_debug, use_optimize, use_verbose, includes, cflags := config.get_target_config_values(target) + + assert is_shared == false + assert use_debug == false + assert use_optimize == true + assert use_verbose == false + assert includes.contains('src') + assert cflags.contains('-O3') +} + +fn test_build_compiler_command_basic() { + cfg := config.BuildConfig{ + compiler: 'g++' + debug: true + include_dirs: ['include'] + cflags: ['-DTEST'] + } + + cmd := config.build_compiler_command('src/main.cpp', 'build/main.o', cfg) + + assert cmd.starts_with('g++') + assert cmd.contains('-c') + assert cmd.contains('-Iinclude') + assert cmd.contains('-g') + assert cmd.contains('-O0') + assert cmd.contains('-Wall') + assert cmd.contains('-Wextra') + assert cmd.contains('-DTEST') + assert cmd.contains('src/main.cpp') + assert cmd.contains('-o build/main.o') +} + +fn test_build_compiler_command_optimized() { + cfg := config.BuildConfig{ + compiler: 'clang++' + debug: false + optimize: true + } + + cmd := config.build_compiler_command('main.cpp', 'main.o', cfg) + + assert cmd.starts_with('clang++') + assert cmd.contains('-O3') + assert !cmd.contains('-g') +} + +fn test_build_compiler_command_default_optimization() { + cfg := config.BuildConfig{ + compiler: 'g++' + debug: false + optimize: false + } + + cmd := config.build_compiler_command('main.cpp', 'main.o', cfg) + + assert cmd.contains('-O2') + assert !cmd.contains('-O3') + assert !cmd.contains('-g') +} diff --git a/tests/source_discovery_test.v b/tests/source_discovery_test.v new file mode 100644 index 0000000..9d38179 --- /dev/null +++ b/tests/source_discovery_test.v @@ -0,0 +1,199 @@ +module tests + +import os +import config + +fn test_find_source_files_finds_cpp_files() { + tmp := new_temp_dir('lana_find_cpp') + defer { + os.rmdir_all(tmp) or {} + } + + os.write_file(os.join_path(tmp, 'main.cpp'), '// cpp') or { panic(err) } + os.write_file(os.join_path(tmp, 'utils.cpp'), '// cpp') or { panic(err) } + os.write_file(os.join_path(tmp, 'header.h'), '// header') or { panic(err) } + os.write_file(os.join_path(tmp, 'readme.txt'), '// text') or { panic(err) } + + files := config.find_source_files(tmp) or { panic(err) } + + assert files.len == 2 + assert files.any(it.ends_with('main.cpp')) + assert files.any(it.ends_with('utils.cpp')) +} + +fn test_find_source_files_finds_cc_files() { + tmp := new_temp_dir('lana_find_cc') + defer { + os.rmdir_all(tmp) or {} + } + + os.write_file(os.join_path(tmp, 'main.cc'), '// cc') or { panic(err) } + + files := config.find_source_files(tmp) or { panic(err) } + + assert files.len == 1 + assert files[0].ends_with('main.cc') +} + +fn test_find_source_files_finds_cxx_files() { + tmp := new_temp_dir('lana_find_cxx') + defer { + os.rmdir_all(tmp) or {} + } + + os.write_file(os.join_path(tmp, 'main.cxx'), '// cxx') or { panic(err) } + + files := config.find_source_files(tmp) or { panic(err) } + + assert files.len == 1 + assert files[0].ends_with('main.cxx') +} + +fn test_find_source_files_searches_subdirectories() { + tmp := new_temp_dir('lana_find_subdir') + defer { + os.rmdir_all(tmp) or {} + } + + subdir := os.join_path(tmp, 'lib', 'core') + os.mkdir_all(subdir) or { panic(err) } + + os.write_file(os.join_path(tmp, 'main.cpp'), '// root') or { panic(err) } + os.write_file(os.join_path(subdir, 'core.cpp'), '// subdir') or { panic(err) } + + files := config.find_source_files(tmp) or { panic(err) } + + assert files.len == 2 + assert files.any(it.ends_with('main.cpp')) + assert files.any(it.ends_with('core.cpp')) +} + +fn test_find_source_files_returns_error_for_nonexistent_dir() { + if _ := config.find_source_files('/nonexistent/directory') { + assert false, 'Expected error for nonexistent directory' + } + // If we get here without the or block catching an error, the test passes +} + +fn test_find_source_files_returns_empty_for_empty_dir() { + tmp := new_temp_dir('lana_find_empty') + defer { + os.rmdir_all(tmp) or {} + } + + files := config.find_source_files(tmp) or { panic(err) } + + assert files.len == 0 +} + +fn test_find_source_files_ignores_hidden_directories() { + tmp := new_temp_dir('lana_find_hidden') + defer { + os.rmdir_all(tmp) or {} + } + + hidden_dir := os.join_path(tmp, '.hidden') + os.mkdir_all(hidden_dir) or { panic(err) } + + os.write_file(os.join_path(tmp, 'visible.cpp'), '// visible') or { panic(err) } + os.write_file(os.join_path(hidden_dir, 'hidden.cpp'), '// hidden') or { panic(err) } + + files := config.find_source_files(tmp) or { panic(err) } + + // Note: The current implementation doesn't filter hidden dirs, + // so this test documents current behavior + assert files.any(it.ends_with('visible.cpp')) +} + +fn test_build_directives_parse_all_directive_types() { + tmp := new_temp_dir('lana_all_directives') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + os.mkdir_all(src_dir) or { panic(err) } + + source := '// build-directive: unit-name(myunit)\n// build-directive: depends-units(dep1, dep2)\n// build-directive: link(lib1.so, lib2.so)\n// build-directive: out(bin/myunit)\n// build-directive: cflags(-O2 -DNDEBUG)\n// build-directive: ldflags(-lpthread -lm)\n// build-directive: shared(true)\nint main() { return 0; }\n' + os.write_file(os.join_path(src_dir, 'myunit.cpp'), source) or { panic(err) } + + mut cfg := config.BuildConfig{ + src_dir: src_dir + } + + cfg.parse_build_directives() or { panic(err) } + + assert cfg.build_directives.len == 1 + + d := cfg.build_directives[0] + assert d.unit_name == 'myunit' + assert d.depends_units.len == 2 + assert d.link_libs.len == 2 + assert d.output_path == 'bin/myunit' + assert d.cflags.len == 2 + assert d.ldflags.len == 2 + assert d.is_shared == true +} + +fn test_build_directives_ignore_non_directive_comments() { + tmp := new_temp_dir('lana_ignore_comments') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + os.mkdir_all(src_dir) or { panic(err) } + + source := '// This is a regular comment\n// Another comment\n// build-directive: unit-name(test)\n/* Block comment */\nint main() { return 0; }\n' + os.write_file(os.join_path(src_dir, 'test.cpp'), source) or { panic(err) } + + mut cfg := config.BuildConfig{ + src_dir: src_dir + } + + cfg.parse_build_directives() or { panic(err) } + + assert cfg.build_directives.len == 1 + assert cfg.build_directives[0].unit_name == 'test' +} + +fn test_build_directives_handles_empty_src_dir() { + tmp := new_temp_dir('lana_empty_src') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + os.mkdir_all(src_dir) or { panic(err) } + + mut cfg := config.BuildConfig{ + src_dir: src_dir + } + + cfg.parse_build_directives() or { panic(err) } + + assert cfg.build_directives.len == 0 +} + +fn test_build_directives_skips_files_without_unit_name() { + tmp := new_temp_dir('lana_no_unit') + defer { + os.rmdir_all(tmp) or {} + } + + src_dir := os.join_path(tmp, 'src') + os.mkdir_all(src_dir) or { panic(err) } + + // File with directives but no unit-name + source := '// build-directive: cflags(-O2)\n// build-directive: ldflags(-lm)\nint main() { return 0; }\n' + os.write_file(os.join_path(src_dir, 'nounit.cpp'), source) or { panic(err) } + + mut cfg := config.BuildConfig{ + src_dir: src_dir + } + + cfg.parse_build_directives() or { panic(err) } + + // Should not be added without unit-name + assert cfg.build_directives.len == 0 +} diff --git a/tests/toolchain_test.v b/tests/toolchain_test.v new file mode 100644 index 0000000..455d6e0 --- /dev/null +++ b/tests/toolchain_test.v @@ -0,0 +1,221 @@ +module tests + +import config + +fn test_gcc_toolchain_compile_command_includes_debug_flags() { + cfg := config.BuildConfig{ + debug: true + optimize: false + compiler: 'g++' + toolchain: 'gcc' + include_dirs: ['include'] + cflags: ['-DTEST'] + } + tc := config.get_toolchain(cfg) + + target := config.TargetConfig(config.ToolConfig{ + name: 'test_tool' + debug: true + }) + + cmd := tc.compile_command('test.cpp', 'test.o', &cfg, target) + + assert cmd.contains('-g') + assert cmd.contains('-O0') + assert cmd.contains('-Iinclude') + assert cmd.contains('-DTEST') + assert cmd.contains('-Wall') + assert cmd.contains('-Wextra') + assert cmd.contains('test.cpp') + assert cmd.contains('-o test.o') +} + +fn test_gcc_toolchain_compile_command_includes_optimize_flags() { + cfg := config.BuildConfig{ + debug: false + optimize: true + compiler: 'g++' + toolchain: 'gcc' + } + tc := config.get_toolchain(cfg) + + target := config.TargetConfig(config.ToolConfig{ + name: 'test_tool' + optimize: true + }) + + cmd := tc.compile_command('src/main.cpp', 'build/main.o', &cfg, target) + + assert cmd.contains('-O3') + assert !cmd.contains('-g') + assert !cmd.contains('-O0') +} + +fn test_clang_toolchain_compile_command() { + cfg := config.BuildConfig{ + debug: true + compiler: 'clang++' + toolchain: 'clang' + } + tc := config.get_toolchain(cfg) + + target := config.TargetConfig(config.ToolConfig{ + name: 'test_tool' + debug: true + }) + + cmd := tc.compile_command('main.cpp', 'main.o', &cfg, target) + + assert cmd.starts_with('clang++') + assert cmd.contains('-c') + assert cmd.contains('-g') +} + +fn test_shared_lib_compile_includes_fpic() { + cfg := config.BuildConfig{ + debug: false + compiler: 'g++' + toolchain: 'gcc' + } + tc := config.get_toolchain(cfg) + + target := config.TargetConfig(config.SharedLibConfig{ + name: 'mylib' + }) + + cmd := tc.compile_command('lib.cpp', 'lib.o', &cfg, target) + + assert cmd.contains('-fPIC') +} + +fn test_tool_compile_does_not_include_fpic() { + cfg := config.BuildConfig{ + debug: false + compiler: 'g++' + toolchain: 'gcc' + } + tc := config.get_toolchain(cfg) + + target := config.TargetConfig(config.ToolConfig{ + name: 'mytool' + }) + + cmd := tc.compile_command('main.cpp', 'main.o', &cfg, target) + + assert !cmd.contains('-fPIC') +} + +fn test_shared_link_command_includes_shared_flag() { + cfg := config.BuildConfig{ + compiler: 'g++' + toolchain: 'gcc' + bin_dir: 'bin' + } + tc := config.get_toolchain(cfg) + + lib_cfg := config.SharedLibConfig{ + name: 'mylib' + libraries: ['dep'] + } + + cmd := tc.shared_link_command(['obj1.o', 'obj2.o'], 'mylib', 'bin/lib', &cfg, lib_cfg) + + assert cmd.contains('-shared') + assert cmd.contains('obj1.o') + assert cmd.contains('obj2.o') + assert cmd.contains('-o bin/lib/mylib.so') +} + +fn test_tool_link_command_links_libraries() { + cfg := config.BuildConfig{ + compiler: 'g++' + toolchain: 'gcc' + bin_dir: 'bin' + libraries: ['pthread'] + } + tc := config.get_toolchain(cfg) + + tool_cfg := config.ToolConfig{ + name: 'mytool' + libraries: ['core'] + } + + cmd := tc.tool_link_command(['main.o'], 'bin/tools/mytool', &cfg, tool_cfg) + + assert cmd.contains('main.o') + assert cmd.contains('-lpthread') + assert cmd.contains('-o bin/tools/mytool') +} + +fn test_get_toolchain_defaults_to_gcc() { + cfg := config.BuildConfig{ + toolchain: '' + compiler: '' + } + tc := config.get_toolchain(cfg) + + assert tc.description() == 'gcc' +} + +fn test_get_toolchain_returns_clang_when_specified() { + cfg := config.BuildConfig{ + toolchain: 'clang' + compiler: 'clang++' + } + tc := config.get_toolchain(cfg) + + assert tc.description() == 'clang' +} + +fn test_compile_command_includes_lib_search_paths() { + cfg := config.BuildConfig{ + compiler: 'g++' + toolchain: 'gcc' + lib_search_paths: ['/usr/local/lib', 'deps/lib'] + } + tc := config.get_toolchain(cfg) + + target := config.TargetConfig(config.ToolConfig{name: 'test'}) + cmd := tc.compile_command('test.cpp', 'test.o', &cfg, target) + + assert cmd.contains('-L/usr/local/lib') + assert cmd.contains('-Ldeps/lib') +} + +fn test_target_specific_include_dirs_added() { + cfg := config.BuildConfig{ + compiler: 'g++' + toolchain: 'gcc' + include_dirs: ['global/include'] + } + tc := config.get_toolchain(cfg) + + target := config.TargetConfig(config.ToolConfig{ + name: 'test' + include_dirs: ['tool/include'] + }) + + cmd := tc.compile_command('test.cpp', 'test.o', &cfg, target) + + assert cmd.contains('-Iglobal/include') + assert cmd.contains('-Itool/include') +} + +fn test_target_specific_cflags_added() { + cfg := config.BuildConfig{ + compiler: 'g++' + toolchain: 'gcc' + cflags: ['-DGLOBAL'] + } + tc := config.get_toolchain(cfg) + + target := config.TargetConfig(config.ToolConfig{ + name: 'test' + cflags: ['-DLOCAL'] + }) + + cmd := tc.compile_command('test.cpp', 'test.o', &cfg, target) + + assert cmd.contains('-DGLOBAL') + assert cmd.contains('-DLOCAL') +} diff --git a/util/util.v b/util/util.v new file mode 100644 index 0000000..199a5d6 --- /dev/null +++ b/util/util.v @@ -0,0 +1,56 @@ +module util + +import os + +// shitty thing I need to pass around or else the compiler shits itself +pub struct ToolInfo { +pub: + name string + output_dir string +} + +// get_main_executable finds the main executable path given project info and tools list. +// It first looks for a tool matching the project name, then falls back to the +// first tool, and finally the legacy bin/ location. +pub fn get_main_executable(project_name string, bin_dir string, tools []ToolInfo) string { + // First try to find a tool with the project name + for tool in tools { + if tool.name == project_name { + return os.join_path(tool.output_dir, tool.name) + } + } + + // Then try the first tool + if tools.len > 0 { + return os.join_path(tools[0].output_dir, tools[0].name) + } + + // Fallback to old behavior + return os.join_path(bin_dir, project_name) +} + +// find_source_files recursively finds all C++ source files (.cpp, .cc, .cxx) in a directory. +pub fn find_source_files(dir string) ![]string { + mut files := []string{} + + if !os.is_dir(dir) { + return error('Source directory does not exist: ${dir}') + } + + items := os.ls(dir) or { return error('Failed to list directory: ${dir}') } + + for item in items { + full_path := os.join_path(dir, item) + if os.is_file(full_path) { + if item.ends_with('.cpp') || item.ends_with('.cc') || item.ends_with('.cxx') { + files << full_path + } + } else if os.is_dir(full_path) { + // Recursively search subdirectories + sub_files := find_source_files(full_path)! + files << sub_files + } + } + + return files +}