Community Engee

Избавляемся от рутины в Engee

Author
avatar-nikfilaretovnikfilaretov
Notebook

Автоматизация верификации кода

Я очень много занимаюсь генерацией кода и проверками того, что он работает одинаково с моделью. Весь этот процесс занимает много ручного труда, и в какой-то момент я понял что мне надоело делать одну и ту же работу руками. Поэтому, я автоматизировал ее, и в этом проекте мы посмотрим на автоматизацию тестирования сгенерированного кода в режиме ПО-в-Контуре (SIL).

О чем вообще речь?

Метод проверки ПО-в-Контуре (SIL-тестирование) заключается в том, что мы сравниваем результаты работа кода на хосте относительно работы модели, используя одинаковые тестовые вектора. Схематично, тестирование выглядит так:

image.png

Из модели алгоритма генерируется код, а затем модель алгоритма подменяется вызовами сгенерированного кода. Сам же код компилируется на хосте. Таким образом, SIL-тестирование позволяет проверить поведение кода относительно модели без перехода на целевые вычислители (тестирование кода на целевом вычислителе - это отдельный этап тестирования, называемый Процессор-в-контуре, PIL).

Пример

Чтобы показать автоматизацию SIL-тестирования, я взял пример из этой публикации. Для удобства тестирования я встроил модель фильтра Калмана в большую модель при помощи модели ссылки:

image.png

Модель-ссылка дает следующие преимущества:

  1. Изоляция тестируемого компонента в отдельной модели. При инициализации обвязки интерфейсы модели ссылки инициализируются отдельно.
  2. Простота замены тестируемого компонента на блок Си-функции для SIL-теста через программное управление.

Автоматизация тестирования

Для написания автоматизации нам потребуется использовать программное управление. При этом, я хочу сделать так, чтобы моя автоматизация была максимально переиспользуемой. Поэтому я написал свой модуль, который автоматизирует большую часть работы. Причем модуль был написан с учетом того, что его функции будут использоваться в тестах, которые я создам в Test.jl.

Давайте подключим все необходимые пакеты и загрузим модуль!

In [ ]:
import Pkg; Pkg.add("Test")
using Test

