mbed-os/storage/filesystem/littlefsv2/tests/TESTS/COMMON/atomic_usage.cpp

694 lines
18 KiB
C++

/* mbed Microcontroller Library
* Copyright (c) 2017-2020 ARM Limited
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This file contains code which performs various atomic operations using
* littlefs. It is intended for use in tests and test applications to
* validate that the defined behavior below is being met.
*
* # Defined behavior
* - A file rename is atomic (Note - rename can be used to replace a file)
* - Atomic file rename tested by setup/perform/check_file_rename
* - Atomic file replace tested by setup/perform/check_file_rename_replace
* - A directory rename is atomic (Note - rename can be used to replace an empty directory)
* - Tested by setup/perform/check_directory_rename
* - Directory create is atomic
* - Directory delete is atomic
* - File create is atomic
* - File delete is atomic
* - File contents are atomically written on close
* - Tested by setup/perform/check_file_change_contents
*/
#include "mbed.h"
#include "greentea-client/test_env.h"
#include "unity.h"
#include "utest.h"
#include <stdlib.h>
#include <errno.h>
#include "ObservingBlockDevice.h"
#include "ExhaustibleBlockDevice.h"
#include "FileSystem.h"
#include "atomic_usage.h"
// test configuration
#ifndef MBED_TEST_FILESYSTEM
#define MBED_TEST_FILESYSTEM LittleFileSystem2
#endif
#ifndef MBED_TEST_FILESYSTEM_DECL
#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs")
#endif
// declarations
#define STRINGIZE(x) STRINGIZE2(x)
#define STRINGIZE2(x) #x
#define INCLUDE(x) STRINGIZE(x.h)
#include INCLUDE(MBED_TEST_FILESYSTEM)
#define DEBUG(...)
#define DEBUG_CHECK(...)
#define BUFFER_SIZE 64
// Version is written to a file and is used
// to determine if a reformat is required
#define ATOMIC_USAGE_VERSION 1
#define ARRAY_LENGTH(array) (sizeof(array) / sizeof(array[0]))
#define TEST_ASSERT_OR_EXIT(condition) \
TEST_ASSERT(condition); if (!(condition)) {error("Assert failed");}
#define TEST_ASSERT_EQUAL_OR_EXIT(expected, actual) \
TEST_ASSERT_EQUAL(expected, actual); if ((int64_t)(expected) != (int64_t)(actual)) {error("Assert failed");}
using namespace utest::v1;
typedef void (*test_function_t)(FileSystem *fs);
typedef bool (*test_function_bool_t)(FileSystem *fs);
struct TestEntry {
const char *name;
test_function_t setup;
test_function_bool_t perform;
test_function_t check;
};
/**
* Write data to the file while checking for error conditions
*
* @param file File to write to
* @param data Data to write
* @param size Size of data to write
* @return true if flash has been exhausted, false otherwise
*/
static bool file_write(File *file, uint8_t *data, uint32_t size)
{
int res = file->write(data, size);
if (-ENOSPC == res) {
return true;
}
TEST_ASSERT_EQUAL_OR_EXIT(size, res);
return false;
}
/**
* Write padding data of the given size
*
* @param file Pointer to the file to write to
* @param padding Value to pad
* @param size Size to pad
* @return true if flash has been exhausted, false otherwise
*/
static bool file_pad(File *file, char padding, uint32_t size)
{
uint8_t buf[BUFFER_SIZE];
memset(buf, padding, sizeof(buf));
while (size > 0) {
uint32_t write_size = sizeof(buf) <= size ? sizeof(buf) : size;
if (file_write(file, buf, write_size)) {
return true;
}
size -= write_size;
}
return false;
}
/*
* Similar to fscanf but uses and mbed file
*
* @param file File to scan from
* @param format Format string of values to read
* @return the number of arguments read
*/
static int file_scanf(File *file, const char *format, ...)
{
uint8_t buf[BUFFER_SIZE];
va_list args;
memset(buf, 0, sizeof(buf));
int res = file->read(buf, sizeof(buf) - 1);
TEST_ASSERT_OR_EXIT(res >= 0);
va_start(args, format);
int count = vsscanf((char *)buf, format, args);
va_end(args);
TEST_ASSERT_OR_EXIT(count >= 0);
return count;
}
/*
* Similar to fprintf but uses and mbed file
*
* @param file File to print to
* @param format Format string of values to write
* @return size written to file or -1 on out of space
*/
static int file_printf(File *file, const char *format, ...)
{
uint8_t buf[BUFFER_SIZE];
va_list args;
va_start(args, format);
int size = vsprintf((char *)buf, format, args);
va_end(args);
TEST_ASSERT_OR_EXIT((size >= 0) && (size <= (int)sizeof(buf)));
if (file_write(file, buf, size)) {
return -1;
}
return size;
}
static const char FILE_RENAME_A[] = "file_to_rename_a.txt";
static const char FILE_RENAME_B[] = "file_to_rename_b.txt";
static const char FILE_RENAME_CONTENTS[] = "Test contents for the file to be renamed";
static const int FILE_RENAME_LEN = strlen(FILE_RENAME_CONTENTS);
/**
* Setup for the file rename test
*
* Create file FILE_RENAME_A with contents FILE_RENAME_CONTENTS.
*/
static void setup_file_rename(FileSystem *fs)
{
DEBUG("setup_file_rename()\n");
File file;
int res = file.open(fs, FILE_RENAME_A, O_WRONLY | O_CREAT);
DEBUG(" open result %i\n", res);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
res = file.write(FILE_RENAME_CONTENTS, FILE_RENAME_LEN);
DEBUG(" write result %i\n", res);
TEST_ASSERT_EQUAL_OR_EXIT(FILE_RENAME_LEN, res);
}
/**
* Change the file name to either FILE_RENAME_A or FILE_RENAME_B
*/
static bool perform_file_rename(FileSystem *fs)
{
DEBUG("perform_file_rename()\n");
struct stat st;
int res = fs->stat(FILE_RENAME_A, &st);
const char *src = (res == 0) ? FILE_RENAME_A : FILE_RENAME_B;
const char *dst = (res == 0) ? FILE_RENAME_B : FILE_RENAME_A;
DEBUG(" stat result %i\n", res);
TEST_ASSERT_OR_EXIT((res == -ENOENT) || (res == 0));
DEBUG(" Renaming %s to %s\n", src, dst);
res = fs->rename(src, dst);
if (-ENOSPC == res) {
return true;
}
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
return false;
}
/**
* Check that the file rename is in a good state
*
* Check that there is only one file and that file contains the correct
* contents.
*
* Allowed states:
* - File FILE_RENAME_A exists with contents and FILE_RENAME_B does not
* - File FILE_RENAME_B exists with contents and FILE_RENAME_A does not
*
*/
static void check_file_rename(FileSystem *fs)
{
int files = 0;
int valids = 0;
const char *const filenames[] = {FILE_RENAME_A, FILE_RENAME_B};
for (int i = 0; i < 2; i++) {
File file;
if (file.open(fs, filenames[i], O_RDONLY) == 0) {
uint8_t buf[BUFFER_SIZE];
files++;
memset(buf, 0, sizeof(buf));
int res = file.read(buf, FILE_RENAME_LEN);
if (res != FILE_RENAME_LEN) {
break;
}
if (memcmp(buf, FILE_RENAME_CONTENTS, FILE_RENAME_LEN) != 0) {
break;
}
valids++;
}
}
TEST_ASSERT_EQUAL_OR_EXIT(1, files);
TEST_ASSERT_EQUAL_OR_EXIT(1, valids);
}
static const char FILE_RENAME_REPLACE[] = "rename_replace_file.txt";
static const char FILE_RENAME_REPLACE_NEW[] = "new_rename_replace_file.txt";
static const char FILE_RENAME_REPLACE_FMT[] = "file replace count: %lu\n";
/**
* Create the file FILE_RENAME_REPLACE with initial contents
*
* Create an write an initial count of 0 to the file.
*/
static void setup_file_rename_replace(FileSystem *fs)
{
DEBUG("setup_file_rename_replace()\n");
File file;
// Write out initial count
int res = file.open(fs, FILE_RENAME_REPLACE, O_WRONLY | O_CREAT);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
uint32_t count = 0;
uint8_t buf[BUFFER_SIZE];
memset(buf, 0, sizeof(buf));
const int length = sprintf((char *)buf, FILE_RENAME_REPLACE_FMT, count);
TEST_ASSERT_OR_EXIT(length > 0);
res = file.write(buf, length);
DEBUG(" write result %i\n", res);
TEST_ASSERT_EQUAL_OR_EXIT(length, res);
}
/**
* Atomically increment the count in FILE_RENAME_REPLACE using a rename
*/
bool perform_file_rename_replace(FileSystem *fs)
{
DEBUG("perform_file_rename_replace()\n");
File file;
// Read in previous count
int res = file.open(fs, FILE_RENAME_REPLACE, O_RDONLY);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
uint64_t count;
int args_read = file_scanf(&file, FILE_RENAME_REPLACE_FMT, &count);
TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);
res = file.close();
if (-ENOSPC == res) {
return true;
}
TEST_ASSERT_EQUAL(0, res);
// Write out new count
count++;
res = file.open(fs, FILE_RENAME_REPLACE_NEW, O_WRONLY | O_CREAT);
if (-ENOSPC == res) {
return true;
}
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
if (file_printf(&file, FILE_RENAME_REPLACE_FMT, count) <= 0) {
return true;
}
res = file.close();
if (-ENOSPC == res) {
return true;
}
TEST_ASSERT_EQUAL(0, res);
// Rename file
res = fs->rename(FILE_RENAME_REPLACE_NEW, FILE_RENAME_REPLACE);
if (-ENOSPC == res) {
return true;
}
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
DEBUG(" count %llu -> %llu\n", count - 1, count);
return false;
}
/**
* Check that FILE_RENAME_REPLACE always has a valid count
*
* Allowed states:
* - FILE_RENAME_REPLACE exists with valid contents
*/
static void check_file_rename_replace(FileSystem *fs)
{
DEBUG_CHECK("check_file_rename_replace()\n");
File file;
// Read in previous count
int res = file.open(fs, FILE_RENAME_REPLACE, O_RDONLY);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
uint64_t count;
int args_read = file_scanf(&file, FILE_RENAME_REPLACE_FMT, &count);
TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);
DEBUG_CHECK(" count %llu\n", count);
}
static const char DIRECTORY_RENAME_A[] = "dir_a";
static const char DIRECTORY_RENAME_B[] = "dir_b";
/**
* Create DIRECTORY_RENAME_A with initial contents
*/
static void setup_directory_rename(FileSystem *fs)
{
DEBUG("setup_directory_rename()\n");
int res = fs->mkdir(DIRECTORY_RENAME_A, 0777);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
}
/*
* Change the directory name from either DIRECTORY_RENAME_A or DIRECTORY_RENAME_B to the other
*/
static bool perform_directory_rename(FileSystem *fs)
{
DEBUG("perform_directory_rename()\n");
struct stat st;
int res = fs->stat(DIRECTORY_RENAME_A, &st);
const char *src = (res == 0) ? DIRECTORY_RENAME_A : DIRECTORY_RENAME_B;
const char *dst = (res == 0) ? DIRECTORY_RENAME_B : DIRECTORY_RENAME_A;
DEBUG(" stat result %i\n", res);
TEST_ASSERT_OR_EXIT((res == -ENOENT) || (res == 0));
DEBUG(" Renaming %s to %s\n", src, dst);
res = fs->rename(src, dst);
if (-ENOSPC == res) {
return true;
}
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
return false;
}
/*
* Change the directory name from either DIRECTORY_RENAME_A or DIRECTORY_RENAME_B to the other
*
* Allowed states:
* - DIRECTORY_RENAME_A exists with valid contents and DIRECTORY_RENAME_B does not exist
* - DIRECTORY_RENAME_B exists with valid contents and DIRECTORY_RENAME_A does not exist
*/
static void check_directory_rename(FileSystem *fs)
{
DEBUG_CHECK("check_directory_rename()\n");
static const char *directory_names[] = {
DIRECTORY_RENAME_A,
DIRECTORY_RENAME_B
};
size_t directories = 0;
for (size_t i = 0; i < ARRAY_LENGTH(directory_names); i++) {
Dir dir;
int res = dir.open(fs, directory_names[i]);
TEST_ASSERT_OR_EXIT((res == -ENOENT) || (res == 0));
if (res == 0) {
directories++;
}
}
TEST_ASSERT_EQUAL_OR_EXIT(1, directories);
}
static const char CHANGE_CONTENTS_NAME[] = "file_changing_contents.txt";
static const char CHANGE_CONTENTS_FILL = ' ';
static const uint32_t BLOCK_SIZE = 512;
/**
* Create file CHANGE_CONTENTS_NAME with initial contents
*
* File contains three blocks of data each which start
* with a count.
*/
static void setup_file_change_contents(FileSystem *fs)
{
DEBUG("setup_file_change_contents()\n");
File file;
int res = file.open(fs, CHANGE_CONTENTS_NAME, O_WRONLY | O_CREAT);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
for (int count = 1; count <= 3; count++) {
int size = file_printf(&file, "%lu\n", count);
TEST_ASSERT_OR_EXIT(size >= 0);
bool dead = file_pad(&file, CHANGE_CONTENTS_FILL, BLOCK_SIZE - size);
TEST_ASSERT_EQUAL_OR_EXIT(false, dead);
}
}
/**
* Atomically increment the counts in the file CHANGE_CONTENTS_NAME
*
* Read in the current counts, increment them and then write them
* back in non-sequential order.
*/
static bool perform_file_change_contents(FileSystem *fs)
{
DEBUG("perform_file_change_contents()\n");
File file;
int res = file.open(fs, CHANGE_CONTENTS_NAME, O_RDWR);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
// Read in values
uint32_t values[3];
for (int i = 0; i < 3; i++) {
file.seek(i * BLOCK_SIZE);
int args_read = file_scanf(&file, "%lu\n", &values[i]);
TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);
}
// Increment values
for (int i = 0; i < 3; i++) {
values[i]++;
}
// Write values out of order
int i;
i = 0;
file.seek(i * BLOCK_SIZE);
if (file_printf(&file, "%lu\n", values[i]) <= 0) {
return true;
}
DEBUG(" value[%i]: %lu -> %lu\n", i, values[i] - 1, values[i]);
i = 2;
file.seek(i * BLOCK_SIZE);
if (file_printf(&file, "%lu\n", values[i]) <= 0) {
return true;
}
DEBUG(" value[%i]: %lu -> %lu\n", i, values[i] - 1, values[i]);
i = 1;
file.seek(i * BLOCK_SIZE);
if (file_printf(&file, "%lu\n", values[i]) <= 0) {
return true;
}
DEBUG(" value[%i]: %lu -> %lu\n", i, values[i] - 1, values[i]);
res = file.close();
if (-ENOSPC == res) {
return true;
}
TEST_ASSERT_EQUAL(0, res);
return false;
}
/*
* Change the directory name from either DIRECTORY_RENAME_A or DIRECTORY_RENAME_B to the other
*
* Allowed states:
* - CHANGE_CONTENTS_NAME exists and contains 3 counts which are in order
*/
static void check_file_change_contents(FileSystem *fs)
{
DEBUG_CHECK("check_file_change_contents()\n");
File file;
int res = file.open(fs, CHANGE_CONTENTS_NAME, O_RDONLY);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
// Read in values
uint32_t values[3];
for (int i = 0; i < 3; i++) {
file.seek(i * BLOCK_SIZE);
int args_read = file_scanf(&file, "%lu\n", &values[i]);
TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);
DEBUG_CHECK(" value[%i]: %lu\n", i, values[i]);
}
TEST_ASSERT_EQUAL_OR_EXIT(values[0] + 1, values[1]);
TEST_ASSERT_EQUAL_OR_EXIT(values[1] + 1, values[2]);
}
static const TestEntry atomic_test_entries[] = {
{"File rename", setup_file_rename, perform_file_rename, check_file_rename},
{"File rename replace", setup_file_rename_replace, perform_file_rename_replace, check_file_rename_replace},
{"Directory rename", setup_directory_rename, perform_directory_rename, check_directory_rename},
{"File change contents", setup_file_change_contents, perform_file_change_contents, check_file_change_contents},
};
static const char FILE_SETUP_COMPLETE[] = "setup_complete.txt";
static const char FILE_SETUP_COMPLETE_FMT[] = "Test version: %lu\n";
static bool format_required(BlockDevice *bd)
{
MBED_TEST_FILESYSTEM_DECL;
if (fs.mount(bd) != 0) {
return true;
}
// Check if setup complete file exists
File file;
int res = file.open(&fs, FILE_SETUP_COMPLETE, O_RDONLY);
if (res != 0) {
return true;
}
// Read contents of setup complete file
uint8_t buf[BUFFER_SIZE];
memset(buf, 0, sizeof(buf));
int size_read = file.read(buf, sizeof(buf) - 1);
if (size_read <= 0) {
return true;
}
// Get the test version
uint32_t version = 0;
res = sscanf((char *)buf, FILE_SETUP_COMPLETE_FMT, &version);
if (res != 1) {
return true;
}
if (ATOMIC_USAGE_VERSION != version) {
return true;
}
// Setup file exists and is the correct version
return false;
}
static void format(BlockDevice *bd)
{
MBED_TEST_FILESYSTEM_DECL;
int res = fs.format(bd);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
res = fs.mount(bd);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
for (size_t i = 0; i < ARRAY_LENGTH(atomic_test_entries); i++) {
atomic_test_entries[i].setup(&fs);
}
File file;
res = file.open(&fs, FILE_SETUP_COMPLETE, O_CREAT | O_WRONLY);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
int size = file_printf(&file, FILE_SETUP_COMPLETE_FMT, (uint32_t)ATOMIC_USAGE_VERSION);
TEST_ASSERT_OR_EXIT(size >= 0);
}
static int64_t get_cycle_count(FileSystem *fs)
{
File file;
int res = file.open(fs, FILE_RENAME_REPLACE, O_RDONLY);
TEST_ASSERT_EQUAL_OR_EXIT(0, res);
uint64_t count = 0;
int args_read = file_scanf(&file, FILE_RENAME_REPLACE_FMT, &count);
TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);
file.close();
return (int64_t)count;
}
bool setup_atomic_operations(BlockDevice *bd, bool force_rebuild)
{
if (force_rebuild || format_required(bd)) {
format(bd);
TEST_ASSERT_EQUAL_OR_EXIT(false, format_required(bd));
return true;
}
return false;
}
int64_t perform_atomic_operations(BlockDevice *bd)
{
MBED_TEST_FILESYSTEM_DECL;
bool out_of_space = false;
fs.mount(bd);
for (size_t i = 0; i < ARRAY_LENGTH(atomic_test_entries); i++) {
out_of_space |= atomic_test_entries[i].perform(&fs);
}
int64_t cycle_count = get_cycle_count(&fs);
fs.unmount();
if (out_of_space) {
return -1;
} else {
return cycle_count;
}
}
void check_atomic_operations(BlockDevice *bd)
{
MBED_TEST_FILESYSTEM_DECL;
fs.mount(bd);
for (size_t i = 0; i < ARRAY_LENGTH(atomic_test_entries); i++) {
atomic_test_entries[i].check(&fs);
}
fs.unmount();
}