Разработка и тестирование целочисленного сумматора с AXI-Stream интерфейсами Часть 4

от автора

Автор: https://github.com/VSHEV92

Исходные коды:
https://github.com/pcbproj/AXI-Stream-Adder/tree/axis-adder-v1
https://github.com/pcbproj/AXI-Stream-Adder/tree/axis-adder-v2

Оглавление

  • Введение

  • Тестовое окружение для проверки сумматора

  • Настройки тестового окружения

  • Вспомогательные функции

  • Сигналы тестового окружения

  • Драйверы и мониторы AXI-Stream интерфейсов

  • Описание Scoreboard

  • Остальные компоненты окружения

  • Примеры использования окружения

  • Заключение

Введение

В предыдущей части был рассмотрен основной подход, применяемым для тестирования сложных цифровых устройств — constraint random testing. Мы узнали, как автоматизировать проверку корректности работы устройства с помощью сравнения его выходов с эталонной моделью. Тестовые окружения, работающие по такому принципу, называются self-test testbench. Мы увидели из каких компонентов строятся тестовые окружения и разработали структуру окружения для проверки сумматора с AXI-Stream интерфейсами. В этой статье мы перейдем от теории к практике и покажем, как реализовать это окружение на языке Verilog.

Тестовое окружение для проверки сумматора

Напомним структуру тестового окружения, которое мы хотим описать.

Окружение состоит из трех драйверов, два из которых выступают в роли передатчиков, а один — приемника. Задача передатчиков заключается в формировании случайных слагаемых и отправки их сумматору в соответствии с правилами AXI-Stream интерфейса. Эти два драйвера будут управлять сигналами tdata и tvalid входных интерфейсов сумматора. Драйвер-приемник должен управлять сигналом tready для выходного AXI-Stream интерфейса сумматора. Задержки на интерфейсах будут случайными в пределах настраиваемого диапазона.

Также окружение содержит три монитора, по одному на каждый AXI-Stream интерфейс. Мониторы будут непрерывно анализировать сигналы tvalid и tready и при обнаружении handshake отправлять данные на шине tdata в scoreboard.

В свою очередь scoreboard будет получать транзакции от мониторов и принимать решение о корректности выполнения суммирования. Входные слагаемые будут временно сохраняться в массивы с помощью коллекторов. После получения транзакции от выходного монитора scoreboard будет считывать слагаемые из массивов, передавать их в эталонную модель и далее отправлять эталонный результат в checker. Checker получает результаты от эталонной модели и тестируемого сумматора и выполняет их сравнение. В случае несовпадения выводится сообщение об ошибке.

Также мы будем использовать сторожевой таймер (watchdog), чтобы иметь возможность завершить тест при зависании проверяемого устройства или одного из компонентов окружения.

Перейдем, наконец, к практической части и начнем поэтапно реализовывать наше тестовое окружение на языке Verilog.

Настройки тестового окружения

Для удобства описания тестового окружения выделим некоторые его части в виде отдельных файлов. Окружение будет запускать один случайный тест, который можно будет конфигурировать с помощью различных настроек. Ниже представлено содержимое файла tb_defines.vh, который содержит все параметры теста.

`ifndef TB_DEFINES_VH `define TB_DEFINES_VH  // разрядность шины входных слагаемых `ifndef WIDTH   `define WIDTH 4 `endif  // ширина входных и выходных шин AXI-Stream интерфейса `define AXIS_IN_WIDTH $ceil($itor(`WIDTH)/8)*8 `define AXIS_OUT_WIDTH $ceil($itor(`WIDTH+1)/8)*8  // число транзакций в тесте `ifndef TRANS_NUMBER   `define TRANS_NUMBER 5 `endif  // начальное состояние генератора случайных чисел `ifndef SEED   `define SEED 0 `endif  // минимальная задержка в тактах на AXI-Stream интерфейсе `ifndef MIN_AXIS_DELAY   `define MIN_AXIS_DELAY 0 `endif  // максимальная задержка в тактах на AXI-Stream интерфейсе `ifndef MAX_AXIS_DELAY   `define MAX_AXIS_DELAY 10 `endif  // максимальное значение, генерируемое драйвером AXI-Stream интерфейса `ifndef MAX_AXIS_VALUE   `define MAX_AXIS_VALUE 2**`WIDTH - 1 `endif  // максимальная длительность теста в тактах `ifndef MAX_CLK_IN_TEST   `define MAX_CLK_IN_TEST 300 `endif  `endif 

