mirror of
https://github.com/xen-project/xen.git
synced 2026-10-28 02:01:24 +01:00
Bits through 24 are already defined, meaning that we're not far off needing the second word. Put both in right away. As both halves are present now, the arch_caps field is full width. Adjust the unit test, which notices. The bool bitfield names in the arch_caps union are unused, and somewhat out of date. They'll shortly be automatically generated. Add CPUID and MSR prefixes to the ./xen-cpuid verbose output, now that there are a mix of the two. Signed-off-by: Andrew Cooper <andrew.cooper3@citrix.com> Acked-by: Jan Beulich <jbeulich@suse.com>
677 lines
18 KiB
C
677 lines
18 KiB
C
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <err.h>
|
|
|
|
#include <xen-tools/common-macros.h>
|
|
#include <xen/asm/x86-vendors.h>
|
|
#include <xen/lib/x86/cpu-policy.h>
|
|
#include <xen/domctl.h>
|
|
|
|
static unsigned int nr_failures;
|
|
#define fail(fmt, ...) \
|
|
({ \
|
|
nr_failures++; \
|
|
(void)printf(fmt, ##__VA_ARGS__); \
|
|
})
|
|
|
|
#define memdup(ptr) \
|
|
({ \
|
|
typeof(*(ptr)) *p_ = (ptr); \
|
|
void *n_ = malloc(sizeof(*p_)); \
|
|
\
|
|
if ( !n_ ) \
|
|
err(1, "%s malloc failure", __func__); \
|
|
\
|
|
memcpy(n_, p_, sizeof(*p_)); \
|
|
})
|
|
|
|
static void test_vendor_identification(void)
|
|
{
|
|
static const struct test {
|
|
union {
|
|
char ident[12];
|
|
struct {
|
|
uint32_t b, d, c;
|
|
};
|
|
};
|
|
unsigned int vendor;
|
|
} tests[] = {
|
|
/* The 1st entry should remain here to work around gcc bug 91667. */
|
|
{ { "" }, X86_VENDOR_UNKNOWN },
|
|
{ { " " }, X86_VENDOR_UNKNOWN },
|
|
{ { "xxxxxxxxxxxx" }, X86_VENDOR_UNKNOWN },
|
|
|
|
{ { "GenuineIntel" }, X86_VENDOR_INTEL },
|
|
{ { "AuthenticAMD" }, X86_VENDOR_AMD },
|
|
{ { "CentaurHauls" }, X86_VENDOR_CENTAUR },
|
|
{ { " Shanghai " }, X86_VENDOR_SHANGHAI },
|
|
{ { "HygonGenuine" }, X86_VENDOR_HYGON },
|
|
};
|
|
|
|
printf("Testing CPU vendor identification:\n");
|
|
|
|
for ( size_t i = 0; i < ARRAY_SIZE(tests); ++i )
|
|
{
|
|
const struct test *t = &tests[i];
|
|
unsigned int vendor = x86_cpuid_lookup_vendor(t->b, t->c, t->d);
|
|
|
|
if ( vendor != t->vendor )
|
|
fail(" Test '%.12s', expected vendor %u, got %u\n",
|
|
t->ident, t->vendor, vendor);
|
|
}
|
|
}
|
|
|
|
static bool leaves_are_sorted(const xen_cpuid_leaf_t *leaves, unsigned int nr)
|
|
{
|
|
for ( unsigned int i = 1; i < nr; ++i )
|
|
{
|
|
/* leaf index went backwards => not sorted. */
|
|
if ( leaves[i - 1].leaf > leaves[i].leaf )
|
|
return false;
|
|
|
|
/* leaf index went forwards => ok */
|
|
if ( leaves[i - 1].leaf < leaves[i].leaf )
|
|
continue;
|
|
|
|
/* leave index the same, subleaf didn't increase => not sorted. */
|
|
if ( leaves[i - 1].subleaf >= leaves[i].subleaf )
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool msrs_are_sorted(const xen_msr_entry_t *entries, unsigned int nr)
|
|
{
|
|
for ( unsigned int i = 1; i < nr; ++i )
|
|
/* MSR index went backwards => not sorted. */
|
|
if ( entries[i - 1].idx > entries[i].idx )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void test_cpuid_current(void)
|
|
{
|
|
struct cpu_policy p;
|
|
xen_cpuid_leaf_t leaves[CPUID_MAX_SERIALISED_LEAVES];
|
|
unsigned int nr = ARRAY_SIZE(leaves);
|
|
int rc;
|
|
|
|
printf("Testing CPUID on current CPU\n");
|
|
|
|
x86_cpu_policy_fill_native(&p);
|
|
|
|
rc = x86_cpuid_copy_to_buffer(&p, leaves, &nr);
|
|
if ( rc != 0 )
|
|
return fail(" Serialise, expected rc 0, got %d\n", rc);
|
|
|
|
if ( !leaves_are_sorted(leaves, nr) )
|
|
return fail(" Leaves not sorted\n");
|
|
}
|
|
|
|
static void test_cpuid_serialise_success(void)
|
|
{
|
|
static const struct test {
|
|
struct cpu_policy p;
|
|
const char *name;
|
|
unsigned int nr_leaves;
|
|
} tests[] = {
|
|
{
|
|
.name = "empty policy",
|
|
.nr_leaves = 4,
|
|
},
|
|
|
|
/* Leaf 4 serialisation stops at the first subleaf with type 0. */
|
|
{
|
|
.name = "empty leaf 4",
|
|
.p = {
|
|
.basic.max_leaf = 4,
|
|
},
|
|
.nr_leaves = 4 + 4,
|
|
},
|
|
{
|
|
.name = "partial leaf 4",
|
|
.p = {
|
|
.basic.max_leaf = 4,
|
|
.cache.subleaf[0].type = 1,
|
|
},
|
|
.nr_leaves = 4 + 4 + 1,
|
|
},
|
|
|
|
/* Leaf 7 serialisation stops at max_subleaf. */
|
|
{
|
|
.name = "empty leaf 7",
|
|
.p = {
|
|
.basic.max_leaf = 7,
|
|
},
|
|
.nr_leaves = 4 + 7,
|
|
},
|
|
{
|
|
.name = "partial leaf 7",
|
|
.p = {
|
|
.basic.max_leaf = 7,
|
|
.feat.max_subleaf = 1,
|
|
},
|
|
.nr_leaves = 4 + 7 + 1,
|
|
},
|
|
|
|
/* Leaf 0xb serialisation stops at the first subleaf with type 0. */
|
|
{
|
|
.name = "empty leaf 0xb",
|
|
.p = {
|
|
.basic.max_leaf = 0xb,
|
|
},
|
|
.nr_leaves = 4 + 0xb,
|
|
},
|
|
{
|
|
.name = "partial leaf 0xb",
|
|
.p = {
|
|
.basic.max_leaf = 0xb,
|
|
.topo.subleaf[0].type = 1,
|
|
},
|
|
.nr_leaves = 4 + 0xb + 1,
|
|
},
|
|
|
|
/*
|
|
* Leaf 0xd serialisation automatically has two leaves, and stops the
|
|
* highest bit set in {xcr0,xss}_{high,low}.
|
|
*/
|
|
{
|
|
.name = "empty leaf 0xd",
|
|
.p = {
|
|
.basic.max_leaf = 0xd,
|
|
},
|
|
.nr_leaves = 4 + 0xd + 1,
|
|
},
|
|
{
|
|
.name = "partial 0xd",
|
|
.p = {
|
|
.basic.max_leaf = 0xd,
|
|
.xstate.xcr0_low = 7,
|
|
},
|
|
.nr_leaves = 4 + 0xd + 1 + 1,
|
|
},
|
|
};
|
|
|
|
printf("Testing CPUID serialise success:\n");
|
|
|
|
for ( size_t i = 0; i < ARRAY_SIZE(tests); ++i )
|
|
{
|
|
const struct test *t = &tests[i];
|
|
unsigned int nr = t->nr_leaves;
|
|
xen_cpuid_leaf_t *leaves = malloc(nr * sizeof(*leaves));
|
|
int rc;
|
|
|
|
if ( !leaves )
|
|
err(1, "%s() malloc failure", __func__);
|
|
|
|
rc = x86_cpuid_copy_to_buffer(&t->p, leaves, &nr);
|
|
|
|
if ( rc != 0 )
|
|
{
|
|
fail(" Test %s, expected rc 0, got %d\n",
|
|
t->name, rc);
|
|
goto test_done;
|
|
}
|
|
|
|
if ( nr != t->nr_leaves )
|
|
{
|
|
fail(" Test %s, expected %u leaves, got %u\n",
|
|
t->name, t->nr_leaves, nr);
|
|
goto test_done;
|
|
}
|
|
|
|
if ( !leaves_are_sorted(leaves, nr) )
|
|
{
|
|
fail(" Test %s, leaves not sorted\n",
|
|
t->name);
|
|
goto test_done;
|
|
}
|
|
|
|
test_done:
|
|
free(leaves);
|
|
}
|
|
}
|
|
|
|
static void test_msr_serialise_success(void)
|
|
{
|
|
static const struct test {
|
|
struct cpu_policy p;
|
|
const char *name;
|
|
unsigned int nr_msrs;
|
|
} tests[] = {
|
|
{
|
|
.name = "empty policy",
|
|
.nr_msrs = MSR_MAX_SERIALISED_ENTRIES,
|
|
},
|
|
};
|
|
|
|
printf("Testing MSR serialise success:\n");
|
|
|
|
for ( size_t i = 0; i < ARRAY_SIZE(tests); ++i )
|
|
{
|
|
const struct test *t = &tests[i];
|
|
unsigned int nr = t->nr_msrs;
|
|
xen_msr_entry_t *msrs = malloc(nr * sizeof(*msrs));
|
|
int rc;
|
|
|
|
if ( !msrs )
|
|
err(1, "%s() malloc failure", __func__);
|
|
|
|
rc = x86_msr_copy_to_buffer(&t->p, msrs, &nr);
|
|
|
|
if ( rc != 0 )
|
|
{
|
|
fail(" Test %s, expected rc 0, got %d\n",
|
|
t->name, rc);
|
|
goto test_done;
|
|
}
|
|
|
|
if ( nr != t->nr_msrs )
|
|
{
|
|
fail(" Test %s, expected %u msrs, got %u\n",
|
|
t->name, t->nr_msrs, nr);
|
|
goto test_done;
|
|
}
|
|
|
|
if ( !msrs_are_sorted(msrs, nr) )
|
|
{
|
|
fail(" Test %s, MSR entries not sorted\n",
|
|
t->name);
|
|
goto test_done;
|
|
}
|
|
|
|
test_done:
|
|
free(msrs);
|
|
}
|
|
}
|
|
|
|
static void test_cpuid_deserialise_failure(void)
|
|
{
|
|
static const struct test {
|
|
const char *name;
|
|
xen_cpuid_leaf_t leaf;
|
|
} tests[] = {
|
|
{
|
|
.name = "incorrect basic subleaf",
|
|
.leaf = { .leaf = 0, .subleaf = 0 },
|
|
},
|
|
{
|
|
.name = "incorrect hv1 subleaf",
|
|
.leaf = { .leaf = 0x40000000, .subleaf = 0 },
|
|
},
|
|
{
|
|
.name = "incorrect hv2 subleaf",
|
|
.leaf = { .leaf = 0x40000100, .subleaf = 0 },
|
|
},
|
|
{
|
|
.name = "incorrect extd subleaf",
|
|
.leaf = { .leaf = 0x80000000, .subleaf = 0 },
|
|
},
|
|
{
|
|
.name = "OoB basic leaf",
|
|
.leaf = { .leaf = CPUID_GUEST_NR_BASIC },
|
|
},
|
|
{
|
|
.name = "OoB cache leaf",
|
|
.leaf = { .leaf = 0x4, .subleaf = CPUID_GUEST_NR_CACHE },
|
|
},
|
|
{
|
|
.name = "OoB feat leaf",
|
|
.leaf = { .leaf = 0x7, .subleaf = CPUID_GUEST_NR_FEAT },
|
|
},
|
|
{
|
|
.name = "OoB topo leaf",
|
|
.leaf = { .leaf = 0xb, .subleaf = CPUID_GUEST_NR_TOPO },
|
|
},
|
|
{
|
|
.name = "OoB xstate leaf",
|
|
.leaf = { .leaf = 0xd, .subleaf = CPUID_GUEST_NR_XSTATE },
|
|
},
|
|
{
|
|
.name = "OoB extd leaf",
|
|
.leaf = { .leaf = 0x80000000 | CPUID_GUEST_NR_EXTD },
|
|
},
|
|
};
|
|
|
|
printf("Testing CPUID deserialise failure:\n");
|
|
|
|
for ( size_t i = 0; i < ARRAY_SIZE(tests); ++i )
|
|
{
|
|
const struct test *t = &tests[i];
|
|
uint32_t err_leaf, err_subleaf;
|
|
int rc;
|
|
|
|
/* No writes should occur. Use NULL to catch errors. */
|
|
rc = x86_cpuid_copy_from_buffer(NULL, &t->leaf, 1,
|
|
&err_leaf, &err_subleaf);
|
|
|
|
if ( rc != -ERANGE )
|
|
{
|
|
fail(" Test %s, expected rc %d, got %d\n",
|
|
t->name, -ERANGE, rc);
|
|
continue;
|
|
}
|
|
|
|
if ( err_leaf != t->leaf.leaf || err_subleaf != t->leaf.subleaf )
|
|
{
|
|
fail(" Test %s, expected err %08x:%08x, got %08x:%08x\n",
|
|
t->name, t->leaf.leaf, t->leaf.subleaf,
|
|
err_leaf, err_subleaf);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void test_msr_deserialise_failure(void)
|
|
{
|
|
static const struct test {
|
|
const char *name;
|
|
xen_msr_entry_t msr;
|
|
int rc;
|
|
} tests[] = {
|
|
{
|
|
.name = "bad msr index",
|
|
.msr = { .idx = 0xdeadc0de },
|
|
.rc = -ERANGE,
|
|
},
|
|
{
|
|
.name = "nonzero flags",
|
|
.msr = { .idx = 0xce, .flags = 1 },
|
|
.rc = -EINVAL,
|
|
},
|
|
{
|
|
.name = "truncated val",
|
|
.msr = { .idx = 0xce, .val = ~0ull },
|
|
.rc = -EOVERFLOW,
|
|
},
|
|
};
|
|
|
|
printf("Testing MSR deserialise failure:\n");
|
|
|
|
for ( size_t i = 0; i < ARRAY_SIZE(tests); ++i )
|
|
{
|
|
const struct test *t = &tests[i];
|
|
uint32_t err_msr;
|
|
int rc;
|
|
|
|
/* No writes should occur. Use NULL to catch errors. */
|
|
rc = x86_msr_copy_from_buffer(NULL, &t->msr, 1, &err_msr);
|
|
|
|
if ( rc != t->rc )
|
|
{
|
|
fail(" Test %s, expected rc %d, got %d\n",
|
|
t->name, t->rc, rc);
|
|
continue;
|
|
}
|
|
|
|
if ( err_msr != t->msr.idx )
|
|
{
|
|
fail(" Test %s, expected err_msr %#x, got %#x\n",
|
|
t->name, t->msr.idx, err_msr);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void test_cpuid_out_of_range_clearing(void)
|
|
{
|
|
static const struct test {
|
|
const char *name;
|
|
unsigned int nr_markers;
|
|
struct cpu_policy p;
|
|
} tests[] = {
|
|
{
|
|
.name = "basic",
|
|
.nr_markers = 1,
|
|
.p = {
|
|
/* Retains marker in leaf 0. Clears others. */
|
|
.basic.max_leaf = 0,
|
|
.basic.vendor_ebx = 0xc2,
|
|
|
|
.basic.raw_fms = 0xc2,
|
|
.cache.raw[0].a = 0xc2,
|
|
.feat.raw[0].a = 0xc2,
|
|
.topo.raw[0].a = 0xc2,
|
|
.xstate.raw[0].a = 0xc2,
|
|
.xstate.raw[1].a = 0xc2,
|
|
},
|
|
},
|
|
{
|
|
.name = "cache",
|
|
.nr_markers = 1,
|
|
.p = {
|
|
/* Retains marker in subleaf 0. Clears others. */
|
|
.basic.max_leaf = 4,
|
|
.cache.raw[0] = { .a = 1, .b = 0xc2 },
|
|
|
|
.cache.raw[1].b = 0xc2,
|
|
.feat.raw[0].a = 0xc2,
|
|
.topo.raw[0].a = 0xc2,
|
|
.xstate.raw[0].a = 0xc2,
|
|
.xstate.raw[1].a = 0xc2,
|
|
},
|
|
},
|
|
{
|
|
.name = "feat",
|
|
.nr_markers = 1,
|
|
.p = {
|
|
/* Retains marker in subleaf 0. Clears others. */
|
|
.basic.max_leaf = 7,
|
|
.feat.raw[0].b = 0xc2,
|
|
|
|
.feat.raw[1].b = 0xc2,
|
|
.topo.raw[0].a = 0xc2,
|
|
.xstate.raw[0].a = 0xc2,
|
|
.xstate.raw[1].a = 0xc2,
|
|
},
|
|
},
|
|
{
|
|
.name = "topo",
|
|
.nr_markers = 1,
|
|
.p = {
|
|
/* Retains marker in subleaf 0. Clears others. */
|
|
.basic.max_leaf = 0xb,
|
|
.topo.raw[0] = { .b = 0xc2, .c = 0x0100 },
|
|
|
|
.topo.raw[1].b = 0xc2,
|
|
.xstate.raw[0].a = 0xc2,
|
|
.xstate.raw[1].a = 0xc2,
|
|
},
|
|
},
|
|
{
|
|
.name = "xstate x87",
|
|
.nr_markers = 2,
|
|
.p = {
|
|
/* First two subleaves always valid. Others cleared. */
|
|
.basic.max_leaf = 0xd,
|
|
.xstate.raw[0].a = 1,
|
|
.xstate.raw[0].b = 0xc2,
|
|
.xstate.raw[1].b = 0xc2,
|
|
|
|
.xstate.raw[2].b = 0xc2,
|
|
.xstate.raw[3].b = 0xc2,
|
|
},
|
|
},
|
|
{
|
|
.name = "xstate sse",
|
|
.nr_markers = 2,
|
|
.p = {
|
|
/* First two subleaves always valid. Others cleared. */
|
|
.basic.max_leaf = 0xd,
|
|
.xstate.raw[0].a = 2,
|
|
.xstate.raw[0].b = 0xc2,
|
|
.xstate.raw[1].b = 0xc2,
|
|
|
|
.xstate.raw[2].b = 0xc2,
|
|
.xstate.raw[3].b = 0xc2,
|
|
},
|
|
},
|
|
{
|
|
.name = "xstate avx",
|
|
.nr_markers = 3,
|
|
.p = {
|
|
/* Third subleaf also valid. Others cleared. */
|
|
.basic.max_leaf = 0xd,
|
|
.xstate.raw[0].a = 7,
|
|
.xstate.raw[0].b = 0xc2,
|
|
.xstate.raw[1].b = 0xc2,
|
|
.xstate.raw[2].b = 0xc2,
|
|
|
|
.xstate.raw[3].b = 0xc2,
|
|
},
|
|
},
|
|
{
|
|
.name = "extd",
|
|
.nr_markers = 1,
|
|
.p = {
|
|
/* Retains marker in leaf 0. Clears others. */
|
|
.extd.max_leaf = 0,
|
|
.extd.vendor_ebx = 0xc2,
|
|
|
|
.extd.raw_fms = 0xc2,
|
|
},
|
|
},
|
|
};
|
|
|
|
printf("Testing CPUID out-of-range clearing:\n");
|
|
|
|
for ( size_t i = 0; i < ARRAY_SIZE(tests); ++i )
|
|
{
|
|
const struct test *t = &tests[i];
|
|
struct cpu_policy *p = memdup(&t->p);
|
|
void *ptr;
|
|
unsigned int nr_markers;
|
|
|
|
x86_cpu_policy_clear_out_of_range_leaves(p);
|
|
|
|
/* Count the number of 0xc2's still remaining. */
|
|
for ( ptr = p, nr_markers = 0;
|
|
(ptr = memchr(ptr, 0xc2, (void *)p + sizeof(*p) - ptr));
|
|
ptr++, nr_markers++ )
|
|
;
|
|
|
|
if ( nr_markers != t->nr_markers )
|
|
fail(" Test %s fail - expected %u markers, got %u\n",
|
|
t->name, t->nr_markers, nr_markers);
|
|
|
|
free(p);
|
|
}
|
|
}
|
|
|
|
static void test_is_compatible_success(void)
|
|
{
|
|
static struct test {
|
|
const char *name;
|
|
struct cpu_policy host, guest;
|
|
} tests[] = {
|
|
{
|
|
.name = "Host CPUID faulting, Guest not",
|
|
.host = {
|
|
.platform_info.cpuid_faulting = true,
|
|
},
|
|
},
|
|
{
|
|
.name = "Host CPUID faulting, Guest wanted",
|
|
.host = {
|
|
.platform_info.cpuid_faulting = true,
|
|
},
|
|
.guest = {
|
|
.platform_info.cpuid_faulting = true,
|
|
},
|
|
},
|
|
};
|
|
struct cpu_policy_errors no_errors = INIT_CPU_POLICY_ERRORS;
|
|
|
|
printf("Testing policy compatibility success:\n");
|
|
|
|
for ( size_t i = 0; i < ARRAY_SIZE(tests); ++i )
|
|
{
|
|
struct test *t = &tests[i];
|
|
struct cpu_policy_errors e;
|
|
int res = x86_cpu_policies_are_compatible(&t->host, &t->guest, &e);
|
|
|
|
/* Check the expected error output. */
|
|
if ( res != 0 || memcmp(&no_errors, &e, sizeof(no_errors)) )
|
|
fail(" Test '%s' expected no errors\n"
|
|
" got res %d { leaf %08x, subleaf %08x, msr %08x }\n",
|
|
t->name, res, e.leaf, e.subleaf, e.msr);
|
|
}
|
|
}
|
|
|
|
static void test_is_compatible_failure(void)
|
|
{
|
|
static struct test {
|
|
const char *name;
|
|
struct cpu_policy host, guest;
|
|
struct cpu_policy_errors e;
|
|
} tests[] = {
|
|
{
|
|
.name = "Host basic.max_leaf out of range",
|
|
.guest.basic.max_leaf = 1,
|
|
.e = { 0, -1, -1 },
|
|
},
|
|
{
|
|
.name = "Host extd.max_leaf out of range",
|
|
.guest.extd.max_leaf = 1,
|
|
.e = { 0x80000000, -1, -1 },
|
|
},
|
|
{
|
|
.name = "Host no CPUID faulting, Guest wanted",
|
|
.guest = {
|
|
.platform_info.cpuid_faulting = true,
|
|
},
|
|
.e = { -1, -1, 0xce },
|
|
},
|
|
};
|
|
|
|
printf("Testing policy compatibility failure:\n");
|
|
|
|
for ( size_t i = 0; i < ARRAY_SIZE(tests); ++i )
|
|
{
|
|
struct test *t = &tests[i];
|
|
struct cpu_policy_errors e;
|
|
int res = x86_cpu_policies_are_compatible(&t->host, &t->guest, &e);
|
|
|
|
/* Check the expected error output. */
|
|
if ( res == 0 || memcmp(&t->e, &e, sizeof(t->e)) )
|
|
fail(" Test '%s' res %d\n"
|
|
" expected { leaf %08x, subleaf %08x, msr %08x }\n"
|
|
" got { leaf %08x, subleaf %08x, msr %08x }\n",
|
|
t->name, res,
|
|
t->e.leaf, t->e.subleaf, t->e.msr,
|
|
e.leaf, e.subleaf, e.msr);
|
|
}
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
printf("CPU Policy unit tests\n");
|
|
|
|
test_vendor_identification();
|
|
|
|
test_cpuid_current();
|
|
test_cpuid_serialise_success();
|
|
test_cpuid_deserialise_failure();
|
|
test_cpuid_out_of_range_clearing();
|
|
|
|
test_msr_serialise_success();
|
|
test_msr_deserialise_failure();
|
|
|
|
test_is_compatible_success();
|
|
test_is_compatible_failure();
|
|
|
|
if ( nr_failures )
|
|
printf("Done: %u failures\n", nr_failures);
|
|
else
|
|
printf("Done: all ok\n");
|
|
|
|
return !!nr_failures;
|
|
}
|