Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ members = [

"./examples/mobc",
"./examples/subobc",

"./component_driver",
]

[workspace.dependencies]
Expand Down
8 changes: 8 additions & 0 deletions component_driver/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "c2a-core-component-driver"
description = "C2A Component Driver unit tests"
Comment on lines +2 to +3
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions testing with cargo test -p c2a-core-component-driver, but the actual package name in Cargo.toml uses underscores in "component_driver" while the package name uses hyphens "component-driver".

This inconsistency should be verified - Cargo normalizes package names (hyphens in package names, underscores in code), but it's worth confirming the command in the PR description actually works as stated.

Copilot uses AI. Check for mistakes.
version.workspace = true
edition = "2021"
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Cargo.toml is missing the build key to specify the build script. Without this, Cargo may not execute build.rs automatically.

Add the following to the [package] section:

build = "build.rs"

This ensures that the CMake build and test generation happens during the cargo build process.

Suggested change
edition = "2021"
edition = "2021"
build = "build.rs"

Copilot uses AI. Check for mistakes.

[lib]
path = "src/lib.rs"
130 changes: 130 additions & 0 deletions component_driver/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;

/// CamelCase を snake_case に変換
fn camel_to_snake(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
} else {
result.push(c);
}
}
Comment on lines +8 to +17
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The camel_to_snake conversion function has a potential issue: it doesn't handle consecutive uppercase letters correctly. For example, "HTTPServer" would become "h_t_t_p_server" instead of the more idiomatic "http_server".

Consider using a more robust conversion that handles acronyms properly, such as:

  • Only insert underscore if the previous character was lowercase
  • Or use a well-tested conversion library
Suggested change
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
} else {
result.push(c);
}
}
let chars: Vec<char> = s.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c.is_uppercase() {
if i > 0 {
let prev = chars[i - 1];
let next_is_lowercase = chars
.get(i + 1)
.map(|n| n.is_lowercase())
.unwrap_or(false);
if prev.is_lowercase() || next_is_lowercase {
result.push('_');
}
}
for lc in c.to_lowercase() {
result.push(lc);
}
} else {
result.push(c);
}
}

Copilot uses AI. Check for mistakes.
result
}

fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap();
let build_dir = PathBuf::from(&out_dir).join("cpp_tests");

std::fs::create_dir_all(&build_dir).unwrap();

let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let tests_dir = PathBuf::from(&manifest_dir).join("tests");

// CMake configure
let configure_status = Command::new("cmake")
.args([tests_dir.to_str().unwrap(), "-DCMAKE_BUILD_TYPE=Debug"])
.current_dir(&build_dir)
.status()
.expect("cmake configure failed");

if !configure_status.success() {
panic!("cmake configure failed with status: {}", configure_status);
}

// CMake build
let build_status = Command::new("cmake")
.args(["--build", ".", "-j"])
.current_dir(&build_dir)
.status()
.expect("cmake build failed");

if !build_status.success() {
panic!("cmake build failed with status: {}", build_status);
Comment on lines +31 to +49
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The panic messages on build failures (lines 38, 49) don't include the actual error output from cmake/cmake --build. This makes debugging build failures difficult.

Consider capturing and displaying stderr in the panic message to provide more context when builds fail:

.output().expect("cmake configure failed");
if !result.status.success() {
    eprintln!("{}", String::from_utf8_lossy(&result.stderr));
    panic!("cmake configure failed");
}
Suggested change
let configure_status = Command::new("cmake")
.args([tests_dir.to_str().unwrap(), "-DCMAKE_BUILD_TYPE=Debug"])
.current_dir(&build_dir)
.status()
.expect("cmake configure failed");
if !configure_status.success() {
panic!("cmake configure failed with status: {}", configure_status);
}
// CMake build
let build_status = Command::new("cmake")
.args(["--build", ".", "-j"])
.current_dir(&build_dir)
.status()
.expect("cmake build failed");
if !build_status.success() {
panic!("cmake build failed with status: {}", build_status);
let configure_output = Command::new("cmake")
.args([tests_dir.to_str().unwrap(), "-DCMAKE_BUILD_TYPE=Debug"])
.current_dir(&build_dir)
.output()
.expect("failed to run cmake configure");
if !configure_output.status.success() {
let stderr = String::from_utf8_lossy(&configure_output.stderr);
panic!(
"cmake configure failed with status: {}. Stderr:\n{}",
configure_output.status, stderr
);
}
// CMake build
let build_output = Command::new("cmake")
.args(["--build", ".", "-j"])
.current_dir(&build_dir)
.output()
.expect("failed to run cmake build");
if !build_output.status.success() {
let stderr = String::from_utf8_lossy(&build_output.stderr);
panic!(
"cmake build failed with status: {}. Stderr:\n{}",
build_output.status, stderr
);

Copilot uses AI. Check for mistakes.
}

// ctest -N でテスト一覧を取得
let ctest_output = Command::new("ctest")
.args(["-N"])
.current_dir(&build_dir)
.output()
.expect("ctest -N failed");

let test_list = String::from_utf8_lossy(&ctest_output.stdout);

// テスト名を抽出("Test #1: TestName" or "Test #10: TestName" 形式)
let test_names: Vec<String> = test_list
.lines()
.filter_map(|line| {
// "Test" で始まり、"#" の後に数字と ":" がある行を探す
if line.trim_start().starts_with("Test") {
if let Some(colon_pos) = line.find(':') {
let name = line[colon_pos + 1..].trim();
if !name.is_empty() {
return Some(name.to_string());
}
}
}
None
})
.collect();

// テストコードを生成
let mut generated_tests = String::new();
generated_tests.push_str("// Auto-generated test wrappers for C++ GoogleTest tests\n\n");

for test_name in &test_names {
// Rust の識別子として有効な名前に変換(. を _ に)
let rust_name = test_name.replace('.', "_").to_lowercase();

// テストバイナリ名を推測
// StreamRecBufferTest -> test_stream_rec_buffer
let binary_name = if let Some(dot_pos) = test_name.find('.') {
let suite = &test_name[..dot_pos];
// "Test" サフィックスを除去
let suite = suite.strip_suffix("Test").unwrap_or(suite);
format!("test_{}", camel_to_snake(suite))
} else {
Comment on lines +90 to +93
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite name parsing logic (lines 88-92) assumes test names follow the "SuiteTest.TestCase" pattern and strips the "Test" suffix. However, this logic will fail or produce incorrect names if:

  1. The suite name doesn't end with "Test" (returns the original suite name)
  2. The test name doesn't contain a dot (defaults to "test")
  3. The suite name contains "Test" in the middle (e.g., "TestingFramework" becomes "ingFramework")

Consider adding validation or logging when the expected pattern isn't matched, and handle edge cases more robustly.

Suggested change
// "Test" サフィックスを除去
let suite = suite.strip_suffix("Test").unwrap_or(suite);
format!("test_{}", camel_to_snake(suite))
} else {
// "Test" サフィックスを除去(末尾に "Test" がない場合はそのまま利用)
let (suite_without_suffix, had_suffix) = if let Some(stripped) = suite.strip_suffix("Test") {
(stripped, true)
} else {
(suite, false)
};
if !had_suffix {
eprintln!(
"Warning: expected test suite name to end with 'Test', but got '{suite}' in test '{test_name}'. Using full suite name for binary inference."
);
}
format!("test_{}", camel_to_snake(suite_without_suffix))
} else {
eprintln!(
"Warning: test name '{test_name}' does not contain a '.', defaulting test binary name to 'test'."
);

Copilot uses AI. Check for mistakes.
"test".to_string()
};

generated_tests.push_str(&format!(
r#"#[test]
fn {rust_name}() {{
let test_dir = env!("CPP_TEST_DIR");
let binary = std::path::Path::new(test_dir).join("{binary_name}");
let output = std::process::Command::new(&binary)
.arg("--gtest_filter={test_name}")
.output()
.expect("failed to run test binary");

if !output.status.success() {{
eprintln!("{{}}", String::from_utf8_lossy(&output.stdout));
eprintln!("{{}}", String::from_utf8_lossy(&output.stderr));
panic!("C++ test {test_name} failed");
}}
Comment on lines +102 to +111
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated test wrapper doesn't pass through test output in real-time. The output is only shown if the test fails (lines 107-109). This means:

  1. Users don't see test progress during long-running tests
  2. Debugging hangs or timeouts is difficult
  3. The test output format doesn't match typical cargo test output

Consider using .spawn() and streaming output instead of .output(), or add an option to show output for all tests during development.

Suggested change
let output = std::process::Command::new(&binary)
.arg("--gtest_filter={test_name}")
.output()
.expect("failed to run test binary");
if !output.status.success() {{
eprintln!("{{}}", String::from_utf8_lossy(&output.stdout));
eprintln!("{{}}", String::from_utf8_lossy(&output.stderr));
panic!("C++ test {test_name} failed");
}}
let status = std::process::Command::new(&binary)
.arg("--gtest_filter={test_name}")
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.expect("failed to run test binary");
assert!(status.success(), "C++ test {test_name} failed");

Copilot uses AI. Check for mistakes.
}}

"#,
rust_name = rust_name,
binary_name = binary_name,
test_name = test_name
));
}

// 生成したコードをファイルに書き出し
let generated_path = PathBuf::from(&out_dir).join("generated_tests.rs");
let mut file = std::fs::File::create(&generated_path).expect("failed to create generated file");
file.write_all(generated_tests.as_bytes())
.expect("failed to write generated tests");

// テストバイナリのパスを環境変数で渡す
println!("cargo:rustc-env=CPP_TEST_DIR={}", build_dir.display());
println!("cargo:rerun-if-changed=tests/");
}
11 changes: 11 additions & 0 deletions component_driver/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! C2A Component Driver unit tests
//!
//! This crate wraps C++ GoogleTest tests for the component_driver module.
//! Run with `cargo test` to execute all C++ unit tests.
//!
//! Each C++ test case is exposed as an individual Rust test.

#[cfg(test)]
mod tests {
include!(concat!(env!("OUT_DIR"), "/generated_tests.rs"));
}
78 changes: 78 additions & 0 deletions component_driver/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
cmake_minimum_required(VERSION 3.14)

project(component_driver_tests)

# GoogleTest を FetchContent で取得
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)