Разберем назначение каждого параметра:

  • WIDTH — разрядность входных слагаемых;

  • AXIS_IN_WIDTH — разрядность шины tdata для входных слагаемых;

  • AXIS_OUT_WIDTH — разрядность шины tdata для результата суммирования;

  • TRANS_NUMBER — число суммирований, которое будет выполнено в тесте. Используется для завершения теста;

  • SEED — параметр, задающий начальное состояние генераторов случайных чисел. Изменяя это значение, можно формировать разные последовательности случайных воздействий;

  • MIN_AXIS_DELAY — минимальная задержка в тактах перед установкой сигналов tvalid или tready в AXI-Stream интерфейсе;

  • MAX_AXIS_DELAY — максимальная задержка в тактах перед установкой сигналов tvalid или tready в AXI-Stream интерфейсе;

  • MAX_AXIS_VALUE — максимальное значение, которое может появится на шине tdata. Используется для ограничения значений для входных слагаемых. По умолчанию определяется, исходя из значения WIDTH;

  • MAX_CLK_IN_TEST — максимальная длительность теста в тактах. Используется для настройки watchdog.

Отметим, что почти все настройки объявлены внутри конструкции ifndef ... endif. Это сделано для того, чтобы при запуске теста их значения можно было переопределять. Например, если в качестве симулятора используется Icarus Verilog, то добавив в команду запуска конструкцию -D TRANS_NUMBER=15, мы получим тест, который завершится после выполнения 15 суммирований. Если при запуске теста значение для какого-либо параметра не задано, то будет использовано значение по умолчанию, определенное в файле tb_defines.vh. Например, для числа суммирований TRANS_NUMBER значение по умолчанию равно 5. Исключением являются параметры AXIS_IN_WIDTH и AXIS_OUT_WIDTH, которые напрямую зависят от значения WIDTH и не задаются вручную.

В начале файла также присутствуют следующие две строки:

`ifndef TB_DEFINES_VH `define TB_DEFINES_VH 

Это известная в программировании конструкция, которая называется include guard. Она служит для защиты от ошибок множественного определения при многократном выполнении директивы include для одного и того же файла.

Вспомогательные функции

Создадим файл с именем tb_tasks.vh, в котором объявим некоторые вспомогательные процедуры. Файл будет начинаться с include guard и включать в себя настройки теста из tb_defines.vh.

`ifndef TB_TASKS_VH `define TB_TASKS_VH  `include "tb_defines.vh" 

С помощью отдельной процедуры gold_adder опишем эталонную модель сумматора. Модель будет принимать входные слагаемые data1_i и data2_i и вычислять результат суммирования data_o. Ширина шина tdata из-за требований AXI-Stream интерфейса (кратность восьми битам) может превышать значение WIDTH, поэтому перед вычислением суммы выделяем из слагаемых младшие WIDTH бит:

// эталонный сумматор task gold_adder(input integer data1_i, input integer data2_i, output integer data_o);   integer in_1, in_2;   begin     in_1 = data1_i[`WIDTH-1:0];     in_2 = data2_i[`WIDTH-1:0];     data_o = in_1 + in_2;   end endtask 

Также в виде отдельной процедуры с именем compare реализуем checker. Процедура принимает входные слагаемые data1_i и data2_i от коллекторов, пропускает их через модель gold_adder и получает эталонный результат gold_out. Далее этот эталон сравнивается с выходом сумматора data_o. Если значения не совпадают, то выводится сообщение об ошибке. Также на вход процедуры поступает однобитный сигнал error_flag, который служит индикатором наличия ошибок в процессе работы теста. Если checker обнаруживает несовпадение данных, то сигнал error_flag устанавливается в единицу и остается в этом состоянии до конца теста.

// сравнение с эталонной моделью task compare(input integer data1_i, input integer data2_i, input integer data_o, inout reg error_flag);   integer gold_out, dut_out;   begin     gold_adder(data1_i, data2_i, gold_out);     dut_out = data_o[`WIDTH:0];     // вывод на экран и установка флага ошибки     if (gold_out != dut_out) begin       $display("ERROR! Data mismatch! input 1: %0d, input 2: %0d, output: %0d, gold: %0d, time: %0t", data1_i, data2_i, dut_out, gold_out, $time);       error_flag = 1'b1;     end   end endtask 

