Compare commits
16 Commits
f5379472ce
...
8faaf4db99
| Author | SHA1 | Date | |
|---|---|---|---|
| 8faaf4db99 | |||
| 202e66c862 | |||
| 85b580bb23 | |||
| 5b6570a1b0 | |||
| a407891827 | |||
| 34d21f43fe | |||
| 3e44deed72 | |||
| e0348d80bf | |||
| 644d307abe | |||
| 9a32e9b6f7 | |||
| fc5493bd59 | |||
| 4157ba2ff7 | |||
| 54bfdc4613 | |||
| 051434b388 | |||
| 026c9eea7c | |||
| b0ab8e77d5 |
@@ -2,4 +2,5 @@ __pycache__/
|
||||
.pytest_cache/
|
||||
*.py[cod]
|
||||
.harness/build/
|
||||
out/
|
||||
.worktrees/
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"projectType": "cmake",
|
||||
"cmake": {
|
||||
"sourceDir": ".",
|
||||
"binaryDir": "out/build/windows-debug",
|
||||
"configurePreset": "windows-debug",
|
||||
"buildPreset": "windows-debug",
|
||||
"testPreset": "windows-debug"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
## 기술 기준
|
||||
|
||||
- 언어 표준: C++20
|
||||
- 컴파일러: Visual Studio 2022 MSVC v143
|
||||
- 컴파일러: Visual Studio 2026 MSVC v145
|
||||
- 대상 플랫폼: Windows x64
|
||||
- 빌드 및 테스트: CMake, CMake Presets, CTest, GoogleTest/GoogleMock
|
||||
- 수치 연산 및 희소 직접해법: Intel oneAPI MKL
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
cmake_minimum_required(VERSION 3.30)
|
||||
|
||||
project(FESA VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
if(NOT MSVC)
|
||||
message(FATAL_ERROR "FESA requires the Microsoft Visual C++ compiler (MSVC v145).")
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_VS_PLATFORM_TOOLSET STREQUAL "v145")
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"FESA requires the v145 platform toolset; configured toolset is "
|
||||
"'${CMAKE_VS_PLATFORM_TOOLSET}'."
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_GENERATOR_PLATFORM STREQUAL "x64")
|
||||
message(FATAL_ERROR "FESA requires the x64 generator platform.")
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(FATAL_ERROR "FESA requires a 64-bit target.")
|
||||
endif()
|
||||
|
||||
include(CTest)
|
||||
include(cmake/FesaDependencies.cmake)
|
||||
|
||||
add_library(fesa_core STATIC
|
||||
src/fesa/core/version.cpp
|
||||
)
|
||||
|
||||
target_include_directories(fesa_core
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include"
|
||||
)
|
||||
|
||||
target_compile_features(fesa_core PUBLIC cxx_std_20)
|
||||
target_compile_options(fesa_core PRIVATE /W4 /permissive- /EHsc)
|
||||
|
||||
add_executable(fesa
|
||||
src/fesa/cli/main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(fesa PRIVATE fesa_core)
|
||||
target_compile_features(fesa PRIVATE cxx_std_20)
|
||||
target_compile_options(fesa PRIVATE /W4 /permissive- /EHsc)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"version": 6,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "windows-debug",
|
||||
"displayName": "Windows Debug",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": {
|
||||
"value": "x64",
|
||||
"strategy": "set"
|
||||
},
|
||||
"toolset": {
|
||||
"value": "v145",
|
||||
"strategy": "set"
|
||||
},
|
||||
"binaryDir": "${sourceDir}/out/build/windows-debug",
|
||||
"cacheVariables": {
|
||||
"CMAKE_CXX_EXTENSIONS": "OFF",
|
||||
"CMAKE_CXX_STANDARD": "20",
|
||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-release",
|
||||
"displayName": "Windows Release",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": {
|
||||
"value": "x64",
|
||||
"strategy": "set"
|
||||
},
|
||||
"toolset": {
|
||||
"value": "v145",
|
||||
"strategy": "set"
|
||||
},
|
||||
"binaryDir": "${sourceDir}/out/build/windows-release",
|
||||
"cacheVariables": {
|
||||
"CMAKE_CXX_EXTENSIONS": "OFF",
|
||||
"CMAKE_CXX_STANDARD": "20",
|
||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "windows-debug",
|
||||
"configurePreset": "windows-debug",
|
||||
"configuration": "Debug"
|
||||
},
|
||||
{
|
||||
"name": "windows-release",
|
||||
"configurePreset": "windows-release",
|
||||
"configuration": "Release"
|
||||
}
|
||||
],
|
||||
"testPresets": [
|
||||
{
|
||||
"name": "windows-debug",
|
||||
"configurePreset": "windows-debug",
|
||||
"configuration": "Debug",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"execution": {
|
||||
"noTestsAction": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-release",
|
||||
"configurePreset": "windows-release",
|
||||
"configuration": "Release",
|
||||
"output": {
|
||||
"outputOnFailure": true
|
||||
},
|
||||
"execution": {
|
||||
"noTestsAction": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
if(NOT WIN32 OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(FATAL_ERROR "FESA dependencies require a Windows x64 build.")
|
||||
endif()
|
||||
|
||||
set(MKL_ARCH intel64)
|
||||
set(MKL_INTERFACE lp64)
|
||||
set(MKL_LINK dynamic)
|
||||
set(MKL_THREADING tbb_thread)
|
||||
|
||||
find_package(MKL CONFIG REQUIRED)
|
||||
find_package(TBB CONFIG REQUIRED COMPONENTS tbb)
|
||||
find_package(HDF5 CONFIG REQUIRED COMPONENTS C shared)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
find_package(GTest CONFIG REQUIRED)
|
||||
endif()
|
||||
|
||||
function(fesa_require_imported_target target_name)
|
||||
if(NOT TARGET "${target_name}")
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Required imported target '${target_name}' is unavailable. "
|
||||
"Verify the installed x64 package and its runtime DLLs."
|
||||
)
|
||||
endif()
|
||||
|
||||
get_target_property(target_is_imported "${target_name}" IMPORTED)
|
||||
if(NOT target_is_imported)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"Dependency target '${target_name}' must be provided by an "
|
||||
"installed package."
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
fesa_require_imported_target(MKL::MKL)
|
||||
fesa_require_imported_target(TBB::tbb)
|
||||
fesa_require_imported_target(hdf5::hdf5-shared)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
fesa_require_imported_target(GTest::gtest_main)
|
||||
fesa_require_imported_target(GTest::gtest)
|
||||
fesa_require_imported_target(GTest::gmock)
|
||||
endif()
|
||||
|
||||
add_library(HDF5::HDF5 INTERFACE IMPORTED GLOBAL)
|
||||
set_property(
|
||||
TARGET HDF5::HDF5
|
||||
PROPERTY INTERFACE_LINK_LIBRARIES hdf5::hdf5-shared
|
||||
)
|
||||
|
||||
set(
|
||||
FESA_DEPENDENCY_RUNTIME_MODIFICATIONS
|
||||
"PATH=path_list_prepend:${MKL_DLL_DIR}"
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:TBB::tbb>"
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:hdf5::hdf5-shared>"
|
||||
)
|
||||
+23
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
## ADR-001: C++20, MSVC v143, x64와 CMake Presets
|
||||
|
||||
**상태:** Accepted
|
||||
**상태:** Superseded by ADR-016
|
||||
|
||||
**상황:** 첫 배포는 Windows 개발팀 내부 검증용이며 Intel oneAPI와 HDF5를 일관되게
|
||||
연동하고 Harness에서 자동 검증해야 한다.
|
||||
@@ -275,3 +275,25 @@ flat `Domain`으로 정규화한다. 외부 entity는 `(instance name, part-loca
|
||||
- 요청한 파일이 없으면 실패하며 비요청 물리량을 통과로 오인하지 않는다.
|
||||
- 단일 Instance에서는 Instance 열을 생략할 수 있다.
|
||||
- tolerance와 검증 출처는 test registration과 `docs/VALIDATION.md`에서 관리한다.
|
||||
|
||||
## ADR-016: Visual Studio 2026 MSVC v145로 툴체인 갱신
|
||||
|
||||
**상태:** Accepted
|
||||
|
||||
**상황:** 개발 환경에 Visual Studio 2026 Community와 MSVC v145가 설치되어 있고,
|
||||
CMake 4.4.0이 `Visual Studio 18 2026` 생성기를 지원한다. 반면 기존 ADR-001의
|
||||
Visual Studio 2022 MSVC v143은 설치되어 있지 않아 solver bootstrap을 진행할 수
|
||||
없다.
|
||||
|
||||
**결정:** ADR-001의 컴파일러 선택을 대체해 C++20, Visual Studio 2026 MSVC v145,
|
||||
Windows x64를 사용한다. CMake Preset은 `Visual Studio 18 2026` 생성기와 `v145`
|
||||
toolset을 명시한다. CMake, CMake Presets, CTest 및 GoogleTest/GoogleMock 선택은
|
||||
유지한다.
|
||||
|
||||
**결과와 트레이드오프:**
|
||||
|
||||
- 현재 설치된 개발 환경에서 별도 v143 설치 없이 bootstrap을 진행할 수 있다.
|
||||
- 빌드 계약이 Visual Studio 2026과 v145에 고정되므로 이전 MSVC toolset은 Phase 1
|
||||
보장 대상이 아니다.
|
||||
- 컴파일러 갱신에 따른 경고와 표준 라이브러리 동작은 전체 Debug/Release 검증에서
|
||||
다시 확인해야 한다.
|
||||
|
||||
+36
-1
@@ -20,6 +20,20 @@ python scripts/execute.py <phase-name>
|
||||
python scripts/execute.py <phase-name> --push
|
||||
```
|
||||
|
||||
Codex의 `workspace-write` sandbox 밖에 설치된 실행 도구나 runtime이 필요한 경우에만
|
||||
`--codex-add-dir`를 반복 지정한다. 경로는 존재하는 절대 디렉터리여야 하며, Codex
|
||||
CLI의 추가 writable directory와 child PATH 끝에 함께 전달된다.
|
||||
|
||||
```powershell
|
||||
python scripts/execute.py <phase-name> `
|
||||
--codex-add-dir "C:\path\to\tool-bin" `
|
||||
--codex-add-dir "C:\path\to\runtime"
|
||||
```
|
||||
|
||||
이 옵션은 지정한 디렉터리에 child agent의 쓰기 권한도 부여하므로, 사용자 프로필이나
|
||||
드라이브 루트처럼 넓은 경로를 지정하지 말고 실행에 필요한 최소 설치 디렉터리만
|
||||
허용한다.
|
||||
|
||||
## Harness Python 검증
|
||||
|
||||
이 저장소의 테스트와 최종 acceptance 검증은 pytest를 시스템 Python에 설치하지 않고
|
||||
@@ -35,6 +49,27 @@ uv run --with pytest python -m pytest -v -rs
|
||||
`binaryDir`, `configurePreset`, `buildPreset`, `testPreset`은 preset을 사용할 때 함께
|
||||
지정해야 한다. 빌드 산출물은 저장소의 `.harness/build/`처럼 격리된 경로에 둔다.
|
||||
|
||||
FESA의 외부 패키지 위치는 개발 머신마다 다르므로 tracked preset이나 production
|
||||
CMake 기본값에 절대경로를 넣지 않는다. 새 PowerShell 세션에서는 configure 전에
|
||||
다음 package directory 환경 변수를 설정한다. 아래 값은 현재 검증된 개발 환경이며,
|
||||
설치 버전이나 위치가 다르면 해당 `*Config.cmake`가 있는 디렉터리로 바꾼다.
|
||||
|
||||
```powershell
|
||||
$env:MKL_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\mkl"
|
||||
$env:TBB_DIR = "C:\Program Files (x86)\Intel\oneAPI\2026.1\lib\cmake\tbb"
|
||||
$env:HDF5_DIR = "C:\Program Files\HDF_Group\HDF5\2.1.1\cmake"
|
||||
$env:GTest_DIR = "C:\Users\baram\AppData\Local\FESA\dependencies\googletest-1.17.0-v145-x64-crt\lib\cmake\GTest"
|
||||
|
||||
Test-Path "$env:MKL_DIR\MKLConfig.cmake"
|
||||
Test-Path "$env:TBB_DIR\TBBConfig.cmake"
|
||||
Test-Path "$env:HDF5_DIR\hdf5-config.cmake"
|
||||
Test-Path "$env:GTest_DIR\GTestConfig.cmake"
|
||||
```
|
||||
|
||||
네 확인 명령이 모두 `True`인 같은 세션에서 preset을 실행한다. oneAPI는 component별
|
||||
별칭 디렉터리가 아니라 위 통합 `2026.1` package를 사용해야 HDF5의 Intel runtime
|
||||
의존성까지 CTest PATH에 포함된다.
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
@@ -50,7 +85,7 @@ uv run --with pytest python -m pytest -v -rs
|
||||
```
|
||||
|
||||
```powershell
|
||||
cmake --preset windows-debug
|
||||
cmake --fresh --preset windows-debug
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
**Architecture:** Implement one end-to-end vertical slice first, then complete the input, numerical, parallel, result, and reference-verification contracts behind the module boundaries in `docs/ARCHITECTURE.md`. Keep the semantic model independent of Abaqus syntax and isolate oneMKL, oneTBB, and HDF5 behind adapters.
|
||||
|
||||
**Tech Stack:** C++20, Visual Studio 2022 MSVC v143 x64, CMake/CMake Presets/CTest, GoogleTest/GoogleMock, Intel oneAPI MKL PARDISO, Intel oneAPI TBB, HDF5 C API, Python 3 Harness.
|
||||
**Tech Stack:** C++20, Visual Studio 2026 MSVC v145 x64, CMake/CMake Presets/CTest, GoogleTest/GoogleMock, Intel oneAPI MKL PARDISO, Intel oneAPI TBB, HDF5 C API, Python 3 Harness.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
@@ -28,12 +28,18 @@
|
||||
The planning environment currently has:
|
||||
|
||||
- CMake 4.4.0
|
||||
- Visual Studio Community 2026 at `C:/Program Files/Microsoft Visual Studio/18/Community`
|
||||
- MSVC v145 tools at `C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC/14.51.36231`
|
||||
- CMake generator `Visual Studio 18 2026`
|
||||
- oneMKL CMake package at `C:/Program Files (x86)/Intel/oneAPI/2026.1/lib/cmake/mkl`
|
||||
- oneTBB CMake package at `C:/Program Files (x86)/Intel/oneAPI/2026.1/lib/cmake/tbb`
|
||||
- no HDF5 or GoogleTest CMake package found in the standard Program Files trees
|
||||
- `MSBuild.exe` not currently available on `PATH`
|
||||
- HDF5 2.1.1 CMake package at `C:/Program Files/HDF_Group/HDF5/2.1.1/cmake`
|
||||
- GoogleTest/GoogleMock v1.17 VS2026/v145 x64 package at
|
||||
`C:/Users/baram/AppData/Local/FESA/dependencies/googletest-1.17.0-v145-x64-crt`
|
||||
- `MSBuild.exe` at `C:/Program Files/Microsoft Visual Studio/18/Community/MSBuild/Current/Bin/MSBuild.exe`
|
||||
|
||||
Task 1 must stop as `blocked` rather than downloading packages if HDF5, GoogleTest, or the MSVC toolchain is still unavailable.
|
||||
The solver bootstrap must stop as `blocked` rather than downloading packages if a dependency
|
||||
required by its current step is unavailable.
|
||||
|
||||
## Required Research Record
|
||||
|
||||
@@ -73,7 +79,7 @@ intentionally differs.
|
||||
|
||||
| Order | Harness phase | Plan tasks | Independent deliverable |
|
||||
| ---: | --- | --- | --- |
|
||||
| 0 | `solver-bootstrap` | 1-2 | Reproducible C++20 build, dependency smoke tests, core IDs and diagnostics |
|
||||
| 0 | `solver-bootstrap` | Harness baseline, 1-2 | Harness self-tests, reproducible C++20 build, dependency smoke tests, core IDs and diagnostics |
|
||||
| 1 | `domain-and-input-skeleton` | 3-4 | Flat or single-Instance B31 input becomes an immutable normalized `Domain` |
|
||||
| 2 | `fem-and-beam-kernel` | 5-6 | Real Timoshenko Beam local stiffness with analytical sanity tests |
|
||||
| 3 | `equation-and-linear-solve` | 7-8 | Deterministic serial CSR system solved by PARDISO |
|
||||
@@ -123,6 +129,9 @@ docs/VALIDATION.md benchmark matrix and qualification result
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create before CMake bootstrap: `tests/harness/test_config.py`
|
||||
- Create before CMake bootstrap: `tests/harness/test_discovery.py`
|
||||
- Create before CMake bootstrap: `tests/harness/test_process.py`
|
||||
- Create: `CMakeLists.txt`
|
||||
- Create: `CMakePresets.json`
|
||||
- Create: `cmake/FesaDependencies.cmake`
|
||||
@@ -141,20 +150,31 @@ docs/VALIDATION.md benchmark matrix and qualification result
|
||||
- Produces presets: `windows-debug`, `windows-release`
|
||||
- Later tasks consume the common warning and include-directory policies.
|
||||
|
||||
Before the CMake task, add characterization tests for the existing Harness
|
||||
configuration, project discovery, and validation-result contracts. This
|
||||
precondition makes the repository-level pytest command collect at least one
|
||||
test without changing production Harness behavior.
|
||||
|
||||
- [ ] **Step 1: Verify required installed packages without changing the machine**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
cmake --version
|
||||
Get-Command MSBuild.exe
|
||||
& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" `
|
||||
-latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
|
||||
-property installationPath
|
||||
Test-Path "C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
Get-ChildItem "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC" -Directory
|
||||
Get-ChildItem "C:\Program Files (x86)\Intel\oneAPI" -Recurse -Filter MKLConfig.cmake
|
||||
Get-ChildItem "C:\Program Files (x86)\Intel\oneAPI" -Recurse -Filter TBBConfig.cmake
|
||||
Get-ChildItem "C:\Program Files" -Recurse -Filter hdf5-config.cmake
|
||||
Get-ChildItem "C:\Program Files" -Recurse -Filter GTestConfig.cmake
|
||||
```
|
||||
|
||||
Expected: MSVC, oneMKL, oneTBB, HDF5, and GoogleTest are all discoverable. If any are missing, mark the Harness step `blocked` and name the missing package; do not download it.
|
||||
Expected: Visual Studio 2026/MSVC v145, oneMKL, oneTBB, HDF5, and GoogleTest
|
||||
are all discoverable before the step that consumes each dependency. If a required item is
|
||||
missing, mark that Harness step `blocked` and name it; do not download it.
|
||||
|
||||
- [ ] **Step 2: Write the failing version test**
|
||||
|
||||
@@ -231,9 +251,11 @@ git commit -m "build: bootstrap FESA CMake project"
|
||||
- Create: `include/fesa/core/vec3.hpp`
|
||||
- Create: `include/fesa/core/source_location.hpp`
|
||||
- Create: `include/fesa/core/diagnostic.hpp`
|
||||
- Create: `include/fesa/core/status.hpp`
|
||||
- Create: `tests/unit/core/entity_id_test.cpp`
|
||||
- Create: `tests/unit/core/vec3_test.cpp`
|
||||
- Create: `tests/unit/core/diagnostic_test.cpp`
|
||||
- Create: `tests/unit/core/status_test.cpp`
|
||||
- Modify: `CMakeLists.txt`
|
||||
- Modify: `tests/CMakeLists.txt`
|
||||
|
||||
@@ -263,21 +285,28 @@ struct SourceLocation final {
|
||||
};
|
||||
|
||||
enum class DiagnosticStage {
|
||||
io, lexical, syntax, semantic, model, equation, solver, results
|
||||
io, syntax, semantic, model, equation, solver, results, validation
|
||||
};
|
||||
|
||||
enum class Severity { warning, error };
|
||||
|
||||
struct Diagnostic final {
|
||||
DiagnosticStage stage;
|
||||
Severity severity;
|
||||
std::string code;
|
||||
std::string message;
|
||||
std::optional<SourceLocation> source;
|
||||
std::optional<std::int64_t> entity_id;
|
||||
};
|
||||
|
||||
struct Status final {
|
||||
bool succeeded;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
Cover negative/zero entity-ID rejection, typed-ID non-interchangeability at compile time, finite `Vec3` validation, and full diagnostic context preservation.
|
||||
Cover negative entity-ID rejection, typed-ID non-interchangeability at compile time, finite `Vec3` validation, full diagnostic context preservation, and success/failure status preservation.
|
||||
|
||||
- [ ] **Step 2: Run focused tests and confirm compile or assertion failure**
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <fesa/core/source_location.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
enum class DiagnosticStage {
|
||||
io,
|
||||
syntax,
|
||||
semantic,
|
||||
model,
|
||||
equation,
|
||||
solver,
|
||||
results,
|
||||
validation
|
||||
};
|
||||
|
||||
enum class Severity { warning, error };
|
||||
|
||||
struct Diagnostic final {
|
||||
DiagnosticStage stage;
|
||||
Severity severity;
|
||||
std::string code;
|
||||
std::string message;
|
||||
std::optional<SourceLocation> source;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
template <class Tag>
|
||||
class EntityId final {
|
||||
public:
|
||||
explicit constexpr EntityId(const std::int64_t value) : value_{value} {
|
||||
if (value < 0) {
|
||||
throw std::invalid_argument{"EntityId value must not be negative."};
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr std::int64_t value() const noexcept {
|
||||
return value_;
|
||||
}
|
||||
|
||||
auto operator<=>(const EntityId&) const = default;
|
||||
|
||||
private:
|
||||
std::int64_t value_;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
struct SourceLocation final {
|
||||
std::filesystem::path file;
|
||||
std::size_t line;
|
||||
std::size_t column;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
struct Status final {
|
||||
bool succeeded;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
struct Vec3 final {
|
||||
double x;
|
||||
double y;
|
||||
double z;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline bool is_finite(const Vec3 value) noexcept {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) &&
|
||||
std::isfinite(value.z);
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
[[nodiscard]] std::string_view version() noexcept;
|
||||
|
||||
}
|
||||
+3
-2
@@ -2,7 +2,8 @@
|
||||
"phases": [
|
||||
{
|
||||
"dir": "solver-bootstrap",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"completed_at": "2026-07-30T13:01:15+0900"
|
||||
},
|
||||
{
|
||||
"dir": "domain-and-input-skeleton",
|
||||
@@ -41,4 +42,4 @@
|
||||
"status": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ packaging 전에 `docs/BUILDING.md`, `docs/INPUT_FORMAT.md`,
|
||||
|
||||
체크리스트는 다음 증거 위치와 command를 포함해야 한다.
|
||||
|
||||
- MSVC v143 x64, C++20 및 dependency versions
|
||||
- Visual Studio 2026 MSVC v145 x64, C++20 및 dependency versions
|
||||
- Debug/Release configure, build, zero-warning, nonzero CTest count
|
||||
- flat 및 단일 무변환 Instance example
|
||||
- HDF5 schema inspection
|
||||
|
||||
@@ -4,18 +4,37 @@
|
||||
"steps": [
|
||||
{
|
||||
"step": 0,
|
||||
"name": "cmake-project-scaffold",
|
||||
"status": "pending"
|
||||
"name": "harness-self-tests",
|
||||
"status": "completed",
|
||||
"summary": "Added tests/harness/test_config.py, test_discovery.py, and test_process.py characterization tests for documented Harness contracts.",
|
||||
"started_at": "2026-07-29T23:58:15+0900",
|
||||
"completed_at": "2026-07-30T00:02:27+0900"
|
||||
},
|
||||
{
|
||||
"step": 1,
|
||||
"name": "dependency-smoke-tests",
|
||||
"status": "pending"
|
||||
"name": "cmake-project-scaffold",
|
||||
"status": "completed",
|
||||
"summary": "Added the CMake/CTest presets and Harness config plus the fesa_core version API and fesa --version CLI scaffold.",
|
||||
"started_at": "2026-07-30T00:24:06+0900",
|
||||
"completed_at": "2026-07-30T00:46:21+0900"
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"name": "dependency-smoke-tests",
|
||||
"status": "completed",
|
||||
"started_at": "2026-07-30T12:18:16+0900",
|
||||
"summary": "Added installed-package discovery for MKL::MKL (LP64/dynamic/TBB), TBB::tbb, hdf5::hdf5-shared via HDF5::HDF5, and GoogleTest/GoogleMock plus a runtime dependency smoke test.",
|
||||
"completed_at": "2026-07-30T12:51:15+0900"
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "core-ids-and-diagnostics",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"summary": "Added dependency-free EntityId, Vec3 finiteness validation, SourceLocation/Diagnostic/Status headers, and core unit tests.",
|
||||
"started_at": "2026-07-30T12:53:17+0900",
|
||||
"completed_at": "2026-07-30T13:01:14+0900"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-29T23:58:15+0900",
|
||||
"completed_at": "2026-07-30T13:01:15+0900"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,62 +1,65 @@
|
||||
# Step 0: CMake Project Scaffold
|
||||
# Step 0: Harness Self Tests
|
||||
|
||||
## 읽어야 할 파일
|
||||
|
||||
먼저 아래 파일을 모두 읽고 저장소 계약을 파악하라.
|
||||
먼저 아래 파일을 모두 읽고 현재 Harness 계약을 파악하라.
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/PRD.md`
|
||||
- `/docs/ARCHITECTURE.md`
|
||||
- `/docs/ADR.md`
|
||||
- `/docs/HARNESS.md`
|
||||
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
|
||||
- `/.harness/config.example.json`
|
||||
- `/pytest.ini`
|
||||
- `/scripts/msvc_harness/config.py`
|
||||
- `/scripts/msvc_harness/discovery.py`
|
||||
- `/scripts/msvc_harness/models.py`
|
||||
- `/scripts/msvc_harness/process.py`
|
||||
- `/scripts/msvc_harness/tdd_policy.py`
|
||||
- `/scripts/hooks/stop_validation.py`
|
||||
|
||||
## 작업
|
||||
|
||||
C++20/MSVC x64 프로젝트의 최소 실행 가능한 build/test 뼈대를 만든다.
|
||||
현재 Harness Python 검증이 테스트 0개 수집으로 실패하는 기준선 문제를 해결한다.
|
||||
production Harness 동작은 변경하지 않고, 이미 문서화된 핵심 계약을 검증하는 최소
|
||||
자가 테스트를 작성한다.
|
||||
|
||||
- `CMakeLists.txt`, `CMakePresets.json`, `cmake/FesaDependencies.cmake`,
|
||||
`.harness/config.json`, `tests/CMakeLists.txt`를 만든다.
|
||||
- 실제 target은 `fesa_core` 정적 라이브러리와 `fesa` CLI 두 개만 만든다.
|
||||
- `include/fesa/core/version.hpp`, `src/fesa/core/version.cpp`,
|
||||
`src/fesa/cli/main.cpp`에 다음 계약을 구현한다.
|
||||
- 다음 파일을 만든다.
|
||||
- `tests/harness/test_config.py`
|
||||
- `tests/harness/test_discovery.py`
|
||||
- `tests/harness/test_process.py`
|
||||
- `test_config.py`는 기본 설정 로드, repository 밖 경로 거부, CMake preset 필드의
|
||||
all-or-none 규칙을 실제 임시 디렉터리로 검증한다.
|
||||
- `test_discovery.py`는 빈 저장소 건너뛰기, CMake 우선 감지, C/C++ 파일만 존재하는
|
||||
orphan 프로젝트 거부를 실제 임시 디렉터리로 검증한다.
|
||||
- `test_process.py`는 CTest JSON에 테스트가 0개이면 실패하고 하나 이상이면 통과하는
|
||||
결과 검사를 안전한 fake command runner로 검증한다. subprocess shell을 사용하지
|
||||
않는 계약도 유지한다.
|
||||
- 각 테스트는 공개 함수인 `load_config`, `discover_project`, `execute_plan`을 통해
|
||||
관찰 가능한 동작만 확인한다.
|
||||
|
||||
```cpp
|
||||
namespace fesa {
|
||||
[[nodiscard]] std::string_view version() noexcept;
|
||||
}
|
||||
```
|
||||
|
||||
- CLI는 이 step에서 `fesa --version`만 처리한다.
|
||||
- 먼저 `VersionCommand` CTest를 등록해 실패를 확인한 뒤 최소 구현한다.
|
||||
- `windows-debug`, `windows-release` configure/build/test preset을 정의한다.
|
||||
- build 산출물은 `out/build/<preset>` 아래에만 둔다.
|
||||
- MSVC가 아니거나 x64가 아니면 configure 단계에서 명확히 실패시킨다.
|
||||
이 step은 기존 production 동작의 characterization test만 추가한다. 테스트가 기존
|
||||
문서 계약과 다르게 실패하면 production 코드를 임의로 고치지 말고 step을 `blocked`로
|
||||
표시하고 불일치를 기록한다.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
```powershell
|
||||
uv run --with pytest python -m pytest -v -rs
|
||||
cmake --preset windows-debug
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R VersionCommand --output-on-failure
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
```
|
||||
|
||||
`fesa --version`은 비어 있지 않은 FESA 버전을 출력하고 종료 코드 0을 반환해야 한다.
|
||||
CTest는 0개가 아니어야 한다.
|
||||
pytest가 테스트를 한 개 이상 수집하고 모든 테스트가 통과해야 한다.
|
||||
|
||||
## 검증 절차
|
||||
|
||||
1. 테스트가 구현 전 실패하는 것을 확인한다.
|
||||
2. Acceptance Criteria 명령을 새로 실행한다.
|
||||
3. MSVC/C++20/x64와 산출물 경로를 확인한다.
|
||||
4. 성공 시 index의 step을 `completed`로 바꾸고 생성 파일을 summary에 기록한다.
|
||||
5. 도구가 없으면 자동 설치하지 말고 `blocked`와 정확한 누락 항목을 기록한다.
|
||||
1. 변경 전 명령이 테스트 0개 수집으로 실패한다는 기존 기준선 기록을 확인한다.
|
||||
2. 위 세 테스트 파일만 추가한다.
|
||||
3. Acceptance Criteria 명령을 새로 실행한다.
|
||||
4. production Harness 파일에 변경이 없는지 `git diff`로 확인한다.
|
||||
5. 성공 시 index의 step을 `completed`로 바꾸고 테스트 파일을 summary에 기록한다.
|
||||
|
||||
## 금지사항
|
||||
|
||||
- 외부 패키지를 다운로드하지 마라. 이유: 사전 설치 의존성 정책을 위반한다.
|
||||
- MKL, TBB, HDF5 기능을 구현하지 마라. 이유: 다음 step의 독립 범위다.
|
||||
- 빈 미래 모듈을 만들지 마라. 이유: Phase 1 최소 실체화 원칙을 위반한다.
|
||||
- production Harness 코드를 변경하지 마라. 이유: 이 step은 기존 계약의 기준선
|
||||
테스트만 마련한다.
|
||||
- C++ 프로젝트 파일이나 `.harness/config.json`을 만들지 마라. 이유: 다음 step의
|
||||
독립 범위다.
|
||||
- subprocess로 실제 CMake, MSBuild 또는 CTest를 실행하지 마라. 이유: self-test가
|
||||
개발 머신 도구 설치 상태에 의존하게 된다.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,55 +1,68 @@
|
||||
# Step 1: Dependency Smoke Tests
|
||||
# Step 1: CMake Project Scaffold
|
||||
|
||||
## 읽어야 할 파일
|
||||
|
||||
먼저 아래 파일을 모두 읽고 저장소 계약을 파악하라.
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/PRD.md`
|
||||
- `/docs/ARCHITECTURE.md`
|
||||
- `/docs/ADR.md`
|
||||
- `/docs/HARNESS.md`
|
||||
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
|
||||
- `/CMakeLists.txt`
|
||||
- `/CMakePresets.json`
|
||||
- `/cmake/FesaDependencies.cmake`
|
||||
- `/.harness/config.json`
|
||||
- `/tests/CMakeLists.txt`
|
||||
|
||||
이전 step의 CMake target과 preset을 그대로 확장하라.
|
||||
- `/.harness/config.example.json`
|
||||
- `/tests/harness/test_config.py`
|
||||
- `/tests/harness/test_discovery.py`
|
||||
- `/tests/harness/test_process.py`
|
||||
|
||||
## 작업
|
||||
|
||||
사전 설치된 oneMKL, oneTBB, HDF5 C API, GoogleTest/GoogleMock을 CMake imported
|
||||
target으로 찾고 링크 계약을 검증한다.
|
||||
C++20/Visual Studio 2026 MSVC v145 x64 프로젝트의 최소 실행 가능한 build/test
|
||||
뼈대를 만든다.
|
||||
|
||||
- `tests/unit/dependencies/dependency_smoke_test.cpp`를 먼저 작성한다.
|
||||
- 테스트는 MKL의 작은 vector 연산, TBB의 제한된 parallel loop, HDF5 임시 파일
|
||||
생성·닫기, GoogleTest 실행을 확인한다.
|
||||
- `FesaDependencies.cmake`는 `MKL::MKL`, TBB imported target, HDF5 C target,
|
||||
GoogleTest target을 제공해야 한다.
|
||||
- oneMKL은 LP64, dynamic link, TBB threading 조합을 사용한다.
|
||||
- runtime DLL 또는 architecture 불일치는 configure diagnostic으로 보고한다.
|
||||
- `CMakeLists.txt`, `CMakePresets.json`, `cmake/FesaDependencies.cmake`,
|
||||
`.harness/config.json`, `tests/CMakeLists.txt`를 만든다.
|
||||
- 실제 target은 `fesa_core` 정적 라이브러리와 `fesa` CLI 두 개만 만든다.
|
||||
- `include/fesa/core/version.hpp`, `src/fesa/core/version.cpp`,
|
||||
`src/fesa/cli/main.cpp`에 다음 계약을 구현한다.
|
||||
|
||||
```cpp
|
||||
namespace fesa {
|
||||
[[nodiscard]] std::string_view version() noexcept;
|
||||
}
|
||||
```
|
||||
|
||||
- CLI는 이 step에서 `fesa --version`만 처리한다.
|
||||
- 먼저 `VersionCommand` CTest를 등록해 실패를 확인한 뒤 최소 구현한다.
|
||||
- `windows-debug`, `windows-release` configure/build/test preset을 정의한다.
|
||||
- configure preset은 `Visual Studio 18 2026` 생성기와 `v145` toolset, x64
|
||||
architecture를 명시한다.
|
||||
- build 산출물은 `out/build/<preset>` 아래에만 둔다.
|
||||
- MSVC v145가 아니거나 x64가 아니면 configure 단계에서 명확히 실패시킨다.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
```powershell
|
||||
uv run --with pytest python -m pytest -v -rs
|
||||
cmake --preset windows-debug
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R DependencySmoke --output-on-failure
|
||||
ctest --preset windows-debug -R VersionCommand --output-on-failure
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
```
|
||||
|
||||
네 의존성을 실제 호출하는 smoke test가 통과해야 하며 새 MSVC 경고가 없어야 한다.
|
||||
`fesa --version`은 비어 있지 않은 FESA 버전을 출력하고 종료 코드 0을 반환해야 한다.
|
||||
CTest는 0개가 아니어야 한다.
|
||||
|
||||
## 검증 절차
|
||||
|
||||
1. smoke test를 먼저 추가하고 link 또는 실행 실패를 확인한다.
|
||||
2. dependency discovery와 target link만 최소 수정한다.
|
||||
3. 전체 configure/build/test를 새로 실행한다.
|
||||
4. 성공 시 정확한 imported target과 탐색 파일을 summary에 기록한다.
|
||||
5. 패키지나 MSVC가 없으면 세 차례 임의 수정하지 말고 `blocked`로 종료한다.
|
||||
1. 테스트가 구현 전 실패하는 것을 확인한다.
|
||||
2. Acceptance Criteria 명령을 새로 실행한다.
|
||||
3. Visual Studio 2026/MSVC v145/C++20/x64와 산출물 경로를 확인한다.
|
||||
4. 성공 시 index의 step을 `completed`로 바꾸고 생성 파일을 summary에 기록한다.
|
||||
5. 도구가 없으면 자동 설치하지 말고 `blocked`와 정확한 누락 항목을 기록한다.
|
||||
|
||||
## 금지사항
|
||||
|
||||
- FetchContent, vcpkg, Conan 또는 다운로드를 추가하지 마라. 이유: ADR-002 위반이다.
|
||||
- vendor 절대경로를 public header에 노출하지 마라. 이유: backend 격리를 깨뜨린다.
|
||||
- solver 기능을 구현하지 마라. 이유: 이 step은 build dependency 계약만 다룬다.
|
||||
- 외부 패키지를 다운로드하지 마라. 이유: 사전 설치 의존성 정책을 위반한다.
|
||||
- MKL, TBB, HDF5 기능을 구현하지 마라. 이유: 다음 step의 독립 범위다.
|
||||
- 빈 미래 모듈을 만들지 마라. 이유: Phase 1 최소 실체화 원칙을 위반한다.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"step": 2,
|
||||
"name": "dependency-smoke-tests",
|
||||
"exitCode": 0,
|
||||
"stdout": "The Harness Codex invocation produced the Step 2 source changes but exceeded its 1800-second timeout before returning. The supervising Codex recovered the step by running the documented acceptance commands in the same worktree with package locations supplied only through the build environment: cmake --preset windows-debug; cmake --build --preset windows-debug; ctest --preset windows-debug -R DependencySmoke --output-on-failure; ctest --preset windows-debug --output-on-failure. Configure and build succeeded without new MSVC warnings, DependencySmoke passed, and all 2 CTest tests passed. The 18 Harness Python self-tests also passed.",
|
||||
"stderr": "Recovered after the original Harness Codex subprocess timed out; no configure, build, or test failure remained.",
|
||||
"recovered": true
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# Step 2: Core IDs and Diagnostics
|
||||
# Step 2: Dependency Smoke Tests
|
||||
|
||||
## 읽어야 할 파일
|
||||
|
||||
@@ -6,67 +6,65 @@
|
||||
- `/docs/PRD.md`
|
||||
- `/docs/ARCHITECTURE.md`
|
||||
- `/docs/ADR.md`
|
||||
- `/docs/HARNESS.md`
|
||||
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
|
||||
- `/CMakeLists.txt`
|
||||
- `/CMakePresets.json`
|
||||
- `/cmake/FesaDependencies.cmake`
|
||||
- `/.harness/config.json`
|
||||
- `/tests/CMakeLists.txt`
|
||||
- `/include/fesa/core/version.hpp`
|
||||
|
||||
이전 step의 CMake target과 preset을 그대로 확장하라.
|
||||
|
||||
현재 개발환경에서 검증된 package 위치:
|
||||
|
||||
- HDF5 2.1.1:
|
||||
`C:/Program Files/HDF_Group/HDF5/2.1.1/cmake/hdf5-config.cmake`
|
||||
- HDF5 runtime:
|
||||
`C:/Program Files/HDF_Group/HDF5/2.1.1/bin`
|
||||
- GoogleTest/GoogleMock v1.17, VS2026/MSVC v145 x64:
|
||||
`C:/Users/baram/AppData/Local/FESA/dependencies/googletest-1.17.0-v145-x64-crt/lib/cmake/GTest/GTestConfig.cmake`
|
||||
|
||||
위 파일의 존재를 먼저 확인하고 package root를 `CMAKE_PREFIX_PATH` 또는
|
||||
`HDF5_DIR`/`GTest_DIR` hint로 사용하라. 이 절대경로를 production CMake에
|
||||
하드코딩하지 마라. 이유: 개발환경별 설치 위치를 build contract와 분리해야 한다.
|
||||
|
||||
## 작업
|
||||
|
||||
외부 라이브러리에 의존하지 않는 `core` 값 타입을 TDD로 구현한다.
|
||||
사전 설치된 oneMKL, oneTBB, HDF5 C API, GoogleTest/GoogleMock을 CMake imported
|
||||
target으로 찾고 링크 계약을 검증한다.
|
||||
|
||||
- 생성 파일:
|
||||
`include/fesa/core/entity_id.hpp`, `vec3.hpp`, `source_location.hpp`,
|
||||
`diagnostic.hpp`, `status.hpp`와 대응 테스트
|
||||
- 인터페이스:
|
||||
|
||||
```cpp
|
||||
template<class Tag>
|
||||
class EntityId final {
|
||||
public:
|
||||
explicit constexpr EntityId(std::int64_t value);
|
||||
[[nodiscard]] constexpr std::int64_t value() const noexcept;
|
||||
auto operator<=>(const EntityId&) const = default;
|
||||
};
|
||||
|
||||
struct Vec3 final { double x; double y; double z; };
|
||||
struct SourceLocation final {
|
||||
std::filesystem::path file;
|
||||
std::size_t line;
|
||||
std::size_t column;
|
||||
};
|
||||
enum class DiagnosticStage { io, syntax, semantic, model, equation, solver, results, validation };
|
||||
enum class Severity { warning, error };
|
||||
struct Diagnostic final {
|
||||
DiagnosticStage stage;
|
||||
Severity severity;
|
||||
std::string code;
|
||||
std::string message;
|
||||
std::optional<SourceLocation> source;
|
||||
};
|
||||
```
|
||||
|
||||
- typed ID의 잘못된 암시 변환, 음수 ID, nonfinite vector와 diagnostic source 보존을
|
||||
실패 테스트로 먼저 고정한다.
|
||||
- `tests/unit/dependencies/dependency_smoke_test.cpp`를 먼저 작성한다.
|
||||
- 테스트는 MKL의 작은 vector 연산, TBB의 제한된 parallel loop, HDF5 임시 파일
|
||||
생성·닫기, GoogleTest 실행을 확인한다.
|
||||
- `FesaDependencies.cmake`는 `MKL::MKL`, TBB imported target, HDF5 C target,
|
||||
GoogleTest target을 제공해야 한다.
|
||||
- oneMKL은 LP64, dynamic link, TBB threading 조합을 사용한다.
|
||||
- runtime DLL 또는 architecture 불일치는 configure diagnostic으로 보고한다.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
```powershell
|
||||
cmake --preset windows-debug
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R "Core|Diagnostic|EntityId" --output-on-failure
|
||||
ctest --preset windows-debug -R DependencySmoke --output-on-failure
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
```
|
||||
|
||||
네 의존성을 실제 호출하는 smoke test가 통과해야 하며 새 MSVC 경고가 없어야 한다.
|
||||
|
||||
## 검증 절차
|
||||
|
||||
1. production header 전에 실패하는 GoogleTest를 작성한다.
|
||||
2. 최소 값 타입만 구현한다.
|
||||
3. focused test와 전체 CTest를 실행한다.
|
||||
4. `core`가 MKL, TBB, HDF5, Abaqus header를 include하지 않는지 확인한다.
|
||||
5. index와 summary를 갱신한다.
|
||||
1. smoke test를 먼저 추가하고 link 또는 실행 실패를 확인한다.
|
||||
2. dependency discovery와 target link만 최소 수정한다.
|
||||
3. 전체 configure/build/test를 새로 실행한다.
|
||||
4. 성공 시 정확한 imported target과 탐색 파일을 summary에 기록한다.
|
||||
5. 패키지나 MSVC가 없으면 세 차례 임의 수정하지 말고 `blocked`로 종료한다.
|
||||
|
||||
## 금지사항
|
||||
|
||||
- 단위 변환 시스템을 만들지 마라. 이유: FESA는 일관 단위계만 사용한다.
|
||||
- 범용 reflection이나 serialization을 만들지 마라. 이유: 요구되지 않았다.
|
||||
- equation ID를 정의하지 마라. 이유: `DofManager` 단계의 책임이다.
|
||||
- FetchContent, vcpkg, Conan 또는 다운로드를 추가하지 마라. 이유: ADR-002 위반이다.
|
||||
- vendor 절대경로를 public header에 노출하지 마라. 이유: backend 격리를 깨뜨린다.
|
||||
- 현재 개발환경의 package 절대경로를 tracked CMake cache variable 기본값으로 넣지
|
||||
마라. 이유: 다른 개발환경의 configure를 깨뜨린다.
|
||||
- solver 기능을 구현하지 마라. 이유: 이 step은 build dependency 계약만 다룬다.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,73 @@
|
||||
# Step 3: Core IDs and Diagnostics
|
||||
|
||||
## 읽어야 할 파일
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/PRD.md`
|
||||
- `/docs/ARCHITECTURE.md`
|
||||
- `/docs/ADR.md`
|
||||
- `/docs/superpowers/plans/2026-07-29-fesa-phase-1.md`
|
||||
- `/CMakeLists.txt`
|
||||
- `/tests/CMakeLists.txt`
|
||||
- `/include/fesa/core/version.hpp`
|
||||
- `/tests/unit/dependencies/dependency_smoke_test.cpp`
|
||||
|
||||
## 작업
|
||||
|
||||
외부 라이브러리에 의존하지 않는 `core` 값 타입을 TDD로 구현한다.
|
||||
|
||||
- 생성 파일:
|
||||
`include/fesa/core/entity_id.hpp`, `vec3.hpp`, `source_location.hpp`,
|
||||
`diagnostic.hpp`, `status.hpp`와 대응 테스트
|
||||
- 인터페이스:
|
||||
|
||||
```cpp
|
||||
template<class Tag>
|
||||
class EntityId final {
|
||||
public:
|
||||
explicit constexpr EntityId(std::int64_t value);
|
||||
[[nodiscard]] constexpr std::int64_t value() const noexcept;
|
||||
auto operator<=>(const EntityId&) const = default;
|
||||
};
|
||||
|
||||
struct Vec3 final { double x; double y; double z; };
|
||||
struct SourceLocation final {
|
||||
std::filesystem::path file;
|
||||
std::size_t line;
|
||||
std::size_t column;
|
||||
};
|
||||
enum class DiagnosticStage { io, syntax, semantic, model, equation, solver, results, validation };
|
||||
enum class Severity { warning, error };
|
||||
struct Diagnostic final {
|
||||
DiagnosticStage stage;
|
||||
Severity severity;
|
||||
std::string code;
|
||||
std::string message;
|
||||
std::optional<SourceLocation> source;
|
||||
};
|
||||
```
|
||||
|
||||
- typed ID의 잘못된 암시 변환, 음수 ID, nonfinite vector와 diagnostic source 보존을
|
||||
실패 테스트로 먼저 고정한다.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
```powershell
|
||||
cmake --build --preset windows-debug
|
||||
ctest --preset windows-debug -R "Core|Diagnostic|EntityId" --output-on-failure
|
||||
ctest --preset windows-debug --output-on-failure
|
||||
```
|
||||
|
||||
## 검증 절차
|
||||
|
||||
1. production header 전에 실패하는 GoogleTest를 작성한다.
|
||||
2. 최소 값 타입만 구현한다.
|
||||
3. focused test와 전체 CTest를 실행한다.
|
||||
4. `core`가 MKL, TBB, HDF5, Abaqus header를 include하지 않는지 확인한다.
|
||||
5. index와 summary를 갱신한다.
|
||||
|
||||
## 금지사항
|
||||
|
||||
- 단위 변환 시스템을 만들지 마라. 이유: FESA는 일관 단위계만 사용한다.
|
||||
- 범용 reflection이나 serialization을 만들지 마라. 이유: 요구되지 않았다.
|
||||
- equation ID를 정의하지 마라. 이유: `DofManager` 단계의 책임이다.
|
||||
+132
-18
@@ -3,13 +3,14 @@
|
||||
Harness Step Executor — phase 내 step을 순차 실행하고 자가 교정한다.
|
||||
|
||||
Usage:
|
||||
python scripts/execute.py <phase-dir> [--push]
|
||||
python scripts/execute.py <phase-dir> [--push] [--codex-add-dir DIR]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@@ -26,6 +27,78 @@ class CodexEnvironmentError(RuntimeError):
|
||||
"""재시도로 해결할 수 없는 Codex CLI 환경 오류."""
|
||||
|
||||
|
||||
def normalize_codex_add_dirs(raw_dirs) -> tuple[Path, ...]:
|
||||
"""검증된 Codex 추가 writable directory를 반환한다."""
|
||||
normalized = []
|
||||
for raw in raw_dirs:
|
||||
candidate = Path(raw)
|
||||
if not candidate.is_absolute():
|
||||
raise CodexEnvironmentError(
|
||||
f"Codex 추가 경로는 절대 경로여야 합니다: {raw}"
|
||||
)
|
||||
resolved = candidate.resolve()
|
||||
if not resolved.is_dir():
|
||||
raise CodexEnvironmentError(
|
||||
f"Codex 추가 경로가 존재하는 디렉터리가 아닙니다: {resolved}"
|
||||
)
|
||||
normalized.append(resolved)
|
||||
return tuple(normalized)
|
||||
|
||||
|
||||
def build_codex_command(
|
||||
codex_executable: Path,
|
||||
root: Path,
|
||||
add_dirs: tuple[Path, ...],
|
||||
) -> list[str]:
|
||||
"""workspace-write Codex command를 안전한 argv로 구성한다."""
|
||||
command = [
|
||||
str(codex_executable.resolve()),
|
||||
"exec",
|
||||
"--json",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--dangerously-bypass-hook-trust",
|
||||
]
|
||||
for path in add_dirs:
|
||||
command.extend(("--add-dir", str(path)))
|
||||
command.extend(("--cd", str(root.resolve()), "-"))
|
||||
return command
|
||||
|
||||
|
||||
def build_codex_environment(
|
||||
base_env,
|
||||
add_dirs: tuple[Path, ...],
|
||||
) -> dict[str, str]:
|
||||
"""명시적 tool directory를 기존 PATH 뒤에 추가한다."""
|
||||
environment = dict(base_env)
|
||||
path_entries = [environment.get("PATH", "")]
|
||||
path_entries.extend(str(path) for path in add_dirs)
|
||||
environment["PATH"] = os.pathsep.join(entry for entry in path_entries if entry)
|
||||
return environment
|
||||
|
||||
|
||||
def run_utf8_process(command, *, cwd, prompt, env, timeout):
|
||||
"""UTF-8 prompt/output 계약으로 subprocess를 실행한다."""
|
||||
return subprocess.run(
|
||||
list(command),
|
||||
cwd=cwd,
|
||||
input=prompt,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
env=env,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
def configure_standard_streams(*streams) -> None:
|
||||
"""Windows legacy console에서 status 출력이 중단되지 않게 한다."""
|
||||
for stream in streams:
|
||||
reconfigure = getattr(stream, "reconfigure", None)
|
||||
if reconfigure is not None:
|
||||
reconfigure(errors="replace")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def progress_indicator(label: str):
|
||||
"""터미널 진행 표시기. with 문으로 사용하며 .elapsed 로 경과 시간을 읽는다."""
|
||||
@@ -62,13 +135,20 @@ class StepExecutor:
|
||||
CHORE_MSG = "chore({phase}): step {num} output"
|
||||
TZ = timezone(timedelta(hours=9))
|
||||
|
||||
def __init__(self, phase_dir_name: str, *, auto_push: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
phase_dir_name: str,
|
||||
*,
|
||||
auto_push: bool = False,
|
||||
codex_add_dirs=(),
|
||||
):
|
||||
self._root = str(ROOT)
|
||||
self._phases_dir = ROOT / "phases"
|
||||
self._phase_dir = self._phases_dir / phase_dir_name
|
||||
self._phase_dir_name = phase_dir_name
|
||||
self._top_index_file = self._phases_dir / "index.json"
|
||||
self._auto_push = auto_push
|
||||
self._codex_add_dirs = normalize_codex_add_dirs(codex_add_dirs)
|
||||
|
||||
if not self._phase_dir.is_dir():
|
||||
print(f"ERROR: {self._phase_dir} not found")
|
||||
@@ -241,26 +321,45 @@ class StepExecutor:
|
||||
sys.exit(1)
|
||||
|
||||
prompt = preamble + step_file.read_text(encoding="utf-8")
|
||||
command = [
|
||||
"codex",
|
||||
"exec",
|
||||
"--json",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--dangerously-bypass-hook-trust",
|
||||
"--cd",
|
||||
self._root,
|
||||
"-",
|
||||
]
|
||||
codex_executable = shutil.which("codex")
|
||||
if codex_executable is None:
|
||||
raise CodexEnvironmentError(
|
||||
"Codex CLI를 찾을 수 없습니다. codex를 설치하고 PATH를 확인하세요."
|
||||
)
|
||||
command = build_codex_command(
|
||||
Path(codex_executable),
|
||||
Path(self._root),
|
||||
self._codex_add_dirs,
|
||||
)
|
||||
environment = build_codex_environment(
|
||||
os.environ,
|
||||
self._codex_add_dirs,
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
result = run_utf8_process(
|
||||
command,
|
||||
cwd=self._root,
|
||||
input=prompt,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
prompt=prompt,
|
||||
env=environment,
|
||||
timeout=1800,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
stdout = exc.stdout or ""
|
||||
stderr = exc.stderr or ""
|
||||
if isinstance(stdout, bytes):
|
||||
stdout = stdout.decode("utf-8", errors="replace")
|
||||
if isinstance(stderr, bytes):
|
||||
stderr = stderr.decode("utf-8", errors="replace")
|
||||
timeout_message = (
|
||||
f"Codex timed out after {exc.timeout} seconds."
|
||||
)
|
||||
stderr = f"{stderr}\n{timeout_message}".strip()
|
||||
result = subprocess.CompletedProcess(
|
||||
command,
|
||||
returncode=124,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise CodexEnvironmentError(
|
||||
"Codex CLI를 찾을 수 없습니다. codex를 설치하고 PATH를 확인하세요."
|
||||
@@ -465,13 +564,28 @@ class StepExecutor:
|
||||
|
||||
|
||||
def main():
|
||||
configure_standard_streams(sys.stdout, sys.stderr)
|
||||
parser = argparse.ArgumentParser(description="Harness Step Executor")
|
||||
parser.add_argument("phase_dir", help="Phase directory name (e.g. 0-mvp)")
|
||||
parser.add_argument("--push", action="store_true", help="Push branch after completion")
|
||||
parser.add_argument(
|
||||
"--codex-add-dir",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="DIR",
|
||||
help=(
|
||||
"Codex workspace-write sandbox에 추가할 절대 tool/runtime directory. "
|
||||
"여러 번 지정할 수 있습니다."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
StepExecutor(args.phase_dir, auto_push=args.push).run()
|
||||
StepExecutor(
|
||||
args.phase_dir,
|
||||
auto_push=args.push,
|
||||
codex_add_dirs=args.codex_add_dir,
|
||||
).run()
|
||||
except CodexEnvironmentError as exc:
|
||||
print(f"ERROR: {exc}")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#include <fesa/core/version.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc == 2 && std::string_view{argv[1]} == "--version") {
|
||||
std::cout << fesa::version() << '\n';
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::cerr << "Usage: fesa --version\n";
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#include <fesa/core/version.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
std::string_view version() noexcept {
|
||||
return "0.1.0";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
add_test(
|
||||
NAME VersionCommand
|
||||
COMMAND "${CMAKE_BINARY_DIR}/$<CONFIG>/fesa.exe" --version
|
||||
)
|
||||
|
||||
set_tests_properties(
|
||||
VersionCommand
|
||||
PROPERTIES
|
||||
PASS_REGULAR_EXPRESSION "[0-9]+\\.[0-9]+\\.[0-9]+"
|
||||
)
|
||||
|
||||
add_executable(fesa_dependency_smoke_test
|
||||
unit/dependencies/dependency_smoke_test.cpp
|
||||
)
|
||||
|
||||
target_compile_features(fesa_dependency_smoke_test PRIVATE cxx_std_20)
|
||||
target_compile_options(fesa_dependency_smoke_test PRIVATE /W4 /permissive- /EHsc)
|
||||
|
||||
target_link_libraries(fesa_dependency_smoke_test
|
||||
PRIVATE
|
||||
MKL::MKL
|
||||
TBB::tbb
|
||||
HDF5::HDF5
|
||||
GTest::gtest_main
|
||||
GTest::gtest
|
||||
GTest::gmock
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME DependencySmoke
|
||||
COMMAND "$<TARGET_FILE:fesa_dependency_smoke_test>"
|
||||
)
|
||||
|
||||
set_property(
|
||||
TEST DependencySmoke
|
||||
PROPERTY ENVIRONMENT_MODIFICATION
|
||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ProductionConfigureWithoutGTest
|
||||
COMMAND
|
||||
"${CMAKE_COMMAND}"
|
||||
--fresh
|
||||
-S "${CMAKE_SOURCE_DIR}"
|
||||
-B "${CMAKE_BINARY_DIR}/testing/production-no-gtest"
|
||||
-G "${CMAKE_GENERATOR}"
|
||||
-A "${CMAKE_GENERATOR_PLATFORM}"
|
||||
-T "${CMAKE_VS_PLATFORM_TOOLSET}"
|
||||
-DBUILD_TESTING=OFF
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_GTest=TRUE
|
||||
"-DMKL_DIR=${MKL_DIR}"
|
||||
"-DTBB_DIR=${TBB_DIR}"
|
||||
"-DHDF5_DIR=${HDF5_DIR}"
|
||||
)
|
||||
|
||||
add_executable(fesa_core_value_tests
|
||||
unit/core/diagnostic_test.cpp
|
||||
unit/core/entity_id_test.cpp
|
||||
unit/core/status_test.cpp
|
||||
unit/core/vec3_test.cpp
|
||||
unit/core/version_test.cpp
|
||||
)
|
||||
|
||||
target_compile_features(fesa_core_value_tests PRIVATE cxx_std_20)
|
||||
target_compile_options(fesa_core_value_tests PRIVATE /W4 /permissive- /EHsc)
|
||||
|
||||
target_link_libraries(fesa_core_value_tests
|
||||
PRIVATE
|
||||
fesa_core
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME CoreValueTypes
|
||||
COMMAND "$<TARGET_FILE:fesa_core_value_tests>"
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.msvc_harness.config import ConfigError, load_config
|
||||
|
||||
|
||||
def _write_config(root, data):
|
||||
config_dir = root / ".harness"
|
||||
config_dir.mkdir()
|
||||
(config_dir / "config.json").write_text(
|
||||
json.dumps(data),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_load_config_uses_defaults(tmp_path):
|
||||
config = load_config(tmp_path)
|
||||
|
||||
assert config.version == 1
|
||||
assert config.project_type == "auto"
|
||||
assert config.cmake.source_dir == tmp_path.resolve()
|
||||
assert config.cmake.binary_dir is None
|
||||
assert config.tdd.test_roots == (
|
||||
(tmp_path / "tests").resolve(),
|
||||
(tmp_path / "test").resolve(),
|
||||
)
|
||||
|
||||
|
||||
def test_load_config_rejects_path_outside_repository(tmp_path):
|
||||
root = tmp_path / "repo"
|
||||
root.mkdir()
|
||||
_write_config(
|
||||
root,
|
||||
{
|
||||
"version": 1,
|
||||
"cmake": {
|
||||
"sourceDir": "../outside",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(ConfigError, match="outside the repository"):
|
||||
load_config(root)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"missing_field",
|
||||
["binaryDir", "configurePreset", "buildPreset", "testPreset"],
|
||||
)
|
||||
def test_load_config_requires_all_cmake_preset_fields(tmp_path, missing_field):
|
||||
cmake = {
|
||||
"binaryDir": "out/build/windows-debug",
|
||||
"configurePreset": "windows-debug",
|
||||
"buildPreset": "windows-debug",
|
||||
"testPreset": "windows-debug",
|
||||
}
|
||||
del cmake[missing_field]
|
||||
_write_config(
|
||||
tmp_path,
|
||||
{
|
||||
"version": 1,
|
||||
"cmake": cmake,
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(ConfigError, match="must be specified together"):
|
||||
load_config(tmp_path)
|
||||
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
|
||||
from scripts.msvc_harness.config import load_config
|
||||
from scripts.msvc_harness.discovery import DiscoveryError, discover_project
|
||||
from scripts.msvc_harness.models import ProjectKind
|
||||
|
||||
|
||||
def test_discover_project_skips_empty_repository(tmp_path):
|
||||
result = discover_project(tmp_path, load_config(tmp_path))
|
||||
|
||||
assert result.selection is None
|
||||
|
||||
|
||||
def test_discover_project_prefers_cmake(tmp_path):
|
||||
cmake_lists = tmp_path / "CMakeLists.txt"
|
||||
cmake_lists.write_text("cmake_minimum_required(VERSION 3.25)\n", encoding="utf-8")
|
||||
(tmp_path / "fallback.sln").write_text("", encoding="utf-8")
|
||||
|
||||
result = discover_project(tmp_path, load_config(tmp_path))
|
||||
|
||||
assert result.selection is not None
|
||||
assert result.selection.kind is ProjectKind.CMAKE
|
||||
assert result.selection.project_file == cmake_lists
|
||||
|
||||
|
||||
def test_discover_project_rejects_orphan_cpp_project(tmp_path):
|
||||
(tmp_path / "orphan.cpp").write_text("int value = 0;\n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(DiscoveryError, match="C/C\\+\\+ files exist"):
|
||||
discover_project(tmp_path, load_config(tmp_path))
|
||||
@@ -0,0 +1,214 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import scripts.execute as execute_module
|
||||
from scripts.execute import (
|
||||
CodexEnvironmentError,
|
||||
StepExecutor,
|
||||
build_codex_command,
|
||||
build_codex_environment,
|
||||
configure_standard_streams,
|
||||
normalize_codex_add_dirs,
|
||||
run_utf8_process,
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_codex_add_dirs_accepts_existing_absolute_directories(tmp_path):
|
||||
first = tmp_path / "tool"
|
||||
second = tmp_path / "runtime"
|
||||
first.mkdir()
|
||||
second.mkdir()
|
||||
|
||||
result = normalize_codex_add_dirs([str(first), str(second)])
|
||||
|
||||
assert result == (first.resolve(), second.resolve())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", ["relative/tool", "missing"])
|
||||
def test_normalize_codex_add_dirs_rejects_unsafe_paths(tmp_path, raw):
|
||||
candidate = raw if raw.startswith("relative") else str(tmp_path / raw)
|
||||
|
||||
with pytest.raises(CodexEnvironmentError):
|
||||
normalize_codex_add_dirs([candidate])
|
||||
|
||||
|
||||
def test_build_codex_command_grants_only_explicit_directories(tmp_path):
|
||||
root = tmp_path / "repo"
|
||||
tool = tmp_path / "tool"
|
||||
runtime = tmp_path / "runtime"
|
||||
root.mkdir()
|
||||
tool.mkdir()
|
||||
runtime.mkdir()
|
||||
codex = tmp_path / "codex.exe"
|
||||
|
||||
command = build_codex_command(
|
||||
codex,
|
||||
root,
|
||||
(tool.resolve(), runtime.resolve()),
|
||||
)
|
||||
|
||||
assert command == [
|
||||
str(codex),
|
||||
"exec",
|
||||
"--json",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--dangerously-bypass-hook-trust",
|
||||
"--add-dir",
|
||||
str(tool.resolve()),
|
||||
"--add-dir",
|
||||
str(runtime.resolve()),
|
||||
"--cd",
|
||||
str(root.resolve()),
|
||||
"-",
|
||||
]
|
||||
|
||||
|
||||
def test_build_codex_environment_appends_tool_directories(tmp_path):
|
||||
tool = (tmp_path / "tool").resolve()
|
||||
runtime = (tmp_path / "runtime").resolve()
|
||||
base = {"PATH": os.pathsep.join(("existing-one", "existing-two")), "KEEP": "value"}
|
||||
|
||||
result = build_codex_environment(base, (tool, runtime))
|
||||
|
||||
assert result["PATH"].split(os.pathsep) == [
|
||||
"existing-one",
|
||||
"existing-two",
|
||||
str(tool),
|
||||
str(runtime),
|
||||
]
|
||||
assert result["KEEP"] == "value"
|
||||
assert result is not base
|
||||
|
||||
|
||||
def test_run_utf8_process_sends_prompt_as_utf8(tmp_path):
|
||||
verifier = tmp_path / "verify_utf8.py"
|
||||
verifier.write_text(
|
||||
"import sys\n"
|
||||
"payload = sys.stdin.buffer.read()\n"
|
||||
"text = payload.decode('utf-8')\n"
|
||||
"sys.stdout.buffer.write(text.encode('utf-8'))\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
prompt = "FESA 한글 prompt"
|
||||
|
||||
result = run_utf8_process(
|
||||
[sys.executable, str(verifier)],
|
||||
cwd=tmp_path,
|
||||
prompt=prompt,
|
||||
env=os.environ.copy(),
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert result.stdout == prompt
|
||||
|
||||
|
||||
def test_invoke_codex_records_timeout_with_partial_output(tmp_path, monkeypatch):
|
||||
phase_dir = tmp_path / "phases" / "test-phase"
|
||||
phase_dir.mkdir(parents=True)
|
||||
(phase_dir / "step0.md").write_text("# Timed step\n", encoding="utf-8")
|
||||
codex = tmp_path / "codex.exe"
|
||||
codex.touch()
|
||||
|
||||
executor = StepExecutor.__new__(StepExecutor)
|
||||
executor._root = str(tmp_path)
|
||||
executor._phase_dir = phase_dir
|
||||
executor._codex_add_dirs = ()
|
||||
|
||||
monkeypatch.setattr(
|
||||
execute_module.shutil, "which", lambda executable: str(codex)
|
||||
)
|
||||
|
||||
def raise_timeout(*args, **kwargs):
|
||||
raise subprocess.TimeoutExpired(
|
||||
cmd=["codex", "exec"],
|
||||
timeout=1800,
|
||||
output=b"partial stdout",
|
||||
stderr=b"partial stderr",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(execute_module, "run_utf8_process", raise_timeout)
|
||||
|
||||
output = executor._invoke_codex(
|
||||
{"step": 0, "name": "timed-step"}, "preamble\n"
|
||||
)
|
||||
|
||||
assert output["exitCode"] == 124
|
||||
assert output["stdout"] == "partial stdout"
|
||||
assert "partial stderr" in output["stderr"]
|
||||
assert "timed out after 1800 seconds" in output["stderr"]
|
||||
assert (
|
||||
execute_module.json.loads(
|
||||
(phase_dir / "step0-output.json").read_text(encoding="utf-8")
|
||||
)
|
||||
== output
|
||||
)
|
||||
|
||||
|
||||
def test_execute_single_step_retries_timeout_three_times(tmp_path, monkeypatch):
|
||||
index_file = tmp_path / "index.json"
|
||||
index_file.write_text(
|
||||
execute_module.json.dumps(
|
||||
{
|
||||
"steps": [
|
||||
{"step": 0, "name": "timed-step", "status": "pending"}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
executor = StepExecutor.__new__(StepExecutor)
|
||||
executor._index_file = index_file
|
||||
executor._total = 1
|
||||
executor._project = "FESA"
|
||||
executor._phase_dir_name = "test-phase"
|
||||
attempts = 0
|
||||
|
||||
def timeout_output(step, preamble):
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
return {
|
||||
"step": 0,
|
||||
"name": "timed-step",
|
||||
"exitCode": 124,
|
||||
"stdout": "",
|
||||
"stderr": "Codex timed out after 1800 seconds.",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(executor, "_invoke_codex", timeout_output)
|
||||
monkeypatch.setattr(executor, "_commit_step", lambda *args: None)
|
||||
monkeypatch.setattr(executor, "_update_top_index", lambda *args: None)
|
||||
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
executor._execute_single_step(
|
||||
{"step": 0, "name": "timed-step", "status": "pending"}, ""
|
||||
)
|
||||
|
||||
assert exit_info.value.code == 1
|
||||
assert attempts == 3
|
||||
index = execute_module.json.loads(index_file.read_text(encoding="utf-8"))
|
||||
assert index["steps"][0]["status"] == "error"
|
||||
assert "timed out after 1800 seconds" in index["steps"][0]["error_message"]
|
||||
|
||||
|
||||
def test_configure_standard_streams_replaces_unencodable_status_characters():
|
||||
class RecordingStream:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def reconfigure(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
|
||||
stdout = RecordingStream()
|
||||
stderr = RecordingStream()
|
||||
|
||||
configure_standard_streams(stdout, stderr)
|
||||
|
||||
assert stdout.calls == [{"errors": "replace"}]
|
||||
assert stderr.calls == [{"errors": "replace"}]
|
||||
@@ -0,0 +1,74 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.msvc_harness.models import (
|
||||
CommandSpec,
|
||||
ProjectKind,
|
||||
ResultCheck,
|
||||
ResultCheckKind,
|
||||
ValidationPlan,
|
||||
ValidationStep,
|
||||
)
|
||||
from scripts.msvc_harness.process import ValidationFailure, execute_plan
|
||||
|
||||
|
||||
def _ctest_plan(root):
|
||||
command = CommandSpec(
|
||||
("ctest", "--show-only=json-v1"),
|
||||
root,
|
||||
"CTest discovery",
|
||||
)
|
||||
check = ResultCheck(ResultCheckKind.CTEST_HAS_TESTS)
|
||||
return ValidationPlan(
|
||||
ProjectKind.CMAKE,
|
||||
(ValidationStep(command, (check,)),),
|
||||
)
|
||||
|
||||
|
||||
def test_execute_plan_rejects_ctest_json_with_no_tests(tmp_path):
|
||||
def fake_run(*args, **kwargs):
|
||||
return SimpleNamespace(returncode=0, stdout='{"tests": []}', stderr="")
|
||||
|
||||
with pytest.raises(ValidationFailure, match="CTest discovered no tests"):
|
||||
execute_plan(
|
||||
_ctest_plan(tmp_path),
|
||||
tmp_path,
|
||||
deadline=10,
|
||||
run=fake_run,
|
||||
clock=lambda: 0,
|
||||
)
|
||||
|
||||
|
||||
def test_execute_plan_accepts_ctest_json_with_tests_without_shell(tmp_path):
|
||||
calls = []
|
||||
|
||||
def fake_run(*args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout='{"tests": [{"name": "unit"}]}',
|
||||
stderr="",
|
||||
)
|
||||
|
||||
results = execute_plan(
|
||||
_ctest_plan(tmp_path),
|
||||
tmp_path,
|
||||
deadline=10,
|
||||
run=fake_run,
|
||||
clock=lambda: 0,
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
assert calls == [
|
||||
(
|
||||
(["ctest", "--show-only=json-v1"],),
|
||||
{
|
||||
"cwd": tmp_path.resolve(),
|
||||
"env": None,
|
||||
"capture_output": True,
|
||||
"shell": False,
|
||||
"timeout": 10,
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(Diagnostic, PreservesSourceLocation) {
|
||||
const fesa::Diagnostic diagnostic{
|
||||
fesa::DiagnosticStage::syntax,
|
||||
fesa::Severity::error,
|
||||
"ABAQUS_INVALID_NODE",
|
||||
"Node data is incomplete.",
|
||||
fesa::SourceLocation{
|
||||
std::filesystem::path{"models/beam.inp"}, 42, 9}};
|
||||
|
||||
ASSERT_TRUE(diagnostic.source.has_value());
|
||||
EXPECT_EQ(
|
||||
diagnostic.source->file, std::filesystem::path{"models/beam.inp"});
|
||||
EXPECT_EQ(diagnostic.source->line, 42U);
|
||||
EXPECT_EQ(diagnostic.source->column, 9U);
|
||||
}
|
||||
|
||||
TEST(Diagnostic, AllowsDiagnosticsWithoutSourceLocation) {
|
||||
const fesa::Diagnostic diagnostic{
|
||||
fesa::DiagnosticStage::solver,
|
||||
fesa::Severity::warning,
|
||||
"SOLVER_RESIDUAL",
|
||||
"Residual is above the reporting threshold.",
|
||||
std::nullopt};
|
||||
|
||||
EXPECT_FALSE(diagnostic.source.has_value());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,36 @@
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <fesa/core/entity_id.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
struct NodeTag;
|
||||
struct ElementTag;
|
||||
|
||||
using NodeId = fesa::EntityId<NodeTag>;
|
||||
using ElementId = fesa::EntityId<ElementTag>;
|
||||
|
||||
static_assert(std::is_constructible_v<NodeId, std::int64_t>);
|
||||
static_assert(!std::is_convertible_v<std::int64_t, NodeId>);
|
||||
static_assert(!std::is_convertible_v<NodeId, ElementId>);
|
||||
|
||||
TEST(EntityId, PreservesValueAndSupportsTypedOrdering) {
|
||||
constexpr NodeId first{7};
|
||||
constexpr NodeId second{11};
|
||||
|
||||
static_assert(first.value() == 7);
|
||||
static_assert(first < second);
|
||||
|
||||
EXPECT_EQ(first.value(), 7);
|
||||
EXPECT_LT(first, second);
|
||||
}
|
||||
|
||||
TEST(EntityId, RejectsNegativeValues) {
|
||||
EXPECT_THROW((NodeId{-1}), std::invalid_argument);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,35 @@
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <fesa/core/status.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(CoreStatus, PreservesFailureDiagnostics) {
|
||||
const fesa::Status status{
|
||||
false,
|
||||
{fesa::Diagnostic{
|
||||
fesa::DiagnosticStage::model,
|
||||
fesa::Severity::error,
|
||||
"MODEL_NONFINITE_VECTOR",
|
||||
"A vector component is not finite.",
|
||||
fesa::SourceLocation{
|
||||
std::filesystem::path{"models/invalid.inp"}, 8, 3}}}};
|
||||
|
||||
EXPECT_FALSE(status.succeeded);
|
||||
ASSERT_EQ(status.diagnostics.size(), 1U);
|
||||
EXPECT_EQ(status.diagnostics.front().code, "MODEL_NONFINITE_VECTOR");
|
||||
ASSERT_TRUE(status.diagnostics.front().source.has_value());
|
||||
EXPECT_EQ(status.diagnostics.front().source->line, 8U);
|
||||
}
|
||||
|
||||
TEST(CoreStatus, RepresentsSuccessWithoutDiagnostics) {
|
||||
const fesa::Status status{true, {}};
|
||||
|
||||
EXPECT_TRUE(status.succeeded);
|
||||
EXPECT_TRUE(status.diagnostics.empty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,20 @@
|
||||
#include <limits>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <fesa/core/vec3.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(CoreVec3, ReportsOnlyFiniteVectorsAsFinite) {
|
||||
EXPECT_TRUE(fesa::is_finite(fesa::Vec3{1.0, -2.0, 0.0}));
|
||||
|
||||
const double infinity = std::numeric_limits<double>::infinity();
|
||||
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||
|
||||
EXPECT_FALSE(fesa::is_finite(fesa::Vec3{infinity, 0.0, 0.0}));
|
||||
EXPECT_FALSE(fesa::is_finite(fesa::Vec3{0.0, -infinity, 0.0}));
|
||||
EXPECT_FALSE(fesa::is_finite(fesa::Vec3{0.0, 0.0, nan}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,11 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <fesa/core/version.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(Version, ReportsPhaseOneSemanticVersion) {
|
||||
EXPECT_EQ(fesa::version(), "0.1.0");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,56 @@
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <hdf5.h>
|
||||
#include <mkl.h>
|
||||
#include <oneapi/tbb/global_control.h>
|
||||
#include <oneapi/tbb/parallel_for.h>
|
||||
|
||||
namespace {
|
||||
|
||||
static_assert(sizeof(MKL_INT) == 4, "FESA requires the oneMKL LP64 interface.");
|
||||
|
||||
TEST(DependencySmoke, CallsOneMklVectorOperation) {
|
||||
constexpr std::array<double, 3> x{1.0, 2.0, 3.0};
|
||||
std::array<double, 3> y{4.0, 5.0, 6.0};
|
||||
|
||||
cblas_daxpy(
|
||||
static_cast<MKL_INT>(x.size()), 2.0, x.data(), 1, y.data(), 1);
|
||||
|
||||
EXPECT_THAT(y, ::testing::ElementsAre(6.0, 9.0, 12.0));
|
||||
}
|
||||
|
||||
TEST(DependencySmoke, RunsLimitedOneTbbParallelLoop) {
|
||||
std::array<std::size_t, 16> values{};
|
||||
const oneapi::tbb::global_control limit{
|
||||
oneapi::tbb::global_control::max_allowed_parallelism, 2};
|
||||
|
||||
oneapi::tbb::parallel_for(
|
||||
std::size_t{0}, values.size(), [&values](const std::size_t index) {
|
||||
values[index] = index + 1;
|
||||
});
|
||||
|
||||
EXPECT_THAT(values, ::testing::ElementsAre(
|
||||
1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U,
|
||||
9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U));
|
||||
}
|
||||
|
||||
TEST(DependencySmoke, CreatesAndClosesHdf5File) {
|
||||
const auto path =
|
||||
std::filesystem::path{::testing::TempDir()} /
|
||||
"fesa_dependency_smoke.h5";
|
||||
|
||||
const hid_t file = H5Fcreate(
|
||||
path.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
|
||||
ASSERT_GE(file, 0);
|
||||
EXPECT_GE(H5Fclose(file), 0);
|
||||
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path, error);
|
||||
EXPECT_FALSE(error);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user