446 lines
18 KiB
C
446 lines
18 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/**
|
|
* @file test_harvesting.c
|
|
* @brief Off-hardware harvest matrix for ft_tt_coord: every single-column Tensix harvest
|
|
* and a set of multi-column ones (the p100's two-column case included), DRAM single-bank
|
|
* harvests (the doc's D2 example) and the "at most one bank" refusal, and
|
|
* ft_tt_coord_tensix_harvest_mask round trips.
|
|
*
|
|
* This is the only coverage for code a fully-enabled card never runs. Expectations come
|
|
* from ft_tt_blackhole_doc_layout.h (the public docs: [COORD] "X is remapped to put the
|
|
* fused columns at maximal X ... X = 8 always refers to the column containing CPUs, and
|
|
* X = 9 always refers to a column containing DRAM", [LUA] bh_y211 / bh_y1223) and the
|
|
* spec-given telemetry bit order — never from ft_tt_arch_blackhole.c.
|
|
*
|
|
* Grid convention assumed (no public page states it): GDDR word bit b = bank b.
|
|
*
|
|
* @status Covers the harvest paths of every ft_tt_coord.h lookup.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
|
|
#include "ft_tt_arch.h"
|
|
#include "ft_tt_coord.h"
|
|
#include "ft_tt_device.h"
|
|
#include "ft_tt_blackhole_doc_layout.h"
|
|
|
|
/* [COORD] / [LUA] bh_y211: the p100 example fuses the columns at NoC #0 x = 5 and 11. */
|
|
#define P100_FUSED_COLUMNS ((1u << 4) | (1u << 8))
|
|
|
|
/* [LUA] bh_y1223: the p100 example fuses DRAM bank D2. */
|
|
#define P100_FUSED_BANK 2u
|
|
|
|
static int checks_run;
|
|
static int checks_failed;
|
|
|
|
static void check(bool condition, const char *what)
|
|
{
|
|
checks_run++;
|
|
if (!condition) {
|
|
checks_failed++;
|
|
printf(" FAIL %s\n", what);
|
|
}
|
|
}
|
|
|
|
static void check_rc(int got, int want, const char *what)
|
|
{
|
|
checks_run++;
|
|
if (got != want) {
|
|
checks_failed++;
|
|
printf(" FAIL %s: rc %d (expected %d)\n", what, got, want);
|
|
}
|
|
}
|
|
|
|
static void check_u32(uint32_t got, uint32_t want, const char *what)
|
|
{
|
|
checks_run++;
|
|
if (got != want) {
|
|
checks_failed++;
|
|
printf(" FAIL %s = %u (expected %u)\n", what, got, want);
|
|
}
|
|
}
|
|
|
|
static void check_coord(int rc, ft_tt_noc_coord got, uint32_t want_x, uint32_t want_y,
|
|
const char *what, uint32_t index)
|
|
{
|
|
checks_run++;
|
|
if (rc != FT_TT_OK || got.x != want_x || got.y != want_y) {
|
|
checks_failed++;
|
|
printf(" FAIL %s [%u]: rc %d (%u,%u), expected (%u,%u)\n", what, index, rc, got.x,
|
|
got.y, want_x, want_y);
|
|
}
|
|
}
|
|
|
|
static void check_at(bool condition, const char *what, uint32_t x, uint32_t y)
|
|
{
|
|
checks_run++;
|
|
if (!condition) {
|
|
checks_failed++;
|
|
printf(" FAIL %s at (%u,%u)\n", what, x, y);
|
|
}
|
|
}
|
|
|
|
static uint32_t count_bits(uint32_t value)
|
|
{
|
|
uint32_t count = 0u;
|
|
|
|
for (; value; value &= value - 1u)
|
|
count++;
|
|
return count;
|
|
}
|
|
|
|
static ft_tt_grid_info make_grid(uint32_t harvested_columns, uint32_t enabled_gddr)
|
|
{
|
|
ft_tt_grid_info grid = { 0 };
|
|
|
|
grid.valid = true;
|
|
grid.arch = ft_tt_arch_blackhole();
|
|
grid.tensix_enabled_col = bh_doc_enabled_column_word(harvested_columns);
|
|
grid.enabled_eth = 0u; /* [COORD]: Ethernet "fused off in p100" */
|
|
grid.enabled_gddr = enabled_gddr;
|
|
grid.enabled_l2cpu = BH_DOC_ALL_L2CPU_MASK;
|
|
grid.noc_translation_enabled = 1u;
|
|
return grid;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Telemetry word <-> harvest mask */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void test_harvest_mask_round_trips(void)
|
|
{
|
|
const ft_tt_arch *arch = ft_tt_arch_blackhole();
|
|
uint32_t mask = 0u;
|
|
uint32_t harvested;
|
|
uint32_t column;
|
|
uint32_t mismatches = 0u;
|
|
|
|
printf("harvest_mask: telemetry word -> grid-column mask\n");
|
|
check_rc(ft_tt_coord_tensix_harvest_mask(arch, bh_doc_enabled_column_word(0u), &mask),
|
|
FT_TT_OK, "all enabled");
|
|
check_u32(mask, 0u, "all enabled harvests nothing");
|
|
check_rc(ft_tt_coord_tensix_harvest_mask(arch, 0u, &mask), FT_TT_OK, "none enabled");
|
|
check_u32(mask, BH_DOC_ALL_COLUMNS_MASK, "none enabled harvests every column");
|
|
|
|
for (column = 0; column < BH_DOC_TENSIX_COLUMNS; column++) {
|
|
uint32_t word = BH_DOC_ALL_COLUMNS_MASK & ~(1u << bh_doc_tensix_telemetry_bit(column));
|
|
|
|
check_rc(ft_tt_coord_tensix_harvest_mask(arch, word, &mask), FT_TT_OK,
|
|
"single cleared telemetry bit");
|
|
check_u32(mask, 1u << column, "a cleared telemetry bit names its grid column");
|
|
}
|
|
|
|
/* The doc's p100: x = 5 is telemetry bit 8 and x = 11 is bit 11 (spec bit order). */
|
|
check_rc(ft_tt_coord_tensix_harvest_mask(arch, BH_DOC_ALL_COLUMNS_MASK & ~0x900u, &mask),
|
|
FT_TT_OK, "p100 telemetry word");
|
|
check_u32(mask, P100_FUSED_COLUMNS, "p100 word names grid columns 4 (x 5) and 8 (x 11)");
|
|
|
|
for (harvested = 0; harvested <= BH_DOC_ALL_COLUMNS_MASK; harvested++) {
|
|
if (ft_tt_coord_tensix_harvest_mask(arch, bh_doc_enabled_column_word(harvested),
|
|
&mask) != FT_TT_OK ||
|
|
mask != harvested)
|
|
mismatches++;
|
|
}
|
|
check_u32(mismatches, 0u, "all 16384 harvest patterns round-trip through the word");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Tensix layout under a harvest */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** The n-th translated Tensix x: 1..7 then 10..16 ([COORD] gap at 8, 9). */
|
|
static uint32_t translated_slot_x(uint32_t slot)
|
|
{
|
|
return slot < 7u ? slot + 1u : slot + 3u;
|
|
}
|
|
|
|
/** Logical columns: NoC #0 position, translated position, renumbering rules. */
|
|
static void check_tensix_columns(const ft_tt_grid_info *grid, uint32_t harvested, uint32_t columns)
|
|
{
|
|
ft_tt_noc_coord coord = { 0u, 0u };
|
|
uint32_t logical = 0u;
|
|
uint32_t column;
|
|
uint32_t row;
|
|
|
|
for (column = 0; column < BH_DOC_TENSIX_COLUMNS; column++) {
|
|
uint32_t noc0_x = bh_doc_tensix_noc0_x(column);
|
|
uint32_t translated_x = bh_doc_tensix_translated_x(noc0_x, harvested);
|
|
bool fused = (harvested & (1u << column)) != 0u;
|
|
|
|
check(translated_x != 8u && translated_x != 9u, "translated x never uses 8 or 9");
|
|
check(fused == (translated_x > translated_slot_x(columns - 1u)),
|
|
"fused columns, and only they, sit above the live translated range");
|
|
for (row = 0; row < BH_DOC_TENSIX_ROWS; row++) {
|
|
uint32_t y = BH_DOC_FIRST_TENSIX_ROW + row;
|
|
|
|
check_at(ft_tt_coord_is_enabled(grid, FT_TT_COORD_NOC0, noc0_x, y) == !fused,
|
|
"NOC0 is_enabled follows the harvest", noc0_x, y);
|
|
check_at(ft_tt_coord_classify(grid, FT_TT_COORD_NOC0, noc0_x, y) == FT_TT_CORE_TENSIX,
|
|
"a fused Tensix keeps its type", noc0_x, y);
|
|
if (fused)
|
|
continue;
|
|
check_coord(ft_tt_coord_tensix_at(grid, logical, row, FT_TT_COORD_NOC0, &coord),
|
|
coord, noc0_x, y, "tensix_at NOC0", logical * 100u + row);
|
|
check_coord(ft_tt_coord_tensix_at(grid, logical, row, FT_TT_COORD_TRANSLATED, &coord),
|
|
coord, translated_x, y, "tensix_at TRANSLATED", logical * 100u + row);
|
|
}
|
|
if (!fused) {
|
|
check_u32(translated_x, translated_slot_x(logical),
|
|
"live columns take the lowest translated slots, in NoC #0 order");
|
|
logical++;
|
|
}
|
|
}
|
|
check_u32(logical, columns, "logical columns = enabled columns");
|
|
check_rc(ft_tt_coord_tensix_at(grid, columns, 0u, FT_TT_COORD_NOC0, &coord), FT_TT_EINVAL,
|
|
"tensix_at past the live columns");
|
|
}
|
|
|
|
/** Translated Tensix space: live slots below the enabled count, fused slots above. */
|
|
static void check_tensix_translated_slots(const ft_tt_grid_info *grid, uint32_t columns)
|
|
{
|
|
uint32_t slot;
|
|
uint32_t row;
|
|
|
|
for (slot = 0; slot < BH_DOC_TENSIX_COLUMNS; slot++) {
|
|
uint32_t x = translated_slot_x(slot);
|
|
|
|
for (row = 0; row < BH_DOC_TENSIX_ROWS; row++) {
|
|
uint32_t y = BH_DOC_FIRST_TENSIX_ROW + row;
|
|
|
|
check_at(ft_tt_coord_is_enabled(grid, FT_TT_COORD_TRANSLATED, x, y) == (slot < columns),
|
|
"translated slot is live iff below the enabled count", x, y);
|
|
check_at(ft_tt_coord_classify(grid, FT_TT_COORD_TRANSLATED, x, y) ==
|
|
FT_TT_CORE_TENSIX,
|
|
"translated Tensix slot classifies as Tensix", x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** By-index enumeration is dense: exactly the enabled nodes, each once, row-major. */
|
|
static void check_tensix_by_index(const ft_tt_grid_info *grid, uint32_t harvested,
|
|
uint32_t columns)
|
|
{
|
|
bool seen[BH_DOC_NOC_WIDTH][BH_DOC_NOC_HEIGHT] = { { false } };
|
|
ft_tt_noc_coord coord = { 0u, 0u };
|
|
ft_tt_noc_coord expected = { 0u, 0u };
|
|
uint32_t cores = columns * BH_DOC_TENSIX_ROWS;
|
|
uint32_t duplicates_or_fused = 0u;
|
|
uint32_t index;
|
|
|
|
for (index = 0; index < cores; index++) {
|
|
uint32_t column;
|
|
|
|
ft_tt_coord_tensix_at(grid, index % columns, index / columns, FT_TT_COORD_TRANSLATED,
|
|
&expected);
|
|
check_coord(ft_tt_coord_tensix_by_index(grid, index, FT_TT_COORD_TRANSLATED, &coord),
|
|
coord, expected.x, expected.y, "by_index TRANSLATED is row-major", index);
|
|
if (ft_tt_coord_tensix_by_index(grid, index, FT_TT_COORD_NOC0, &coord) != FT_TT_OK ||
|
|
coord.x >= BH_DOC_NOC_WIDTH || coord.y >= BH_DOC_NOC_HEIGHT) {
|
|
duplicates_or_fused++;
|
|
continue;
|
|
}
|
|
column = bh_doc_tensix_column_at_x(coord.x);
|
|
if (column == BH_DOC_TENSIX_COLUMNS || (harvested & (1u << column)) ||
|
|
seen[coord.x][coord.y])
|
|
duplicates_or_fused++;
|
|
seen[coord.x][coord.y] = true;
|
|
}
|
|
check_u32(duplicates_or_fused, 0u, "by_index NOC0 hits live Tensix only, each once");
|
|
check_u32(ft_tt_coord_tensix_core_count(grid), cores, "core count = live columns x 10");
|
|
check_rc(ft_tt_coord_tensix_by_index(grid, cores, FT_TT_COORD_NOC0, &coord), FT_TT_EINVAL,
|
|
"by_index at the core count is out of range");
|
|
}
|
|
|
|
static void check_tensix_harvest(uint32_t harvested, const char *what)
|
|
{
|
|
ft_tt_grid_info grid = make_grid(harvested, BH_DOC_ALL_BANKS_MASK);
|
|
uint32_t columns = BH_DOC_TENSIX_COLUMNS - count_bits(harvested);
|
|
int failed_before = checks_failed;
|
|
|
|
check_rc(ft_tt_coord_check_grid(&grid), FT_TT_OK, "harvested grid is accepted");
|
|
check_u32(ft_tt_coord_tensix_column_count(&grid), columns, "column count");
|
|
check_u32(ft_tt_coord_tensix_row_count(&grid), BH_DOC_TENSIX_ROWS, "rows are never fused");
|
|
check_tensix_columns(&grid, harvested, columns);
|
|
check_tensix_translated_slots(&grid, columns);
|
|
check_tensix_by_index(&grid, harvested, columns);
|
|
printf(" %-44s %s\n", what, checks_failed == failed_before ? "ok" : "FAILED");
|
|
}
|
|
|
|
static void test_single_column_harvests(void)
|
|
{
|
|
char what[64];
|
|
uint32_t column;
|
|
|
|
printf("Tensix: every single-column harvest\n");
|
|
for (column = 0; column < BH_DOC_TENSIX_COLUMNS; column++) {
|
|
snprintf(what, sizeof(what), "column %u (NoC #0 x = %u) fused", column,
|
|
bh_doc_tensix_noc0_x(column));
|
|
check_tensix_harvest(1u << column, what);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* [LUA] bh_y211, read literally: on the p100 example the translated x of the column at
|
|
* NoC #0 x is this table — checked as plain numbers, independent of the renumbering helper.
|
|
*/
|
|
static void test_p100_doc_numbering(void)
|
|
{
|
|
static const uint8_t translated_by_noc0_x[BH_DOC_NOC_WIDTH] = { 0, 1, 2, 3, 4, 15, 5, 6, 0,
|
|
0, 7, 16, 10, 11, 12, 13, 14 };
|
|
ft_tt_grid_info grid = make_grid(P100_FUSED_COLUMNS, BH_DOC_ALL_BANKS_MASK);
|
|
ft_tt_noc_coord coord = { 0u, 0u };
|
|
uint32_t logical = 0u;
|
|
uint32_t x;
|
|
|
|
printf("Tensix: the doc's p100 numbering (x = 5 and 11 fused)\n");
|
|
for (x = 1u; x < BH_DOC_NOC_WIDTH; x++) {
|
|
uint32_t column = bh_doc_tensix_column_at_x(x);
|
|
|
|
if (column == BH_DOC_TENSIX_COLUMNS)
|
|
continue;
|
|
check_u32(bh_doc_tensix_translated_x(x, P100_FUSED_COLUMNS), translated_by_noc0_x[x],
|
|
"renumbering helper agrees with the doc diagram");
|
|
if (P100_FUSED_COLUMNS & (1u << column))
|
|
continue;
|
|
check_coord(ft_tt_coord_tensix_at(&grid, logical, 0u, FT_TT_COORD_TRANSLATED, &coord),
|
|
coord, translated_by_noc0_x[x], BH_DOC_FIRST_TENSIX_ROW,
|
|
"p100 tensix_at TRANSLATED (doc bh_y211)", logical);
|
|
logical++;
|
|
}
|
|
check_u32(ft_tt_coord_tensix_core_count(&grid), 120u, "p100 has 120 Tensix ([COORD] 70+50)");
|
|
}
|
|
|
|
static void test_multi_column_harvests(void)
|
|
{
|
|
static const struct {
|
|
uint32_t harvested;
|
|
const char *what;
|
|
} cases[] = {
|
|
{ P100_FUSED_COLUMNS, "p100 doc example: x 5 + 11" },
|
|
{ (1u << 12) | (1u << 13), "x 15 + 16 (already at maximal X)" },
|
|
{ (1u << 0) | (1u << 1), "x 1 + 2 (westmost pair)" },
|
|
{ (1u << 0) | (1u << 13), "x 1 + 16 (both edges)" },
|
|
{ (1u << 6) | (1u << 7), "x 7 + 10 (either side of the gap)" },
|
|
{ 0x007Fu, "all seven west columns" },
|
|
{ 0x3F80u, "all seven east columns" },
|
|
{ 0x1555u, "alternate columns" },
|
|
{ BH_DOC_ALL_COLUMNS_MASK & ~1u, "all but x 1" },
|
|
{ BH_DOC_ALL_COLUMNS_MASK & ~(1u << 13), "all but x 16" },
|
|
{ (1u << 3) | (1u << 9) | (1u << 10), "three scattered (x 4, 12, 13)" },
|
|
};
|
|
uint32_t index;
|
|
|
|
printf("Tensix: multi-column harvests\n");
|
|
for (index = 0; index < sizeof(cases) / sizeof(cases[0]); index++)
|
|
check_tensix_harvest(cases[index].harvested, cases[index].what);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* DRAM harvests */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/** Translated DRAM alias liveness: [COORD] puts the fused bank at (18, 21-23) whichever
|
|
* bank it is ("fused off in p100", and the D2 example's x = 0 -> 18, y -> 21-23). */
|
|
static void check_dram_alias_liveness(const ft_tt_grid_info *grid)
|
|
{
|
|
uint32_t x;
|
|
uint32_t y;
|
|
|
|
for (x = BH_DOC_DRAM_ALIAS_FIRST_X; x <= BH_DOC_DRAM_ALIAS_FIRST_X + 1u; x++) {
|
|
for (y = BH_DOC_DRAM_ALIAS_FIRST_Y; y <= BH_DOC_DRAM_ALIAS_LAST_Y; y++) {
|
|
bool fused_slot = x == BH_DOC_DRAM_ALIAS_FIRST_X + 1u && y >= 21u;
|
|
|
|
check_at(ft_tt_coord_classify(grid, FT_TT_COORD_TRANSLATED, x, y) == FT_TT_CORE_DRAM,
|
|
"DRAM alias classifies as DRAM", x, y);
|
|
check_at(ft_tt_coord_is_enabled(grid, FT_TT_COORD_TRANSLATED, x, y) == !fused_slot,
|
|
"only (18, 21-23) is fused in translated space", x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void check_single_bank_harvest(uint32_t fused_bank)
|
|
{
|
|
ft_tt_grid_info grid = make_grid(0u, BH_DOC_ALL_BANKS_MASK & ~(1u << fused_bank));
|
|
ft_tt_noc_coord coord = { 0u, 0u };
|
|
uint32_t logical = 0u;
|
|
uint32_t bank;
|
|
uint32_t port;
|
|
int failed_before = checks_failed;
|
|
|
|
check_rc(ft_tt_coord_check_grid(&grid), FT_TT_OK, "one fused bank is accepted");
|
|
check_u32(ft_tt_coord_dram_bank_count(&grid), BH_DOC_DRAM_BANKS - 1u, "bank count");
|
|
for (bank = 0; bank < BH_DOC_DRAM_BANKS; bank++) {
|
|
for (port = 0; port < BH_DOC_DRAM_PORTS_PER_BANK; port++) {
|
|
uint32_t x = bh_doc_dram_noc0_x(bank);
|
|
uint32_t y = bh_doc_dram_port_noc0_y(bank, port);
|
|
|
|
check_at(ft_tt_coord_is_enabled(&grid, FT_TT_COORD_NOC0, x, y) == (bank != fused_bank),
|
|
"NOC0 DRAM tile is_enabled follows the harvest", x, y);
|
|
check_at(ft_tt_coord_classify(&grid, FT_TT_COORD_NOC0, x, y) == FT_TT_CORE_DRAM,
|
|
"a fused DRAM tile keeps its type", x, y);
|
|
if (bank == fused_bank)
|
|
continue;
|
|
check_coord(ft_tt_coord_dram_port(&grid, logical, port, FT_TT_COORD_NOC0, &coord),
|
|
coord, x, y, "dram_port NOC0 skips the fused bank", logical * 10u + port);
|
|
}
|
|
if (bank != fused_bank)
|
|
logical++;
|
|
}
|
|
check_rc(ft_tt_coord_dram_port(&grid, logical, 0u, FT_TT_COORD_NOC0, &coord), FT_TT_EINVAL,
|
|
"dram_port past the live banks");
|
|
/* ft_tt_coord.h contract: the harvested-bank alias numbering is not modelled. */
|
|
check_rc(ft_tt_coord_dram_bank(&grid, 0u, FT_TT_COORD_TRANSLATED, &coord), FT_TT_EUNAVAIL,
|
|
"dram_bank TRANSLATED refuses with a fused bank");
|
|
check_dram_alias_liveness(&grid);
|
|
printf(" bank %u fused%-33s %s\n", fused_bank, "",
|
|
checks_failed == failed_before ? "ok" : "FAILED");
|
|
}
|
|
|
|
static void test_dram_harvests(void)
|
|
{
|
|
ft_tt_grid_info p100 = make_grid(P100_FUSED_COLUMNS,
|
|
BH_DOC_ALL_BANKS_MASK & ~(1u << P100_FUSED_BANK));
|
|
uint32_t first;
|
|
uint32_t second;
|
|
uint32_t accepted_pairs = 0u;
|
|
|
|
printf("DRAM: every single-bank harvest (D2 is the doc's p100 example)\n");
|
|
for (first = 0; first < BH_DOC_DRAM_BANKS; first++)
|
|
check_single_bank_harvest(first);
|
|
|
|
printf("DRAM: at most one bank may be harvested\n");
|
|
for (first = 0; first < BH_DOC_DRAM_BANKS; first++) {
|
|
for (second = first + 1u; second < BH_DOC_DRAM_BANKS; second++) {
|
|
ft_tt_grid_info grid = make_grid(0u, BH_DOC_ALL_BANKS_MASK & ~(1u << first) &
|
|
~(1u << second));
|
|
|
|
if (ft_tt_coord_check_grid(&grid) != FT_TT_EIO ||
|
|
ft_tt_coord_dram_bank_count(&grid) != 0u)
|
|
accepted_pairs++;
|
|
}
|
|
}
|
|
check_u32(accepted_pairs, 0u, "all 28 two-bank harvests are refused and count 0 banks");
|
|
|
|
printf("the doc's p100: x 5 + 11 and D2 fused, no Ethernet\n");
|
|
check_rc(ft_tt_coord_check_grid(&p100), FT_TT_OK, "p100 grid is accepted");
|
|
check_u32(ft_tt_coord_tensix_core_count(&p100), 120u, "p100 Tensix cores");
|
|
check_u32(ft_tt_coord_dram_bank_count(&p100), 7u, "p100 DRAM banks (28 of 32 GiB)");
|
|
check_u32(ft_tt_coord_eth_channel_count(&p100), 0u, "p100 Ethernet channels");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
test_harvest_mask_round_trips();
|
|
test_single_column_harvests();
|
|
test_p100_doc_numbering();
|
|
test_multi_column_harvests();
|
|
test_dram_harvests();
|
|
|
|
printf("\ntest_harvesting: %d checks, %d failed\n", checks_run, checks_failed);
|
|
return checks_failed == 0 ? 0 : 1;
|
|
}
|