Последняя процедура check_finish отвечает за завершение теста и вывод отчета о его результатах. На вход поступает число выполненных суммирований trans_cnt, запланированное число суммирований trans_number и сигнал наличия ошибок error_flag. Если число выполненных суммирований совпадает с числом запланированных (trans_cnt == trans_number), то тест завершается с помощью функции $finish. Перед этим проверяется значение сигнала error_flag. Если оно равно единице, то это означает, что во время выполнения теста были обнаружены ошибки, поэтому выводится сообщение TEST FAILED!. Иначе выводится сообщение TEST PASSED!.

// проверка числа обработанных сложений и завершение теста task check_finish(input integer trans_cnt, input integer trans_number, input reg error_flag);   begin     if (trans_cnt == trans_number) begin       if (error_flag) begin         $display("----------------------");         $display("---- TEST FAILED! ----");         $display("----------------------");       end else begin         $display("----------------------");         $display("---- TEST PASSED! ----");         $display("----------------------");       end       $finish;     end   end endtask 

Сигналы тестового окружения

Разобравшись со вспомогательными файлами, начнем описывать основные части тестового окружения. Для начала включим в окружение вспомогательные файлы:

module adder_axis_tb ();    `include "tb_defines.vh"   `include "tb_tasks.vh" 

Далее объявим входные и выходные сигналы сумматора:

  // тактовый сигнал и сигнал сброса   reg aclk = 1'b0;   reg aresetn = 1'b0;    // сигналы для AXI-Stream интерфейсов   reg  data1_i_tvalid, data2_i_tvalid, data_o_tready;   wire data1_i_tready, data2_i_tready, data_o_tvalid;    // слагаемые и результат суммы   reg  [ `AXIS_IN_WIDTH-1:0]  data1_i_tdata;   reg  [ `AXIS_IN_WIDTH-1:0]  data2_i_tdata;   wire [ `AXIS_OUT_WIDTH-1:0] data_o_tdata; 

Объявим массивы, в которые коллекторы будут складывать входные слагаемые. Длина массивов определяется через запланированное число суммирований (TRANS_NUMBER):

  // массивы для сохранения входных слагаемых   reg [`WIDTH-1:0] axis_data1 [0:`TRANS_NUMBER];   reg [`WIDTH-1:0] axis_data2 [0:`TRANS_NUMBER]; 

Создадим несколько счетчиков. Сигналы axis_data1_cnt и axis_data2_cnt подсчитывают число входных слагаемых и используются для индексирования в массивах коллекторов. Счетчик числа суммирований trans_cnt передается в scoreboard для проверки условия завершения теста.

  // счетчики числа слагаемых и результатов суммы   integer unsigned axis_data1_cnt = 0;   integer unsigned axis_data2_cnt = 0;   integer unsigned trans_cnt = 0; 

Отдельно объявим переменную seed для задания начального состояния генераторов случайных чисел. Функция $urandom() требует в качестве аргумента целочисленную переменную. Мы не можем в $urandom() напрямую передавать константу «SEEDиз файла **tb_defines.vh**. Для этих целей будет использоваться переменнаяseed`.

  // начальное состояние генератора случайных чисел   integer seed = `SEED; 

В Verilog все блоки initial и always, а также непрерывные присваивания assign, выполняются одновременно и параллельно друг относительно друга. Для обеспечения cинхронизации этих процессов в Verilog используются события (events). В нашем окружении с помощью events будет координироваться совместная работа мониторов и scoreboard:

  // события handshake на AXI-Stream интерфейсах   event data1_i_e, data2_i_e, data_o_e; 

Объявим однобитный сигнал, указывающий на появление ошибок в процессе выполнения теста, и инициализируем его нулевым значением:

  // флаг наличия ошибок в тесте   reg error_flag = 1'b0; 

Наконец, добавим в окружение наш сумматор и подключим к его портам все необходимые сигналы:

// проверяемый модуль   adder_axis_pipe #(       .ADDER_WIDTH(`WIDTH)   ) dut (       .aclk          (aclk),       .aresetn       (aresetn),       .data1_i_tdata (data1_i_tdata),       .data1_i_tvalid(data1_i_tvalid),       .data1_i_tready(data1_i_tready),       .data2_i_tdata (data2_i_tdata),       .data2_i_tvalid(data2_i_tvalid),       .data2_i_tready(data2_i_tready),       .data_o_tdata  (data_o_tdata),       .data_o_tvalid (data_o_tvalid),       .data_o_tready (data_o_tready)   ); 

