Refactor on duplicate functions and new tests :3

main
Joca 2025-11-24 19:52:52 -03:00
parent 63fa9ef0b7
commit 9d3d5fb105
Signed by: jocadbz
GPG Key ID: B1836DCE2F50BDF7
10 changed files with 1168 additions and 97 deletions

View File

@ -5,6 +5,7 @@ import config
import deps import deps
import runtime import runtime
import time import time
import util
// BuildTarget represents a build target (shared lib or tool) // BuildTarget represents a build target (shared lib or tool)
pub enum BuildTarget { pub enum BuildTarget {
@ -653,7 +654,7 @@ fn auto_discover_sources(mut build_config config.BuildConfig) {
// Look for sources in src/lib/<lib_name>/ // Look for sources in src/lib/<lib_name>/
lib_src_dir := os.join_path('src', 'lib', lib_config.name) lib_src_dir := os.join_path('src', 'lib', lib_config.name)
if os.is_dir(lib_src_dir) { 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 lib_config.sources = lib_sources
if build_config.verbose && lib_sources.len > 0 { if build_config.verbose && lib_sources.len > 0 {
println('Auto-discovered ${lib_sources.len} source files for shared lib ${lib_config.name}') 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_name>/ // Look for sources in src/tools/<tool_name>/
tool_src_dir := os.join_path('src', 'tools', tool_config.name) tool_src_dir := os.join_path('src', 'tools', tool_config.name)
if os.is_dir(tool_src_dir) { 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 { if tool_sources.len > 0 {
tool_config.sources = tool_sources tool_config.sources = tool_sources
} else { } 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 { if build_config.tools.len > 0 && build_config.tools[0].sources.len == 0 {
mut default_tool := &build_config.tools[0] mut default_tool := &build_config.tools[0]
if default_tool.name == build_config.project_name { 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 { if all_sources.len > 0 {
default_tool.sources = all_sources default_tool.sources = all_sources
if build_config.verbose { if build_config.verbose {
@ -1015,31 +1016,6 @@ fn get_object_file(source_file string, object_dir string) string {
return obj_file 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 { fn needs_recompile(source_file string, object_file string) bool {
if !os.is_file(source_file) { if !os.is_file(source_file) {
// source missing, signal recompile to allow upstream code to handle error // source missing, signal recompile to allow upstream code to handle error

View File

@ -1,6 +1,7 @@
module config module config
import os import os
import util
// BuildDirective represents a single build directive found in source files // BuildDirective represents a single build directive found in source files
pub struct BuildDirective { pub struct BuildDirective {
@ -351,7 +352,7 @@ pub fn (mut build_config BuildConfig) parse_build_directives() ! {
mut directives := []BuildDirective{} mut directives := []BuildDirective{}
// Find all source files in src directory // 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 { if build_config.verbose {
println('No source files found in ${build_config.src_dir}') 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 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 { pub fn find_source_files(dir string) ![]string {
mut files := []string{} return util.find_source_files(dir)
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
} }

29
lana.v
View File

@ -7,10 +7,7 @@ import runner
import initializer import initializer
import deps import deps
import help import help
import util
// For runner compatibility
const bin_dir = 'bin'
const tools_dir = 'bin/tools'
fn main() { fn main() {
mut config_data := config.parse_args() or { config.default_config } 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) // 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) { if main_executable != '' && os.is_file(main_executable) {
runner.run_executable(config_data) runner.run_executable(config_data)
} else { } 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)
}

View File

@ -2,19 +2,24 @@ module runner
import os import os
import config import config
import util
pub fn run_executable(build_config config.BuildConfig) { 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) { if !os.is_file(main_executable) {
println('Main executable not found: ${main_executable}') println('Main executable not found: ${main_executable}')
println('Please run "lana build" first') println('Please run "lana build" first')
return return
} }
println('Running ${main_executable}...') println('Running ${main_executable}...')
res := os.execute('${main_executable}') res := os.execute('${main_executable}')
if res.exit_code == 0 { if res.exit_code == 0 {
println('Execution completed successfully!') println('Execution completed successfully!')
if res.output.len > 0 { if res.output.len > 0 {
@ -26,22 +31,4 @@ pub fn run_executable(build_config config.BuildConfig) {
println('Output:\n${res.output}') 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)
} }

View File

@ -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
}

241
tests/config_parsing_test.v Normal file
View File

@ -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++'
}

144
tests/default_config_test.v Normal file
View File

@ -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')
}

View File

@ -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
}

221
tests/toolchain_test.v Normal file
View File

@ -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')
}

56
util/util.v Normal file
View File

@ -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/<project_name> 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
}