mirror of
https://github.com/eledio-devices/thirdparty-littlefs.git
synced 2025-11-01 16:14:13 +01:00
Compare commits
17 Commits
bf_lfs_fil
...
devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c7e232086 | ||
|
|
c676bcee4c | ||
|
|
03f088b92c | ||
|
|
99f58139cb | ||
|
|
5801169348 | ||
|
|
2d6f4ead13 | ||
|
|
3d1b89b41a | ||
|
|
45cefb825d | ||
|
|
bbb9e3873e | ||
|
|
c6d3c48939 | ||
|
|
1363c9f9d4 | ||
|
|
5bc682a0d4 | ||
|
|
1877c40aac | ||
|
|
e29e7aeefa | ||
|
|
4977fa0c0e | ||
|
|
fdda3b4aa2 | ||
|
|
fb2c311bb4 |
14
SPEC.md
14
SPEC.md
@@ -233,19 +233,19 @@ Metadata tag fields:
|
|||||||
into a 3-bit abstract type and an 8-bit chunk field. Note that the value
|
into a 3-bit abstract type and an 8-bit chunk field. Note that the value
|
||||||
`0x000` is invalid and not assigned a type.
|
`0x000` is invalid and not assigned a type.
|
||||||
|
|
||||||
3. **Type1 (3-bits)** - Abstract type of the tag. Groups the tags into
|
1. **Type1 (3-bits)** - Abstract type of the tag. Groups the tags into
|
||||||
8 categories that facilitate bitmasked lookups.
|
8 categories that facilitate bitmasked lookups.
|
||||||
|
|
||||||
4. **Chunk (8-bits)** - Chunk field used for various purposes by the different
|
2. **Chunk (8-bits)** - Chunk field used for various purposes by the different
|
||||||
abstract types. type1+chunk+id form a unique identifier for each tag in the
|
abstract types. type1+chunk+id form a unique identifier for each tag in the
|
||||||
metadata block.
|
metadata block.
|
||||||
|
|
||||||
5. **Id (10-bits)** - File id associated with the tag. Each file in a metadata
|
3. **Id (10-bits)** - File id associated with the tag. Each file in a metadata
|
||||||
block gets a unique id which is used to associate tags with that file. The
|
block gets a unique id which is used to associate tags with that file. The
|
||||||
special value `0x3ff` is used for any tags that are not associated with a
|
special value `0x3ff` is used for any tags that are not associated with a
|
||||||
file, such as directory and global metadata.
|
file, such as directory and global metadata.
|
||||||
|
|
||||||
6. **Length (10-bits)** - Length of the data in bytes. The special value
|
4. **Length (10-bits)** - Length of the data in bytes. The special value
|
||||||
`0x3ff` indicates that this tag has been deleted.
|
`0x3ff` indicates that this tag has been deleted.
|
||||||
|
|
||||||
## Metadata types
|
## Metadata types
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ int lfs_filebd_read(const struct lfs_config *cfg, lfs_block_t block,
|
|||||||
LFS_ASSERT(size % cfg->read_size == 0);
|
LFS_ASSERT(size % cfg->read_size == 0);
|
||||||
LFS_ASSERT(block < cfg->block_count);
|
LFS_ASSERT(block < cfg->block_count);
|
||||||
|
|
||||||
// zero for reproducability (in case file is truncated)
|
// zero for reproducibility (in case file is truncated)
|
||||||
if (bd->cfg->erase_value != -1) {
|
if (bd->cfg->erase_value != -1) {
|
||||||
memset(buffer, bd->cfg->erase_value, size);
|
memset(buffer, bd->cfg->erase_value, size);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ int lfs_rambd_createcfg(const struct lfs_config *cfg,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// zero for reproducability?
|
// zero for reproducibility?
|
||||||
if (bd->cfg->erase_value != -1) {
|
if (bd->cfg->erase_value != -1) {
|
||||||
memset(bd->buffer, bd->cfg->erase_value,
|
memset(bd->buffer, bd->cfg->erase_value,
|
||||||
cfg->block_size * cfg->block_count);
|
cfg->block_size * cfg->block_count);
|
||||||
|
} else {
|
||||||
|
memset(bd->buffer, 0, cfg->block_size * cfg->block_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
LFS_RAMBD_TRACE("lfs_rambd_createcfg -> %d", 0);
|
LFS_RAMBD_TRACE("lfs_rambd_createcfg -> %d", 0);
|
||||||
|
|||||||
30
lfs.c
30
lfs.c
@@ -11,6 +11,7 @@
|
|||||||
#define LFS_BLOCK_INLINE ((lfs_block_t)-2)
|
#define LFS_BLOCK_INLINE ((lfs_block_t)-2)
|
||||||
|
|
||||||
/// Caching block device operations ///
|
/// Caching block device operations ///
|
||||||
|
|
||||||
static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) {
|
static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) {
|
||||||
// do not zero, cheaper if cache is readonly or only going to be
|
// do not zero, cheaper if cache is readonly or only going to be
|
||||||
// written with identical data (during relocates)
|
// written with identical data (during relocates)
|
||||||
@@ -268,22 +269,26 @@ static inline int lfs_pair_cmp(
|
|||||||
paira[0] == pairb[1] || paira[1] == pairb[0]);
|
paira[0] == pairb[1] || paira[1] == pairb[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef LFS_READONLY
|
||||||
static inline bool lfs_pair_sync(
|
static inline bool lfs_pair_sync(
|
||||||
const lfs_block_t paira[2],
|
const lfs_block_t paira[2],
|
||||||
const lfs_block_t pairb[2]) {
|
const lfs_block_t pairb[2]) {
|
||||||
return (paira[0] == pairb[0] && paira[1] == pairb[1]) ||
|
return (paira[0] == pairb[0] && paira[1] == pairb[1]) ||
|
||||||
(paira[0] == pairb[1] && paira[1] == pairb[0]);
|
(paira[0] == pairb[1] && paira[1] == pairb[0]);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static inline void lfs_pair_fromle32(lfs_block_t pair[2]) {
|
static inline void lfs_pair_fromle32(lfs_block_t pair[2]) {
|
||||||
pair[0] = lfs_fromle32(pair[0]);
|
pair[0] = lfs_fromle32(pair[0]);
|
||||||
pair[1] = lfs_fromle32(pair[1]);
|
pair[1] = lfs_fromle32(pair[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef LFS_READONLY
|
||||||
static inline void lfs_pair_tole32(lfs_block_t pair[2]) {
|
static inline void lfs_pair_tole32(lfs_block_t pair[2]) {
|
||||||
pair[0] = lfs_tole32(pair[0]);
|
pair[0] = lfs_tole32(pair[0]);
|
||||||
pair[1] = lfs_tole32(pair[1]);
|
pair[1] = lfs_tole32(pair[1]);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// operations on 32-bit entry tags
|
// operations on 32-bit entry tags
|
||||||
typedef uint32_t lfs_tag_t;
|
typedef uint32_t lfs_tag_t;
|
||||||
@@ -365,6 +370,7 @@ static inline bool lfs_gstate_iszero(const lfs_gstate_t *a) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef LFS_READONLY
|
||||||
static inline bool lfs_gstate_hasorphans(const lfs_gstate_t *a) {
|
static inline bool lfs_gstate_hasorphans(const lfs_gstate_t *a) {
|
||||||
return lfs_tag_size(a->tag);
|
return lfs_tag_size(a->tag);
|
||||||
}
|
}
|
||||||
@@ -376,6 +382,7 @@ static inline uint8_t lfs_gstate_getorphans(const lfs_gstate_t *a) {
|
|||||||
static inline bool lfs_gstate_hasmove(const lfs_gstate_t *a) {
|
static inline bool lfs_gstate_hasmove(const lfs_gstate_t *a) {
|
||||||
return lfs_tag_type1(a->tag);
|
return lfs_tag_type1(a->tag);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static inline bool lfs_gstate_hasmovehere(const lfs_gstate_t *a,
|
static inline bool lfs_gstate_hasmovehere(const lfs_gstate_t *a,
|
||||||
const lfs_block_t *pair) {
|
const lfs_block_t *pair) {
|
||||||
@@ -388,11 +395,13 @@ static inline void lfs_gstate_fromle32(lfs_gstate_t *a) {
|
|||||||
a->pair[1] = lfs_fromle32(a->pair[1]);
|
a->pair[1] = lfs_fromle32(a->pair[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef LFS_READONLY
|
||||||
static inline void lfs_gstate_tole32(lfs_gstate_t *a) {
|
static inline void lfs_gstate_tole32(lfs_gstate_t *a) {
|
||||||
a->tag = lfs_tole32(a->tag);
|
a->tag = lfs_tole32(a->tag);
|
||||||
a->pair[0] = lfs_tole32(a->pair[0]);
|
a->pair[0] = lfs_tole32(a->pair[0]);
|
||||||
a->pair[1] = lfs_tole32(a->pair[1]);
|
a->pair[1] = lfs_tole32(a->pair[1]);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// other endianness operations
|
// other endianness operations
|
||||||
static void lfs_ctz_fromle32(struct lfs_ctz *ctz) {
|
static void lfs_ctz_fromle32(struct lfs_ctz *ctz) {
|
||||||
@@ -416,6 +425,7 @@ static inline void lfs_superblock_fromle32(lfs_superblock_t *superblock) {
|
|||||||
superblock->attr_max = lfs_fromle32(superblock->attr_max);
|
superblock->attr_max = lfs_fromle32(superblock->attr_max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef LFS_READONLY
|
||||||
static inline void lfs_superblock_tole32(lfs_superblock_t *superblock) {
|
static inline void lfs_superblock_tole32(lfs_superblock_t *superblock) {
|
||||||
superblock->version = lfs_tole32(superblock->version);
|
superblock->version = lfs_tole32(superblock->version);
|
||||||
superblock->block_size = lfs_tole32(superblock->block_size);
|
superblock->block_size = lfs_tole32(superblock->block_size);
|
||||||
@@ -424,6 +434,7 @@ static inline void lfs_superblock_tole32(lfs_superblock_t *superblock) {
|
|||||||
superblock->file_max = lfs_tole32(superblock->file_max);
|
superblock->file_max = lfs_tole32(superblock->file_max);
|
||||||
superblock->attr_max = lfs_tole32(superblock->attr_max);
|
superblock->attr_max = lfs_tole32(superblock->attr_max);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef LFS_NO_ASSERT
|
#ifndef LFS_NO_ASSERT
|
||||||
static bool lfs_mlist_isopen(struct lfs_mlist *head,
|
static bool lfs_mlist_isopen(struct lfs_mlist *head,
|
||||||
@@ -1449,7 +1460,7 @@ static int lfs_dir_alloc(lfs_t *lfs, lfs_mdir_t *dir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// zero for reproducability in case initial block is unreadable
|
// zero for reproducibility in case initial block is unreadable
|
||||||
dir->rev = 0;
|
dir->rev = 0;
|
||||||
|
|
||||||
// rather than clobbering one of the blocks we just pretend
|
// rather than clobbering one of the blocks we just pretend
|
||||||
@@ -1509,7 +1520,6 @@ static int lfs_dir_split(lfs_t *lfs,
|
|||||||
lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount,
|
lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount,
|
||||||
lfs_mdir_t *source, uint16_t split, uint16_t end) {
|
lfs_mdir_t *source, uint16_t split, uint16_t end) {
|
||||||
// create tail directory
|
// create tail directory
|
||||||
lfs_alloc_ack(lfs);
|
|
||||||
lfs_mdir_t tail;
|
lfs_mdir_t tail;
|
||||||
int err = lfs_dir_alloc(lfs, &tail);
|
int err = lfs_dir_alloc(lfs, &tail);
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -2730,19 +2740,15 @@ static int lfs_file_outline(lfs_t *lfs, lfs_file_t *file) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static void lfs_file_invalidate_reading_flag(lfs_t *lfs, lfs_file_t *file) {
|
static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) {
|
||||||
if (file->flags & LFS_F_READING) {
|
if (file->flags & LFS_F_READING) {
|
||||||
if (!(file->flags & LFS_F_INLINE)) {
|
if (!(file->flags & LFS_F_INLINE)) {
|
||||||
lfs_cache_drop(lfs, &file->cache);
|
lfs_cache_drop(lfs, &file->cache);
|
||||||
}
|
}
|
||||||
file->flags &= ~LFS_F_READING;
|
file->flags &= ~LFS_F_READING;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef LFS_READONLY
|
#ifndef LFS_READONLY
|
||||||
static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) {
|
|
||||||
lfs_file_invalidate_reading_flag(lfs, file);
|
|
||||||
|
|
||||||
if (file->flags & LFS_F_WRITING) {
|
if (file->flags & LFS_F_WRITING) {
|
||||||
lfs_off_t pos = file->pos;
|
lfs_off_t pos = file->pos;
|
||||||
|
|
||||||
@@ -2809,10 +2815,10 @@ relocate:
|
|||||||
|
|
||||||
file->pos = pos;
|
file->pos = pos;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef LFS_READONLY
|
#ifndef LFS_READONLY
|
||||||
static int lfs_file_rawsync(lfs_t *lfs, lfs_file_t *file) {
|
static int lfs_file_rawsync(lfs_t *lfs, lfs_file_t *file) {
|
||||||
@@ -3085,17 +3091,11 @@ static lfs_soff_t lfs_file_rawseek(lfs_t *lfs, lfs_file_t *file,
|
|||||||
return npos;
|
return npos;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef LFS_READONLY
|
|
||||||
// write out everything beforehand, may be noop if rdonly
|
// write out everything beforehand, may be noop if rdonly
|
||||||
int err = lfs_file_flush(lfs, file);
|
int err = lfs_file_flush(lfs, file);
|
||||||
if (err) {
|
if (err) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
// Seek doesn't update cache parameters properly.
|
|
||||||
// It has to be invalidated otherwise next read will return incorrect values.
|
|
||||||
lfs_file_invalidate_reading_flag(lfs,file);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// update pos
|
// update pos
|
||||||
file->pos = npos;
|
file->pos = npos;
|
||||||
@@ -4082,7 +4082,7 @@ static int lfs_fs_relocate(lfs_t *lfs,
|
|||||||
lfs_fs_prepmove(lfs, 0x3ff, NULL);
|
lfs_fs_prepmove(lfs, 0x3ff, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace bad pair, either we clean up desync, or no desync occured
|
// replace bad pair, either we clean up desync, or no desync occurred
|
||||||
lfs_pair_tole32(newpair);
|
lfs_pair_tole32(newpair);
|
||||||
err = lfs_dir_commit(lfs, &parent, LFS_MKATTRS(
|
err = lfs_dir_commit(lfs, &parent, LFS_MKATTRS(
|
||||||
{LFS_MKTAG_IF(moveid != 0x3ff,
|
{LFS_MKTAG_IF(moveid != 0x3ff,
|
||||||
|
|||||||
36
lfs.h
36
lfs.h
@@ -159,49 +159,49 @@ struct lfs_config {
|
|||||||
// information to the block device operations
|
// information to the block device operations
|
||||||
void *context;
|
void *context;
|
||||||
|
|
||||||
// Read a region in a block. Negative error codes are propogated
|
// Read a region in a block. Negative error codes are propagated
|
||||||
// to the user.
|
// to the user.
|
||||||
int (*read)(const struct lfs_config *c, lfs_block_t block,
|
int (*read)(const struct lfs_config *c, lfs_block_t block,
|
||||||
lfs_off_t off, void *buffer, lfs_size_t size);
|
lfs_off_t off, void *buffer, lfs_size_t size);
|
||||||
|
|
||||||
// Program a region in a block. The block must have previously
|
// Program a region in a block. The block must have previously
|
||||||
// been erased. Negative error codes are propogated to the user.
|
// been erased. Negative error codes are propagated to the user.
|
||||||
// May return LFS_ERR_CORRUPT if the block should be considered bad.
|
// May return LFS_ERR_CORRUPT if the block should be considered bad.
|
||||||
int (*prog)(const struct lfs_config *c, lfs_block_t block,
|
int (*prog)(const struct lfs_config *c, lfs_block_t block,
|
||||||
lfs_off_t off, const void *buffer, lfs_size_t size);
|
lfs_off_t off, const void *buffer, lfs_size_t size);
|
||||||
|
|
||||||
// Erase a block. A block must be erased before being programmed.
|
// Erase a block. A block must be erased before being programmed.
|
||||||
// The state of an erased block is undefined. Negative error codes
|
// The state of an erased block is undefined. Negative error codes
|
||||||
// are propogated to the user.
|
// are propagated to the user.
|
||||||
// May return LFS_ERR_CORRUPT if the block should be considered bad.
|
// May return LFS_ERR_CORRUPT if the block should be considered bad.
|
||||||
int (*erase)(const struct lfs_config *c, lfs_block_t block);
|
int (*erase)(const struct lfs_config *c, lfs_block_t block);
|
||||||
|
|
||||||
// Sync the state of the underlying block device. Negative error codes
|
// Sync the state of the underlying block device. Negative error codes
|
||||||
// are propogated to the user.
|
// are propagated to the user.
|
||||||
int (*sync)(const struct lfs_config *c);
|
int (*sync)(const struct lfs_config *c);
|
||||||
|
|
||||||
#ifdef LFS_THREADSAFE
|
#ifdef LFS_THREADSAFE
|
||||||
// Lock the underlying block device. Negative error codes
|
// Lock the underlying block device. Negative error codes
|
||||||
// are propogated to the user.
|
// are propagated to the user.
|
||||||
int (*lock)(const struct lfs_config *c);
|
int (*lock)(const struct lfs_config *c);
|
||||||
|
|
||||||
// Unlock the underlying block device. Negative error codes
|
// Unlock the underlying block device. Negative error codes
|
||||||
// are propogated to the user.
|
// are propagated to the user.
|
||||||
int (*unlock)(const struct lfs_config *c);
|
int (*unlock)(const struct lfs_config *c);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Minimum size of a block read. All read operations will be a
|
// Minimum size of a block read in bytes. All read operations will be a
|
||||||
// multiple of this value.
|
// multiple of this value.
|
||||||
lfs_size_t read_size;
|
lfs_size_t read_size;
|
||||||
|
|
||||||
// Minimum size of a block program. All program operations will be a
|
// Minimum size of a block program in bytes. All program operations will be
|
||||||
// multiple of this value.
|
// a multiple of this value.
|
||||||
lfs_size_t prog_size;
|
lfs_size_t prog_size;
|
||||||
|
|
||||||
// Size of an erasable block. This does not impact ram consumption and
|
// Size of an erasable block in bytes. This does not impact ram consumption
|
||||||
// may be larger than the physical erase size. However, non-inlined files
|
// and may be larger than the physical erase size. However, non-inlined
|
||||||
// take up at minimum one block. Must be a multiple of the read
|
// files take up at minimum one block. Must be a multiple of the read and
|
||||||
// and program sizes.
|
// program sizes.
|
||||||
lfs_size_t block_size;
|
lfs_size_t block_size;
|
||||||
|
|
||||||
// Number of erasable blocks on the device.
|
// Number of erasable blocks on the device.
|
||||||
@@ -215,11 +215,11 @@ struct lfs_config {
|
|||||||
// Set to -1 to disable block-level wear-leveling.
|
// Set to -1 to disable block-level wear-leveling.
|
||||||
int32_t block_cycles;
|
int32_t block_cycles;
|
||||||
|
|
||||||
// Size of block caches. Each cache buffers a portion of a block in RAM.
|
// Size of block caches in bytes. Each cache buffers a portion of a block in
|
||||||
// The littlefs needs a read cache, a program cache, and one additional
|
// RAM. The littlefs needs a read cache, a program cache, and one additional
|
||||||
// cache per file. Larger caches can improve performance by storing more
|
// cache per file. Larger caches can improve performance by storing more
|
||||||
// data and reducing the number of disk accesses. Must be a multiple of
|
// data and reducing the number of disk accesses. Must be a multiple of the
|
||||||
// the read and program sizes, and a factor of the block size.
|
// read and program sizes, and a factor of the block size.
|
||||||
lfs_size_t cache_size;
|
lfs_size_t cache_size;
|
||||||
|
|
||||||
// Size of the lookahead buffer in bytes. A larger lookahead buffer
|
// Size of the lookahead buffer in bytes. A larger lookahead buffer
|
||||||
@@ -485,7 +485,7 @@ int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info);
|
|||||||
// Returns the size of the attribute, or a negative error code on failure.
|
// Returns the size of the attribute, or a negative error code on failure.
|
||||||
// Note, the returned size is the size of the attribute on disk, irrespective
|
// Note, the returned size is the size of the attribute on disk, irrespective
|
||||||
// of the size of the buffer. This can be used to dynamically allocate a buffer
|
// of the size of the buffer. This can be used to dynamically allocate a buffer
|
||||||
// or check for existance.
|
// or check for existence.
|
||||||
lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path,
|
lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path,
|
||||||
uint8_t type, void *buffer, lfs_size_t size);
|
uint8_t type, void *buffer, lfs_size_t size);
|
||||||
|
|
||||||
|
|||||||
@@ -565,7 +565,7 @@ class TestSuite:
|
|||||||
path=self.path))
|
path=self.path))
|
||||||
mk.write('\n')
|
mk.write('\n')
|
||||||
|
|
||||||
# add truely global defines globally
|
# add truly global defines globally
|
||||||
for k, v in sorted(self.defines.items()):
|
for k, v in sorted(self.defines.items()):
|
||||||
mk.write('%s.test: override CFLAGS += -D%s=%r\n'
|
mk.write('%s.test: override CFLAGS += -D%s=%r\n'
|
||||||
% (self.path, k, v))
|
% (self.path, k, v))
|
||||||
@@ -656,7 +656,7 @@ def main(**args):
|
|||||||
for path in glob.glob(testpath):
|
for path in glob.glob(testpath):
|
||||||
suites.append(TestSuite(path, classes, defines, filter, **args))
|
suites.append(TestSuite(path, classes, defines, filter, **args))
|
||||||
|
|
||||||
# sort for reproducability
|
# sort for reproducibility
|
||||||
suites = sorted(suites)
|
suites = sorted(suites)
|
||||||
|
|
||||||
# generate permutations
|
# generate permutations
|
||||||
|
|||||||
Reference in New Issue
Block a user