Драйверы и мониторы AXI-Stream интерфейсов

Для начала рассмотрим реализацию драйверов для входных интерфейсов сумматора. В нашем окружении должно быть два драйвера, которые выступают в роли передатчиков. Их задача заключается в создании входных слагаемых и отправки их сумматору. Каждый драйвер должен формировать данные для шины tdata и управлять сигналом валидности tvalid. Чтобы обнаружить момент возникновения handshake, драйвер должен следить за сигналом tready.

Ниже представлено описание драйвера для первого входного интерфейса сумматора:

  // драйвер для data1_i AXI-Stream интерфейса   initial begin     // ожидаем выхода из состояния сброса     @(posedge aresetn);        while (1) begin       data1_i_tvalid <= 1'b0;       // выполняем задержку на случайное число тактов       repeat ($urandom(seed) % (`MAX_AXIS_DELAY + 1) + `MIN_AXIS_DELAY) @(posedge aclk);       // выставляем сигнал tvalid и данные       data1_i_tdata  <= $urandom(seed) % (`MAX_AXIS_VALUE + 1);       data1_i_tvalid <= 1'b1;       @(posedge aclk);       // ожидаем сигнал tready для handshake       while (!data1_i_tready) @(posedge aclk);     end   end 

Драйвер реализован внутри блока initial. В начале теста сумматор будет находиться в состоянии сброса. Перед тем, как подавать на него транзакции, мы ожидаем установки сигнала сброса в неактивный единичный уровень (@(posedge aresetn)). Далее драйвер в бесконечном цикле (while (1)) начинает формировать случайные слагаемые и отправлять их сумматору.

В начале драйвер не имеет транзакций для сумматора, поэтому сигнал валидности data1_i_tvalid устанавливается в нулевое значение. С помощью функции $urandom() мы получаем целое число в диапазоне от MIN_AXIS_DELAY до MAX_AXIS_DELAY и используем его для формирования случайной задержки. Для этих целей мы используем цикл repeat, внутри которого ожидаем заданное число фронтов тактового сигнала @(posedge aclk).

После этого мы генерируем случайные данные data1_i_tdata, выставляем сигнал валидности data1_i_tvalid в единичное значение и ждем появления фронта сигнала aclk. Если сигнал готовности data1_i_tready равен единице, то это значит, что на шине произошел handshake. Сумматор получил транзакцию и мы можем повторить весь цикл заново. Иначе с помощью цикла while на каждом такте @(posedge aclk) мы проверяем, произошел ли handshake. Цикл while выполняется до тех пор, пока сигнал data1_i_tready не примет единичное значение.

Драйвер для второго входного интерфейса имеет тот же вид, за исключением того, что теперь мы используем сигналы data2_i_tdata, data2_i_tvalid и data2_i_tready. Его описание представлено ниже:

  // драйвер для data2_i AXI-Stream интерфейса   initial begin     // ожидаем выхода из состояния сброса     @(posedge aresetn);      while (1) begin       data2_i_tvalid <= 1'b0;       // выполняем задержку на случайное число тактов       repeat ($urandom(seed) % (`MAX_AXIS_DELAY + 1) + `MIN_AXIS_DELAY) @(posedge aclk);       // выставляем сигнал tvalid и данные       data2_i_tdata  <= $urandom(seed) % (`MAX_AXIS_VALUE + 1);       data2_i_tvalid <= 1'b1;       @(posedge aclk);       // ожидаем сигнал tready для handshake       while (!data2_i_tready) @(posedge aclk);     end   end 

Теперь рассмотрим, как устроен драйвер для выходного интерфейса сумматора. В этом случае драйвер выступает в качестве приемника данных, и его задача заключается в управлении сигналом tready. В соответствии с правилами AXI-Stream интерфейса сигнал tready не должен зависеть от сигнала tvalid и может изменять свое значение на каждом такте сигнала aclk.

