Skip to content

Commit e3ddc2c

Browse files
authored
feat(task2): add dsp_fixture native test — float* + struct-out-param patterns
1 parent 6e1c63c commit e3ddc2c

6 files changed

Lines changed: 397 additions & 1 deletion

File tree

native/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ include(cmake/ferrum_helpers.cmake)
1616
# without any external consumer source. A real consumer would instead call
1717
# ferrum_add_static_library(my_lib SOURCES ...) from their own CMakeLists.
1818
# ---------------------------------------------------------------------------
19-
option(FERRUM_BUILD_TEST_STUB "Build the in-tree add() test stub" ON)
19+
option(FERRUM_BUILD_TEST_STUB "Build the in-tree add() test stub" ON)
20+
option(FERRUM_BUILD_DSP_FIXTURE "Build the in-tree DSP test fixture" ON)
2021

2122
if(FERRUM_BUILD_TEST_STUB)
2223
add_subdirectory(test_stub)
2324
endif()
25+
26+
if(FERRUM_BUILD_DSP_FIXTURE)
27+
add_subdirectory(dsp_fixture)
28+
endif()

native/dsp_fixture/CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
cmake_minimum_required(VERSION 3.21)
2+
project(ferrum_dsp_fixture LANGUAGES C)
3+
4+
include(../cmake/ferrum_helpers.cmake)
5+
6+
ferrum_add_static_library(ferrum_dsp_fixture
7+
SOURCES src/ferrum_dsp.c
8+
INCLUDES include
9+
)
10+
11+
include(GNUInstallDirs)
12+
install(TARGETS ferrum_dsp_fixture
13+
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
14+
)
15+
install(DIRECTORY include/ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* ferrum_dsp.h — Second test fixture for the Ferrum codegen/build pipeline.
3+
*
4+
* This header exercises the two real-consumer patterns that ferrum_add(int,int)
5+
* cannot cover:
6+
* 1. A function that takes a float* buffer with an int length.
7+
* 2. A function that writes a result through a struct out-parameter.
8+
*
9+
* Both patterns appear constantly in audio, signal, and ML processing ABIs and
10+
* must round-trip through ferrum-codegen without error.
11+
*
12+
* This file is NOT domain-specific application logic; it is framework test
13+
* infrastructure that validates the plumbing.
14+
*/
15+
#ifndef FERRUM_DSP_H
16+
#define FERRUM_DSP_H
17+
18+
#include <stdint.h>
19+
20+
#ifdef __cplusplus
21+
extern "C" {
22+
#endif
23+
24+
/**
25+
* Fixed-layout, blittable statistics summary.
26+
* All fields are 32-bit; no padding needed on any supported ABI.
27+
*/
28+
typedef struct {
29+
float min_val; /**< Minimum value in the buffer. */
30+
float max_val; /**< Maximum value in the buffer. */
31+
float mean; /**< Arithmetic mean of the buffer.*/
32+
int32_t count; /**< Number of elements processed. */
33+
} FerrumDspStats;
34+
35+
/**
36+
* Multiplies every element in buf[0..len-1] by factor in-place.
37+
* Validates the (float* buf, int32_t len) parameter pattern.
38+
*/
39+
void ferrum_dsp_scale(float* buf, int32_t len, float factor);
40+
41+
/**
42+
* Computes min, max, mean, and count of buf[0..len-1] and writes
43+
* the result through the out-parameter.
44+
* Validates the const-float* input + blittable-struct-out-parameter pattern.
45+
*/
46+
void ferrum_dsp_stats(const float* buf, int32_t len, FerrumDspStats* result);
47+
48+
#ifdef __cplusplus
49+
}
50+
#endif
51+
52+
#endif /* FERRUM_DSP_H */
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* ferrum_dsp.c — Implementation of the Ferrum DSP test fixture.
3+
* See ferrum_dsp.h for documentation.
4+
*/
5+
#include "ferrum_dsp.h"
6+
7+
void ferrum_dsp_scale(float* buf, int32_t len, float factor)
8+
{
9+
for (int32_t i = 0; i < len; ++i)
10+
buf[i] *= factor;
11+
}
12+
13+
void ferrum_dsp_stats(const float* buf, int32_t len, FerrumDspStats* result)
14+
{
15+
if (len <= 0 || !buf || !result)
16+
return;
17+
18+
float min_val = buf[0];
19+
float max_val = buf[0];
20+
float sum = 0.0f;
21+
22+
for (int32_t i = 0; i < len; ++i)
23+
{
24+
float v = buf[i];
25+
if (v < min_val) min_val = v;
26+
if (v > max_val) max_val = v;
27+
sum += v;
28+
}
29+
30+
result->min_val = min_val;
31+
result->max_val = max_val;
32+
result->mean = sum / (float)len;
33+
result->count = len;
34+
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
using Xunit;
2+
using Ferrum.Codegen.Emitter;
3+
using Ferrum.Codegen.Parser;
4+
5+
namespace Ferrum.Framework.Tests.Codegen;
6+
7+
/// <summary>
8+
/// Exercises ferrum-codegen against the DSP fixture header, which contains two
9+
/// real-consumer patterns not covered by the trivial <c>ferrum_add(int,int)</c>
10+
/// test stub:
11+
/// <list type="bullet">
12+
/// <item>A function taking a <c>float*</c> buffer and an <c>int32_t</c> length.</item>
13+
/// <item>A function with a blittable struct out-parameter.</item>
14+
/// </list>
15+
/// </summary>
16+
public sealed class DspFixtureCodegenTests
17+
{
18+
// ── Inline copy of native/dsp_fixture/include/ferrum_dsp.h ───────────────
19+
// Using an inline copy makes the tests self-contained and runnable without
20+
// requiring the native tree to be present in the test working directory.
21+
private const string DspHeader = @"
22+
#ifndef FERRUM_DSP_H
23+
#define FERRUM_DSP_H
24+
25+
#include <stdint.h>
26+
27+
#ifdef __cplusplus
28+
extern ""C"" {
29+
#endif
30+
31+
typedef struct {
32+
float min_val;
33+
float max_val;
34+
float mean;
35+
int32_t count;
36+
} FerrumDspStats;
37+
38+
void ferrum_dsp_scale(float* buf, int32_t len, float factor);
39+
void ferrum_dsp_stats(const float* buf, int32_t len, FerrumDspStats* result);
40+
41+
#ifdef __cplusplus
42+
}
43+
#endif
44+
45+
#endif
46+
";
47+
48+
private readonly HeaderParser _parser = new();
49+
50+
// ── Struct extraction ─────────────────────────────────────────────────────
51+
52+
[Fact]
53+
public void Parse_DspFixture_ExtractsOneStruct()
54+
{
55+
var header = _parser.Parse(DspHeader);
56+
Assert.Single(header.Structs);
57+
Assert.Equal("FerrumDspStats", header.Structs[0].Name);
58+
}
59+
60+
[Fact]
61+
public void Parse_DspFixture_StructHasFourFields()
62+
{
63+
var header = _parser.Parse(DspHeader);
64+
Assert.Equal(4, header.Structs[0].Fields.Count);
65+
}
66+
67+
[Fact]
68+
public void Parse_DspFixture_StructFloatFieldsMapped()
69+
{
70+
var header = _parser.Parse(DspHeader);
71+
var s = header.Structs[0];
72+
Assert.Equal("float", s.Fields[0].Type.TypeName); // min_val
73+
Assert.Equal("float", s.Fields[1].Type.TypeName); // max_val
74+
Assert.Equal("float", s.Fields[2].Type.TypeName); // mean
75+
}
76+
77+
[Fact]
78+
public void Parse_DspFixture_StructInt32FieldMapped()
79+
{
80+
var header = _parser.Parse(DspHeader);
81+
var s = header.Structs[0];
82+
Assert.Equal("int", s.Fields[3].Type.TypeName); // count (int32_t → int)
83+
Assert.Equal("count", s.Fields[3].Name);
84+
}
85+
86+
// ── Function extraction ───────────────────────────────────────────────────
87+
88+
[Fact]
89+
public void Parse_DspFixture_ExtractsTwoFunctions()
90+
{
91+
var header = _parser.Parse(DspHeader);
92+
Assert.Equal(2, header.Functions.Count);
93+
}
94+
95+
[Fact]
96+
public void Parse_DspFixture_ScaleFunction_FloatBufferParam()
97+
{
98+
var header = _parser.Parse(DspHeader);
99+
var fn = header.Functions.Single(f => f.Name == "ferrum_dsp_scale");
100+
Assert.Equal("float*", fn.Parameters[0].Type.TypeName);
101+
Assert.True(fn.Parameters[0].Type.IsPointer);
102+
Assert.Equal("buf", fn.Parameters[0].Name);
103+
}
104+
105+
[Fact]
106+
public void Parse_DspFixture_ScaleFunction_LenParam()
107+
{
108+
var header = _parser.Parse(DspHeader);
109+
var fn = header.Functions.Single(f => f.Name == "ferrum_dsp_scale");
110+
Assert.Equal("int", fn.Parameters[1].Type.TypeName);
111+
Assert.Equal("len", fn.Parameters[1].Name);
112+
}
113+
114+
[Fact]
115+
public void Parse_DspFixture_ScaleFunction_FactorParam()
116+
{
117+
var header = _parser.Parse(DspHeader);
118+
var fn = header.Functions.Single(f => f.Name == "ferrum_dsp_scale");
119+
Assert.Equal("float", fn.Parameters[2].Type.TypeName);
120+
Assert.Equal("factor", fn.Parameters[2].Name);
121+
}
122+
123+
[Fact]
124+
public void Parse_DspFixture_StatsFunction_ConstFloatPointerParam()
125+
{
126+
var header = _parser.Parse(DspHeader);
127+
var fn = header.Functions.Single(f => f.Name == "ferrum_dsp_stats");
128+
// const float* maps to float* (const is stripped — no MarshalAs, blittable only)
129+
Assert.True(fn.Parameters[0].Type.IsPointer);
130+
Assert.Equal("buf", fn.Parameters[0].Name);
131+
}
132+
133+
[Fact]
134+
public void Parse_DspFixture_StatsFunction_StructOutParam()
135+
{
136+
var header = _parser.Parse(DspHeader);
137+
var fn = header.Functions.Single(f => f.Name == "ferrum_dsp_stats");
138+
Assert.Equal("FerrumDspStats*", fn.Parameters[2].Type.TypeName);
139+
Assert.True(fn.Parameters[2].Type.IsPointer);
140+
Assert.Equal("result", fn.Parameters[2].Name);
141+
}
142+
143+
[Fact]
144+
public void Parse_DspFixture_BothFunctionsReturnVoid()
145+
{
146+
var header = _parser.Parse(DspHeader);
147+
foreach (var fn in header.Functions)
148+
{
149+
Assert.Equal("void", fn.ReturnType.TypeName);
150+
Assert.True(fn.ReturnType.IsVoid);
151+
}
152+
}
153+
154+
// ── Emitter round-trip ────────────────────────────────────────────────────
155+
156+
[Fact]
157+
public void Emit_DspFixture_ContainsStructLayout()
158+
{
159+
var header = _parser.Parse(DspHeader);
160+
var emitter = new BindingEmitter(new EmitterOptions
161+
{
162+
Namespace = "Ferrum.Tests.Generated",
163+
ClassName = "DspBindings",
164+
LibraryName = "__Internal",
165+
});
166+
string output = emitter.Emit(header);
167+
168+
Assert.Contains("[StructLayout(LayoutKind.Sequential)]", output);
169+
Assert.Contains("public unsafe struct FerrumDspStats", output);
170+
}
171+
172+
[Fact]
173+
public void Emit_DspFixture_ContainsLibraryImportForBothFunctions()
174+
{
175+
var header = _parser.Parse(DspHeader);
176+
var emitter = new BindingEmitter(new EmitterOptions
177+
{
178+
Namespace = "Ferrum.Tests.Generated",
179+
ClassName = "DspBindings",
180+
LibraryName = "__Internal",
181+
});
182+
string output = emitter.Emit(header);
183+
184+
Assert.Contains("ferrum_dsp_scale", output);
185+
Assert.Contains("ferrum_dsp_stats", output);
186+
Assert.Contains("[LibraryImport(LibraryName)]", output);
187+
}
188+
189+
[Fact]
190+
public void Emit_DspFixture_UsesInternalModifier()
191+
{
192+
var header = _parser.Parse(DspHeader);
193+
var emitter = new BindingEmitter(new EmitterOptions
194+
{
195+
Namespace = "Ferrum.Tests.Generated",
196+
ClassName = "DspBindings",
197+
LibraryName = "__Internal",
198+
});
199+
string output = emitter.Emit(header);
200+
201+
// Generated class must be internal (not public) to follow least-privilege convention
202+
Assert.Contains("internal static unsafe partial class DspBindings", output);
203+
}
204+
205+
[Fact]
206+
public void Emit_DspFixture_OutputMatchesCommittedBindings()
207+
{
208+
// Regression guard: the pre-generated FerrumDspBindings.cs committed to
209+
// the repo must stay in sync with what the tool would actually produce.
210+
// If this test fails, re-run ferrum-codegen against the fixture header
211+
// and commit the updated output.
212+
var header = _parser.Parse(DspHeader, "ferrum_dsp.h");
213+
var emitter = new BindingEmitter(new EmitterOptions
214+
{
215+
Namespace = "Ferrum.Tests.Generated",
216+
ClassName = "DspBindings",
217+
LibraryName = "__Internal",
218+
});
219+
string generated = emitter.Emit(header);
220+
221+
// Load the committed file
222+
string committedPath = FindCommittedBindingsFile();
223+
string committed = File.ReadAllText(committedPath);
224+
225+
Assert.Equal(
226+
committed.ReplaceLineEndings("\n"),
227+
generated.ReplaceLineEndings("\n"),
228+
StringComparer.Ordinal);
229+
}
230+
231+
// ── Helpers ───────────────────────────────────────────────────────────────
232+
233+
/// <summary>
234+
/// Locates the committed <c>FerrumDspBindings.cs</c> by walking up from the
235+
/// test assembly's directory until the repo root is found.
236+
/// </summary>
237+
private static string FindCommittedBindingsFile()
238+
{
239+
string dir = AppContext.BaseDirectory;
240+
while (!string.IsNullOrEmpty(dir))
241+
{
242+
string candidate = Path.Combine(
243+
dir,
244+
"src", "Framework.Tests", "Codegen", "Generated", "FerrumDspBindings.cs");
245+
if (File.Exists(candidate))
246+
return candidate;
247+
dir = Path.GetDirectoryName(dir)!;
248+
}
249+
throw new FileNotFoundException(
250+
"Cannot locate FerrumDspBindings.cs. "
251+
+ "Ensure the repo root is an ancestor of the test output directory.");
252+
}
253+
}

0 commit comments

Comments
 (0)