This commit is contained in:
Steven Dan
2025-12-11 09:43:42 +08:00
commit d8b2974133
1822 changed files with 280037 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.21)
include($ENV{XMOS_CMAKE_PATH}/xcommon.cmake)
project(unit_test)
set(APP_HW_TARGET XCORE-AI-EXPLORER)
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../..)
set(APP_DEPENDENT_MODULES lib_unity lib_audio_dsp)
set(APP_INCLUDES build/dummy_pipeline ${CMAKE_CURRENT_LIST_DIR})
file(GLOB APP_C_SRCS CONFIGURE_DEPENDS RELATIVE ${CMAKE_CURRENT_LIST_DIR} src/*.c build/dummy_pipeline/*.c)
file(GLOB ADSP_ADDITIONAL_STAGE_CONFIG ${CMAKE_CURRENT_LIST_DIR}/stages/*.yaml )
file(GLOB tests RELATIVE ${CMAKE_CURRENT_LIST_DIR} CONFIGURE_DEPENDS src/test*.c)
foreach(test_file ${tests})
get_filename_component(test_name ${test_file} NAME_WE)
set(SOURCE_FILES_${test_name} ${test_file})
set(APP_COMPILER_FLAGS_${test_name}
-O0
-g
-report
-Wall
# -Werror
-fxscope
-DDEBUG_PRINT_ENABLE=1)
endforeach()
set(LIB_AUDIO_DSP_DISABLE_OPTIMISATION ON)
set(LIB_UNITY_AUTO_TEST_RUNNER ON)
set(LIB_UNITY_USE_FIXTURE OFF)
set(LIB_UNITY_USE_MEMORY OFF)
XMOS_REGISTER_APP()

View File

@@ -0,0 +1,86 @@
# Copyright 2021-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import pytest
import subprocess
from pathlib import Path
import re
import os
from audio_dsp.design import pipeline
from stages.dummy import Dummy
def gen_dummy_pipeline():
p = pipeline.Pipeline(1, identifier="dummy")
d = p.stage(Dummy, p.i, label="dummy")
p.set_outputs(d)
pipeline.generate_dsp_main(p, out_dir=Path(__file__).parent / "build/dummy_pipeline")
def pytest_configure():
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
if worker_id is None:
# dont run on the worker threads
gen_dummy_pipeline()
if not Path("bin").exists():
subprocess.run(["cmake", "-B", "build"], check=True)
subprocess.run(["cmake", "--build", "build"], check=True)
## Begin pytest magic to convert xe files into tests
def pytest_collect_file(parent, path):
"""Custom collection function to inform pytest that xe files contain tests."""
if path.ext == ".xe":
return UnityTestSource.from_parent(parent, path=Path(path))
class UnityTestSource(pytest.File):
"""
Each xe file contains 1 pytest test.
"""
def collect(self):
yield UnityTestExecutable.from_parent(self, xe=self.path, name=self.path.stem)
class UnityTestExecutable(pytest.Item):
"""
Run the xe file in xsim, this is the work of the test.
"""
def __init__(self, xe, **kwargs):
super().__init__(**kwargs)
self.xe=xe
self.fail_reason=[]
def runtest(self):
"""
fancy test output processing.
"""
proc = subprocess.run(["xsim", self.xe], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self.add_report_section("call", "stdout", proc.stdout)
unity_result_pattern=r"^(?P<path>[^\n:]+):(?P<line>\d+):(?P<name>[^:]+):(?P<status>PASS|FAIL)(: (?P<message>.*))?$"
unlikely_repl = "unlikely_repl"
result = [i for i in re.finditer(unity_result_pattern, proc.stdout, re.MULTILINE)]
all_out = [i for i in re.sub(unity_result_pattern, unlikely_repl, proc.stdout, flags=re.MULTILINE).split(unlikely_repl)]
for match, output in zip(result, all_out):
file, line, test_name, status, message = match.group("path", "line", "name", "status", "message")
fail_reason = f"{test_name}:{line}:{message}"
self.add_report_section("call", f"{status} {test_name}", output + "\n" + fail_reason)
if status == "FAIL":
self.fail_reason.append(fail_reason)
if proc.returncode:
raise UnityTestException
def repr_failure(self, excinfo):
if isinstance(excinfo.value, UnityTestException):
return "Failure summary:\n\t" + "\n\t".join(self.fail_reason)
return super().repr_failure(excinfo)
def reportinfo(self):
return self.path, 0, self.xe.stem
class UnityTestException(Exception):
pass
## End pytest magic

View File

@@ -0,0 +1,153 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
/// Unit tests for adsp_control.c
#include <unity.h>
#include <stages/adsp_control.h>
#include <stages/dummy.h>
#include <adsp_generated_dummy.h>
#include <adsp_instance_id_dummy.h>
#include <cmds.h>
#include <stdio.h>
#include <swlock.h>
void setUp(){}
void tearDown(){}
/// Test that basic read and write works.
void test_basic_control(void) {
adsp_pipeline_t* p = adsp_dummy_pipeline_init();
module_instance_t* dummy_inst = &p->modules[dummy_stage_index];
adsp_controller_t ctrl;
adsp_controller_init(&ctrl, p);
int test_val = 5;
adsp_stage_control_cmd_t cmd = {
.instance_id = dummy_stage_index,
.cmd_id = CMD_DUMMY_DUMMY_FIELD,
.payload_len = sizeof(int),
.payload = &test_val
};
// First write succeeds as stage isn't busy
adsp_control_status_t status = adsp_write_module_config(&ctrl, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_SUCCESS, status);
// Now the stage is busy
status = adsp_write_module_config(&ctrl, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_BUSY, status);
// Now the stage is busy
status = adsp_read_module_config(&ctrl, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_BUSY, status);
// Stage processes control
dummy_control(dummy_inst->state, &dummy_inst->control);
*(int*)cmd.payload = 0; // clear the payload, ready for read.
// request a read, stage must process it so it is busy
status = adsp_read_module_config(&ctrl, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_BUSY, status);
// The stage is busy
status = adsp_write_module_config(&ctrl, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_BUSY, status);
// Stage processes control
dummy_control(dummy_inst->state, &dummy_inst->control);
// re-request the read, stage must process it so it is busy
status = adsp_read_module_config(&ctrl, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_SUCCESS, status);
TEST_ASSERT_EQUAL(test_val, *(int*)cmd.payload);
}
/// Test that when there are two controllers, only the controller who made
/// the request will get a response.
void test_adsp_control_2_controllers(void) {
adsp_pipeline_t* p = adsp_dummy_pipeline_init();
module_instance_t* dummy_inst = &p->modules[dummy_stage_index];
adsp_controller_t ctrl_a;
adsp_controller_init(&ctrl_a, p);
adsp_controller_t ctrl_b;
adsp_controller_init(&ctrl_b, p);
int test_val = 5;
adsp_stage_control_cmd_t cmd = {
.instance_id = dummy_stage_index,
.cmd_id = CMD_DUMMY_DUMMY_FIELD,
.payload_len = sizeof(int),
.payload = &test_val
};
// First write succeeds as stage isn't busy
adsp_control_status_t status = adsp_write_module_config(&ctrl_a, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_SUCCESS, status);
// stage is now busy for both controllers.
status = adsp_write_module_config(&ctrl_a, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_BUSY, status);
status = adsp_write_module_config(&ctrl_b, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_BUSY, status);
// Stage processes control
dummy_control(dummy_inst->state, &dummy_inst->control);
*(int*)cmd.payload = 0; // clear the payload, ready for read.
// request a read, stage must process it so it is busy
status = adsp_read_module_config(&ctrl_a, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_BUSY, status);
// Stage processes control
dummy_control(dummy_inst->state, &dummy_inst->control);
// Even though a read was processed, b still gets busy
// because it didn't ask for it.
status = adsp_read_module_config(&ctrl_b, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_BUSY, status);
// but a gets the result.
status = adsp_read_module_config(&ctrl_a, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_SUCCESS, status);
TEST_ASSERT_EQUAL(test_val, *(int*)cmd.payload);
}
/// Ensure that busy is returned when the lock is taken for both read
/// and write.
void test_lock_contention(void) {
adsp_pipeline_t* p = adsp_dummy_pipeline_init();
module_instance_t* dummy_inst = &p->modules[dummy_stage_index];
adsp_controller_t ctrl;
adsp_controller_init(&ctrl, p);
int test_val = 5;
adsp_stage_control_cmd_t cmd = {
.instance_id = dummy_stage_index,
.cmd_id = CMD_DUMMY_DUMMY_FIELD,
.payload_len = sizeof(int),
.payload = &test_val
};
// take the lock to emulate contention.
swlock_acquire(&dummy_inst->control.lock);
// Get busy even though the stage isn't doing anything because the lock
// is taken.
adsp_control_status_t status = adsp_write_module_config(&ctrl, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_BUSY, status);
swlock_release(&dummy_inst->control.lock);
status = adsp_write_module_config(&ctrl, &cmd);
TEST_ASSERT_EQUAL(ADSP_CONTROL_SUCCESS, status);
// Stage processes control
dummy_control(dummy_inst->state, &dummy_inst->control);
}

View File

@@ -0,0 +1,41 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
// Minimal stage required to process some control.
#pragma once
#include <stages/bump_allocator.h>
#include <stages/adsp_module.h>
#include <string.h>
#include <dummy_config.h>
typedef struct {
dummy_config_t config;
} dummy_state_t;
#define DUMMY_STAGE_REQUIRED_MEMORY 0
static inline void dummy_init(module_instance_t* state, adsp_bump_allocator_t* alloc, int index, int nin, int nout, int chan) {
}
static inline void dummy_process(int32_t **input, int32_t **output, void *app_data_state) {}
static inline void dummy_control(dummy_state_t *module_state, module_control_t *control) {
dummy_state_t *state = module_state;
dummy_config_t *config = control->config;
if(control->config_rw_state == config_write_pending)
{
// Finish the write by updating the working copy with the new config
memcpy(&state->config, config, sizeof(dummy_config_t));
control->config_rw_state = config_none_pending;
}
else if(control->config_rw_state == config_read_pending)
{
memcpy(config, &state->config, sizeof(dummy_config_t));
control->config_rw_state = config_read_updated;
}
}

View File

@@ -0,0 +1,12 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from audio_dsp.design.stage import Stage
from pathlib import Path
class Dummy(Stage):
"""Stage that does nothing but has control"""
def __init__(self, **kwargs):
super().__init__(config=Path(__file__).parent / "dummy.yaml", **kwargs)
self.create_outputs(self.n_in)

View File

@@ -0,0 +1,6 @@
module:
dummy:
dummy_field:
type: int
help: Some help string