Описание драйвера для выходного интерфейса сумматора представлено ниже:

  // драйвер для data_o AXI-Stream интерфейса   initial begin     // ожидаем выхода из состояния сброса     @(posedge aresetn);          while (1) begin       // сбрасываем сигнал tready       data_o_tready <= 1'b0;       // выполняем задержку на случайное число тактов       repeat($urandom(seed) % (`MAX_AXIS_DELAY + 1) + `MIN_AXIS_DELAY)         @(posedge aclk);       // выставляем сигнал tready       data_o_tready <= 1'b1;       @(posedge aclk);       // опять выполняем задержку на случайное число тактов       repeat($urandom(seed) % (`MAX_AXIS_DELAY + 1) + `MIN_AXIS_DELAY)         @(posedge aclk);     end   end 

Мы ждем выхода из состояния сброса (@(posedge aresetn)), после чего в бесконечном цикле (while (1)) на каждом такте формируем значение сигнала data_o_tready. Сначала задаем нулевое значение и с помощью функции $urandom() и цикла repeat удерживаем сигнал data_o_tready в этом состоянии в течение случайного числа тактов. Далее присваиваем единичное значение и опять выполняем задержку на случайное число тактов. После этого цикл повторяется.

Разобравшись с драйверами, перейдем к описанию мониторов. Монитор должен наблюдать за сигналами интерфейса и определять моменты времени, когда происходит handshake. Для этого на каждом такте отслеживаются значения сигналов tvalid и tready. Handshake наступает, когда оба сигнала равны единице. Описание всех мониторов представлено ниже:

  // монитор для data1_i AXI-Stream интерфейса   always begin     @(posedge aclk);     if (data1_i_tready && data1_i_tvalid) -> data1_i_e;   end    // монитор для data2_i AXI-Stream интерфейса   always begin     @(posedge aclk);     if (data2_i_tready && data2_i_tvalid) -> data2_i_e;   end      // монитор для data_o AXI-Stream интерфейса   always begin     @(posedge aclk);     if (data_o_tready && data_o_tvalid) -> data_o_e;   end 

Для примера, рассмотрим, монитор для выходного интерфейса. Внутри блока always на каждом такте (@(posedge aclk)) проверяется условие, что сигналы data_o_tready и data_o_tvalid принимают единичное значение. При его выполнении с помощью оператора -> запускается событие data_o_e. Так монитор сообщает scoreboard о появлении транзакции на выходном AXI-Stream интерфейсе сумматора.

Описание Scoreboard

Теперь рассмотрим реализацию самого крупного компонента окружения — scoreboard. Он состоит из нескольких частей: коллекторы, эталонная модель и checker. Описание коллектора для слагаемых на первом входном интерфейсе сумматора показано ниже:

  always begin       @(data1_i_e);       axis_data1[axis_data1_cnt] = data1_i_tdata;       axis_data1_cnt = axis_data1_cnt + 1;   end 

В блоке always мы ожидаем срабатывания event (@(data1_i_e)). Его появление означает, что в текущий момент времени на интерфейсе происходит handshake. Поэтому мы берем значение на шине data1_i_tdata и сохраняем его в массив коллектора axis_data1. После этого мы увеличиваем счетчик полученных слагаемых (axis_data2_cnt) на единицу и ждем следующего срабатывания события data1_i_e.

Коллектор для второго входного интерфейса работает таким же образом:

  always begin       @(data2_i_e);       axis_data2[axis_data2_cnt] = data2_i_tdata;       axis_data2_cnt = axis_data2_cnt + 1;   end 

