mirror of
https://github.com/Telecominfraproject/OpenCellular.git
synced 2026-01-09 00:51:29 +00:00
To avoid SRAM footprint, let's use dynamic memory allocation in nvram_vars. No one is using this module yet, but the cr50 use case is coming up. BRANCH=none BUG=chrome-os-partner:61107 TEST=make buildall -j passes Change-Id: I21534430217ad387a3787fcc127da596a1b48e03 Signed-off-by: Vadim Bendebury <vbendeb@chromium.org> Reviewed-on: https://chromium-review.googlesource.com/426088 Reviewed-by: Bill Richardson <wfrichar@chromium.org>
534 lines
14 KiB
C
534 lines
14 KiB
C
/* Copyright 2016 The Chromium OS Authors. All rights reserved.
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
*
|
|
* Test of the key=val variable implementation (set, get, delete, etc).
|
|
*/
|
|
|
|
#include <string.h>
|
|
|
|
#include "common.h"
|
|
#include "compile_time_macros.h"
|
|
#include "nvmem.h"
|
|
#include "nvmem_vars.h"
|
|
#include "printf.h"
|
|
#include "shared_mem.h"
|
|
#include "test_util.h"
|
|
|
|
/* Declare the user regions (see test_config.h) */
|
|
uint32_t nvmem_user_sizes[] = {
|
|
CONFIG_FLASH_NVMEM_VARS_USER_SIZE,
|
|
};
|
|
BUILD_ASSERT(ARRAY_SIZE(nvmem_user_sizes) == NVMEM_NUM_USERS);
|
|
|
|
/****************************************************************************/
|
|
/* Mock the flash storage */
|
|
|
|
static uint8_t ram_buffer[CONFIG_FLASH_NVMEM_VARS_USER_SIZE];
|
|
static uint8_t flash_buffer[CONFIG_FLASH_NVMEM_VARS_USER_SIZE];
|
|
|
|
extern char *rbuf;
|
|
|
|
/* Internal functions exported for test */
|
|
void release_local_copy(void)
|
|
{
|
|
rbuf = NULL;
|
|
}
|
|
|
|
int get_local_copy(void)
|
|
{
|
|
if (!rbuf) {
|
|
memcpy(ram_buffer, flash_buffer, sizeof(ram_buffer));
|
|
rbuf = (char *)ram_buffer;
|
|
}
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
int nvmem_read(uint32_t startOffset, uint32_t size,
|
|
void *data_, enum nvmem_users user)
|
|
{
|
|
/* Our mocks make some assumptions */
|
|
if (startOffset != 0 ||
|
|
size > CONFIG_FLASH_NVMEM_VARS_USER_SIZE ||
|
|
user != CONFIG_FLASH_NVMEM_VARS_USER_NUM)
|
|
return EC_ERROR_UNIMPLEMENTED;
|
|
|
|
if (!data_)
|
|
return EC_ERROR_INVAL;
|
|
|
|
memcpy(data_, flash_buffer, size);
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
int nvmem_write(uint32_t startOffset, uint32_t size,
|
|
void *data_, enum nvmem_users user)
|
|
{
|
|
/* Our mocks make some assumptions */
|
|
if (startOffset != 0 ||
|
|
size > CONFIG_FLASH_NVMEM_VARS_USER_SIZE ||
|
|
user != CONFIG_FLASH_NVMEM_VARS_USER_NUM)
|
|
return EC_ERROR_UNIMPLEMENTED;
|
|
|
|
if (!data_)
|
|
return EC_ERROR_INVAL;
|
|
|
|
memcpy(ram_buffer, data_, size);
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
int nvmem_commit(void)
|
|
{
|
|
memcpy(flash_buffer, ram_buffer, CONFIG_FLASH_NVMEM_VARS_USER_SIZE);
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* Helper routines */
|
|
|
|
static void erase_flash(void)
|
|
{
|
|
/* Invalidate the RAM cache */
|
|
release_local_copy();
|
|
|
|
/* Zero flash */
|
|
memset(flash_buffer, 0xff, sizeof(flash_buffer));
|
|
}
|
|
|
|
/* Erase flash, then copy data_ over it */
|
|
static void load_flash(const uint8_t *data_, size_t data_len)
|
|
{
|
|
erase_flash();
|
|
memcpy(flash_buffer, data_, data_len);
|
|
}
|
|
|
|
/* Return true if flash matches data_, and is followed by 0xff to the end */
|
|
static int verify_flash(const uint8_t *data_, size_t data_len)
|
|
{
|
|
size_t i;
|
|
|
|
/* mismatch means false */
|
|
if (memcmp(flash_buffer, data_, data_len))
|
|
return 0;
|
|
|
|
for (i = data_len;
|
|
i < CONFIG_FLASH_NVMEM_VARS_USER_SIZE - data_len;
|
|
i++)
|
|
if (flash_buffer[i] != 0xff)
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Treating both as strings, save the <key, value> pair.
|
|
*/
|
|
int str_setvar(const char *key, const char *val)
|
|
{
|
|
/* Only for tests, so assume the length will fit */
|
|
uint8_t key_len, val_len;
|
|
|
|
key_len = strlen(key);
|
|
val_len = val ? strlen(val) : 0;
|
|
|
|
return setvar(key, key_len, val, val_len);
|
|
}
|
|
|
|
/*
|
|
* Treating both as strings, lookup the key and compare the result with the
|
|
* expected value. Return true if they match.
|
|
*/
|
|
static int str_matches(const char *key, const char *expected_val)
|
|
{
|
|
const struct tuple *t = getvar(key, strlen(key));
|
|
uint8_t expected_len;
|
|
|
|
if (!expected_val && !t)
|
|
return 1;
|
|
|
|
if (expected_val && !t)
|
|
return 0;
|
|
|
|
if (!expected_val && t)
|
|
return 0;
|
|
|
|
expected_len = strlen(expected_val);
|
|
return !memcmp(tuple_val(t), expected_val, expected_len);
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/* Tests */
|
|
|
|
static int check_init(void)
|
|
{
|
|
/* Valid entries */
|
|
const uint8_t good[] = { 0x01, 0x01, 0x00, 'A', 'a',
|
|
0x01, 0x01, 0x00, 'B', 'b',
|
|
0x00 };
|
|
|
|
/* Empty variables are 0x00, followed by all 0xff */
|
|
const uint8_t empty[] = { 0x00 };
|
|
|
|
/*
|
|
* This is parsed as though there's only one variable, but it's wrong
|
|
* because the rest of the storage isn't 0xff.
|
|
*/
|
|
const uint8_t bad_key[] = { 0x01, 0x01, 0x00, 'A', 'a',
|
|
0x00, 0x01, 0x00, 'B', 'b',
|
|
0x00 };
|
|
|
|
/* Zero-length variables are not allowed */
|
|
const uint8_t bad_val[] = { 0x01, 0x01, 0x00, 'A', 'a',
|
|
0x01, 0x00, 0x00, 'B', 'b',
|
|
0x00 };
|
|
|
|
/* The next constants use magic numbers based on on the region size */
|
|
BUILD_ASSERT(CONFIG_FLASH_NVMEM_VARS_USER_SIZE == 600);
|
|
|
|
/* This is one byte too large */
|
|
const uint8_t too_big[] = { [0] = 0xff, [1] = 0xff, /* 0 - 512 */
|
|
[513] = 0x01, [514] = 0x53, /* 513 - 599 */
|
|
[599] = 0x00 };
|
|
|
|
/* This should just barely fit */
|
|
const uint8_t just_right[] = { [0] = 0xff, [1] = 0xff, /* 0-512 */
|
|
[513] = 0x01, [514] = 0x52, /* 513-598 */
|
|
[599] = 0x00 };
|
|
|
|
/* No end marker */
|
|
const uint8_t not_right[] = { [0] = 0xff, [1] = 0xff, /* 0-512 */
|
|
[513] = 0x01, [514] = 0x52, /* 513-598 */
|
|
[599] = 0xff };
|
|
|
|
load_flash(good, sizeof(good));
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(good, sizeof(good)));
|
|
|
|
load_flash(empty, sizeof(empty));
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(empty, sizeof(empty)));
|
|
|
|
/* All 0xff quickly runs off the end of the storage */
|
|
erase_flash();
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(empty, sizeof(empty)));
|
|
|
|
load_flash(bad_key, sizeof(bad_key));
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(empty, sizeof(empty)));
|
|
|
|
load_flash(bad_val, sizeof(bad_val));
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(empty, sizeof(empty)));
|
|
|
|
load_flash(too_big, sizeof(too_big));
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(empty, sizeof(empty)));
|
|
|
|
load_flash(just_right, sizeof(just_right));
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(just_right, sizeof(just_right)));
|
|
|
|
load_flash(not_right, sizeof(not_right));
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(empty, sizeof(empty)));
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int simple_search(void)
|
|
{
|
|
const uint8_t preload[] = {
|
|
0x02, 0x02, 0x00, 'h', 'o', 'y', 'o',
|
|
0x02, 0x4, 0x00, 'y', 'o', 'h', 'o', 'y', 'o',
|
|
0x02, 0x06, 0x00, 'm', 'o', 'y', 'o', 'h', 'o', 'y', 'o',
|
|
0x00 };
|
|
|
|
load_flash(preload, sizeof(preload));
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(preload, sizeof(preload)));
|
|
|
|
TEST_ASSERT(str_matches("no", 0));
|
|
TEST_ASSERT(str_matches("ho", "yo"));
|
|
TEST_ASSERT(str_matches("yo", "hoyo"));
|
|
TEST_ASSERT(str_matches("mo", "yohoyo"));
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int simple_write(void)
|
|
{
|
|
const uint8_t after_one[] = {
|
|
0x02, 0x02, 0x00, 'h', 'o', 'y', 'o',
|
|
0x00 };
|
|
|
|
const uint8_t after_two[] = {
|
|
0x02, 0x02, 0x00, 'h', 'o', 'y', 'o',
|
|
0x02, 0x4, 0x00, 'y', 'o', 'h', 'o', 'y', 'o',
|
|
0x00 };
|
|
|
|
const uint8_t after_three[] = {
|
|
0x02, 0x02, 0x00, 'h', 'o', 'y', 'o',
|
|
0x02, 0x4, 0x00, 'y', 'o', 'h', 'o', 'y', 'o',
|
|
0x02, 0x06, 0x00, 'm', 'o', 'y', 'o', 'h', 'o', 'y', 'o',
|
|
0x00 };
|
|
|
|
erase_flash();
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
|
|
TEST_ASSERT(setvar("ho", 2, "yo", 2) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(after_one, sizeof(after_one)));
|
|
|
|
TEST_ASSERT(setvar("yo", 2, "hoyo", 4) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(after_two, sizeof(after_two)));
|
|
|
|
TEST_ASSERT(setvar("mo", 2, "yohoyo", 6) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(after_three, sizeof(after_three)));
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int simple_delete(void)
|
|
{
|
|
const char start_with[] = {
|
|
0x01, 0x05, 0x00, 'A', 'a', 'a', 'a', 'a', 'a',
|
|
0x02, 0x03, 0x00, 'B', 'B', 'b', 'b', 'b',
|
|
0x03, 0x06, 0x00, 'C', 'C', 'C', 'x', 'y', 'z', 'p', 'd', 'q',
|
|
0x01, 0x03, 0x00, 'M', 'm', '0', 'm',
|
|
0x04, 0x01, 0x00, 'N', 'N', 'N', 'N', 'n',
|
|
0x00 };
|
|
|
|
const char after_one[] = {
|
|
0x02, 0x03, 0x00, 'B', 'B', 'b', 'b', 'b',
|
|
0x03, 0x06, 0x00, 'C', 'C', 'C', 'x', 'y', 'z', 'p', 'd', 'q',
|
|
0x01, 0x03, 0x00, 'M', 'm', '0', 'm',
|
|
0x04, 0x01, 0x00, 'N', 'N', 'N', 'N', 'n',
|
|
0x00 };
|
|
|
|
const char after_two[] = {
|
|
0x02, 0x03, 0x00, 'B', 'B', 'b', 'b', 'b',
|
|
0x03, 0x06, 0x00, 'C', 'C', 'C', 'x', 'y', 'z', 'p', 'd', 'q',
|
|
0x01, 0x03, 0x00, 'M', 'm', '0', 'm',
|
|
0x00 };
|
|
|
|
const char after_three[] = {
|
|
0x02, 0x03, 0x00, 'B', 'B', 'b', 'b', 'b',
|
|
0x01, 0x03, 0x00, 'M', 'm', '0', 'm',
|
|
0x00 };
|
|
|
|
const char empty[] = { 0x00 };
|
|
|
|
erase_flash();
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
|
|
TEST_ASSERT(setvar("A", 1, "aaaaa", 5) == EC_SUCCESS);
|
|
TEST_ASSERT(setvar("BB", 2, "bbb", 3) == EC_SUCCESS);
|
|
TEST_ASSERT(setvar("CCC", 3, "xyzpdq", 6) == EC_SUCCESS);
|
|
TEST_ASSERT(setvar("M", 1, "m0m", 3) == EC_SUCCESS);
|
|
TEST_ASSERT(setvar("NNNN", 4, "n", 1) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(start_with, sizeof(start_with)));
|
|
|
|
/* Zap first variable by setting var_len to 0 */
|
|
TEST_ASSERT(setvar("A", 1, "yohoyo", 0) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(after_one, sizeof(after_one)));
|
|
|
|
/* Zap last variable by passing null pointer */
|
|
TEST_ASSERT(setvar("NNNN", 4, 0, 3) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(after_two, sizeof(after_two)));
|
|
|
|
/* Ensure that zapping nonexistant variable does nothing */
|
|
TEST_ASSERT(setvar("XXX", 3, 0, 0) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(after_two, sizeof(after_two)));
|
|
|
|
/* Zap variable in the middle */
|
|
TEST_ASSERT(setvar("CCC", 3, 0, 0) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(after_three, sizeof(after_three)));
|
|
|
|
/* Zap the rest */
|
|
TEST_ASSERT(setvar("BB", 2, 0, 0) == EC_SUCCESS);
|
|
TEST_ASSERT(setvar("M", 1, 0, 0) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(empty, sizeof(empty)));
|
|
|
|
/* Zapping a nonexistant variable still does nothing */
|
|
TEST_ASSERT(setvar("XXX", 3, 0, 0) == EC_SUCCESS);
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
TEST_ASSERT(verify_flash(empty, sizeof(empty)));
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int complex_write(void)
|
|
{
|
|
erase_flash();
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
|
|
/* Do a bunch of writes and erases */
|
|
str_setvar("ho", "aa");
|
|
str_setvar("zo", "nn");
|
|
str_setvar("yo", "CCCCCCCC");
|
|
str_setvar("zooo", "yyyyyyy");
|
|
str_setvar("yo", "AA");
|
|
str_setvar("ho", 0);
|
|
str_setvar("yi", "BBB");
|
|
str_setvar("yi", "AA");
|
|
str_setvar("hixx", 0);
|
|
str_setvar("yo", "BBB");
|
|
str_setvar("zo", "");
|
|
str_setvar("hi", "bbb");
|
|
str_setvar("ho", "cccccc");
|
|
str_setvar("yo", "");
|
|
str_setvar("zo", "ggggg");
|
|
|
|
/* What do we expect to find? */
|
|
TEST_ASSERT(str_matches("hi", "bbb"));
|
|
TEST_ASSERT(str_matches("hixx", 0));
|
|
TEST_ASSERT(str_matches("ho", "cccccc"));
|
|
TEST_ASSERT(str_matches("yi", "AA"));
|
|
TEST_ASSERT(str_matches("yo", 0));
|
|
TEST_ASSERT(str_matches("zo", "ggggg"));
|
|
TEST_ASSERT(str_matches("zooo", "yyyyyyy"));
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int weird_keys(void)
|
|
{
|
|
uint8_t keyA[255];
|
|
uint8_t keyB[255];
|
|
const char *valA = "this is A";
|
|
const char *valB = "THIS IS b";
|
|
int i;
|
|
const struct tuple *t;
|
|
|
|
erase_flash();
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
|
|
for (i = 0; i < 255; i++) {
|
|
keyA[i] = i;
|
|
keyB[i] = 255 - i;
|
|
}
|
|
|
|
TEST_ASSERT(setvar(keyA, sizeof(keyA),
|
|
valA, strlen(valA)) == EC_SUCCESS);
|
|
|
|
TEST_ASSERT(setvar(keyB, sizeof(keyB),
|
|
valB, strlen(valB)) == EC_SUCCESS);
|
|
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
|
|
t = getvar(keyA, sizeof(keyA));
|
|
TEST_ASSERT(t);
|
|
TEST_ASSERT(t->val_len == strlen(valA));
|
|
TEST_ASSERT(memcmp(tuple_val(t), valA, strlen(valA)) == 0);
|
|
|
|
t = getvar(keyB, sizeof(keyB));
|
|
TEST_ASSERT(t);
|
|
TEST_ASSERT(t->val_len == strlen(valB));
|
|
TEST_ASSERT(memcmp(tuple_val(t), valB, strlen(valB)) == 0);
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int weird_values(void)
|
|
{
|
|
const char *keyA = "this is A";
|
|
const char *keyB = "THIS IS b";
|
|
char valA[255];
|
|
char valB[255];
|
|
int i;
|
|
const struct tuple *t;
|
|
|
|
erase_flash();
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
|
|
for (i = 0; i < 255; i++) {
|
|
valA[i] = i;
|
|
valB[i] = 255 - i;
|
|
}
|
|
|
|
TEST_ASSERT(setvar(keyA, strlen(keyA),
|
|
valA, sizeof(valA)) == EC_SUCCESS);
|
|
TEST_ASSERT(str_setvar("c", "CcC") == EC_SUCCESS);
|
|
TEST_ASSERT(setvar(keyB, strlen(keyB),
|
|
valB, sizeof(valB)) == EC_SUCCESS);
|
|
TEST_ASSERT(str_setvar("d", "dDd") == EC_SUCCESS);
|
|
|
|
TEST_ASSERT(writevars() == EC_SUCCESS);
|
|
|
|
t = getvar(keyA, strlen(keyA));
|
|
TEST_ASSERT(t);
|
|
TEST_ASSERT(memcmp(tuple_val(t), valA, sizeof(valA)) == 0);
|
|
|
|
t = getvar(keyB, strlen(keyB));
|
|
TEST_ASSERT(t);
|
|
TEST_ASSERT(memcmp(tuple_val(t), valB, sizeof(valB)) == 0);
|
|
|
|
TEST_ASSERT(str_matches("c", "CcC"));
|
|
TEST_ASSERT(str_matches("d", "dDd"));
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int fill_it_up(void)
|
|
{
|
|
int i, n;
|
|
char key[20];
|
|
|
|
erase_flash();
|
|
TEST_ASSERT(initvars() == EC_SUCCESS);
|
|
|
|
/*
|
|
* Some magic numbers here, because we want to use up 10 bytes at a
|
|
* time and end up with exactly 9 free bytes left.
|
|
*/
|
|
TEST_ASSERT(CONFIG_FLASH_NVMEM_VARS_USER_SIZE % 10 == 0);
|
|
n = CONFIG_FLASH_NVMEM_VARS_USER_SIZE / 10;
|
|
TEST_ASSERT(n < 1000);
|
|
|
|
/* Fill up the storage */
|
|
for (i = 0; i < n - 1; i++) {
|
|
/* 3-byte header, 5-char key, 2-char val, == 10 chars */
|
|
snprintf(key, sizeof(key), "kk%03d", i);
|
|
TEST_ASSERT(setvar(key, 5, "aa", 2) == EC_SUCCESS);
|
|
}
|
|
|
|
/*
|
|
* Should be nine bytes left in rbuf (because we need one more '\0' at
|
|
* the end). This won't fit.
|
|
*/
|
|
TEST_ASSERT(setvar("kk999", 5, "aa", 2) == EC_ERROR_OVERFLOW);
|
|
/* But this will. */
|
|
TEST_ASSERT(setvar("kk999", 5, "a", 1) == EC_SUCCESS);
|
|
/* And this, because it replaces a previous entry */
|
|
TEST_ASSERT(setvar("kk000", 5, "bc", 2) == EC_SUCCESS);
|
|
/* But this still won't fit */
|
|
TEST_ASSERT(setvar("kk999", 5, "de", 2) == EC_ERROR_OVERFLOW);
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
void run_test(void)
|
|
{
|
|
test_reset();
|
|
|
|
RUN_TEST(check_init);
|
|
RUN_TEST(simple_write);
|
|
RUN_TEST(simple_search);
|
|
RUN_TEST(simple_delete);
|
|
RUN_TEST(complex_write);
|
|
RUN_TEST(weird_keys);
|
|
RUN_TEST(weird_values);
|
|
RUN_TEST(fill_it_up);
|
|
|
|
test_print_result();
|
|
}
|