set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()

set(MOCK_PRELUDE_FLAG "-include ${CMAKE_CURRENT_SOURCE_DIR}/mocks/driver_super_mock_prelude.h")

# StreamRecBuffer のテスト
add_executable(test_stream_rec_buffer
test_stream_rec_buffer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../driver_super.c
mocks/mock_hal_handler_registry.c
mocks/mock_time_manager.c
)

# driver_super.c にモック prelude を適用(test_stream_rec_buffer 用)
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/../driver_super.c
TARGET_DIRECTORY test_stream_rec_buffer
PROPERTIES COMPILE_FLAGS "${MOCK_PRELUDE_FLAG}"
)

target_include_directories(test_stream_rec_buffer PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/mocks # モックが実際のヘッダより優先される
${CMAKE_CURRENT_SOURCE_DIR}/mocks/src_user # <src_user/...> インクルード用
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_CURRENT_SOURCE_DIR}/../..
)

target_link_libraries(test_stream_rec_buffer
GTest::gtest_main
)

include(GoogleTest)
gtest_discover_tests(test_stream_rec_buffer)

# FrameAnalysis のテスト
add_executable(test_frame_analysis
test_frame_analysis.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../driver_super.c
mocks/mock_hal_handler_registry.c
mocks/mock_time_manager.c
)

# driver_super.c と test_frame_analysis.cpp にモック prelude を適用
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/../driver_super.c
${CMAKE_CURRENT_SOURCE_DIR}/test_frame_analysis.cpp
TARGET_DIRECTORY test_frame_analysis
PROPERTIES COMPILE_FLAGS "${MOCK_PRELUDE_FLAG}"
)

target_include_directories(test_frame_analysis PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/mocks # モックが実際のヘッダより優先される
${CMAKE_CURRENT_SOURCE_DIR}/mocks/src_user # <src_user/...> インクルード用
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_CURRENT_SOURCE_DIR}/../..
)
Comment on lines +35 to +72
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The include directories configuration (lines 25-29, 45-49) uses relative paths with ".." which makes the build structure fragile. If the directory layout changes, these paths will break.

Consider using CMake variables to make the paths more robust:

set(C2A_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../..)
target_include_directories(test_stream_rec_buffer PRIVATE
  ${CMAKE_CURRENT_SOURCE_DIR}
  ${CMAKE_CURRENT_SOURCE_DIR}/..
  ${C2A_ROOT_DIR}
)

This makes the intent clearer and easier to maintain.

Copilot uses AI. Check for mistakes.

target_link_libraries(test_frame_analysis
GTest::gtest_main
)

gtest_discover_tests(test_frame_analysis)
Loading
Loading