include("SILAutomation.jl");
MIL_Harness = "ABfilter_Harness";
SIL_Harness = "ABfilter_Harness_SIL";
CUT = "alphabetafilter";
   Resolving package versions...
   Installed ArrayLayouts ───────────── v1.12.0
   Installed DifferentialEquations ──── v7.16.1
   Installed LazyArrays ─────────────── v2.8.0
   Installed EnzymeCore ─────────────── v0.8.15
   Installed LineSearches ───────────── v7.4.0
   Installed ExponentialUtilities ───── v1.27.0
   Installed Krylov ─────────────────── v0.10.2
   Installed DifferentiationInterface ─ v0.7.10
   Installed RData ──────────────────── v0.8.3
   Installed Optim ──────────────────── v1.13.2
   Installed RDatasets ──────────────── v0.7.7
   Installed JuliaInterpreter ───────── v0.10.6
   Installed BenchmarkTools ─────────── v1.6.2
   Installed BandedMatrices ─────────── v1.10.1
   Installed SparseConnectivityTracer ─ v1.1.2
   Installed TimeZones ──────────────── v1.22.1
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`

Давайте посмотрим на код модуля:

In [ ]:
;cat SILAutomation.jl
module SILAutomotion

import Main.engee
export runSims,buildCUT,buildSILHarness,compare_signals

using DataFrames

function runSims(MIL_Harness::String,SIL_Harness::String)
    MIL_mdl = engee.load("$MIL_Harness.engee", force = true)
    SIL_mdl = engee.load("$SIL_Harness.engee", force = true)
    SILres = engee.run(SIL_mdl)
    MILres = engee.run(MIL_mdl)
    return (MILres,SILres)
end

function buildCUT(CUT::String)::Bool
    status = false;
    try
        abfilter = engee.load("$(CUT).engee")
        engee.set_param!(engee.gcm(),"CreateCFunction"=>true)
        engee.generate_code("$(CUT).engee", "$(CUT)_cg")
        include("$(CUT)_cg/$(CUT)_verification.jl")
        status = true;
    catch ex
        return status
    end
    return status
end

function buildSILHarness(SIL_Harness::String,CUT::String,MIL_Harness::String)
# Create harness (copy contents)
# Guard for old harnesses
    model_names = [m.name for m in engee.get_all_models()];
    if any(model_names .== SIL_Harness)
        engee.close(SIL_Harness,force=true)
    end
    if isfile("$SIL_Harness.engee")
        rm("$SIL_Harness.engee")
    end

    SIL_mdl = engee.create("$SIL_Harness")
    engee.load("$(MIL_Harness).engee")
    engee.copy_contents(MIL_Harness,SIL_Harness)
    engee.delete_block("$SIL_Harness/$CUT")
    try
        engee.copy_block("$(CUT)_verification/C Function","$SIL_Harness/C Function")
    catch error
        show(error)
    end

    engee.open(SIL_Harness)
    #connect SIL Function And prepare simulation
    engee.add_line("ProcessNoiseVariance/1","C Function/1")
    engee.add_line("MeasurementNoiseVariance/1","C Function/2")
    engee.add_line("Add-1/1","C Function/3")
    engee.add_line("C Function/1","Add/2")
    engee.add_line("C Function/1","SysOut/1")
    engee.set_log("C Function/1") 
    engee.set_param!(SIL_mdl,"FixedStep"=>engee.get_param(MIL_Harness,"FixedStep"))
    engee.arrange_system()
    engee.save("$SIL_Harness.engee")
end

function compare_signals(sig_one,sig_two)
    Ds = collect(sig_one);
    Rs = collect(sig_two);
    Cmp = isapprox.(Ds, Rs)
    issynched = all(Cmp.time)
    issimilar = all(Cmp.value) 
    return (issynched, issimilar)
end

end

Генерация проверочной Си функции

Чтобы сгенерировать проверочную Си функцию из тестируемого компонента необходимо включить соответствующую настройку в модели и сгенерировать код. За это в моем модуле отвечает функция buildCUT:

function buildCUT(CUT::String)::Bool
    status = false;
    try
        abfilter = engee.load("$(CUT).engee")
        engee.set_param!(engee.gcm(),"CreateCFunction"=>true)
        engee.generate_code("$(CUT).engee", "$(CUT)_cg")
        include("$(CUT)_cg/$(CUT)_verification.jl")
        status = true;
    catch ex
        return status
    end
    return status
end

Для того чтобы эта функция была тестируема, будем возвращать статус как булеву переменную.

Автоматизация сборки SIL-обвязки

Я не хочу изменять обвязку для MIL-теста, но мне надо обеспечить эквивалентность тестовых векторов, поэтому я создам новую модель для SIL-теста. Посмотрим на код функции buildSILHarness:

function buildSILHarness(SIL_Harness::String,CUT::String,MIL_Harness::String)
# Create harness (copy contents)
# Guard for old harnesses
    model_names = [m.name for m in engee.get_all_models()];
    if any(model_names .== SIL_Harness)
        engee.close(SIL_Harness,force=true)
    end
    if isfile("$SIL_Harness.engee")
        rm("$SIL_Harness.engee")
    end

    SIL_mdl = engee.create("$SIL_Harness")
    engee.load("$(MIL_Harness).engee")
    engee.copy_contents(MIL_Harness,SIL_Harness)
    engee.delete_block("$SIL_Harness/$CUT")
    try
        engee.copy_block("$(CUT)_verification/C Function","$SIL_Harness/C Function")
    catch error
        show(error)
    end

    engee.open(SIL_Harness)
    #connect SIL Function And prepare simulation
    engee.add_line("ProcessNoiseVariance/1","C Function/1")
    engee.add_line("MeasurementNoiseVariance/1","C Function/2")
    engee.add_line("Add-1/1","C Function/3")
    engee.add_line("C Function/1","Add/2")
    engee.add_line("C Function/1","SysOut/1")
    engee.set_log("C Function/1") 
    engee.set_param!(SIL_mdl,"FixedStep"=>engee.get_param(MIL_Harness,"FixedStep"))
    engee.arrange_model()
    engee.save("$SIL_Harness.engee")
end

Сначала с помощью engee.create() я создаю новую пустую модель, а затем копирую содержимое оригинальной модели в новую при помощи engee.copy_contents(). Далее я удаляю модель ссылку и подставляю проверочную Си-функцию с помощью engee.delete_block() и engee.copy_block(). Затем я восстанавливаю сигналы и указываю какой сигнал надо записать. Затем я упорядочиваю модель и сохраняю ее. Итоговая модель будет выглядеть вот так:

image.png

Как правильно сравнивать сигналы?

Мы все привыкли сравнивать два числа через оператор ==. Например:

In [ ]:
1.0 == 1.0
Out[0]:
true

Но тут есть две проблемы:

  1. Числа с плавающей точкой лучше не сравнивать напрямую, а сравнивать модуль их разности с некоторой допустимой погрешностью
  2. Если говорить о сигналах, то мы опять-таки хотим учесть, что сигналы могут быть неодинаковыми и хотим учитывать небольшую ошибку

Дополнительно, надо проверить, что сигналы синхронизированы.

Посмотрим на код функции сравнения:

function compare_signals(sig_one,sig_two)
    Ds = collect(sig_one);
    Rs = collect(sig_two);
    Cmp = isapprox.(Ds, Rs)
    issynched = all(Cmp.time)
    issimilar = all(Cmp.value) 
    return (issynched, issimilar)
end

Мы сравниваем два сигнала, которые представлены как DataFrame с двумя столбцами: time и value. Сравнение будем делать через функцию isapprox(). Возвращать будем две булевы переменные:

  • issynched - показывает, что сигналы синхронизированы по времени
  • issimilar - показывает, что сигналы одинаковы с определенной точностью

Создание набора тестов и его запуск

Теперь, когда у нас есть все необходимое для автоматизации тестирования, создадим наборы тестов с помощью Test.jl. Причем я разнесу тесты на сборку верификационной модели и эквивалентности работы кода и модели. Запустим тесты и посмотрим на их результат:

In [ ]:
@testset verbose = true "SIL" begin
    @testset "Code Generation" begin
        @test SILAutomotion.buildCUT(CUT)==true
        @test isfile(CUT*"_verification.engee")
    end
    
    SILAutomotion.buildCUT(CUT)
    SILAutomotion.buildSILHarness(SIL_Harness,CUT, MIL_Harness)
    
    @testset "SIL Equality" begin
        (MR,SR) = SILAutomotion.runSims(MIL_Harness,SIL_Harness)
        (sync,equal) = SILAutomotion.compare_signals(MR["filtered"],SR["C Function.1"])
        @test sync==true
        @test equal==true
    end
end 
Test Summary:     | Pass  Total   Time
SIL               |    4      4  40.3s
  Code Generation |    2      2   8.5s
  SIL Equality    |    2      2  22.3s
Out[0]:
Test.DefaultTestSet("SIL", Any[Test.DefaultTestSet("Code Generation", Any[], 2, false, false, true, 1.766386254735e9, 1.766386263245493e9, false, "In[4]"), Test.DefaultTestSet("SIL Equality", Any[], 2, false, false, true, 1.766386272794816e9, 1.76638629507223e9, false, "In[4]")], 0, false, true, true, 1.7663862547349e9, 1.766386295072248e9, false, "In[4]")

Видно, что все тесты прошли, а моя автоматизация работает!

Выводы и следующие шаги

Применяя программное управление мне удалось автоматизировать рутину. Можно и дальше развить эту автоматизацию, расширив ее, например, на PIL-тестирование. Написанные с помощью Test.jl тесты также могут быть расширены для более глубокого тестирования. И, наконец, можно организовать тестирование в рамках единой обвязки с помощью блоков Variant Source/Variant Sink. Последнее требует отдельной публикации.