From faa75dfc2956d1046eb45ebf6190101073e9e719 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 15 Jul 2026 17:08:44 +0200 Subject: [PATCH 1/8] chore(runtime): ignore generated analyzer artifacts --- .gitignore | 6 ++++++ scripts/format-check.sh | 2 +- scripts/format.sh | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d4fb281..b554bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,9 @@ # debug information files *.dwo + +# Runtime analyzer artifacts +runtime-analyzer-artifacts/ + +# Python cache +__pycache__/ diff --git a/scripts/format-check.sh b/scripts/format-check.sh index 35ac7f0..fcfa497 100755 --- a/scripts/format-check.sh +++ b/scripts/format-check.sh @@ -8,7 +8,7 @@ files=() while IFS= read -r -d '' file; do files+=("$file") done < <(find "${REPO_ROOT}" \ - \( -path "${REPO_ROOT}/build" -o -path "${REPO_ROOT}/extern-project" -o -path "${REPO_ROOT}/external" -o -path "${REPO_ROOT}/third_party" -o -path "${REPO_ROOT}/vendor" -o -path "${REPO_ROOT}/.git" \) -prune -o \ + \( -path "${REPO_ROOT}/build" -o -path "${REPO_ROOT}/test" -o -path "${REPO_ROOT}/runtime-analyzer-artifacts" -o -path "${REPO_ROOT}/extern-project" -o -path "${REPO_ROOT}/external" -o -path "${REPO_ROOT}/third_party" -o -path "${REPO_ROOT}/vendor" -o -path "${REPO_ROOT}/.git" \) -prune -o \ -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' -o -name '*.h' -o -name '*.hh' -o -name '*.hpp' -o -name '*.hxx' \) -print0) if [ "${#files[@]}" -eq 0 ]; then diff --git a/scripts/format.sh b/scripts/format.sh index 1f595b4..ae13f10 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -8,7 +8,7 @@ files=() while IFS= read -r -d '' file; do files+=("$file") done < <(find "${REPO_ROOT}" \ - \( -path "${REPO_ROOT}/build" -o -path "${REPO_ROOT}/extern-project" -o -path "${REPO_ROOT}/external" -o -path "${REPO_ROOT}/third_party" -o -path "${REPO_ROOT}/vendor" -o -path "${REPO_ROOT}/.git" \) -prune -o \ + \( -path "${REPO_ROOT}/build" -o -path "${REPO_ROOT}/test" -o -path "${REPO_ROOT}/runtime-analyzer-artifacts" -o -path "${REPO_ROOT}/extern-project" -o -path "${REPO_ROOT}/external" -o -path "${REPO_ROOT}/third_party" -o -path "${REPO_ROOT}/vendor" -o -path "${REPO_ROOT}/.git" \) -prune -o \ -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' -o -name '*.h' -o -name '*.hh' -o -name '*.hpp' -o -name '*.hxx' \) -print0) if [ "${#files[@]}" -eq 0 ]; then From b55e173df765f93039415f1e38037483538e06e2 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 15 Jul 2026 17:09:48 +0200 Subject: [PATCH 2/8] feat(runtime): implement minimal runtime analyzer execution --- CMakeLists.txt | 14 +- include/runtime_analyzer.hpp | 76 +++ include/your_tool_name.hpp | 1 - main.cpp | 7 +- src/runtime_analyzer.cpp | 1079 ++++++++++++++++++++++++++++++++++ src/your_tool_name.cpp | 1 - 6 files changed, 1165 insertions(+), 13 deletions(-) create mode 100644 include/runtime_analyzer.hpp delete mode 100644 include/your_tool_name.hpp create mode 100644 src/runtime_analyzer.cpp delete mode 100644 src/your_tool_name.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 61a7a35..d22c3e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,20 +2,18 @@ cmake_minimum_required(VERSION 3.16) # ============================================================================ -# CoreTrace Tool Template — Generic CMakeLists -# -# Goal: a consumer should only edit the section "USER CONFIG" below. +# CoreTrace Runtime Analyzer # ============================================================================ # ------------------------------ -# USER CONFIG (edit this block) +# PROJECT CONFIG # ------------------------------ -set(TOOL_NAME "your_tool_name" CACHE STRING "Tool/project name") +set(TOOL_NAME "runtime-analyzer" CACHE STRING "Tool/project name" FORCE) set(TOOL_NAMESPACE "coretrace" CACHE STRING "CMake namespace for ALIAS targets") # Library sources (add/remove files here) set(TOOL_SOURCES - src/your_tool_name.cpp + src/runtime_analyzer.cpp ) # CLI @@ -43,10 +41,10 @@ set(CORETRACE_COMPILER_GIT_REPOSITORY "https://github.com/CoreTrace/coretrace-co set(CORETRACE_COMPILER_GIT_TAG "main" CACHE STRING "coretrace-compiler git tag") # ------------------------------ -# END USER CONFIG +# END PROJECT CONFIG # ------------------------------ -project(${TOOL_NAME} LANGUAGES CXX) +project(${TOOL_NAME} LANGUAGES C CXX) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") include(CheckLLVMVersion OPTIONAL) diff --git a/include/runtime_analyzer.hpp b/include/runtime_analyzer.hpp new file mode 100644 index 0000000..ecb2527 --- /dev/null +++ b/include/runtime_analyzer.hpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef CORETRACE_RUNTIME_ANALYZER_HPP +#define CORETRACE_RUNTIME_ANALYZER_HPP + +#include +#include +#include + +namespace coretrace::runtime_analyzer +{ + struct AnalyzerOptions + { + std::vector compiler_args; + std::vector program_args; + std::vector environment; + std::vector test_directories; + std::string output_path = "runtime-analyzer.out"; + std::string output_directory = "runtime-analyzer-artifacts"; + std::string working_directory; + bool explicit_output_path = false; + bool run_program = true; + bool show_program_output = false; + bool show_events = false; + bool strict_test_exit = false; + }; + + struct CollectionSummary + { + std::uint64_t coretrace_lines = 0; + std::uint64_t entry_events = 0; + std::uint64_t exit_events = 0; + std::uint64_t allocation_events = 0; + std::uint64_t bounds_errors = 0; + std::uint64_t leak_reports = 0; + std::uint64_t vtable_events = 0; + std::uint64_t warnings = 0; + std::uint64_t errors = 0; + }; + + struct AnalyzerResult + { + bool success = false; + bool compile_success = false; + bool executed = false; + int exit_code = 1; + std::string output_path; + std::string diagnostics; + std::string stdout_text; + std::string stderr_text; + std::vector coretrace_events; + CollectionSummary summary; + }; + + struct TestFileResult + { + std::string source_path; + AnalyzerResult analyzer_result; + }; + + struct BatchResult + { + bool success = false; + int exit_code = 1; + std::vector tests; + CollectionSummary summary; + std::string diagnostics; + std::uint64_t compile_failures = 0; + std::uint64_t runtime_failures = 0; + }; + + [[nodiscard]] AnalyzerResult Run(const AnalyzerOptions& options); + [[nodiscard]] BatchResult RunBatch(const AnalyzerOptions& options); + [[nodiscard]] int Main(int argc, char** argv); +} // namespace coretrace::runtime_analyzer + +#endif // CORETRACE_RUNTIME_ANALYZER_HPP diff --git a/include/your_tool_name.hpp b/include/your_tool_name.hpp deleted file mode 100644 index 7b9637e..0000000 --- a/include/your_tool_name.hpp +++ /dev/null @@ -1 +0,0 @@ -#pragma once \ No newline at end of file diff --git a/main.cpp b/main.cpp index 62a616b..da8f62a 100644 --- a/main.cpp +++ b/main.cpp @@ -1,6 +1,7 @@ -#include "your_tool_name.hpp" +// SPDX-License-Identifier: Apache-2.0 +#include "runtime_analyzer.hpp" -int main(void) +int main(int argc, char** argv) { - return 0; + return coretrace::runtime_analyzer::Main(argc, argv); } diff --git a/src/runtime_analyzer.cpp b/src/runtime_analyzer.cpp new file mode 100644 index 0000000..1ff2483 --- /dev/null +++ b/src/runtime_analyzer.cpp @@ -0,0 +1,1079 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "runtime_analyzer.hpp" + +#include "compilerlib/compiler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#include +#include +#include +#endif + +namespace coretrace::runtime_analyzer +{ + namespace + { + constexpr std::string_view kDefaultOutputPath = "runtime-analyzer.out"; + + struct ParseResult + { + bool ok = true; + bool help = false; + AnalyzerOptions options; + std::string error; + }; + + struct OutputArgument + { + bool present = false; + std::string path; + }; + + struct ProcessResult + { + int exit_code = 1; + std::string stdout_text; + std::string stderr_text; + std::string error; + }; + + [[nodiscard]] bool IsCompileOnlyFlag(std::string_view arg) + { + return arg == "-c" || arg == "-S" || arg == "-E" || arg == "-emit-llvm" || + arg == "--precompile" || arg == "-fsyntax-only"; + } + + [[nodiscard]] bool IsSourceFile(const std::filesystem::path& path) + { + const std::string ext = path.extension().string(); + return ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx"; + } + + [[nodiscard]] bool Contains(std::string_view haystack, std::string_view needle) + { + return haystack.find(needle) != std::string_view::npos; + } + + [[nodiscard]] bool HasCompileOnlyAction(const std::vector& args) + { + return std::ranges::any_of(args, IsCompileOnlyFlag); + } + + [[nodiscard]] bool HasLanguageOverride(const std::vector& args) + { + for (std::size_t i = 0; i < args.size(); ++i) + { + if (args[i] == "-x" && i + 1 < args.size()) + { + return true; + } + if (args[i].rfind("-x=", 0) == 0 || args[i].rfind("-x", 0) == 0) + { + return true; + } + } + return false; + } + + [[nodiscard]] std::string ReadSmallTextFile(const std::filesystem::path& path) + { + std::ifstream input(path); + if (!input) + { + return {}; + } + + std::ostringstream output; + output << input.rdbuf(); + return output.str(); + } + + [[nodiscard]] bool CSourceLooksLikeCxx(const std::filesystem::path& source, + std::string_view text) + { + if (source.extension() != ".c") + { + return false; + } + return Contains(text, "#include ") || Contains(text, "#include ") || + Contains(text, "std::") || Contains(text, "static_cast<") || + Contains(text, "reinterpret_cast<") || Contains(text, "class ") || + Contains(text, "namespace ") || Contains(text, "template <") || + Contains(text, "virtual "); + } + + [[nodiscard]] bool SourceRequiresDebugDefine(std::string_view text) + { + return Contains(text, "#ifndef DEBUG") || Contains(text, "#if !defined(DEBUG)"); + } + + void AddPerSourceCompatibilityArgs(const std::filesystem::path& source, + std::vector& args) + { + const std::string text = ReadSmallTextFile(source); + if (!HasLanguageOverride(args) && CSourceLooksLikeCxx(source, text)) + { + args.emplace_back("-x"); + args.emplace_back("c++"); + } + if (SourceRequiresDebugDefine(text) && + std::ranges::none_of(args, [](const std::string& arg) + { return arg == "-DDEBUG" || arg == "-DDEBUG=1"; })) + { + args.emplace_back("-DDEBUG"); + } + } + + [[nodiscard]] OutputArgument FindOutputArgument(const std::vector& args) + { + OutputArgument result; + for (std::size_t i = 0; i < args.size(); ++i) + { + const std::string& arg = args[i]; + if (arg == "-o" || arg == "--output") + { + result.present = true; + if (i + 1 < args.size()) + { + result.path = args[i + 1]; + } + return result; + } + if (arg.rfind("-o=", 0) == 0) + { + result.present = true; + result.path = arg.substr(3); + return result; + } + if (arg.rfind("--output=", 0) == 0) + { + result.present = true; + result.path = arg.substr(9); + return result; + } + } + return result; + } + + [[nodiscard]] std::vector + BuildCompilerArguments(const AnalyzerOptions& options, std::string& output_path) + { + std::vector args; + args.reserve(options.compiler_args.size() + 2); + for (const std::string& arg : options.compiler_args) + { + if (arg == "--instrument") + { + continue; + } + args.push_back(arg); + } + + const OutputArgument output = FindOutputArgument(args); + if (output.present) + { + output_path = output.path; + return args; + } + + output_path = + options.output_path.empty() ? std::string(kDefaultOutputPath) : options.output_path; + args.emplace_back("-o"); + args.push_back(output_path); + return args; + } + + [[nodiscard]] bool IsValidEnvironmentAssignment(std::string_view assignment) + { + const std::size_t eq = assignment.find('='); + return eq != std::string_view::npos && eq != 0; + } + + [[nodiscard]] std::string AbsolutePathForExecution(std::string_view output_path) + { + std::error_code error; + std::filesystem::path path(output_path); + std::filesystem::path absolute = std::filesystem::absolute(path, error); + if (error) + { + return std::string(output_path); + } + return absolute.string(); + } + + [[nodiscard]] std::string StripAnsi(std::string_view input) + { + std::string output; + output.reserve(input.size()); + for (std::size_t i = 0; i < input.size(); ++i) + { + if (input[i] == '\x1b' && i + 1 < input.size() && input[i + 1] == '[') + { + i += 2; + while (i < input.size() && !((input[i] >= 'A' && input[i] <= 'Z') || + (input[i] >= 'a' && input[i] <= 'z'))) + { + ++i; + } + continue; + } + output.push_back(input[i]); + } + return output; + } + + [[nodiscard]] bool IsCoreTraceLine(std::string_view line) + { + return Contains(line, "==ct==") || Contains(line, "ct:") || + Contains(line, "[ENTRY-FUNCTION]") || Contains(line, "[EXIT-FUNCTION]") || + Contains(line, "[VTABLE-DIAG]") || Contains(line, "tracing-") || + Contains(line, "auto-free") || Contains(line, "heap-buffer-overflow") || + Contains(line, "heap-use-after-free"); + } + + void UpdateSummary(std::string_view line, CollectionSummary& summary) + { + if (Contains(line, "[ENTRY-FUNCTION]") || Contains(line, "ct: enter ")) + { + ++summary.entry_events; + } + if (Contains(line, "[EXIT-FUNCTION]")) + { + ++summary.exit_events; + } + if (Contains(line, "tracing-") || Contains(line, "auto-free") || + Contains(line, "leaks detected") || Contains(line, "ct: leak ")) + { + ++summary.allocation_events; + } + if (Contains(line, "heap-buffer-overflow") || Contains(line, "heap-use-after-free")) + { + ++summary.bounds_errors; + } + if (Contains(line, "leaks detected") || Contains(line, "ct: leak ")) + { + ++summary.leak_reports; + } + if (Contains(line, "[VTABLE-DIAG]")) + { + ++summary.vtable_events; + } + if (Contains(line, "[WARN]") || Contains(line, " WARN ")) + { + ++summary.warnings; + } + if (Contains(line, "[ERROR]") || Contains(line, " ERROR ")) + { + ++summary.errors; + } + } + + [[nodiscard]] CollectionSummary CollectEvents(std::string_view text, + std::vector& events) + { + CollectionSummary summary; + std::istringstream stream(std::string(StripAnsi(text))); + std::string line; + while (std::getline(stream, line)) + { + if (!IsCoreTraceLine(line)) + { + continue; + } + + ++summary.coretrace_lines; + UpdateSummary(line, summary); + events.push_back(std::move(line)); + } + return summary; + } + + [[nodiscard]] CollectionSummary MergeSummaries(CollectionSummary lhs, + const CollectionSummary& rhs) + { + lhs.coretrace_lines += rhs.coretrace_lines; + lhs.entry_events += rhs.entry_events; + lhs.exit_events += rhs.exit_events; + lhs.allocation_events += rhs.allocation_events; + lhs.bounds_errors += rhs.bounds_errors; + lhs.leak_reports += rhs.leak_reports; + lhs.vtable_events += rhs.vtable_events; + lhs.warnings += rhs.warnings; + lhs.errors += rhs.errors; + return lhs; + } + + [[nodiscard]] std::string SanitizeArtifactName(const std::filesystem::path& path) + { + std::string name = path.generic_string(); + for (char& ch : name) + { + const bool keep = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || ch == '-' || ch == '_'; + if (!keep) + { + ch = '_'; + } + } + if (name.empty()) + { + return "test"; + } + return name; + } + + [[nodiscard]] std::uint64_t StablePathHash(std::string_view text) + { + constexpr std::uint64_t kFnvOffsetBasis = 14695981039346656037ULL; + constexpr std::uint64_t kFnvPrime = 1099511628211ULL; + + std::uint64_t hash = kFnvOffsetBasis; + for (const char ch : text) + { + hash ^= static_cast(ch); + hash *= kFnvPrime; + } + return hash; + } + + [[nodiscard]] std::string HexDigest(std::uint64_t value) + { + std::ostringstream output; + output << std::hex << std::nouppercase << std::setfill('0') << std::setw(16) << value; + return output.str(); + } + + [[nodiscard]] std::vector + DiscoverSourceFiles(const std::vector& directories, std::string& diagnostics) + { + std::vector sources; + for (const std::string& directory : directories) + { + std::error_code error; + const std::filesystem::path root(directory); + if (!std::filesystem::exists(root, error)) + { + diagnostics += "test directory does not exist: " + directory + "\n"; + continue; + } + if (!std::filesystem::is_directory(root, error)) + { + diagnostics += "test path is not a directory: " + directory + "\n"; + continue; + } + + std::filesystem::recursive_directory_iterator it( + root, std::filesystem::directory_options::skip_permission_denied, error); + const std::filesystem::recursive_directory_iterator end; + while (!error && it != end) + { + const std::filesystem::directory_entry& entry = *it; + if (entry.is_regular_file(error) && IsSourceFile(entry.path())) + { + sources.push_back(entry.path()); + } + it.increment(error); + } + if (error) + { + diagnostics += "failed while scanning test directory " + directory + ": " + + error.message() + "\n"; + } + } + + std::ranges::sort(sources); + sources.erase(std::ranges::unique(sources).begin(), sources.end()); + return sources; + } + + [[nodiscard]] std::filesystem::path MakeTestOutputPath(const AnalyzerOptions& options, + const std::filesystem::path& source) + { + std::error_code error; + std::filesystem::path relative = std::filesystem::relative(source, error); + if (error) + { + relative = source.filename(); + } + + const std::string hash_input = relative.generic_string(); + std::filesystem::path artifact_name = relative; + artifact_name.replace_extension(); + return std::filesystem::path(options.output_directory) / + (SanitizeArtifactName(artifact_name) + "_" + + HexDigest(StablePathHash(hash_input))); + } + +#if defined(_WIN32) + [[nodiscard]] ProcessResult RunProcess(const std::string&, const std::vector&, + const AnalyzerOptions&) + { + ProcessResult result; + result.error = "runtime execution is not implemented on Windows yet"; + return result; + } +#else + [[nodiscard]] bool SetCloseOnExec(int fd) + { + const int flags = fcntl(fd, F_GETFD); + if (flags < 0) + { + return false; + } + return fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == 0; + } + + [[nodiscard]] bool SetNonBlocking(int fd) + { + const int flags = fcntl(fd, F_GETFL); + if (flags < 0) + { + return false; + } + return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0; + } + + void AppendFromFd(int fd, std::string& output, bool& open) + { + char buffer[4096]; + for (;;) + { + const ssize_t count = read(fd, buffer, sizeof(buffer)); + if (count > 0) + { + output.append(buffer, static_cast(count)); + continue; + } + if (count == 0) + { + close(fd); + open = false; + return; + } + if (errno == EINTR) + { + continue; + } + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + return; + } + close(fd); + open = false; + return; + } + } + + [[nodiscard]] ProcessResult RunProcess(const std::string& executable, + const std::vector& program_args, + const AnalyzerOptions& options) + { + ProcessResult result; + int stdout_pipe[2] = {-1, -1}; + int stderr_pipe[2] = {-1, -1}; + if (pipe(stdout_pipe) != 0) + { + result.error = std::string("pipe failed: ") + std::strerror(errno); + return result; + } + if (pipe(stderr_pipe) != 0) + { + result.error = std::string("pipe failed: ") + std::strerror(errno); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + return result; + } + + (void)SetCloseOnExec(stdout_pipe[0]); + (void)SetCloseOnExec(stderr_pipe[0]); + (void)SetNonBlocking(stdout_pipe[0]); + (void)SetNonBlocking(stderr_pipe[0]); + + const pid_t pid = fork(); + if (pid < 0) + { + result.error = std::string("fork failed: ") + std::strerror(errno); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + close(stderr_pipe[0]); + close(stderr_pipe[1]); + return result; + } + + if (pid == 0) + { + close(stdout_pipe[0]); + close(stderr_pipe[0]); + (void)dup2(stdout_pipe[1], STDOUT_FILENO); + (void)dup2(stderr_pipe[1], STDERR_FILENO); + close(stdout_pipe[1]); + close(stderr_pipe[1]); + + if (!options.working_directory.empty() && + chdir(options.working_directory.c_str()) != 0) + { + _exit(126); + } + + for (const std::string& assignment : options.environment) + { + const std::size_t eq = assignment.find('='); + if (eq != std::string::npos && eq != 0) + { + std::string name = assignment.substr(0, eq); + std::string value = assignment.substr(eq + 1); + setenv(name.c_str(), value.c_str(), 1); + } + } + + std::vector argv_storage; + argv_storage.reserve(program_args.size() + 1); + argv_storage.push_back(executable); + argv_storage.insert(argv_storage.end(), program_args.begin(), program_args.end()); + + std::vector argv; + argv.reserve(argv_storage.size() + 1); + for (std::string& arg : argv_storage) + { + argv.push_back(arg.data()); + } + argv.push_back(nullptr); + + execv(executable.c_str(), argv.data()); + _exit(errno == ENOENT ? 127 : 126); + } + + close(stdout_pipe[1]); + close(stderr_pipe[1]); + + bool stdout_open = true; + bool stderr_open = true; + while (stdout_open || stderr_open) + { + fd_set read_set; + FD_ZERO(&read_set); + int max_fd = -1; + if (stdout_open) + { + FD_SET(stdout_pipe[0], &read_set); + max_fd = std::max(max_fd, stdout_pipe[0]); + } + if (stderr_open) + { + FD_SET(stderr_pipe[0], &read_set); + max_fd = std::max(max_fd, stderr_pipe[0]); + } + + const int selected = select(max_fd + 1, &read_set, nullptr, nullptr, nullptr); + if (selected < 0) + { + if (errno == EINTR) + { + continue; + } + result.error = std::string("select failed: ") + std::strerror(errno); + break; + } + + if (stdout_open && FD_ISSET(stdout_pipe[0], &read_set)) + { + AppendFromFd(stdout_pipe[0], result.stdout_text, stdout_open); + } + if (stderr_open && FD_ISSET(stderr_pipe[0], &read_set)) + { + AppendFromFd(stderr_pipe[0], result.stderr_text, stderr_open); + } + } + + int status = 0; + while (waitpid(pid, &status, 0) < 0) + { + if (errno != EINTR) + { + result.error = std::string("waitpid failed: ") + std::strerror(errno); + return result; + } + } + + if (WIFEXITED(status)) + { + result.exit_code = WEXITSTATUS(status); + } + else if (WIFSIGNALED(status)) + { + result.exit_code = 128 + WTERMSIG(status); + } + else + { + result.exit_code = 1; + } + + return result; + } +#endif + + void PrintHelp(std::ostream& out) + { + out << "Usage: runtime-analyzer [options] -- \n\n" + << "Builds an instrumented binary with coretrace-compiler library mode, runs it, " + "and collects basic runtime events.\n\n" + << "Options:\n" + << " -o, --output Instrumented binary path when compiler args do " + "not contain -o\n" + << " --run-arg Argument passed to the instrumented binary\n" + << " --env NAME=VALUE Environment variable passed to the " + "instrumented binary\n" + << " --cwd Working directory used when running the binary\n" + << " --test-dir Run every C/C++ source file under a test " + "directory\n" + << " --output-dir Artifact directory used by --test-dir\n" + << " --strict-test-exit Make batch mode fail when any test binary exits " + "non-zero\n" + << " --no-run Compile only, without executing the binary\n" + << " --show-output Print captured stdout/stderr after the summary\n" + << " --show-events Print collected CoreTrace event lines\n" + << " -h, --help Show this help\n\n" + << "Example:\n" + << " runtime-analyzer -o ./app -- --ct-modules=trace,alloc,bounds main.c\n"; + } + + [[nodiscard]] ParseResult ParseArgs(int argc, char** argv) + { + ParseResult result; + bool compiler_args = false; + + for (int i = 1; i < argc; ++i) + { + std::string arg = argv[i]; + if (compiler_args) + { + result.options.compiler_args.push_back(std::move(arg)); + continue; + } + + if (arg == "--") + { + compiler_args = true; + continue; + } + if (arg == "-h" || arg == "--help") + { + result.help = true; + return result; + } + if (arg == "-o" || arg == "--output") + { + if (i + 1 >= argc) + { + result.ok = false; + result.error = arg + " requires a value"; + return result; + } + result.options.output_path = argv[++i]; + result.options.explicit_output_path = true; + continue; + } + if (arg.rfind("--output=", 0) == 0) + { + result.options.output_path = arg.substr(9); + result.options.explicit_output_path = true; + continue; + } + if (arg == "--run-arg") + { + if (i + 1 >= argc) + { + result.ok = false; + result.error = "--run-arg requires a value"; + return result; + } + result.options.program_args.emplace_back(argv[++i]); + continue; + } + if (arg == "--env") + { + if (i + 1 >= argc) + { + result.ok = false; + result.error = "--env requires NAME=VALUE"; + return result; + } + std::string assignment = argv[++i]; + if (!IsValidEnvironmentAssignment(assignment)) + { + result.ok = false; + result.error = "--env requires NAME=VALUE"; + return result; + } + result.options.environment.push_back(std::move(assignment)); + continue; + } + if (arg == "--cwd") + { + if (i + 1 >= argc) + { + result.ok = false; + result.error = "--cwd requires a path"; + return result; + } + result.options.working_directory = argv[++i]; + continue; + } + if (arg == "--test-dir") + { + if (i + 1 >= argc) + { + result.ok = false; + result.error = "--test-dir requires a path"; + return result; + } + result.options.test_directories.emplace_back(argv[++i]); + continue; + } + if (arg == "--output-dir") + { + if (i + 1 >= argc) + { + result.ok = false; + result.error = "--output-dir requires a path"; + return result; + } + result.options.output_directory = argv[++i]; + continue; + } + if (arg == "--strict-test-exit") + { + result.options.strict_test_exit = true; + continue; + } + if (arg == "--no-run") + { + result.options.run_program = false; + continue; + } + if (arg == "--show-output") + { + result.options.show_program_output = true; + continue; + } + if (arg == "--show-events") + { + result.options.show_events = true; + continue; + } + + result.ok = false; + result.error = + "unknown analyzer option: " + arg + " (compiler arguments go after --)"; + return result; + } + + if ((!compiler_args || result.options.compiler_args.empty()) && + result.options.test_directories.empty()) + { + result.ok = false; + result.error = "missing compiler arguments; use -- "; + } + + return result; + } + + void PrintResult(const AnalyzerResult& result, const AnalyzerOptions& options) + { + std::cout << "runtime-analyzer: binary=" << result.output_path << '\n'; + std::cout << "runtime-analyzer: exit_code=" << result.exit_code << '\n'; + std::cout << "runtime-analyzer: collection\n"; + std::cout << " coretrace_lines=" << result.summary.coretrace_lines << '\n'; + std::cout << " entry_events=" << result.summary.entry_events << '\n'; + std::cout << " exit_events=" << result.summary.exit_events << '\n'; + std::cout << " allocation_events=" << result.summary.allocation_events << '\n'; + std::cout << " bounds_errors=" << result.summary.bounds_errors << '\n'; + std::cout << " leak_reports=" << result.summary.leak_reports << '\n'; + std::cout << " vtable_events=" << result.summary.vtable_events << '\n'; + std::cout << " warnings=" << result.summary.warnings << '\n'; + std::cout << " errors=" << result.summary.errors << '\n'; + + if (options.show_events && !result.coretrace_events.empty()) + { + std::cout << "\nruntime-analyzer: events\n"; + for (const std::string& event : result.coretrace_events) + { + std::cout << event << '\n'; + } + } + + if (options.show_program_output) + { + std::cout << "\nruntime-analyzer: stdout\n"; + std::cout << result.stdout_text; + if (!result.stdout_text.empty() && result.stdout_text.back() != '\n') + { + std::cout << '\n'; + } + + std::cout << "\nruntime-analyzer: stderr\n"; + std::cout << result.stderr_text; + if (!result.stderr_text.empty() && result.stderr_text.back() != '\n') + { + std::cout << '\n'; + } + } + } + + void PrintBatchResult(const BatchResult& result, const AnalyzerOptions& options) + { + std::cout << "runtime-analyzer: batch\n"; + std::cout << " tests=" << result.tests.size() << '\n'; + std::cout << " compile_failures=" << result.compile_failures << '\n'; + std::cout << " runtime_failures=" << result.runtime_failures << '\n'; + std::cout << " coretrace_lines=" << result.summary.coretrace_lines << '\n'; + std::cout << " entry_events=" << result.summary.entry_events << '\n'; + std::cout << " exit_events=" << result.summary.exit_events << '\n'; + std::cout << " allocation_events=" << result.summary.allocation_events << '\n'; + std::cout << " bounds_errors=" << result.summary.bounds_errors << '\n'; + std::cout << " leak_reports=" << result.summary.leak_reports << '\n'; + std::cout << " vtable_events=" << result.summary.vtable_events << '\n'; + std::cout << " warnings=" << result.summary.warnings << '\n'; + std::cout << " errors=" << result.summary.errors << '\n'; + + for (const TestFileResult& test : result.tests) + { + const AnalyzerResult& analyzer = test.analyzer_result; + const char* status = "PASS"; + if (!analyzer.compile_success) + { + status = "COMPILE"; + } + else if (!analyzer.success) + { + status = "RUNTIME"; + } + + std::cout << "runtime-analyzer: [" << status << "] " << test.source_path + << " exit_code=" << analyzer.exit_code + << " coretrace_lines=" << analyzer.summary.coretrace_lines + << " binary=" << analyzer.output_path << '\n'; + + if (options.show_events && !analyzer.coretrace_events.empty()) + { + for (const std::string& event : analyzer.coretrace_events) + { + std::cout << " " << event << '\n'; + } + } + } + } + } // namespace + + [[nodiscard]] AnalyzerResult Run(const AnalyzerOptions& options) + { + AnalyzerResult result; + if (HasCompileOnlyAction(options.compiler_args) && options.run_program) + { + result.diagnostics = + "runtime-analyzer requires a linked executable; remove compile-only flags or pass " + "--no-run"; + return result; + } + + std::string output_path; + std::vector compiler_args = BuildCompilerArguments(options, output_path); + if (options.explicit_output_path) + { + const OutputArgument forwarded_output = FindOutputArgument(options.compiler_args); + if (forwarded_output.present) + { + result.diagnostics = "output path specified twice: use analyzer -o/--output or " + "compiler -o, not both"; + return result; + } + } + + compilerlib::CompileResult compile_result = + compilerlib::compile(compiler_args, compilerlib::OutputMode::ToFile, true); + result.diagnostics = compile_result.diagnostics; + result.output_path = output_path; + if (!compile_result.success) + { + return result; + } + result.compile_success = true; + + if (!options.run_program) + { + result.success = true; + result.exit_code = 0; + return result; + } + + const std::string executable = AbsolutePathForExecution(output_path); + ProcessResult process = RunProcess(executable, options.program_args, options); + result.stdout_text = std::move(process.stdout_text); + result.stderr_text = std::move(process.stderr_text); + result.exit_code = process.exit_code; + result.executed = process.error.empty(); + if (!process.error.empty()) + { + result.diagnostics += process.error; + if (!result.diagnostics.empty() && result.diagnostics.back() != '\n') + { + result.diagnostics.push_back('\n'); + } + return result; + } + + std::vector stdout_events; + std::vector stderr_events; + CollectionSummary stdout_summary = CollectEvents(result.stdout_text, stdout_events); + CollectionSummary stderr_summary = CollectEvents(result.stderr_text, stderr_events); + result.summary = MergeSummaries(stdout_summary, stderr_summary); + result.coretrace_events.reserve(stdout_events.size() + stderr_events.size()); + result.coretrace_events.insert(result.coretrace_events.end(), + std::make_move_iterator(stdout_events.begin()), + std::make_move_iterator(stdout_events.end())); + result.coretrace_events.insert(result.coretrace_events.end(), + std::make_move_iterator(stderr_events.begin()), + std::make_move_iterator(stderr_events.end())); + + result.success = result.exit_code == 0; + return result; + } + + [[nodiscard]] BatchResult RunBatch(const AnalyzerOptions& options) + { + BatchResult batch; + if (HasCompileOnlyAction(options.compiler_args) && options.run_program) + { + batch.diagnostics = "runtime-analyzer batch mode requires linked executables; remove " + "compile-only flags " + "or pass --no-run"; + return batch; + } + + std::error_code error; + std::filesystem::create_directories(options.output_directory, error); + if (error) + { + batch.diagnostics = "failed to create output directory " + options.output_directory + + ": " + error.message(); + return batch; + } + + std::string discovery_diagnostics; + const std::vector sources = + DiscoverSourceFiles(options.test_directories, discovery_diagnostics); + batch.diagnostics += discovery_diagnostics; + if (sources.empty()) + { + batch.diagnostics += "no C/C++ test sources found\n"; + return batch; + } + + for (const std::filesystem::path& source : sources) + { + AnalyzerOptions test_options = options; + test_options.test_directories.clear(); + test_options.output_path = MakeTestOutputPath(options, source).string(); + test_options.explicit_output_path = true; + test_options.compiler_args = options.compiler_args; + AddPerSourceCompatibilityArgs(source, test_options.compiler_args); + test_options.compiler_args.push_back(source.string()); + std::filesystem::remove(test_options.output_path, error); + error.clear(); + + AnalyzerResult analyzer = Run(test_options); + if (!analyzer.diagnostics.empty()) + { + batch.diagnostics += source.string() + ":\n" + analyzer.diagnostics; + if (!batch.diagnostics.empty() && batch.diagnostics.back() != '\n') + { + batch.diagnostics.push_back('\n'); + } + } + + if (analyzer.output_path.empty()) + { + analyzer.output_path = test_options.output_path; + } + + if (!analyzer.compile_success) + { + ++batch.compile_failures; + } + else if (analyzer.exit_code != 0) + { + ++batch.runtime_failures; + } + batch.summary = MergeSummaries(batch.summary, analyzer.summary); + batch.tests.push_back(TestFileResult{source.string(), std::move(analyzer)}); + } + + const bool runtime_ok = options.strict_test_exit ? batch.runtime_failures == 0 : true; + batch.success = batch.compile_failures == 0 && runtime_ok; + batch.exit_code = batch.success ? 0 : 1; + return batch; + } + + [[nodiscard]] int Main(int argc, char** argv) + { + ParseResult parsed = ParseArgs(argc, argv); + if (parsed.help) + { + PrintHelp(std::cout); + return 0; + } + if (!parsed.ok) + { + std::cerr << "runtime-analyzer: " << parsed.error << '\n'; + PrintHelp(std::cerr); + return 2; + } + + if (!parsed.options.test_directories.empty()) + { + BatchResult result = RunBatch(parsed.options); + if (!result.diagnostics.empty()) + { + std::cerr << result.diagnostics; + if (result.diagnostics.back() != '\n') + { + std::cerr << '\n'; + } + } + PrintBatchResult(result, parsed.options); + return result.exit_code; + } + + AnalyzerResult result = Run(parsed.options); + if (!result.diagnostics.empty()) + { + std::cerr << result.diagnostics; + if (result.diagnostics.back() != '\n') + { + std::cerr << '\n'; + } + } + + if (result.output_path.empty()) + { + result.output_path = parsed.options.output_path; + } + PrintResult(result, parsed.options); + return result.success ? 0 : result.exit_code; + } +} // namespace coretrace::runtime_analyzer diff --git a/src/your_tool_name.cpp b/src/your_tool_name.cpp deleted file mode 100644 index 17ade95..0000000 --- a/src/your_tool_name.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "your_tool_name.hpp" From d6df3ac8b476018028b0f1cc9c90b70991cc5bf2 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 15 Jul 2026 17:10:02 +0200 Subject: [PATCH 3/8] docs(runtime): document runtime analyzer usage --- README.md | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 344ace4..caa3bc4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,82 @@ -# coretrace-tool-template -Standardized template repository for building CoreTrace tools with a unified architecture, CI pipeline, and best practices for scalable static and dynamic analysis tooling. +# coretrace-runtime-analyzer + +Minimal execution and collection tool for CoreTrace-instrumented binaries. + +`runtime-analyzer` uses `coretrace-compiler` in library mode, builds an instrumented binary from +forwarded compiler arguments, executes it, captures stdout/stderr, and prints a small runtime event +summary. + +## Usage + +```zsh +runtime-analyzer -o ./app -- --ct-modules=trace,alloc,bounds main.c +runtime-analyzer --show-events -- main.c +runtime-analyzer --run-arg input.txt --env CT_LOG_LEVEL=info -- main.c +runtime-analyzer --test-dir test --output-dir /tmp/runtime-analyzer-tests -- --ct-modules=all +``` + +Arguments before `--` belong to `runtime-analyzer`. Arguments after `--` are forwarded to +`coretrace-compiler` and then to Clang. The analyzer enables instrumentation through the +`compilerlib::compile(..., instrument=true)` API, so `--instrument` is optional and ignored if it is +present after `--`. + +The initial collection is intentionally basic: it counts CoreTrace log lines, function entry/exit +events, allocation events, bounds errors, leak reports, vtable diagnostics, warnings, and errors. + +## Batch Test Directory Mode + +Use `--test-dir ` to run every `.c`, `.cc`, `.cpp`, and `.cxx` source under a test directory. +Each source is compiled and executed as a separate instrumented binary, which avoids linker +collisions between test files that each define `main`. + +Arguments after `--` are shared compiler/CoreTrace flags for every test file. Batch mode keeps going +after runtime failures and reports them at the end. Add `--strict-test-exit` if a non-zero test +binary exit should make the analyzer return a non-zero exit code. + +## Python Test Runner + +`BTP-RUNTIME-ANALYZER.py` compiles every C/C++ source in `test/`, verifies that each source has a +generated executable, then runs every binary with a 10 second timeout and prints stdout/stderr. +Before the directory sweep, it also runs a generated minimal C probe and asserts that +`runtime-analyzer` builds an instrumented binary, executes it, captures the program output, and +collects basic CoreTrace entry/exit lines. + +```zsh +python3 BTP-RUNTIME-ANALYZER.py +python3 BTP-RUNTIME-ANALYZER.py --timeout 10 -- --ct-no-alloc-trace --ct-no-trace --ct-bounds-no-abort +``` + +Non-zero binary exits are reported without failing the script by default, because some runtime tests +intentionally abort. Use `--strict-exit` to fail on any non-zero binary exit. + +## F4 Minimal Runtime Analyzer Proof + +`BTP-RUNTIME-ANALYZER_F4.py` proves minimal execution of an instrumented binary and basic runtime +collection through `coretrace-runtime-analyzer`. It generates a small C probe in +`runtime-analyzer-artifacts/`, compiles it with `runtime-analyzer`, verifies that the instrumented +binary exists and is executable, then checks that the program output and CoreTrace entry/exit +collection were captured. + +```zsh +python3 BTP-RUNTIME-ANALYZER_F4.py +python3 BTP-RUNTIME-ANALYZER_F4.py --build-first +``` + +## F11 Bounds Overflow Proof + +`BTP-RUNTIME-ANALYZER_F11.py` proves runtime overflow detection through CoreTrace bounds +instrumentation. It uses an existing `ct_bounds_overflow` fixture if one is present in `test/`; +otherwise it generates a small heap-overflow probe in `runtime-analyzer-artifacts/`. + +```zsh +python3 BTP-RUNTIME-ANALYZER_F11.py +python3 BTP-RUNTIME-ANALYZER_F11.py --no-color +``` + +The proof expects a generated instrumented binary, a runtime `heap-buffer-overflow` report, and a +non-zero `bounds_errors` collection count. Status checks print the tested file name in purple, +green `OK` for validated conditions, red `NO` for missing conditions, and separate file reports +with `--------`. ## Code style (clang-format) From 3980fb328e2992948870bbbe82e65382816bfd9c Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 15 Jul 2026 17:10:37 +0200 Subject: [PATCH 4/8] test(runtime): add batch analyzer validation script --- BTP-RUNTIME-ANALYZER.py | 507 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100755 BTP-RUNTIME-ANALYZER.py diff --git a/BTP-RUNTIME-ANALYZER.py b/BTP-RUNTIME-ANALYZER.py new file mode 100755 index 0000000..f4a24c9 --- /dev/null +++ b/BTP-RUNTIME-ANALYZER.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import argparse +import os +import re +import shlex +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, Sequence + + +SOURCE_EXTENSIONS = {".c", ".cc", ".cpp", ".cxx"} +BATCH_RECORD_RE = re.compile( + r"^runtime-analyzer: \[(?P[A-Z]+)\] " + r"(?P.*?) exit_code=(?P-?\d+) " + r"coretrace_lines=(?P\d+) binary=(?P.*)$" +) +COLLECTION_RECORD_RE = re.compile(r"^\s*(?P[a-z_]+)=(?P\d+)$") +PROOF_MARKER = "coretrace-runtime-analyzer-proof" +PROOF_SOURCE = f"""\ +#include + +static int proof_helper(int value) {{ + return value + 1; +}} + +int main(void) {{ + puts("{PROOF_MARKER}"); + return proof_helper(40) == 41 ? 0 : 1; +}} +""" + + +@dataclass(frozen=True) +class CompiledBinary: + source: Path + binary: Path + status: str + exit_code: int + coretrace_lines: int + + +@dataclass +class ScriptResult: + proof_failures: int = 0 + compile_failures: int = 0 + missing_binaries: int = 0 + non_executable_binaries: int = 0 + timeouts: int = 0 + nonzero_exits: int = 0 + + def failed(self, strict_exit: bool) -> bool: + return ( + self.proof_failures > 0 + or self.compile_failures > 0 + or self.missing_binaries > 0 + or self.non_executable_binaries > 0 + or self.timeouts > 0 + or (strict_exit and self.nonzero_exits > 0) + ) + + +def split_compiler_args(argv: Sequence[str]) -> tuple[list[str], list[str]]: + if "--" not in argv: + return list(argv), [] + index = argv.index("--") + return list(argv[:index]), list(argv[index + 1 :]) + + +def parse_args(argv: Sequence[str]) -> tuple[argparse.Namespace, list[str]]: + analyzer_args, compiler_args = split_compiler_args(argv) + parser = argparse.ArgumentParser( + description=( + "Compile every C/C++ source in TEST with runtime-analyzer, verify the " + "generated binaries, and execute each binary with a timeout." + ) + ) + parser.add_argument( + "--test-dir", + default="test", + help="Directory containing test files. Default: test", + ) + parser.add_argument( + "--runtime-analyzer", + default="build/runtime-analyzer", + help="Path to the runtime-analyzer executable. Default: build/runtime-analyzer", + ) + parser.add_argument( + "--output-dir", + default="runtime-analyzer-artifacts/btp-runtime-analyzer", + help="Directory where generated test binaries are written.", + ) + parser.add_argument( + "--timeout", + type=float, + default=10.0, + help="Execution timeout in seconds for each generated binary. Default: 10", + ) + parser.add_argument( + "--build-first", + action="store_true", + help="Run cmake --build build --target runtime-analyzer before testing.", + ) + parser.add_argument( + "--skip-proof", + action="store_true", + help="Skip the minimal runtime-analyzer proof case.", + ) + parser.add_argument( + "--strict-exit", + action="store_true", + help="Fail when a generated binary exits with a non-zero status.", + ) + parser.add_argument( + "--show-compile-output", + action="store_true", + help="Print runtime-analyzer compilation stdout/stderr.", + ) + return parser.parse_args(analyzer_args), compiler_args + + +def repo_root() -> Path: + return Path(__file__).resolve().parent + + +def resolve_under_repo(root: Path, value: str) -> Path: + path = Path(value) + if not path.is_absolute(): + path = root / path + return path.resolve() + + +def resolve_directory_case_insensitive(path: Path) -> Path: + if path.exists(): + return path + + parent = path.parent + if not parent.exists(): + return path + + requested = path.name.lower() + for child in parent.iterdir(): + if child.name.lower() == requested and child.is_dir(): + return child.resolve() + return path + + +def relative_to_root(root: Path, path: Path) -> str: + try: + return path.resolve().relative_to(root).as_posix() + except ValueError: + return path.as_posix() + + +def discover_sources(test_dir: Path) -> list[Path]: + return sorted( + path.resolve() + for path in test_dir.rglob("*") + if path.is_file() and path.suffix in SOURCE_EXTENSIONS + ) + + +def discover_non_sources(test_dir: Path, sources: Iterable[Path]) -> list[Path]: + source_set = {source.resolve() for source in sources} + return sorted( + path.resolve() + for path in test_dir.rglob("*") + if path.is_file() and path.resolve() not in source_set + ) + + +def print_stream(title: str, text: str | bytes | None) -> None: + if isinstance(text, bytes): + text = text.decode(errors="replace") + if text is None: + text = "" + + print(f"--- {title} ---") + if text: + print(text, end="" if text.endswith("\n") else "\n") + else: + print("") + + +def run_checked(command: Sequence[str], cwd: Path) -> subprocess.CompletedProcess[str]: + print(f"$ {shlex.join(command)}") + return subprocess.run( + command, + cwd=cwd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + +def build_runtime_analyzer(root: Path) -> bool: + completed = run_checked( + ["cmake", "--build", "build", "--target", "runtime-analyzer"], root + ) + if completed.stdout: + print_stream("build stdout", completed.stdout) + if completed.stderr: + print_stream("build stderr", completed.stderr) + return completed.returncode == 0 + + +def parse_collection_summary(output: str) -> dict[str, int]: + summary: dict[str, int] = {} + for line in output.splitlines(): + match = COLLECTION_RECORD_RE.match(line) + if match: + summary[match.group("name")] = int(match.group("value")) + return summary + + +def run_minimal_runtime_analyzer_proof( + root: Path, + runtime_analyzer: Path, + output_dir: Path, + timeout: float, + result: ScriptResult, +) -> None: + print("\n===== PROOF Minimal execution and basic collection =====") + proof_dir = output_dir / "proof" + proof_dir.mkdir(parents=True, exist_ok=True) + proof_source = proof_dir / "minimal_execution_probe.c" + proof_binary = proof_dir / "minimal_execution_probe" + proof_source.write_text(PROOF_SOURCE, encoding="utf-8") + + command = [ + str(runtime_analyzer), + "-o", + str(proof_binary), + "--show-output", + "--", + str(proof_source), + ] + print(f"$ {shlex.join(command)}") + + try: + completed = subprocess.run( + command, + cwd=root, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired as error: + result.proof_failures += 1 + print("[PROOF] FAIL: runtime-analyzer timed out") + print_stream("proof stdout", error.stdout) + print_stream("proof stderr", error.stderr) + return + + print_stream("proof stdout", completed.stdout) + print_stream("proof stderr", completed.stderr) + + collection = parse_collection_summary(completed.stdout) + checks = { + "runtime_analyzer_exit_zero": completed.returncode == 0, + "binary_generated": proof_binary.is_file(), + "binary_executable": os.access(proof_binary, os.X_OK), + "program_output_captured": PROOF_MARKER in completed.stdout, + "coretrace_lines_collected": collection.get("coretrace_lines", 0) > 0, + "entry_events_collected": collection.get("entry_events", 0) > 0, + "exit_events_collected": collection.get("exit_events", 0) > 0, + } + + for name, passed in checks.items(): + print(f"[PROOF] {name}={'ok' if passed else 'fail'}") + + print( + "[PROOF] collection " + f"coretrace_lines={collection.get('coretrace_lines', 0)} " + f"entry_events={collection.get('entry_events', 0)} " + f"exit_events={collection.get('exit_events', 0)}" + ) + + if all(checks.values()): + print( + "[PROOF] PASS: Minimal execution of an instrumented binary and " + "basic collection (WIP) via coretrace-runtime-analyzer." + ) + return + + result.proof_failures += 1 + print( + "[PROOF] FAIL: Minimal execution of an instrumented binary and " + "basic collection was not proven." + ) + + +def parse_batch_records(root: Path, output: str) -> dict[Path, CompiledBinary]: + records: dict[Path, CompiledBinary] = {} + for line in output.splitlines(): + match = BATCH_RECORD_RE.match(line) + if not match: + continue + + source = Path(match.group("source")) + if not source.is_absolute(): + source = root / source + + binary = Path(match.group("binary")) + if not binary.is_absolute(): + binary = root / binary + + source = source.resolve() + records[source] = CompiledBinary( + source=source, + binary=binary.resolve(), + status=match.group("status"), + exit_code=int(match.group("exit_code")), + coretrace_lines=int(match.group("coretrace_lines")), + ) + return records + + +def compile_sources( + root: Path, + runtime_analyzer: Path, + test_dir: Path, + output_dir: Path, + compiler_args: Sequence[str], + show_compile_output: bool, +) -> tuple[int, dict[Path, CompiledBinary]]: + output_dir.mkdir(parents=True, exist_ok=True) + command = [ + str(runtime_analyzer), + "--test-dir", + str(test_dir), + "--output-dir", + str(output_dir), + "--no-run", + "--", + *compiler_args, + ] + completed = run_checked(command, root) + + if show_compile_output or completed.returncode != 0: + print_stream("compile stdout", completed.stdout) + print_stream("compile stderr", completed.stderr) + + records = parse_batch_records(root, completed.stdout) + return completed.returncode, records + + +def verify_binaries( + root: Path, + sources: Sequence[Path], + records: dict[Path, CompiledBinary], + result: ScriptResult, +) -> list[CompiledBinary]: + verified: list[CompiledBinary] = [] + for source in sources: + record = records.get(source) + if record is None: + print(f"[COMPILE] {relative_to_root(root, source)}: no batch record emitted") + result.compile_failures += 1 + continue + + if record.status != "PASS": + print( + f"[COMPILE] {relative_to_root(root, source)}: " + f"runtime-analyzer status={record.status}" + ) + result.compile_failures += 1 + continue + + if not record.binary.is_file(): + print( + f"[MISSING] {relative_to_root(root, source)}: " + f"binary not found at {record.binary}" + ) + result.missing_binaries += 1 + continue + + if not os.access(record.binary, os.X_OK): + print( + f"[NOT-EXECUTABLE] {relative_to_root(root, source)}: " + f"binary is not executable at {record.binary}" + ) + result.non_executable_binaries += 1 + continue + + print( + f"[BINARY] {relative_to_root(root, source)} -> " + f"{relative_to_root(root, record.binary)}" + ) + verified.append(record) + + return verified + + +def execute_binary( + root: Path, record: CompiledBinary, timeout: float, result: ScriptResult +) -> None: + label = relative_to_root(root, record.source) + print(f"\n===== EXEC {label} =====") + print(f"binary: {record.binary}") + print(f"timeout: {timeout:g}s") + + try: + completed = subprocess.run( + [str(record.binary)], + cwd=root, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired as error: + result.timeouts += 1 + print("[TIMEOUT]") + print_stream("stdout", error.stdout) + print_stream("stderr", error.stderr) + return + + print(f"exit_code: {completed.returncode}") + if completed.returncode != 0: + result.nonzero_exits += 1 + print_stream("stdout", completed.stdout) + print_stream("stderr", completed.stderr) + + +def main(argv: Sequence[str]) -> int: + args, compiler_args = parse_args(argv) + root = repo_root() + test_dir = resolve_directory_case_insensitive( + resolve_under_repo(root, args.test_dir) + ) + runtime_analyzer = resolve_under_repo(root, args.runtime_analyzer) + output_dir = resolve_under_repo(root, args.output_dir) + + if args.build_first and not build_runtime_analyzer(root): + return 1 + + if not test_dir.is_dir(): + print(f"error: test directory not found: {test_dir}", file=sys.stderr) + return 1 + if not runtime_analyzer.is_file(): + print( + f"error: runtime-analyzer not found: {runtime_analyzer}\n" + "run `cmake --build build --target runtime-analyzer` or pass " + "--runtime-analyzer ", + file=sys.stderr, + ) + return 1 + + sources = discover_sources(test_dir) + skipped = discover_non_sources(test_dir, sources) + print(f"test_dir: {test_dir}") + print(f"runtime_analyzer: {runtime_analyzer}") + print(f"output_dir: {output_dir}") + print(f"source_files: {len(sources)}") + print(f"non_source_files_skipped: {len(skipped)}") + + if not sources: + print("error: no C/C++ source files found", file=sys.stderr) + return 1 + + result = ScriptResult() + if not args.skip_proof: + run_minimal_runtime_analyzer_proof( + root, runtime_analyzer, output_dir, args.timeout, result + ) + + compile_exit, records = compile_sources( + root, + runtime_analyzer, + test_dir, + output_dir, + compiler_args, + args.show_compile_output, + ) + if compile_exit != 0: + result.compile_failures += 1 + + verified = verify_binaries(root, sources, records, result) + for record in verified: + execute_binary(root, record, args.timeout, result) + + print("\n===== SUMMARY =====") + print(f"proof_failures={result.proof_failures}") + print(f"sources={len(sources)}") + print(f"binaries_verified={len(verified)}") + print(f"compile_failures={result.compile_failures}") + print(f"missing_binaries={result.missing_binaries}") + print(f"non_executable_binaries={result.non_executable_binaries}") + print(f"timeouts={result.timeouts}") + print(f"nonzero_exits={result.nonzero_exits}") + print(f"strict_exit={str(args.strict_exit).lower()}") + + return 1 if result.failed(args.strict_exit) else 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) From ef2ca00ed6637eb1ba11987f0883d2c1f32cad0a Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 15 Jul 2026 17:10:57 +0200 Subject: [PATCH 5/8] test(runtime): add F4 minimal execution proof --- BTP-RUNTIME-ANALYZER_F4.py | 372 +++++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100755 BTP-RUNTIME-ANALYZER_F4.py diff --git a/BTP-RUNTIME-ANALYZER_F4.py b/BTP-RUNTIME-ANALYZER_F4.py new file mode 100755 index 0000000..af7ea7b --- /dev/null +++ b/BTP-RUNTIME-ANALYZER_F4.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import argparse +import os +import re +import shlex +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Sequence + + +COLLECTION_RECORD_RE = re.compile(r"^\s*(?P[a-z_]+)=(?P\d+)$") +RUNTIME_EXIT_RE = re.compile(r"^runtime-analyzer:\s+exit_code=(?P-?\d+)$") +ANSI_GREEN = "\033[32m" +ANSI_PURPLE = "\033[35m" +ANSI_RED = "\033[31m" +ANSI_RESET = "\033[0m" +SEPARATOR = "--------" +F4_MARKER = "coretrace-runtime-analyzer-f4" +F4_SOURCE = f"""\ +#include + +static int f4_helper(int value) +{{ + return value + 1; +}} + +int main(void) +{{ + puts("{F4_MARKER}"); + return f4_helper(40) == 41 ? 0 : 1; +}} +""" + + +@dataclass(frozen=True) +class ProbeSource: + path: Path + mode: str + + +@dataclass +class F4Result: + use_color: bool + source_name: str + failures: int = 0 + + def fail(self, message: str) -> None: + self.failures += 1 + print(f"[F4] {self.file_label()} {message}={status_label(False, self.use_color)}") + + def ok(self, message: str) -> None: + print(f"[F4] {self.file_label()} {message}={status_label(True, self.use_color)}") + + def file_label(self) -> str: + return colorize(self.source_name, ANSI_PURPLE, self.use_color) + + +def colorize(text: str, color: str, enabled: bool) -> str: + if not enabled: + return text + return f"{color}{text}{ANSI_RESET}" + + +def status_label(passed: bool, use_color: bool) -> str: + if passed: + return colorize("OK", ANSI_GREEN, use_color) + return colorize("NO", ANSI_RED, use_color) + + +def should_use_color(disabled: bool) -> bool: + return not disabled + + +def print_separator() -> None: + print(SEPARATOR) + + +def print_file_header(path: Path, use_color: bool) -> None: + print_separator() + print(f"[F4] file={colorize(path.name, ANSI_PURPLE, use_color)}") + print_separator() + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Prove minimal execution of an instrumented binary and basic " + "collection through coretrace-runtime-analyzer." + ) + ) + parser.add_argument( + "--runtime-analyzer", + default="build/runtime-analyzer", + help="Path to the runtime-analyzer executable. Default: build/runtime-analyzer", + ) + parser.add_argument( + "--source", + help=( + "Explicit C/C++ source to compile instead of the generated F4 probe. " + "When omitted, a minimal proof source is generated." + ), + ) + parser.add_argument( + "--expected-output", + help=( + "Output marker expected in the instrumented program output. " + "Defaults to the generated F4 marker only when --source is omitted." + ), + ) + parser.add_argument( + "--output-dir", + default="runtime-analyzer-artifacts/btp-runtime-analyzer-f4", + help="Directory where generated F4 artifacts are written.", + ) + parser.add_argument( + "--timeout", + type=float, + default=10.0, + help="Execution timeout in seconds for runtime-analyzer. Default: 10", + ) + parser.add_argument( + "--build-first", + action="store_true", + help="Run cmake --build build --target runtime-analyzer before testing.", + ) + parser.add_argument( + "--no-color", + action="store_true", + help="Disable colored OK/NO status output.", + ) + return parser.parse_args(argv) + + +def repo_root() -> Path: + return Path(__file__).resolve().parent + + +def resolve_under_repo(root: Path, value: str) -> Path: + path = Path(value) + if not path.is_absolute(): + path = root / path + return path.resolve() + + +def print_stream(title: str, text: str | bytes | None) -> None: + if isinstance(text, bytes): + text = text.decode(errors="replace") + if text is None: + text = "" + + print(f"--- {title} ---") + if text: + print(text, end="" if text.endswith("\n") else "\n") + else: + print("") + + +def parse_collection_summary(output: str) -> dict[str, int]: + summary: dict[str, int] = {} + for line in output.splitlines(): + match = COLLECTION_RECORD_RE.match(line) + if match: + summary[match.group("name")] = int(match.group("value")) + return summary + + +def parse_runtime_exit_code(output: str) -> int | None: + for line in output.splitlines(): + match = RUNTIME_EXIT_RE.match(line) + if match: + return int(match.group("value")) + return None + + +def run_command( + command: Sequence[str], cwd: Path, timeout: float | None = None +) -> subprocess.CompletedProcess[str] | subprocess.TimeoutExpired[str]: + print(f"$ {shlex.join(command)}") + try: + return subprocess.run( + command, + cwd=cwd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired as error: + return error + + +def build_runtime_analyzer(root: Path, use_color: bool) -> bool: + completed = run_command( + ["cmake", "--build", "build", "--target", "runtime-analyzer"], root + ) + if isinstance(completed, subprocess.TimeoutExpired): + print(f"[F4] build_timeout={status_label(False, use_color)}") + return False + + if completed.stdout: + print_stream("build stdout", completed.stdout) + if completed.stderr: + print_stream("build stderr", completed.stderr) + return completed.returncode == 0 + + +def select_probe_source(root: Path, args: argparse.Namespace, output_dir: Path) -> ProbeSource: + if args.source: + return ProbeSource(resolve_under_repo(root, args.source), "explicit") + + probe_dir = output_dir / "proof" + probe_dir.mkdir(parents=True, exist_ok=True) + probe_source = probe_dir / "minimal_execution_probe.c" + probe_source.write_text(F4_SOURCE, encoding="utf-8") + return ProbeSource(probe_source.resolve(), "generated") + + +def expected_output_for(args: argparse.Namespace, probe: ProbeSource) -> str | None: + if args.expected_output is not None: + return args.expected_output + if probe.mode == "generated": + return F4_MARKER + return None + + +def run_f4_proof( + root: Path, + runtime_analyzer: Path, + probe: ProbeSource, + output_dir: Path, + timeout: float, + expected_output: str | None, + use_color: bool, +) -> int: + result = F4Result(use_color=use_color, source_name=probe.path.name) + binary = output_dir / f"{probe.path.stem}.instrumented" + command = [ + str(runtime_analyzer), + "-o", + str(binary), + "--show-events", + "--show-output", + "--", + str(probe.path), + ] + + print_file_header(probe.path, use_color) + print(f"[F4] source_mode={probe.mode}") + print(f"[F4] binary={binary}") + print_separator() + completed = run_command(command, root, timeout=timeout) + print_separator() + + if isinstance(completed, subprocess.TimeoutExpired): + print_stream("runtime-analyzer stdout", completed.stdout) + print_stream("runtime-analyzer stderr", completed.stderr) + result.fail("runtime_analyzer_timeout") + result.fail("runtime_analyzer_exit_zero") + result.fail("instrumented_binary_generated") + result.fail("instrumented_binary_executable") + return result.failures + + print_stream("runtime-analyzer stdout", completed.stdout) + print_stream("runtime-analyzer stderr", completed.stderr) + print_separator() + + combined_output = f"{completed.stdout}\n{completed.stderr}" + collection = parse_collection_summary(completed.stdout) + runtime_exit_code = parse_runtime_exit_code(completed.stdout) + + if completed.returncode == 0: + result.ok("runtime_analyzer_exit_zero") + else: + result.fail("runtime_analyzer_exit_zero") + + if binary.is_file(): + result.ok("instrumented_binary_generated") + else: + result.fail("instrumented_binary_generated") + + if binary.is_file() and os.access(binary, os.X_OK): + result.ok("instrumented_binary_executable") + else: + result.fail("instrumented_binary_executable") + + if runtime_exit_code == 0: + result.ok("instrumented_program_exit_zero") + else: + result.fail("instrumented_program_exit_zero") + + if expected_output is None: + print(f"[F4] {result.file_label()} program_output_check=skipped") + elif expected_output in combined_output: + result.ok("program_output_captured") + else: + result.fail("program_output_captured") + + print_separator() + print("--- collection summary ---") + for key in ( + "coretrace_lines", + "entry_events", + "exit_events", + "allocation_events", + "bounds_errors", + "leak_reports", + "vtable_events", + "warnings", + "errors", + ): + print(f"{key}={collection.get(key, 0)}") + print_separator() + + if collection.get("coretrace_lines", 0) > 0: + result.ok("coretrace_lines_collected") + else: + result.fail("coretrace_lines_collected") + + if collection.get("entry_events", 0) > 0: + result.ok("entry_events_collected") + else: + result.fail("entry_events_collected") + + if collection.get("exit_events", 0) > 0: + result.ok("exit_events_collected") + else: + result.fail("exit_events_collected") + + return result.failures + + +def main(argv: Sequence[str]) -> int: + args = parse_args(argv) + root = repo_root() + use_color = should_use_color(args.no_color) + output_dir = resolve_under_repo(root, args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + runtime_analyzer = resolve_under_repo(root, args.runtime_analyzer) + + if args.build_first and not build_runtime_analyzer(root, use_color): + return 1 + + probe = select_probe_source(root, args, output_dir) + expected_output = expected_output_for(args, probe) + failures = run_f4_proof( + root=root, + runtime_analyzer=runtime_analyzer, + probe=probe, + output_dir=output_dir, + timeout=args.timeout, + expected_output=expected_output, + use_color=use_color, + ) + + print_separator() + if failures == 0: + print(f"[F4] result={status_label(True, use_color)}") + return 0 + + print(f"[F4] result={status_label(False, use_color)} failures={failures}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) From 601e35aa93a5ac748bdf2e744a8db48b7ac62f10 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 15 Jul 2026 17:11:09 +0200 Subject: [PATCH 6/8] test(runtime): add F11 bounds overflow proof --- BTP-RUNTIME-ANALYZER_F11.py | 356 ++++++++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100755 BTP-RUNTIME-ANALYZER_F11.py diff --git a/BTP-RUNTIME-ANALYZER_F11.py b/BTP-RUNTIME-ANALYZER_F11.py new file mode 100755 index 0000000..6b9eb52 --- /dev/null +++ b/BTP-RUNTIME-ANALYZER_F11.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import argparse +import os +import re +import shlex +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Sequence + + +COLLECTION_RECORD_RE = re.compile(r"^\s*(?P[a-z_]+)=(?P\d+)$") +ANSI_GREEN = "\033[32m" +ANSI_PURPLE = "\033[35m" +ANSI_RED = "\033[31m" +ANSI_RESET = "\033[0m" +SEPARATOR = "--------" +F11_BEFORE_MARKER = "coretrace-runtime-analyzer-f11-before-overflow" +F11_AFTER_MARKER = "coretrace-runtime-analyzer-f11-after-overflow" +F11_SOURCE = f"""\ +#include +#include + +int main(void) +{{ + volatile unsigned char* buffer = (volatile unsigned char*)malloc(8); + if (buffer == NULL) + {{ + return 2; + }} + + for (unsigned long i = 0; i < 8; ++i) + {{ + buffer[i] = (unsigned char)i; + }} + + puts("{F11_BEFORE_MARKER}"); + buffer[8] = 0xF1; + puts("{F11_AFTER_MARKER}"); + + free((void*)buffer); + return 0; +}} +""" + + +@dataclass(frozen=True) +class ProbeSource: + path: Path + mode: str + + +@dataclass +class F11Result: + use_color: bool + source_name: str + failures: int = 0 + + def fail(self, message: str) -> None: + self.failures += 1 + print(f"[F11] {self.file_label()} {message}={status_label(False, self.use_color)}") + + def ok(self, message: str) -> None: + print(f"[F11] {self.file_label()} {message}={status_label(True, self.use_color)}") + + def file_label(self) -> str: + return colorize(self.source_name, ANSI_PURPLE, self.use_color) + + +def colorize(text: str, color: str, enabled: bool) -> str: + if not enabled: + return text + return f"{color}{text}{ANSI_RESET}" + + +def status_label(passed: bool, use_color: bool) -> str: + if passed: + return colorize("OK", ANSI_GREEN, use_color) + return colorize("NO", ANSI_RED, use_color) + + +def should_use_color(disabled: bool) -> bool: + return not disabled + + +def print_separator() -> None: + print(SEPARATOR) + + +def print_file_header(path: Path, use_color: bool) -> None: + print_separator() + print(f"[F11] file={colorize(path.name, ANSI_PURPLE, use_color)}") + print_separator() + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Prove overflow detection at runtime via bounds instrumentation " + "through coretrace-runtime-analyzer." + ) + ) + parser.add_argument( + "--runtime-analyzer", + default="build/runtime-analyzer", + help="Path to the runtime-analyzer executable. Default: build/runtime-analyzer", + ) + parser.add_argument( + "--test-dir", + default="test", + help="Directory searched for an existing ct_bounds_overflow fixture.", + ) + parser.add_argument( + "--source", + help="Explicit C/C++ overflow probe source to compile instead of auto-selection.", + ) + parser.add_argument( + "--output-dir", + default="runtime-analyzer-artifacts/btp-runtime-analyzer-f11", + help="Directory where generated F11 artifacts are written.", + ) + parser.add_argument( + "--timeout", + type=float, + default=10.0, + help="Execution timeout in seconds for runtime-analyzer. Default: 10", + ) + parser.add_argument( + "--build-first", + action="store_true", + help="Run cmake --build build --target runtime-analyzer before testing.", + ) + parser.add_argument( + "--no-color", + action="store_true", + help="Disable colored OK/NO status output.", + ) + return parser.parse_args(argv) + + +def repo_root() -> Path: + return Path(__file__).resolve().parent + + +def resolve_under_repo(root: Path, value: str) -> Path: + path = Path(value) + if not path.is_absolute(): + path = root / path + return path.resolve() + + +def print_stream(title: str, text: str | bytes | None) -> None: + if isinstance(text, bytes): + text = text.decode(errors="replace") + if text is None: + text = "" + + print(f"--- {title} ---") + if text: + print(text, end="" if text.endswith("\n") else "\n") + else: + print("") + + +def parse_collection_summary(output: str) -> dict[str, int]: + summary: dict[str, int] = {} + for line in output.splitlines(): + match = COLLECTION_RECORD_RE.match(line) + if match: + summary[match.group("name")] = int(match.group("value")) + return summary + + +def run_command( + command: Sequence[str], cwd: Path, timeout: float | None = None +) -> subprocess.CompletedProcess[str] | subprocess.TimeoutExpired[str]: + print(f"$ {shlex.join(command)}") + try: + return subprocess.run( + command, + cwd=cwd, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired as error: + return error + + +def build_runtime_analyzer(root: Path, use_color: bool) -> bool: + completed = run_command( + ["cmake", "--build", "build", "--target", "runtime-analyzer"], root + ) + if isinstance(completed, subprocess.TimeoutExpired): + print(f"[F11] build_timeout={status_label(False, use_color)}") + return False + + if completed.stdout: + print_stream("build stdout", completed.stdout) + if completed.stderr: + print_stream("build stderr", completed.stderr) + return completed.returncode == 0 + + +def find_existing_bounds_fixture(test_dir: Path) -> Path | None: + candidates = [ + test_dir / "ct_bounds_overflow.c", + test_dir / "ct_bounds_overflow.cpp", + test_dir / "ct_bounds_heap_overflow.c", + test_dir / "ct_bounds_heap_overflow.cpp", + test_dir / "ct_heap_buffer_overflow.c", + test_dir / "ct_heap_buffer_overflow.cpp", + ] + for candidate in candidates: + if candidate.is_file(): + return candidate.resolve() + return None + + +def select_probe_source(root: Path, args: argparse.Namespace, output_dir: Path) -> ProbeSource: + if args.source: + return ProbeSource(resolve_under_repo(root, args.source), "explicit") + + test_dir = resolve_under_repo(root, args.test_dir) + existing = find_existing_bounds_fixture(test_dir) + if existing is not None: + return ProbeSource(existing, "existing-test") + + probe_dir = output_dir / "proof" + probe_dir.mkdir(parents=True, exist_ok=True) + probe_source = probe_dir / "ct_bounds_overflow_probe.c" + probe_source.write_text(F11_SOURCE, encoding="utf-8") + return ProbeSource(probe_source.resolve(), "generated") + + +def run_f11_proof( + root: Path, + runtime_analyzer: Path, + probe: ProbeSource, + output_dir: Path, + timeout: float, + use_color: bool, +) -> int: + result = F11Result(use_color=use_color, source_name=probe.path.name) + binary = output_dir / "ct_bounds_overflow_probe" + command = [ + str(runtime_analyzer), + "-o", + str(binary), + "--show-events", + "--show-output", + "--", + "--ct-bounds", + "--ct-bounds-no-abort", + str(probe.path), + ] + + print_file_header(probe.path, use_color) + print("===== F11 Overflow detection via bounds instrumentation =====") + print(f"probe_source: {probe.path}") + print(f"probe_source_mode: {probe.mode}") + print(f"output_binary: {binary}") + print(f"timeout: {timeout:g}s") + print_separator() + + completed = run_command(command, root, timeout) + if isinstance(completed, subprocess.TimeoutExpired): + result.fail("runtime_analyzer_timeout") + print_stream("runtime-analyzer stdout", completed.stdout) + print_stream("runtime-analyzer stderr", completed.stderr) + return 1 + + print_stream("runtime-analyzer stdout", completed.stdout) + print_stream("runtime-analyzer stderr", completed.stderr) + print_separator() + + combined_output = completed.stdout + completed.stderr + collection = parse_collection_summary(completed.stdout) + + checks = { + "runtime_analyzer_exit_zero": completed.returncode == 0, + "binary_generated": binary.is_file(), + "binary_executable": os.access(binary, os.X_OK), + "program_reached_overflow_site": F11_BEFORE_MARKER in combined_output, + "program_continued_after_detection": F11_AFTER_MARKER in combined_output, + "heap_buffer_overflow_reported": "heap-buffer-overflow" in combined_output, + "write_overflow_reported": "WRITE" in combined_output, + "bounds_errors_collected": collection.get("bounds_errors", 0) > 0, + "coretrace_lines_collected": collection.get("coretrace_lines", 0) > 0, + } + + for name, passed in checks.items(): + if passed: + result.ok(name) + else: + result.fail(name) + print_separator() + + print( + f"[F11] {result.file_label()} collection " + f"coretrace_lines={collection.get('coretrace_lines', 0)} " + f"bounds_errors={collection.get('bounds_errors', 0)} " + f"errors={collection.get('errors', 0)}" + ) + + if result.failures == 0: + print( + "[F11] PASS: Overflow detection at runtime via bounds " + "instrumentation via coretrace-runtime-analyzer." + ) + return 0 + + print( + "[F11] FAIL: Overflow detection at runtime via bounds instrumentation " + "was not proven." + ) + return 1 + + +def main(argv: Sequence[str]) -> int: + args = parse_args(argv) + root = repo_root() + runtime_analyzer = resolve_under_repo(root, args.runtime_analyzer) + output_dir = resolve_under_repo(root, args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + use_color = should_use_color(args.no_color) + + if args.build_first and not build_runtime_analyzer(root, use_color): + return 1 + + if not runtime_analyzer.is_file(): + print( + f"error: runtime-analyzer not found: {runtime_analyzer}\n" + "run `cmake --build build --target runtime-analyzer` or pass " + "--runtime-analyzer ", + file=sys.stderr, + ) + return 1 + + probe = select_probe_source(root, args, output_dir) + if not probe.path.is_file(): + print(f"error: overflow probe source not found: {probe.path}", file=sys.stderr) + return 1 + + return run_f11_proof(root, runtime_analyzer, probe, output_dir, args.timeout, use_color) + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) From 6332c057de0caaed0ca6e709fe4db12496064daa Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 15 Jul 2026 17:11:36 +0200 Subject: [PATCH 7/8] docs(runtime): add runtime analyzer implementation issue --- issues/runtime-analyzer-implementation.md | 79 +++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 issues/runtime-analyzer-implementation.md diff --git a/issues/runtime-analyzer-implementation.md b/issues/runtime-analyzer-implementation.md new file mode 100644 index 0000000..fb2701a --- /dev/null +++ b/issues/runtime-analyzer-implementation.md @@ -0,0 +1,79 @@ +# Implement `runtime-analyzer` Minimal Execution Tool + +## Summary + +Implement the `runtime-analyzer` executable for `coretrace-runtime-analyzer`. + +The tool must use `coretrace-compiler` in library mode to compile an instrumented +C/C++ source, execute the generated binary, capture stdout/stderr, and produce a +basic runtime collection summary. + +## Scope + +- Build the executable as `runtime-analyzer`. +- Integrate `coretrace-compiler` through its library API. +- Compile forwarded C/C++ arguments into an instrumented executable. +- Execute the generated binary. +- Capture runtime stdout and stderr. +- Report the executed binary exit code. +- Collect and summarize basic CoreTrace runtime events: + - CoreTrace log lines + - function entry events + - function exit events + - allocation events + - bounds errors + - leak reports + - vtable diagnostics + - warnings and errors +- Support analyzer options for: + - output binary path + - runtime arguments + - environment variables + - event display + - captured output display +- Support `--test-dir` batch mode by compiling and running each source file as an + independent instrumented binary. + +## Architecture Notes + +The implementation should keep the CLI entry point small and delegate behavior to +a runtime analyzer module. Compiler integration, process execution, stream +capture, and event collection should remain separated so each responsibility can +evolve independently. + +The tool should call `coretrace-compiler` through the library API instead of +shelling out to a wrapper command. This keeps the integration generic, avoids +hardcoded compiler behavior, and makes the analyzer easier to reuse from tests or +future orchestration layers. + +Batch mode should compile each source independently rather than linking multiple +test files together. This avoids symbol collisions between files that each define +their own `main` function and keeps failures isolated per source. + +## Acceptance Criteria + +- `cmake --build build --target runtime-analyzer` builds successfully. +- `runtime-analyzer` compiles a C/C++ source into an instrumented binary. +- The generated binary exists and is executable. +- The tool runs the generated binary. +- The tool reports the instrumented binary exit code. +- The tool captures stdout and stderr from the executed binary. +- The tool prints a basic runtime collection summary. +- `--test-dir` discovers supported C/C++ sources and runs each one independently. +- Runtime failures in batch mode are reported without stopping the full batch. +- A strict mode exists for treating non-zero test binary exits as analyzer + failures. + +## Out of Scope + +- Python BTP validation scripts. +- Feature-specific proof scripts such as F4 or F11. +- Advanced report formats. +- Deep semantic analysis. +- A full runtime policy engine. + +## Proposed Commit Message + +```text +feat(runtime): implement minimal runtime analyzer execution +``` From c42f7b59d368c21db0895f69cf44ef9e8a4c58e5 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 15 Jul 2026 17:12:09 +0200 Subject: [PATCH 8/8] test: add some tests --- test/ct_alloc_basic.c | 19 + test/ct_alloc_growth.c | 32 ++ test/ct_autofree_aligned_alloc.c | 9 + test/ct_autofree_brk.c | 18 + test/ct_autofree_inttoptr.c | 15 + test/ct_autofree_inttoptr_escape.c | 18 + test/ct_autofree_local.c | 50 +++ test/ct_autofree_mmap.c | 15 + test/ct_autofree_new_nothrow.cpp | 10 + test/ct_autofree_posix_memalign.c | 13 + test/ct_autofree_ptrtoint.c | 14 + test/ct_autofree_ptrtoint_escape.c | 12 + test/ct_autofree_return_unused.c | 14 + test/ct_autofree_sbrk.c | 13 + test/ct_autofree_select.c | 12 + test/ct_autofree_select_escape.c | 16 + test/ct_new_delete.cpp | 12 + test/ct_new_delete_sized.cpp | 22 ++ test/ct_new_delete_variants.cpp | 19 + test/ct_realloc_zero.c | 10 + test/ct_shadow_pages.c | 26 ++ test/ct_vtable_basic.cpp | 28 ++ test/ct_vtable_diag_fake.cpp | 33 ++ test/ct_vtable_diag_freed.cpp | 34 ++ test/ct_vtable_diag_mismatch.cpp | 42 ++ test/ct_vtable_diag_null.cpp | 11 + test/ct_vtable_diag_stack_target.cpp | 35 ++ test/ct_vtable_interface.cpp | 25 ++ test/ct_vtable_multi.cpp | 41 ++ test/ct_vtable_uaf.cpp | 27 ++ test/ct_vtable_virtual_base.cpp | 43 ++ test/docker/Dockerfile | 49 +++ test/examples/fixtures/cpp_as_c.c | 8 + test/examples/fixtures/debug.c | 8 + test/examples/fixtures/hello.c | 7 + test/examples/fixtures/hello.cpp | 8 + test/examples/fixtures/vtable.cpp | 15 + test/examples/test_extern_project.py | 306 ++++++++++++++ test/examples/test_help_smoke.py | 66 ++++ test/examples/test_smoke.py | 571 +++++++++++++++++++++++++++ test/run_autofree_tests.sh | 152 +++++++ test/scripts/linux_compile.sh | 72 ++++ test/scripts/macos_compile.sh | 72 ++++ 43 files changed, 2022 insertions(+) create mode 100644 test/ct_alloc_basic.c create mode 100644 test/ct_alloc_growth.c create mode 100644 test/ct_autofree_aligned_alloc.c create mode 100644 test/ct_autofree_brk.c create mode 100644 test/ct_autofree_inttoptr.c create mode 100644 test/ct_autofree_inttoptr_escape.c create mode 100644 test/ct_autofree_local.c create mode 100644 test/ct_autofree_mmap.c create mode 100644 test/ct_autofree_new_nothrow.cpp create mode 100644 test/ct_autofree_posix_memalign.c create mode 100644 test/ct_autofree_ptrtoint.c create mode 100644 test/ct_autofree_ptrtoint_escape.c create mode 100644 test/ct_autofree_return_unused.c create mode 100644 test/ct_autofree_sbrk.c create mode 100644 test/ct_autofree_select.c create mode 100644 test/ct_autofree_select_escape.c create mode 100644 test/ct_new_delete.cpp create mode 100644 test/ct_new_delete_sized.cpp create mode 100644 test/ct_new_delete_variants.cpp create mode 100644 test/ct_realloc_zero.c create mode 100644 test/ct_shadow_pages.c create mode 100644 test/ct_vtable_basic.cpp create mode 100644 test/ct_vtable_diag_fake.cpp create mode 100644 test/ct_vtable_diag_freed.cpp create mode 100644 test/ct_vtable_diag_mismatch.cpp create mode 100644 test/ct_vtable_diag_null.cpp create mode 100644 test/ct_vtable_diag_stack_target.cpp create mode 100644 test/ct_vtable_interface.cpp create mode 100644 test/ct_vtable_multi.cpp create mode 100644 test/ct_vtable_uaf.cpp create mode 100644 test/ct_vtable_virtual_base.cpp create mode 100644 test/docker/Dockerfile create mode 100644 test/examples/fixtures/cpp_as_c.c create mode 100644 test/examples/fixtures/debug.c create mode 100644 test/examples/fixtures/hello.c create mode 100644 test/examples/fixtures/hello.cpp create mode 100644 test/examples/fixtures/vtable.cpp create mode 100644 test/examples/test_extern_project.py create mode 100644 test/examples/test_help_smoke.py create mode 100644 test/examples/test_smoke.py create mode 100755 test/run_autofree_tests.sh create mode 100644 test/scripts/linux_compile.sh create mode 100644 test/scripts/macos_compile.sh diff --git a/test/ct_alloc_basic.c b/test/ct_alloc_basic.c new file mode 100644 index 0000000..3ec388d --- /dev/null +++ b/test/ct_alloc_basic.c @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main(void) +{ + char* p = (char*)malloc(8); + if (p) + { + p[0] = 'a'; + } + free(p); + + char* q = (char*)calloc(4, 4); + q = (char*)realloc(q, 64); + free(q); + + malloc(16); // unreachable -> tracing-malloc-unreachable (+ autofree if enabled) + return 0; +} diff --git a/test/ct_alloc_growth.c b/test/ct_alloc_growth.c new file mode 100644 index 0000000..e63c658 --- /dev/null +++ b/test/ct_alloc_growth.c @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +int main(void) +{ + const size_t count = 70000; // > 2^16 to trigger table growth + void** ptrs = (void**)calloc(count, sizeof(void*)); + if (!ptrs) + { + printf("alloc for ptrs failed\n"); + return 1; + } + + size_t i = 0; + for (; i < count; ++i) + { + ptrs[i] = malloc(16); + if (!ptrs[i]) + { + printf("malloc failed at %zu\n", i); + break; + } + } + + for (size_t j = 0; j < i; ++j) + { + free(ptrs[j]); + } + free(ptrs); + return 0; +} diff --git a/test/ct_autofree_aligned_alloc.c b/test/ct_autofree_aligned_alloc.c new file mode 100644 index 0000000..0e27946 --- /dev/null +++ b/test/ct_autofree_aligned_alloc.c @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main(void) +{ + void* p = aligned_alloc(64, 256); + (void)p; + return 0; +} diff --git a/test/ct_autofree_brk.c b/test/ct_autofree_brk.c new file mode 100644 index 0000000..cb088e5 --- /dev/null +++ b/test/ct_autofree_brk.c @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main(void) +{ + void* cur = sbrk(0); + if (cur == (void*)-1) + { + return 0; + } + void* next = (char*)cur + 64; + if (brk(next) != 0) + { + return 0; + } + (void)brk(cur); + return 0; +} diff --git a/test/ct_autofree_inttoptr.c b/test/ct_autofree_inttoptr.c new file mode 100644 index 0000000..f595ff0 --- /dev/null +++ b/test/ct_autofree_inttoptr.c @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +int main(void) +{ + void* p = malloc(24); + uintptr_t x = (uintptr_t)p; + void* q = (void*)x; + if (q) + { + (void)q; + } + return 0; +} diff --git a/test/ct_autofree_inttoptr_escape.c b/test/ct_autofree_inttoptr_escape.c new file mode 100644 index 0000000..f608094 --- /dev/null +++ b/test/ct_autofree_inttoptr_escape.c @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +static volatile uintptr_t g_bits; +static volatile void* g_ptr; + +int main(void) +{ + void* p = malloc(24); + + g_bits = (uintptr_t)p; /* escape: stored globally as integer */ + + void* q = (void*)g_bits; /* inttoptr */ + g_ptr = q; /* escape: stored globally as pointer */ + + return g_ptr == NULL ? 0 : 1; +} diff --git a/test/ct_autofree_local.c b/test/ct_autofree_local.c new file mode 100644 index 0000000..9cc73c2 --- /dev/null +++ b/test/ct_autofree_local.c @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +void* global; +void* global2; + +static void foo(void) +{ + + void* ptr_normally_freed = malloc(sizeof(void*) * 2); + void* ptr_leaked = malloc(sizeof(void*) * 3); + void* test = malloc(sizeof(void*) * 4); + (void)ptr_leaked; + malloc(sizeof(void*)); + global = ptr_leaked; + global2 = ptr_normally_freed; + printf("%p\n", test); + // sleep(5); +} + +void c() +{ + foo(); +} + +void b() +{ + c(); + malloc(sizeof(void*)); + char* p = malloc(100); + char* q = p + 16; // pointeur “interior” +} + +void a() +{ + b(); + // sleep(5); +} + +int main(void) +{ + a(); + // printf("%p\n", global); + // printf("%p\n", global2); + // free(global); + // free(global2); + return 0; +} diff --git a/test/ct_autofree_mmap.c b/test/ct_autofree_mmap.c new file mode 100644 index 0000000..0fda063 --- /dev/null +++ b/test/ct_autofree_mmap.c @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +int main(void) +{ + size_t page = (size_t)sysconf(_SC_PAGESIZE); + void* p = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (p == MAP_FAILED) + { + return 0; + } + (void)p; + return 0; +} diff --git a/test/ct_autofree_new_nothrow.cpp b/test/ct_autofree_new_nothrow.cpp new file mode 100644 index 0000000..b8c4ad3 --- /dev/null +++ b/test/ct_autofree_new_nothrow.cpp @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main() +{ + int* p = new (std::nothrow) int(42); + new (std::nothrow) int(42); + (void)p; + return 0; +} diff --git a/test/ct_autofree_posix_memalign.c b/test/ct_autofree_posix_memalign.c new file mode 100644 index 0000000..3d61090 --- /dev/null +++ b/test/ct_autofree_posix_memalign.c @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main(void) +{ + void* p = NULL; + if (posix_memalign(&p, 64, 128) != 0) + { + return 0; + } + (void)p; + return 0; +} diff --git a/test/ct_autofree_ptrtoint.c b/test/ct_autofree_ptrtoint.c new file mode 100644 index 0000000..8c045a5 --- /dev/null +++ b/test/ct_autofree_ptrtoint.c @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +int main(void) +{ + void* p = malloc(24); + uintptr_t x = (uintptr_t)p; + if ((x & 1u) == 0u) + { + (void)x; + } + return 0; +} diff --git a/test/ct_autofree_ptrtoint_escape.c b/test/ct_autofree_ptrtoint_escape.c new file mode 100644 index 0000000..65ace11 --- /dev/null +++ b/test/ct_autofree_ptrtoint_escape.c @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +static volatile uintptr_t g_ptr_bits; + +int main(void) +{ + void* p = malloc(24); + g_ptr_bits = (uintptr_t)p; /* escape: stored globally */ + return g_ptr_bits == 0 ? 0 : 1; +} diff --git a/test/ct_autofree_return_unused.c b/test/ct_autofree_return_unused.c new file mode 100644 index 0000000..cdfd445 --- /dev/null +++ b/test/ct_autofree_return_unused.c @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +void* bar(void) +{ + return malloc(sizeof(void*)); +} + +int main(void) +{ + bar(); + free(bar()); + return 0; +} diff --git a/test/ct_autofree_sbrk.c b/test/ct_autofree_sbrk.c new file mode 100644 index 0000000..bd5ab71 --- /dev/null +++ b/test/ct_autofree_sbrk.c @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main(void) +{ + void* p = sbrk(64); + if (p == (void*)-1) + { + return 0; + } + (void)p; + return 0; +} diff --git a/test/ct_autofree_select.c b/test/ct_autofree_select.c new file mode 100644 index 0000000..cddc8de --- /dev/null +++ b/test/ct_autofree_select.c @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main(void) +{ + void* p = malloc(16); + void* q = malloc(32); + int cond = 1; + void* r = cond ? p : q; + (void)r; + return 0; +} diff --git a/test/ct_autofree_select_escape.c b/test/ct_autofree_select_escape.c new file mode 100644 index 0000000..be85226 --- /dev/null +++ b/test/ct_autofree_select_escape.c @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +static volatile void* g_ptr; + +int main(void) +{ + void* p = malloc(16); + int cond = 1; + void* r = cond ? p : NULL; + + g_ptr = r; /* escape: stored globally */ + + return g_ptr == NULL ? 0 : 1; +} diff --git a/test/ct_new_delete.cpp b/test/ct_new_delete.cpp new file mode 100644 index 0000000..d4c1436 --- /dev/null +++ b/test/ct_new_delete.cpp @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +int main() +{ + int* p = new int(7); + delete p; + + int* a = new int[4]; + delete[] a; + + new int; // unreachable -> tracing-new-unreachable (+ autofree if enabled) + return 0; +} diff --git a/test/ct_new_delete_sized.cpp b/test/ct_new_delete_sized.cpp new file mode 100644 index 0000000..a3c1c58 --- /dev/null +++ b/test/ct_new_delete_sized.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +int main() +{ + int* p = new int(7); + ::operator delete(p, sizeof(int)); + + int* a = new int[4]; + ::operator delete[](a, sizeof(int) * 4); + +#if defined(__cpp_aligned_new) + auto* q = new (std::align_val_t(64)) int(1); + ::operator delete(q, std::align_val_t(64)); + + auto* r = new (std::align_val_t(64)) int[2]; + ::operator delete[](r, sizeof(int) * 2, std::align_val_t(64)); +#endif + + return 0; +} diff --git a/test/ct_new_delete_variants.cpp b/test/ct_new_delete_variants.cpp new file mode 100644 index 0000000..153d7d9 --- /dev/null +++ b/test/ct_new_delete_variants.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +int main() +{ + int* p = new (std::nothrow) int(7); + ::operator delete(p, std::nothrow); + + int* a = new (std::nothrow) int[4]; + ::operator delete[](a, std::nothrow); + +#if defined(__cpp_lib_destroying_delete) && __cpp_lib_destroying_delete >= 201806L + int* d = new int(1); + ::operator delete(d, std::destroying_delete_t{}); +#endif + + return 0; +} diff --git a/test/ct_realloc_zero.c b/test/ct_realloc_zero.c new file mode 100644 index 0000000..114a852 --- /dev/null +++ b/test/ct_realloc_zero.c @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main(void) +{ + char* p = (char*)malloc(32); + p = (char*)realloc(p, 0); // free-like behavior per libc + (void)p; + return 0; +} diff --git a/test/ct_shadow_pages.c b/test/ct_shadow_pages.c new file mode 100644 index 0000000..df26f04 --- /dev/null +++ b/test/ct_shadow_pages.c @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +int main(void) +{ + const size_t page_size = 4096; + const size_t pages = 70000; // adjust down if memory is tight + const size_t size = pages * page_size; + + char* buf = (char*)malloc(size); + if (!buf) + { + printf("malloc failed\n"); + return 1; + } + + for (size_t i = 0; i < size; i += page_size) + { + buf[i] = (char)(i / page_size); + } + + free(buf); + return 0; +} diff --git a/test/ct_vtable_basic.cpp b/test/ct_vtable_basic.cpp new file mode 100644 index 0000000..fc41561 --- /dev/null +++ b/test/ct_vtable_basic.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +struct Base +{ + virtual ~Base() = default; + virtual int value() const + { + return 1; + } +}; + +struct Derived : Base +{ + int value() const override + { + return 2; + } +}; + +int main() +{ + Base* ptr = new Derived(); + int out = ptr->value(); + std::printf("value=%d\n", out); + delete ptr; + return 0; +} diff --git a/test/ct_vtable_diag_fake.cpp b/test/ct_vtable_diag_fake.cpp new file mode 100644 index 0000000..978e060 --- /dev/null +++ b/test/ct_vtable_diag_fake.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +extern "C" void __ct_vtable_dump(void* this_ptr, const char* site, const char* static_type); + +struct FakeObject +{ + void** vptr; +}; + +int main() +{ + std::puts("ct_vtable_diag_fake"); + + void** table = static_cast(std::calloc(4, sizeof(void*))); + if (!table) + { + return 1; + } + + table[0] = nullptr; // offset-to-top + table[1] = nullptr; // missing typeinfo + table[2] = nullptr; // fake entry + + FakeObject obj{}; + obj.vptr = &table[2]; // vtable pointer points to heap (unresolvable module) + + __ct_vtable_dump(&obj, "ct_vtable_diag_fake.cpp:22:5", "FakeObject"); + + std::free(table); + return 0; +} diff --git a/test/ct_vtable_diag_freed.cpp b/test/ct_vtable_diag_freed.cpp new file mode 100644 index 0000000..448a4c6 --- /dev/null +++ b/test/ct_vtable_diag_freed.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +extern "C" void __ct_vtable_dump(void* this_ptr, const char* site, const char* static_type); + +struct Base +{ + virtual ~Base() = default; + virtual int value() const + { + return 1; + } +}; + +struct Derived : Base +{ + int value() const override + { + return 2; + } +}; + +int main() +{ + std::puts("ct_vtable_diag_freed"); + + Derived* ptr = new Derived(); + delete ptr; + + // UB: intentionally using a freed pointer to trigger the diagnostic. + __ct_vtable_dump(ptr, "ct_vtable_diag_freed.cpp:22:5", "Derived"); + + return 0; +} diff --git a/test/ct_vtable_diag_mismatch.cpp b/test/ct_vtable_diag_mismatch.cpp new file mode 100644 index 0000000..cda1d05 --- /dev/null +++ b/test/ct_vtable_diag_mismatch.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +extern "C" void __ct_vcall_trace(void* this_ptr, void* target, const char* site, + const char* static_type); + +struct Base +{ + virtual ~Base() = default; + virtual int value() const + { + return 1; + } +}; + +struct Derived : Base +{ + int value() const override + { + return 2; + } +}; + +int main() +{ + std::puts("ct_vtable_diag_mismatch"); + + Derived obj; + Base* base = &obj; + + void* target = dlsym(RTLD_DEFAULT, "puts"); + if (!target) + { + std::puts("ct_vtable_diag_mismatch: dlsym failed"); + return 1; + } + + __ct_vcall_trace(base, target, "ct_vtable_diag_mismatch.cpp:24:5", "Base"); + + return 0; +} diff --git a/test/ct_vtable_diag_null.cpp b/test/ct_vtable_diag_null.cpp new file mode 100644 index 0000000..c315d49 --- /dev/null +++ b/test/ct_vtable_diag_null.cpp @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +extern "C" void __ct_vtable_dump(void* this_ptr, const char* site, const char* static_type); + +int main() +{ + std::puts("ct_vtable_diag_null"); + __ct_vtable_dump(nullptr, "ct_vtable_diag_null.cpp:7:5", "Base"); + return 0; +} diff --git a/test/ct_vtable_diag_stack_target.cpp b/test/ct_vtable_diag_stack_target.cpp new file mode 100644 index 0000000..3a600bf --- /dev/null +++ b/test/ct_vtable_diag_stack_target.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +extern "C" void __ct_vcall_trace(void* this_ptr, void* target, const char* site, + const char* static_type); + +struct Base +{ + virtual ~Base() = default; + virtual int value() const + { + return 1; + } +}; + +struct Derived : Base +{ + int value() const override + { + return 2; + } +}; + +int main() +{ + std::puts("ct_vtable_diag_stack_target"); + + Derived obj; + Base* base = &obj; + int local = 42; + + __ct_vcall_trace(base, &local, "ct_vtable_diag_stack_target.cpp:25:5", "Derived"); + + return 0; +} diff --git a/test/ct_vtable_interface.cpp b/test/ct_vtable_interface.cpp new file mode 100644 index 0000000..ab4320e --- /dev/null +++ b/test/ct_vtable_interface.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +struct IFace +{ + virtual ~IFace() = default; + virtual int run(int value) = 0; +}; + +struct Impl : IFace +{ + int run(int value) override + { + return value * 3; + } +}; + +int main() +{ + IFace* ptr = new Impl(); + int out = ptr->run(7); + std::printf("run=%d\n", out); + delete ptr; + return 0; +} diff --git a/test/ct_vtable_multi.cpp b/test/ct_vtable_multi.cpp new file mode 100644 index 0000000..66f3a37 --- /dev/null +++ b/test/ct_vtable_multi.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +struct Base1 +{ + virtual ~Base1() = default; + virtual const char* name1() const + { + return "Base1"; + } +}; + +struct Base2 +{ + virtual ~Base2() = default; + virtual int id2() const + { + return 2; + } +}; + +struct Derived : Base1, Base2 +{ + const char* name1() const override + { + return "Derived"; + } + int id2() const override + { + return 42; + } +}; + +int main() +{ + Derived obj; + Base1* b1 = &obj; + Base2* b2 = &obj; + std::printf("%s %d\n", b1->name1(), b2->id2()); + return 0; +} diff --git a/test/ct_vtable_uaf.cpp b/test/ct_vtable_uaf.cpp new file mode 100644 index 0000000..f632d23 --- /dev/null +++ b/test/ct_vtable_uaf.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// test/ct_vtable_uaf.cpp +#include +struct Base +{ + virtual ~Base() = default; + virtual int value() const + { + return 1; + } +}; +struct Derived : Base +{ + int value() const override + { + return 2; + } +}; + +int main() +{ + Base* ptr = new Derived(); + delete ptr; + // UAF volontaire pour tester le diag + std::printf("%d\n", ptr->value()); + return 0; +} diff --git a/test/ct_vtable_virtual_base.cpp b/test/ct_vtable_virtual_base.cpp new file mode 100644 index 0000000..b82a595 --- /dev/null +++ b/test/ct_vtable_virtual_base.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +struct VBase +{ + virtual ~VBase() = default; + virtual int value() const + { + return 1; + } +}; + +struct Left : virtual VBase +{ + int value() const override + { + return 11; + } +}; + +struct Right : virtual VBase +{ + int value() const override + { + return 12; + } +}; + +struct Most : Left, Right +{ + int value() const override + { + return 99; + } +}; + +int main() +{ + Most obj; + VBase* base = &obj; + std::printf("value=%d\n", base->value()); + return 0; +} diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile new file mode 100644 index 0000000..935bd12 --- /dev/null +++ b/test/docker/Dockerfile @@ -0,0 +1,49 @@ +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive +ARG LLVM_VERSION=20 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gnupg \ + lsb-release \ + software-properties-common \ + build-essential \ + cmake \ + ninja-build \ + python3-venv \ + python3 \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install LLVM/Clang toolchain +RUN curl -fsSL https://apt.llvm.org/llvm.sh -o /tmp/llvm.sh \ + && chmod +x /tmp/llvm.sh \ + && /tmp/llvm.sh ${LLVM_VERSION} \ + && rm -f /tmp/llvm.sh \ + && apt-get update \ + && apt-get install -y --no-install-recommends libclang-20-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /repo +COPY . /repo + +RUN rm -rf build \ + && cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_DIR=/usr/lib/llvm-${LLVM_VERSION}/lib/cmake/llvm \ + -DClang_DIR=/usr/lib/llvm-${LLVM_VERSION}/lib/cmake/clang \ + -DCLANG_LINK_CLANG_DYLIB=ON \ + -DLLVM_LINK_LLVM_DYLIB=ON \ + -DUSE_SHARED_LIB=OFF \ + && cmake --build build -j"$(nproc)" + +RUN bash test/scripts/linux_compile.sh + +RUN python3 -m venv .venv \ + && . .venv/bin/activate \ + && python -m pip install --upgrade pip \ + && python -m pip install git+https://github.com/CoreTrace/coretrace-testkit.git \ + && python test/examples/test_smoke.py \ + && python test/examples/test_extern_project.py \ + && python test/examples/test_help_smoke.py diff --git a/test/examples/fixtures/cpp_as_c.c b/test/examples/fixtures/cpp_as_c.c new file mode 100644 index 0000000..e054a7e --- /dev/null +++ b/test/examples/fixtures/cpp_as_c.c @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main() +{ + std::string s = "hello"; + return (static_cast(s.size()) == 5) ? 0 : 1; +} diff --git a/test/examples/fixtures/debug.c b/test/examples/fixtures/debug.c new file mode 100644 index 0000000..c038b0e --- /dev/null +++ b/test/examples/fixtures/debug.c @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef DEBUG +#error "DEBUG not defined" +#endif +int main() +{ + return 0; +} diff --git a/test/examples/fixtures/hello.c b/test/examples/fixtures/hello.c new file mode 100644 index 0000000..05a5c82 --- /dev/null +++ b/test/examples/fixtures/hello.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +int main() +{ + puts("hello"); + return 0; +} diff --git a/test/examples/fixtures/hello.cpp b/test/examples/fixtures/hello.cpp new file mode 100644 index 0000000..e054a7e --- /dev/null +++ b/test/examples/fixtures/hello.cpp @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +int main() +{ + std::string s = "hello"; + return (static_cast(s.size()) == 5) ? 0 : 1; +} diff --git a/test/examples/fixtures/vtable.cpp b/test/examples/fixtures/vtable.cpp new file mode 100644 index 0000000..3041f99 --- /dev/null +++ b/test/examples/fixtures/vtable.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +struct Base +{ + virtual ~Base() = default; + virtual int foo() + { + return 1; + } +}; + +int main() +{ + Base b; + return b.foo(); +} diff --git a/test/examples/test_extern_project.py b/test/examples/test_extern_project.py new file mode 100644 index 0000000..02111e0 --- /dev/null +++ b/test/examples/test_extern_project.py @@ -0,0 +1,306 @@ +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations +from pathlib import Path +import os +import shutil +import subprocess +import tempfile + +from ctestfw.runner import CompilerRunner, RunnerConfig +from ctestfw.plan import CompilePlan +from ctestfw.framework.testcase import TestCase +from ctestfw.framework.reporter import ConsoleReporter +from ctestfw.assertions.core import Assertion, require +from ctestfw.assertions.compiler import ( + assert_exit_code, + assert_output_name, + assert_output_exists, + assert_native_binary_kind, +) + +ROOT = Path(__file__).resolve().parents[2] +EXTERN = ROOT / "extern-project" +FIXTURES = EXTERN / "tests" +WORK = EXTERN / ".work" +BUILD = EXTERN / ".build-python" + + +def _platform_executable(path: Path) -> Path: + if os.name == "nt" and path.suffix.lower() != ".exe": + return path.with_suffix(".exe") + return path + + +def _powershell_quote(value: str) -> str: + return "'" + value.replace("'", "''") + "'" + + +def _read_artifact_bytes(res, path: str) -> bytes: + artifact = Path(path) + if not artifact.is_absolute(): + artifact = res.run.cwd / artifact + require(artifact.exists(), f"output does not exist: {artifact}") + return artifact.read_bytes() + + +def _is_windows_native_artifact(data: bytes) -> bool: + if data.startswith(b"MZ"): + return True + if len(data) < 2: + return False + return data[:2] in {b"\x64\x86", b"\x4c\x01", b"\x64\xaa"} + + +def assert_windows_native_artifact(path: str) -> Assertion: + def _check(res) -> None: + data = _read_artifact_bytes(res, path) + require( + _is_windows_native_artifact(data), + f"expected PE/COFF artifact at {path}, got unrecognized header", + ) + return Assertion(name=f"windows_native_artifact_{Path(path).name}", check=_check) + + +def run_cmd(argv: list[str], cwd: Path) -> tuple[int, str, str]: + try: + p = subprocess.run( + argv, + cwd=str(cwd), + capture_output=True, + text=True, + ) + return p.returncode, p.stdout or "", p.stderr or "" + except FileNotFoundError: + return 127, "", f"command not found: {argv[0]}" + + +def find_windows_dev_shell() -> str | None: + program_files_x86 = os.environ.get("ProgramFiles(x86)") + if not program_files_x86: + return None + + vswhere = Path(program_files_x86) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe" + if not vswhere.exists(): + return None + + rc, out, _ = run_cmd([ + str(vswhere), + "-latest", + "-products", "*", + "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-property", "installationPath", + ], ROOT) + if rc != 0 or not out.strip(): + return None + + dev_shell = Path(out.strip()) / "Common7" / "Tools" / "Launch-VsDevShell.ps1" + if not dev_shell.exists(): + return None + return str(dev_shell) + + +def run_cmd_in_vsdev(argv: list[str], cwd: Path) -> tuple[int, str, str]: + dev_shell = find_windows_dev_shell() + if dev_shell is None: + return 127, "", "Visual Studio developer shell not found" + + quoted_args = ", ".join(_powershell_quote(arg) for arg in argv) + script = ( + "$ErrorActionPreference = 'Stop'; " + f". {_powershell_quote(dev_shell)} -Arch amd64 -HostArch amd64 | Out-Null; " + f"$cmd = @({quoted_args}); " + "& $cmd[0] @($cmd[1..($cmd.Length - 1)])" + ) + return run_cmd( + ["powershell", "-ExecutionPolicy", "Bypass", "-Command", script], + cwd, + ) + + +def read_cache_var(cache_path: Path, key: str) -> str | None: + if not cache_path.exists(): + return None + prefix = f"{key}:" + try: + for line in cache_path.read_text().splitlines(): + if line.startswith(prefix) and "=" in line: + return line.split("=", 1)[1].strip() + except OSError: + return None + return None + + +def detect_llvm_clang_dirs() -> tuple[str | None, str | None]: + llvm_dir = os.environ.get("LLVM_DIR") + clang_dir = os.environ.get("Clang_DIR") + + # Try to reuse the main build configuration if present. + if not llvm_dir or not clang_dir: + for cache in (ROOT / "build" / "CMakeCache.txt", ROOT / "build-win" / "CMakeCache.txt"): + llvm_dir = llvm_dir or read_cache_var(cache, "LLVM_DIR") + clang_dir = clang_dir or read_cache_var(cache, "Clang_DIR") + if llvm_dir and clang_dir: + break + + # Try llvm-config if still missing. + if not llvm_dir: + rc, out, _ = run_cmd(["llvm-config", "--cmakedir"], ROOT) + if rc == 0 and out.strip(): + llvm_dir = out.strip() + + # Derive clang dir from llvm dir if possible. + if llvm_dir and not clang_dir: + llvm_path = Path(llvm_dir) + if llvm_path.name == "llvm": + candidate = llvm_path.parent / "clang" + if candidate.exists(): + clang_dir = str(candidate) + + # Brew fallback (macOS) if still missing. + if not llvm_dir or not clang_dir: + for formula in ("llvm@20", "llvm@19", "llvm"): + rc, out, _ = run_cmd(["brew", "--prefix", formula], ROOT) + if rc == 0 and out.strip(): + prefix = Path(out.strip()) + llvm_candidate = prefix / "lib" / "cmake" / "llvm" + clang_candidate = prefix / "lib" / "cmake" / "clang" + if not llvm_dir and llvm_candidate.exists(): + llvm_dir = str(llvm_candidate) + if not clang_dir and clang_candidate.exists(): + clang_dir = str(clang_candidate) + if llvm_dir and clang_dir: + break + + return llvm_dir, clang_dir + + +def configure_and_build() -> Path: + if os.name == "nt" and BUILD.exists(): + shutil.rmtree(BUILD, ignore_errors=True) + + if BUILD.exists(): + cache = BUILD / "CMakeCache.txt" + cached_src = read_cache_var(cache, "CMAKE_HOME_DIRECTORY") + if cached_src and Path(cached_src).resolve() != EXTERN.resolve(): + shutil.rmtree(BUILD, ignore_errors=True) + + cmake_args = [ + "cmake", + "-S", str(EXTERN), + "-B", str(BUILD), + "-DCMAKE_BUILD_TYPE=Release", + f"-DFETCHCONTENT_SOURCE_DIR_CC={ROOT}", + ] + llvm_dir, clang_dir = detect_llvm_clang_dirs() + runner = run_cmd + if os.name == "nt": + llvm_root = Path(llvm_dir).parents[2] if llvm_dir else None + clang_cl = llvm_root / "bin" / "clang-cl.exe" if llvm_root else None + cmake_args.extend([ + "-G", "Ninja Multi-Config", + ]) + if clang_cl and clang_cl.exists(): + cmake_args.append(f"-DCMAKE_C_COMPILER={clang_cl}") + cmake_args.append(f"-DCMAKE_CXX_COMPILER={clang_cl}") + runner = run_cmd_in_vsdev + if llvm_dir: + cmake_args.append(f"-DLLVM_DIR={llvm_dir}") + if clang_dir: + cmake_args.append(f"-DClang_DIR={clang_dir}") + logger_source_dir = os.environ.get("CORETRACE_LOGGER_SOURCE_DIR") + if logger_source_dir: + cmake_args.append(f"-DFETCHCONTENT_SOURCE_DIR_CORETRACE_LOGGER={Path(logger_source_dir).resolve()}") + if not llvm_dir or not clang_dir: + print("LLVM/Clang CMake dirs not found. Set LLVM_DIR and Clang_DIR, or build the main project first.") + + rc, out, err = runner(cmake_args, ROOT) + if rc != 0: + print("cmake configure failed") + print(out) + print(err) + raise SystemExit(1) + + build_args = ["cmake", "--build", str(BUILD), "--config", "Release"] + if os.name != "nt": + build_args.append("-j") + rc, out, err = runner(build_args, ROOT) + if rc != 0: + print("cmake build failed") + print(out) + print(err) + raise SystemExit(1) + + cc1 = _platform_executable(BUILD / "cc1") + if not cc1.exists(): + cc1 = _platform_executable(BUILD / "Release" / "cc1") + if not cc1.exists(): + cc1 = _platform_executable(BUILD / "Debug" / "cc1") + if not cc1.exists(): + print(f"cc1 binary not found after build: {cc1}") + raise SystemExit(1) + return cc1.resolve() + + +def copy_fixtures(ws: Path, files: list[Path]) -> None: + for f in files: + dst = ws / f.name + shutil.copy2(f, dst) + + +def main() -> int: + cc1 = configure_and_build() + runner = CompilerRunner(RunnerConfig(executable=cc1)) + + src_c = FIXTURES / "hello.c" + src_cpp = FIXTURES / "hello.cpp" + for src in (src_c, src_cpp): + if not src.exists(): + print(f"fixture not found: {src}") + return 1 + + tc_c = TestCase( + name="extern_project_compile_c", + plan=CompilePlan( + name="extern_project_compile_c", + sources=[Path("hello.c")], + out=Path("hello_ext_c.o"), + extra_args=["-c"], + ), + assertions=[ + assert_exit_code(0), + assert_output_name("hello_ext_c.o"), + assert_output_exists(), + assert_windows_native_artifact("hello_ext_c.o") if os.name == "nt" else assert_native_binary_kind(), + ], + ) + + tc_cpp = TestCase( + name="extern_project_compile_cpp", + plan=CompilePlan( + name="extern_project_compile_cpp", + sources=[Path("hello.cpp")], + out=Path("hello_ext_cpp.o"), + extra_args=["-c"], + ), + assertions=[ + assert_exit_code(0), + assert_output_name("hello_ext_cpp.o"), + assert_output_exists(), + assert_windows_native_artifact("hello_ext_cpp.o") if os.name == "nt" else assert_native_binary_kind(), + ], + ) + + WORK.mkdir(parents=True, exist_ok=True) + reports = [] + for tc in (tc_c, tc_cpp): + with tempfile.TemporaryDirectory(prefix=f"{tc.name}_", dir=str(WORK)) as d: + ws = Path(d) + copy_fixtures(ws, [src_c, src_cpp]) + reports.append(tc.run(runner, ws)) + + rep = type("Tmp", (), {"name": "extern_project", "reports": reports})() + return ConsoleReporter().render(rep) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/examples/test_help_smoke.py b/test/examples/test_help_smoke.py new file mode 100644 index 0000000..e8cfe50 --- /dev/null +++ b/test/examples/test_help_smoke.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: Apache-2.0 +from pathlib import Path +import shutil +import tempfile + +from ctestfw.runner import CompilerRunner, RunnerConfig +from ctestfw.plan import CompilePlan +from ctestfw.framework.testcase import TestCase +from ctestfw.framework.reporter import ConsoleReporter +from ctestfw.assertions.compiler import ( + assert_exit_code, + assert_stdout_contains, +) + +ROOT = Path(__file__).resolve().parents[2] +FIXTURES = ROOT / "test" / "examples" / "fixtures" +WORK = ROOT / "test" / "examples" / ".work" + + +def copy_fixtures(ws: Path, files: list[Path]) -> None: + for f in files: + dst = ws / f.name + shutil.copy2(f, dst) + + +def main() -> int: + cc_bin = (ROOT / "build" / "cc").resolve() + runner = CompilerRunner(RunnerConfig(executable=cc_bin)) + if not cc_bin.exists(): + print(f"cc binary not found: {cc_bin}") + return 1 + + src = FIXTURES / "hello.c" + if not src.exists(): + print(f"fixture not found: {src}") + return 1 + + tc_help = TestCase( + name="help_smoke", + plan=CompilePlan( + name="help_smoke", + sources=[Path("hello.c")], + out=None, + extra_args=["--help"], + ), + assertions=[ + assert_exit_code(0), + assert_stdout_contains("Usage:"), + assert_stdout_contains("Core options:"), + assert_stdout_contains("--instrument"), + assert_stdout_contains("Exit codes:"), + ], + ) + + WORK.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix=f"{tc_help.name}_", dir=str(WORK)) as d: + ws = Path(d) + copy_fixtures(ws, [src]) + report = tc_help.run(runner, ws) + + rep = type("Tmp", (), {"name": "help_smoke", "reports": [report]})() + return ConsoleReporter().render(rep) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/examples/test_smoke.py b/test/examples/test_smoke.py new file mode 100644 index 0000000..9434a5a --- /dev/null +++ b/test/examples/test_smoke.py @@ -0,0 +1,571 @@ +# SPDX-License-Identifier: Apache-2.0 +import os +from pathlib import Path +import shutil + +from ctestfw.runner import CompilerRunner, RunnerConfig +from ctestfw.plan import CompilePlan +from ctestfw.framework.testcase import TestCase +from ctestfw.framework.suite import TestSuite +from ctestfw.framework.reporter import ConsoleReporter +from ctestfw.assertions.core import Assertion, require +from ctestfw.assertions.compiler import ( + assert_exit_code, + assert_argv_contains, + assert_output_exists, + assert_output_name, + assert_output_kind, + assert_native_binary_kind, + assert_output_exists_at, + assert_native_binary_kind_at, + assert_output_kind_at, + assert_output_nonempty_at, + assert_stdout_contains +) +from ctestfw.inspect.filetype import ArtifactKind +from ctestfw.platform import detect_platform, OS + +ROOT = Path(__file__).resolve().parents[2] +FIXTURES = ROOT / "test" / "examples" / "fixtures" +WORK = ROOT / "test" / "examples" / ".work" + +def copy_fixtures(ws: Path, files: list[Path]) -> None: + for f in files: + src = f + dst = ws / f.name + shutil.copy2(src, dst) + +def assert_file_contains(path: str, text: str) -> Assertion: + def _check(res) -> None: + p = Path(path) + if not p.is_absolute(): + p = res.run.cwd / p + require(p.exists(), f"output does not exist: {p}") + data = p.read_text(encoding="utf-8", errors="ignore") + require(text in data, f"file does not contain '{text}': {p}") + return Assertion(name=f"file_contains_{Path(path).name}", check=_check) + +def assert_stderr_contains(text: str) -> Assertion: + def _check(res) -> None: + require(text in (res.run.stderr or ""), + f"stderr does not contain '{text}'\nstderr:\n{res.run.stderr}") + return Assertion(name=f"stderr_contains_{text}", check=_check) + +def _read_artifact_bytes(res, path: str) -> bytes: + artifact = Path(path) + if not artifact.is_absolute(): + artifact = res.run.cwd / artifact + require(artifact.exists(), f"output does not exist: {artifact}") + return artifact.read_bytes() + +def _is_windows_native_artifact(data: bytes) -> bool: + if data.startswith(b"MZ"): + return True + if len(data) < 2: + return False + return data[:2] in {b"\x64\x86", b"\x4c\x01", b"\x64\xaa"} + +def assert_windows_native_artifact_at(path: str) -> Assertion: + def _check(res) -> None: + data = _read_artifact_bytes(res, path) + require( + _is_windows_native_artifact(data), + f"expected PE/COFF artifact at {path}, got unrecognized header", + ) + return Assertion(name=f"windows_native_artifact_{Path(path).name}", check=_check) + +def native_artifact_assert_at(path: str, platform_os: OS) -> Assertion: + if platform_os == OS.WINDOWS: + return assert_windows_native_artifact_at(path) + return assert_native_binary_kind_at(path) + +def resolve_compiler_binary() -> Path | None: + candidates: list[Path] = [] + + env_override = os.environ.get("CORETRACE_COMPILER_TEST_CC") + if env_override: + candidates.append(Path(env_override)) + + candidates.extend([ + ROOT / "dist" / "windows" / "bin" / "cc.exe", + ROOT / "build" / "cc", + ROOT / "build" / "Release" / "cc.exe", + ROOT / "build-win" / "cc.exe", + ROOT / "build-win" / "Release" / "cc.exe", + ]) + + for candidate in candidates: + if candidate.exists(): + return candidate.resolve() + + return None + +def main() -> int: + platform = detect_platform() + cc_bin = resolve_compiler_binary() + if cc_bin is None: + print("cc binary not found. Tried:") + for candidate in [ + os.environ.get("CORETRACE_COMPILER_TEST_CC", ""), + str(ROOT / "dist" / "windows" / "bin" / "cc.exe"), + str(ROOT / "build" / "cc"), + str(ROOT / "build" / "Release" / "cc.exe"), + str(ROOT / "build-win" / "cc.exe"), + str(ROOT / "build-win" / "Release" / "cc.exe"), + ]: + if candidate: + print(f" - {candidate}") + return 1 + + runner = CompilerRunner(RunnerConfig(executable=cc_bin)) + + # Fixtures (ex: hello.c) + src = FIXTURES / "hello.c" + debug_src = FIXTURES / "debug.c" + cpp_src = FIXTURES / "hello.cpp" + cpp_as_c_src = FIXTURES / "cpp_as_c.c" + vtable_src = FIXTURES / "vtable.cpp" + + def base_out_assertions(out_name: str): + assertions = [ + assert_exit_code(0), + assert_argv_contains(["-o"]), # check args passed + assert_output_name(out_name), # check binary name respected + assert_output_exists(), + ] + if platform.os == OS.WINDOWS: + assertions.append(assert_windows_native_artifact_at(out_name)) + return assertions + + tc_macho = TestCase( + name="compile_macho_hello", + plan=CompilePlan( + name="compile_macho_hello", + sources=[Path("hello.c")], # sera copié dans workspace + out=Path("hello.out"), + extra_args=[], + ), + assertions=base_out_assertions("hello.out") + [ + assert_output_kind(ArtifactKind.MACHO), + ], + ) + + tc_elf = TestCase( + name="compile_elf_hello", + plan=CompilePlan( + name="compile_elf_hello", + sources=[Path("hello.c")], + out=Path("hello.out"), + extra_args=[], + ), + assertions=base_out_assertions("hello.out") + [ + assert_output_kind(ArtifactKind.ELF), + ], + ) + + tc_native = TestCase( + name="compile_native_hello", + plan=CompilePlan( + name="compile_native_hello", + sources=[Path("hello.c")], + out=Path("hello.out"), + extra_args=[], + ), + assertions=base_out_assertions("hello.out") + [ + assert_windows_native_artifact_at("hello.out") if platform.os == OS.WINDOWS else assert_native_binary_kind(), + ], + ) + + tc_cpp = TestCase( + name="compile_cpp_hello", + plan=CompilePlan( + name="compile_cpp_hello", + sources=[Path("hello.cpp")], + out=Path("hello_cpp.out"), + extra_args=[], + ), + assertions=base_out_assertions("hello_cpp.out") + [ + assert_windows_native_artifact_at("hello_cpp.out") if platform.os == OS.WINDOWS else assert_native_binary_kind(), + ], + ) + + tc_o_eq = TestCase( + name="compile_o_equals", + plan=CompilePlan( + name="compile_o_equals", + sources=[Path("hello.c")], + out=None, + extra_args=["-o=main"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["-o=main"]), + assert_output_exists_at("main"), + native_artifact_assert_at("main", platform.os), + ], + ) + + tc_d_space = TestCase( + name="compile_define_space", + plan=CompilePlan( + name="compile_define_space", + sources=[Path("debug.c")], + out=None, + extra_args=["-D", "DEBUG", "-o=debug_space"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["-D", "DEBUG"]), + assert_output_exists_at("debug_space"), + native_artifact_assert_at("debug_space", platform.os), + ], + ) + + tc_d_compact = TestCase( + name="compile_define_compact", + plan=CompilePlan( + name="compile_define_compact", + sources=[Path("debug.c")], + out=None, + extra_args=["-DDEBUG", "-o=debug_compact"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["-DDEBUG"]), + assert_output_exists_at("debug_compact"), + native_artifact_assert_at("debug_compact", platform.os), + ], + ) + + tc_x_cxx = TestCase( + name="compile_x_cxx", + plan=CompilePlan( + name="compile_x_cxx", + sources=[], + out=None, + extra_args=["-x=c++", "cpp_as_c.c", "-o=hello_xcxx.out"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["-x=c++"]), + assert_output_exists_at("hello_xcxx.out"), + native_artifact_assert_at("hello_xcxx.out", platform.os), + ], + ) + + tc_instrument_c = TestCase( + name="compile_instrument_c", + plan=CompilePlan( + name="compile_instrument_c", + sources=[Path("hello.c")], + out=Path("hello_instr_c.out"), + extra_args=["--instrument"], + ), + assertions=base_out_assertions("hello_instr_c.out") + [ + assert_argv_contains(["--instrument"]), + assert_windows_native_artifact_at("hello_instr_c.out") if platform.os == OS.WINDOWS else assert_native_binary_kind(), + ], + ) + + tc_instrument_cpp = TestCase( + name="compile_instrument_cpp", + plan=CompilePlan( + name="compile_instrument_cpp", + sources=[Path("hello.cpp")], + out=Path("hello_instr_cpp.out"), + extra_args=["--instrument"], + ), + assertions=base_out_assertions("hello_instr_cpp.out") + [ + assert_argv_contains(["--instrument"]), + assert_windows_native_artifact_at("hello_instr_cpp.out") if platform.os == OS.WINDOWS else assert_native_binary_kind(), + ], + ) + + tc_instrument_x_cxx = TestCase( + name="compile_instrument_x_cxx", + plan=CompilePlan( + name="compile_instrument_x_cxx", + sources=[], + out=None, + extra_args=["--instrument", "-x=c++", "cpp_as_c.c", "-o=hello_instr_xcxx.out"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--instrument", "-x=c++"]), + assert_output_exists_at("hello_instr_xcxx.out"), + native_artifact_assert_at("hello_instr_xcxx.out", platform.os), + ], + ) + + tc_instrument_emit_llvm = TestCase( + name="compile_instrument_emit_llvm", + plan=CompilePlan( + name="compile_instrument_emit_llvm", + sources=[Path("hello.c")], + out=None, + extra_args=["--instrument", "-S", "-emit-llvm", "-o=hello_instr.ll"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--instrument", "-S", "-emit-llvm"]), + assert_output_exists_at("hello_instr.ll"), + assert_output_kind_at("hello_instr.ll", ArtifactKind.LLVM_IR_TEXT), + assert_output_nonempty_at("hello_instr.ll"), + ], + ) + + tc_instrument_emit_bc = TestCase( + name="compile_instrument_emit_bc", + plan=CompilePlan( + name="compile_instrument_emit_bc", + sources=[Path("hello.c")], + out=None, + extra_args=["--instrument", "-c", "-emit-llvm", "-o=hello_instr.bc"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--instrument", "-c", "-emit-llvm"]), + assert_output_exists_at("hello_instr.bc"), + assert_output_nonempty_at("hello_instr.bc"), + ], + ) + + tc_readme_emit_llvm = TestCase( + name="readme_emit_llvm", + plan=CompilePlan( + name="readme_emit_llvm", + sources=[Path("hello.cpp")], + out=None, + extra_args=["-S", "-emit-llvm"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["-S", "-emit-llvm"]), + assert_output_kind_at("hello.ll", ArtifactKind.LLVM_IR_TEXT), + ], + ) + + tc_readme_asm = TestCase( + name="readme_asm", + plan=CompilePlan( + name="readme_asm", + sources=[Path("hello.cpp")], + out=None, + extra_args=["-S"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["-S"]), + assert_output_nonempty_at("hello.s"), + ], + ) + + tc_readme_c_obj = TestCase( + name="readme_c_obj", + plan=CompilePlan( + name="readme_c_obj", + sources=[Path("hello.c")], + out=None, + extra_args=["-c"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["-c"]), + native_artifact_assert_at("hello.o", platform.os), + ], + ) + + tc_readme_c_obj_o2 = TestCase( + name="readme_c_obj_o2", + plan=CompilePlan( + name="readme_c_obj_o2", + sources=[Path("hello.c")], + out=None, + extra_args=["-c", "-O2"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["-c", "-O2"]), + native_artifact_assert_at("hello.o", platform.os), + ], + ) + + tc_readme_instrument = TestCase( + name="readme_instrument", + plan=CompilePlan( + name="readme_instrument", + sources=[Path("hello.c")], + out=None, + extra_args=["--instrument", "-o", "app"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--instrument", "-o", "app"]), + assert_output_exists_at("app"), + native_artifact_assert_at("app", platform.os), + ], + ) + + tc_readme_shadow = TestCase( + name="readme_shadow", + plan=CompilePlan( + name="readme_shadow", + sources=[Path("hello.c")], + out=None, + extra_args=["--instrument", "--ct-shadow", "-o", "app_shadow"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--instrument", "--ct-shadow"]), + assert_output_exists_at("app_shadow"), + native_artifact_assert_at("app_shadow", platform.os), + ], + ) + + tc_readme_shadow_aggr = TestCase( + name="readme_shadow_aggr", + plan=CompilePlan( + name="readme_shadow_aggr", + sources=[Path("hello.c")], + out=None, + extra_args=["--instrument", "--ct-shadow-aggressive", "--ct-bounds-no-abort", "-o", "app_shadow_aggr"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--instrument", "--ct-shadow-aggressive", "--ct-bounds-no-abort"]), + assert_output_exists_at("app_shadow_aggr"), + native_artifact_assert_at("app_shadow_aggr", platform.os), + ], + ) + + tc_readme_vtable = TestCase( + name="readme_vtable", + plan=CompilePlan( + name="readme_vtable", + sources=[Path("vtable.cpp")], + out=None, + extra_args=["--instrument", "--ct-modules=vtable", "--ct-vcall-trace", "-o", "app_vtable"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--instrument", "--ct-modules=vtable", "--ct-vcall-trace"]), + assert_output_exists_at("app_vtable"), + native_artifact_assert_at("app_vtable", platform.os), + ], + ) + + tc_readme_inmem = TestCase( + name="readme_inmem", + plan=CompilePlan( + name="readme_inmem", + sources=[Path("hello.c")], + out=None, + extra_args=["--in-mem", "-S", "-emit-llvm"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--in-mem", "-S", "-emit-llvm"]), + assert_stdout_contains("target triple"), + ], + ) + + tc_optnone_emit_llvm = TestCase( + name="compile_optnone_emit_llvm", + plan=CompilePlan( + name="compile_optnone_emit_llvm", + sources=[Path("hello.c")], + out=None, + extra_args=["--ct-optnone", "-O1", "-S", "-emit-llvm", "-o=hello_optnone.ll"], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--ct-optnone", "-O1", "-S", "-emit-llvm"]), + assert_output_exists_at("hello_optnone.ll"), + assert_output_kind_at("hello_optnone.ll", ArtifactKind.LLVM_IR_TEXT), + assert_output_nonempty_at("hello_optnone.ll"), + assert_file_contains("hello_optnone.ll", "optnone"), + ], + ) + + tc_optnone_disable_o0 = TestCase( + name="compile_optnone_disable_o0", + plan=CompilePlan( + name="compile_optnone_disable_o0", + sources=[Path("hello.c")], + out=None, + extra_args=[ + "--ct-optnone", + "-O0", + "-Xclang", + "-disable-O0-optnone", + "-S", + "-emit-llvm", + "-o", + "-", + ], + ), + assertions=[ + assert_exit_code(0), + assert_argv_contains(["--ct-optnone", "-O0", "-Xclang", "-disable-O0-optnone"]), + assert_stdout_contains("optnone"), + assert_stderr_contains( + "warning: ct: -disable-O0-optnone ignored because --ct-optnone is enabled" + ), + ], + ) + + common_cases = [tc_o_eq, tc_d_space, tc_d_compact, tc_cpp, tc_x_cxx] + instrument_cases = [ + tc_instrument_c, + tc_instrument_cpp, + tc_instrument_x_cxx, + tc_instrument_emit_llvm, + tc_instrument_emit_bc, + ] + readme_cases = [ + tc_readme_emit_llvm, + tc_readme_asm, + tc_readme_c_obj, + tc_readme_c_obj_o2, + tc_readme_instrument, + tc_readme_shadow, + tc_readme_shadow_aggr, + tc_readme_vtable, + tc_readme_inmem, + tc_optnone_emit_llvm, + tc_optnone_disable_o0, + ] + if platform.os == OS.MACOS: + cases = [tc_macho, *common_cases, *instrument_cases, *readme_cases] + elif platform.os == OS.LINUX: + cases = [tc_elf, *common_cases, *instrument_cases, *readme_cases] + else: + windows_readme_cases = [ + tc_readme_emit_llvm, + tc_readme_c_obj, + tc_readme_c_obj_o2, + tc_readme_instrument, + tc_readme_shadow, + tc_readme_shadow_aggr, + tc_readme_inmem, + tc_optnone_emit_llvm, + tc_optnone_disable_o0, + ] + cases = [tc_native, *common_cases, *instrument_cases, *windows_readme_cases] + + suite = TestSuite(name="compiler_smoke", cases=cases) + + reports = [] + WORK.mkdir(parents=True, exist_ok=True) + for case in suite.cases: + import tempfile + with tempfile.TemporaryDirectory(prefix=f"{case.name}_", dir=str(WORK)) as d: + ws = Path(d) + copy_fixtures(ws, [src, debug_src, cpp_src, cpp_as_c_src, vtable_src]) + reports.append(case.run(runner, ws)) + + rep = type("Tmp", (), {"name": suite.name, "reports": reports})() + return ConsoleReporter().render(rep) + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/run_autofree_tests.sh b/test/run_autofree_tests.sh new file mode 100755 index 0000000..92a9b4e --- /dev/null +++ b/test/run_autofree_tests.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CC_BIN="${ROOT_DIR}/build/cc" +OUT_DIR="${1:-/tmp/ct_autofree_tests}" + +if command -v rg >/dev/null 2>&1; then + MATCH_TOOL="rg" +else + MATCH_TOOL="grep" +fi + +has_match() { + local pattern="$1" + local file="$2" + if [[ "${MATCH_TOOL}" == "rg" ]]; then + rg -q "${pattern}" "${file}" + else + grep -q "${pattern}" "${file}" + fi +} + +if [[ ! -x "${CC_BIN}" ]]; then + echo "ERROR: ${CC_BIN} not found or not executable." + echo "Build coretrace-compiler first (cmake --build build)." + exit 1 +fi + +mkdir -p "${OUT_DIR}" + +expect_leak() { + case "$1" in + ct_autofree_local.c|ct_autofree_select_escape.c|ct_autofree_ptrtoint_escape.c|ct_autofree_inttoptr_escape.c) + return 0 + ;; + *) + return 1 + ;; + esac +} + +expect_autofree() { + case "$1" in + ct_autofree_return_unused.c|ct_autofree_select.c|ct_autofree_ptrtoint.c|ct_autofree_inttoptr.c|ct_autofree_new_nothrow.cpp|ct_autofree_posix_memalign.c|ct_autofree_aligned_alloc.c|ct_autofree_mmap.c|ct_autofree_sbrk.c) + return 0 + ;; + *) + return 1 + ;; + esac +} + +expect_nonzero_exit() { + case "$1" in + ct_autofree_select_escape.c|ct_autofree_ptrtoint_escape.c|ct_autofree_inttoptr_escape.c) + return 0 + ;; + *) + return 1 + ;; + esac +} + +TESTS=( + ct_autofree_local.c + ct_autofree_return_unused.c + ct_autofree_select.c + ct_autofree_select_escape.c + ct_autofree_ptrtoint.c + ct_autofree_ptrtoint_escape.c + ct_autofree_inttoptr.c + ct_autofree_inttoptr_escape.c + ct_autofree_new_nothrow.cpp + ct_autofree_posix_memalign.c + ct_autofree_aligned_alloc.c + ct_autofree_mmap.c + ct_autofree_sbrk.c + ct_autofree_brk.c +) + +PASS=0 +FAIL=0 + +run_one() { + local test_file="$1" + local test_path="${ROOT_DIR}/test/${test_file}" + local base="${test_file%.*}" + local bin="${OUT_DIR}/${base}" + local compile_log="${OUT_DIR}/${base}.compile.log" + local run_log="${OUT_DIR}/${base}.run.log" + + echo "==> ${test_file}" + + "${CC_BIN}" --instrument --ct-modules=trace,alloc --ct-autofree \ + "${test_path}" -o "${bin}" >"${compile_log}" 2>&1 || { + echo " FAIL: compile (see ${compile_log})" + return 1 + } + + set +e + "${bin}" >"${run_log}" 2>&1 + local run_rc=$? + set -e + + if expect_nonzero_exit "${test_file}"; then + if [[ "${run_rc}" -eq 0 ]]; then + echo " FAIL: expected non-zero exit, got 0" + return 1 + fi + else + if [[ "${run_rc}" -ne 0 ]]; then + echo " FAIL: run (see ${run_log})" + return 1 + fi + fi + + if expect_leak "${test_file}"; then + if ! has_match "ct: leaks detected" "${run_log}"; then + echo " FAIL: expected leak, none found" + return 1 + fi + else + if has_match "ct: leaks detected" "${run_log}"; then + echo " FAIL: unexpected leak detected" + return 1 + fi + fi + + if expect_autofree "${test_file}"; then + if ! has_match "auto-free ptr=" "${run_log}"; then + echo " FAIL: expected auto-free log, none found" + return 1 + fi + fi + + echo " OK" + return 0 +} + +for t in "${TESTS[@]}"; do + if run_one "${t}"; then + PASS=$((PASS + 1)) + else + FAIL=$((FAIL + 1)) + fi +done + +echo "" +echo "Summary: ${PASS} passed, ${FAIL} failed" +[[ "${FAIL}" -eq 0 ]] diff --git a/test/scripts/linux_compile.sh b/test/scripts/linux_compile.sh new file mode 100644 index 0000000..7176528 --- /dev/null +++ b/test/scripts/linux_compile.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BUILD_DIR="${BUILD_DIR:-$ROOT/build}" +CC_BIN="${CC_BIN:-$BUILD_DIR/cc}" + +if [ ! -x "$CC_BIN" ]; then + echo "cc binary not found at $CC_BIN" + echo "Build it first (see README.md)." + exit 1 +fi + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +cat >"$TMP_DIR/foo.cpp" <<'EOF' +#include +int foo() { + std::string s = "foo"; + return static_cast(s.size()); +} +EOF + +cat >"$TMP_DIR/bar.cpp" <<'EOF' +#include +extern int foo(); +int bar() { + std::vector v{1, 2, 3}; + return static_cast(v.size()); +} +int main() { + return (foo() + bar() == 6) ? 0 : 1; +} +EOF + +cat >"$TMP_DIR/baz.c" <<'EOF' +#include +int baz() { + std::string s = "baz"; + return static_cast(s.size()); +} +EOF + +pushd "$TMP_DIR" >/dev/null + +"$CC_BIN" --instrument foo.cpp bar.cpp -o app_instrumented +./app_instrumented + +"$CC_BIN" --instrument -x c++ -c foo.cpp bar.cpp +"$CC_BIN" --instrument -x=c++ -c baz.c -o=baz_inst.o +"$CC_BIN" --instrument foo.o bar.o -o app_instrumented_obj +./app_instrumented_obj +"$CC_BIN" --instrument foo.o bar.o baz_inst.o -o=app_instrumented_obj_eq +./app_instrumented_obj_eq + +rm -f foo.o bar.o baz_inst.o + +"$CC_BIN" foo.cpp bar.cpp -o app_plain +./app_plain + +"$CC_BIN" -x c++ -c foo.cpp bar.cpp +"$CC_BIN" foo.o bar.o -o app_plain_obj +./app_plain_obj +"$CC_BIN" -x=c++ -c baz.c -o=baz_eq.o +"$CC_BIN" foo.o bar.o baz_eq.o -o=app_plain_obj_eq +./app_plain_obj_eq + +rm -f foo.o bar.o baz_eq.o + +popd >/dev/null diff --git a/test/scripts/macos_compile.sh b/test/scripts/macos_compile.sh new file mode 100644 index 0000000..7176528 --- /dev/null +++ b/test/scripts/macos_compile.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BUILD_DIR="${BUILD_DIR:-$ROOT/build}" +CC_BIN="${CC_BIN:-$BUILD_DIR/cc}" + +if [ ! -x "$CC_BIN" ]; then + echo "cc binary not found at $CC_BIN" + echo "Build it first (see README.md)." + exit 1 +fi + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +cat >"$TMP_DIR/foo.cpp" <<'EOF' +#include +int foo() { + std::string s = "foo"; + return static_cast(s.size()); +} +EOF + +cat >"$TMP_DIR/bar.cpp" <<'EOF' +#include +extern int foo(); +int bar() { + std::vector v{1, 2, 3}; + return static_cast(v.size()); +} +int main() { + return (foo() + bar() == 6) ? 0 : 1; +} +EOF + +cat >"$TMP_DIR/baz.c" <<'EOF' +#include +int baz() { + std::string s = "baz"; + return static_cast(s.size()); +} +EOF + +pushd "$TMP_DIR" >/dev/null + +"$CC_BIN" --instrument foo.cpp bar.cpp -o app_instrumented +./app_instrumented + +"$CC_BIN" --instrument -x c++ -c foo.cpp bar.cpp +"$CC_BIN" --instrument -x=c++ -c baz.c -o=baz_inst.o +"$CC_BIN" --instrument foo.o bar.o -o app_instrumented_obj +./app_instrumented_obj +"$CC_BIN" --instrument foo.o bar.o baz_inst.o -o=app_instrumented_obj_eq +./app_instrumented_obj_eq + +rm -f foo.o bar.o baz_inst.o + +"$CC_BIN" foo.cpp bar.cpp -o app_plain +./app_plain + +"$CC_BIN" -x c++ -c foo.cpp bar.cpp +"$CC_BIN" foo.o bar.o -o app_plain_obj +./app_plain_obj +"$CC_BIN" -x=c++ -c baz.c -o=baz_eq.o +"$CC_BIN" foo.o bar.o baz_eq.o -o=app_plain_obj_eq +./app_plain_obj_eq + +rm -f foo.o bar.o baz_eq.o + +popd >/dev/null