mirror of
https://github.com/VectorCamp/vectorscan.git
synced 2025-06-28 16:41:01 +03:00
In some ring-based models, we know that if the ring is not stale, then all our bounds should fit within 32-bits. This change makes these explicitly u32 rather than implicitly narrowing later on.
978 lines
33 KiB
C++
978 lines
33 KiB
C++
/*
|
|
* Copyright (c) 2015, Intel Corporation
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright notice,
|
|
* this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * Neither the name of Intel Corporation nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "gtest/gtest.h"
|
|
|
|
#include "nfa/limex_ring.h"
|
|
#include "nfa/repeat.h"
|
|
#include "nfa/repeatcompile.h"
|
|
#include "util/depth.h"
|
|
#include "util/make_unique.h"
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
using namespace std;
|
|
using namespace testing;
|
|
using namespace ue2;
|
|
|
|
struct RepeatTestInfo {
|
|
enum RepeatType type;
|
|
depth repeatMin;
|
|
depth repeatMax;
|
|
};
|
|
|
|
static
|
|
ostream& operator<<(ostream &os, const RepeatInfo &info) {
|
|
os << "{" << info.repeatMin;
|
|
if (info.repeatMin == info.repeatMax) {
|
|
os << "}";
|
|
} else if (info.repeatMax == REPEAT_INF) {
|
|
os << ",}";
|
|
} else {
|
|
os << "," << info.repeatMax << "}";
|
|
}
|
|
os << " " << repeatTypeName(info.type);
|
|
os << " (period " << info.minPeriod << ")";
|
|
return os;
|
|
}
|
|
|
|
class RepeatTest : public TestWithParam<RepeatTestInfo> {
|
|
protected:
|
|
virtual void SetUp() {
|
|
test_info = GetParam();
|
|
|
|
info.type = test_info.type;
|
|
info.repeatMin = test_info.repeatMin;
|
|
info.repeatMax = test_info.repeatMax.is_finite()
|
|
? (u32)test_info.repeatMax
|
|
: REPEAT_INF;
|
|
info.minPeriod = 0;
|
|
|
|
RepeatStateInfo rsi(test_info.type, test_info.repeatMin,
|
|
test_info.repeatMax, 0);
|
|
info.packedCtrlSize = rsi.packedCtrlSize;
|
|
info.stateSize = rsi.stateSize;
|
|
info.horizon = rsi.horizon;
|
|
std::copy(rsi.packedFieldSizes.begin(), rsi.packedFieldSizes.end(),
|
|
info.packedFieldSizes);
|
|
|
|
ctrl = new RepeatControl();
|
|
state_int = new char[info.stateSize + 7]; /* state may have mmbits */
|
|
state = state_int + 7;
|
|
}
|
|
|
|
virtual void TearDown() {
|
|
delete ctrl;
|
|
delete [] state_int;
|
|
}
|
|
|
|
RepeatTestInfo test_info; // Test params
|
|
RepeatInfo info; // Repeat info structure
|
|
RepeatControl *ctrl;
|
|
char *state;
|
|
private:
|
|
char *state_int;
|
|
|
|
};
|
|
|
|
static const RepeatTestInfo repeatTests[] = {
|
|
// Fixed repeats -- ring model
|
|
{ REPEAT_RING, 2, 2 },
|
|
{ REPEAT_RING, 4, 4 },
|
|
{ REPEAT_RING, 10, 10 },
|
|
{ REPEAT_RING, 16, 16 },
|
|
{ REPEAT_RING, 20, 20 },
|
|
{ REPEAT_RING, 30, 30 },
|
|
{ REPEAT_RING, 50, 50 },
|
|
{ REPEAT_RING, 64, 64 },
|
|
{ REPEAT_RING, 65, 65 },
|
|
{ REPEAT_RING, 100, 100 },
|
|
{ REPEAT_RING, 200, 200 },
|
|
{ REPEAT_RING, 1000, 1000 },
|
|
{ REPEAT_RING, 4100, 4100 },
|
|
{ REPEAT_RING, 16000, 16000 },
|
|
// {0, N} repeats -- last model
|
|
{ REPEAT_LAST, 0, 4 },
|
|
{ REPEAT_LAST, 0, 10 },
|
|
{ REPEAT_LAST, 0, 20 },
|
|
{ REPEAT_LAST, 0, 30 },
|
|
{ REPEAT_LAST, 0, 50 },
|
|
{ REPEAT_LAST, 0, 100 },
|
|
{ REPEAT_LAST, 0, 200 },
|
|
{ REPEAT_LAST, 0, 1000 },
|
|
{ REPEAT_LAST, 0, 16000 },
|
|
// {0, N} repeats -- ring model (though we use 'last' model in practice)
|
|
{ REPEAT_RING, 0, 2 },
|
|
{ REPEAT_RING, 0, 4 },
|
|
{ REPEAT_RING, 0, 10 },
|
|
{ REPEAT_RING, 0, 20 },
|
|
{ REPEAT_RING, 0, 30 },
|
|
{ REPEAT_RING, 0, 50 },
|
|
{ REPEAT_RING, 0, 64 },
|
|
{ REPEAT_RING, 0, 65 },
|
|
{ REPEAT_RING, 0, 100 },
|
|
{ REPEAT_RING, 0, 200 },
|
|
{ REPEAT_RING, 0, 1000 },
|
|
{ REPEAT_RING, 0, 16000 },
|
|
// {N, M} repeats -- ring model
|
|
{ REPEAT_RING, 2, 3 },
|
|
{ REPEAT_RING, 1, 4 },
|
|
{ REPEAT_RING, 5, 10 },
|
|
{ REPEAT_RING, 10, 20 },
|
|
{ REPEAT_RING, 10, 50 },
|
|
{ REPEAT_RING, 50, 60 },
|
|
{ REPEAT_RING, 100, 200 },
|
|
{ REPEAT_RING, 1, 200 },
|
|
{ REPEAT_RING, 10, 16000 },
|
|
{ REPEAT_RING, 10000, 16000 },
|
|
// {N, M} repeats -- range model
|
|
{ REPEAT_RANGE, 1, 4 },
|
|
{ REPEAT_RANGE, 5, 10 },
|
|
{ REPEAT_RANGE, 10, 20 },
|
|
{ REPEAT_RANGE, 10, 50 },
|
|
{ REPEAT_RANGE, 50, 60 },
|
|
{ REPEAT_RANGE, 100, 200 },
|
|
{ REPEAT_RANGE, 1, 200 },
|
|
{ REPEAT_RANGE, 10, 16000 },
|
|
{ REPEAT_RANGE, 10000, 16000 },
|
|
// {N,M} repeats -- small bitmap model
|
|
{ REPEAT_BITMAP, 1, 2 },
|
|
{ REPEAT_BITMAP, 5, 10 },
|
|
{ REPEAT_BITMAP, 10, 20 },
|
|
{ REPEAT_BITMAP, 20, 40 },
|
|
{ REPEAT_BITMAP, 1, 63 },
|
|
{ REPEAT_BITMAP, 50, 63 },
|
|
// {N,M} repeats -- trailer model
|
|
{ REPEAT_TRAILER, 1, 2 },
|
|
{ REPEAT_TRAILER, 8, 8 },
|
|
{ REPEAT_TRAILER, 0, 8 },
|
|
{ REPEAT_TRAILER, 10, 20 },
|
|
{ REPEAT_TRAILER, 1, 32 },
|
|
{ REPEAT_TRAILER, 64, 64 },
|
|
{ REPEAT_TRAILER, 1, 64 },
|
|
{ REPEAT_TRAILER, 1, 100 },
|
|
{ REPEAT_TRAILER, 1, 2000 },
|
|
{ REPEAT_TRAILER, 50, 200 },
|
|
{ REPEAT_TRAILER, 50, 1000 },
|
|
{ REPEAT_TRAILER, 64, 1024 },
|
|
// {N,} repeats -- first model
|
|
{ REPEAT_FIRST, 0, depth::infinity() },
|
|
{ REPEAT_FIRST, 1, depth::infinity() },
|
|
{ REPEAT_FIRST, 4, depth::infinity() },
|
|
{ REPEAT_FIRST, 10, depth::infinity() },
|
|
{ REPEAT_FIRST, 50, depth::infinity() },
|
|
{ REPEAT_FIRST, 100, depth::infinity() },
|
|
{ REPEAT_FIRST, 1000, depth::infinity() },
|
|
{ REPEAT_FIRST, 3000, depth::infinity() },
|
|
{ REPEAT_FIRST, 10000, depth::infinity() }
|
|
};
|
|
|
|
INSTANTIATE_TEST_CASE_P(Repeat, RepeatTest, ValuesIn(repeatTests));
|
|
|
|
TEST_P(RepeatTest, MatchSuccess) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << info);
|
|
|
|
u64a offset = 1000;
|
|
repeatStore(&info, ctrl, state, offset, 0);
|
|
|
|
for (u32 i = info.repeatMin; i <= info.repeatMax; i++) {
|
|
enum TriggerResult rv = processTugTrigger(&info, ctrl, state, offset + i);
|
|
if (rv == TRIGGER_SUCCESS_CACHE) {
|
|
rv = TRIGGER_SUCCESS;
|
|
}
|
|
ASSERT_EQ(TRIGGER_SUCCESS, rv);
|
|
}
|
|
}
|
|
|
|
TEST_P(RepeatTest, MatchNegSuccess) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << info);
|
|
|
|
// Write a top at offset 1000.
|
|
const u64a offset = 1000;
|
|
repeatStore(&info, ctrl, state, offset, 0);
|
|
|
|
// Write another match at offset 1002, using is_alive=0 (i.e. the repeat
|
|
// was killed between these two tops).
|
|
repeatStore(&info, ctrl, state, offset + 2, 0);
|
|
|
|
enum TriggerResult rv;
|
|
|
|
// Match at offset + repeatMin should fail, while offset + repeatMin + 2
|
|
// should succeed.
|
|
if (info.repeatMin > 2) {
|
|
rv = processTugTrigger(&info, ctrl, state, offset + info.repeatMin);
|
|
ASSERT_EQ(TRIGGER_FAIL, rv);
|
|
}
|
|
rv = processTugTrigger(&info, ctrl, state, offset + info.repeatMin + 2);
|
|
if (rv == TRIGGER_SUCCESS_CACHE) {
|
|
rv = TRIGGER_SUCCESS;
|
|
}
|
|
ASSERT_EQ(TRIGGER_SUCCESS, rv);
|
|
}
|
|
|
|
TEST_P(RepeatTest, MatchFail) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << info);
|
|
|
|
// Write a match at offset 1000.
|
|
const u64a offset = 1000;
|
|
repeatStore(&info, ctrl, state, offset, 0);
|
|
|
|
// Test for a match, if possible, at offset + repeatMin - 1.
|
|
enum TriggerResult rv;
|
|
|
|
if (info.repeatMin > 0) {
|
|
u64a testOffset = offset + info.repeatMin - 1;
|
|
rv = processTugTrigger(&info, ctrl, state, testOffset);
|
|
ASSERT_EQ(TRIGGER_FAIL, rv);
|
|
}
|
|
|
|
// Match after repeatMax should fail as well.
|
|
if (info.repeatMax != REPEAT_INF) {
|
|
u64a testOffset = offset + info.repeatMax + 1;
|
|
rv = processTugTrigger(&info, ctrl, state, testOffset);
|
|
ASSERT_EQ(TRIGGER_STALE, rv);
|
|
}
|
|
}
|
|
|
|
// Fill the ring with matches.
|
|
TEST_P(RepeatTest, FillRing) {
|
|
if (info.repeatMax == REPEAT_INF) {
|
|
return;
|
|
}
|
|
|
|
const u64a offset = 1000;
|
|
repeatStore(&info, ctrl, state, offset, 0);
|
|
for (u64a i = 1; i <= info.repeatMax; i++) {
|
|
repeatStore(&info, ctrl, state, offset + i, 1);
|
|
}
|
|
|
|
// We should be able to see matches for all of these (beyond the last top offset).
|
|
enum TriggerResult rv;
|
|
for (u64a i = offset + info.repeatMax;
|
|
i <= offset + info.repeatMax + info.repeatMin; i++) {
|
|
rv = processTugTrigger(&info, ctrl, state, i);
|
|
if (rv == TRIGGER_SUCCESS_CACHE) {
|
|
rv = TRIGGER_SUCCESS;
|
|
}
|
|
ASSERT_EQ(TRIGGER_SUCCESS, rv);
|
|
}
|
|
}
|
|
|
|
TEST_P(RepeatTest, FindTops) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << info);
|
|
|
|
repeatStore(&info, ctrl, state, 1000, 0);
|
|
ASSERT_EQ(1000, repeatLastTop(&info, ctrl, state));
|
|
|
|
repeatStore(&info, ctrl, state, 2000, 1);
|
|
if (info.type == REPEAT_FIRST) {
|
|
ASSERT_EQ(1000, repeatLastTop(&info, ctrl, state));
|
|
} else {
|
|
ASSERT_EQ(2000, repeatLastTop(&info, ctrl, state));
|
|
}
|
|
|
|
repeatStore(&info, ctrl, state, 3000, 0);
|
|
ASSERT_EQ(3000, repeatLastTop(&info, ctrl, state));
|
|
}
|
|
|
|
TEST_P(RepeatTest, NextMatch) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << info);
|
|
|
|
u64a top = 10000000ULL;
|
|
repeatStore(&info, ctrl, state, top, 0);
|
|
|
|
u64a i = top + info.repeatMin;
|
|
|
|
if (info.repeatMin != 0) {
|
|
// First match after the top should be at top+min.
|
|
ASSERT_EQ(i, repeatNextMatch(&info, ctrl, state, top));
|
|
}
|
|
|
|
while (i < info.repeatMax) {
|
|
ASSERT_EQ(i + 1, repeatNextMatch(&info, ctrl, state, i));
|
|
i++;
|
|
}
|
|
|
|
if (info.repeatMax != REPEAT_INF) {
|
|
ASSERT_EQ(0, repeatNextMatch(&info, ctrl, state, top + info.repeatMax));
|
|
}
|
|
}
|
|
|
|
TEST_P(RepeatTest, NextMatchFilledRepeat) {
|
|
// This test is only really appropriate for repeat models that store more
|
|
// than one top.
|
|
if (info.type != REPEAT_RING && info.type != REPEAT_RANGE) {
|
|
return;
|
|
}
|
|
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << info);
|
|
|
|
u64a top = 10000000ULL;
|
|
repeatStore(&info, ctrl, state, top, 0);
|
|
for (u64a i = 1; i <= info.repeatMax; i++) {
|
|
repeatStore(&info, ctrl, state, top + i, 1);
|
|
}
|
|
|
|
u64a last_top = top + info.repeatMax;
|
|
u64a i = top + info.repeatMin;
|
|
|
|
if (info.repeatMin != 0) {
|
|
// First match after the initial top should be at top+min.
|
|
ASSERT_EQ(i, repeatNextMatch(&info, ctrl, state, top));
|
|
}
|
|
|
|
while (i < last_top + info.repeatMax) {
|
|
ASSERT_EQ(i + 1, repeatNextMatch(&info, ctrl, state, i));
|
|
i++;
|
|
}
|
|
|
|
if (info.repeatMax != REPEAT_INF) {
|
|
ASSERT_EQ(0, repeatNextMatch(&info, ctrl, state, last_top + info.repeatMax));
|
|
}
|
|
}
|
|
|
|
TEST_P(RepeatTest, TwoTops) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << info);
|
|
|
|
// Only appropriate for tests that store more than one top.
|
|
if (info.type == REPEAT_FIRST || info.type == REPEAT_LAST) {
|
|
return;
|
|
}
|
|
|
|
const u32 top_range = info.repeatMax;
|
|
|
|
// Limit the scope of this test for runtime brevity. For small cases, we
|
|
// check every offset, but for bigger ones we use a bigger step.
|
|
const u32 iter_step = std::max(1u, top_range / 256u);
|
|
|
|
const u64a top1 = 10000ull;
|
|
|
|
for (u32 i = 1; i < top_range; i += iter_step) {
|
|
const u64a top2 = top1 + i;
|
|
SCOPED_TRACE(testing::Message() << "Tops at offsets " << top1 << " and "
|
|
<< top2);
|
|
|
|
repeatStore(&info, ctrl, state, top1, 0);
|
|
ASSERT_EQ(top1, repeatLastTop(&info, ctrl, state));
|
|
repeatStore(&info, ctrl, state, top2, 1);
|
|
ASSERT_EQ(top2, repeatLastTop(&info, ctrl, state));
|
|
|
|
// We should have those matches from the top1 match window that are
|
|
// greater than or equal to top2.
|
|
for (u64a j = std::max(top1 + info.repeatMin, top2);
|
|
j <= top1 + info.repeatMax; j++) {
|
|
ASSERT_EQ(REPEAT_MATCH, repeatHasMatch(&info, ctrl, state, j))
|
|
<< j << " should be a match due to top 1";
|
|
}
|
|
|
|
// If the two match windows don't overlap, we should have some
|
|
// non-matching positions between them.
|
|
for (u64a j = top1 + info.repeatMax + 1; j < top2 + info.repeatMin;
|
|
j++) {
|
|
ASSERT_EQ(REPEAT_NOMATCH, repeatHasMatch(&info, ctrl, state, j))
|
|
<< j << " should not be a match";
|
|
}
|
|
|
|
// We should have all the matches in the match window from top 2.
|
|
for (u64a j = top2 + info.repeatMin; j <= top2 + info.repeatMax; j++) {
|
|
ASSERT_EQ(REPEAT_MATCH, repeatHasMatch(&info, ctrl, state, j))
|
|
<< j << " should be a match due to top 2";
|
|
}
|
|
|
|
// One past the end should be stale.
|
|
u64a past_end = top2 + info.repeatMax + 1;
|
|
ASSERT_EQ(REPEAT_STALE, repeatHasMatch(&info, ctrl, state, past_end))
|
|
<< "repeat should be stale at " << past_end;
|
|
}
|
|
}
|
|
|
|
TEST_P(RepeatTest, Pack) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << info);
|
|
u64a offset = 1000;
|
|
|
|
repeatStore(&info, ctrl, state, offset, 0);
|
|
|
|
// We should be able to pack and then unpack the control block at any
|
|
// offset up to repeatMin and get a match at both the min and max repeats.
|
|
|
|
unique_ptr<char[]> packed = ue2::make_unique<char[]>(info.packedCtrlSize);
|
|
|
|
for (u32 i = 0; i < info.repeatMax; i++) {
|
|
SCOPED_TRACE(testing::Message() << "i=" << i);
|
|
const u64a pack_offset = offset + i;
|
|
|
|
memset(packed.get(), 0xff, info.packedCtrlSize);
|
|
repeatPack(packed.get(), &info, ctrl, pack_offset);
|
|
memset(ctrl, 0xff, sizeof(*ctrl));
|
|
repeatUnpack(packed.get(), &info, pack_offset, ctrl);
|
|
|
|
// We should have a match at every offset in [offset + repeatMin,
|
|
// offset + repeatMax]. For brevity, we just check the first one and
|
|
// the last one.
|
|
|
|
u64a first = offset + std::max(i, info.repeatMin);
|
|
u64a last = offset + info.repeatMax;
|
|
ASSERT_EQ(REPEAT_MATCH, repeatHasMatch(&info, ctrl, state, first))
|
|
<< "repeat should have match at " << first;
|
|
ASSERT_EQ(REPEAT_MATCH, repeatHasMatch(&info, ctrl, state, last))
|
|
<< "repeat should have match at " << last;
|
|
}
|
|
}
|
|
|
|
static
|
|
const u32 sparsePeriods[] = {
|
|
2,
|
|
4,
|
|
6,
|
|
8,
|
|
10,
|
|
12,
|
|
15,
|
|
18,
|
|
20,
|
|
22,
|
|
24,
|
|
26,
|
|
28,
|
|
30,
|
|
/* 40,
|
|
50,
|
|
60,
|
|
80,
|
|
100,
|
|
120,
|
|
150,
|
|
180,
|
|
200,
|
|
250,
|
|
300,
|
|
350,
|
|
400,*/
|
|
};
|
|
|
|
static
|
|
const RepeatTestInfo sparseRepeats[] = {
|
|
// Fixed repeats
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 10, 10 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 20, 20 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 40, 40 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 80, 80 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 100, 100 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 150, 150 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 200, 200 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 250, 250 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 300, 300 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 350, 350 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 400, 400 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 500, 500 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 600, 600 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 800, 800 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 1000, 1000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 1500, 1500 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 2000, 2000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 2500, 2500 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 3000, 3000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 3500, 3500 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 4000, 4000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 4500, 4500 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 5000, 5000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 65534, 65534 },
|
|
// {N, M} repeats
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 10, 20 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 20, 40 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 40, 80 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 80, 100 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 100, 120 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 150, 180 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 200, 400 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 250, 500 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 300, 400 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 350, 500 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 400, 500 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 500, 600 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 600, 700 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 800, 1000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 1000, 1200 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 1500, 1800 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 2000, 4000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 2500, 3000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 3000, 3500 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 3500, 4000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 4000, 8000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 4500, 8000 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 5000, 5001 },
|
|
{ REPEAT_SPARSE_OPTIMAL_P, 60000, 65534 }
|
|
};
|
|
|
|
static
|
|
void test_sparse2entry(const RepeatInfo *info, RepeatControl *ctrl,
|
|
char *state, u64a second) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << *info);
|
|
SCOPED_TRACE(second);
|
|
|
|
if (second > info->repeatMax || second < info->minPeriod) {
|
|
return;
|
|
}
|
|
|
|
u64a offset = 1000;
|
|
u64a exit = offset + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset, 0);
|
|
ASSERT_EQ(offset, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset2 = 1000 + second;
|
|
u64a exit2 = offset2 + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset2, 1);
|
|
ASSERT_EQ(offset2, repeatLastTop(info, ctrl, state));
|
|
|
|
u32 range = info->repeatMax - info->repeatMin;
|
|
for (u32 i = offset2; i < offset + info->repeatMax * 2 + 100; i++) {
|
|
SCOPED_TRACE(i);
|
|
RepeatMatch r = repeatHasMatch(info, ctrl, state, i);
|
|
if ((i >= exit && i <= exit + range) ||
|
|
(i >= exit2 && i <= exit2 + range)) {
|
|
ASSERT_EQ(REPEAT_MATCH, r);
|
|
} else if (i > exit2 + range) {
|
|
ASSERT_EQ(REPEAT_STALE, r);
|
|
} else {
|
|
ASSERT_EQ(REPEAT_NOMATCH, r);
|
|
}
|
|
}
|
|
|
|
ASSERT_EQ(MAX(exit + range + 1, exit2),
|
|
repeatNextMatch(info, ctrl, state, exit + range));
|
|
ASSERT_EQ(MAX(exit + range + 2, exit2),
|
|
repeatNextMatch(info, ctrl, state, exit + range + 1));
|
|
ASSERT_EQ(exit2, repeatNextMatch(info, ctrl, state, exit2 - 1));
|
|
ASSERT_EQ(0, repeatNextMatch(info, ctrl, state, exit2 + range));
|
|
}
|
|
|
|
static
|
|
void test_sparse3entry(const RepeatInfo *info, RepeatControl *ctrl,
|
|
char *state, u64a diff) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat:" << *info);
|
|
SCOPED_TRACE(diff);
|
|
|
|
if (diff * 2 > info->repeatMax || diff < info->minPeriod) {
|
|
return;
|
|
}
|
|
|
|
u64a offset = 1000;
|
|
u64a exit = offset + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset, 0);
|
|
ASSERT_EQ(offset, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset2 = 1000 + diff;
|
|
u64a exit2 = offset2 + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset2, 1);
|
|
ASSERT_EQ(offset2, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset3 = 1000 + 2 * diff;
|
|
u64a exit3 = offset3 + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset3, 1);
|
|
ASSERT_EQ(offset3, repeatLastTop(info, ctrl, state));
|
|
|
|
u32 range = info->repeatMax - info->repeatMin;
|
|
for (u32 i = offset2; i < offset + info->repeatMax * 2 + 100; i++) {
|
|
SCOPED_TRACE(i);
|
|
RepeatMatch r = repeatHasMatch(info, ctrl, state, i);
|
|
if((i >= exit && i <= exit + range)||
|
|
(i >= exit2 && i <= exit2 + range) ||
|
|
(i >= exit3 && i <= exit3 + range)) {
|
|
ASSERT_EQ(REPEAT_MATCH, r);
|
|
} else if (i > exit3 + range) {
|
|
ASSERT_EQ(REPEAT_STALE, r);
|
|
} else {
|
|
ASSERT_EQ(REPEAT_NOMATCH, r);
|
|
}
|
|
}
|
|
|
|
ASSERT_EQ(MAX(exit + range + 1, exit2),
|
|
repeatNextMatch(info, ctrl, state, exit + range));
|
|
ASSERT_EQ(MAX(exit + range + 2, exit2),
|
|
repeatNextMatch(info, ctrl, state, exit + range + 1));
|
|
ASSERT_EQ(exit2, repeatNextMatch(info, ctrl, state, exit2 - 1));
|
|
ASSERT_EQ(MAX(exit2 + range + 1, exit3),
|
|
repeatNextMatch(info, ctrl, state, exit2 + range));
|
|
ASSERT_EQ(MAX(exit2 + range + 2, exit3),
|
|
repeatNextMatch(info, ctrl, state, exit2 + range + 1));
|
|
ASSERT_EQ(exit3, repeatNextMatch(info, ctrl, state, exit3 - 1));
|
|
ASSERT_EQ(0, repeatNextMatch(info, ctrl, state, exit3 + range));
|
|
}
|
|
|
|
static
|
|
void test_sparse3entryNeg(const RepeatInfo *info, RepeatControl *ctrl,
|
|
char *state, u64a diff) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat:" << *info);
|
|
SCOPED_TRACE(diff);
|
|
|
|
if (diff * 2 > info->repeatMax || diff < info->minPeriod) {
|
|
return;
|
|
}
|
|
|
|
u64a offset = 1000;
|
|
repeatStore(info, ctrl, state, offset, 0);
|
|
ASSERT_EQ(offset, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset2 = 1000 + diff;
|
|
repeatStore(info, ctrl, state, offset2, 0);
|
|
ASSERT_EQ(offset2, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset3 = 1000 + 2 * diff;
|
|
u64a exit3 = offset3 + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset3, 0);
|
|
ASSERT_EQ(offset3, repeatLastTop(info, ctrl, state));
|
|
|
|
u32 range = info->repeatMax - info->repeatMin;
|
|
for (u32 i = offset3; i < offset + info->repeatMax * 2 + 100; i++) {
|
|
SCOPED_TRACE(i);
|
|
RepeatMatch r = repeatHasMatch(info, ctrl, state, i);
|
|
if(i >= exit3 && i <= exit3 + range) {
|
|
ASSERT_EQ(REPEAT_MATCH, r);
|
|
} else if (i > exit3 + range) {
|
|
ASSERT_EQ(REPEAT_STALE, r);
|
|
} else {
|
|
ASSERT_EQ(REPEAT_NOMATCH, r);
|
|
}
|
|
}
|
|
}
|
|
|
|
static
|
|
void test_sparse3entryExpire(const RepeatInfo *info, RepeatControl *ctrl,
|
|
char *state, u64a diff) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat:" << *info);
|
|
SCOPED_TRACE(diff);
|
|
|
|
if (diff * 2 > info->repeatMax || diff < info->minPeriod) {
|
|
return;
|
|
}
|
|
|
|
u64a offset = 1000;
|
|
repeatStore(info, ctrl, state, offset, 0);
|
|
ASSERT_EQ(offset, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset2 = 1000 + diff;
|
|
repeatStore(info, ctrl, state, offset2, 1);
|
|
ASSERT_EQ(offset2, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset3 = 1000 + 2 * info->repeatMax;
|
|
u64a exit3 = offset3 + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset3, 1);
|
|
ASSERT_EQ(offset3, repeatLastTop(info, ctrl, state));
|
|
|
|
u32 range = info->repeatMax - info->repeatMin;
|
|
for (u32 i = offset3; i < offset3 + info->repeatMax + 100; i++) {
|
|
SCOPED_TRACE(i);
|
|
RepeatMatch r = repeatHasMatch(info, ctrl, state, i);
|
|
if(i >= exit3 && i <= exit3 + range) {
|
|
ASSERT_EQ(REPEAT_MATCH, r);
|
|
} else if (i > exit3 + range) {
|
|
ASSERT_EQ(REPEAT_STALE, r);
|
|
} else {
|
|
ASSERT_EQ(REPEAT_NOMATCH, r);
|
|
}
|
|
}
|
|
}
|
|
|
|
class SparseOptimalTest : public TestWithParam<tuple<u32, RepeatTestInfo> > {
|
|
protected:
|
|
virtual void SetUp() {
|
|
u32 period;
|
|
tie(period, test_info) = GetParam();
|
|
|
|
RepeatStateInfo rsi(REPEAT_SPARSE_OPTIMAL_P, test_info.repeatMin,
|
|
test_info.repeatMax, period);
|
|
|
|
ptr = new char[sizeof(RepeatInfo) +
|
|
sizeof(u64a) * (rsi.patchSize + 2)];
|
|
|
|
info = (struct RepeatInfo *)ptr;
|
|
|
|
info->type = REPEAT_SPARSE_OPTIMAL_P;
|
|
info->repeatMin = test_info.repeatMin;
|
|
info->repeatMax = test_info.repeatMax;
|
|
info->minPeriod = period;
|
|
|
|
info->packedCtrlSize = rsi.packedCtrlSize;
|
|
info->stateSize = rsi.stateSize;
|
|
info->horizon = rsi.horizon;
|
|
std::copy(rsi.packedFieldSizes.begin(), rsi.packedFieldSizes.end(),
|
|
info->packedFieldSizes);
|
|
info->patchCount = rsi.patchCount;
|
|
info->patchSize = rsi.patchSize;
|
|
info->encodingSize = rsi.encodingSize;
|
|
info->patchesOffset = rsi.patchesOffset;
|
|
|
|
u32 repeatMax = info->patchSize;
|
|
u64a *table = (u64a *)(ROUNDUP_PTR((ptr + sizeof(RepeatInfo)),
|
|
alignof(u64a)));
|
|
for (u32 i = 0; i < repeatMax + 1; i++) {
|
|
table[i] = rsi.table[i];
|
|
}
|
|
|
|
ctrl = new RepeatControl();
|
|
state_int = new char[info->stateSize + 7]; /* state may have mmbits */
|
|
state = state_int + 7;
|
|
}
|
|
|
|
virtual void TearDown() {
|
|
delete ctrl;
|
|
delete[] state_int;
|
|
delete[] ptr;
|
|
}
|
|
|
|
RepeatTestInfo test_info; // Test params
|
|
RepeatInfo *info; // Repeat info structure
|
|
RepeatControl *ctrl;
|
|
char *state;
|
|
private:
|
|
char *ptr;
|
|
char *state_int;
|
|
|
|
};
|
|
|
|
TEST_P(SparseOptimalTest, Simple1) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << *info);
|
|
|
|
u64a offset = 1000;
|
|
repeatStore(info, ctrl, state, offset, 0);
|
|
|
|
ASSERT_EQ(1000U, repeatLastTop(info, ctrl, state));
|
|
|
|
for (u32 i = 0; i < info->repeatMax * 2 + 100; i++) {
|
|
SCOPED_TRACE(i);
|
|
RepeatMatch r = repeatHasMatch(info, ctrl, state, offset + i);
|
|
if (i >= info->repeatMin && i <= info->repeatMax) {
|
|
ASSERT_EQ(REPEAT_MATCH, r);
|
|
} else if (i > info->repeatMax) {
|
|
ASSERT_EQ(REPEAT_STALE, r);
|
|
} else {
|
|
ASSERT_EQ(REPEAT_NOMATCH, r);
|
|
}
|
|
}
|
|
|
|
u64a exp = 1000 + info->repeatMin;
|
|
ASSERT_EQ(exp, repeatNextMatch(info, ctrl, state, 1000));
|
|
ASSERT_EQ(exp, repeatNextMatch(info, ctrl, state, 1001));
|
|
ASSERT_EQ(exp, repeatNextMatch(info, ctrl, state, 1002));
|
|
ASSERT_EQ(exp, repeatNextMatch(info, ctrl, state, 1003));
|
|
ASSERT_EQ(exp, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMin - 5));
|
|
ASSERT_EQ(exp, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMin - 4));
|
|
ASSERT_EQ(exp, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMin - 3));
|
|
ASSERT_EQ(exp, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMin - 2));
|
|
ASSERT_EQ(exp, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMin - 1));
|
|
ASSERT_EQ(0U, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMax));
|
|
ASSERT_EQ(0U, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMax + 1));
|
|
ASSERT_EQ(0U, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMax * 2 - 1));
|
|
ASSERT_EQ(0U, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMax * 2));
|
|
ASSERT_EQ(0U, repeatNextMatch(info, ctrl, state,
|
|
1000 + info->repeatMax * 2 + 1));
|
|
ASSERT_EQ(0U, repeatNextMatch(info, ctrl, state, 100000));
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, TwoTopsNeg) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << *info);
|
|
|
|
u32 patch_count = info->patchCount;
|
|
u32 patch_size = info->patchSize;
|
|
u64a offset = 1000;
|
|
u64a exit = offset + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset, 0);
|
|
ASSERT_EQ(offset, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset2 = 1000 + patch_count * patch_size;
|
|
u64a exit2 = offset2 + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset2, 1);
|
|
ASSERT_EQ(offset2, repeatLastTop(info, ctrl, state));
|
|
|
|
u32 range = info->repeatMax - info->repeatMin;
|
|
for (u32 i = offset2; i < offset + info->repeatMax * 2 + 100; i++) {
|
|
SCOPED_TRACE(i);
|
|
RepeatMatch r = repeatHasMatch(info, ctrl, state, i);
|
|
if (i >= exit2 && i <= exit2 + range) {
|
|
ASSERT_EQ(REPEAT_MATCH, r);
|
|
} else if (i > exit2 + range) {
|
|
ASSERT_EQ(REPEAT_STALE, r);
|
|
} else {
|
|
ASSERT_EQ(REPEAT_NOMATCH, r);
|
|
}
|
|
}
|
|
|
|
const struct RepeatRingControl *xs = (const struct RepeatRingControl *)
|
|
ctrl;
|
|
ASSERT_EQ(exit2, repeatNextMatch(info, ctrl, state,
|
|
MAX(xs->offset, exit)));
|
|
ASSERT_EQ(exit2, repeatNextMatch(info, ctrl, state,
|
|
MAX(xs->offset, exit + 1)));
|
|
ASSERT_EQ(exit2, repeatNextMatch(info, ctrl, state, exit2 - 1));
|
|
ASSERT_EQ(0, repeatNextMatch(info, ctrl, state, exit2 + range));
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple2a) {
|
|
test_sparse2entry(info, ctrl, state, info->minPeriod);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple2b) {
|
|
test_sparse2entry(info, ctrl, state, info->repeatMax - 1);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple2c) {
|
|
test_sparse2entry(info, ctrl, state, info->repeatMax);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple2d) {
|
|
test_sparse2entry(info, ctrl, state, info->minPeriod + 1);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple2e) {
|
|
test_sparse2entry(info, ctrl, state, 2 * info->minPeriod - 1);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple3a) {
|
|
test_sparse3entry(info, ctrl, state, info->minPeriod);
|
|
test_sparse3entryNeg(info, ctrl, state, info->minPeriod);
|
|
test_sparse3entryExpire(info, ctrl, state, info->minPeriod);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple3b) {
|
|
test_sparse3entry(info, ctrl, state, info->repeatMax / 2 - 1);
|
|
test_sparse3entryNeg(info, ctrl, state, info->repeatMax / 2 - 1);
|
|
test_sparse3entryExpire(info, ctrl, state, info->repeatMax / 2 - 1);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple3c) {
|
|
test_sparse3entry(info, ctrl, state, info->repeatMax / 2);
|
|
test_sparse3entryNeg(info, ctrl, state, info->repeatMax / 2);
|
|
test_sparse3entryExpire(info, ctrl, state, info->repeatMax / 2);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple3d) {
|
|
test_sparse3entry(info, ctrl, state, info->minPeriod + 1);
|
|
test_sparse3entryNeg(info, ctrl, state, info->minPeriod + 1);
|
|
test_sparse3entryExpire(info, ctrl, state, info->minPeriod + 1);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, Simple3e) {
|
|
test_sparse3entry(info, ctrl, state, 2 * info->minPeriod - 1);
|
|
test_sparse3entryNeg(info, ctrl, state, 2 * info->minPeriod - 1);
|
|
test_sparse3entryExpire(info, ctrl, state, 2 * info->minPeriod - 1);
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, ThreeTops) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << *info);
|
|
|
|
u32 patch_count = info->patchCount;
|
|
u32 patch_size = info->patchSize;
|
|
if (patch_count < 3) {
|
|
return;
|
|
}
|
|
|
|
u64a offset = 1000;
|
|
repeatStore(info, ctrl, state, offset, 0);
|
|
ASSERT_EQ(offset, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset2 = 1000 + 2 * patch_size - 1;
|
|
u64a exit2 = offset2 + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset2, 1);
|
|
ASSERT_EQ(offset2, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset3 = 1000 + patch_count * patch_size;
|
|
u64a exit3 = offset3 + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset3, 1);
|
|
ASSERT_EQ(offset3, repeatLastTop(info, ctrl, state));
|
|
|
|
|
|
for (u32 i = offset2 + info->repeatMin;
|
|
i <= offset2 + info->repeatMax; i++) {
|
|
ASSERT_EQ(REPEAT_MATCH, repeatHasMatch(info, ctrl, state, i));
|
|
}
|
|
|
|
for (u32 i = offset2 + info->repeatMax + 1;
|
|
i < offset3 + info->repeatMin; i++) {
|
|
ASSERT_EQ(REPEAT_NOMATCH, repeatHasMatch(info, ctrl, state, i));
|
|
}
|
|
|
|
for (u32 i = offset3 + info->repeatMin;
|
|
i <= offset3 + info->repeatMax; i++) {
|
|
ASSERT_EQ(REPEAT_MATCH, repeatHasMatch(info, ctrl, state, i));
|
|
}
|
|
|
|
u32 range = info->repeatMax - info->repeatMin;
|
|
ASSERT_EQ(MAX(exit2 + range + 1, exit3),
|
|
repeatNextMatch(info, ctrl, state, exit2 + range));
|
|
ASSERT_EQ(MAX(exit2 + range + 2, exit3),
|
|
repeatNextMatch(info, ctrl, state, exit2 + range + 1));
|
|
ASSERT_EQ(exit3, repeatNextMatch(info, ctrl, state, exit3 - 1));
|
|
ASSERT_EQ(0, repeatNextMatch(info, ctrl, state, exit3 + range));
|
|
}
|
|
|
|
TEST_P(SparseOptimalTest, FillTops) {
|
|
SCOPED_TRACE(testing::Message() << "Repeat: " << *info);
|
|
|
|
u32 patch_count = info->patchCount;
|
|
u32 patch_size = info->patchSize;
|
|
u32 min_period = info->minPeriod;
|
|
|
|
u64a offset = 1000;
|
|
u64a exit = offset + info->repeatMin;
|
|
repeatStore(info, ctrl, state, offset, 0);
|
|
ASSERT_EQ(offset, repeatLastTop(info, ctrl, state));
|
|
|
|
u64a offset2;
|
|
for (u32 i = min_period; i < patch_count * patch_size; i += min_period) {
|
|
offset2 = offset + i;
|
|
repeatStore(info, ctrl, state, offset2, 1);
|
|
ASSERT_EQ(offset2, repeatLastTop(info, ctrl, state));
|
|
}
|
|
|
|
u64a exit2;
|
|
for (u32 i = 0; i < patch_count * patch_size; i += min_period) {
|
|
exit2 = exit + i;
|
|
for (u32 j = exit2 + info->repeatMin;
|
|
j <= offset + info->repeatMax; j++) {
|
|
ASSERT_EQ(REPEAT_MATCH, repeatHasMatch(info, ctrl, state, j));
|
|
}
|
|
}
|
|
}
|
|
|
|
INSTANTIATE_TEST_CASE_P(SparseOptimal, SparseOptimalTest,
|
|
Combine(ValuesIn(sparsePeriods),
|
|
ValuesIn(sparseRepeats)));
|