mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-31 03:03:42 +00:00
172 lines
7.1 KiB
Python
172 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise the production small-table sorter and its platform fallback."""
|
|
from pathlib import Path
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
HARNESS = r'''
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <numeric>
|
|
#include <random>
|
|
#include <vector>
|
|
#include <helpers/ContactListOrder.h>
|
|
#include <helpers/SmallTableSort.h>
|
|
|
|
struct Contact {
|
|
uint8_t flags;
|
|
uint32_t timestamp;
|
|
};
|
|
static std::array<Contact, 358> contacts;
|
|
static size_t comparisons;
|
|
static int compareIndexes(const void* a, const void* b) {
|
|
++comparisons;
|
|
const Contact& left = contacts[*static_cast<const uint16_t*>(a)];
|
|
const Contact& right = contacts[*static_cast<const uint16_t*>(b)];
|
|
return mesh::compareContactListOrder(left.flags, left.timestamp,
|
|
right.flags, right.timestamp);
|
|
}
|
|
struct Advert {
|
|
uint8_t public_key[8];
|
|
char name[32];
|
|
uint32_t timestamp;
|
|
uint8_t path[64];
|
|
uint16_t identity;
|
|
};
|
|
static_assert(sizeof(uint16_t) == 2, "production compact contact index");
|
|
static_assert(sizeof(Advert) > 100, "exercise wide bytewise record swaps");
|
|
static int compareAdverts(const void* a, const void* b) {
|
|
++comparisons;
|
|
const auto left = static_cast<const Advert*>(a)->timestamp;
|
|
const auto right = static_cast<const Advert*>(b)->timestamp;
|
|
return left > right ? -1 : left < right ? 1 : 0;
|
|
}
|
|
// Defined in a second translation unit: both consumers must share the inline
|
|
// header's single STM32/nRF52 function body and compatible callback signature.
|
|
void sortElsewhere(void*, size_t, size_t, int (*)(const void*, const void*));
|
|
|
|
static void checkComparisonBound(size_t count) {
|
|
size_t height = 0;
|
|
for (size_t level = count; level > 1; level = (level + 1) / 2) ++height;
|
|
// Allow two comparisons per heap level and linear heap construction. This
|
|
// also admits the platform qsort fallback without fixing one comparison
|
|
// sequence or the unspecified tie order. Quadratic selection sort fails
|
|
// this bound on both 108- and 358-entry tables.
|
|
assert(comparisons <= count * (2 * height + 2));
|
|
}
|
|
|
|
static void checkIndexes(size_t count, unsigned order, std::mt19937& random) {
|
|
std::vector<uint16_t> indexes(count);
|
|
std::iota(indexes.begin(), indexes.end(), uint16_t(0));
|
|
std::sort(indexes.begin(), indexes.end(), [](uint16_t a, uint16_t b) {
|
|
return compareIndexes(&a, &b) < 0;
|
|
});
|
|
if (order == 1) std::reverse(indexes.begin(), indexes.end());
|
|
else if (order == 2) std::shuffle(indexes.begin(), indexes.end(), random);
|
|
comparisons = 0;
|
|
mesh::sortSmallTable(indexes.data(), count, sizeof(indexes[0]), compareIndexes);
|
|
checkComparisonBound(count);
|
|
std::vector<bool> seen(count);
|
|
for (size_t i = 0; i < count; ++i) {
|
|
assert(indexes[i] < count && !seen[indexes[i]]);
|
|
seen[indexes[i]] = true;
|
|
if (i) assert(compareIndexes(&indexes[i - 1], &indexes[i]) <= 0);
|
|
}
|
|
}
|
|
static void checkAdverts(size_t count, unsigned order, std::mt19937& random) {
|
|
std::vector<Advert> original(count), records(count);
|
|
for (size_t i = 0; i < count; ++i) {
|
|
// Include object padding in the preservation check. Repeated timestamps
|
|
// intentionally leave equal-key ordering unspecified, matching qsort.
|
|
std::memset(&original[i], static_cast<int>(i ^ 0xa5), sizeof(Advert));
|
|
original[i].identity = static_cast<uint16_t>(i);
|
|
original[i].timestamp = static_cast<uint32_t>((i * 7) % 5);
|
|
records[i] = original[i];
|
|
}
|
|
std::sort(records.begin(), records.end(), [](const Advert& a, const Advert& b) {
|
|
return compareAdverts(&a, &b) < 0;
|
|
});
|
|
if (order == 1) std::reverse(records.begin(), records.end());
|
|
else if (order == 2) std::shuffle(records.begin(), records.end(), random);
|
|
comparisons = 0;
|
|
sortElsewhere(records.data(), count, sizeof(Advert), compareAdverts);
|
|
checkComparisonBound(count);
|
|
std::vector<bool> seen(count);
|
|
for (size_t i = 0; i < count; ++i) {
|
|
const auto identity = records[i].identity;
|
|
assert(identity < count && !seen[identity]);
|
|
seen[identity] = true;
|
|
assert(std::memcmp(&records[i], &original[identity], sizeof(Advert)) == 0);
|
|
if (i) assert(compareAdverts(&records[i - 1], &records[i]) <= 0);
|
|
}
|
|
}
|
|
int main() {
|
|
std::mt19937 random(0x1a180001);
|
|
mesh::sortSmallTable(nullptr, 0, sizeof(uint16_t), compareIndexes);
|
|
uint16_t untouched = 0x1234;
|
|
mesh::sortSmallTable(&untouched, contacts.size(), 0, compareIndexes);
|
|
assert(untouched == 0x1234);
|
|
for (unsigned pattern = 0; pattern < 3; ++pattern) {
|
|
for (size_t i = 0; i < contacts.size(); ++i) {
|
|
contacts[i].flags = pattern == 0 ? 0 : static_cast<uint8_t>(i % 3 == 0);
|
|
contacts[i].timestamp = pattern == 2 ? 42 : static_cast<uint32_t>((i * 7919) % 31);
|
|
}
|
|
for (size_t count : {0U, 1U, 2U, 16U, 100U, 108U, 350U, 358U}) {
|
|
for (unsigned order = 0; order < 3; ++order) {
|
|
checkIndexes(count, order, random);
|
|
checkAdverts(count, order, random);
|
|
}
|
|
}
|
|
}
|
|
puts("PASS: compact indexes, wide records, duplicate keys, order and permutation");
|
|
}
|
|
'''
|
|
|
|
SECOND_TRANSLATION_UNIT = r'''
|
|
#include <helpers/SmallTableSort.h>
|
|
void sortElsewhere(void* items, size_t count, size_t width,
|
|
int (*compare)(const void*, const void*)) {
|
|
mesh::sortSmallTable(items, count, width, compare);
|
|
}
|
|
'''
|
|
|
|
|
|
class SmallTableSortTests(unittest.TestCase):
|
|
def test_sort_on_all_platforms(self):
|
|
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler, "a host C++17 compiler is required")
|
|
for platform in (None, "STM32_PLATFORM", "NRF52_PLATFORM", "ESP32_PLATFORM", "RP2040_PLATFORM"):
|
|
with self.subTest(platform=platform or "native"):
|
|
with tempfile.TemporaryDirectory(prefix="small-table-sort-") as directory:
|
|
work = Path(directory)
|
|
(work / "test.cpp").write_text(HARNESS)
|
|
(work / "second.cpp").write_text(SECOND_TRANSLATION_UNIT)
|
|
binary = work / "sort"
|
|
command = [compiler, "-std=c++17", "-O2", "-Wall", "-Wextra", "-Werror",
|
|
f"-I{ROOT / 'src'}", str(work / "test.cpp"),
|
|
str(work / "second.cpp"), "-o", str(binary)]
|
|
if platform:
|
|
command.insert(1, f"-D{platform}=1")
|
|
if sys.platform.startswith("linux"):
|
|
command[1:1] = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
|
"-fno-pie", "-no-pie"]
|
|
built = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
|
|
self.assertIn("duplicate keys, order and permutation", tested.stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|