Оставшаяся часть scoreboard, состоящая из эталонной модели и checker, реализуется с помощью еще одного блока always:

  always begin       @(data_o_e);       compare(axis_data1[trans_cnt], axis_data2[trans_cnt], data_o_tdata, error_flag);       trans_cnt = trans_cnt + 1;       check_finish(trans_cnt, `TRANS_NUMBER, error_flag);   end 

Мы ожидаем событие data_o_e, срабатывание которого указывает на появление транзакции на выходном AXI-Stream интерфейсе. Это в свою очередь означает, что ранее уже были получены входные слагаемые, которые сейчас находятся в массивах коллекторов.

Далее мы вызываем процедуру compare, в которую передаем слагаемые из коллекторов axis_data1[trans_cnt] и axis_data2[trans_cnt], результат сложения на выходе сумматора (data_o_tdata) и сигнал наличия ошибок error_flag. Эта процедура вызывает эталонную модель и выполняет сравнение. В случае несовпадения результатов выводится сообщение об ошибке и значение сигнала error_flag устанавливается в единицу. После этого мы увеличиваем счетчик выполненных суммирований (trans_cnt) и проверяем, можно ли завершить тест. Когда число выполненных суммирований trans_cnt станет равным TRANS_NUMBER, тест завершается и выводится отчет о его выполнении. Если сигнал error_flag равен нулю, то тест пройден успешно, если единице — то нет.

Остальные компоненты окружения

Для завершения описания окружения нам необходимо добавить еще несколько компонентов. С помощью блоков always и initial сформируем тактовый сигнал и сигнал сброса:

  // создание тактового сигнала   always #5 aclk = ~aclk;    // создание сигнала сброса   initial begin     repeat (10) @(posedge aclk);     aresetn <= 1'b1;   end 

Включим в окружение сторожевой таймер. Для этого в блоке initial с помощью цикла repeat будем ожидать появления заданного числа фронтов тактового сигнала (@(posedge aclk)). Если моделирование выполняется уже на протяжении MAX_CLK_IN_TEST тактов, то мы считаем, что тест завис. Сторожевой таймер завершает тест, и выводится соответствующее сообщение об ошибке.

  // сторожевой таймер для отслеживания зависания теста   initial begin     repeat(`MAX_CLK_IN_TEST) @(posedge aclk);     $display("ERROR! Watchdog error!");     $display("----------------------");     $display("---- TEST FAILED! ----");     $display("----------------------");     $finish;   end 

Наконец, добавим последний блок inital, отвечающий за дамп временных диаграмм в формате VCD.

  // дамп waveforms в VCD файл   initial begin     $dumpfile("wave_dump.vcd");     $dumpvars(0);   end 

Примеры использования окружения

Наше тестовое окружение готово к использованию. Давайте проверим его работу. Для начала рассмотрим, как оно детектирует различные ошибки. Для этого изменим описание сумматора таким образом, чтобы входной регистр для второго слагаемого работал некорректно и всегда содержал нулевое значение. Запускаем тест, который должен выполнить 5 суммирований, и видим следующие временные диаграммы:

На входы сумматора поступают случайные слагаемые, однако результат суммы всегда совпадает со значением первого слагаемого. Ниже представлены сообщения от симулятора:

Можно увидеть, что окружение обнаружило 5 несовпадений. В сообщениях указаны входные слагаемые, эталонный и фактический результаты сложения, а также момент времени, когда обнаружена ошибка. Также выводится сообщение о том, что тест не пройден.

Теперь внесем в сумматор другую ошибку. Пусть мы забыли подключить к выходному порту data_o_tvalid сумматора сигнал валидности данных от блока управления. После запуска теста получим следующую временную диаграмму:

Драйверы генерируют множество входных транзакций, но сигнал data_o_tvalid всегда находится в z-состоянии. Из-за этого монитор не видит ни одной транзакции на выходе сумматора и ничего не передает в scoreboard. Тот в свою очередь никогда не получит запланированное число транзакций (TRANS_NUMBER) и не завершит тест. Однако, тест не будет выполняться бесконечно долго, так как сторожевой таймер обнаружит зависание и прервет моделирование. В результате мы получим следующее сообщение от симулятора:

Наконец, уберем все ошибки из сумматора и запустим моделирование. Глядя на временные диаграммы, мы можем увидеть, что сложение выполняется правильно:

Однако, чтобы понять, что сумматор работает корректно, не обязательно просматривать все временные диаграммы. Достаточно просто прочитать сообщение симулятора о результатах теста:

Заключение

Мы реализовали на языке Verilog тестовое окружение, структура которого была разработана в предыдущей статье. Окружение построено по принципу self-test testbench и выполняет проверку сумматора с помощью consraint-random testing. Однако, у нас есть еще широкий простор для его улучшения.

Во-первых, при описании драйверов и мониторов мы получили большой объем дублированного кода. Это всегда приводит к сложностям при дальнейшей поддержке и модификации окружения. Во-вторых, для задания параметров теста мы использовали директивы define. Это не самый лучший подход, так как при изменении настроек теста, нам придется заново компилировать все окружение. В случае больших проектов перекомпиляция исходников может занимать достаточно много времени. Есть более удобный способ настройки окружения. Мы исправим эти недостатки и получим окончательный вариант тестового окружения в следующей статье.


ссылка на оригинал статьи https://habr.com/ru/articles/774412/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *