Преглед изворни кода

LibTest: Add the RandomRun abstraction

To be used later by generators and during shrinking.

The generators used in randomized tests will record the drawn random
bits into a RandomRun. This is a layer of indirection that will help us
automatically shrink any generated value without any user input. (Having
to provide manually defined shrinkers is a downside to many randomized /
property-based testing libraries like QuickCheck for Haskell.)
Martin Janiczek пре 1 година
родитељ
комит
d4e4189a34
1 измењених фајлова са 117 додато и 0 уклоњено
  1. 117 0
      Userland/Libraries/LibTest/Randomized/RandomRun.h

+ 117 - 0
Userland/Libraries/LibTest/Randomized/RandomRun.h

@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2023, Martin Janiczek <martin@janiczek.cz>.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/QuickSort.h>
+#include <AK/Vector.h>
+#include <LibTest/Randomized/Chunk.h>
+
+namespace Test {
+namespace Randomized {
+
+// RandomRun is a record of random bits used in generation of random values.
+// Once a value failing user test is found, we then attempt to shrink its
+// RandomRun using various ShrinkCommands.
+//
+// This means that we construct new RandomRuns by saying "OK, but what if the
+// PRNG gave you 0 instead of 23 that time..." The runner then tries to
+// generate a new value from the new RandomRun; if it succeeds and the value
+// still fails the test, we've shrunk our counterexample some!
+//
+// RandomRun is conceptually a sequence of unsigned integers, e.g.
+// [5,3,10,8,0,0,1].
+class RandomRun {
+public:
+    RandomRun() = default;
+    RandomRun(RandomRun const& rhs) = default;
+    RandomRun& operator=(RandomRun const& rhs) = default;
+    explicit RandomRun(Vector<u32> const& data)
+        : m_data(move(data))
+    {
+    }
+    bool is_empty() const { return m_data.is_empty(); }
+    bool contains_chunk(Chunk const& c) const { return c.index + c.size <= m_data.size(); }
+    void append(u32 n) { m_data.append(n); }
+    size_t size() const { return m_data.size(); }
+    Optional<u32> next()
+    {
+        if (m_current_index < m_data.size()) {
+            return m_data[m_current_index++];
+        }
+        return Optional<u32> {};
+    }
+    u32& operator[](size_t index) { return m_data[index]; }
+    u32 const& operator[](size_t index) const { return m_data[index]; }
+    Vector<u32> data() const { return m_data; }
+
+    // Shortlex sorting
+    //
+    // This is the metric by which we try to minimize (shrink) the sequence of
+    // random choices, from which we later generate values.
+    //
+    // Shorter is better; if the length is equal then lexicographic order is
+    // used. See [paper], section 2.2.
+    //
+    // Examples:
+    // [9,9,9] < [0,0,0,0] (shorter is better)
+    // [8,9,9] < [9,0,0] (lexicographic ordering: numbers that appear earlier
+    //                    are more "important" than numbers that follow them)
+    //
+    // [paper]: https://drops.dagstuhl.de/opus/volltexte/2020/13170/
+    bool is_shortlex_smaller_than(RandomRun const& rhs) const
+    {
+        auto lhs_size = size();
+        auto rhs_size = rhs.size();
+
+        if (lhs_size < rhs_size)
+            return true;
+
+        if (lhs_size > rhs_size)
+            return false;
+
+        for (size_t i = 0; i < lhs_size; i++) {
+            if (m_data[i] < rhs.m_data[i])
+                return true;
+
+            if (m_data[i] > rhs.m_data[i])
+                return false;
+        }
+        return false;
+    }
+
+    RandomRun with_sorted(Chunk c) const
+    {
+        Vector<u32> new_data = m_data;
+        AK::dual_pivot_quick_sort(
+            new_data,
+            c.index,
+            c.index + c.size - 1,
+            [](auto& a, auto& b) { return a < b; });
+        return RandomRun(move(new_data));
+    }
+    RandomRun with_deleted(Chunk c) const
+    {
+        Vector<u32> new_data(m_data);
+        new_data.remove(c.index, c.size);
+        return RandomRun(move(new_data));
+    }
+
+private:
+    Vector<u32> m_data;
+    size_t m_current_index = 0;
+};
+
+} // namespace Randomized
+} // namespace Test
+
+template<>
+struct AK::Formatter<Test::Randomized::RandomRun> : Formatter<StringView> {
+    ErrorOr<void> format(FormatBuilder& builder, Test::Randomized::RandomRun run)
+    {
+        return Formatter<StringView>::format(builder, TRY(String::formatted("[{}]"sv, TRY(String::join(',', run.data(), "{}"sv)))));
+    }
+};